优化财务与管理页视觉 Wave4(05-27-pc-finance-admin-visual-wave4)
这个提交包含在:
@@ -527,6 +527,78 @@
|
||||
@apply text-xs font-medium;
|
||||
}
|
||||
|
||||
.finance-status-strip {
|
||||
@apply grid gap-3 md:grid-cols-2 xl:grid-cols-5;
|
||||
}
|
||||
|
||||
.finance-metric-card {
|
||||
border-color: rgba(203, 213, 225, 0.88);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.88));
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xs);
|
||||
@apply border p-4;
|
||||
}
|
||||
|
||||
.finance-metric-card__label {
|
||||
color: var(--color-text-tertiary);
|
||||
@apply text-xs font-semibold;
|
||||
}
|
||||
|
||||
.finance-metric-card__value {
|
||||
color: var(--color-text-primary);
|
||||
@apply mt-2 text-2xl font-bold;
|
||||
}
|
||||
|
||||
.finance-workspace-grid {
|
||||
@apply grid gap-5 xl:grid-cols-[340px,minmax(0,1fr)];
|
||||
}
|
||||
|
||||
.finance-command-panel {
|
||||
border-color: rgba(203, 213, 225, 0.92);
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xs);
|
||||
@apply border p-4 md:p-5;
|
||||
}
|
||||
|
||||
.finance-list-card {
|
||||
border-color: rgba(226, 232, 240, 0.94);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-radius: var(--radius-sm);
|
||||
@apply w-full border px-3 py-3 text-left transition-colors hover:border-slate-300 hover:bg-slate-50/60;
|
||||
}
|
||||
|
||||
.finance-list-card--active {
|
||||
border-color: rgba(0, 57, 138, 0.34);
|
||||
background: rgba(0, 57, 138, 0.055);
|
||||
}
|
||||
|
||||
.admin-command-panel {
|
||||
border-color: rgba(203, 213, 225, 0.92);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.9));
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xs);
|
||||
@apply border p-4 md:p-5;
|
||||
}
|
||||
|
||||
.admin-tile-grid {
|
||||
@apply grid gap-4 md:grid-cols-2 xl:grid-cols-3;
|
||||
}
|
||||
|
||||
.admin-tile-card {
|
||||
border-color: rgba(226, 232, 240, 0.94);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xs);
|
||||
@apply border p-4 transition-colors hover:border-slate-300 hover:bg-slate-50/60 md:p-5;
|
||||
}
|
||||
|
||||
.admin-workspace-grid {
|
||||
@apply grid gap-5 xl:grid-cols-[220px,280px,minmax(0,1fr)];
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
@apply inline-flex items-center rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700 ring-1 ring-inset ring-emerald-600/15;
|
||||
}
|
||||
|
||||
@@ -248,9 +248,9 @@ function SummaryCard({
|
||||
: "text-zinc-900";
|
||||
|
||||
return (
|
||||
<div className="card-std p-5">
|
||||
<div className="text-sm text-zinc-500">{title}</div>
|
||||
<div className={`mt-2 text-2xl font-bold ${toneClass}`}>{value}</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">{title}</div>
|
||||
<div className={`finance-metric-card__value ${toneClass}`}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -618,7 +618,7 @@ export function ApOperationsPage() {
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="财务付款与对账"
|
||||
subtitle="待付款、异常付款、风险和对账。"
|
||||
subtitle="待付款、异常付款、风险、对账和台账集中处理。"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
@@ -661,7 +661,7 @@ export function ApOperationsPage() {
|
||||
<SectionError message={resolveErrorMessage(financeLedgerQuery.error, "财务台账加载失败")} />
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-5">
|
||||
<div className="finance-status-strip" data-testid="ap-operations-summary">
|
||||
<SummaryCard title="待付款" value={summaryQuery.data?.summary.pendingPaymentCount ?? "-"} />
|
||||
<SummaryCard title="异常" value={summaryQuery.data?.summary.exceptionCount ?? "-"} tone="red" />
|
||||
<SummaryCard
|
||||
@@ -677,9 +677,9 @@ export function ApOperationsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card-std p-4 md:p-5">
|
||||
<div className="finance-command-panel">
|
||||
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as OperationsTab)}>
|
||||
<TabsList className="flex w-full flex-wrap justify-start gap-1">
|
||||
<TabsList className="ops-command-bar__tabs !w-full !justify-start">
|
||||
<TabsTrigger value="pending">待付款 {pendingBatches.length}</TabsTrigger>
|
||||
<TabsTrigger value="failed">异常付款 {failedItems.length}</TabsTrigger>
|
||||
<TabsTrigger value="risk">风险标记 {filteredRiskFlags.length}</TabsTrigger>
|
||||
@@ -688,7 +688,7 @@ export function ApOperationsPage() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="ledger" className="mt-6 space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-6">
|
||||
<div className="finance-command-panel grid gap-3 !p-3 md:grid-cols-6">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-zinc-700">审批状态</div>
|
||||
<select
|
||||
@@ -791,7 +791,7 @@ export function ApOperationsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200" data-testid="finance-ledger-table">
|
||||
<div className="ops-table-focus overflow-x-auto" data-testid="finance-ledger-table">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -901,7 +901,7 @@ export function ApOperationsPage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pending" className="mt-6 space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<div className="finance-command-panel grid gap-3 !p-3 md:grid-cols-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-zinc-700">实体类型</div>
|
||||
<select
|
||||
@@ -943,7 +943,7 @@ export function ApOperationsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<div className="ops-table-focus overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -1046,7 +1046,7 @@ export function ApOperationsPage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="failed" className="mt-6 space-y-4">
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<div className="ops-table-focus overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -1170,7 +1170,7 @@ export function ApOperationsPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<div className="ops-table-focus overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -1248,7 +1248,7 @@ export function ApOperationsPage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="reconciliation" className="mt-6 space-y-4">
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<div className="ops-table-focus overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<tr>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { isApiError } from "../api/types";
|
||||
import { exportAuditLogs } from "../api/exports";
|
||||
import { fetchAuditLogs, type AuditLogListResponse } from "../api/audit";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
|
||||
import { downloadBlob } from "../lib/download";
|
||||
@@ -68,10 +68,10 @@ export function AuditLogsPage() {
|
||||
}, [actionFilter, page, targetTypeFilter]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="审计工作台"
|
||||
subtitle="只读查看系统关键操作审计记录。"
|
||||
subtitle="关键操作审计只读查询与导出。"
|
||||
actions={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -135,7 +135,15 @@ export function AuditLogsPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="card-std p-6 space-y-4">
|
||||
<div className="finance-status-strip" data-testid="audit-summary">
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">记录总数</div><div className="finance-metric-card__value">{data?.total ?? 0}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">当前页</div><div className="finance-metric-card__value">{page}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">Action 过滤</div><div className="finance-metric-card__value text-base">{actionFilter || "全部"}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">Target 过滤</div><div className="finance-metric-card__value text-base">{targetTypeFilter || "全部"}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">模式</div><div className="finance-metric-card__value text-base">只读</div></div>
|
||||
</div>
|
||||
|
||||
<section className="admin-command-panel space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<label className="text-sm text-zinc-700">
|
||||
Action
|
||||
@@ -177,7 +185,7 @@ export function AuditLogsPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="overflow-x-auto rounded-lg border border-zinc-200">
|
||||
<div className="ops-table-focus overflow-x-auto">
|
||||
<table className="min-w-full text-left text-sm">
|
||||
<thead className="bg-zinc-50 text-zinc-600">
|
||||
<tr>
|
||||
@@ -251,6 +259,6 @@ export function AuditLogsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,10 @@ import {
|
||||
type FeatureFlagSnapshot,
|
||||
} from "../api/featureFlags";
|
||||
import { isApiError } from "../api/types";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { WorkspaceNotice } from "../components/patterns/ListWorkspace";
|
||||
import { ListWorkspace, WorkspaceHeader, WorkspaceNotice } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -261,6 +260,13 @@ export function FeatureFlagsPage() {
|
||||
}, [departmentsQuery.data?.items]);
|
||||
|
||||
const items = flagsQuery.data?.items ?? [];
|
||||
const flagStats = {
|
||||
total: items.length,
|
||||
effectiveOn: items.filter((item) => item.effectiveEnabled).length,
|
||||
forcedOn: items.filter((item) => item.overrideMode === "force_on").length,
|
||||
forcedOff: items.filter((item) => item.overrideMode === "force_off").length,
|
||||
allowlist: items.filter((item) => item.rolloutType === "department_allowlist").length,
|
||||
};
|
||||
const selectedItems = useMemo(
|
||||
() => items.filter((item) => selectedFlagKeys.includes(item.flagKey)),
|
||||
[items, selectedFlagKeys],
|
||||
@@ -316,10 +322,10 @@ export function FeatureFlagsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={batchMode ? "space-y-6 pb-32" : "space-y-6"}>
|
||||
<PageHeader
|
||||
<ListWorkspace className={batchMode ? "pb-32" : undefined}>
|
||||
<WorkspaceHeader
|
||||
title="功能开关"
|
||||
subtitle="外部桥接、验真和灰度开关。"
|
||||
subtitle="外部桥接、验真、灰度和试运行边界。"
|
||||
/>
|
||||
<WorkspaceNotice title="安全摘要" tone="warning">
|
||||
Env 仍是硬闸;批量开启、关闭或灰度变更会影响试运行边界,执行前必须保留变更说明。
|
||||
@@ -329,6 +335,14 @@ export function FeatureFlagsPage() {
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
|
||||
) : null}
|
||||
|
||||
<div className="finance-status-strip" data-testid="feature-flags-summary">
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">开关总数</div><div className="finance-metric-card__value">{flagStats.total}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">实际开启</div><div className="finance-metric-card__value">{flagStats.effectiveOn}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">强制开启</div><div className="finance-metric-card__value">{flagStats.forcedOn}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">强制关闭</div><div className="finance-metric-card__value">{flagStats.forcedOff}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">部门灰度</div><div className="finance-metric-card__value">{flagStats.allowlist}</div></div>
|
||||
</div>
|
||||
|
||||
<SectionPanel
|
||||
title="开关列表"
|
||||
description="单条编辑或批量切换,必须填写变更说明。"
|
||||
@@ -351,7 +365,7 @@ export function FeatureFlagsPage() {
|
||||
) : null}
|
||||
|
||||
{!flagsQuery.isLoading && !flagsQuery.isError ? (
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<div className="ops-table-focus overflow-x-auto">
|
||||
<Table data-testid="feature-flags-table">
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -831,6 +845,6 @@ export function FeatureFlagsPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
@@ -436,6 +436,16 @@ export function InvoicePoolPage() {
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
||||
const detail = detailQuery.data;
|
||||
const invoiceStats = useMemo(() => {
|
||||
const items = listQuery.data?.items ?? [];
|
||||
return {
|
||||
total: listQuery.data?.total ?? 0,
|
||||
pending: items.filter((item) => item.status === "pending_ocr").length,
|
||||
failed: items.filter((item) => item.status === "ocr_failed").length,
|
||||
available: items.filter((item) => item.status === "available").length,
|
||||
bound: items.filter((item) => item.status === "bound").length,
|
||||
};
|
||||
}, [listQuery.data?.items, listQuery.data?.total]);
|
||||
const canEditDetail = Boolean(
|
||||
detail &&
|
||||
(canManageInvoice ||
|
||||
@@ -449,7 +459,7 @@ export function InvoicePoolPage() {
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="票据中心"
|
||||
subtitle="上传、识别、绑定。"
|
||||
subtitle="上传、识别、绑定和异常复核集中处理。"
|
||||
actions={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -480,6 +490,29 @@ export function InvoicePoolPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="finance-status-strip" data-testid="invoice-pool-summary">
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">票据总数</div>
|
||||
<div className="finance-metric-card__value">{invoiceStats.total}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">识别中</div>
|
||||
<div className="finance-metric-card__value">{invoiceStats.pending}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">识别失败</div>
|
||||
<div className="finance-metric-card__value">{invoiceStats.failed}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">可绑定</div>
|
||||
<div className="finance-metric-card__value">{invoiceStats.available}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">已绑定</div>
|
||||
<div className="finance-metric-card__value">{invoiceStats.bound}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InvoiceUploadCard
|
||||
selectedFiles={selectedFiles}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
|
||||
@@ -169,6 +169,15 @@ export function MasterDataPage() {
|
||||
const legalEntities = legalEntitiesQuery.data?.items ?? [];
|
||||
const costCenters = costCentersQuery.data?.items ?? [];
|
||||
const documentCategories = documentCategoriesQuery.data?.items ?? [];
|
||||
const masterDataStats = {
|
||||
legalEntities: legalEntities.length,
|
||||
costCenters: costCenters.length,
|
||||
documentCategories: documentCategories.length,
|
||||
archived:
|
||||
legalEntities.filter((item) => item.archivedAt).length +
|
||||
costCenters.filter((item) => item.archivedAt).length +
|
||||
documentCategories.filter((item) => item.archivedAt).length,
|
||||
};
|
||||
|
||||
const availableParentCostCenters = useMemo(() => {
|
||||
return costCenters.filter(
|
||||
@@ -315,8 +324,16 @@ export function MasterDataPage() {
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
|
||||
) : 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.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>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">当前视图</div><div className="finance-metric-card__value text-base">{includeArchived ? "含归档" : "仅启用"}</div></div>
|
||||
</div>
|
||||
|
||||
<Tabs value={tab} onValueChange={(value) => setTab(value as typeof tab)}>
|
||||
<TabsList className="flex w-full flex-wrap justify-start gap-1">
|
||||
<TabsList className="ops-command-bar__tabs !w-full !justify-start">
|
||||
<TabsTrigger value="legal-entities">法体</TabsTrigger>
|
||||
<TabsTrigger value="cost-centers">成本中心</TabsTrigger>
|
||||
<TabsTrigger value="document-categories">单据分类</TabsTrigger>
|
||||
@@ -325,7 +342,7 @@ export function MasterDataPage() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="legal-entities" className="space-y-6">
|
||||
<FilterBar 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>
|
||||
<select
|
||||
@@ -341,7 +358,7 @@ export function MasterDataPage() {
|
||||
</FilterBar>
|
||||
|
||||
{canManage ? (
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建法体</h3>
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="space-y-1">
|
||||
@@ -377,11 +394,11 @@ export function MasterDataPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<h3 className="text-base font-semibold text-zinc-900">法体列表</h3>
|
||||
{legalEntitiesQuery.isLoading ? <TableSkeleton columns={6} rows={4} /> : null}
|
||||
{!legalEntitiesQuery.isLoading ? (
|
||||
<PrimaryTable>
|
||||
<PrimaryTable className="ops-table-focus">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -438,7 +455,7 @@ export function MasterDataPage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="cost-centers" className="space-y-6">
|
||||
<FilterBar className="space-y-4">
|
||||
<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<select
|
||||
className="select-std"
|
||||
@@ -467,7 +484,7 @@ export function MasterDataPage() {
|
||||
</FilterBar>
|
||||
|
||||
{canManage ? (
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<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">
|
||||
@@ -534,11 +551,11 @@ export function MasterDataPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<h3 className="text-base font-semibold text-zinc-900">成本中心列表</h3>
|
||||
{costCentersQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
|
||||
{!costCentersQuery.isLoading ? (
|
||||
<PrimaryTable>
|
||||
<PrimaryTable className="ops-table-focus">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -597,7 +614,7 @@ export function MasterDataPage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="document-categories" className="space-y-6">
|
||||
<FilterBar className="space-y-4">
|
||||
<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<select
|
||||
className="select-std"
|
||||
@@ -626,7 +643,7 @@ export function MasterDataPage() {
|
||||
</FilterBar>
|
||||
|
||||
{canManage ? (
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建单据分类</h3>
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
@@ -697,10 +714,11 @@ export function MasterDataPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<h3 className="text-base font-semibold text-zinc-900">单据分类列表</h3>
|
||||
{documentCategoriesQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
|
||||
{!documentCategoriesQuery.isLoading ? (
|
||||
<PrimaryTable className="ops-table-focus">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -757,6 +775,7 @@ export function MasterDataPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PrimaryTable>
|
||||
) : null}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type NotificationChannelWriteConfig,
|
||||
} from "../api/notificationChannels";
|
||||
import { isApiError } from "../api/types";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
@@ -325,14 +325,22 @@ export function NotificationChannelsPage() {
|
||||
}
|
||||
|
||||
const channels = channelsQuery.data?.items ?? [];
|
||||
const deliveryItems = deliveriesQuery.data?.items ?? [];
|
||||
const channelStats = {
|
||||
total: channels.length,
|
||||
enabled: channels.filter((item) => item.enabled).length,
|
||||
appMessage: channels.filter((item) => item.configJson.mode === "wecom_app_message").length,
|
||||
failedDeliveries: deliveryItems.filter((item) => item.status === "failed").length,
|
||||
pendingDeliveries: deliveryItems.filter((item) => item.status === "pending").length,
|
||||
};
|
||||
const isWebhookMode = form.mode === "wecom_webhook";
|
||||
const isAppMessageMode = form.mode === "wecom_app_message";
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="通知渠道"
|
||||
subtitle="维护企微通知渠道、业务域路由、测试发送与最近投递记录。目标业务通知会在提交后同步尝试外推。"
|
||||
subtitle="渠道、路由、测试发送和投递追踪集中治理。"
|
||||
actions={
|
||||
<Button type="button" variant="secondary" onClick={startCreate} data-testid="notification-channels-create">
|
||||
新建渠道
|
||||
@@ -344,7 +352,15 @@ export function NotificationChannelsPage() {
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
|
||||
) : null}
|
||||
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="finance-status-strip" data-testid="notification-channels-summary">
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">渠道总数</div><div className="finance-metric-card__value">{channelStats.total}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">启用渠道</div><div className="finance-metric-card__value">{channelStats.enabled}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">应用消息</div><div className="finance-metric-card__value">{channelStats.appMessage}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">失败投递</div><div className="finance-metric-card__value">{channelStats.failedDeliveries}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">处理中</div><div className="finance-metric-card__value">{channelStats.pendingDeliveries}</div></div>
|
||||
</div>
|
||||
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-base font-semibold text-zinc-900">渠道列表</h3>
|
||||
<p className="text-sm text-zinc-500">当前共 {channels.length} 条渠道配置</p>
|
||||
@@ -359,7 +375,7 @@ export function NotificationChannelsPage() {
|
||||
) : null}
|
||||
|
||||
{!channelsQuery.isLoading && !channelsQuery.isError ? (
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<div className="ops-table-focus overflow-x-auto">
|
||||
<Table data-testid="notification-channels-table">
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -428,7 +444,7 @@ export function NotificationChannelsPage() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">最近投递记录</h3>
|
||||
@@ -474,7 +490,7 @@ export function NotificationChannelsPage() {
|
||||
) : null}
|
||||
|
||||
{!deliveriesQuery.isLoading && !deliveriesQuery.isError ? (
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<div className="ops-table-focus overflow-x-auto">
|
||||
<Table data-testid="notification-deliveries-table">
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -520,7 +536,7 @@ export function NotificationChannelsPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]">
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">
|
||||
@@ -818,7 +834,7 @@ export function NotificationChannelsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">测试发送</h3>
|
||||
<p className="text-sm text-zinc-500">仅对已保存渠道执行同步测试,不写业务 Outbox。</p>
|
||||
@@ -866,6 +882,6 @@ export function NotificationChannelsPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,10 +123,18 @@ export function PaymentBatchesPage() {
|
||||
});
|
||||
|
||||
const selectedBatchId = searchParams.get("id") ?? "";
|
||||
const batches = useMemo(() => listQuery.data?.items ?? [], [listQuery.data?.items]);
|
||||
const selectedBatch = useMemo<PaymentBatch | null>(() => {
|
||||
const items = listQuery.data?.items ?? [];
|
||||
return items.find((item) => item.id === selectedBatchId) ?? items[0] ?? null;
|
||||
}, [listQuery.data?.items, selectedBatchId]);
|
||||
return batches.find((item) => item.id === selectedBatchId) ?? batches[0] ?? null;
|
||||
}, [batches, selectedBatchId]);
|
||||
const batchStats = useMemo(() => {
|
||||
return {
|
||||
total: batches.length,
|
||||
actionable: batches.filter((item) => ["draft", "reviewed", "failed"].includes(item.status)).length,
|
||||
highRisk: batches.filter((item) => item.highRisk).length,
|
||||
failed: batches.filter((item) => item.status === "failed").length,
|
||||
};
|
||||
}, [batches]);
|
||||
|
||||
const receiptAttachmentsQuery = useQuery({
|
||||
queryKey: ["payment-batch-receipts", selectedBatch?.id],
|
||||
@@ -332,12 +340,35 @@ export function PaymentBatchesPage() {
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="付款批次" subtitle="线下银行付款后的 OA 复核、回单留存和已付款标记;高风险批次达到 5 万元后需完成二次复核。" />
|
||||
<WorkspaceHeader title="付款批次" subtitle="复核、回单和已付款标记集中处理。" />
|
||||
|
||||
{error ? <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</div> : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[360px,1fr]">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="finance-status-strip" data-testid="payment-batches-summary">
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">批次数</div>
|
||||
<div className="finance-metric-card__value">{batchStats.total}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">可处理</div>
|
||||
<div className="finance-metric-card__value">{batchStats.actionable}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">高风险</div>
|
||||
<div className="finance-metric-card__value">{batchStats.highRisk}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">失败批次</div>
|
||||
<div className="finance-metric-card__value">{batchStats.failed}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">当前批次</div>
|
||||
<div className="finance-metric-card__value text-base">{selectedBatch?.batchNo ?? "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="finance-workspace-grid">
|
||||
<div className="finance-command-panel space-y-4">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建批次</h3>
|
||||
{!canManageBatch ? (
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
|
||||
@@ -411,15 +442,15 @@ export function PaymentBatchesPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="card-std p-4 md:p-5">
|
||||
<div className="space-y-5">
|
||||
<div className="finance-command-panel">
|
||||
<h3 className="mb-3 text-base font-semibold text-zinc-900">批次列表</h3>
|
||||
<div className="space-y-2">
|
||||
{(listQuery.data?.items ?? []).map((item) => (
|
||||
{batches.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={selectedBatch?.id === item.id ? "w-full rounded-lg border border-primary bg-primary/5 px-3 py-3 text-left" : "w-full rounded-lg border border-zinc-200 px-3 py-3 text-left hover:bg-zinc-50"}
|
||||
className={selectedBatch?.id === item.id ? "finance-list-card finance-list-card--active" : "finance-list-card"}
|
||||
onClick={() => {
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.set("id", item.id);
|
||||
@@ -443,8 +474,8 @@ export function PaymentBatchesPage() {
|
||||
</div>
|
||||
|
||||
{selectedBatch ? (
|
||||
<div className="space-y-6">
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<div className="space-y-5">
|
||||
<div className="finance-command-panel space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">{selectedBatch.batchNo}</div>
|
||||
@@ -499,13 +530,13 @@ export function PaymentBatchesPage() {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 text-sm md:grid-cols-3 xl:grid-cols-6">
|
||||
<div>状态:{batchStatusLabel(selectedBatch.status)}</div>
|
||||
<div>实体:{entityTypeLabel(selectedBatch.entityType)}</div>
|
||||
<div>总金额:{selectedBatch.totalAmount}</div>
|
||||
<div>默认日期:{formatDisplayDate(selectedBatch.defaultPaymentDate)}</div>
|
||||
<div>默认渠道:{paymentChannelLabel(selectedBatch.defaultPaymentChannel)}</div>
|
||||
<div>复核进度:{selectedBatch.reviewRequirement.completedReviewCount}/{selectedBatch.reviewRequirement.requiredReviewCount}</div>
|
||||
<div className="detail-fact-grid text-sm xl:grid-cols-6">
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">状态</div><div className="detail-fact-card__value">{batchStatusLabel(selectedBatch.status)}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">实体</div><div className="detail-fact-card__value">{entityTypeLabel(selectedBatch.entityType)}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">总金额</div><div className="detail-fact-card__value">{selectedBatch.totalAmount}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">默认日期</div><div className="detail-fact-card__value">{formatDisplayDate(selectedBatch.defaultPaymentDate)}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">默认渠道</div><div className="detail-fact-card__value">{paymentChannelLabel(selectedBatch.defaultPaymentChannel)}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">复核进度</div><div className="detail-fact-card__value">{selectedBatch.reviewRequirement.completedReviewCount}/{selectedBatch.reviewRequirement.requiredReviewCount}</div></div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-3 text-sm text-zinc-600">
|
||||
当前要求:{selectedBatch.reviewRequirement.requiredReviewCount === 2 ? `批次金额达到 ${selectedBatch.reviewRequirement.thresholdAmount},需两轮复核` : "单轮复核后可执行"}。
|
||||
@@ -515,7 +546,7 @@ export function PaymentBatchesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="finance-command-panel space-y-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">银行回单附件</h3>
|
||||
@@ -562,7 +593,7 @@ export function PaymentBatchesPage() {
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
{(receiptAttachmentsQuery.data?.items ?? []).map((attachment) => (
|
||||
<div key={attachment.id} className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-3">
|
||||
<div key={attachment.id} className="detail-evidence-card flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium text-zinc-900">{attachment.originalName}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
@@ -602,11 +633,11 @@ export function PaymentBatchesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="finance-command-panel space-y-4">
|
||||
<h3 className="text-base font-semibold text-zinc-900">批次项</h3>
|
||||
<div className="space-y-4">
|
||||
{selectedBatch.items.map((item) => (
|
||||
<div key={item.id} className="rounded-lg border border-zinc-200 p-4">
|
||||
<div key={item.id} className="detail-evidence-card">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-medium text-zinc-900">{item.entityCode}</div>
|
||||
|
||||
@@ -15,10 +15,17 @@ const ITEMS = [
|
||||
export function SystemSettingsPage() {
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="系统设置" subtitle="小团队模式下的后台设置总入口。" />
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<WorkspaceHeader title="系统设置" subtitle="后台能力入口按管理职责收拢。" />
|
||||
<div className="finance-status-strip" data-testid="system-settings-summary">
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">入口总数</div><div className="finance-metric-card__value">{ITEMS.length}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">业务配置</div><div className="finance-metric-card__value">5</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">外部桥接</div><div className="finance-metric-card__value">2</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">人员相关</div><div className="finance-metric-card__value">1</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">模式</div><div className="finance-metric-card__value text-base">小团队</div></div>
|
||||
</div>
|
||||
<div className="admin-tile-grid">
|
||||
{ITEMS.map((item) => (
|
||||
<Link key={item.to} to={item.to} className="card-std p-4 hover:bg-zinc-50 md:p-5">
|
||||
<Link key={item.to} to={item.to} className="admin-tile-card">
|
||||
<div className="text-base font-semibold text-zinc-900">{item.title}</div>
|
||||
<div className="mt-2 text-sm text-zinc-600">{item.description}</div>
|
||||
</Link>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type UserStatus,
|
||||
type UserSummary,
|
||||
} from "../api/users";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
@@ -76,6 +76,13 @@ export function UserManagementPage() {
|
||||
});
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
||||
const users = listQuery.data?.items ?? [];
|
||||
const userStats = {
|
||||
total: listQuery.data?.total ?? 0,
|
||||
active: users.filter((item) => item.status === "active").length,
|
||||
archived: users.filter((item) => item.status === "archived").length,
|
||||
roles: new Set(users.flatMap((item) => item.roleCodes)).size,
|
||||
};
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (payload: CreateUserFormData) => createUser(payload),
|
||||
@@ -150,10 +157,10 @@ export function UserManagementPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="系统用户管理"
|
||||
subtitle="创建/启用/禁用账号,系统自动生成工号并展示一次性口令,管理员只负责最小开通与角色分配。"
|
||||
subtitle="账号开通、停用、角色和一次性口令集中处理。"
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
<UserImportDialog
|
||||
@@ -213,8 +220,16 @@ export function UserManagementPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="finance-status-strip" data-testid="users-summary">
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">用户总数</div><div className="finance-metric-card__value">{userStats.total}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">当前页生效</div><div className="finance-metric-card__value">{userStats.active}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">当前页停用</div><div className="finance-metric-card__value">{userStats.archived}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">当前页角色</div><div className="finance-metric-card__value">{userStats.roles}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">当前页</div><div className="finance-metric-card__value">{page}</div></div>
|
||||
</div>
|
||||
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<div className="ops-command-bar__row">
|
||||
<Input
|
||||
data-testid="users-search"
|
||||
value={q}
|
||||
@@ -252,7 +267,7 @@ export function UserManagementPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<div className="ops-table-focus overflow-x-auto">
|
||||
<Table data-testid="users-table">
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -264,8 +279,8 @@ export function UserManagementPage() {
|
||||
</tr>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(listQuery.data?.items ?? []).length ? (
|
||||
listQuery.data?.items.map((item) => (
|
||||
{users.length ? (
|
||||
users.map((item) => (
|
||||
<TableRow data-testid="users-row" key={item.id}>
|
||||
<TableCell>
|
||||
<div className="font-medium text-zinc-900">
|
||||
@@ -352,6 +367,6 @@ export function UserManagementPage() {
|
||||
}}
|
||||
isPending={rolesMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,13 @@ export function WorkflowDefinitionsPage() {
|
||||
const items = listQuery.data?.items ?? [];
|
||||
const scopedItems = useMemo(() => items.filter((item) => item.entityType === entityType), [items, entityType]);
|
||||
const selected = useMemo(() => scopedItems.find((item) => item.id === selectedId) ?? scopedItems[0] ?? null, [scopedItems, selectedId]);
|
||||
const workflowStats = {
|
||||
total: items.length,
|
||||
scoped: scopedItems.length,
|
||||
draft: items.filter((item) => item.status === "draft").length,
|
||||
published: items.filter((item) => item.status === "published").length,
|
||||
archived: items.filter((item) => item.status === "archived").length,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) return;
|
||||
@@ -86,9 +93,16 @@ export function WorkflowDefinitionsPage() {
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="流程定义" subtitle="线性流程的版本化治理;报销继续由审批规则投影。" />
|
||||
<div className="grid gap-6 xl:grid-cols-[220px,280px,1fr]">
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<WorkspaceHeader title="流程定义" subtitle="线性流程版本治理,发布动作需确认。" />
|
||||
<div className="finance-status-strip" data-testid="workflow-definitions-summary">
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">定义总数</div><div className="finance-metric-card__value">{workflowStats.total}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">当前业务域</div><div className="finance-metric-card__value">{workflowStats.scoped}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">草稿</div><div className="finance-metric-card__value">{workflowStats.draft}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">已发布</div><div className="finance-metric-card__value">{workflowStats.published}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">已归档</div><div className="finance-metric-card__value">{workflowStats.archived}</div></div>
|
||||
</div>
|
||||
<div className="admin-workspace-grid">
|
||||
<div className="admin-command-panel space-y-3">
|
||||
<Label>业务域</Label>
|
||||
<select className="select-std" value={entityType} onChange={(event) => setEntityType(event.target.value as WorkflowDefinitionEditorModel["entityType"])}>
|
||||
{ENTITY_TYPES.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
@@ -96,11 +110,11 @@ export function WorkflowDefinitionsPage() {
|
||||
{entityType !== "reimbursement" ? <Button type="button" onClick={() => createMutation.mutate()}>创建草稿</Button> : null}
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<div className="admin-command-panel space-y-3">
|
||||
<h3 className="text-base font-semibold text-zinc-900">版本列表</h3>
|
||||
<div className="space-y-2">
|
||||
{scopedItems.map((item) => (
|
||||
<button key={item.id} type="button" className={selected?.id === item.id ? "w-full rounded-lg border border-primary bg-primary/5 px-3 py-3 text-left" : "w-full rounded-lg border border-zinc-200 px-3 py-3 text-left hover:bg-zinc-50"} onClick={() => setSelectedId(item.id)}>
|
||||
<button key={item.id} type="button" className={selected?.id === item.id ? "finance-list-card finance-list-card--active" : "finance-list-card"} onClick={() => setSelectedId(item.id)}>
|
||||
<div className="font-medium text-zinc-900">v{item.version} · {item.name}</div>
|
||||
<div className="text-xs text-zinc-500">{item.status} · {item.source}</div>
|
||||
</button>
|
||||
@@ -109,7 +123,7 @@ export function WorkflowDefinitionsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="admin-command-panel space-y-4">
|
||||
{selected ? (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -157,7 +171,7 @@ export function WorkflowDefinitionsPage() {
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="rounded-lg border border-zinc-200 p-4">
|
||||
<div key={step.id} className="detail-evidence-card">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<Input value={step.id} disabled={selected.status !== "draft"} onChange={(event) => setSteps((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, id: event.target.value } : item))} />
|
||||
<Input value={step.name} disabled={selected.status !== "draft"} onChange={(event) => setSteps((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, name: event.target.value } : item))} />
|
||||
|
||||
在新工单中引用
屏蔽一个用户