R94 A2/A3/A4 移动端列表卡片化

这个提交包含在:
2026-05-06 00:30:28 +08:00
父节点 79799018ab
当前提交 e4e47fea3b
共修改 3 个文件,包含 323 行新增和 19 行删除
+192 -6
查看文件
@@ -380,6 +380,7 @@ export function PurchaseRequestsPage() {
}, [costCentersQuery.data?.items, legalEntityFilter]);
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
const purchaseItems = listQuery.data?.items ?? [];
const totalAmountPreview = useMemo(
() => items.reduce((sum, item) => sum + Number(moneyPreview(item.quantity, item.unitPrice)), 0).toFixed(2),
[items],
@@ -833,7 +834,48 @@ export function PurchaseRequestsPage() {
) : null}
{!listQuery.isLoading && !listQuery.isError ? (
<div className="rounded-lg border border-zinc-200">
<>
<div className="space-y-3 md:hidden">
{purchaseItems.length ? (
purchaseItems.map((item) => (
<button
data-testid="purchase-request-mobile-row"
key={item.id}
type="button"
className={`w-full rounded-lg border p-3 text-left ${
item.id === selectedId ? "border-zinc-400 bg-zinc-50" : "border-zinc-200 bg-white"
}`}
onClick={() => {
setSelectedId(item.id);
setHydratedDetailId("");
}}
>
<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.requestNo || item.title || item.id}
</div>
<div className="mt-1 truncate text-xs text-zinc-500">{item.title || "未命名采购申请"}</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.applicant?.name ?? "-"}</div>
</div>
<div>
<div className="text-zinc-400">金额</div>
<div className="mt-0.5 font-medium text-zinc-900">{item.totalAmount}</div>
</div>
</div>
</button>
))
) : (
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-500">暂无采购申请</div>
)}
</div>
<div className="hidden rounded-lg border border-zinc-200 md:block">
<Table data-testid="purchase-requests-table">
<TableHeader>
<tr>
@@ -844,8 +886,8 @@ export function PurchaseRequestsPage() {
</tr>
</TableHeader>
<TableBody>
{(listQuery.data?.items ?? []).length ? (
listQuery.data?.items.map((item) => (
{purchaseItems.length ? (
purchaseItems.map((item) => (
<TableRow
key={item.id}
className={item.id === selectedId ? "bg-zinc-50" : undefined}
@@ -872,7 +914,8 @@ export function PurchaseRequestsPage() {
)}
</TableBody>
</Table>
</div>
</div>
</>
) : null}
<Pagination
@@ -1027,7 +1070,112 @@ export function PurchaseRequestsPage() {
新增明细
</Button>
</div>
<div className="rounded-lg border border-zinc-200">
<div className="space-y-3 md:hidden">
{items.length ? (
items.map((item, index) => (
<div key={item.id} className="rounded-lg border border-zinc-200 bg-white p-4">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="text-sm font-semibold text-zinc-900">明细 {index + 1}</div>
<div className="text-sm font-medium text-zinc-900">{moneyPreview(item.quantity, item.unitPrice)}</div>
</div>
<div className="space-y-3">
<div className="space-y-1.5">
<label className="text-xs text-zinc-500">描述</label>
<Input
data-testid={`purchase-item-description-mobile-${item.id}`}
value={item.description}
disabled={!canEditDraft}
onChange={(event) =>
setItems((current) =>
current.map((entry) =>
entry.id === item.id ? { ...entry, description: event.target.value } : entry,
),
)
}
/>
</div>
<div className="space-y-1.5">
<label className="text-xs text-zinc-500">规格</label>
<Input
value={item.specification}
disabled={!canEditDraft}
onChange={(event) =>
setItems((current) =>
current.map((entry) =>
entry.id === item.id ? { ...entry, specification: event.target.value } : entry,
),
)
}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-xs text-zinc-500">数量</label>
<Input
data-testid={`purchase-item-quantity-mobile-${item.id}`}
value={item.quantity}
disabled={!canEditDraft}
onChange={(event) =>
setItems((current) =>
current.map((entry) =>
entry.id === item.id ? { ...entry, quantity: event.target.value } : entry,
),
)
}
/>
</div>
<div className="space-y-1.5">
<label className="text-xs text-zinc-500">单价</label>
<Input
data-testid={`purchase-item-unit-price-mobile-${item.id}`}
value={item.unitPrice}
disabled={!canEditDraft}
onChange={(event) =>
setItems((current) =>
current.map((entry) =>
entry.id === item.id ? { ...entry, unitPrice: event.target.value } : entry,
),
)
}
/>
</div>
</div>
<div className="flex items-center justify-between gap-3">
<label className="flex items-center gap-2 text-sm text-zinc-700">
<input
type="checkbox"
checked={item.isAssetCandidate}
disabled={!canEditDraft}
onChange={(event) =>
setItems((current) =>
current.map((entry) =>
entry.id === item.id ? { ...entry, isAssetCandidate: event.target.checked } : entry,
),
)
}
/>
资产候选
</label>
<Button
type="button"
size="sm"
variant="secondary"
disabled={!canEditDraft}
onClick={() => setItems((current) => current.filter((entry) => entry.id !== item.id))}
>
删除
</Button>
</div>
</div>
</div>
))
) : (
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-500">
暂无明细,请先新增至少一条采购项。
</div>
)}
</div>
<div className="hidden rounded-lg border border-zinc-200 md:block">
<Table>
<TableHeader>
<tr>
@@ -1154,7 +1302,45 @@ export function PurchaseRequestsPage() {
}}
/>
</div>
<div className="rounded-lg border border-zinc-200">
<div className="space-y-3 md:hidden">
{detail.attachments.length ? (
detail.attachments.map((attachment) => (
<div key={attachment.id} className="rounded-lg border border-zinc-200 bg-white p-4">
<div className="font-medium text-zinc-900">{attachment.originalName}</div>
<div className="mt-1 text-xs text-zinc-500">{attachment.mimeType} / {attachment.fileSize} bytes</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">{attachment.uploader?.name ?? "-"}</div>
</div>
<div>
<div className="text-zinc-400">时间</div>
<div className="mt-0.5 text-zinc-700">
{attachment.createdAt ? new Date(attachment.createdAt).toLocaleString() : "-"}
</div>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<Button type="button" size="sm" variant="secondary" onClick={() => void handleDownloadAttachment(attachment.id)}>
下载
</Button>
<Button
type="button"
size="sm"
variant="secondary"
disabled={!canEditDraft || deleteAttachmentMutation.isPending}
onClick={() => deleteAttachmentMutation.mutate(attachment.id)}
>
删除
</Button>
</div>
</div>
))
) : (
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-500">暂无附件</div>
)}
</div>
<div className="hidden rounded-lg border border-zinc-200 md:block">
<Table>
<TableHeader>
<tr>