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:
SpecialX
2026-07-17 13:07:24 +08:00
parent f623dcf4a7
commit 2910a90271
73 changed files with 8206 additions and 87 deletions

View File

@@ -0,0 +1,87 @@
"use client";
/**
* notifications-widgetuniversal / 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>
);
}

View File

@@ -0,0 +1,28 @@
/**
* notifications-widget 插件清单universal
*
* main 区通知完整列表,区别于 topbar 的 notification-bell仅徽标提示
* 通过 useWidgetQuery 查询 apollo-router → msg 子图 notifications 数据。
*/
import type { PluginManifest } from "@/lib/types";
export const manifestMeta: Omit<PluginManifest, "Component"> = {
pluginId: "notifications-widget",
version: "0.1.0",
requiredShellVersion: "^1.0.0",
metadata: {
displayName: "通知列表",
description: "main 区通知完整列表(含正文、时间、类型徽章)",
category: "universal",
requiredRoles: ["teacher", "student", "parent"],
defaultSlot: "main",
defaultSize: { colSpan: 2, rowSpan: 1 },
defaultProps: { limit: 20 },
propsSchema: {
type: "object",
properties: {
limit: { type: "number", description: "展示条数", default: 20 },
},
},
},
};