优化接单工单详情展示

This commit is contained in:
yml2213
2026-07-28 10:24:49 +08:00
parent f0ba0fe17f
commit dca45afcc8
4 changed files with 312 additions and 5 deletions
@@ -1301,6 +1301,8 @@ function mapWorkerUser(worker: WorkerUserRow) {
function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
return {
workOrderId: Number(workOrder.id),
orderId: workOrder.order_id ? Number(workOrder.order_id) : null,
orderItemId: workOrder.order_item_id ? Number(workOrder.order_item_id) : null,
workOrderNo: workOrder.work_order_no,
platformOrderId: workOrder.platform_order_id,
productName: workOrder.product_name,
@@ -1336,7 +1338,14 @@ function mapWorkOrderForWorker(
permissions: { depositFreeAmount: number },
) {
const mapped = mapWorkOrderAdmin(workOrder)
const { requiredDepositAmount, depositThresholdAmount, worker: _worker, ...visibleOrder } = mapped
const {
requiredDepositAmount,
depositThresholdAmount,
worker: _worker,
orderId: _orderId,
orderItemId: _orderItemId,
...visibleOrder
} = mapped
return {
...visibleOrder,
freezeDepositAmount: resolveFreezeDepositAmount(workOrder, permissions),
@@ -12,6 +12,8 @@ import {
App,
Button,
Card,
Descriptions,
Drawer,
Form,
Input,
InputNumber,
@@ -27,7 +29,9 @@ import {
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import { useNavigate } from 'react-router'
import JsonPreview from '@/components/admin/JsonPreview'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import PageHeader from '@/components/admin/PageHeader'
import {
@@ -63,6 +67,7 @@ import {
ADMIN_DEFAULT_PAGE_SIZE,
buildAdminTablePagination,
} from '@/utils/admin-pagination'
import { formatAdminDateTime } from '@/utils/admin-time'
export default function AdminWorkerPlatformPage() {
return (
@@ -119,10 +124,12 @@ function SummaryCards() {
function WorkOrdersPanel() {
const { message } = App.useApp()
const navigate = useNavigate()
const queryClient = useQueryClient()
const [status, setStatus] = useState('')
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
const [problemOrder, setProblemOrder] = useState<WorkOrder | null>(null)
const [resolutionOrder, setResolutionOrder] = useState<WorkOrder | null>(null)
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
@@ -227,13 +234,27 @@ function WorkOrdersPanel() {
materialForm.setFieldsValue({ fields: getMaterialFields(row) })
}
function openDetailDrawer(row: WorkOrder) {
setDetailOrder(row)
}
const columns: TableColumnsType<WorkOrder> = [
{
title: '订单',
minWidth: 300,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong>{row.productName}</Typography.Text>
<Typography.Link onClick={() => openDetailDrawer(row)}>
{row.productName || row.workOrderNo}
</Typography.Link>
<Space size={[6, 6]} wrap>
{row.categoryName ? <Tag>{row.categoryName}</Tag> : null}
{row.orderId ? (
<Typography.Text type="secondary">
#{row.orderId}
</Typography.Text>
) : null}
</Space>
<Typography.Text type="secondary">
{row.platformOrderId} · {row.workOrderNo}
</Typography.Text>
@@ -283,9 +304,10 @@ function WorkOrdersPanel() {
},
{
title: '操作',
width: 340,
width: 400,
render: (_, row) => (
<Space wrap>
<Button onClick={() => openDetailDrawer(row)}></Button>
{row.status === 'pending_material' ? (
<Button
icon={<FormOutlined />}
@@ -412,10 +434,173 @@ function WorkOrdersPanel() {
setPageSize(nextPageSize)
},
})}
scroll={{ x: 1100 }}
scroll={{ x: 1240 }}
/>
</Card>
<Drawer
title={
detailOrder
? `${detailOrder.productName || '工单详情'} · ${detailOrder.workOrderNo}`
: '工单详情'
}
width={860}
open={Boolean(detailOrder)}
destroyOnHidden
onClose={() => setDetailOrder(null)}
extra={
detailOrder ? (
<Space>
<Tag color={resolveStatusColor(detailOrder.status)}>
{formatStatus(detailOrder.status)}
</Tag>
{detailOrder.orderId ? (
<Button
onClick={() => {
const orderId = detailOrder.orderId
setDetailOrder(null)
navigate(`/admin/orders/${orderId}`)
}}
>
</Button>
) : null}
</Space>
) : null
}
>
{detailOrder ? (
<div className="worker-order-detail-stack">
<Card title="基础信息" size="small">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="工单号">
{detailOrder.workOrderNo}
</Descriptions.Item>
<Descriptions.Item label="平台订单号">
{detailOrder.platformOrderId || '-'}
</Descriptions.Item>
<Descriptions.Item label="源订单 ID">
{detailOrder.orderId || '-'}
</Descriptions.Item>
<Descriptions.Item label="订单子项 ID">
{detailOrder.orderItemId || '-'}
</Descriptions.Item>
<Descriptions.Item label="商品">
{detailOrder.productName || '-'}
</Descriptions.Item>
<Descriptions.Item label="分类">
{detailOrder.categoryName || '-'}
</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={resolveStatusColor(detailOrder.status)}>
{formatStatus(detailOrder.status)}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="打手">
{detailOrder.worker?.displayName ||
detailOrder.worker?.username ||
'-'}
</Descriptions.Item>
<Descriptions.Item label="接单金额">
{formatMoney(detailOrder.rewardAmount)}
</Descriptions.Item>
<Descriptions.Item label="押金">
{formatMoney(detailOrder.requiredDepositAmount)}
</Descriptions.Item>
<Descriptions.Item label="发布时间">
{formatAdminDateTime(detailOrder.publishedAt)}
</Descriptions.Item>
<Descriptions.Item label="接单时间">
{formatAdminDateTime(detailOrder.assignedAt)}
</Descriptions.Item>
<Descriptions.Item label="提交验收">
{formatAdminDateTime(getAcceptanceSubmittedAt(detailOrder))}
</Descriptions.Item>
<Descriptions.Item label="验收通过">
{formatAdminDateTime(detailOrder.acceptedAt)}
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{formatAdminDateTime(detailOrder.createdAt)}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{formatAdminDateTime(detailOrder.updatedAt)}
</Descriptions.Item>
<Descriptions.Item label="问题备注" span={2}>
<span className="worker-order-detail-value">
{String(detailOrder.problemNote || '').trim() || '-'}
</span>
</Descriptions.Item>
</Descriptions>
</Card>
<Card title="资料信息" size="small">
{renderDetailFieldValues(
getMaterialDetailItems(detailOrder),
'当前还没有填写资料',
)}
</Card>
<Card title="资料要求" size="small">
{renderRequirementFieldValues(detailOrder)}
</Card>
<Card title="验收信息" size="small">
<div className="worker-order-detail-stack">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="提交时间">
{formatAdminDateTime(getAcceptanceSubmittedAt(detailOrder))}
</Descriptions.Item>
<Descriptions.Item label="验收图片">
{getAcceptanceFiles(detailOrder).length +
getAcceptanceImageUrls(detailOrder).length || '-'}
</Descriptions.Item>
<Descriptions.Item label="备注" span={2}>
<span className="worker-order-detail-value">
{String(detailOrder.acceptance?.note || '').trim() || '-'}
</span>
</Descriptions.Item>
</Descriptions>
{getAcceptanceFiles(detailOrder).length ||
getAcceptanceImageUrls(detailOrder).length ? (
<ImagePreviewList
files={getAcceptanceFiles(detailOrder)}
imageUrls={getAcceptanceImageUrls(detailOrder)}
size={72}
/>
) : (
<Typography.Text type="secondary">
</Typography.Text>
)}
</div>
</Card>
<Card title="原始数据" size="small">
<Tabs
className="worker-order-detail-json-tabs"
items={[
{
key: 'material',
label: '资料',
children: <JsonPreview value={detailOrder.material} />,
},
{
key: 'requirement',
label: '需求',
children: <JsonPreview value={detailOrder.requirement} />,
},
{
key: 'acceptance',
label: '验收',
children: <JsonPreview value={detailOrder.acceptance} />,
},
]}
/>
</Card>
</div>
) : null}
</Drawer>
<Modal
title="标记问题单"
open={Boolean(problemOrder)}
@@ -1188,6 +1373,13 @@ function getRequirementFields(order: WorkOrder | null): CollectField[] {
.filter((item): item is CollectField => Boolean(item))
}
type DetailFieldItem = {
key: string
label: string
required: boolean
value: string
}
function getMaterialFields(order: WorkOrder): Record<string, string> {
const material = asRecord(order.material)
const collect = asRecord(material.collect)
@@ -1197,7 +1389,88 @@ function getMaterialFields(order: WorkOrder): Record<string, string> {
)
}
function getMaterialDetailItems(order: WorkOrder): DetailFieldItem[] {
const values = getMaterialFields(order)
const requiredFields = getRequirementFields(order)
const usedKeys = new Set<string>()
const items = requiredFields.map((field) => {
usedKeys.add(field.key)
return {
key: field.key,
label: field.label,
required: field.required,
value: String(values[field.key] || ''),
}
})
Object.entries(values).forEach(([key, value]) => {
if (usedKeys.has(key)) return
items.push({
key,
label: key,
required: false,
value: String(value || ''),
})
})
return items
}
function renderDetailFieldValues(
items: DetailFieldItem[],
emptyText: string,
) {
if (items.length === 0) {
return <Typography.Text type="secondary">{emptyText}</Typography.Text>
}
return (
<Descriptions column={1} bordered size="small">
{items.map((item) => (
<Descriptions.Item
key={item.key}
label={
<Space size={6} wrap>
<span>{item.label}</span>
{item.required ? <Tag color="orange"></Tag> : null}
</Space>
}
>
<span className="worker-order-detail-value">
{String(item.value || '').trim() || '-'}
</span>
</Descriptions.Item>
))}
</Descriptions>
)
}
function renderRequirementFieldValues(order: WorkOrder) {
const fields = getRequirementFields(order)
if (fields.length === 0) {
return <Typography.Text type="secondary"></Typography.Text>
}
return (
<Descriptions column={1} bordered size="small">
{fields.map((field) => (
<Descriptions.Item key={field.key} label={field.label}>
<Space size={6} wrap>
<Typography.Text code>{field.key}</Typography.Text>
<Tag color={field.required ? 'orange' : 'default'}>
{field.required ? '必填' : '选填'}
</Tag>
</Space>
</Descriptions.Item>
))}
</Descriptions>
)
}
function renderMaterialSummary(order: WorkOrder) {
const fieldLabelMap = new Map(
getRequirementFields(order).map((field) => [field.key, field.label]),
)
const fields = getMaterialFields(order)
const entries = Object.entries(fields).filter(([, value]) =>
String(value || '').trim(),
@@ -1210,7 +1483,7 @@ function renderMaterialSummary(order: WorkOrder) {
<div className="cell-stack">
{entries.slice(0, 2).map(([key, value]) => (
<Typography.Text key={key} ellipsis>
{key}: {value}
{fieldLabelMap.get(key) || key}: {value}
</Typography.Text>
))}
{entries.length > 2 ? (
@@ -1234,6 +1507,14 @@ function getAcceptanceImageUrls(row: WorkOrder): string[] {
: []
}
function getAcceptanceSubmittedAt(order: WorkOrder): string | null {
const acceptanceSubmittedAt =
typeof order.acceptance?.submittedAt === 'string'
? order.acceptance.submittedAt
: null
return order.submittedAt || acceptanceSubmittedAt || null
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
+15
View File
@@ -1162,6 +1162,21 @@ select {
min-width: 0;
}
.worker-order-detail-stack {
display: grid;
gap: 16px;
}
.worker-order-detail-value {
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}
.worker-order-detail-json-tabs > .ant-tabs-nav {
margin-bottom: 12px;
}
.metric-grid {
display: grid;
gap: 12px;
@@ -92,6 +92,8 @@ export type WorkOrderStatus =
export type WorkOrder = {
workOrderId: number
orderId?: number | null
orderItemId?: number | null
workOrderNo: string
platformOrderId: string
productName: string