feat(portal-shell): extract domain API layer and migrate 31 widgets
Task 4-10 of portal-shell data abstraction plan (M1-M2). Add 7 domain API modules under src/lib/api/ (parent/admin/teacher/ student/universal/sidebar/topbar), each exposing semantic hooks that wrap useWidgetQuery/useWidgetMutation and return flattened domain models. Widget code now imports from @/lib/api instead of inlining gql literals. - 31 widgets migrated (gql literal count in widgets: 0) - 7 test files (85 cases, all passing) - topbar.useNotifications renamed to useNotificationBell to avoid barrel export collision with universal.useNotifications - typecheck + lint (0 errors) + test (85/85) verified
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"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-button bg-danger px-sm py-xs text-tiny text-ink-onAccent">
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (type === "warning") {
|
||||
return (
|
||||
<span className="rounded-button bg-accent px-sm py-xs text-tiny text-ink-onAccent">
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="rounded-button bg-subtle px-sm py-xs text-tiny text-ink-muted">
|
||||
{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-card border border-rule bg-surface p-md">
|
||||
<div className="flex">
|
||||
<h3 className="flex-1 text-heading-3 text-ink">通知</h3>
|
||||
<span className="text-small text-ink-muted">共 {total} 条</span>
|
||||
</div>
|
||||
{items.length === 0 ? (
|
||||
<p className="mt-sm text-small text-ink-muted">暂无通知</p>
|
||||
) : (
|
||||
<ul className="mt-sm space-y-sm">
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className="border-b border-rule py-xs text-small text-ink"
|
||||
>
|
||||
<div className="flex items-center gap-md">
|
||||
<span className="flex-1">{item.title}</span>
|
||||
<TypeBadge type={item.type} />
|
||||
</div>
|
||||
<p className="mt-xs text-tiny text-ink-muted">{item.body}</p>
|
||||
<p className="mt-xs text-tiny text-ink-muted">{item.createdAt}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user