feat(order-service): 优化客户订单状态与验收展示
This commit is contained in:
@@ -19,7 +19,6 @@ import {
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Steps,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
@@ -42,14 +41,15 @@ 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: 2,
|
||||
pending_acceptance: 3,
|
||||
accepted: 3,
|
||||
problem: 2,
|
||||
const STATUS_STEP_ORDER = ['submitted', 'scheduled', 'processing', 'completed'] as const
|
||||
const STATUS_STEP_KEY: Record<string, (typeof STATUS_STEP_ORDER)[number]> = {
|
||||
pending_material: 'submitted',
|
||||
unassigned: 'scheduled',
|
||||
open: 'processing',
|
||||
in_progress: 'processing',
|
||||
pending_acceptance: 'completed',
|
||||
accepted: 'completed',
|
||||
problem: 'processing',
|
||||
}
|
||||
const DEFAULT_STATUS_STEPS: OrderServiceItem['statusSteps'] = {
|
||||
submitted: { title: '提交资料', description: '' },
|
||||
@@ -292,6 +292,10 @@ export default function OrderServicePage() {
|
||||
|
||||
const pageConfig = info?.items[0]
|
||||
const pendingItems = (info?.items || []).filter((item) => item.canSubmit)
|
||||
const purchaseSummary =
|
||||
[
|
||||
...new Set(info?.items.map((item) => item.productName || item.categoryName).filter(Boolean)),
|
||||
].join('、') || '订单商品'
|
||||
|
||||
return (
|
||||
<main className="collect-link-page">
|
||||
@@ -313,9 +317,14 @@ export default function OrderServicePage() {
|
||||
<div className="collect-link-heading">
|
||||
<div>
|
||||
<Title level={4}>{pageConfig?.title || '订单服务'}</Title>
|
||||
<Text type="secondary">订单号:{info.platformOrderId}</Text>
|
||||
<div className="collect-link-order-meta">
|
||||
<span>订单号:{info.platformOrderId}</span>
|
||||
<span>
|
||||
购买物品:<strong>{purchaseSummary}</strong>
|
||||
</span>
|
||||
</div>
|
||||
{pageConfig?.description ? (
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<div className="collect-link-description">
|
||||
<Text type="secondary">{pageConfig.description}</Text>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -387,9 +396,7 @@ export default function OrderServicePage() {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -398,8 +405,12 @@ function OrderServiceStatusCard({ item }: { item: OrderServiceItem }) {
|
||||
: null,
|
||||
].filter((row): row is { label: string; value: string } => Boolean(row?.value))
|
||||
const acceptanceImageUrls = item.acceptanceImageUrls || []
|
||||
const showSteps = item.state !== 'cancelled'
|
||||
const acceptanceVideo = item.acceptanceVideo
|
||||
const statusSteps = item.statusSteps || DEFAULT_STATUS_STEPS
|
||||
const statusStepKey = STATUS_STEP_KEY[item.state]
|
||||
const currentStepIndex = statusStepKey ? STATUS_STEP_ORDER.indexOf(statusStepKey) : -1
|
||||
const currentStep = statusStepKey ? statusSteps[statusStepKey] : null
|
||||
const currentStatusText = currentStep?.description?.trim() || item.message
|
||||
|
||||
return (
|
||||
<Card size="small" className="collect-link-status-card">
|
||||
@@ -409,37 +420,24 @@ function OrderServiceStatusCard({ item }: { item: OrderServiceItem }) {
|
||||
<Tag color={STATUS_TAG_COLORS[item.state] || 'default'}>{item.stateLabel}</Tag>
|
||||
</div>
|
||||
|
||||
{showSteps ? (
|
||||
<Steps
|
||||
className="collect-link-status-steps"
|
||||
current={currentStep}
|
||||
items={[
|
||||
{
|
||||
title: statusSteps.submitted.title,
|
||||
description: statusSteps.submitted.description || undefined,
|
||||
},
|
||||
{
|
||||
title: statusSteps.scheduled.title,
|
||||
description: statusSteps.scheduled.description || undefined,
|
||||
},
|
||||
{
|
||||
title: statusSteps.processing.title,
|
||||
description: statusSteps.processing.description || undefined,
|
||||
},
|
||||
{
|
||||
title: statusSteps.completed.title,
|
||||
description: statusSteps.completed.description || undefined,
|
||||
},
|
||||
]}
|
||||
responsive
|
||||
size="small"
|
||||
status={
|
||||
item.state === 'accepted' ? 'finish' : item.state === 'problem' ? 'error' : 'process'
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<SubmittedMaterialSummary item={item} />
|
||||
|
||||
<div className={`collect-link-status-message is-${item.state}`}>{item.message}</div>
|
||||
{currentStep && currentStepIndex >= 0 ? (
|
||||
<div className={`collect-link-current-status is-${item.state}`}>
|
||||
<span className="collect-link-current-status-index">{currentStepIndex + 1}</span>
|
||||
<div className="collect-link-current-status-copy">
|
||||
<div>
|
||||
<Text strong>{currentStep.title}</Text>
|
||||
<Text type="secondary">
|
||||
当前进度 {currentStepIndex + 1}/{STATUS_STEP_ORDER.length}
|
||||
</Text>
|
||||
</div>
|
||||
<p>{currentStatusText}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`collect-link-status-message is-${item.state}`}>{item.message}</div>
|
||||
)}
|
||||
|
||||
{times.length > 0 ? (
|
||||
<div className="collect-link-status-times">
|
||||
@@ -451,19 +449,60 @@ function OrderServiceStatusCard({ item }: { item: OrderServiceItem }) {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{acceptanceImageUrls.length > 0 ? (
|
||||
{acceptanceImageUrls.length > 0 || acceptanceVideo ? (
|
||||
<div className="collect-link-acceptance">
|
||||
<div>
|
||||
<Text strong>交付验收图片</Text>
|
||||
<Text type="secondary">点击图片可查看大图</Text>
|
||||
<Text strong>交付验收</Text>
|
||||
<Text type="secondary">
|
||||
{acceptanceVideo && acceptanceImageUrls.length > 0
|
||||
? '图片可点击查看大图,视频可直接播放'
|
||||
: acceptanceVideo
|
||||
? '可直接播放并确认交付内容'
|
||||
: '点击图片可查看大图'}
|
||||
</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} />
|
||||
))}
|
||||
{acceptanceVideo ? (
|
||||
<div className="collect-link-acceptance-video">
|
||||
<video
|
||||
controls
|
||||
playsInline
|
||||
preload="metadata"
|
||||
poster={acceptanceVideo.thumbnailUrl || undefined}
|
||||
src={acceptanceVideo.url}
|
||||
>
|
||||
您的浏览器暂不支持视频播放,请
|
||||
<a href={acceptanceVideo.url} rel="noreferrer" target="_blank">
|
||||
点击下载验收视频
|
||||
</a>
|
||||
</video>
|
||||
<div>
|
||||
<Text type="secondary">
|
||||
{acceptanceVideo.filename || '验收视频'}
|
||||
{acceptanceVideo.durationSeconds
|
||||
? ` · ${Math.ceil(acceptanceVideo.durationSeconds)} 秒`
|
||||
: ''}
|
||||
</Text>
|
||||
<a href={acceptanceVideo.url} rel="noreferrer" target="_blank">
|
||||
打开原视频
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Image.PreviewGroup>
|
||||
) : null}
|
||||
{acceptanceImageUrls.length > 0 ? (
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</Space>
|
||||
@@ -471,6 +510,72 @@ function OrderServiceStatusCard({ item }: { item: OrderServiceItem }) {
|
||||
)
|
||||
}
|
||||
|
||||
function SubmittedMaterialSummary({ item }: { item: OrderServiceItem }) {
|
||||
const values = item.values || {}
|
||||
const rows =
|
||||
item.fields && item.fields.length > 0
|
||||
? item.fields.map((field) => ({
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
value: String(values[field.key] || '').trim(),
|
||||
}))
|
||||
: Object.entries(values).map(([key, value]) => ({
|
||||
key,
|
||||
label: key,
|
||||
value: String(value || '').trim(),
|
||||
}))
|
||||
const filledRows = rows.filter((row) => row.value)
|
||||
const screenshots = item.screenshots || []
|
||||
|
||||
if (filledRows.length === 0 && screenshots.length === 0) return null
|
||||
|
||||
return (
|
||||
<section className="collect-link-submitted">
|
||||
<div className="collect-link-submitted-head">
|
||||
<span>
|
||||
<Text strong>买家提交的资料</Text>
|
||||
<Text type="secondary">交付时会按以下信息核对</Text>
|
||||
</span>
|
||||
{item.submittedAt ? (
|
||||
<Text type="secondary">提交于 {formatOrderServiceTime(item.submittedAt)}</Text>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{filledRows.length > 0 ? (
|
||||
<div className="collect-link-submitted-fields">
|
||||
{filledRows.map((row) => (
|
||||
<div key={row.key}>
|
||||
<span>{row.label}</span>
|
||||
<strong>{row.value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{screenshots.length > 0 ? (
|
||||
<div className="collect-link-submitted-screenshots">
|
||||
<span>
|
||||
{item.screenshotsRule?.label || '买家截图'}:{screenshots.length} 张
|
||||
</span>
|
||||
<Image.PreviewGroup>
|
||||
<div>
|
||||
{screenshots.map((file, index) => (
|
||||
<Image
|
||||
key={file.objectKey || file.url}
|
||||
alt={file.filename || `买家截图 ${index + 1}`}
|
||||
src={file.thumbnailUrl || file.url}
|
||||
width={68}
|
||||
height={68}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function OrderServiceForm({
|
||||
item,
|
||||
code,
|
||||
|
||||
@@ -614,6 +614,15 @@ export type OrderServiceItem = {
|
||||
screenshotsRule?: OrderServiceScreenshots
|
||||
guideImage?: UploadedFile | Record<string, unknown>
|
||||
acceptanceImageUrls?: string[]
|
||||
acceptanceVideo?: {
|
||||
url: string
|
||||
thumbnailUrl: string
|
||||
filename: string
|
||||
contentType: string
|
||||
durationSeconds: number | null
|
||||
width: number | null
|
||||
height: number | null
|
||||
}
|
||||
submittedAt?: string
|
||||
acceptanceSubmittedAt?: string
|
||||
acceptedAt?: string
|
||||
|
||||
@@ -526,19 +526,216 @@
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.collect-link-status-steps {
|
||||
padding: 4px 2px 8px;
|
||||
.collect-link-order-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 18px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.collect-link-status-steps .ant-steps-item-title {
|
||||
font-size: 12px !important;
|
||||
.collect-link-order-meta strong {
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.collect-link-status-steps .ant-steps-item-description {
|
||||
max-width: 140px;
|
||||
font-size: 11px !important;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
.collect-link-description {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.collect-link-submitted {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e7edf5;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 5px 14px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-head > span {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-head .ant-typography:last-child {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-head > .ant-typography {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-fields > div {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #edf1f6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-fields span,
|
||||
.collect-link-submitted-screenshots > span {
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-fields strong {
|
||||
color: #1e293b;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.collect-link-submitted-screenshots {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-screenshots > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-screenshots .ant-image {
|
||||
overflow: hidden;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.collect-link-submitted-screenshots .ant-image-img {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.collect-link-current-status {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding: 13px;
|
||||
color: #475569;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e8edf4;
|
||||
border-left: 4px solid #94a3b8;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.collect-link-current-status-index {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
background: #64748b;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.collect-link-current-status-copy {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.collect-link-current-status-copy > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px 12px;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.collect-link-current-status-copy > div .ant-typography:last-child {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.collect-link-current-status-copy p {
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-open,
|
||||
.collect-link-current-status.is-pending_material {
|
||||
color: #8a4b00;
|
||||
background: #fffaf0;
|
||||
border-color: #ffe7ba;
|
||||
border-left-color: #faad14;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-open .collect-link-current-status-index,
|
||||
.collect-link-current-status.is-pending_material .collect-link-current-status-index {
|
||||
background: #faad14;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-in_progress,
|
||||
.collect-link-current-status.is-unassigned {
|
||||
color: #0958d9;
|
||||
background: #f0f7ff;
|
||||
border-color: #d6e8ff;
|
||||
border-left-color: #1677ff;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-in_progress .collect-link-current-status-index,
|
||||
.collect-link-current-status.is-unassigned .collect-link-current-status-index {
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-pending_acceptance {
|
||||
color: #531dab;
|
||||
background: #f9f0ff;
|
||||
border-color: #efdbff;
|
||||
border-left-color: #722ed1;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-pending_acceptance .collect-link-current-status-index {
|
||||
background: #722ed1;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-accepted {
|
||||
color: #237804;
|
||||
background: #f6ffed;
|
||||
border-color: #d9f7be;
|
||||
border-left-color: #52c41a;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-accepted .collect-link-current-status-index {
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-problem {
|
||||
color: #a8071a;
|
||||
background: #fff1f0;
|
||||
border-color: #ffccc7;
|
||||
border-left-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.collect-link-current-status.is-problem .collect-link-current-status-index {
|
||||
background: #ff4d4f;
|
||||
}
|
||||
|
||||
.collect-link-status-message {
|
||||
@@ -626,6 +823,32 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.collect-link-acceptance-video {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.collect-link-acceptance-video video {
|
||||
width: min(100%, 520px);
|
||||
max-height: 360px;
|
||||
background: #0f172a;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.collect-link-acceptance-video > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 14px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.collect-link-acceptance-video a {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.collect-link-title {
|
||||
margin: 0 0 4px !important;
|
||||
}
|
||||
@@ -898,6 +1121,15 @@
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.collect-link-order-meta,
|
||||
.collect-link-submitted-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.collect-link-order-meta {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 兑换中心 (Redeem Page) ===== */
|
||||
|
||||
Reference in New Issue
Block a user