Files
Edu/apps/admin-portal/src/hooks/use-announcements.ts
SpecialX 7cf9aec20e feat(admin-portal): 完成参考项目差距闭环 - 4 批次补齐 + Vitest 测试
差距分析闭环(workline §7-§8):
- P0:审计子模块 3 页 + 学校组织 7 页
- P1:系统设置 4 Card + 公告管理 + 邀请码管理
- P2:文件管理 + AI 配置
- P6:5 测试文件 / 69 用例全过

共享基础设施:
- permissions +11 权限点 +14 路由
- view-models +20 接口
- i18n +100 key
- graphql-client +25 operation
- fixtures/handlers +15 mock +20 handler

质量:typecheck + lint 零错误,vitest 69/69 通过,arch.db 已更新
2026-07-13 13:05:29 +08:00

129 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 公告管理 Hooks
*
* contract §2.4adminAnnouncements / createAnnouncement / publishAnnouncement / archiveAnnouncement / toggleAnnouncementPin
*/
import { useCallback, useState } from "react";
import { useGraphMutation, useGraphQuery } from "./use-graphql";
import {
ADMIN_ANNOUNCEMENTS_QUERY,
CREATE_ANNOUNCEMENT_MUTATION,
PUBLISH_ANNOUNCEMENT_MUTATION,
ARCHIVE_ANNOUNCEMENT_MUTATION,
TOGGLE_ANNOUNCEMENT_PIN_MUTATION,
} from "@/lib/graphql-client";
import type {
AnnouncementViewModel,
PaginatedResult,
ListFilter,
} from "@/types/view-models";
interface AdminAnnouncementsResponse {
adminAnnouncements: PaginatedResult<AnnouncementViewModel>;
}
interface AnnouncementMutationResponse {
createAnnouncement?: { id: string; title: string; status: string };
publishAnnouncement?: { id: string; status: string };
archiveAnnouncement?: { id: string; status: string };
toggleAnnouncementPin?: { id: string; isPinned: boolean };
}
interface CreateAnnouncementInput {
title: string;
content: string;
audience?: string[];
}
export function useAnnouncements(filter: Partial<ListFilter>) {
const result = useGraphQuery<AdminAnnouncementsResponse>(
ADMIN_ANNOUNCEMENTS_QUERY,
{ filter },
);
return {
...result,
data: result.data?.adminAnnouncements ?? null,
};
}
export function useAnnouncementFilter(initial?: Partial<ListFilter>) {
const [filter, setFilter] = useState<Partial<ListFilter>>({
page: 1,
pageSize: 20,
...initial,
});
const setPage = useCallback(
(page: number) => setFilter((f) => ({ ...f, page })),
[],
);
const setSearch = useCallback(
(search: string) => setFilter((f) => ({ ...f, search, page: 1 })),
[],
);
const setStatus = useCallback(
(status: string) => setFilter((f) => ({ ...f, status, page: 1 })),
[],
);
return { filter, setFilter, setPage, setSearch, setStatus };
}
export function useCreateAnnouncement() {
const [run, state] = useGraphMutation<
AnnouncementMutationResponse,
{ input: CreateAnnouncementInput }
>(CREATE_ANNOUNCEMENT_MUTATION);
const create = useCallback(
async (input: CreateAnnouncementInput) => {
const res = await run({ input });
return res.data?.createAnnouncement ?? null;
},
[run],
);
return [create, state] as const;
}
export function usePublishAnnouncement() {
const [run, state] = useGraphMutation<
AnnouncementMutationResponse,
{ id: string }
>(PUBLISH_ANNOUNCEMENT_MUTATION);
const publish = useCallback(
async (id: string) => {
const res = await run({ id });
return res.data?.publishAnnouncement ?? null;
},
[run],
);
return [publish, state] as const;
}
export function useArchiveAnnouncement() {
const [run, state] = useGraphMutation<
AnnouncementMutationResponse,
{ id: string }
>(ARCHIVE_ANNOUNCEMENT_MUTATION);
const archive = useCallback(
async (id: string) => {
const res = await run({ id });
return res.data?.archiveAnnouncement ?? null;
},
[run],
);
return [archive, state] as const;
}
export function useToggleAnnouncementPin() {
const [run, state] = useGraphMutation<
AnnouncementMutationResponse,
{ id: string }
>(TOGGLE_ANNOUNCEMENT_PIN_MUTATION);
const toggle = useCallback(
async (id: string) => {
const res = await run({ id });
return res.data?.toggleAnnouncementPin ?? null;
},
[run],
);
return [toggle, state] as const;
}