170 行
6.5 KiB
TypeScript
170 行
6.5 KiB
TypeScript
import { useMemo } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { fetchSystemNavigation, fetchWorkspaceSettings, updateWorkspaceSettings } from "../api/workspace";
|
|
import { isApiError } from "../api/types";
|
|
import { mapApiErrorMessage } from "../api/errorMessages";
|
|
import { PageHeader } from "../components/common/PageHeader";
|
|
import { Button } from "../components/ui/button";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
|
import { toast } from "sonner";
|
|
|
|
const MODULE_LABELS: Record<string, string> = {
|
|
portal: "工作台",
|
|
team: "团队",
|
|
leaveRequests: "请假",
|
|
attendance: "考勤",
|
|
reimbursements: "报销",
|
|
assets: "资产",
|
|
weeklyReports: "周报",
|
|
todos: "待办",
|
|
announcements: "公告",
|
|
projects: "项目",
|
|
workspace: "工作区设置",
|
|
people: "人员管理",
|
|
organization: "组织架构",
|
|
contractsRegistry: "合同台账",
|
|
workflowDefinitions: "流程定义",
|
|
systemSettings: "系统设置",
|
|
featureFlags: "功能开关",
|
|
auditLogs: "审计日志",
|
|
approvalRules: "审批规则",
|
|
userManagement: "用户管理",
|
|
departments: "部门管理",
|
|
masterData: "主数据",
|
|
notificationChannels: "通知渠道",
|
|
workflowTasks: "审批待办",
|
|
dashboard: "仪表盘",
|
|
purchaseRequests: "采购申请",
|
|
travelRequests: "差旅申请",
|
|
budgets: "预算管理",
|
|
paymentBatches: "付款批次",
|
|
apOperations: "AP 运营",
|
|
invoicePool: "票据中心",
|
|
inventoryChecks: "资产盘点",
|
|
cutover: "切换控制台",
|
|
erpIntegrations: "ERP 集成",
|
|
profile: "个人设置",
|
|
};
|
|
|
|
export function WorkspaceSetupPage() {
|
|
const queryClient = useQueryClient();
|
|
const workspaceQuery = useQuery({ queryKey: ["workspace-settings"], queryFn: fetchWorkspaceSettings });
|
|
const navigationQuery = useQuery({ queryKey: ["system-navigation"], queryFn: fetchSystemNavigation });
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: updateWorkspaceSettings,
|
|
onSuccess: async () => {
|
|
await Promise.all([
|
|
queryClient.invalidateQueries({ queryKey: ["workspace-settings"] }),
|
|
queryClient.invalidateQueries({ queryKey: ["system-navigation"] }),
|
|
queryClient.invalidateQueries({ queryKey: ["portal-home"] }),
|
|
]);
|
|
toast.success("工作区设置已保存");
|
|
},
|
|
onError: (error) => {
|
|
const message = isApiError(error)
|
|
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
|
|
: error instanceof Error
|
|
? error.message
|
|
: "保存失败";
|
|
toast.error(message);
|
|
},
|
|
});
|
|
|
|
const workspace = workspaceQuery.data;
|
|
const rolePresets = navigationQuery.data?.recommendedRolePresets ?? [];
|
|
const moduleEntries = useMemo(() => Object.entries(workspace?.moduleVisibility ?? {}), [workspace?.moduleVisibility]);
|
|
|
|
if (workspaceQuery.isLoading || !workspace) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
title="工作区设置"
|
|
subtitle="按 10 人以下小团队模式收敛主导航与默认入口。"
|
|
/>
|
|
<div className="card-std p-6 text-sm text-zinc-500">加载工作区设置中...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
title="工作区设置"
|
|
subtitle="按 10 人以下小团队模式收敛主导航与默认入口。"
|
|
/>
|
|
|
|
<div className="grid gap-4 xl:grid-cols-3">
|
|
<div className="card-std p-6">
|
|
<div className="text-sm text-zinc-500">产品模式</div>
|
|
<div className="mt-2 text-xl font-semibold text-zinc-900">{workspace.profile}</div>
|
|
<div className="mt-3 text-sm text-zinc-600">单公司、账号密码、站内通知、桌面优先。</div>
|
|
</div>
|
|
<div className="card-std p-6">
|
|
<div className="text-sm text-zinc-500">初始化状态</div>
|
|
<div className="mt-2 text-xl font-semibold text-zinc-900">{workspace.persisted ? "已完成" : "待初始化"}</div>
|
|
<div className="mt-3 text-sm text-zinc-600">首次管理员进入此页后保存一次,即完成小团队工作区初始化。</div>
|
|
</div>
|
|
<div className="card-std p-6">
|
|
<div className="text-sm text-zinc-500">推荐角色预设</div>
|
|
<div className="mt-3 space-y-2 text-sm">
|
|
{rolePresets.map((item) => (
|
|
<div key={item.key} className="rounded-lg border border-zinc-200 p-3">
|
|
<div className="font-medium text-zinc-900">{item.label}</div>
|
|
<div className="mt-1 text-zinc-600">{item.description}</div>
|
|
<div className="mt-1 text-xs text-zinc-500">角色:{item.roleCodes.join(" / ")}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card-std p-6 space-y-4">
|
|
<div>
|
|
<h3 className="text-base font-semibold text-zinc-900">模块显隐</h3>
|
|
<p className="mt-1 text-sm text-zinc-500">主导航只保留高频入口;备用模块保留代码与数据,但默认放在管理员备用区。</p>
|
|
</div>
|
|
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>模块</TableHead>
|
|
<TableHead>当前显隐</TableHead>
|
|
<TableHead>操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{moduleEntries.map(([key, mode]) => (
|
|
<TableRow key={key}>
|
|
<TableCell>{MODULE_LABELS[key] ?? key}</TableCell>
|
|
<TableCell>{mode}</TableCell>
|
|
<TableCell>
|
|
<div className="flex flex-wrap gap-2">
|
|
{(["main", "admin", "hidden"] as const).map((nextMode) => (
|
|
<Button
|
|
key={nextMode}
|
|
size="sm"
|
|
variant={mode === nextMode ? "default" : "secondary"}
|
|
type="button"
|
|
disabled={mutation.isPending}
|
|
onClick={() => {
|
|
const nextVisibility = { ...(workspace.moduleVisibility ?? {}), [key]: nextMode };
|
|
mutation.mutate({ moduleVisibility: nextVisibility });
|
|
}}
|
|
>
|
|
{nextMode}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default WorkspaceSetupPage;
|