修复验收图片多选暂存

This commit is contained in:
yml2213
2026-08-15 14:49:48 +08:00
parent 190445319b
commit d433518038
5 changed files with 305 additions and 126 deletions
@@ -45,6 +45,7 @@ import type {
WorkerAcceptance,
} from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import { getImageIdentity } from '@/utils/image-identity'
const STATUS_OPTIONS = [
{ value: '', label: '全部订单' },
@@ -169,7 +170,7 @@ export default function WorkerOrdersPage() {
? getWorkerAcceptance(order)
: getWorkerDraft(order)
setAcceptanceFiles(getUniqueAcceptanceFiles(source))
form.setFieldsValue({ note: String(source.note || '') })
form.setFieldsValue({ note: String(source?.note || '') })
setDetailOrder(null)
}
@@ -263,38 +264,43 @@ export default function WorkerOrdersPage() {
const columns: TableColumnsType<WorkOrder> = [
{
title: '订单信息',
minWidth: 280,
width: 270,
render: (_, row) => (
<div className="cell-stack">
<Typography.Link onClick={() => setDetailOrder(row)}>
<Typography.Link
strong
onClick={() => setDetailOrder(row)}
title={row.productName || row.workOrderNo}
ellipsis
>
{row.productName || row.workOrderNo}
</Typography.Link>
<Space size={[6, 6]} wrap>
{row.categoryName ? <Tag>{row.categoryName}</Tag> : null}
<Space size={[4, 4]} wrap>
{row.categoryName ? <Tag color="blue">{row.categoryName}</Tag> : null}
{row.myShare ? <Tag color="purple"></Tag> : null}
</Space>
<Typography.Text type="secondary">
{row.workOrderNo} · {row.platformOrderId || '-'}
<Typography.Text type="secondary" style={{ fontSize: 12 }} ellipsis={{ tooltip: `${row.workOrderNo} / ${row.platformOrderId || '-'}` }}>
{row.workOrderNo}
</Typography.Text>
<Typography.Text type="secondary">
{formatDateTime(row.assignedAt)}
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatDateTime(row.assignedAt)}
</Typography.Text>
</div>
),
},
{
title: '状态',
width: 170,
width: 130,
render: (_, row) => (
<div className="cell-stack">
<Space wrap size={6}>
<Space wrap size={4}>
<Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>
{row.status === 'in_progress' && row.deadlineAt ? (
<DeadlineCountdown deadlineAt={row.deadlineAt} />
) : null}
</Space>
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
<Typography.Text type="danger" ellipsis>
<Typography.Text type="danger" ellipsis={{ tooltip: row.problemNote }} style={{ fontSize: 12 }}>
{row.problemNote}
</Typography.Text>
) : null}
@@ -303,16 +309,18 @@ export default function WorkerOrdersPage() {
},
{
title: '奖励 / 押金',
width: 150,
width: 140,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong>{formatMoney(row.rewardAmount)}</Typography.Text>
<Typography.Text type="secondary">
{formatMoney(row.freezeDepositAmount)}
<Typography.Text strong style={{ color: '#cf1322' }}>
{formatMoney(row.rewardAmount)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatMoney(row.freezeDepositAmount)}
</Typography.Text>
{row.myShare ? (
<Typography.Text type="secondary">
{row.myShare.quantity} · {formatMoney(row.myShare.shareReward)}
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.myShare.quantity} · {formatMoney(row.myShare.shareReward)}
</Typography.Text>
) : null}
</div>
@@ -320,40 +328,38 @@ export default function WorkerOrdersPage() {
},
{
title: '验收信息',
minWidth: 200,
width: 220,
render: (_, row) => {
const submittedAt = getAcceptanceSubmittedAt(row)
const hasAcceptance =
getAcceptanceFiles(row).length > 0 || getAcceptanceImageUrls(row).length > 0
const draftFiles = getDraftFiles(row)
const draftUrls = getDraftImageUrls(row)
const draftCount = draftFiles.length + draftUrls.length
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(row))
const hasAcceptance = acceptanceFiles.length > 0
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(row))
const draftCount = draftFiles.length
if (!hasAcceptance && !submittedAt && draftCount === 0) {
return <Typography.Text type="secondary"></Typography.Text>
return <Typography.Text type="secondary" style={{ fontSize: 12 }}></Typography.Text>
}
return (
<div className="cell-stack">
{hasAcceptance ? (
<ImagePreviewList
files={getAcceptanceFiles(row)}
imageUrls={getAcceptanceImageUrls(row)}
size={40}
/>
<ImagePreviewList files={acceptanceFiles} size={38} />
) : null}
{submittedAt ? (
<Typography.Text type="secondary">
{formatDateTime(submittedAt)}
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatDateTime(submittedAt)}
</Typography.Text>
) : null}
{draftCount > 0 && !hasSubmittedAcceptance(row) ? (
<>
<Tag color="blue"> {draftCount} </Tag>
<ImagePreviewList files={draftFiles} imageUrls={draftUrls} size={40} />
</>
<div className="acceptance-draft-badge-wrap">
<Tag color="processing" icon={<PictureOutlined />}>
{draftCount}
</Tag>
<ImagePreviewList files={draftFiles} size={38} />
</div>
) : null}
{getWorkerAcceptance(row).note ? (
<Typography.Text type="secondary" ellipsis>
{getWorkerAcceptance(row).note}
{getWorkerAcceptance(row)?.note ? (
<Typography.Text type="secondary" ellipsis={{ tooltip: getWorkerAcceptance(row)?.note }} style={{ fontSize: 12 }}>
{getWorkerAcceptance(row)?.note}
</Typography.Text>
) : null}
</div>
@@ -362,7 +368,7 @@ export default function WorkerOrdersPage() {
},
{
title: '快捷复制',
width: 280,
width: 250,
render: (_, row) => {
const items = getMaterialDetailItems(row).filter((item) =>
String(item.value || '').trim(),
@@ -372,7 +378,7 @@ export default function WorkerOrdersPage() {
<div className="cell-stack">
{items.length > 0 ? (
items.map((item) => (
<Typography.Text key={item.key} style={{ fontSize: 12 }} ellipsis>
<Typography.Text key={item.key} style={{ fontSize: 12 }} ellipsis={{ tooltip: `${item.label}: ${item.value}` }}>
{item.label}: {item.value}
</Typography.Text>
))
@@ -382,9 +388,9 @@ export default function WorkerOrdersPage() {
</Typography.Text>
)}
{screenshots.length > 0 ? (
<ImagePreviewList files={screenshots} size={36} />
<ImagePreviewList files={screenshots} size={34} />
) : null}
<Space wrap>
<Space wrap size={4}>
<Button
size="small"
icon={<CopyOutlined />}
@@ -409,27 +415,39 @@ export default function WorkerOrdersPage() {
},
{
title: '备注',
width: 190,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text
type={row.workerNote ? undefined : 'secondary'}
ellipsis={{ tooltip: row.workerNote || undefined }}
>
{row.workerNote || '未填写'}
</Typography.Text>
<Button size="small" icon={<EditOutlined />} onClick={() => openNoteModal(row)}>
</Button>
</div>
),
width: 180,
render: (_, row) => {
const note = String(row.workerNote || '').trim()
return (
<div className="worker-note-cell">
{note ? (
<div className="worker-note-badge" onClick={() => openNoteModal(row)}>
<span className="worker-note-text" title={note}>
{note}
</span>
<EditOutlined className="worker-note-edit-icon" />
</div>
) : (
<Button
type="dashed"
size="small"
icon={<EditOutlined />}
className="worker-note-add-btn"
onClick={() => openNoteModal(row)}
>
</Button>
)}
</div>
)
},
},
{
title: '操作',
width: 190,
width: 150,
render: (_, row) => (
<div className="worker-order-actions">
<Space size={8}>
<Space size={6}>
<Tooltip title="查看详情">
<Button
aria-label="查看详情"
@@ -659,43 +677,45 @@ export default function WorkerOrdersPage() {
<Card title="验收信息" size="small">
<div className="worker-order-detail-stack">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="验收图片数量">
{getAcceptanceFiles(detailOrder).length +
getAcceptanceImageUrls(detailOrder).length || 0}
</Descriptions.Item>
<Descriptions.Item label="提交时间">
{formatDateTime(getAcceptanceSubmittedAt(detailOrder))}
</Descriptions.Item>
<Descriptions.Item label="验收说明" span={2}>
<span className="worker-order-detail-value">
{String(getWorkerAcceptance(detailOrder).note || '').trim() || '-'}
</span>
</Descriptions.Item>
</Descriptions>
{getAcceptanceFiles(detailOrder).length ||
getAcceptanceImageUrls(detailOrder).length ? (
<ImagePreviewList
files={getAcceptanceFiles(detailOrder)}
imageUrls={getAcceptanceImageUrls(detailOrder)}
size={76}
/>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
{!hasSubmittedAcceptance(detailOrder) &&
(getDraftFiles(detailOrder).length || getDraftImageUrls(detailOrder).length) ? (
<>
<Typography.Text type="secondary">
{getDraftFiles(detailOrder).length + getDraftImageUrls(detailOrder).length}
</Typography.Text>
<ImagePreviewList
files={getDraftFiles(detailOrder)}
imageUrls={getDraftImageUrls(detailOrder)}
size={76}
/>
</>
) : null}
{(() => {
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(detailOrder))
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(detailOrder))
const hasAcceptance = acceptanceFiles.length > 0
const hasDraft = draftFiles.length > 0 && !hasSubmittedAcceptance(detailOrder)
return (
<>
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="验收图片数量">
{acceptanceFiles.length}
</Descriptions.Item>
<Descriptions.Item label="提交时间">
{formatDateTime(getAcceptanceSubmittedAt(detailOrder))}
</Descriptions.Item>
<Descriptions.Item label="验收说明" span={2}>
<span className="worker-order-detail-value">
{String(getWorkerAcceptance(detailOrder)?.note || '').trim() || '-'}
</span>
</Descriptions.Item>
</Descriptions>
{hasAcceptance ? (
<ImagePreviewList files={acceptanceFiles} size={76} />
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
{hasDraft ? (
<div className="acceptance-draft-badge-wrap" style={{ marginTop: 8 }}>
<Tag color="processing" icon={<PictureOutlined />}>
{draftFiles.length}
</Tag>
<ImagePreviewList files={draftFiles} size={76} />
</div>
) : null}
</>
)
})()}
</div>
</Card>
@@ -713,6 +733,7 @@ export default function WorkerOrdersPage() {
<Modal
title={`订单备注 · ${noteOrder?.workOrderNo || ''}`}
open={Boolean(noteOrder)}
width={460}
onCancel={() => {
setNoteOrder(null)
noteForm.resetFields()
@@ -721,10 +742,26 @@ export default function WorkerOrdersPage() {
destroyOnHidden
>
<Form form={noteForm} layout="vertical" onFinish={saveOrderNote}>
<Form.Item label="备注" name="note">
<AutoComplete options={[{ value: '已安排' }, { value: '未安排' }]}>
<Input allowClear maxLength={100} placeholder="输入备注或选择状态" />
</AutoComplete>
<Form.Item label="快捷选择标签" style={{ marginBottom: 12 }}>
<div className="worker-note-preset-tags">
{['已安排', '未安排', '打单中', '账号问题', '排队中', '暂缓做单'].map((tag) => (
<Tag
key={tag}
className="worker-note-preset-tag"
onClick={() => noteForm.setFieldsValue({ note: tag })}
>
{tag}
</Tag>
))}
</div>
</Form.Item>
<Form.Item label="备注内容" name="note">
<Input.TextArea
rows={3}
maxLength={100}
showCount
placeholder="请输入备注说明(如:已联系买家、账号异常中等)"
/>
</Form.Item>
</Form>
</Modal>
@@ -949,21 +986,25 @@ function renderDetailFieldValues(items: DetailFieldItem[], emptyText: string) {
}
/** 打手自己的验收资料(拼单时取份额自己的) */
function getWorkerAcceptance(order: WorkOrder): WorkerAcceptance {
function getWorkerAcceptance(order: WorkOrder | null | undefined): WorkerAcceptance | undefined {
if (!order) return undefined
return order.myShare ? order.myShare.acceptance : order.acceptance
}
/** 打手自己的暂存草稿(拼单时取份额自己的) */
function getWorkerDraft(order: WorkOrder): WorkerAcceptance {
function getWorkerDraft(order: WorkOrder | null | undefined): WorkerAcceptance | undefined {
if (!order) return undefined
return order.myShare?.draftAcceptance || order.draftAcceptance
}
function getAcceptanceFilesFromSource(source: WorkerAcceptance): UploadedFile[] {
return Array.isArray(source.files) ? source.files : []
function getAcceptanceFilesFromSource(source?: WorkerAcceptance | null): UploadedFile[] {
if (!source || !Array.isArray(source.files)) return []
return source.files
}
/** 合并草稿兼容字段,并按固定图片地址去重,避免同一图片重复回填。 */
function getUniqueAcceptanceFiles(source: WorkerAcceptance): UploadedFile[] {
function getUniqueAcceptanceFiles(source?: WorkerAcceptance | null): UploadedFile[] {
if (!source) return []
const files = [
...getAcceptanceFilesFromSource(source),
...(Array.isArray(source.imageUrls) ? source.imageUrls : []).map(
@@ -980,26 +1021,19 @@ function getUniqueAcceptanceFiles(source: WorkerAcceptance): UploadedFile[] {
]
const identities = new Set<string>()
return files.filter((file) => {
const identity = getImageIdentity(file.url || file.mediumUrl || file.thumbnailUrl || file.objectKey)
const identity = getImageIdentity(file)
if (!identity || identities.has(identity)) return false
identities.add(identity)
return true
})
}
/** 去除临时签名参数,确保同一个对象的不同访问链接能匹配。 */
function getImageIdentity(value: string): string {
const url = String(value || '').trim()
if (!url) return ''
return url.split(/[?#]/, 1)[0]
}
function getAcceptanceFiles(row: WorkOrder): UploadedFile[] {
return getAcceptanceFilesFromSource(getWorkerAcceptance(row))
}
function getAcceptanceImageUrls(row: WorkOrder): string[] {
const imageUrls = getWorkerAcceptance(row).imageUrls
const imageUrls = getWorkerAcceptance(row)?.imageUrls
return Array.isArray(imageUrls)
? imageUrls.map((item) => String(item || '').trim()).filter(Boolean)
: []
@@ -1010,7 +1044,7 @@ function getDraftFiles(row: WorkOrder): UploadedFile[] {
}
function getDraftImageUrls(row: WorkOrder): string[] {
const imageUrls = getWorkerDraft(row).imageUrls
const imageUrls = getWorkerDraft(row)?.imageUrls
return Array.isArray(imageUrls)
? imageUrls.map((item) => String(item || '').trim()).filter(Boolean)
: []