优化接单工单详情展示
This commit is contained in:
@@ -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>)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user