feat(collect): 补资料改为订单号直连并按状态一次性提交
- 公开补资料页 /supplement/:订单号 直接按平台订单号查询,不再依赖链接记录;旧 token 链接一律 404 - 仅「待完善 + 未指派 + 无拼单份额」可提交,提交后状态变为待分配,重复提交返回 409 - 非可编辑工单只返回商品、工单号、状态与客服提示,不返回已填资料和截图 - 管理端新增「重置补资料」:待分配且未指派、无份额、未发布时一键重置回待完善 - 新增 canResetMaterial 能力与 material_reset 流转事件,工单状态文案统一为「待分配」
This commit is contained in:
@@ -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: '待验收',
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ArrowRightOutlined } from '@ant-design/icons'
|
||||
import { App, Button, Card, Form, Input, Space, Typography } from 'antd'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
import { normalizeOrderNoInput } from '@/utils/collect-link'
|
||||
import { useCollectLinkPageMeta } from './use-collect-link-page-meta'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
|
||||
/**
|
||||
* 补资料入口页(`/supplement/`)。
|
||||
*
|
||||
* 客户只知道平台订单号时,从这里输入订单号再跳到 `/supplement/<订单号>`;
|
||||
* 如果客服已经发了完整链接,可以直接打开链接跳过这一步。
|
||||
*/
|
||||
export default function CollectMaterialLinkEntryPage() {
|
||||
const { message } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
useCollectLinkPageMeta('订单资料补充')
|
||||
|
||||
function handleFinish(values: { orderNo?: string }) {
|
||||
const orderNo = normalizeOrderNoInput(values.orderNo)
|
||||
if (!orderNo) {
|
||||
message.warning('请输入订单号')
|
||||
return
|
||||
}
|
||||
void navigate(`/supplement/${encodeURIComponent(orderNo)}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="collect-link-page">
|
||||
<Card className="collect-link-card" bordered={false}>
|
||||
<Space direction="vertical" size={16} className="full-width">
|
||||
<div>
|
||||
<Title level={3} className="collect-link-title">
|
||||
订单资料补充
|
||||
</Title>
|
||||
<Text type="secondary">请输入下单时使用的订单号,进入补资料页面。</Text>
|
||||
</div>
|
||||
|
||||
<Form layout="vertical" onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
name="orderNo"
|
||||
label="订单号"
|
||||
rules={[{ required: true, whitespace: true, message: '请输入订单号' }]}
|
||||
>
|
||||
<Input autoComplete="off" allowClear placeholder="请输入订单号" size="large" />
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
block
|
||||
icon={<ArrowRightOutlined />}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
<Text type="secondary" className="collect-link-footnote">
|
||||
如果客服已经发给你完整链接,直接打开链接即可。
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
@@ -13,17 +13,23 @@ import {
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router'
|
||||
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import { uploadCollectMaterialLinkFile } from '@/services/files'
|
||||
import { fetchCollectMaterialLink, submitCollectMaterialLink } from '@/services/worker'
|
||||
import type { CollectMaterialLinkInfo, UploadedFile } from '@/types/worker-platform'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import type {
|
||||
CollectMaterialLinkInfo,
|
||||
CollectMaterialLinkOrder,
|
||||
UploadedFile,
|
||||
} from '@/types/worker-platform'
|
||||
import { useCollectLinkPageMeta } from './use-collect-link-page-meta'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
|
||||
const NOT_FOUND_MESSAGE = '订单号无效,请确认订单号是否正确'
|
||||
|
||||
type LinkLoadError = {
|
||||
code: string
|
||||
message: string
|
||||
@@ -38,135 +44,140 @@ function resolveErrorMessage(error: unknown): string {
|
||||
}
|
||||
|
||||
function resolveErrorTitle(code: string): string {
|
||||
if (code === 'material_link_expired') return '链接已过期'
|
||||
if (code === 'material_link_revoked') return '链接已失效'
|
||||
if (code === 'material_link_not_found') return '链接无效'
|
||||
if (code === 'material_link_not_found' || code === 'material_link_work_order_invalid') {
|
||||
return '订单号无效'
|
||||
}
|
||||
if (code === 'material_link_work_order_required') return '请选择要补充的商品'
|
||||
if (code === 'material_link_order_status_invalid') return '订单状态已变化'
|
||||
return '无法打开链接'
|
||||
return '暂时无法打开页面'
|
||||
}
|
||||
|
||||
const STATUS_TAG_COLORS: Record<string, string> = {
|
||||
pending_material: 'orange',
|
||||
unassigned: 'blue',
|
||||
open: 'cyan',
|
||||
in_progress: 'processing',
|
||||
pending_acceptance: 'purple',
|
||||
problem: 'red',
|
||||
accepted: 'green',
|
||||
cancelled: 'default',
|
||||
}
|
||||
|
||||
function resolveStatusColor(status: string): string {
|
||||
return STATUS_TAG_COLORS[status] || 'default'
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户侧一次性补资料页。
|
||||
* 客户侧补资料页。
|
||||
*
|
||||
* 只渲染 requirement 里的动态字段与买家主页截图,不展示发单价格、押金、
|
||||
* 打手、后台备注等任何后台字段;提交成功后链接立即失效。
|
||||
* 只要知道平台订单号就能打开:一个订单号下可能有多个商品工单,页面按 orders
|
||||
* 渲染多张卡片。只有「待完善 + 未指派 + 无份额」的工单返回表单,其余工单只展示
|
||||
* 商品、工单号、状态与客服提示,不暴露已填资料和截图。
|
||||
* 提交成功后工单进入「待分配」,同一工单不能再提交,需联系客服重置。
|
||||
*/
|
||||
export default function CollectMaterialLinkPage() {
|
||||
const { message } = App.useApp()
|
||||
const { token: routeToken } = useParams<{ token?: string }>()
|
||||
const token = String(routeToken || '').trim()
|
||||
const { code: routeCode } = useParams<{ code?: string }>()
|
||||
const code = String(routeCode || '').trim()
|
||||
|
||||
useCollectLinkPageMeta('订单资料补充')
|
||||
|
||||
const [form] = Form.useForm<Record<string, string>>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [info, setInfo] = useState<CollectMaterialLinkInfo | null>(null)
|
||||
const [loadError, setLoadError] = useState<LinkLoadError | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [screenshots, setScreenshots] = useState<UploadedFile[]>([])
|
||||
const [submittingWorkOrderId, setSubmittingWorkOrderId] = useState(0)
|
||||
const submittingRef = useRef(false)
|
||||
const requestSeqRef = useRef(0)
|
||||
const [screenshotsByOrder, setScreenshotsByOrder] = useState<Record<number, UploadedFile[]>>({})
|
||||
|
||||
useEffect(() => {
|
||||
const previousTitle = document.title
|
||||
document.title = '订单资料补充'
|
||||
/**
|
||||
* 读取订单资料状态。
|
||||
*
|
||||
* silent=true 用于提交后刷新:不切整页 loading,避免页面闪烁丢焦点;
|
||||
* 同时用请求序号丢弃过期响应,防止快速重试时旧数据覆盖新数据。
|
||||
*/
|
||||
const loadInfo = useCallback(
|
||||
async (options: { silent?: boolean } = {}) => {
|
||||
const seq = requestSeqRef.current + 1
|
||||
requestSeqRef.current = seq
|
||||
|
||||
function applyMeta(name: string, content: string) {
|
||||
const existing = document.querySelector<HTMLMetaElement>(`meta[name="${name}"]`)
|
||||
const previous = existing?.getAttribute('content') ?? null
|
||||
let created: HTMLMetaElement | null = null
|
||||
if (existing) {
|
||||
existing.setAttribute('content', content)
|
||||
} else {
|
||||
created = document.createElement('meta')
|
||||
created.setAttribute('name', name)
|
||||
created.setAttribute('content', content)
|
||||
document.head.appendChild(created)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (created) {
|
||||
created.remove()
|
||||
} else if (existing) {
|
||||
if (previous === null) {
|
||||
existing.removeAttribute('content')
|
||||
} else {
|
||||
existing.setAttribute('content', previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const restoreRobots = applyMeta('robots', 'noindex,nofollow')
|
||||
const restoreReferrer = applyMeta('referrer', 'no-referrer')
|
||||
|
||||
return () => {
|
||||
document.title = previousTitle
|
||||
restoreRobots()
|
||||
restoreReferrer()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function load() {
|
||||
if (!token) {
|
||||
if (!code) {
|
||||
setInfo(null)
|
||||
setLoadError({ code: 'material_link_not_found', message: NOT_FOUND_MESSAGE })
|
||||
setLoading(false)
|
||||
setLoadError({
|
||||
code: 'material_link_not_found',
|
||||
message: '链接无效,请确认链接是否完整',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setSubmitted(false)
|
||||
if (!options.silent) setLoading(true)
|
||||
try {
|
||||
const response = await fetchCollectMaterialLink(token)
|
||||
if (cancelled) return
|
||||
const response = await fetchCollectMaterialLink(code)
|
||||
if (seq !== requestSeqRef.current) return
|
||||
const orders = response.data.orders || []
|
||||
if (orders.length === 0) {
|
||||
setInfo(null)
|
||||
setLoadError({ code: 'material_link_not_found', message: NOT_FOUND_MESSAGE })
|
||||
return
|
||||
}
|
||||
setInfo(response.data)
|
||||
setLoadError(null)
|
||||
setScreenshots(response.data.screenshots || [])
|
||||
form.resetFields()
|
||||
form.setFieldsValue(response.data.values || {})
|
||||
setScreenshotsByOrder((previous) => {
|
||||
// 只保留仍可提交商品的本地截图,避免刷新后覆盖用户刚上传的图片;
|
||||
// 不可提交商品不再展示资料,直接丢弃。
|
||||
const next: Record<number, UploadedFile[]> = {}
|
||||
for (const order of orders) {
|
||||
if (!order.canSubmit) continue
|
||||
next[order.workOrderId] = previous[order.workOrderId] ?? order.screenshots ?? []
|
||||
}
|
||||
return next
|
||||
})
|
||||
} catch (error) {
|
||||
if (cancelled) return
|
||||
if (seq !== requestSeqRef.current) return
|
||||
setInfo(null)
|
||||
setLoadError({
|
||||
code: resolveErrorCode(error),
|
||||
message: resolveErrorMessage(error),
|
||||
message: resolveErrorMessage(error) || NOT_FOUND_MESSAGE,
|
||||
})
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
if (seq === requestSeqRef.current && !options.silent) setLoading(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[code],
|
||||
)
|
||||
|
||||
void load()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [form, token])
|
||||
useEffect(() => {
|
||||
void loadInfo()
|
||||
}, [loadInfo])
|
||||
|
||||
async function handleSubmit(values: Record<string, string>) {
|
||||
if (!token) return
|
||||
setSubmitting(true)
|
||||
async function handleSubmit(order: CollectMaterialLinkOrder, values: Record<string, string>) {
|
||||
if (!code) return
|
||||
// 多工单时用户可能连点不同卡片;前端只允许一个提交在途,避免列表状态互相覆盖。
|
||||
if (submittingRef.current) return
|
||||
submittingRef.current = true
|
||||
setSubmittingWorkOrderId(order.workOrderId)
|
||||
try {
|
||||
await submitCollectMaterialLink(token, { fields: values, screenshots })
|
||||
setSubmitted(true)
|
||||
await submitCollectMaterialLink(code, {
|
||||
workOrderId: order.workOrderId,
|
||||
fields: values,
|
||||
screenshots: screenshotsByOrder[order.workOrderId] ?? order.screenshots ?? [],
|
||||
})
|
||||
message.success('资料已提交,订单已进入待分配')
|
||||
await loadInfo({ silent: true })
|
||||
} catch (error) {
|
||||
const code = resolveErrorCode(error)
|
||||
if (
|
||||
code === 'material_link_used' ||
|
||||
code === 'material_link_expired' ||
|
||||
code === 'material_link_revoked' ||
|
||||
code === 'material_link_not_found' ||
|
||||
code === 'material_link_order_status_invalid'
|
||||
) {
|
||||
setInfo(null)
|
||||
setLoadError({ code, message: resolveErrorMessage(error) })
|
||||
return
|
||||
}
|
||||
const errorCode = resolveErrorCode(error)
|
||||
message.error(resolveErrorMessage(error))
|
||||
// 状态类错误说明工单已被并发提交、被客服重置或流转到其他状态:
|
||||
// 以服务端状态为准静默刷新,页面切换成只读卡片而不是整页报错。
|
||||
if (
|
||||
errorCode === 'material_link_already_submitted' ||
|
||||
errorCode === 'material_link_order_status_invalid' ||
|
||||
errorCode === 'material_link_work_order_invalid' ||
|
||||
errorCode === 'material_link_not_found'
|
||||
) {
|
||||
await loadInfo({ silent: true })
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
submittingRef.current = false
|
||||
setSubmittingWorkOrderId(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,115 +187,106 @@ export default function CollectMaterialLinkPage() {
|
||||
{loading ? (
|
||||
<div className="collect-link-state">
|
||||
<Spin indicator={<LoadingOutlined spin />} size="large" />
|
||||
<Text type="secondary">正在读取补资料链接...</Text>
|
||||
<Text type="secondary">正在读取订单资料...</Text>
|
||||
</div>
|
||||
) : submitted ? (
|
||||
<Result
|
||||
status="success"
|
||||
title="资料已提交"
|
||||
subTitle="链接已失效,如需修改资料,请联系客服重新获取。"
|
||||
/>
|
||||
) : loadError ? (
|
||||
loadError.code === 'material_link_used' ? (
|
||||
<Result
|
||||
icon={<CheckCircleOutlined style={{ color: '#16a34a' }} />}
|
||||
title="资料已提交"
|
||||
subTitle="该链接已经使用过;如需再次修改资料,请联系客服重新获取链接。"
|
||||
/>
|
||||
) : (
|
||||
<Result
|
||||
status="warning"
|
||||
title={resolveErrorTitle(loadError.code)}
|
||||
subTitle={loadError.message || '请联系客服重新获取链接'}
|
||||
/>
|
||||
)
|
||||
<Result
|
||||
status="warning"
|
||||
title={resolveErrorTitle(loadError.code)}
|
||||
subTitle={loadError.message || NOT_FOUND_MESSAGE}
|
||||
extra={
|
||||
<Link to="/supplement">
|
||||
<Button>重新输入订单号</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
) : info ? (
|
||||
<Space direction="vertical" size={16} className="full-width">
|
||||
<div>
|
||||
<Title level={3} className="collect-link-title">
|
||||
订单资料补充
|
||||
</Title>
|
||||
<Text type="secondary">请补充以下资料,提交后客服会尽快处理。</Text>
|
||||
<Text type="secondary">
|
||||
{info.pendingCount > 0
|
||||
? '请补充以下资料,提交后客服会尽快处理。'
|
||||
: '以下为该订单当前的资料状态,如需修改请联系客服。'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="collect-link-meta">
|
||||
<div>
|
||||
<span>商品</span>
|
||||
<strong>{info.order.productName || '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>工单号</span>
|
||||
<strong>{info.order.workOrderNo || '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>订单号</span>
|
||||
<strong>{info.order.platformOrderId || '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>有效期至</span>
|
||||
<strong>{formatDateTime(info.expiresAt)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
{info.pendingCount > 1 ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={`当前订单有 ${info.pendingCount} 个待完善商品,请逐个提交`}
|
||||
description="每个商品的资料仅可提交一次,提交成功后该商品进入待分配。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="链接仅可提交一次"
|
||||
description="提交成功后链接立即失效;如需再次修改资料,请联系客服重新获取链接。"
|
||||
/>
|
||||
{info.pendingCount === 0 ? (
|
||||
<Alert type="info" showIcon message="该订单当前不可修改资料,如需修改请联系客服" />
|
||||
) : null}
|
||||
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
{info.fields.map((field) => (
|
||||
<Form.Item
|
||||
key={field.key}
|
||||
name={field.key}
|
||||
label={
|
||||
<Space size={6}>
|
||||
<span>{field.label}</span>
|
||||
{field.required ? <Tag color="orange">必填</Tag> : <Tag>选填</Tag>}
|
||||
</Space>
|
||||
}
|
||||
rules={
|
||||
field.required
|
||||
? [{ required: true, whitespace: true, message: `请填写${field.label}` }]
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{field.type === 'select' && (field.options || []).length > 0 ? (
|
||||
<Radio.Group
|
||||
options={(field.options || []).map((option) => ({
|
||||
label: option,
|
||||
value: option,
|
||||
}))}
|
||||
/>
|
||||
{info.orders.map((order) => (
|
||||
<Card
|
||||
key={order.workOrderId}
|
||||
className="collect-link-order-card"
|
||||
size="small"
|
||||
title={order.productName || '订单商品'}
|
||||
extra={<Tag color={resolveStatusColor(order.status)}>{order.statusLabel}</Tag>}
|
||||
>
|
||||
<Space direction="vertical" size={12} className="full-width">
|
||||
<div className="collect-link-meta">
|
||||
<div>
|
||||
<span>工单号</span>
|
||||
<strong>{order.workOrderNo || '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>订单号</span>
|
||||
<strong>{order.platformOrderId || '-'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{order.canSubmit ? (
|
||||
<>
|
||||
{info.pendingCount === 1 ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="该商品资料仅可提交一次"
|
||||
description="提交成功后该商品进入待分配,不能再修改;如需修改请联系客服。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<MaterialLinkOrderForm
|
||||
order={order}
|
||||
code={code}
|
||||
screenshots={
|
||||
screenshotsByOrder[order.workOrderId] ?? order.screenshots ?? []
|
||||
}
|
||||
submitting={submittingWorkOrderId === order.workOrderId}
|
||||
disabled={submittingWorkOrderId !== 0}
|
||||
onScreenshotsChange={(files) =>
|
||||
setScreenshotsByOrder((previous) => ({
|
||||
...previous,
|
||||
[order.workOrderId]: files,
|
||||
}))
|
||||
}
|
||||
onSubmit={(values) => handleSubmit(order, values)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Input autoComplete="off" placeholder={`请输入${field.label}`} />
|
||||
<Alert
|
||||
type={order.blockedReason === 'already_submitted' ? 'info' : 'warning'}
|
||||
showIcon
|
||||
message={order.message || '当前不能修改资料,如需修改请联系客服。'}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
))}
|
||||
|
||||
<Form.Item label="买家主页截图">
|
||||
<ImageUpload
|
||||
scene="collect-material"
|
||||
scope="collect"
|
||||
maxCount={5}
|
||||
showPasteHint
|
||||
captureGlobalPaste
|
||||
value={screenshots}
|
||||
onChange={setScreenshots}
|
||||
uploader={(file, scene) => uploadCollectMaterialLinkFile(file, scene, token)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="collect-link-actions">
|
||||
<Button type="primary" size="large" htmlType="submit" loading={submitting}>
|
||||
提交资料
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Text type="secondary" className="collect-link-footnote">
|
||||
本页面仅用于补充订单资料,请勿将链接转发给他人。
|
||||
本页面仅用于补充订单资料,请妥善保管订单号。
|
||||
</Text>
|
||||
</Space>
|
||||
) : null}
|
||||
@@ -292,3 +294,83 @@ export default function CollectMaterialLinkPage() {
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
type MaterialLinkOrderFormProps = {
|
||||
order: CollectMaterialLinkOrder
|
||||
code: string
|
||||
screenshots: UploadedFile[]
|
||||
submitting: boolean
|
||||
disabled: boolean
|
||||
onScreenshotsChange: (files: UploadedFile[]) => void
|
||||
onSubmit: (values: Record<string, string>) => void
|
||||
}
|
||||
|
||||
function MaterialLinkOrderForm({
|
||||
order,
|
||||
code,
|
||||
screenshots,
|
||||
submitting,
|
||||
disabled,
|
||||
onScreenshotsChange,
|
||||
onSubmit,
|
||||
}: MaterialLinkOrderFormProps) {
|
||||
return (
|
||||
<Form layout="vertical" initialValues={order.values || {}} onFinish={onSubmit}>
|
||||
{(order.fields ?? []).map((field) => (
|
||||
<Form.Item
|
||||
key={field.key}
|
||||
name={field.key}
|
||||
label={
|
||||
<Space size={6}>
|
||||
<span>{field.label}</span>
|
||||
{field.required ? <Tag color="orange">必填</Tag> : <Tag>选填</Tag>}
|
||||
</Space>
|
||||
}
|
||||
rules={
|
||||
field.required
|
||||
? [{ required: true, whitespace: true, message: `请填写${field.label}` }]
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{field.type === 'select' && (field.options || []).length > 0 ? (
|
||||
<Radio.Group
|
||||
options={(field.options || []).map((option) => ({
|
||||
label: option,
|
||||
value: option,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<Input autoComplete="off" placeholder={`请输入${field.label}`} />
|
||||
)}
|
||||
</Form.Item>
|
||||
))}
|
||||
|
||||
<Form.Item label="买家主页截图">
|
||||
<ImageUpload
|
||||
scene="collect-material"
|
||||
scope="collect"
|
||||
maxCount={5}
|
||||
showPasteHint
|
||||
captureGlobalPaste
|
||||
value={screenshots}
|
||||
onChange={onScreenshotsChange}
|
||||
uploader={(file, scene) =>
|
||||
uploadCollectMaterialLinkFile(file, scene, code, order.workOrderId)
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="collect-link-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
loading={submitting}
|
||||
disabled={disabled}
|
||||
>
|
||||
提交资料
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 公开补资料页共用的 document.title / robots / referrer 设置。
|
||||
*
|
||||
* 入口页和补资料页都是可被直接访问的公开路径,必须禁止搜索引擎收录,
|
||||
* 并避免平台订单号通过 referrer 泄漏给第三方。
|
||||
*/
|
||||
export function useCollectLinkPageMeta(title: string) {
|
||||
useEffect(() => {
|
||||
const previousTitle = document.title
|
||||
document.title = title
|
||||
|
||||
function applyMeta(name: string, content: string) {
|
||||
const existing = document.querySelector<HTMLMetaElement>(`meta[name="${name}"]`)
|
||||
const previous = existing?.getAttribute('content') ?? null
|
||||
let created: HTMLMetaElement | null = null
|
||||
if (existing) {
|
||||
existing.setAttribute('content', content)
|
||||
} else {
|
||||
created = document.createElement('meta')
|
||||
created.setAttribute('name', name)
|
||||
created.setAttribute('content', content)
|
||||
document.head.appendChild(created)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (created) {
|
||||
created.remove()
|
||||
} else if (existing) {
|
||||
if (previous === null) {
|
||||
existing.removeAttribute('content')
|
||||
} else {
|
||||
existing.setAttribute('content', previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const restoreRobots = applyMeta('robots', 'noindex,nofollow')
|
||||
const restoreReferrer = applyMeta('referrer', 'no-referrer')
|
||||
|
||||
return () => {
|
||||
document.title = previousTitle
|
||||
restoreRobots()
|
||||
restoreReferrer()
|
||||
}
|
||||
}, [title])
|
||||
}
|
||||
Reference in New Issue
Block a user