feat(work-order): 新增一次性补资料链接
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
import { CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
Radio,
|
||||
Result,
|
||||
Space,
|
||||
Spin,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { 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'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
|
||||
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_expired') return '链接已过期'
|
||||
if (code === 'material_link_revoked') return '链接已失效'
|
||||
if (code === 'material_link_not_found') return '链接无效'
|
||||
if (code === 'material_link_order_status_invalid') return '订单状态已变化'
|
||||
return '无法打开链接'
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户侧一次性补资料页。
|
||||
*
|
||||
* 只渲染 requirement 里的动态字段与买家主页截图,不展示发单价格、押金、
|
||||
* 打手、后台备注等任何后台字段;提交成功后链接立即失效。
|
||||
*/
|
||||
export default function CollectMaterialLinkPage() {
|
||||
const { message } = App.useApp()
|
||||
const { token: routeToken } = useParams<{ token?: string }>()
|
||||
const token = String(routeToken || '').trim()
|
||||
|
||||
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[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const previousTitle = document.title
|
||||
document.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()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function load() {
|
||||
if (!token) {
|
||||
setLoading(false)
|
||||
setLoadError({
|
||||
code: 'material_link_not_found',
|
||||
message: '链接无效,请确认链接是否完整',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setSubmitted(false)
|
||||
try {
|
||||
const response = await fetchCollectMaterialLink(token)
|
||||
if (cancelled) return
|
||||
setInfo(response.data)
|
||||
setLoadError(null)
|
||||
setScreenshots(response.data.screenshots || [])
|
||||
form.resetFields()
|
||||
form.setFieldsValue(response.data.values || {})
|
||||
} catch (error) {
|
||||
if (cancelled) return
|
||||
setInfo(null)
|
||||
setLoadError({
|
||||
code: resolveErrorCode(error),
|
||||
message: resolveErrorMessage(error),
|
||||
})
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void load()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [form, token])
|
||||
|
||||
async function handleSubmit(values: Record<string, string>) {
|
||||
if (!token) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await submitCollectMaterialLink(token, { fields: values, screenshots })
|
||||
setSubmitted(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
|
||||
}
|
||||
message.error(resolveErrorMessage(error))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
) : 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 || '请联系客服重新获取链接'}
|
||||
/>
|
||||
)
|
||||
) : info ? (
|
||||
<Space direction="vertical" size={16} className="full-width">
|
||||
<div>
|
||||
<Title level={3} className="collect-link-title">
|
||||
订单资料补充
|
||||
</Title>
|
||||
<Text type="secondary">请补充以下资料,提交后客服会尽快处理。</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>
|
||||
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="链接仅可提交一次"
|
||||
description="提交成功后链接立即失效,订单会转为未分配;如需再次修改资料,请联系客服重新获取链接。"
|
||||
/>
|
||||
|
||||
<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,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<Input autoComplete="off" placeholder={`请输入${field.label}`} />
|
||||
)}
|
||||
</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>
|
||||
|
||||
<Text type="secondary" className="collect-link-footnote">
|
||||
本页面仅用于补充订单资料,请勿将链接转发给他人。
|
||||
</Text>
|
||||
</Space>
|
||||
) : null}
|
||||
</Card>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user