完成 Wave4-5 财务与管理页刷新(05-29-frontend-ux-wave4-5-finance-admin-refresh)
这个提交包含在:
@@ -3,7 +3,7 @@ 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 { 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";
|
||||
@@ -46,6 +46,16 @@ const MODULE_LABELS: Record<string, string> = {
|
||||
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 });
|
||||
@@ -74,95 +84,139 @@ export function WorkspaceSetupPage() {
|
||||
const workspace = workspaceQuery.data;
|
||||
const rolePresets = navigationQuery.data?.recommendedRolePresets ?? [];
|
||||
const moduleEntries = useMemo(() => Object.entries(workspace?.moduleVisibility ?? {}), [workspace?.moduleVisibility]);
|
||||
const workspaceStats = useMemo(() => {
|
||||
const entries = Object.entries(workspace?.moduleVisibility ?? {});
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="工作区设置"
|
||||
subtitle="按 10 人以下小团队模式收敛主导航与默认入口。"
|
||||
/>
|
||||
<div className="card-std p-6 text-sm text-zinc-500">加载工作区设置中...</div>
|
||||
</div>
|
||||
<div className="admin-command-panel text-sm text-zinc-500">加载工作区设置中...</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="工作区设置"
|
||||
subtitle="按 10 人以下小团队模式收敛主导航与默认入口。"
|
||||
subtitle="按小团队运营模式收敛主导航、管理入口和备用模块。"
|
||||
/>
|
||||
|
||||
<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 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="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 className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">主导航</div>
|
||||
<div className="finance-metric-card__value">{workspaceStats.main}</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">
|
||||
<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="rounded-lg border border-zinc-200 p-3">
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-slate-300 px-4 py-5 text-sm text-slate-500">
|
||||
当前没有推荐角色预设。
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<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 text-zinc-500">主导航只保留高频入口;备用模块保留代码与数据,但默认放在管理员备用区。</p>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-600">
|
||||
主导航只保留高频入口;备用模块保留代码与数据,低频功能默认收进管理区或隐藏。
|
||||
</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>
|
||||
<PrimaryTable>
|
||||
<Table data-testid="workspace-modules-table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>模块</TableHead>
|
||||
<TableHead>当前显隐</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户