- 公开补资料页 /supplement/:订单号 直接按平台订单号查询,不再依赖链接记录;旧 token 链接一律 404 - 仅「待完善 + 未指派 + 无拼单份额」可提交,提交后状态变为待分配,重复提交返回 409 - 非可编辑工单只返回商品、工单号、状态与客服提示,不返回已填资料和截图 - 管理端新增「重置补资料」:待分配且未指派、无份额、未发布时一键重置回待完善 - 新增 canResetMaterial 能力与 material_reset 流转事件,工单状态文案统一为「待分配」
377 lines
13 KiB
TypeScript
377 lines
13 KiB
TypeScript
import { LoadingOutlined } from '@ant-design/icons'
|
|
import {
|
|
Alert,
|
|
App,
|
|
Button,
|
|
Card,
|
|
Form,
|
|
Input,
|
|
Radio,
|
|
Result,
|
|
Space,
|
|
Spin,
|
|
Tag,
|
|
Typography,
|
|
} from 'antd'
|
|
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,
|
|
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
|
|
}
|
|
|
|
function resolveErrorCode(error: unknown): string {
|
|
return String((error as { errorCode?: string })?.errorCode || '').trim()
|
|
}
|
|
|
|
function resolveErrorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : '操作失败,请稍后重试'
|
|
}
|
|
|
|
function resolveErrorTitle(code: string): string {
|
|
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 '暂时无法打开页面'
|
|
}
|
|
|
|
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'
|
|
}
|
|
|
|
/**
|
|
* 客户侧补资料页。
|
|
*
|
|
* 只要知道平台订单号就能打开:一个订单号下可能有多个商品工单,页面按 orders
|
|
* 渲染多张卡片。只有「待完善 + 未指派 + 无份额」的工单返回表单,其余工单只展示
|
|
* 商品、工单号、状态与客服提示,不暴露已填资料和截图。
|
|
* 提交成功后工单进入「待分配」,同一工单不能再提交,需联系客服重置。
|
|
*/
|
|
export default function CollectMaterialLinkPage() {
|
|
const { message } = App.useApp()
|
|
const { code: routeCode } = useParams<{ code?: string }>()
|
|
const code = String(routeCode || '').trim()
|
|
|
|
useCollectLinkPageMeta('订单资料补充')
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
const [info, setInfo] = useState<CollectMaterialLinkInfo | null>(null)
|
|
const [loadError, setLoadError] = useState<LinkLoadError | null>(null)
|
|
const [submittingWorkOrderId, setSubmittingWorkOrderId] = useState(0)
|
|
const submittingRef = useRef(false)
|
|
const requestSeqRef = useRef(0)
|
|
const [screenshotsByOrder, setScreenshotsByOrder] = useState<Record<number, UploadedFile[]>>({})
|
|
|
|
/**
|
|
* 读取订单资料状态。
|
|
*
|
|
* silent=true 用于提交后刷新:不切整页 loading,避免页面闪烁丢焦点;
|
|
* 同时用请求序号丢弃过期响应,防止快速重试时旧数据覆盖新数据。
|
|
*/
|
|
const loadInfo = useCallback(
|
|
async (options: { silent?: boolean } = {}) => {
|
|
const seq = requestSeqRef.current + 1
|
|
requestSeqRef.current = seq
|
|
|
|
if (!code) {
|
|
setInfo(null)
|
|
setLoadError({ code: 'material_link_not_found', message: NOT_FOUND_MESSAGE })
|
|
setLoading(false)
|
|
return
|
|
}
|
|
|
|
if (!options.silent) setLoading(true)
|
|
try {
|
|
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)
|
|
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 (seq !== requestSeqRef.current) return
|
|
setInfo(null)
|
|
setLoadError({
|
|
code: resolveErrorCode(error),
|
|
message: resolveErrorMessage(error) || NOT_FOUND_MESSAGE,
|
|
})
|
|
} finally {
|
|
if (seq === requestSeqRef.current && !options.silent) setLoading(false)
|
|
}
|
|
},
|
|
[code],
|
|
)
|
|
|
|
useEffect(() => {
|
|
void loadInfo()
|
|
}, [loadInfo])
|
|
|
|
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(code, {
|
|
workOrderId: order.workOrderId,
|
|
fields: values,
|
|
screenshots: screenshotsByOrder[order.workOrderId] ?? order.screenshots ?? [],
|
|
})
|
|
message.success('资料已提交,订单已进入待分配')
|
|
await loadInfo({ silent: true })
|
|
} catch (error) {
|
|
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 {
|
|
submittingRef.current = false
|
|
setSubmittingWorkOrderId(0)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="collect-link-page">
|
|
<Card className="collect-link-card" bordered={false}>
|
|
{loading ? (
|
|
<div className="collect-link-state">
|
|
<Spin indicator={<LoadingOutlined spin />} size="large" />
|
|
<Text type="secondary">正在读取订单资料...</Text>
|
|
</div>
|
|
) : loadError ? (
|
|
<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">
|
|
{info.pendingCount > 0
|
|
? '请补充以下资料,提交后客服会尽快处理。'
|
|
: '以下为该订单当前的资料状态,如需修改请联系客服。'}
|
|
</Text>
|
|
</div>
|
|
|
|
{info.pendingCount > 1 ? (
|
|
<Alert
|
|
type="info"
|
|
showIcon
|
|
message={`当前订单有 ${info.pendingCount} 个待完善商品,请逐个提交`}
|
|
description="每个商品的资料仅可提交一次,提交成功后该商品进入待分配。"
|
|
/>
|
|
) : null}
|
|
|
|
{info.pendingCount === 0 ? (
|
|
<Alert type="info" showIcon message="该订单当前不可修改资料,如需修改请联系客服" />
|
|
) : null}
|
|
|
|
{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)}
|
|
/>
|
|
</>
|
|
) : (
|
|
<Alert
|
|
type={order.blockedReason === 'already_submitted' ? 'info' : 'warning'}
|
|
showIcon
|
|
message={order.message || '当前不能修改资料,如需修改请联系客服。'}
|
|
/>
|
|
)}
|
|
</Space>
|
|
</Card>
|
|
))}
|
|
|
|
<Text type="secondary" className="collect-link-footnote">
|
|
本页面仅用于补充订单资料,请妥善保管订单号。
|
|
</Text>
|
|
</Space>
|
|
) : null}
|
|
</Card>
|
|
</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>
|
|
)
|
|
}
|