516 mechanical replacements across 25 widget files: - spacing xs/sm/md/lg/xl to numeric 1/2/3/4/6 - text-heading-3 to text-lg font-semibold - bg-danger to bg-destructive - border border dedup Fix .eslintrc.tokens.js to use typescript-eslint parser (was importing uninstalled @typescript-eslint/parser). lint:tokens now passes.
90 lines
2.6 KiB
TypeScript
90 lines
2.6 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* notifications-widget(universal / main)
|
||
*
|
||
* 通过 useNotifications 查询 apollo-router → msg 子图的 notifications 数据。
|
||
* 与 topbar 的 notification-bell 区别:本插件位于 main 区,展示完整列表
|
||
* (含标题、正文、时间、类型徽章),bell 仅在顶栏做徽标提示。
|
||
*
|
||
* 关联:portal-shell spec §5.6 统一 Hook、M8 验收
|
||
*/
|
||
import { useNotifications } from "@/lib/api/universal";
|
||
import { PluginSkeleton } from "@/shell/PluginLoader";
|
||
import type { PluginProps } from "@/lib/types";
|
||
|
||
const TYPE_LABEL: Record<string, string> = {
|
||
info: "通知",
|
||
warning: "提醒",
|
||
urgent: "紧急",
|
||
};
|
||
|
||
function TypeBadge({ type }: { type: string }): React.ReactElement {
|
||
const label = TYPE_LABEL[type] ?? type;
|
||
if (type === "urgent") {
|
||
return (
|
||
<span className="rounded-md bg-destructive px-2 py-1 text-xs text-primary-foreground">
|
||
{label}
|
||
</span>
|
||
);
|
||
}
|
||
if (type === "warning") {
|
||
return (
|
||
<span className="rounded-md bg-primary px-2 py-1 text-xs text-primary-foreground">
|
||
{label}
|
||
</span>
|
||
);
|
||
}
|
||
return (
|
||
<span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
|
||
{label}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
export default function NotificationsWidget(
|
||
props: PluginProps,
|
||
): React.ReactElement {
|
||
const rawLimit = props.props.limit;
|
||
const limit = typeof rawLimit === "number" ? rawLimit : 20;
|
||
|
||
const { data, loading } = useNotifications({ limit, offset: 0 });
|
||
|
||
if (loading && !data) {
|
||
return <PluginSkeleton variant="list" />;
|
||
}
|
||
|
||
const items = data?.items ?? [];
|
||
const total = data?.total ?? 0;
|
||
|
||
return (
|
||
<section className="rounded-xl border bg-card p-4">
|
||
<div className="flex">
|
||
<h3 className="flex-1 text-lg font-semibold text-foreground">通知</h3>
|
||
<span className="text-sm text-muted-foreground">共 {total} 条</span>
|
||
</div>
|
||
{items.length === 0 ? (
|
||
<p className="mt-2 text-sm text-muted-foreground">暂无通知</p>
|
||
) : (
|
||
<ul className="mt-2 space-y-2">
|
||
{items.map((item) => (
|
||
<li
|
||
key={item.id}
|
||
className="border-b border py-1 text-sm text-foreground"
|
||
>
|
||
<div className="flex items-center gap-4">
|
||
<span className="flex-1">{item.title}</span>
|
||
<TypeBadge type={item.type} />
|
||
</div>
|
||
<p className="mt-1 text-xs text-muted-foreground">{item.body}</p>
|
||
<p className="mt-1 text-xs text-muted-foreground">
|
||
{item.createdAt}
|
||
</p>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|