统一菜单命名并移除剩余台账工具(05-31-unified-menu-remove-ledger-tools-wave2)

这个提交包含在:
2026-05-31 15:06:26 +08:00
父节点 46fa609961
当前提交 43efc9ea08
共修改 47 个文件,包含 498 行新增和 358 行删除
+81 -76
查看文件
@@ -216,7 +216,7 @@ export function ContractsRegistryPage() {
const [editor, setEditor] = useState<EditorState>(() => buildEmptyEditor(initialProjectId));
const [showExpiring, setShowExpiring] = useState(false);
const [showTrialData, setShowTrialData] = useState(false);
const [showLedgerTools, setShowLedgerTools] = useState(false);
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const [showEditor, setShowEditor] = useState(false);
const [confirmExportFormat, setConfirmExportFormat] = useState<ContractRegistryExportFormat | null>(null);
const [confirmStatusTarget, setConfirmStatusTarget] = useState<ContractRegistryItem | null>(null);
@@ -293,9 +293,9 @@ export function ContractsRegistryPage() {
await refreshAll();
setShowEditor(false);
resetEditor();
toast.success(editorId ? "合同台账已更新" : "合同台账已创建");
toast.success(editorId ? "合同已更新" : "合同已创建");
},
onError: (error) => toast.error(resolveError(error, editorId ? "更新合同台账失败" : "创建合同台账失败")),
onError: (error) => toast.error(resolveError(error, editorId ? "更新合同失败" : "创建合同失败")),
});
const toggleStatusMutation = useMutation({
@@ -324,15 +324,15 @@ export function ContractsRegistryPage() {
}),
onSuccess: (response, format) => {
downloadBlob(response.blob, response.filename ?? `contracts-registry.${format}`);
toast.success(`合同台账已导出为 ${format.toUpperCase()}`);
toast.success(`合同已导出为 ${format.toUpperCase()}`);
},
onError: (error) => toast.error(resolveError(error, "导出合同台账失败")),
onError: (error) => toast.error(resolveError(error, "导出合同失败")),
});
return (
<PageScaffold>
<PageHeader
title="合同台账"
title="合同"
actions={
<div className="flex flex-wrap gap-2">
{initialProjectId ? (
@@ -349,73 +349,58 @@ export function ContractsRegistryPage() {
>
新增合同
</Button>
<Button type="button" variant={showLedgerTools ? "default" : "secondary"} onClick={() => setShowLedgerTools((value) => !value)}>
台账工具
<Button
type="button"
variant={showExpiring ? "default" : "secondary"}
disabled={expiringQuery.isLoading}
onClick={() => setShowExpiring((value) => !value)}
>
到期列表 {expiringContractCount}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" variant="secondary" disabled={exportMutation.isPending}>
{exportMutation.isPending ? "导出中..." : "导出"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => setConfirmExportFormat("csv")}>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setConfirmExportFormat("xlsx")}>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant={showAdvancedFilters || filters.expiryDateFrom || filters.expiryDateTo || showTrialData ? "default" : "secondary"}
onClick={() => setShowAdvancedFilters((value) => !value)}
>
筛选
</Button>
</div>
}
/>
{showLedgerTools ? (
<div className="detail-command-panel space-y-4" data-testid="contracts-ledger-tools">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<h3 className="text-base font-semibold text-zinc-900">台账工具</h3>
</div>
<div className="flex shrink-0 flex-wrap gap-2">
<Button
type="button"
variant="secondary"
disabled={expiringQuery.isLoading}
onClick={() => setShowExpiring((value) => !value)}
>
到期列表 {expiringContractCount}
</Button>
<Button type="button" variant="secondary" disabled={exportMutation.isPending} onClick={() => setConfirmExportFormat("csv")}>
{exportMutation.isPending ? "导出中..." : "导出 CSV"}
</Button>
<Button type="button" variant="secondary" disabled={exportMutation.isPending} onClick={() => setConfirmExportFormat("xlsx")}>
{exportMutation.isPending ? "导出中..." : "导出 Excel"}
{expiringQuery.isError ? (
<div className="text-sm text-red-600">{resolveError(expiringQuery.error, "加载到期预警失败")}</div>
) : null}
{showExpiring ? (
<div className="detail-command-panel space-y-2" data-testid="contracts-expiring-list">
{expiringItems.map((item) => (
<div key={item.id} className="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm">
<div className="min-w-0">
<div className="truncate font-medium text-zinc-900">{item.contractNo} · {item.counterpartyName}</div>
<div className="text-xs text-zinc-500">到期日 {formatDisplayDate(item.expiryDate)} · 责任人 {item.owner?.name ?? "-"}</div>
</div>
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
编辑
</Button>
</div>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="space-y-2">
<Label>到期日开始</Label>
<DateTextInput
value={filters.expiryDateFrom}
onChange={(event) => setFilters((current) => ({ ...current, expiryDateFrom: event.target.value }))}
/>
</div>
<div className="space-y-2">
<Label>到期日结束</Label>
<DateTextInput
value={filters.expiryDateTo}
onChange={(event) => setFilters((current) => ({ ...current, expiryDateTo: event.target.value }))}
/>
</div>
</div>
{expiringQuery.isError ? (
<div className="text-sm text-red-600">{resolveError(expiringQuery.error, "加载到期预警失败")}</div>
) : null}
{showExpiring ? (
<div className="space-y-2 border-t border-zinc-200 pt-3">
{expiringItems.map((item) => (
<div key={item.id} className="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm">
<div className="min-w-0">
<div className="truncate font-medium text-zinc-900">{item.contractNo} · {item.counterpartyName}</div>
<div className="text-xs text-zinc-500">到期日 {formatDisplayDate(item.expiryDate)} · 责任人 {item.owner?.name ?? "-"}</div>
</div>
<Button type="button" size="sm" variant="secondary" onClick={() => openEditor(item)}>
编辑
</Button>
</div>
))}
{!expiringQuery.isLoading && !expiringItems.length ? (
<div className="text-sm text-zinc-500">暂无到期合同</div>
) : null}
</div>
))}
{!expiringQuery.isLoading && !expiringItems.length ? (
<div className="text-sm text-zinc-500">暂无到期合同</div>
) : null}
</div>
) : null}
@@ -425,7 +410,7 @@ export function ContractsRegistryPage() {
<div className="card-std space-y-4 p-6" data-testid="contracts-editor-panel">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-zinc-900">{editorId ? "编辑合同台账" : "新建合同台账"}</h3>
<h3 className="text-base font-semibold text-zinc-900">{editorId ? "编辑合同" : "新建合同"}</h3>
<p className="mt-1 text-sm text-zinc-500">
补齐合同类型、金额、签署/生效/到期日期,续约将自动创建新的草稿合同。
</p>
@@ -676,7 +661,7 @@ export function ContractsRegistryPage() {
}
onClick={() => saveMutation.mutate()}
>
{saveMutation.isPending ? (editorId ? "保存中..." : "创建中...") : editorId ? "保存修改" : "创建合同台账"}
{saveMutation.isPending ? (editorId ? "保存中..." : "创建中...") : editorId ? "保存修改" : "创建合同"}
</Button>
</div>
) : null}
@@ -704,13 +689,33 @@ export function ContractsRegistryPage() {
>
重置筛选
</Button>
{showLedgerTools ? (
</>
}
advanced={showAdvancedFilters || filters.expiryDateFrom || filters.expiryDateTo || showTrialData ? (
<div className="grid gap-3 md:grid-cols-[160px_160px_auto] xl:grid-cols-[180px_180px_auto]" data-testid="contracts-advanced-filters">
<div className="space-y-2">
<Label>到期日开始</Label>
<DateTextInput
value={filters.expiryDateFrom}
onChange={(event) => setFilters((current) => ({ ...current, expiryDateFrom: event.target.value }))}
/>
</div>
<div className="space-y-2">
<Label>到期日结束</Label>
<DateTextInput
value={filters.expiryDateTo}
onChange={(event) => setFilters((current) => ({ ...current, expiryDateTo: event.target.value }))}
/>
</div>
<div className="flex items-end">
<Button type="button" variant={showTrialData ? "default" : "secondary"} onClick={() => setShowTrialData((value) => !value)}>
{showTrialData ? "隐藏测试数据" : "显示测试数据"}
</Button>
) : null}
</>
}
</div>
</div>
) : null}
>
<div className="grid gap-3 xl:grid-cols-[minmax(180px,1fr)_160px_160px_minmax(220px,1fr)]">
<div>
@@ -774,7 +779,7 @@ export function ContractsRegistryPage() {
{contractsQuery.isLoading ? <div className="text-sm text-zinc-500">加载中...</div> : null}
{contractsQuery.isError ? (
<div className="text-sm text-red-600">{resolveError(contractsQuery.error, "加载合同台账失败")}</div>
<div className="text-sm text-red-600">{resolveError(contractsQuery.error, "加载合同失败")}</div>
) : null}
{!showTrialData && hiddenTrialContractCount > 0 ? (
@@ -804,7 +809,7 @@ export function ContractsRegistryPage() {
</article>
))}
{!contractsQuery.isLoading && !visibleContracts.length ? (
<div className="rounded-lg border border-zinc-200 bg-white p-4 text-sm text-zinc-500">暂无合同台账</div>
<div className="rounded-lg border border-zinc-200 bg-white p-4 text-sm text-zinc-500">暂无合同</div>
) : null}
</BusinessMobileList>
@@ -884,7 +889,7 @@ export function ContractsRegistryPage() {
) : (
<tr>
<td className="py-8 text-center text-sm text-zinc-500" colSpan={6}>
暂无合同台账
暂无合同
</td>
</tr>
)}
@@ -898,7 +903,7 @@ export function ContractsRegistryPage() {
if (!open) setConfirmExportFormat(null);
}}
kind="sensitiveExport"
targetName="合同台账"
targetName="合同"
description="导出文件可能包含合同金额、相对方、项目归属、责任人和归档位置摘要。请确认当前筛选范围和用途合规。"
onConfirm={() => {
if (!confirmExportFormat) return;