feat(collect): 补资料改为订单号直连并按状态一次性提交

- 公开补资料页 /supplement/:订单号 直接按平台订单号查询,不再依赖链接记录;旧 token 链接一律 404
- 仅「待完善 + 未指派 + 无拼单份额」可提交,提交后状态变为待分配,重复提交返回 409
- 非可编辑工单只返回商品、工单号、状态与客服提示,不返回已填资料和截图
- 管理端新增「重置补资料」:待分配且未指派、无份额、未发布时一键重置回待完善
- 新增 canResetMaterial 能力与 material_reset 流转事件,工单状态文案统一为「待分配」
This commit is contained in:
2026-11-01 18:12:44 +08:00
parent aed76e047e
commit a2cf48f5fc
24 changed files with 1223 additions and 494 deletions
@@ -78,6 +78,7 @@ import {
reduceAdminWorkOrderShareQuantity,
reopenAdminCancelledWorkOrder,
resetAdminWorkOrderGiftMaterial,
resetAdminWorkOrderMaterialStatus,
resolveAdminProblemWorkOrder,
submitAdminWorkOrderMaterial,
unpublishAdminWorkOrder,
@@ -106,6 +107,7 @@ import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import { getAdminUsername, hasAdminPermission, hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import { buildCollectMaterialLinkUrl } from '@/utils/collect-link'
import { isExplicitOrderNoSearch, normalizeSearchText } from '@/utils/admin-realtime-work-orders'
import {
DeadlineCountdown,
@@ -203,10 +205,11 @@ function getConsignmentFulfillment(row: WorkOrder): ConsignmentFulfillmentInfo |
return Object.keys(consignment).length > 0 ? (consignment as ConsignmentFulfillmentInfo) : null
}
/** 公开补资料链接只在「待完善 / 未分配」阶段生成;已进大厅或已接单不允许客户改资料。 */
/** 公开补资料链接只在「待完善 / 待分配」且有平台订单号时生成;已进大厅或已接单不允许客户改资料。 */
function canCopyMaterialLink(order: WorkOrder): boolean {
const status = String(order.rawStatus || order.status || '')
return status === 'pending_material' || status === 'unassigned'
const platformOrderId = String(order.platformOrderId || '').trim()
return (status === 'pending_material' || status === 'unassigned') && Boolean(platformOrderId)
}
/** 剪贴板 API 在 HTTP + IP 下可能不可用,补一个 textarea 兜底。 */
@@ -1491,7 +1494,7 @@ export default function WorkOrdersPanel() {
modal.confirm({
title: `确认删除订单 ${getDisplayOrderNo(row)}?`,
content:
'删除后工单将从列表中移除,且不可恢复。仅未分配、待完善或已取消且无拼单参与的工单可删除。',
'删除后工单将从列表中移除,且不可恢复。仅待分配、待完善或已取消且无拼单参与的工单可删除。',
okText: '确认删除',
okButtonProps: { danger: true },
cancelText: '取消',
@@ -1577,6 +1580,22 @@ export default function WorkOrdersPanel() {
})
}
function requestResetMaterial(row: WorkOrder) {
modal.confirm({
title: '重置补资料状态?',
content:
'将把该工单重置为“待完善”,客户可用订单号重新提交一次资料。已填资料会保留,客户重新提交后覆盖;此操作会记录在订单流转记录中。',
okText: '确认重置',
okButtonProps: { type: 'primary' },
cancelText: '取消',
onOk: () =>
runAction(
() => resetAdminWorkOrderMaterialStatus(row.workOrderId),
'已重置为待完善,客户刷新补资料页即可重新提交',
),
})
}
function copyBuyerMaterialInfo(order: WorkOrder) {
const items = getMaterialDetailItems(order).filter((item) => String(item.value || '').trim())
if (items.length === 0) {
@@ -1638,12 +1657,24 @@ export default function WorkOrdersPanel() {
setMaterialLinkLoadingId(order.workOrderId)
try {
const response = await createAdminWorkOrderMaterialLink(order.workOrderId)
const url = `${window.location.origin}/supplement/${encodeURIComponent(response.data.token)}`
// 新后端返回 platformOrderId;旧后端缺失时回退 token,仅用于前后端滚动发布。
const platformOrderId = String(
response.data.platformOrderId || response.data.token || '',
).trim()
if (!platformOrderId) {
message.error('生成补资料链接失败:缺少平台订单号')
return
}
const url = buildCollectMaterialLinkUrl(window.location.origin, platformOrderId)
if (!url) {
message.error('生成补资料链接失败:链接地址为空')
return
}
const copied = await copyTextToClipboard(url)
const expiresText = formatAdminDateTime(response.data.expiresAt)
if (copied) {
message.success(
`${response.data.reused ? '已复制现有链接' : '已生成并复制新链接'},${expiresText} 前有效,仅可提交一次`,
`${response.data.reused ? '已复制现有链接' : '已生成并复制链接'}(订单号 ${platformOrderId}),${expiresText} 前有效;该订单号下已生成链接的待补商品会一起显示`,
)
} else {
setMaterialLinkFallback({ url, expiresAt: response.data.expiresAt })
@@ -1914,13 +1945,13 @@ export default function WorkOrdersPanel() {
onClick={() => {
modal.confirm({
title: '确认重新启用该订单?',
content: '订单将恢复为未分配状态。请确认资料与金额后,再发布到抢单大厅。',
content: '订单将恢复为待分配状态。请确认资料与金额后,再发布到抢单大厅。',
okText: '重新启用',
cancelText: '取消',
onOk: () =>
runAction(
() => reopenAdminCancelledWorkOrder(row.workOrderId),
'订单已恢复为未分配状态,可编辑后发布',
'订单已恢复为待分配状态,可编辑后发布',
),
})
}}
@@ -1969,11 +2000,20 @@ export default function WorkOrdersPanel() {
icon={<LinkOutlined />}
loading={materialLinkLoadingId === row.workOrderId}
onClick={() => void copyMaterialLink(row)}
title="生成一次性补资料链接(有效期内仅可提交一次)"
title="生成一次性补资料链接(按平台订单号打开;有效期内每个工单仅可提交一次)"
>
复制补资料链接
</Button>
) : null}
{row.capabilities?.canResetMaterial && canUpdateMaterial ? (
<Button
icon={<RollbackOutlined />}
onClick={() => requestResetMaterial(row)}
title="把已提交的补资料重置为待完善,客户可重新提交一次"
>
重置补资料
</Button>
) : null}
{row.capabilities?.canPublish && canPublishOrder ? (
<Button
icon={<SendOutlined />}
@@ -2006,11 +2046,11 @@ export default function WorkOrdersPanel() {
onClick={() => {
modal.confirm({
title: '确认下架该订单?',
content: '下架后订单会回到未分配状态,不再在抢单大厅展示。',
content: '下架后订单会回到待分配状态,不再在抢单大厅展示。',
okText: '确认下架',
cancelText: '取消',
onOk: () =>
runAction(() => unpublishAdminWorkOrder(row.workOrderId), '订单已撤回到未分配'),
runAction(() => unpublishAdminWorkOrder(row.workOrderId), '订单已撤回到待分配'),
})
}}
>
@@ -3251,7 +3291,8 @@ export default function WorkOrdersPanel() {
{materialLinkFallback ? (
<Space direction="vertical" size={10} style={{ width: '100%' }}>
<Typography.Text type="secondary">
链接仅可提交一次,{formatAdminDateTime(materialLinkFallback.expiresAt)} 前有效。
同一订单号下已生成链接的多个待补工单会显示在同一个页面,每个工单各自仅可提交一次;
{formatAdminDateTime(materialLinkFallback.expiresAt)} 前有效。
</Typography.Text>
<Input.TextArea
value={materialLinkFallback.url}
@@ -205,7 +205,7 @@ export function formatMoney(value: number | undefined) {
export function formatStatus(status: string) {
const labels: Record<string, string> = {
pending_material: '待完善',
unassigned: '未分配',
unassigned: '待分配',
open: '待抢单',
in_progress: '代练中',
pending_acceptance: '待验收',