Files
order_site/apps/frontend/src/pages/collect/CollectMaterialLinkPage.tsx
T
zhaoxing a9e2de443e feat(collect): 补资料查询仅限待完善并优化页面展示
- 公开查询只返回待完善工单,待分配/已验收等状态提示「该订单当前无需补充资料」
- 404 失败查询按 IP 冷却 2 秒,成功查询不计数;前端显示「查询过于频繁」
- 提交成功后本地展示成功页,刷新后按严格模式提示无需补充资料
- 收敛补资料页重复提示,移除状态标签与工单号,商品与订单号合并展示
- 修复 React StrictMode 下刷新重复请求查询接口的问题
2026-11-02 10:38:09 +08:00

406 lines
15 KiB
TypeScript

import { CheckCircleFilled, 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_not_pending') return '该订单当前无需补充资料'
if (code === 'rate_limited') return '查询过于频繁'
if (code === 'material_link_work_order_required') return '请选择要补充的商品'
if (code === 'material_link_order_status_invalid') return '订单状态已变化'
return '暂时无法打开页面'
}
/** 公开页不展示「待分配」字样:已提交统一用「资料已提交」表达。 */
function resolveMaterialOrderMessage(order: CollectMaterialLinkOrder): string {
if (order.blockedReason === 'already_submitted') {
return '资料已提交;如需修改请联系客服。'
}
return order.message || '当前不能修改资料,如需修改请联系客服。'
}
/**
* 客户侧补资料页。
*
* 只查询「待完善」工单;其他状态(待分配、已验收等)统一提示无需补充资料,
* 不展示商品、订单号等信息。页面按 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)
/** React StrictMode 开发环境会重复执行 mount effect,这里按订单号去重,避免刷新时打两次查询接口。 */
const loadedCodeRef = useRef<string | null>(null)
const [screenshotsByOrder, setScreenshotsByOrder] = useState<Record<number, UploadedFile[]>>({})
const [submittedSuccess, setSubmittedSuccess] = useState(false)
const [submittedNames, setSubmittedNames] = useState<string[]>([])
/**
* 读取订单资料状态。
*
* silent=true 用于提交后刷新:不切整页 loading,避免页面闪烁丢焦点;
* 同时用请求序号丢弃过期响应,防止快速重试时旧数据覆盖新数据。
*/
const loadInfo = useCallback(
async (options: { silent?: boolean } = {}) => {
const seq = requestSeqRef.current + 1
requestSeqRef.current = seq
if (!options.silent) {
// 新订单号/刷新页面时清空上一次提交成功状态;提交后不再回查
// (严格模式只允许查询待完善订单,回查会变成「无需补充资料」)。
setSubmittedSuccess(false)
setSubmittedNames([])
}
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(() => {
if (loadedCodeRef.current === code) return
loadedCodeRef.current = code
void loadInfo()
}, [code, 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('提交成功')
const submittedName = String(order.productName || '').trim() || '订单商品'
setSubmittedNames((previous) => [...previous, submittedName])
const remainingOrders = (info?.orders || []).filter(
(item) => item.workOrderId !== order.workOrderId,
)
if (remainingOrders.length > 0) {
await loadInfo({ silent: true })
} else {
// 严格模式只查询待完善订单:最后一个商品提交后不再回查,
// 直接本地展示成功页,避免刷新成「无需补充资料」。
setSubmittedSuccess(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)
}
}
const emptyStateMessage = '该订单当前不可修改资料,如需修改请联系客服。'
const submittedProductNames = submittedNames.join('、')
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={loadError.code === 'material_link_not_pending' ? 'info' : 'warning'}
title={resolveErrorTitle(loadError.code)}
subTitle={
loadError.code === 'material_link_not_pending'
? undefined
: loadError.code === 'rate_limited'
? '请稍后再试'
: loadError.message || NOT_FOUND_MESSAGE
}
extra={
<Link to="/supplement">
<Button>重新输入订单号</Button>
</Link>
}
/>
) : submittedSuccess ? (
<div className="collect-link-success">
<CheckCircleFilled className="collect-link-success-icon" />
<Title level={4} className="collect-link-success-title">
提交成功
</Title>
<Text type="secondary">资料已提交;如需修改请联系客服。</Text>
<div className="collect-link-success-products">
<span>本次提交</span>
<strong>{submittedProductNames || '订单资料'}</strong>
</div>
<Text type="secondary" className="collect-link-footnote">
本页面仅用于补充订单资料,请妥善保管订单号。
</Text>
</div>
) : info ? (
<Space direction="vertical" size={16} className="full-width">
{info.pendingCount > 0 ? (
<Text type="secondary">请补充以下资料,提交后客服会尽快处理。</Text>
) : null}
{info.pendingCount > 1 ? (
<Alert
type="info"
showIcon
message={`当前订单有 ${info.pendingCount} 个商品待补充资料,请逐个提交`}
description="每个商品的资料仅可提交一次,提交成功后不能再修改。"
/>
) : null}
{info.pendingCount === 0 ? (
<Alert type="warning" showIcon message={emptyStateMessage} />
) : null}
{info.orders.map((order) => (
<Card key={order.workOrderId} className="collect-link-order-card" size="small">
<Space direction="vertical" size={12} className="full-width">
<div className="collect-link-order-head">
<div className="collect-link-order-name">
<span>{order.productName || '订单商品'}</span>
</div>
<div className="collect-link-order-no">
<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)}
/>
</>
) : info.pendingCount > 0 ? (
<Alert
type={order.blockedReason === 'already_submitted' ? 'info' : 'warning'}
showIcon
message={resolveMaterialOrderMessage(order)}
/>
) : null}
</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>
)
}