feat(order-service): 客户页展示验收图片与订单进度

This commit is contained in:
yml2213
2026-11-04 12:50:56 +08:00
parent a276256d84
commit 3f7a3ab5d1
5 changed files with 320 additions and 11 deletions
@@ -11,6 +11,7 @@ import {
Card,
DatePicker,
Form,
Image,
Input,
InputNumber,
Radio,
@@ -18,6 +19,7 @@ import {
Select,
Space,
Spin,
Steps,
Tag,
Typography,
} from 'antd'
@@ -40,6 +42,32 @@ 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()
@@ -345,17 +373,7 @@ export default function OrderServicePage() {
onSubmit={(values) => void handleSubmit(item, values)}
/>
) : (
<Card key={item.workOrderId} size="small">
<Space direction="vertical" size={6} className="full-width">
<Space size={8} wrap>
<strong>{item.productName || item.categoryName || '订单商品'}</strong>
<Tag color={item.state === 'accepted' ? 'green' : 'blue'}>
{item.stateLabel}
</Tag>
</Space>
<Text type="secondary">{item.message}</Text>
</Space>
</Card>
<OrderServiceStatusCard key={item.workOrderId} item={item} />
),
)}
@@ -369,6 +387,73 @@ 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,
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,
@@ -619,3 +704,8 @@ function normalizeFormFieldValues(item: OrderServiceItem, values: Record<string,
}),
)
}
function formatOrderServiceTime(value: string) {
const parsed = dayjs(value)
return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm') : value
}
+4
View File
@@ -605,6 +605,10 @@ export type OrderServiceItem = {
screenshots?: UploadedFile[]
screenshotsRule?: OrderServiceScreenshots
guideImage?: UploadedFile | Record<string, unknown>
acceptanceImageUrls?: string[]
submittedAt?: string
acceptanceSubmittedAt?: string
acceptedAt?: string
}
export type OrderServiceInfo = {
+115
View File
@@ -504,6 +504,121 @@
padding: 16px;
}
.collect-link-status-card {
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
}
.collect-link-status-card .ant-card-body {
padding: 16px;
}
.collect-link-status-head {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
color: #0f172a;
font-size: 15px;
}
.collect-link-status-head .ant-tag {
margin-inline-end: 0;
}
.collect-link-status-steps {
padding: 4px 2px 8px;
}
.collect-link-status-steps .ant-steps-item-title {
font-size: 12px !important;
}
.collect-link-status-message {
padding: 10px 12px;
color: #475569;
font-size: 13px;
line-height: 1.65;
background: #f8fafc;
border-left: 3px solid #94a3b8;
border-radius: 0 8px 8px 0;
}
.collect-link-status-message.is-open,
.collect-link-status-message.is-pending_material {
color: #8a4b00;
background: #fffaf0;
border-left-color: #faad14;
}
.collect-link-status-message.is-in_progress,
.collect-link-status-message.is-unassigned {
color: #0958d9;
background: #f0f7ff;
border-left-color: #1677ff;
}
.collect-link-status-message.is-pending_acceptance {
color: #531dab;
background: #f9f0ff;
border-left-color: #722ed1;
}
.collect-link-status-message.is-accepted {
color: #237804;
background: #f6ffed;
border-left-color: #52c41a;
}
.collect-link-status-message.is-problem {
color: #a8071a;
background: #fff1f0;
border-left-color: #ff4d4f;
}
.collect-link-status-times {
display: flex;
flex-wrap: wrap;
gap: 4px 18px;
color: #94a3b8;
font-size: 12px;
}
.collect-link-acceptance {
display: grid;
gap: 10px;
padding-top: 12px;
border-top: 1px solid #eef2f7;
}
.collect-link-acceptance > div:first-child {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: 4px 12px;
}
.collect-link-acceptance > div:first-child .ant-typography:last-child {
font-size: 12px;
}
.collect-link-acceptance-images {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.collect-link-acceptance-images .ant-image {
overflow: hidden;
border: 1px solid #dbe3ee;
border-radius: 9px;
}
.collect-link-acceptance-images .ant-image-img {
object-fit: cover;
}
.collect-link-title {
margin: 0 0 4px !important;
}