修复验收图片多选暂存
This commit is contained in:
@@ -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)
|
||||
: []
|
||||
|
||||
Reference in New Issue
Block a user