R94 A2/A3/A4 移动端列表卡片化
这个提交包含在:
@@ -570,6 +570,13 @@ export function AssetRegistryPage() {
|
|||||||
return items;
|
return items;
|
||||||
}, [approvedPurchasesQuery.data?.items, currentAsset?.purchaseRequest]);
|
}, [approvedPurchasesQuery.data?.items, currentAsset?.purchaseRequest]);
|
||||||
|
|
||||||
|
const categoryItems = categoriesQuery.data?.items ?? [];
|
||||||
|
const assetItems = assetsQuery.data?.items ?? [];
|
||||||
|
const selectAsset = (assetId: string) => {
|
||||||
|
setIsCreatingAsset(false);
|
||||||
|
setSelectedAssetId(assetId);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -636,7 +643,33 @@ export function AssetRegistryPage() {
|
|||||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{resolveError(categoriesQuery.error)}</p>
|
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{resolveError(categoriesQuery.error)}</p>
|
||||||
) : null}
|
) : null}
|
||||||
{!categoriesQuery.isLoading && !categoriesQuery.isError ? (
|
{!categoriesQuery.isLoading && !categoriesQuery.isError ? (
|
||||||
<div className="rounded-lg border border-zinc-200">
|
<>
|
||||||
|
<div className="space-y-2 md:hidden">
|
||||||
|
{categoryItems.length ? (
|
||||||
|
categoryItems.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
className="w-full rounded-lg border border-zinc-200 bg-white p-3 text-left"
|
||||||
|
onClick={() => resetCategoryEditor(item)}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<span className="shrink-0 rounded-full bg-zinc-100 px-2 py-1 text-xs text-zinc-600">
|
||||||
|
{item.enabled ? "启用" : "停用"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-zinc-500">{item.documentCategory?.name ?? "系统默认"}</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-zinc-200 p-3 text-sm text-zinc-500">暂无资产分类</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="hidden rounded-lg border border-zinc-200 md:block">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -646,7 +679,7 @@ export function AssetRegistryPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(categoriesQuery.data?.items ?? []).map((item) => (
|
{categoryItems.map((item) => (
|
||||||
<TableRow key={item.id} onClick={() => resetCategoryEditor(item)} className="cursor-pointer">
|
<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">{item.code}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -658,7 +691,8 @@ export function AssetRegistryPage() {
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -724,7 +758,45 @@ export function AssetRegistryPage() {
|
|||||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{resolveError(assetsQuery.error)}</p>
|
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{resolveError(assetsQuery.error)}</p>
|
||||||
) : null}
|
) : null}
|
||||||
{!assetsQuery.isLoading && !assetsQuery.isError ? (
|
{!assetsQuery.isLoading && !assetsQuery.isError ? (
|
||||||
<div className="rounded-lg border border-zinc-200">
|
<>
|
||||||
|
<div className="space-y-3 md:hidden">
|
||||||
|
{assetItems.length ? (
|
||||||
|
assetItems.map((item) => (
|
||||||
|
<button
|
||||||
|
data-testid="asset-mobile-row"
|
||||||
|
type="button"
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => selectAsset(item.id)}
|
||||||
|
className={`w-full rounded-lg border p-3 text-left ${
|
||||||
|
item.id === selectedAssetId && !isCreatingAsset
|
||||||
|
? "border-zinc-400 bg-zinc-50"
|
||||||
|
: "border-zinc-200 bg-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<div>
|
||||||
|
<div className="text-zinc-400">归属人</div>
|
||||||
|
<div className="mt-0.5 truncate text-zinc-700">{item.assignee?.name ?? "-"}</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>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -735,11 +807,10 @@ export function AssetRegistryPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(assetsQuery.data?.items ?? []).length ? (
|
{assetItems.length ? (
|
||||||
assetsQuery.data?.items.map((item) => (
|
assetItems.map((item) => (
|
||||||
<TableRow data-testid="asset-row" key={item.id} onClick={() => {
|
<TableRow data-testid="asset-row" key={item.id} onClick={() => {
|
||||||
setIsCreatingAsset(false);
|
selectAsset(item.id);
|
||||||
setSelectedAssetId(item.id);
|
|
||||||
}} className={item.id === selectedAssetId && !isCreatingAsset ? "bg-zinc-50" : "cursor-pointer"}>
|
}} className={item.id === selectedAssetId && !isCreatingAsset ? "bg-zinc-50" : "cursor-pointer"}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-zinc-900">{item.assetNo}</div>
|
<div className="font-medium text-zinc-900">{item.assetNo}</div>
|
||||||
@@ -759,7 +830,8 @@ export function AssetRegistryPage() {
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export function NotificationsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
||||||
|
const notificationItems = listQuery.data?.items ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -172,7 +173,51 @@ export function NotificationsPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!listQuery.isLoading && !listQuery.isError ? (
|
{!listQuery.isLoading && !listQuery.isError ? (
|
||||||
<div className="rounded-lg border border-zinc-200">
|
<>
|
||||||
|
<div className="space-y-3 md:hidden">
|
||||||
|
{notificationItems.length ? (
|
||||||
|
notificationItems.map((item) => {
|
||||||
|
const targetLink = resolveNotificationTargetLink(item);
|
||||||
|
return (
|
||||||
|
<div data-testid="notification-mobile-card" key={item.id} className="rounded-lg border border-zinc-200 bg-white p-4">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm font-semibold text-zinc-900">{item.title}</div>
|
||||||
|
{item.body ? <div className="mt-1 text-xs leading-5 text-zinc-500">{item.body}</div> : null}
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 rounded-full bg-zinc-100 px-2 py-1 text-xs text-zinc-600">
|
||||||
|
{item.readAt ? "已读" : "未读"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-xs text-zinc-500">{item.createdAt.slice(0, 19).replace("T", " ")}</div>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{targetLink ? (
|
||||||
|
<Button asChild size="sm" variant="secondary" data-testid="notification-mobile-target-link">
|
||||||
|
<Link to={targetLink}>查看</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!item.readAt ? (
|
||||||
|
<Button
|
||||||
|
data-testid="notification-mobile-mark-read"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
disabled={markReadMutation.isPending}
|
||||||
|
onClick={() => markReadMutation.mutate(item.id)}
|
||||||
|
>
|
||||||
|
标为已读
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</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 data-testid="notifications-table">
|
<Table data-testid="notifications-table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -183,8 +228,8 @@ export function NotificationsPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(listQuery.data?.items ?? []).length ? (
|
{notificationItems.length ? (
|
||||||
listQuery.data?.items.map((item) => {
|
notificationItems.map((item) => {
|
||||||
const targetLink = resolveNotificationTargetLink(item);
|
const targetLink = resolveNotificationTargetLink(item);
|
||||||
return (
|
return (
|
||||||
<TableRow data-testid="notification-row" key={item.id}>
|
<TableRow data-testid="notification-row" key={item.id}>
|
||||||
@@ -234,7 +279,8 @@ export function NotificationsPage() {
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -380,6 +380,7 @@ export function PurchaseRequestsPage() {
|
|||||||
}, [costCentersQuery.data?.items, legalEntityFilter]);
|
}, [costCentersQuery.data?.items, legalEntityFilter]);
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
||||||
|
const purchaseItems = listQuery.data?.items ?? [];
|
||||||
const totalAmountPreview = useMemo(
|
const totalAmountPreview = useMemo(
|
||||||
() => items.reduce((sum, item) => sum + Number(moneyPreview(item.quantity, item.unitPrice)), 0).toFixed(2),
|
() => items.reduce((sum, item) => sum + Number(moneyPreview(item.quantity, item.unitPrice)), 0).toFixed(2),
|
||||||
[items],
|
[items],
|
||||||
@@ -833,7 +834,48 @@ export function PurchaseRequestsPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!listQuery.isLoading && !listQuery.isError ? (
|
{!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">
|
<Table data-testid="purchase-requests-table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -844,8 +886,8 @@ export function PurchaseRequestsPage() {
|
|||||||
</tr>
|
</tr>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(listQuery.data?.items ?? []).length ? (
|
{purchaseItems.length ? (
|
||||||
listQuery.data?.items.map((item) => (
|
purchaseItems.map((item) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={item.id === selectedId ? "bg-zinc-50" : undefined}
|
className={item.id === selectedId ? "bg-zinc-50" : undefined}
|
||||||
@@ -872,7 +914,8 @@ export function PurchaseRequestsPage() {
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
@@ -1027,7 +1070,112 @@ export function PurchaseRequestsPage() {
|
|||||||
新增明细
|
新增明细
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1154,7 +1302,45 @@ export function PurchaseRequestsPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户