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 已更新
This commit is contained in:
SpecialX
2026-07-13 13:05:29 +08:00
parent fad6e43b47
commit 7cf9aec20e
40 changed files with 6707 additions and 7 deletions

View File

@@ -0,0 +1,102 @@
/**
* 文件管理 Hooks
*
* contract §2.4adminFiles / adminFileStats / deleteFiles
*/
import { useCallback, useState } from "react";
import { useGraphMutation, useGraphQuery } from "./use-graphql";
import {
ADMIN_FILES_QUERY,
ADMIN_FILE_STATS_QUERY,
DELETE_FILES_MUTATION,
} from "@/lib/graphql-client";
import type {
FileViewModel,
FileStats,
PaginatedResult,
} from "@/types/view-models";
/** 文件列表筛选参数 */
export interface FileFilter {
page?: number;
pageSize?: number;
search?: string;
mimeType?: string;
entityType?: string;
}
interface AdminFilesResponse {
adminFiles: PaginatedResult<FileViewModel>;
}
interface AdminFileStatsResponse {
adminFileStats: FileStats;
}
interface DeleteFilesResponse {
deleteFiles: boolean;
}
export function useFiles(filter: FileFilter) {
const result = useGraphQuery<AdminFilesResponse>(ADMIN_FILES_QUERY, {
filter,
});
return {
...result,
data: result.data?.adminFiles ?? null,
};
}
export function useFileStats() {
const result = useGraphQuery<AdminFileStatsResponse>(ADMIN_FILE_STATS_QUERY);
return {
...result,
data: result.data?.adminFileStats ?? null,
};
}
export function useDeleteFiles() {
const [run, state] = useGraphMutation<DeleteFilesResponse, { ids: string[] }>(
DELETE_FILES_MUTATION,
);
const deleteFiles = useCallback(
async (ids: string[]) => {
const res = await run({ ids });
return res.data?.deleteFiles ?? false;
},
[run],
);
return [deleteFiles, state] as const;
}
/** 文件筛选状态管理 */
export function useFileFilter(initial?: FileFilter) {
const [filter, setFilter] = useState<FileFilter>({
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 setMimeType = useCallback(
(mimeType: string) =>
setFilter((f) => ({ ...f, mimeType: mimeType || undefined, page: 1 })),
[],
);
const setEntityType = useCallback(
(entityType: string) =>
setFilter((f) => ({
...f,
entityType: entityType || undefined,
page: 1,
})),
[],
);
return { filter, setFilter, setPage, setSearch, setMimeType, setEntityType };
}