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
136 lines
2.8 KiB
TypeScript
136 lines
2.8 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* Topbar domain API
|
||
*
|
||
* 涵盖:useNotificationBell、useCurrentUser、useGlobalSearch
|
||
*
|
||
* widget 通过 `import { useNotificationBell } from "@/lib/api/topbar"` 调用,
|
||
* 不再各自内嵌 gql/接口/手写类型。
|
||
*
|
||
* 注意:useNotificationBell 与 universal.useNotifications(分页通知列表)语义不同,
|
||
* 前者是 topbar 铃铛 widget 专用(按 limit 取最近 N 条),后者是分页查询。
|
||
*
|
||
* 关联:spec §2.2
|
||
*/
|
||
import {
|
||
GET_NOTIFICATIONS_DOC,
|
||
GET_CURRENT_USER_DOC,
|
||
SEARCH_DOC,
|
||
} from "@/lib/api/operations/topbar.graphql";
|
||
import { useWidgetQuery } from "@/lib/useWidgetQuery";
|
||
import type { UseQueryResult } from "./types";
|
||
|
||
// ===== 领域模型类型 =====
|
||
|
||
export interface NotificationItem {
|
||
id: string;
|
||
title: string;
|
||
}
|
||
|
||
export interface CurrentUser {
|
||
id: string;
|
||
name: string;
|
||
email: string;
|
||
role: string;
|
||
}
|
||
|
||
export interface SearchResult {
|
||
id: string;
|
||
type: string;
|
||
title: string;
|
||
subtitle: string;
|
||
}
|
||
|
||
// ===== 内部 Query 类型 =====
|
||
|
||
interface NotificationsQueryData {
|
||
notifications: NotificationItem[];
|
||
}
|
||
|
||
type NotificationsQueryVars = {
|
||
limit: number;
|
||
};
|
||
|
||
interface CurrentUserQueryData {
|
||
me: CurrentUser | null;
|
||
}
|
||
|
||
type CurrentUserQueryVars = Record<string, never>;
|
||
|
||
interface SearchQueryData {
|
||
search: SearchResult[];
|
||
}
|
||
|
||
type SearchQueryVars = {
|
||
keyword: string;
|
||
limit: number;
|
||
};
|
||
|
||
// ===== Hooks =====
|
||
|
||
/**
|
||
* 查询通知列表(topbar 铃铛 widget 专用,按 limit 取最近 N 条)。
|
||
*
|
||
* 关联:portal-shell spec §5.6 统一 Hook
|
||
*/
|
||
export function useNotificationBell(
|
||
limit: number,
|
||
): UseQueryResult<NotificationItem[]> {
|
||
const result = useWidgetQuery<NotificationsQueryData, NotificationsQueryVars>(
|
||
GET_NOTIFICATIONS_DOC,
|
||
{ limit },
|
||
);
|
||
|
||
return {
|
||
data: result.data?.notifications,
|
||
loading: result.loading,
|
||
error: result.error,
|
||
refetch: result.refetch,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 查询当前登录用户(iam 子图 me 字段)。
|
||
*
|
||
* 关联:portal-shell spec §5.6 统一 Hook
|
||
*/
|
||
export function useCurrentUser(): UseQueryResult<CurrentUser | null> {
|
||
const result = useWidgetQuery<CurrentUserQueryData, CurrentUserQueryVars>(
|
||
GET_CURRENT_USER_DOC,
|
||
{},
|
||
);
|
||
|
||
return {
|
||
data: result.data?.me,
|
||
loading: result.loading,
|
||
error: result.error,
|
||
refetch: result.refetch,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 全局搜索。
|
||
*
|
||
* `keyword` 为空时不发起查询(enabled: false),避免无效请求。
|
||
*
|
||
* 关联:portal-shell spec §5.6 统一 Hook
|
||
*/
|
||
export function useGlobalSearch(
|
||
keyword: string,
|
||
limit: number,
|
||
): UseQueryResult<SearchResult[]> {
|
||
const result = useWidgetQuery<SearchQueryData, SearchQueryVars>(
|
||
SEARCH_DOC,
|
||
{ keyword, limit },
|
||
{ enabled: keyword.length > 0 },
|
||
);
|
||
|
||
return {
|
||
data: result.data?.search,
|
||
loading: result.loading,
|
||
error: result.error,
|
||
refetch: result.refetch,
|
||
};
|
||
}
|