优化接单工单详情展示
This commit is contained in:
@@ -1301,6 +1301,8 @@ function mapWorkerUser(worker: WorkerUserRow) {
|
|||||||
function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
|
function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
|
||||||
return {
|
return {
|
||||||
workOrderId: Number(workOrder.id),
|
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,
|
workOrderNo: workOrder.work_order_no,
|
||||||
platformOrderId: workOrder.platform_order_id,
|
platformOrderId: workOrder.platform_order_id,
|
||||||
productName: workOrder.product_name,
|
productName: workOrder.product_name,
|
||||||
@@ -1336,7 +1338,14 @@ function mapWorkOrderForWorker(
|
|||||||
permissions: { depositFreeAmount: number },
|
permissions: { depositFreeAmount: number },
|
||||||
) {
|
) {
|
||||||
const mapped = mapWorkOrderAdmin(workOrder)
|
const mapped = mapWorkOrderAdmin(workOrder)
|
||||||
const { requiredDepositAmount, depositThresholdAmount, worker: _worker, ...visibleOrder } = mapped
|
const {
|
||||||
|
requiredDepositAmount,
|
||||||
|
depositThresholdAmount,
|
||||||
|
worker: _worker,
|
||||||
|
orderId: _orderId,
|
||||||
|
orderItemId: _orderItemId,
|
||||||
|
...visibleOrder
|
||||||
|
} = mapped
|
||||||
return {
|
return {
|
||||||
...visibleOrder,
|
...visibleOrder,
|
||||||
freezeDepositAmount: resolveFreezeDepositAmount(workOrder, permissions),
|
freezeDepositAmount: resolveFreezeDepositAmount(workOrder, permissions),
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Descriptions,
|
||||||
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
@@ -27,7 +29,9 @@ import {
|
|||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { TableColumnsType } from 'antd'
|
import type { TableColumnsType } from 'antd'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router'
|
||||||
|
|
||||||
|
import JsonPreview from '@/components/admin/JsonPreview'
|
||||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||||
import PageHeader from '@/components/admin/PageHeader'
|
import PageHeader from '@/components/admin/PageHeader'
|
||||||
import {
|
import {
|
||||||
@@ -63,6 +67,7 @@ import {
|
|||||||
ADMIN_DEFAULT_PAGE_SIZE,
|
ADMIN_DEFAULT_PAGE_SIZE,
|
||||||
buildAdminTablePagination,
|
buildAdminTablePagination,
|
||||||
} from '@/utils/admin-pagination'
|
} from '@/utils/admin-pagination'
|
||||||
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
|
|
||||||
export default function AdminWorkerPlatformPage() {
|
export default function AdminWorkerPlatformPage() {
|
||||||
return (
|
return (
|
||||||
@@ -119,10 +124,12 @@ function SummaryCards() {
|
|||||||
|
|
||||||
function WorkOrdersPanel() {
|
function WorkOrdersPanel() {
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [status, setStatus] = useState('')
|
const [status, setStatus] = useState('')
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||||
|
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
||||||
const [problemOrder, setProblemOrder] = useState<WorkOrder | null>(null)
|
const [problemOrder, setProblemOrder] = useState<WorkOrder | null>(null)
|
||||||
const [resolutionOrder, setResolutionOrder] = useState<WorkOrder | null>(null)
|
const [resolutionOrder, setResolutionOrder] = useState<WorkOrder | null>(null)
|
||||||
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
|
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
|
||||||
@@ -227,13 +234,27 @@ function WorkOrdersPanel() {
|
|||||||
materialForm.setFieldsValue({ fields: getMaterialFields(row) })
|
materialForm.setFieldsValue({ fields: getMaterialFields(row) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openDetailDrawer(row: WorkOrder) {
|
||||||
|
setDetailOrder(row)
|
||||||
|
}
|
||||||
|
|
||||||
const columns: TableColumnsType<WorkOrder> = [
|
const columns: TableColumnsType<WorkOrder> = [
|
||||||
{
|
{
|
||||||
title: '订单',
|
title: '订单',
|
||||||
minWidth: 300,
|
minWidth: 300,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<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">
|
<Typography.Text type="secondary">
|
||||||
{row.platformOrderId} · {row.workOrderNo}
|
{row.platformOrderId} · {row.workOrderNo}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@@ -283,9 +304,10 @@ function WorkOrdersPanel() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 340,
|
width: 400,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
|
<Button onClick={() => openDetailDrawer(row)}>详情</Button>
|
||||||
{row.status === 'pending_material' ? (
|
{row.status === 'pending_material' ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<FormOutlined />}
|
icon={<FormOutlined />}
|
||||||
@@ -412,10 +434,173 @@ function WorkOrdersPanel() {
|
|||||||
setPageSize(nextPageSize)
|
setPageSize(nextPageSize)
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
scroll={{ x: 1100 }}
|
scroll={{ x: 1240 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</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
|
<Modal
|
||||||
title="标记问题单"
|
title="标记问题单"
|
||||||
open={Boolean(problemOrder)}
|
open={Boolean(problemOrder)}
|
||||||
@@ -1188,6 +1373,13 @@ function getRequirementFields(order: WorkOrder | null): CollectField[] {
|
|||||||
.filter((item): item is CollectField => Boolean(item))
|
.filter((item): item is CollectField => Boolean(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DetailFieldItem = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
required: boolean
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
function getMaterialFields(order: WorkOrder): Record<string, string> {
|
function getMaterialFields(order: WorkOrder): Record<string, string> {
|
||||||
const material = asRecord(order.material)
|
const material = asRecord(order.material)
|
||||||
const collect = asRecord(material.collect)
|
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) {
|
function renderMaterialSummary(order: WorkOrder) {
|
||||||
|
const fieldLabelMap = new Map(
|
||||||
|
getRequirementFields(order).map((field) => [field.key, field.label]),
|
||||||
|
)
|
||||||
const fields = getMaterialFields(order)
|
const fields = getMaterialFields(order)
|
||||||
const entries = Object.entries(fields).filter(([, value]) =>
|
const entries = Object.entries(fields).filter(([, value]) =>
|
||||||
String(value || '').trim(),
|
String(value || '').trim(),
|
||||||
@@ -1210,7 +1483,7 @@ function renderMaterialSummary(order: WorkOrder) {
|
|||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
{entries.slice(0, 2).map(([key, value]) => (
|
{entries.slice(0, 2).map(([key, value]) => (
|
||||||
<Typography.Text key={key} ellipsis>
|
<Typography.Text key={key} ellipsis>
|
||||||
{key}: {value}
|
{fieldLabelMap.get(key) || key}: {value}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
))}
|
))}
|
||||||
{entries.length > 2 ? (
|
{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> {
|
function asRecord(value: unknown): Record<string, unknown> {
|
||||||
return value && typeof value === 'object' && !Array.isArray(value)
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
? (value as Record<string, unknown>)
|
? (value as Record<string, unknown>)
|
||||||
|
|||||||
@@ -1162,6 +1162,21 @@ select {
|
|||||||
min-width: 0;
|
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 {
|
.metric-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export type WorkOrderStatus =
|
|||||||
|
|
||||||
export type WorkOrder = {
|
export type WorkOrder = {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
|
orderId?: number | null
|
||||||
|
orderItemId?: number | null
|
||||||
workOrderNo: string
|
workOrderNo: string
|
||||||
platformOrderId: string
|
platformOrderId: string
|
||||||
productName: string
|
productName: string
|
||||||
|
|||||||
Reference in New Issue
Block a user