文件
hyb-oa/client/src/pages/WorkspaceSetupPage.tsx
T

221 行
8.7 KiB
TypeScript

import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
import { useMemo } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { fetchSystemNavigation, fetchWorkspaceSettings, updateWorkspaceSettings } from "../api/workspace";
import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
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: "应付账款",
invoicePool: "票据",
inventoryChecks: "资产盘点",
cutover: "切换控制台",
profile: "个人设置",
};
const VISIBILITY_OPTIONS = [
{ value: "main", label: "常用入口" },
{ value: "admin", label: "管理与更多" },
{ value: "hidden", label: "隐藏" },
] as const;
function visibilityModeLabel(mode: string) {
return VISIBILITY_OPTIONS.find((item) => item.value === mode)?.label ?? mode;
}
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 = resolveUserErrorMessage(error, "保存失败");
toast.error(message);
},
});
const workspace = workspaceQuery.data;
const rolePresets = navigationQuery.data?.recommendedRolePresets ?? [];
const moduleEntries = useMemo(
() => Object.entries(workspace?.moduleVisibility ?? {}).filter(([key]) => key !== "erpIntegrations"),
[workspace?.moduleVisibility],
);
const workspaceStats = useMemo(() => {
const entries = Object.entries(workspace?.moduleVisibility ?? {}).filter(([key]) => key !== "erpIntegrations");
return {
total: entries.length,
main: entries.filter(([, mode]) => mode === "main").length,
admin: entries.filter(([, mode]) => mode === "admin").length,
hidden: entries.filter(([, mode]) => mode === "hidden").length,
rolePresets: rolePresets.length,
};
}, [rolePresets.length, workspace?.moduleVisibility]);
if (workspaceQuery.isLoading || !workspace) {
return (
<ListWorkspace>
<WorkspaceHeader
eyebrow="管理"
title="工作区设置"
subtitle="按 10 人以下小团队模式收敛常用入口与低频模块。"
/>
<div className="admin-command-panel text-sm text-zinc-500">加载工作区设置中...</div>
</ListWorkspace>
);
}
return (
<ListWorkspace>
<WorkspaceHeader
eyebrow="管理"
title="工作区设置"
subtitle="按小团队运营模式收敛常用入口、管理入口和低频模块。"
/>
<div className="finance-status-strip" data-testid="workspace-settings-summary">
<div className="finance-metric-card">
<div className="finance-metric-card__label">模块总数</div>
<div className="finance-metric-card__value">{workspaceStats.total}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">常用入口</div>
<div className="finance-metric-card__value">{workspaceStats.main}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">管理与更多</div>
<div className="finance-metric-card__value">{workspaceStats.admin}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">隐藏</div>
<div className="finance-metric-card__value">{workspaceStats.hidden}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">初始化</div>
<div className="finance-metric-card__value text-base">{workspace.persisted ? "已完成" : "待初始化"}</div>
</div>
</div>
<section className="admin-command-panel space-y-4" data-testid="workspace-profile-panel">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<h2 className="text-base font-semibold text-zinc-900">工作区基线</h2>
<p className="mt-1 text-sm leading-6 text-zinc-600">
{workspace.profile} · 单公司 · 账号密码 · 站内通知 · 桌面优先。
</p>
</div>
<div className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs text-slate-500">
{workspaceStats.rolePresets} 个角色预设
</div>
</div>
{rolePresets.length ? (
<div className="admin-tile-grid">
{rolePresets.map((item) => (
<div key={item.key} className="admin-tile-card">
<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 className="rounded-lg border border-dashed border-slate-300 px-4 py-5 text-sm text-slate-500">
当前没有推荐角色预设。
</div>
)}
</section>
<section className="admin-command-panel space-y-4">
<div>
<h3 className="text-base font-semibold text-zinc-900">入口显隐</h3>
<p className="mt-1 text-sm leading-6 text-zinc-600">
常用入口只保留高频任务;低频能力保留代码与数据,默认收进管理、更多或隐藏。
</p>
</div>
<PrimaryTable>
<Table data-testid="workspace-modules-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>{visibilityModeLabel(mode)}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
{VISIBILITY_OPTIONS.map((option) => (
<Button
key={option.value}
size="sm"
variant={mode === option.value ? "default" : "secondary"}
type="button"
disabled={mutation.isPending}
onClick={() => {
const nextVisibility = { ...(workspace.moduleVisibility ?? {}), [key]: option.value };
mutation.mutate({ moduleVisibility: nextVisibility });
}}
>
{option.label}
</Button>
))}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</PrimaryTable>
</section>
</ListWorkspace>
);
}
export default WorkspaceSetupPage;