import { CheckCircleFilled, LoadingOutlined, ReloadOutlined, SearchOutlined, } from '@ant-design/icons' import { Alert, App, Button, Card, DatePicker, Form, Image, Input, InputNumber, Radio, Result, Select, Space, Spin, Steps, Tag, Typography, } from 'antd' import dayjs, { type Dayjs } from 'dayjs' import { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router' import ImageUpload from '@/components/files/ImageUpload' import { fetchOrderService, submitOrderServiceMaterial, uploadOrderServiceFile, type OrderServiceInfo, type OrderServiceItem, } from '@/services/worker' import type { UploadedFile } from '@/types/worker-platform' import { normalizeOrderServiceInput } from '@/utils/order-service-link' import { useOrderServicePageMeta } from './use-order-service-page-meta' const { Text, Title } = Typography const NOT_FOUND_MESSAGE = '订单号无效,请确认订单号是否正确' const STATUS_REFRESH_INTERVAL_MS = 15_000 const STATUS_STEP_INDEX: Record = { pending_material: 0, unassigned: 1, open: 2, in_progress: 3, pending_acceptance: 4, accepted: 4, problem: 3, } const STATUS_STEP_ITEMS = [ { title: '提交资料' }, { title: '客服安排' }, { title: '发布接单' }, { title: '订单处理' }, { title: '交付完成' }, ] const STATUS_TAG_COLORS: Record = { pending_material: 'orange', unassigned: 'blue', open: 'gold', in_progress: 'processing', pending_acceptance: 'purple', accepted: 'success', problem: 'error', cancelled: 'default', } function resolveErrorCode(error: unknown) { return String((error as { errorCode?: string })?.errorCode || '').trim() } function resolveErrorMessage(error: unknown) { return error instanceof Error ? error.message : '操作失败,请稍后重试' } function resolveErrorTitle(code: string) { if (code === 'order_service_not_found') return '订单号无效' if (code === 'rate_limited') return '查询过于频繁' return '暂时无法打开页面' } export default function OrderServicePage() { const { message, modal } = App.useApp() const navigate = useNavigate() const { orderNo: routeOrderNo } = useParams<{ orderNo?: string }>() const orderNo = String(routeOrderNo || '').trim() const [info, setInfo] = useState(null) const [loading, setLoading] = useState(false) const [loadError, setLoadError] = useState<{ code: string; message: string } | null>(null) const [submittedNames, setSubmittedNames] = useState([]) const [lastSuccessText, setLastSuccessText] = useState('') const [submittingId, setSubmittingId] = useState(0) const [screenshotsByOrder, setScreenshotsByOrder] = useState>({}) const submittingRef = useRef(false) const confirmationOpenRef = useRef(false) const requestSeqRef = useRef(0) const loadedOrderRef = useRef('') useOrderServicePageMeta(info?.items[0]?.title || '订单服务') const load = useCallback( async (options: { silent?: boolean } = {}) => { const seq = requestSeqRef.current + 1 requestSeqRef.current = seq if (!options.silent) { setSubmittedNames([]) setLastSuccessText('') setLoading(true) } if (!orderNo) { setInfo(null) setLoadError(null) setLoading(false) return } try { const response = await fetchOrderService(orderNo) if (seq !== requestSeqRef.current) return setInfo(response.data) setLoadError(null) } catch (error) { if (seq !== requestSeqRef.current) return if (options.silent && info) return setInfo(null) setLoadError({ code: resolveErrorCode(error), message: resolveErrorMessage(error) || NOT_FOUND_MESSAGE, }) } finally { if (seq === requestSeqRef.current && !options.silent) setLoading(false) } }, [info, orderNo], ) useEffect(() => { if (loadedOrderRef.current === orderNo) return loadedOrderRef.current = orderNo setScreenshotsByOrder({}) if (!orderNo) { setInfo(null) setLoadError(null) return } void load() }, [load, orderNo]) useEffect(() => { if (!orderNo) return const refresh = () => { if (document.visibilityState === 'visible' && !submittingRef.current) { void load({ silent: true }) } } const timer = window.setInterval(refresh, STATUS_REFRESH_INTERVAL_MS) document.addEventListener('visibilitychange', refresh) window.addEventListener('focus', refresh) return () => { window.clearInterval(timer) document.removeEventListener('visibilitychange', refresh) window.removeEventListener('focus', refresh) } }, [load, orderNo]) function handleSearch(values: { orderNo?: string }) { const nextOrderNo = normalizeOrderServiceInput(values.orderNo) if (!nextOrderNo) { message.warning('请输入订单号') return } void navigate(`/o/${encodeURIComponent(nextOrderNo)}`) } function handleSubmit(item: OrderServiceItem, values: Record) { if (!orderNo || submittingRef.current || confirmationOpenRef.current) return const screenshots = screenshotsByOrder[item.workOrderId] ?? item.screenshots ?? [] const normalizedValues = normalizeFormFieldValues(item, values) const screenshotRule = item.screenshotsRule if ( screenshotRule?.enabled && screenshotRule.minCount > 0 && screenshots.length < screenshotRule.minCount ) { message.error(`${screenshotRule.label}至少需要 ${screenshotRule.minCount} 张`) return } const confirmation = item.materialConfirmation if (!confirmation?.enabled) { void submitMaterial(item, normalizedValues, screenshots) return } confirmationOpenRef.current = true modal.confirm({ title: confirmation.title || '提交资料确认', content: ( ), okText: confirmation.confirmText || '确认提交', cancelText: confirmation.cancelText || '返回检查', centered: true, maskClosable: false, width: 560, onOk: () => submitMaterial(item, normalizedValues, screenshots), afterClose: () => { confirmationOpenRef.current = false }, }) } async function submitMaterial( item: OrderServiceItem, fields: Record, screenshots: UploadedFile[], ) { if (!orderNo || submittingRef.current) return submittingRef.current = true setSubmittingId(item.workOrderId) try { const response = await submitOrderServiceMaterial(orderNo, { workOrderId: item.workOrderId, fields, screenshots, }) setSubmittedNames((current) => [ ...current, item.productName || item.categoryName || '订单商品', ]) setLastSuccessText(response.data.successText || item.successText || '资料已提交') await load({ silent: true }) } catch (error) { message.error(resolveErrorMessage(error)) const code = resolveErrorCode(error) if ( code === 'order_service_not_found' || code === 'material_link_already_submitted' || code === 'material_link_order_status_invalid' ) { await load({ silent: true }) } } finally { submittingRef.current = false setSubmittingId(0) } } if (!orderNo) { return (
订单服务 请输入下单时使用的订单号,补充资料或查看进度。
} placeholder="请输入订单号" size="large" />
) } const pageConfig = info?.items[0] const pendingItems = (info?.items || []).filter((item) => item.canSubmit) return (
{loading ? (
} size="large" /> 正在读取订单信息...
) : loadError ? ( void load()}>刷新重试} /> ) : info ? (
{pageConfig?.title || '订单服务'} 订单号:{info.platformOrderId} {pageConfig?.description ? (
{pageConfig.description}
) : null}
{submittedNames.length > 0 ? ( ) : null} {pendingItems.length > 1 ? ( ) : pendingItems.length === 1 ? ( ) : null} {info.items.map((item) => item.canSubmit ? ( { setScreenshotsByOrder((current) => ({ ...current, [item.workOrderId]: files, })) }} onSubmit={(values) => void handleSubmit(item, values)} /> ) : ( ), )} {pageConfig?.postSubmitText || '资料提交后如需修改,请联系客服。'}
) : null}
) } function OrderServiceStatusCard({ item }: { item: OrderServiceItem }) { const currentStep = STATUS_STEP_INDEX[item.state] ?? 0 const times = [ item.submittedAt ? { label: '资料提交时间', value: item.submittedAt } : null, item.state === 'pending_acceptance' && item.acceptanceSubmittedAt ? { label: '凭证提交时间', value: item.acceptanceSubmittedAt } : null, item.state === 'accepted' && item.acceptedAt ? { label: '完成时间', value: item.acceptedAt } : null, ].filter((row): row is { label: string; value: string } => Boolean(row?.value)) const acceptanceImageUrls = item.acceptanceImageUrls || [] const showSteps = item.state !== 'cancelled' return (
{item.productName || item.categoryName || '订单商品'} {item.stateLabel}
{showSteps ? ( ) : null}
{item.message}
{times.length > 0 ? (
{times.map((row) => ( {row.label}:{formatOrderServiceTime(row.value)} ))}
) : null} {acceptanceImageUrls.length > 0 ? (
交付验收图片 点击图片可查看大图
{acceptanceImageUrls.map((url, index) => ( {`验收图片 ))}
) : null}
) } function OrderServiceForm({ item, code, submitting, disabled, screenshots, onScreenshotsChange, onSubmit, }: { item: OrderServiceItem code: string submitting: boolean disabled: boolean screenshots: UploadedFile[] onScreenshotsChange: (files: UploadedFile[]) => void onSubmit: (values: Record) => void }) { const rule = item.screenshotsRule || { enabled: false, required: false, minCount: 0, maxCount: 5, label: '截图', helpText: '', } return (
{item.productName || item.categoryName || '订单商品'}
分类 {item.categoryName || '-'}
{item.title ? {item.title} : null} {item.description ? {item.description} : null} {item.guideImage && 'url' in item.guideImage && item.guideImage.url ? ( 填写引导图 ) : null} {item.fields && item.fields.length > 0 ? (
{item.fields.map((field) => ( {field.label} {field.required ? 必填 : 选填} } extra={field.helpText} rules={[ ...(field.required ? [{ required: true, whitespace: true, message: `请填写${field.label}` }] : []), ...(field.validation === 'digits' ? [ { validator: (_rule: unknown, value: unknown) => { const text = String(value ?? '').trim() if (!text || /^\d+$/.test(text)) return Promise.resolve() return Promise.reject(new Error(`${field.label}只能填写数字`)) }, }, ] : []), ]} > {renderFieldInput(field)} ))} {rule.enabled ? ( {rule.label} {rule.required ? '必填' : '选填'} } extra={rule.helpText} > { const response = await uploadOrderServiceFile( file, scene, code, item.workOrderId, ) return response.data.file }} /> ) : null}
) : ( )}
) } function renderFieldInput(field: NonNullable[number]) { const placeholder = field.placeholder || `请输入${field.label}` if (field.validation === 'digits') { return ( ) } if (field.type === 'textarea') { return } if (field.type === 'select') { return ( } function MaterialConfirmationSummary({ item, platformOrderId, values, screenshots, notice, }: { item: OrderServiceItem platformOrderId: string values: Record screenshots: UploadedFile[] notice: string }) { const screenshotRule = item.screenshotsRule return (
{item.productName || item.categoryName || '订单商品'} 订单号:{platformOrderId}
{item.fields && item.fields.length > 0 ? (
{item.fields.map((field) => { const value = String(values[field.key] || '').trim() return (
{field.label} {value || '未填写'}
) })}
) : null} {screenshotRule?.enabled ? (
{screenshotRule.label || '截图'} {screenshots.length} 张
{screenshots.length > 0 ? (
{screenshots.map((file) => ( {file.filename ))}
) : null}
) : null}
{notice}
) } function normalizeFormFieldValues(item: OrderServiceItem, values: Record) { const fields = new Map((item.fields || []).map((field) => [field.key, field])) return Object.fromEntries( Object.entries(values).map(([key, value]) => { const field = fields.get(key) if (field?.type === 'date' && value && dayjs.isDayjs(value)) { return [key, (value as Dayjs).format('YYYY-MM-DD')] } return [key, String(value ?? '').trim()] }), ) } function formatOrderServiceTime(value: string) { const parsed = dayjs(value) return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm') : value }