OAFRONT6 压缩长编号展示并修正视觉探针

这个提交包含在:
2026-05-25 19:58:23 +08:00
父节点 15db07a799
当前提交 971d5311e3
共修改 12 个文件,包含 249 行新增和 36 行删除
+74 -17
查看文件
@@ -38,7 +38,7 @@ import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { formatDisplayDateTime } from "../lib/displayFormat";
import { formatCompactIdentifier, formatDisplayDateTime, formatTextWithCompactLongNumbers, isCompactedIdentifier } from "../lib/displayFormat";
function resolveError(error: unknown) {
if (isApiError(error)) {
@@ -156,6 +156,41 @@ function typeHint(assetType: AssetType) {
}
}
function CompactAssetNo({ value, copyable = true }: { value: string; copyable?: boolean }) {
const displayValue = formatCompactIdentifier(value, { head: 14, tail: 6, minLength: 24 });
const compacted = isCompactedIdentifier(value, displayValue);
async function copyAssetNo() {
if (!compacted) return;
try {
await navigator.clipboard.writeText(value);
toast.success("已复制完整资产编号");
} catch {
toast.error("复制失败,可在资产详情中查看完整编号");
}
}
return (
<span className="inline-flex min-w-0 max-w-full items-center gap-1.5">
<span className="truncate" title={compacted ? value : undefined}>
{displayValue}
</span>
{compacted && copyable ? (
<button
className="shrink-0 rounded border border-zinc-200 px-1.5 py-0.5 text-[11px] font-medium text-zinc-500 hover:border-zinc-300 hover:text-zinc-700"
onClick={(event) => {
event.stopPropagation();
void copyAssetNo();
}}
type="button"
>
复制
</button>
) : null}
</span>
);
}
function unsafeAssetTextReason(value: string) {
if (!value.trim()) {
return "";
@@ -796,7 +831,7 @@ export function AssetRegistryPage() {
<option value="">使用系统默认</option>
{(documentCategoriesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.code})
{formatTextWithCompactLongNumbers(item.name)} ({formatTextWithCompactLongNumbers(item.code)})
</option>
))}
</select>
@@ -829,8 +864,12 @@ export function AssetRegistryPage() {
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{item.name}</div>
<div className="mt-1 text-xs text-zinc-500">{item.code}</div>
<div className="truncate text-sm font-semibold text-zinc-900" title={item.name}>
{formatTextWithCompactLongNumbers(item.name)}
</div>
<div className="mt-1 text-xs text-zinc-500" title={item.code}>
{formatTextWithCompactLongNumbers(item.code)}
</div>
</div>
<span className="shrink-0 rounded-full bg-zinc-100 px-2 py-1 text-xs text-zinc-600">
{item.enabled ? "启用" : "停用"}
@@ -855,9 +894,11 @@ export function AssetRegistryPage() {
<TableBody>
{categoryItems.map((item) => (
<TableRow key={item.id} onClick={() => resetCategoryEditor(item)} className="cursor-pointer">
<TableCell className="font-medium text-zinc-900">{item.code}</TableCell>
<TableCell className="font-medium text-zinc-900" title={item.code}>
{formatTextWithCompactLongNumbers(item.code)}
</TableCell>
<TableCell>
<div>{item.name}</div>
<div title={item.name}>{formatTextWithCompactLongNumbers(item.name)}</div>
<div className="text-xs text-zinc-500">{item.documentCategory?.name ?? "系统默认"}</div>
</TableCell>
<TableCell>{item.enabled ? "启用" : "停用"}</TableCell>
@@ -903,7 +944,7 @@ export function AssetRegistryPage() {
<option value="">全部分类</option>
{(categoriesQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name}
{formatTextWithCompactLongNumbers(item.name)}
</option>
))}
</select>
@@ -968,15 +1009,21 @@ export function AssetRegistryPage() {
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{item.assetNo}</div>
<div className="mt-1 truncate text-xs text-zinc-500">{item.name}</div>
<div className="truncate text-sm font-semibold text-zinc-900">
<CompactAssetNo copyable={false} value={item.assetNo} />
</div>
<div className="mt-1 truncate text-xs text-zinc-500" title={item.name}>
{formatTextWithCompactLongNumbers(item.name)}
</div>
</div>
<StatusBadge status={item.status} />
</div>
<div className="mt-3 grid grid-cols-2 gap-2 text-xs text-zinc-600">
<div>
<div className="text-zinc-400">分类</div>
<div className="mt-0.5 truncate text-zinc-700">{item.category?.name ?? "-"}</div>
<div className="mt-0.5 truncate text-zinc-700" title={item.category?.name ?? undefined}>
{formatTextWithCompactLongNumbers(item.category?.name ?? "-")}
</div>
</div>
<div>
<div className="text-zinc-400">类型</div>
@@ -1012,11 +1059,17 @@ export function AssetRegistryPage() {
selectAsset(item.id);
}} className={item.id === selectedAssetId && !isCreatingAsset ? "bg-zinc-50" : "cursor-pointer"}>
<TableCell>
<div className="font-medium text-zinc-900">{item.assetNo}</div>
<div className="text-xs text-zinc-500">{item.name}</div>
<div className="font-medium text-zinc-900">
<CompactAssetNo value={item.assetNo} />
</div>
<div className="text-xs text-zinc-500" title={item.name}>
{formatTextWithCompactLongNumbers(item.name)}
</div>
</TableCell>
<TableCell>{assetTypeLabel(item.assetType)}</TableCell>
<TableCell>{item.category?.name ?? "-"}</TableCell>
<TableCell title={item.category?.name ?? undefined}>
{formatTextWithCompactLongNumbers(item.category?.name ?? "-")}
</TableCell>
<TableCell>{sensitivityLabel(item.sensitivityLevel)}</TableCell>
<TableCell><StatusBadge status={item.status} /></TableCell>
<TableCell>{item.owner?.name ?? item.assignee?.name ?? "-"}</TableCell>
@@ -1052,8 +1105,12 @@ export function AssetRegistryPage() {
<>
<div className="flex items-center justify-between gap-3">
<div>
<h3 className="text-lg font-semibold text-zinc-900">{isCreatingAsset ? "新建资产卡片" : currentAsset?.assetNo ?? "资产卡片"}</h3>
<p className="text-sm text-zinc-500">{isCreatingAsset ? "创建新的资产台账卡片。" : currentAsset?.name}</p>
<h3 className="text-lg font-semibold text-zinc-900">
{isCreatingAsset ? "新建资产卡片" : currentAsset ? <CompactAssetNo value={currentAsset.assetNo} /> : "资产卡片"}
</h3>
<p className="text-sm text-zinc-500" title={currentAsset?.name ?? undefined}>
{isCreatingAsset ? "创建新的资产台账卡片。" : formatTextWithCompactLongNumbers(currentAsset?.name)}
</p>
</div>
{!isCreatingAsset && currentAsset ? <StatusBadge status={currentAsset.status} /> : null}
</div>
@@ -1379,9 +1436,9 @@ export function AssetRegistryPage() {
</div>
<div className="grid gap-2 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600 md:grid-cols-2">
<div>资产编号:{assetErpTrack?.asset.assetNo ?? currentAsset?.assetNo ?? "-"}</div>
<div>资产编号:{formatTextWithCompactLongNumbers(assetErpTrack?.asset.assetNo ?? currentAsset?.assetNo ?? "-")}</div>
<div>最近变更:{assetErpChangeTypeLabel(assetErpStage.preview.changeType)}</div>
<div>分类:{assetErpStage.preview.categoryCode ?? "-"}</div>
<div>分类:{formatTextWithCompactLongNumbers(assetErpStage.preview.categoryCode ?? "-")}</div>
<div>资产状态:{assetStatusLabel(assetErpStage.preview.status ?? assetErpTrack?.asset.status)}</div>
<div>法体:{assetErpStage.preview.legalEntityCode ?? "-"}</div>
<div>成本中心:{assetErpStage.preview.costCenterCode ?? "-"}</div>
+55 -8
查看文件
@@ -49,7 +49,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
import { Textarea } from "../components/ui/textarea";
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { formatDisplayDateTime } from "../lib/displayFormat";
import { formatCompactIdentifier, formatDisplayDateTime, formatTextWithCompactLongNumbers, isCompactedIdentifier } from "../lib/displayFormat";
import { buildPrecheckDialogMessage } from "../lib/precheck";
import {
formatApprovalHistoryTime,
@@ -112,6 +112,41 @@ function statusOptions(): Array<{ value: PurchaseRequestStatus | ""; label: stri
];
}
function CompactBusinessCode({ value, copyable = true }: { value: string; copyable?: boolean }) {
const displayValue = formatCompactIdentifier(value, { head: 12, tail: 4, minLength: 18 });
const compacted = isCompactedIdentifier(value, displayValue);
async function copyBusinessCode() {
if (!compacted) return;
try {
await navigator.clipboard.writeText(value);
toast.success("已复制完整单据编号");
} catch {
toast.error("复制失败,可在详情页查看完整编号");
}
}
return (
<span className="inline-flex min-w-0 max-w-full items-center gap-1.5">
<span className="truncate" title={compacted ? value : undefined}>
{displayValue}
</span>
{compacted && copyable ? (
<button
className="shrink-0 rounded border border-zinc-200 px-1.5 py-0.5 text-[11px] font-medium text-zinc-500 hover:border-zinc-300 hover:text-zinc-700"
onClick={(event) => {
event.stopPropagation();
void copyBusinessCode();
}}
type="button"
>
复制
</button>
) : null}
</span>
);
}
type PurchaseErpStageKey = keyof PurchaseRequestErpTrack["stages"];
const PURCHASE_ERP_STAGE_META: Record<
@@ -933,9 +968,11 @@ export function PurchaseRequestsPage() {
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">
{resolvePurchaseDisplayCode(item)}
<CompactBusinessCode copyable={false} value={resolvePurchaseDisplayCode(item)} />
</div>
<div className="mt-1 truncate text-xs text-zinc-500" title={item.title || undefined}>
{formatTextWithCompactLongNumbers(item.title || "未命名采购申请")}
</div>
<div className="mt-1 truncate text-xs text-zinc-500">{item.title || "未命名采购申请"}</div>
</div>
<StatusBadge status={item.status} />
</div>
@@ -951,7 +988,9 @@ export function PurchaseRequestsPage() {
<div className="col-span-2">
<div className="text-zinc-400">关联项目</div>
<div className="mt-0.5 truncate text-zinc-700">
{item.project ? `${item.project.projectNo} · ${item.project.name}` : "未关联项目"}
{item.project
? `${formatTextWithCompactLongNumbers(item.project.projectNo)} · ${item.project.name}`
: "未关联项目"}
</div>
</div>
</div>
@@ -984,14 +1023,20 @@ export function PurchaseRequestsPage() {
}}
>
<TableCell className="cursor-pointer">
<div className="font-medium text-zinc-900">{resolvePurchaseDisplayCode(item)}</div>
<div className="text-xs text-zinc-500">{item.title || "未命名采购申请"}</div>
<div className="font-medium text-zinc-900">
<CompactBusinessCode value={resolvePurchaseDisplayCode(item)} />
</div>
<div className="text-xs text-zinc-500" title={item.title || undefined}>
{formatTextWithCompactLongNumbers(item.title || "未命名采购申请")}
</div>
</TableCell>
<TableCell>{item.applicant?.name ?? "-"}</TableCell>
<TableCell>
{item.project ? (
<div>
<div className="font-medium text-zinc-900">{item.project.projectNo}</div>
<div className="font-medium text-zinc-900" title={item.project.projectNo}>
{formatTextWithCompactLongNumbers(item.project.projectNo)}
</div>
<div className="text-xs text-zinc-500">{item.project.name}</div>
</div>
) : (
@@ -1042,7 +1087,9 @@ export function PurchaseRequestsPage() {
<>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 className="text-lg font-semibold text-zinc-900">{resolvePurchaseDisplayCode(detail)}</h3>
<h3 className="text-lg font-semibold text-zinc-900">
<CompactBusinessCode value={resolvePurchaseDisplayCode(detail)} />
</h3>
<p className="text-sm text-zinc-500">申请人:{detail.applicant?.name ?? "-"}</p>
<p className="mt-1 text-sm text-zinc-500" data-testid="purchase-status-hint">
{resolvePurchaseStatusHint(detail)}
@@ -7,8 +7,9 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Input } from "../../components/ui/input";
import { Textarea } from "../../components/ui/textarea";
import { DateTextInput } from "../../components/common/DateTextInput";
import { formatDisplayDate, formatDisplayDateTime } from "../../lib/displayFormat";
import { formatCompactIdentifier, formatDisplayDate, formatDisplayDateTime, isCompactedIdentifier } from "../../lib/displayFormat";
import { formatInvoiceOcrErrorMessage } from "../../lib/invoiceOcr";
import { toast } from "sonner";
export type InvoiceDetailPanelProps = {
detail: InvoicePoolDetail | undefined;
@@ -150,6 +151,8 @@ export function InvoiceDetailPanel({
const invoiceNo = detail
? [detail.invoiceCode, detail.invoiceNumber].filter(Boolean).join("-") || detail.originalName || "待识别"
: "";
const invoiceNoDisplay = formatCompactIdentifier(invoiceNo, { head: 12, tail: 4, minLength: 18 });
const invoiceNoCompacted = isCompactedIdentifier(invoiceNo, invoiceNoDisplay);
const bindingStatus = detail?.linkedReimbursementId
? "已绑定报销单"
: detail?.linkedTravelOrderId
@@ -168,6 +171,15 @@ export function InvoiceDetailPanel({
setDuplicateOverrideRefFormNumber("");
setDuplicateOverrideReason("");
};
const copyInvoiceNo = async () => {
if (!invoiceNoCompacted) return;
try {
await navigator.clipboard.writeText(invoiceNo);
toast.success("已复制完整发票号码");
} catch {
toast.error("复制失败,可在编辑表单中查看完整号码");
}
};
return (
<>
@@ -178,7 +190,16 @@ export function InvoiceDetailPanel({
<div className="grid grid-cols-2 gap-y-3 pt-2 text-sm text-zinc-700">
<div className="col-span-2">
<span className="text-zinc-500">发票号码:</span>
{invoiceNo}
<span title={invoiceNoCompacted ? invoiceNo : undefined}>{invoiceNoDisplay}</span>
{invoiceNoCompacted ? (
<button
className="ml-2 rounded border border-zinc-200 px-1.5 py-0.5 text-[11px] font-medium text-zinc-500 hover:border-zinc-300 hover:text-zinc-700"
onClick={() => void copyInvoiceNo()}
type="button"
>
复制
</button>
) : null}
</div>
<div><span className="text-zinc-500">状态:</span>{invoiceStatusLabel(detail.status)}</div>
<div><span className="text-zinc-500">绑定状态:</span>{bindingStatus}</div>
@@ -5,8 +5,9 @@ import { TableSkeleton } from "../../components/common/TableSkeleton";
import { Pagination } from "../../components/common/Pagination";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../components/ui/dropdown-menu";
import { Button } from "../../components/ui/button";
import { formatDisplayDate } from "../../lib/displayFormat";
import { formatCompactIdentifier, formatDisplayDate, isCompactedIdentifier } from "../../lib/displayFormat";
import { formatInvoiceOcrErrorMessage } from "../../lib/invoiceOcr";
import { toast } from "sonner";
export type InvoiceListCardProps = {
listQuery: UseQueryResult<InvoicePoolListResponse, Error>;
@@ -32,6 +33,41 @@ function invoiceNumberDisplay(item: InvoicePoolListResponse["items"][number]) {
return [item.invoiceCode, item.invoiceNumber].filter(Boolean).join("-") || "-";
}
function CompactInvoiceNumber({ value }: { value: string }) {
const displayValue = formatCompactIdentifier(value, { head: 10, tail: 4, minLength: 16 });
const compacted = isCompactedIdentifier(value, displayValue);
async function copyInvoiceNumber() {
if (!compacted) return;
try {
await navigator.clipboard.writeText(value);
toast.success("已复制完整发票号码");
} catch {
toast.error("复制失败,可在票据详情中查看完整号码");
}
}
return (
<span className="inline-flex min-w-0 max-w-full items-center gap-1.5">
<span className="truncate" title={compacted ? value : undefined}>
{displayValue}
</span>
{compacted ? (
<button
className="shrink-0 rounded border border-zinc-200 px-1.5 py-0.5 text-[11px] font-medium text-zinc-500 hover:border-zinc-300 hover:text-zinc-700"
onClick={(event) => {
event.stopPropagation();
void copyInvoiceNumber();
}}
type="button"
>
复制
</button>
) : null}
</span>
);
}
export function InvoiceListCard({
listQuery,
status,
@@ -100,7 +136,7 @@ export function InvoiceListCard({
<div>
<div className="text-xs text-zinc-500">发票号码</div>
<div className="truncate font-medium text-zinc-900">
{invoiceNumberDisplay(item)}
<CompactInvoiceNumber value={invoiceNumberDisplay(item)} />
</div>
</div>
<div>
@@ -169,7 +205,7 @@ export function InvoiceListCard({
</td>
<td className="table-td"><StatusBadge status={item.status} /></td>
<td className="table-td font-medium text-zinc-900">
{invoiceNumberDisplay(item)}
<CompactInvoiceNumber value={invoiceNumberDisplay(item)} />
</td>
<td className="table-td">
{item.status === "pending_ocr" ? (