712 lines
23 KiB
TypeScript
712 lines
23 KiB
TypeScript
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<string, number> = {
|
||
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<string, string> = {
|
||
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<OrderServiceInfo | null>(null)
|
||
const [loading, setLoading] = useState(false)
|
||
const [loadError, setLoadError] = useState<{ code: string; message: string } | null>(null)
|
||
const [submittedNames, setSubmittedNames] = useState<string[]>([])
|
||
const [lastSuccessText, setLastSuccessText] = useState('')
|
||
const [submittingId, setSubmittingId] = useState(0)
|
||
const [screenshotsByOrder, setScreenshotsByOrder] = useState<Record<number, UploadedFile[]>>({})
|
||
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<string, unknown>) {
|
||
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: (
|
||
<MaterialConfirmationSummary
|
||
item={item}
|
||
platformOrderId={orderNo}
|
||
values={normalizedValues}
|
||
screenshots={screenshots}
|
||
notice={
|
||
confirmation.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<string, string>,
|
||
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 (
|
||
<main className="collect-link-page">
|
||
<Card className="collect-link-card" bordered={false}>
|
||
<Space direction="vertical" size={16} className="full-width">
|
||
<div>
|
||
<Title level={3}>订单服务</Title>
|
||
<Text type="secondary">请输入下单时使用的订单号,补充资料或查看进度。</Text>
|
||
</div>
|
||
<Form layout="vertical" onFinish={handleSearch}>
|
||
<Form.Item
|
||
name="orderNo"
|
||
label="订单号"
|
||
rules={[{ required: true, whitespace: true, message: '请输入订单号' }]}
|
||
>
|
||
<Input
|
||
allowClear
|
||
autoComplete="off"
|
||
prefix={<SearchOutlined />}
|
||
placeholder="请输入订单号"
|
||
size="large"
|
||
/>
|
||
</Form.Item>
|
||
<Button block htmlType="submit" size="large" type="primary">
|
||
查询订单
|
||
</Button>
|
||
</Form>
|
||
</Space>
|
||
</Card>
|
||
</main>
|
||
)
|
||
}
|
||
|
||
const pageConfig = info?.items[0]
|
||
const pendingItems = (info?.items || []).filter((item) => item.canSubmit)
|
||
|
||
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 === 'rate_limited' ? 'warning' : 'info'}
|
||
title={resolveErrorTitle(loadError.code)}
|
||
subTitle={loadError.message}
|
||
extra={<Button onClick={() => void load()}>刷新重试</Button>}
|
||
/>
|
||
) : info ? (
|
||
<Space direction="vertical" size={14} className="full-width">
|
||
<div className="collect-link-heading">
|
||
<div>
|
||
<Title level={4}>{pageConfig?.title || '订单服务'}</Title>
|
||
<Text type="secondary">订单号:{info.platformOrderId}</Text>
|
||
{pageConfig?.description ? (
|
||
<div style={{ marginTop: 6 }}>
|
||
<Text type="secondary">{pageConfig.description}</Text>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
<Button
|
||
icon={<ReloadOutlined />}
|
||
size="small"
|
||
onClick={() => void load({ silent: true })}
|
||
>
|
||
刷新状态
|
||
</Button>
|
||
</div>
|
||
|
||
{submittedNames.length > 0 ? (
|
||
<Alert
|
||
type="success"
|
||
showIcon
|
||
message={lastSuccessText || '资料已提交'}
|
||
description={`${submittedNames.join('、')};页面会每 15 秒自动更新订单状态。`}
|
||
/>
|
||
) : null}
|
||
|
||
{pendingItems.length > 1 ? (
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message={`当前订单有 ${pendingItems.length} 个商品需要补充资料`}
|
||
description="请逐个商品提交;每个商品资料仅可提交一次。"
|
||
/>
|
||
) : pendingItems.length === 1 ? (
|
||
<Alert
|
||
type="warning"
|
||
showIcon
|
||
message="该商品资料仅可提交一次"
|
||
description="提交成功后不能再修改;如需修改请联系客服。"
|
||
/>
|
||
) : null}
|
||
|
||
{info.items.map((item) =>
|
||
item.canSubmit ? (
|
||
<OrderServiceForm
|
||
key={item.workOrderId}
|
||
item={item}
|
||
code={orderNo}
|
||
submitting={submittingId === item.workOrderId}
|
||
disabled={submittingId !== 0}
|
||
screenshots={screenshotsByOrder[item.workOrderId] ?? item.screenshots ?? []}
|
||
onScreenshotsChange={(files) => {
|
||
setScreenshotsByOrder((current) => ({
|
||
...current,
|
||
[item.workOrderId]: files,
|
||
}))
|
||
}}
|
||
onSubmit={(values) => void handleSubmit(item, values)}
|
||
/>
|
||
) : (
|
||
<OrderServiceStatusCard key={item.workOrderId} item={item} />
|
||
),
|
||
)}
|
||
|
||
<Text type="secondary" className="collect-link-footnote">
|
||
{pageConfig?.postSubmitText || '资料提交后如需修改,请联系客服。'}
|
||
</Text>
|
||
</Space>
|
||
) : null}
|
||
</Card>
|
||
</main>
|
||
)
|
||
}
|
||
|
||
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 (
|
||
<Card size="small" className="collect-link-status-card">
|
||
<Space direction="vertical" size={12} className="full-width">
|
||
<div className="collect-link-status-head">
|
||
<strong>{item.productName || item.categoryName || '订单商品'}</strong>
|
||
<Tag color={STATUS_TAG_COLORS[item.state] || 'default'}>{item.stateLabel}</Tag>
|
||
</div>
|
||
|
||
{showSteps ? (
|
||
<Steps
|
||
className="collect-link-status-steps"
|
||
current={currentStep}
|
||
items={STATUS_STEP_ITEMS}
|
||
responsive
|
||
size="small"
|
||
status={
|
||
item.state === 'accepted' ? 'finish' : item.state === 'problem' ? 'error' : 'process'
|
||
}
|
||
/>
|
||
) : null}
|
||
|
||
<div className={`collect-link-status-message is-${item.state}`}>{item.message}</div>
|
||
|
||
{times.length > 0 ? (
|
||
<div className="collect-link-status-times">
|
||
{times.map((row) => (
|
||
<span key={row.label}>
|
||
{row.label}:{formatOrderServiceTime(row.value)}
|
||
</span>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
|
||
{acceptanceImageUrls.length > 0 ? (
|
||
<div className="collect-link-acceptance">
|
||
<div>
|
||
<Text strong>交付验收图片</Text>
|
||
<Text type="secondary">点击图片可查看大图</Text>
|
||
</div>
|
||
<Image.PreviewGroup>
|
||
<div className="collect-link-acceptance-images">
|
||
{acceptanceImageUrls.map((url, index) => (
|
||
<Image key={url} alt={`验收图片 ${index + 1}`} src={url} width={92} height={92} />
|
||
))}
|
||
</div>
|
||
</Image.PreviewGroup>
|
||
</div>
|
||
) : null}
|
||
</Space>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
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<string, unknown>) => void
|
||
}) {
|
||
const rule = item.screenshotsRule || {
|
||
enabled: false,
|
||
required: false,
|
||
minCount: 0,
|
||
maxCount: 5,
|
||
label: '截图',
|
||
helpText: '',
|
||
}
|
||
|
||
return (
|
||
<Card size="small" className="collect-link-order-card">
|
||
<Space direction="vertical" size={12} className="full-width">
|
||
<div className="collect-link-order-head">
|
||
<div className="collect-link-order-name">
|
||
<span>{item.productName || item.categoryName || '订单商品'}</span>
|
||
</div>
|
||
<div className="collect-link-order-no">
|
||
<span>分类</span>
|
||
<strong>{item.categoryName || '-'}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
{item.title ? <Title level={5}>{item.title}</Title> : null}
|
||
{item.description ? <Text type="secondary">{item.description}</Text> : null}
|
||
|
||
{item.guideImage && 'url' in item.guideImage && item.guideImage.url ? (
|
||
<img
|
||
alt="填写引导图"
|
||
src={String(item.guideImage.url)}
|
||
style={{ width: '100%', borderRadius: 8 }}
|
||
/>
|
||
) : null}
|
||
|
||
{item.fields && item.fields.length > 0 ? (
|
||
<Form layout="vertical" initialValues={item.values || {}} onFinish={onSubmit}>
|
||
{item.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>
|
||
}
|
||
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)}
|
||
</Form.Item>
|
||
))}
|
||
|
||
{rule.enabled ? (
|
||
<Form.Item
|
||
label={
|
||
<Space size={6}>
|
||
<span>{rule.label}</span>
|
||
<Tag color={rule.required ? 'orange' : 'default'}>
|
||
{rule.required ? '必填' : '选填'}
|
||
</Tag>
|
||
</Space>
|
||
}
|
||
extra={rule.helpText}
|
||
>
|
||
<ImageUpload
|
||
scene="collect-material"
|
||
scope="collect"
|
||
maxCount={rule.maxCount}
|
||
showPasteHint
|
||
captureGlobalPaste
|
||
value={screenshots}
|
||
onChange={onScreenshotsChange}
|
||
uploader={async (file, scene) => {
|
||
const response = await uploadOrderServiceFile(
|
||
file,
|
||
scene,
|
||
code,
|
||
item.workOrderId,
|
||
)
|
||
return response.data.file
|
||
}}
|
||
/>
|
||
</Form.Item>
|
||
) : null}
|
||
|
||
<div className="collect-link-actions">
|
||
<Button
|
||
block
|
||
htmlType="submit"
|
||
loading={submitting}
|
||
disabled={disabled}
|
||
type="primary"
|
||
>
|
||
{item.submitText || '提交资料'}
|
||
</Button>
|
||
</div>
|
||
</Form>
|
||
) : (
|
||
<Alert type="warning" showIcon message="该商品尚未配置资料收集字段,请联系客服。" />
|
||
)}
|
||
</Space>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
function renderFieldInput(field: NonNullable<OrderServiceItem['fields']>[number]) {
|
||
const placeholder = field.placeholder || `请输入${field.label}`
|
||
if (field.validation === 'digits') {
|
||
return (
|
||
<Input
|
||
autoComplete="off"
|
||
inputMode="numeric"
|
||
maxLength={50}
|
||
pattern="[0-9]*"
|
||
placeholder={placeholder}
|
||
/>
|
||
)
|
||
}
|
||
if (field.type === 'textarea') {
|
||
return <Input.TextArea autoComplete="off" placeholder={placeholder} rows={3} />
|
||
}
|
||
if (field.type === 'select') {
|
||
return (
|
||
<Select
|
||
allowClear
|
||
placeholder={placeholder}
|
||
options={(field.options || []).map((option) => ({ label: option, value: option }))}
|
||
/>
|
||
)
|
||
}
|
||
if (field.type === 'radio') {
|
||
return (
|
||
<Radio.Group
|
||
options={(field.options || []).map((option) => ({ label: option, value: option }))}
|
||
/>
|
||
)
|
||
}
|
||
if (field.type === 'number') {
|
||
return <InputNumber style={{ width: '100%' }} placeholder={placeholder} />
|
||
}
|
||
if (field.type === 'date') {
|
||
return <DatePicker style={{ width: '100%' }} placeholder={placeholder} />
|
||
}
|
||
return <Input autoComplete="off" placeholder={placeholder} />
|
||
}
|
||
|
||
function MaterialConfirmationSummary({
|
||
item,
|
||
platformOrderId,
|
||
values,
|
||
screenshots,
|
||
notice,
|
||
}: {
|
||
item: OrderServiceItem
|
||
platformOrderId: string
|
||
values: Record<string, string>
|
||
screenshots: UploadedFile[]
|
||
notice: string
|
||
}) {
|
||
const screenshotRule = item.screenshotsRule
|
||
return (
|
||
<div className="collect-link-confirm">
|
||
<div className="collect-link-confirm-meta">
|
||
<span>{item.productName || item.categoryName || '订单商品'}</span>
|
||
<span>订单号:{platformOrderId}</span>
|
||
</div>
|
||
|
||
{item.fields && item.fields.length > 0 ? (
|
||
<div className="collect-link-confirm-fields">
|
||
{item.fields.map((field) => {
|
||
const value = String(values[field.key] || '').trim()
|
||
return (
|
||
<div className="collect-link-confirm-field" key={field.key}>
|
||
<span>{field.label}</span>
|
||
<strong className={value ? '' : 'is-empty'}>{value || '未填写'}</strong>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
) : null}
|
||
|
||
{screenshotRule?.enabled ? (
|
||
<div className="collect-link-confirm-screenshots">
|
||
<div>
|
||
<span>{screenshotRule.label || '截图'}</span>
|
||
<strong>{screenshots.length} 张</strong>
|
||
</div>
|
||
{screenshots.length > 0 ? (
|
||
<div className="collect-link-confirm-images">
|
||
{screenshots.map((file) => (
|
||
<img
|
||
key={file.objectKey}
|
||
alt={file.filename || '已上传截图'}
|
||
src={file.thumbnailUrl || file.url}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
) : null}
|
||
|
||
<div className="collect-link-confirm-content">{notice}</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function normalizeFormFieldValues(item: OrderServiceItem, values: Record<string, unknown>) {
|
||
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
|
||
}
|