feat(management-navigation-single-company-ux): 收敛管理导航与单公司配置体验

这个提交包含在:
2026-08-03 12:43:00 +08:00
父节点 f4ddc18f12
当前提交 4b0184c64b
共修改 36 个文件,包含 1249 行新增和 178 行删除
+4 -4
查看文件
@@ -495,7 +495,7 @@ export function AssetRegistryPage() {
throw new Error("请选择资产分类");
}
if (!assetForm.legalEntityId) {
throw new Error("请选择法体");
throw new Error("请选择公司");
}
if (!assetForm.costCenterId) {
throw new Error("请选择成本中心");
@@ -935,7 +935,7 @@ export function AssetRegistryPage() {
setLegalEntityFilter(event.target.value);
setCostCenterFilter("");
}}>
<option value="">全部法体</option>
<option value="">全部公司</option>
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{formatTextWithCompactLongNumbers(item.name)}
@@ -1217,7 +1217,7 @@ export function AssetRegistryPage() {
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">法体</label>
<label className="text-sm font-medium text-zinc-700">公司</label>
<select data-testid="asset-legal-entity" className="select-std" value={assetForm.legalEntityId} disabled={!canEditCurrent} onChange={(event) => {
const legalEntityId = event.target.value;
setAssetForm((current) => ({
@@ -1228,7 +1228,7 @@ export function AssetRegistryPage() {
: "",
}));
}}>
<option value="">请选择法体</option>
<option value="">请选择公司</option>
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
+7 -7
查看文件
@@ -74,9 +74,9 @@ function describeScope(item: BudgetPool) {
}
if (item.scopeType === "cost_center_scoped") {
const legalEntityName = item.legalEntity?.name ?? "未知法体";
const legalEntityName = item.legalEntity?.name ?? "未知公司";
const costCenterName = item.costCenter?.name ?? "未知成本中心";
return `法体:${legalEntityName} / 成本中心:${costCenterName}`;
return `公司:${legalEntityName} / 成本中心:${costCenterName}`;
}
if (item.scopeType === "global") {
@@ -339,7 +339,7 @@ export function BudgetsPage() {
function describeRuleScope(rule: BudgetControlRule) {
const parts: string[] = [];
if (rule.legalEntityId) {
parts.push(`法体:${legalEntityMap.get(rule.legalEntityId) ?? rule.legalEntityId}`);
parts.push(`公司:${legalEntityMap.get(rule.legalEntityId) ?? rule.legalEntityId}`);
}
if (rule.departmentId) {
parts.push(`部门:${departmentMap.get(rule.departmentId) ?? rule.departmentId}`);
@@ -441,7 +441,7 @@ export function BudgetsPage() {
setPage(1);
}}
>
<option value="">全部法体</option>
<option value="">全部公司</option>
{(legalEntitiesQuery.data?.items ?? []).map((legalEntity) => (
<option key={legalEntity.id} value={legalEntity.id}>
{legalEntity.name}
@@ -519,7 +519,7 @@ export function BudgetsPage() {
>
<option value="global">全局预算</option>
<option value="department_legacy">部门预算</option>
<option value="cost_center_scoped">法体 + 成本中心预算</option>
<option value="cost_center_scoped">公司 + 成本中心预算</option>
</select>
{formScopeType === "department_legacy" ? (
@@ -547,7 +547,7 @@ export function BudgetsPage() {
setFormCostCenterId("");
}}
>
<option value="">选择法体</option>
<option value="">选择公司</option>
{(legalEntitiesQuery.data?.items ?? []).map((legalEntity) => (
<option key={legalEntity.id} value={legalEntity.id}>
{legalEntity.name}
@@ -763,7 +763,7 @@ export function BudgetsPage() {
value={ruleForm.legalEntityId}
onChange={(event) => setRuleForm((current) => ({ ...current, legalEntityId: event.target.value }))}
>
<option value="">不限法体</option>
<option value="">不限公司</option>
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
+1 -1
查看文件
@@ -566,7 +566,7 @@ export function CutoverConsolePage() {
scopeKey: event.target.value,
}))
}
placeholder="部门 ID / 法体 ID / 业务域"
placeholder="部门 ID / 公司 ID / 业务域"
/>
</div>
+2 -2
查看文件
@@ -38,7 +38,7 @@ function scopeLabel(scopeType: InventoryCheckScopeType) {
case "all":
return "全量盘点";
case "legal_entity":
return "法体盘点";
return "公司盘点";
case "cost_center":
return "成本中心盘点";
case "location":
@@ -278,7 +278,7 @@ export function InventoryChecksPage() {
setScopeRefId("");
}}>
<option value="all">全量盘点</option>
<option value="legal_entity">法体盘点</option>
<option value="legal_entity">公司盘点</option>
<option value="cost_center">成本中心盘点</option>
<option value="location">位置盘点</option>
</select>
+46 -14
查看文件
@@ -30,6 +30,7 @@ import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
import { resolveSingleCompanyState } from "../features/company/singleCompany";
const DOCUMENT_ENTITY_TYPE_OPTIONS: Array<{ value: DocumentCategoryEntityType; label: string }> = [
{ value: "reimbursement", label: "报销" },
@@ -132,6 +133,12 @@ export function MasterDataPage() {
}),
});
const activeCompaniesQuery = useQuery({
queryKey: ["master-data", "legal-entities", "active-company-state"],
enabled: canView,
queryFn: () => fetchLegalEntities({ enabled: true }),
});
const costCentersQuery = useQuery({
queryKey: [
"master-data",
@@ -167,6 +174,8 @@ export function MasterDataPage() {
});
const legalEntities = legalEntitiesQuery.data?.items ?? [];
const activeCompanies = activeCompaniesQuery.data?.items ?? [];
const companyState = resolveSingleCompanyState(activeCompanies);
const costCenters = costCentersQuery.data?.items ?? [];
const documentCategories = documentCategoriesQuery.data?.items ?? [];
const masterDataStats = {
@@ -211,7 +220,7 @@ export function MasterDataPage() {
setLegalEntityCurrency("CNY");
setLegalEntityTaxNo("");
setError("");
toast.success("法体已创建");
toast.success("公司信息已创建");
},
onError: (mutationError) => {
const message = resolveError(mutationError);
@@ -312,7 +321,7 @@ export function MasterDataPage() {
<ListWorkspace>
<WorkspaceHeader
title="主数据"
subtitle="集中维护法体、成本中心、单据分类、日历与供应商,为预算、采购、请假与资产扩展提供统一骨架。"
subtitle="集中维护公司信息、成本中心、单据分类、日历与供应商,为预算、采购、请假与资产扩展提供统一基础资料。"
actions={
<Button variant="secondary" type="button" onClick={() => setIncludeArchived((value) => !value)}>
{includeArchived ? "隐藏归档" : "显示归档"}
@@ -325,7 +334,7 @@ export function MasterDataPage() {
) : null}
<div className="finance-status-strip" data-testid="master-data-summary">
<div className="finance-metric-card"><div className="finance-metric-card__label">法体</div><div className="finance-metric-card__value">{masterDataStats.legalEntities}</div></div>
<div className="finance-metric-card"><div className="finance-metric-card__label">公司信息</div><div className="finance-metric-card__value">{masterDataStats.legalEntities}</div></div>
<div className="finance-metric-card"><div className="finance-metric-card__label">成本中心</div><div className="finance-metric-card__value">{masterDataStats.costCenters}</div></div>
<div className="finance-metric-card"><div className="finance-metric-card__label">单据分类</div><div className="finance-metric-card__value">{masterDataStats.documentCategories}</div></div>
<div className="finance-metric-card"><div className="finance-metric-card__label">归档项</div><div className="finance-metric-card__value">{masterDataStats.archived}</div></div>
@@ -334,7 +343,7 @@ export function MasterDataPage() {
<Tabs value={tab} onValueChange={(value) => setTab(value as typeof tab)}>
<TabsList className="ops-command-bar__tabs !w-full !justify-start">
<TabsTrigger value="legal-entities">法体</TabsTrigger>
<TabsTrigger value="legal-entities">公司信息</TabsTrigger>
<TabsTrigger value="cost-centers">成本中心</TabsTrigger>
<TabsTrigger value="document-categories">单据分类</TabsTrigger>
<TabsTrigger value="holiday-calendars">日历</TabsTrigger>
@@ -344,7 +353,7 @@ export function MasterDataPage() {
<TabsContent value="legal-entities" className="space-y-4">
<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
<div className="flex flex-wrap items-center justify-between gap-3">
<h3 className="text-base font-semibold text-zinc-900">法体筛选</h3>
<h3 className="text-base font-semibold text-zinc-900">公司信息筛选</h3>
<select
className="select-std max-w-xs"
value={legalEntityEnabledFilter}
@@ -357,9 +366,18 @@ export function MasterDataPage() {
</div>
</FilterBar>
{canManage ? (
{activeCompaniesQuery.isError ? (
<div className="admin-command-panel border-red-200 bg-red-50/70 text-sm text-red-600">
公司信息读取失败,请刷新后重试。
</div>
) : null}
{canManage && !activeCompaniesQuery.isLoading && !activeCompaniesQuery.isError && companyState.kind === "empty" ? (
<div className="admin-command-panel space-y-4">
<h3 className="text-base font-semibold text-zinc-900">新建法体</h3>
<div>
<h3 className="text-base font-semibold text-zinc-900">完善公司信息</h3>
<p className="mt-1 text-sm text-zinc-600">系统仅服务当前公司。完成后,企业银行账户等配置会自动关联这家公司。</p>
</div>
<div className="grid gap-3 md:grid-cols-3">
<div className="space-y-1">
<Label htmlFor="legalEntityCode">编码</Label>
@@ -387,15 +405,29 @@ export function MasterDataPage() {
disabled={legalEntityCreateMutation.isPending || !legalEntityCode.trim() || !legalEntityName.trim() || !legalEntityCurrency.trim()}
onClick={() => legalEntityCreateMutation.mutate()}
>
{legalEntityCreateMutation.isPending ? "创建中..." : "创建法体"}
{legalEntityCreateMutation.isPending ? "创建中..." : "保存公司信息"}
</Button>
</div>
</div>
</div>
) : null}
{!activeCompaniesQuery.isLoading && !activeCompaniesQuery.isError && companyState.kind === "single" ? (
<div className="admin-command-panel">
<div className="text-sm font-medium text-zinc-900">当前公司:{companyState.company.name}</div>
<div className="mt-1 text-sm text-zinc-600">系统按单公司运行,不需要选择或切换公司。</div>
</div>
) : null}
{!activeCompaniesQuery.isLoading && !activeCompaniesQuery.isError && companyState.kind === "multiple" ? (
<div className="admin-command-panel border-red-200 bg-red-50/70">
<div className="text-sm font-medium text-red-700">检测到多条启用的公司信息</div>
<div className="mt-1 text-sm text-red-600">系统不会自动选择或合并,请先停用多余记录后再继续关联配置。</div>
</div>
) : null}
<div className="admin-command-panel space-y-4">
<h3 className="text-base font-semibold text-zinc-900">法体列表</h3>
<h3 className="text-base font-semibold text-zinc-900">公司信息</h3>
{legalEntitiesQuery.isLoading ? <TableSkeleton columns={6} rows={4} /> : null}
{!legalEntitiesQuery.isLoading ? (
<PrimaryTable className="ops-table-focus">
@@ -443,7 +475,7 @@ export function MasterDataPage() {
) : (
<TableRow>
<TableCell colSpan={6} className="text-center text-sm text-zinc-500">
暂无法体数据
暂无公司信息
</TableCell>
</TableRow>
)}
@@ -471,7 +503,7 @@ export function MasterDataPage() {
value={costCenterLegalEntityFilter}
onChange={(event) => setCostCenterLegalEntityFilter(event.target.value)}
>
<option value="">全部法体</option>
<option value="">全部公司</option>
{legalEntities
.filter((item) => !item.archivedAt)
.map((item) => (
@@ -488,7 +520,7 @@ export function MasterDataPage() {
<h3 className="text-base font-semibold text-zinc-900">新建成本中心</h3>
<div className="grid gap-3 md:grid-cols-4">
<div className="space-y-1 md:col-span-2">
<Label htmlFor="costCenterLegalEntity">所属法体</Label>
<Label htmlFor="costCenterLegalEntity">所属公司</Label>
<select
id="costCenterLegalEntity"
className="select-std"
@@ -498,7 +530,7 @@ export function MasterDataPage() {
setCostCenterParentId("");
}}
>
<option value="">请选择法体</option>
<option value="">请选择公司</option>
{legalEntities
.filter((item) => !item.archivedAt)
.map((item) => (
@@ -561,7 +593,7 @@ export function MasterDataPage() {
<TableRow>
<TableHead>编码</TableHead>
<TableHead>名称</TableHead>
<TableHead>所属法体</TableHead>
<TableHead>所属公司</TableHead>
<TableHead>上级</TableHead>
<TableHead>负责人</TableHead>
<TableHead>状态</TableHead>
+2 -2
查看文件
@@ -102,7 +102,7 @@ function assetTypeLabel(assetType: AssetItem["assetType"]) {
function inventoryScopeLabel(inventoryCheck: InventoryCheck) {
if (inventoryCheck.scopeType === "all") return "全量盘点";
if (inventoryCheck.scopeType === "legal_entity") return "法体盘点";
if (inventoryCheck.scopeType === "legal_entity") return "公司盘点";
if (inventoryCheck.scopeType === "cost_center") return "成本中心盘点";
return "位置盘点";
}
@@ -559,7 +559,7 @@ export function MobileAssetScanPage() {
<div className="mobile-info-grid">
<div className="mobile-subcard text-sm">类型:{assetTypeLabel(selectedAsset.assetType)}</div>
<div className="mobile-subcard text-sm">状态:{statusLabel(selectedAsset.status)}</div>
<div className="mobile-subcard text-sm">法体:{selectedAsset.legalEntity?.name ?? "-"}</div>
<div className="mobile-subcard text-sm">公司:{selectedAsset.legalEntity?.name ?? "-"}</div>
<div className="mobile-subcard text-sm">成本中心:{selectedAsset.costCenter?.name ?? "-"}</div>
<div className="mobile-subcard text-sm">位置:{selectedAsset.location ?? "-"}</div>
<div className="mobile-subcard text-sm">归属人:{selectedAsset.assignee?.name ?? "未分配"}</div>
@@ -52,7 +52,7 @@ function resolveMobilePurchaseEvidence(detail: {
projectId: string | null;
}) {
if (!detail.supplierId) return "待补供应商";
if (!detail.legalEntityId || !detail.costCenterId) return "待补法体 / 成本中心";
if (!detail.legalEntityId || !detail.costCenterId) return "待补公司 / 成本中心";
if (!detail.documentCategoryId) return "待补单据分类";
if (!detail.budgetPoolId) return "预算池未匹配";
if (!detail.projectId) return "未关联项目";
@@ -167,7 +167,7 @@ export function MobilePurchaseRequestDetailPage() {
<div className="mobile-field-value">{formatDate(detail.expectedDeliveryDate)}</div>
</div>
<div className="mobile-field-row">
<div className="mobile-field-label">法体</div>
<div className="mobile-field-label">公司</div>
<div className="mobile-field-value">{detail.legalEntity?.name ?? "-"}</div>
</div>
<div className="mobile-field-row">
@@ -49,7 +49,7 @@ function resolvePurchaseListNextStep(status: PurchaseRequestStatus) {
function resolvePurchaseListEvidence(item: PurchaseRequestSummary) {
if (!item.supplierId) return "待补供应商";
if (!item.legalEntityId || !item.costCenterId) return "待补法体 / 成本中心";
if (!item.legalEntityId || !item.costCenterId) return "待补公司 / 成本中心";
if (!item.documentCategoryId) return "待补单据分类";
if (!item.budgetPoolId) return "预算池未匹配";
if (!item.projectId) return "未关联项目";
+63 -11
查看文件
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
@@ -14,8 +15,9 @@ import {
import { fetchLegalEntities } from "../api/masterData";
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { hasAnyPermission, hasPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { resolveSingleCompanyState } from "../features/company/singleCompany";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
@@ -33,6 +35,7 @@ export function PaymentAccountsPage() {
const { permissions } = useAuth();
const canManage = hasPermission(permissions, "payment.account.manage");
const canReveal = hasPermission(permissions, "payment.account.read_full");
const canManageCompanyInformation = hasAnyPermission(permissions, ["masterdata.view", "masterdata.manage"]);
const [editing, setEditing] = useState<PaymentAccount | null>(null);
const [legalEntityId, setLegalEntityId] = useState("");
const [bankInstitutionId, setBankInstitutionId] = useState("");
@@ -51,9 +54,19 @@ export function PaymentAccountsPage() {
enabled: canManage,
});
const activeCompanies = legalEntitiesQuery.data?.items ?? [];
const companyState = resolveSingleCompanyState(activeCompanies);
const singleCompany = companyState.kind === "single" ? companyState.company : null;
useEffect(() => {
if (!editing && singleCompany) {
setLegalEntityId(singleCompany.id);
}
}, [editing, singleCompany]);
const resetForm = () => {
setEditing(null);
setLegalEntityId("");
setLegalEntityId(singleCompany?.id ?? "");
setBankInstitutionId("");
setAccountName("");
setAccountNumber("");
@@ -93,17 +106,56 @@ export function PaymentAccountsPage() {
const accounts = accountsQuery.data?.items ?? [];
return (
<PageScaffold>
<PageHeader title="企业银行账户" subtitle="维护法人主体的付款来源账户;完整账号仅在填写原因后临时显示。" />
<PageHeader title="企业银行账户" subtitle="维护当前公司的付款来源账户;完整账号仅在填写原因后临时显示。" />
<div className="mx-auto max-w-6xl space-y-5">
{canManage ? (
{canManage && legalEntitiesQuery.isLoading ? (
<SectionPanel title="企业银行账户">
<div className="text-sm text-zinc-500">正在读取公司信息...</div>
</SectionPanel>
) : null}
{canManage && legalEntitiesQuery.isError ? (
<SectionPanel title="企业银行账户">
<div className="text-sm text-red-600">公司信息读取失败,请刷新后重试。</div>
</SectionPanel>
) : null}
{canManage && !legalEntitiesQuery.isLoading && !legalEntitiesQuery.isError && companyState.kind === "empty" ? (
<SectionPanel title="先完善公司信息">
<div className="space-y-3 text-sm text-zinc-600">
<p>企业银行账户必须关联当前公司。请先完善公司信息,再回来录入付款账户。</p>
{canManageCompanyInformation ? (
<Button asChild type="button" variant="secondary">
<Link to="/system/master-data">完善公司信息</Link>
</Button>
) : (
<p>请联系管理员先完善公司信息。</p>
)}
</div>
</SectionPanel>
) : null}
{canManage && companyState.kind === "multiple" ? (
<SectionPanel title="公司信息需要处理">
<div className="space-y-3 text-sm text-red-600">
<p>检测到多条启用的公司信息,系统不会猜测付款账户应关联哪一条。请先处理公司信息。</p>
{canManageCompanyInformation ? (
<Button asChild type="button" variant="secondary">
<Link to="/system/master-data">查看公司信息</Link>
</Button>
) : null}
</div>
</SectionPanel>
) : null}
{canManage && singleCompany ? (
<SectionPanel title={editing ? "替换企业银行账户" : "新增企业银行账户"}>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="payment-account-legal-entity">法人主体</Label>
<select id="payment-account-legal-entity" className="select-std w-full" value={legalEntityId} disabled={Boolean(editing)} onChange={(event) => setLegalEntityId(event.target.value)}>
<option value="">请选择</option>
{(legalEntitiesQuery.data?.items ?? []).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
<Label>当前公司</Label>
<div className="input-std flex min-h-10 items-center bg-zinc-50 text-sm text-zinc-700" data-testid="payment-account-company">
{editing?.legalEntity.name ?? singleCompany.name}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="payment-account-bank">开户银行</Label>
@@ -158,7 +210,7 @@ export function PaymentAccountsPage() {
</Dialog>
<Dialog open={Boolean(disableTarget)} onOpenChange={(open) => { if (!open) setDisableTarget(null); }}>
<DialogContent className="max-w-md p-5">
<DialogHeader><DialogTitle>停用企业银行账户</DialogTitle><DialogDescription>停用后不能为新付款批次生成制单;历史批次仍保留冻结账户摘要。确认停用 {disableTarget?.legalEntity.name ?? "该法人主体"} 的当前账户吗?</DialogDescription></DialogHeader>
<DialogHeader><DialogTitle>停用企业银行账户</DialogTitle><DialogDescription>停用后不能为新付款批次生成制单;历史批次仍保留冻结账户摘要。确认停用 {disableTarget?.legalEntity.name ?? "当前公司"} 的当前账户吗?</DialogDescription></DialogHeader>
<DialogFooter><Button type="button" variant="secondary" onClick={() => setDisableTarget(null)}>返回</Button><Button type="button" variant="destructive" disabled={disableMutation.isPending} onClick={() => { if (disableTarget) disableMutation.mutate(disableTarget.id); }}>{disableMutation.isPending ? "停用中..." : "确认停用"}</Button></DialogFooter>
</DialogContent>
</Dialog>
+5 -5
查看文件
@@ -178,7 +178,7 @@ function resolvePurchaseEvidenceHint(item: {
}) {
const missingFields = [
item.supplierId ? null : "供应商",
item.legalEntityId ? null : "法体",
item.legalEntityId ? null : "公司",
item.costCenterId ? null : "成本中心",
item.documentCategoryId ? null : "单据分类",
].filter(Boolean);
@@ -590,7 +590,7 @@ export function PurchaseRequestsPage() {
throw new Error("提交前请选择供应商");
}
if (!legalEntityId) {
throw new Error("提交前请选择法体");
throw new Error("提交前请选择公司");
}
if (!costCenterId) {
throw new Error("提交前请选择成本中心");
@@ -991,7 +991,7 @@ export function PurchaseRequestsPage() {
setPage(1);
}}
>
<option value="">全部法体</option>
<option value="">全部公司</option>
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
@@ -1344,7 +1344,7 @@ export function PurchaseRequestsPage() {
<Input value={currencyCode} onChange={(event) => setCurrencyCode(event.target.value.toUpperCase())} disabled={!canEditDraft} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-700">法体</label>
<label className="text-sm font-medium text-zinc-700">公司</label>
<select
data-testid="purchase-legal-entity"
className="select-std"
@@ -1358,7 +1358,7 @@ export function PurchaseRequestsPage() {
}
}}
>
<option value="">请选择法体</option>
<option value="">请选择公司</option>
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
+4 -32
查看文件
@@ -1,40 +1,12 @@
import { Link } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
type ManagementItem = {
to: string;
icon: string;
title: string;
description: string;
group: "人员与组织" | "流程与规则" | "系统与审计" | "功能管理";
anyOf: string[];
};
const ITEMS: ManagementItem[] = [
{ to: "/people", icon: "badge", title: "人员", description: "员工档案、入转调离和人员状态。", group: "人员与组织", anyOf: ["system.users"] },
{ to: "/organization", icon: "account_tree", title: "组织", description: "组织结构、部门和汇报关系。", group: "人员与组织", anyOf: ["system.users", "report.dashboard.view"] },
{ to: "/system/users", icon: "manage_accounts", title: "账号", description: "账号、角色和登录权限。", group: "人员与组织", anyOf: ["system.accounts.manage", "system.roles.manage"] },
{ to: "/system/departments", icon: "account_tree", title: "部门", description: "部门基础数据维护。", group: "人员与组织", anyOf: ["system.users"] },
{ to: "/system/approval-rules", icon: "rule", title: "审批规则", description: "金额段、审批层级和处理规则。", group: "流程与规则", anyOf: ["system.settings"] },
{ to: "/system/workflow-definitions", icon: "schema", title: "流程定义", description: "流程模板和节点治理。", group: "流程与规则", anyOf: ["system.settings"] },
{ to: "/system/expense-standards", icon: "rule_settings", title: "费用标准", description: "费用上限、类别和管控口径。", group: "流程与规则", anyOf: ["system.settings"] },
{ to: "/system/reimbursement-templates", icon: "receipt_long", title: "报销模板", description: "常见报销场景模板。", group: "流程与规则", anyOf: ["system.settings"] },
{ to: "/system/workspace", icon: "dashboard_customize", title: "工作区", description: "菜单入口、模块显隐和小团队基线。", group: "系统与审计", anyOf: ["system.settings"] },
{ to: "/audit-logs", icon: "manage_search", title: "审计日志", description: "关键操作留痕和排障。", group: "系统与审计", anyOf: ["system.audit.view"] },
{ to: "/system/master-data", icon: "dataset", title: "主数据", description: "法体、成本中心、供应商等基础数据。", group: "功能管理", anyOf: ["masterdata.view", "masterdata.manage"] },
{ to: "/system/payment-accounts", icon: "account_balance", title: "企业银行账户", description: "维护付款来源账户、脱敏查看与变更留痕。", group: "功能管理", anyOf: ["payment.account.read", "payment.account.manage"] },
{ to: "/system/notification-channels", icon: "notifications_active", title: "通知渠道", description: "企业微信、邮件等补充通知。", group: "功能管理", anyOf: ["notification.channel.manage"] },
{ to: "/system/feature-flags", icon: "toggle_on", title: "功能管理", description: "功能开关、灰度和试运行边界。", group: "功能管理", anyOf: ["system.settings"] },
];
const GROUPS: ManagementItem["group"][] = ["人员与组织", "流程与规则", "系统与审计", "功能管理"];
import { getVisibleManagementItems, MANAGEMENT_GROUPS } from "../features/navigation/managementRoutes";
export function SystemSettingsPage() {
const { permissions } = useAuth();
const visibleItems = ITEMS.filter((item) => hasAnyPermission(permissions, item.anyOf));
const visibleGroups = GROUPS.map((group) => ({
const visibleItems = getVisibleManagementItems(permissions);
const visibleGroups = MANAGEMENT_GROUPS.map((group) => ({
group,
items: visibleItems.filter((item) => item.group === group),
})).filter((entry) => entry.items.length > 0);
@@ -74,7 +46,7 @@ export function SystemSettingsPage() {
<h2 className="text-base font-semibold text-zinc-900">{group}</h2>
<div className="admin-tile-grid">
{items.map((item) => (
<Link key={item.to} to={item.to} className="admin-tile-card">
<Link key={item.key} to={item.path} className="admin-tile-card">
<div className="flex items-start gap-3">
<span className="admin-tile-card__icon">
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
+6 -6
查看文件
@@ -407,7 +407,7 @@ export function TravelRequestsPage() {
throw new Error("请选择出差起止日期");
}
if (!legalEntityId || !costCenterId) {
throw new Error("请选择法体与成本中心");
throw new Error("请选择公司与成本中心");
}
if (!estimatedAmount || Number(estimatedAmount) < 0) {
throw new Error("请填写有效的预估金额");
@@ -614,7 +614,7 @@ export function TravelRequestsPage() {
</div>
<div>
<Label htmlFor="travel-filter-legal-entity">法体</Label>
<Label htmlFor="travel-filter-legal-entity">公司</Label>
<select
id="travel-filter-legal-entity"
className="select-std mt-1"
@@ -625,7 +625,7 @@ export function TravelRequestsPage() {
setPage(1);
}}
>
<option value="">全部法体</option>
<option value="">全部公司</option>
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>{item.name}</option>
))}
@@ -828,7 +828,7 @@ export function TravelRequestsPage() {
<p className="font-medium text-zinc-900">{detail.budgetPool?.name ?? "待提交时自动解析"}</p>
</div>
<div>
<p className="text-zinc-500">法体</p>
<p className="text-zinc-500">公司</p>
<p className="font-medium text-zinc-900">{detail.legalEntity?.name ?? "-"}</p>
</div>
<div>
@@ -1030,7 +1030,7 @@ function TravelRequestForm(props: TravelRequestFormProps) {
<Input id="travel-end-date" className="mt-1" type="date" value={endDate} onChange={(event) => setEndDate(event.target.value)} readOnly={readOnly} />
</div>
<div>
<Label htmlFor="travel-legal-entity">法体</Label>
<Label htmlFor="travel-legal-entity">公司</Label>
<select
id="travel-legal-entity"
className="select-std mt-1"
@@ -1041,7 +1041,7 @@ function TravelRequestForm(props: TravelRequestFormProps) {
}}
disabled={readOnly}
>
<option value="">请选择法体</option>
<option value="">请选择公司</option>
{legalEntities.map((item) => (
<option key={item.id} value={item.id}>{item.name}</option>
))}
+2 -2
查看文件
@@ -351,7 +351,7 @@ export function WorkflowTaskDetailPage() {
<DetailField label="标题" value={detail.purchaseRequest.title || "-"} />
<DetailField label="申请人" value={detail.purchaseRequest.applicant?.name ?? "-"} />
<DetailField label="供应商" value={detail.purchaseRequest.supplier?.name ?? "-"} />
<DetailField label="法体" value={detail.purchaseRequest.legalEntity?.name ?? "-"} />
<DetailField label="公司" value={detail.purchaseRequest.legalEntity?.name ?? "-"} />
<DetailField label="成本中心" value={detail.purchaseRequest.costCenter?.name ?? "-"} />
<DetailField label="单据分类" value={detail.purchaseRequest.documentCategory?.name ?? "-"} />
<DetailField label="金额" value={`${detail.purchaseRequest.totalAmount ?? "-"} ${detail.purchaseRequest.currencyCode}`} />
@@ -364,7 +364,7 @@ export function WorkflowTaskDetailPage() {
<DetailField label="差旅单号" value={detail.travelRequest.requestNo ?? "暂未生成"} />
<DetailField label="申请人" value={detail.travelRequest.applicant?.name ?? "-"} />
<DetailField label="出差类型" value={detail.travelRequest.tripType === "international" ? "国际" : "国内"} />
<DetailField label="法体" value={detail.travelRequest.legalEntity?.name ?? "-"} />
<DetailField label="公司" value={detail.travelRequest.legalEntity?.name ?? "-"} />
<DetailField label="成本中心" value={detail.travelRequest.costCenter?.name ?? "-"} />
<DetailField label="起止日期" value={`${detail.travelRequest.startDate ?? "-"} → ${detail.travelRequest.endDate ?? "-"}`} />
<DetailField label="预估金额" value={detail.travelRequest.estimatedAmount ?? "-"} />