优化我的订单页
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
import { CheckOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
CheckOutlined,
|
||||
EyeOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
@@ -18,35 +26,116 @@ import { useState } from 'react'
|
||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import { fetchWorkerMyOrders, submitWorkerAcceptance } from '@/services/worker'
|
||||
import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
|
||||
import type { CollectField, UploadedFile, WorkOrder } from '@/types/worker-platform'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '', label: '全部订单' },
|
||||
{ value: 'in_progress', label: '代练中' },
|
||||
{ value: 'pending_acceptance', label: '待验收' },
|
||||
{ value: 'problem', label: '问题单' },
|
||||
{ value: 'accepted', label: '已验收' },
|
||||
{ value: 'cancelled', label: '已取消' },
|
||||
]
|
||||
] as const
|
||||
|
||||
const STATUS_OVERVIEW = [
|
||||
{
|
||||
key: 'in_progress',
|
||||
label: '代练中',
|
||||
note: '正在执行中的订单',
|
||||
tone: 'blue',
|
||||
},
|
||||
{
|
||||
key: 'pending_acceptance',
|
||||
label: '待验收',
|
||||
note: '已经提交,等待审核',
|
||||
tone: 'gold',
|
||||
},
|
||||
{
|
||||
key: 'problem',
|
||||
label: '问题单',
|
||||
note: '需要补充或重新处理',
|
||||
tone: 'red',
|
||||
},
|
||||
{
|
||||
key: 'accepted',
|
||||
label: '已验收',
|
||||
note: '已完成并结算的订单',
|
||||
tone: 'green',
|
||||
},
|
||||
{
|
||||
key: 'cancelled',
|
||||
label: '已取消',
|
||||
note: '已结束且不再继续',
|
||||
tone: 'slate',
|
||||
},
|
||||
] as const
|
||||
|
||||
type AcceptanceFormValues = {
|
||||
note?: string
|
||||
}
|
||||
|
||||
type DetailFieldItem = {
|
||||
key: string
|
||||
label: string
|
||||
required: boolean
|
||||
value: string
|
||||
}
|
||||
|
||||
export default function WorkerOrdersPage() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [status, setStatus] = useState('')
|
||||
const [keywordInput, setKeywordInput] = useState('')
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
||||
const [submittingOrder, setSubmittingOrder] = useState<WorkOrder | null>(null)
|
||||
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
|
||||
const [form] = Form.useForm()
|
||||
const [form] = Form.useForm<AcceptanceFormValues>()
|
||||
|
||||
const ordersQuery = useQuery({
|
||||
queryKey: ['worker-my-orders', status, page, pageSize],
|
||||
queryFn: () => fetchWorkerMyOrders({ status, page, pageSize }),
|
||||
queryKey: ['worker-my-orders', status, keyword, page, pageSize],
|
||||
queryFn: () => fetchWorkerMyOrders({ status, keyword, page, pageSize }),
|
||||
retry: false,
|
||||
})
|
||||
const pagination = ordersQuery.data?.data.pagination
|
||||
const orders = ordersQuery.data?.data.items || []
|
||||
|
||||
async function submitAcceptance(values: { note?: string }) {
|
||||
const overviewQuery = useQuery({
|
||||
queryKey: ['worker-my-orders-overview'],
|
||||
queryFn: async () => {
|
||||
const results = await Promise.all(
|
||||
STATUS_OPTIONS.map(async (item) => {
|
||||
const response = await fetchWorkerMyOrders({
|
||||
status: item.value,
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
})
|
||||
return [item.value || 'all', response.data.pagination.total] as const
|
||||
}),
|
||||
)
|
||||
return Object.fromEntries(results) as Record<string, number>
|
||||
},
|
||||
retry: false,
|
||||
})
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }),
|
||||
])
|
||||
}
|
||||
|
||||
function openAcceptanceModal(order: WorkOrder) {
|
||||
setSubmittingOrder(order)
|
||||
setAcceptanceFiles([])
|
||||
form.resetFields()
|
||||
setDetailOrder(null)
|
||||
}
|
||||
|
||||
async function submitAcceptance(values: AcceptanceFormValues) {
|
||||
if (!submittingOrder) return
|
||||
if (acceptanceFiles.length === 0) {
|
||||
message.error('请上传验收图片')
|
||||
@@ -61,76 +150,110 @@ export default function WorkerOrdersPage() {
|
||||
setSubmittingOrder(null)
|
||||
setAcceptanceFiles([])
|
||||
form.resetFields()
|
||||
await queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] })
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '提交失败')
|
||||
}
|
||||
}
|
||||
|
||||
function applyKeywordSearch() {
|
||||
setKeyword(keywordInput.trim())
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<WorkOrder> = [
|
||||
{
|
||||
title: '订单',
|
||||
minWidth: 280,
|
||||
title: '订单信息',
|
||||
minWidth: 320,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong>{row.productName}</Typography.Text>
|
||||
<Typography.Link onClick={() => setDetailOrder(row)}>
|
||||
{row.productName || row.workOrderNo}
|
||||
</Typography.Link>
|
||||
<Space size={[6, 6]} wrap>
|
||||
{row.categoryName ? <Tag>{row.categoryName}</Tag> : null}
|
||||
<Typography.Text type="secondary">
|
||||
工单号:{row.workOrderNo}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
<Typography.Text type="secondary">
|
||||
{row.platformOrderId}
|
||||
订单号:{row.platformOrderId || '-'}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
接单时间:{formatDateTime(row.assignedAt)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
width: 130,
|
||||
title: '奖励 / 押金',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<Typography.Text strong>
|
||||
{formatMoney(row.rewardAmount)}
|
||||
</Typography.Text>
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong>{formatMoney(row.rewardAmount)}</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
冻结押金:{formatMoney(row.freezeDepositAmount)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 140,
|
||||
title: '当前状态',
|
||||
width: 180,
|
||||
render: (_, row) => (
|
||||
<Tag color={resolveStatusColor(row.status)}>
|
||||
{formatStatus(row.status)}
|
||||
</Tag>
|
||||
<div className="cell-stack">
|
||||
<Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>
|
||||
<Typography.Text type="secondary">
|
||||
{getStatusHint(row.status)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '验收图片',
|
||||
width: 170,
|
||||
title: '验收信息',
|
||||
minWidth: 210,
|
||||
render: (_, row) => (
|
||||
<ImagePreviewList
|
||||
files={getAcceptanceFiles(row)}
|
||||
imageUrls={getAcceptanceImageUrls(row)}
|
||||
size={46}
|
||||
/>
|
||||
<div className="cell-stack">
|
||||
<ImagePreviewList
|
||||
files={getAcceptanceFiles(row)}
|
||||
imageUrls={getAcceptanceImageUrls(row)}
|
||||
size={46}
|
||||
/>
|
||||
<Typography.Text type="secondary">
|
||||
提交时间:{formatDateTime(getAcceptanceSubmittedAt(row))}
|
||||
</Typography.Text>
|
||||
{row.acceptance?.note ? (
|
||||
<Typography.Text ellipsis>{row.acceptance.note}</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="secondary">暂无验收说明</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '问题备注',
|
||||
dataIndex: 'problemNote',
|
||||
minWidth: 180,
|
||||
render: (value) => String(value || '-'),
|
||||
render: (_, row) => (
|
||||
<Typography.Text ellipsis>
|
||||
{String(row.problemNote || '').trim() || '-'}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 140,
|
||||
render: (_, row) =>
|
||||
['in_progress', 'problem'].includes(row.status) ? (
|
||||
<Button
|
||||
icon={<CheckOutlined />}
|
||||
onClick={() => {
|
||||
setSubmittingOrder(row)
|
||||
setAcceptanceFiles([])
|
||||
form.resetFields()
|
||||
}}
|
||||
>
|
||||
提交验收
|
||||
width: 200,
|
||||
render: (_, row) => (
|
||||
<Space wrap>
|
||||
<Button icon={<EyeOutlined />} onClick={() => setDetailOrder(row)}>
|
||||
详情
|
||||
</Button>
|
||||
) : null,
|
||||
{canSubmitAcceptance(row) ? (
|
||||
<Button icon={<CheckOutlined />} type="primary" onClick={() => openAcceptanceModal(row)}>
|
||||
提交验收
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -138,45 +261,250 @@ export default function WorkerOrdersPage() {
|
||||
<section className="page-stack">
|
||||
<div className="worker-page-head">
|
||||
<Typography.Title level={3}>我的订单</Typography.Title>
|
||||
<Space>
|
||||
<Select
|
||||
value={status}
|
||||
options={STATUS_OPTIONS}
|
||||
style={{ width: 140 }}
|
||||
onChange={(nextStatus) => {
|
||||
setStatus(nextStatus)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={ordersQuery.isFetching}
|
||||
onClick={() => ordersQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={ordersQuery.isFetching || overviewQuery.isFetching}
|
||||
onClick={() => refreshAll()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table<WorkOrder>
|
||||
rowKey="workOrderId"
|
||||
loading={ordersQuery.isLoading}
|
||||
dataSource={ordersQuery.data?.data.items || []}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: pagination?.page || page,
|
||||
pageSize: pagination?.pageSize || pageSize,
|
||||
total: pagination?.total || 0,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
},
|
||||
}}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
<div className="worker-orders-overview-grid">
|
||||
<Card className="worker-orders-overview-card total" bordered={false}>
|
||||
<span className="worker-orders-overview-label">全部订单</span>
|
||||
<strong className="worker-orders-overview-value">
|
||||
{Number(overviewQuery.data?.all || 0)}
|
||||
</strong>
|
||||
<span className="worker-orders-overview-note">当前账号历史接单总数</span>
|
||||
</Card>
|
||||
{STATUS_OVERVIEW.map((item) => (
|
||||
<Card
|
||||
key={item.key}
|
||||
className={`worker-orders-overview-card tone-${item.tone}`}
|
||||
bordered={false}
|
||||
>
|
||||
<span className="worker-orders-overview-label">{item.label}</span>
|
||||
<strong className="worker-orders-overview-value">
|
||||
{Number(overviewQuery.data?.[item.key] || 0)}
|
||||
</strong>
|
||||
<span className="worker-orders-overview-note">{item.note}</span>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card bordered={false} className="worker-orders-table-card">
|
||||
<div className="worker-orders-toolbar">
|
||||
<Space.Compact className="worker-orders-search">
|
||||
<Input
|
||||
allowClear
|
||||
value={keywordInput}
|
||||
placeholder="搜索工单号 / 订单号 / 商品名"
|
||||
prefix={<SearchOutlined />}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value
|
||||
setKeywordInput(nextValue)
|
||||
if (!nextValue.trim()) {
|
||||
setKeyword('')
|
||||
setPage(1)
|
||||
}
|
||||
}}
|
||||
onPressEnter={applyKeywordSearch}
|
||||
/>
|
||||
<Button
|
||||
icon={<SearchOutlined />}
|
||||
loading={ordersQuery.isFetching}
|
||||
onClick={applyKeywordSearch}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<Typography.Text type="secondary">
|
||||
当前筛选:{STATUS_OPTIONS.find((item) => item.value === status)?.label || '全部订单'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="worker-orders-status-tabs">
|
||||
{STATUS_OPTIONS.map((item) => {
|
||||
const active = status === item.value
|
||||
const countKey = item.value || 'all'
|
||||
return (
|
||||
<button
|
||||
key={item.value || 'all'}
|
||||
type="button"
|
||||
className={`worker-orders-status-tab${active ? ' is-active' : ''}`}
|
||||
onClick={() => {
|
||||
setStatus(item.value)
|
||||
setPage(1)
|
||||
}}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<strong>{Number(overviewQuery.data?.[countKey] || 0)}</strong>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Table<WorkOrder>
|
||||
rowKey="workOrderId"
|
||||
loading={ordersQuery.isLoading}
|
||||
dataSource={orders}
|
||||
columns={columns}
|
||||
locale={{
|
||||
emptyText: ordersQuery.error ? (
|
||||
<Empty
|
||||
description={
|
||||
ordersQuery.error instanceof Error
|
||||
? ordersQuery.error.message
|
||||
: '读取我的订单失败'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Empty
|
||||
description={
|
||||
keyword
|
||||
? `没有找到与 “${keyword}” 相关的订单`
|
||||
: '当前筛选下暂无订单'
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
pagination={{
|
||||
current: pagination?.page || page,
|
||||
pageSize: pagination?.pageSize || pageSize,
|
||||
total: pagination?.total || 0,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
},
|
||||
}}
|
||||
scroll={{ x: 1160 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
detailOrder
|
||||
? `${detailOrder.productName || '订单详情'} · ${detailOrder.workOrderNo}`
|
||||
: '订单详情'
|
||||
}
|
||||
width={820}
|
||||
open={Boolean(detailOrder)}
|
||||
destroyOnHidden
|
||||
onClose={() => setDetailOrder(null)}
|
||||
extra={
|
||||
detailOrder ? (
|
||||
<Space>
|
||||
<Tag color={resolveStatusColor(detailOrder.status)}>
|
||||
{formatStatus(detailOrder.status)}
|
||||
</Tag>
|
||||
{canSubmitAcceptance(detailOrder) ? (
|
||||
<Button type="primary" icon={<CheckOutlined />} onClick={() => openAcceptanceModal(detailOrder)}>
|
||||
提交验收
|
||||
</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.productName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">
|
||||
{detailOrder.categoryName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="工单号">
|
||||
{detailOrder.workOrderNo}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台订单号">
|
||||
{detailOrder.platformOrderId || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={resolveStatusColor(detailOrder.status)}>
|
||||
{formatStatus(detailOrder.status)}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="阶段说明">
|
||||
{getStatusHint(detailOrder.status)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="接单奖励">
|
||||
{formatMoney(detailOrder.rewardAmount)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="冻结押金">
|
||||
{formatMoney(detailOrder.freezeDepositAmount)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="接单时间">
|
||||
{formatDateTime(detailOrder.assignedAt)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提交验收">
|
||||
{formatDateTime(getAcceptanceSubmittedAt(detailOrder))}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="验收完成">
|
||||
{formatDateTime(detailOrder.acceptedAt)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">
|
||||
{formatDateTime(detailOrder.updatedAt)}
|
||||
</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="验收图片数量">
|
||||
{getAcceptanceFiles(detailOrder).length +
|
||||
getAcceptanceImageUrls(detailOrder).length || 0}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提交时间">
|
||||
{formatDateTime(getAcceptanceSubmittedAt(detailOrder))}
|
||||
</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={76}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text type="secondary">暂无验收图片</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{String(detailOrder.problemNote || '').trim() ? (
|
||||
<Card title="问题备注" size="small">
|
||||
<span className="worker-order-detail-value">
|
||||
{String(detailOrder.problemNote || '').trim()}
|
||||
</span>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title="提交验收"
|
||||
@@ -191,7 +519,10 @@ export default function WorkerOrdersPage() {
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={submitAcceptance}>
|
||||
<Form.Item label="完成说明" name="note">
|
||||
<Input.TextArea rows={3} />
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="可补充代练结果、注意事项或截图说明。"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="验收图片" required>
|
||||
<ImageUpload
|
||||
@@ -208,6 +539,113 @@ export default function WorkerOrdersPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function canSubmitAcceptance(order: WorkOrder) {
|
||||
return ['in_progress', 'problem'].includes(order.status)
|
||||
}
|
||||
|
||||
function getRequirementFields(order: WorkOrder | null): CollectField[] {
|
||||
if (!order) return []
|
||||
const rawFields = Array.isArray(order.requirement?.fields)
|
||||
? order.requirement.fields
|
||||
: []
|
||||
return rawFields
|
||||
.map((item) => {
|
||||
const source = asRecord(item)
|
||||
const key = String(source.key || '').trim()
|
||||
if (!key) return null
|
||||
return {
|
||||
key,
|
||||
label: String(source.label || key).trim(),
|
||||
required: source.required !== false,
|
||||
}
|
||||
})
|
||||
.filter((item): item is CollectField => Boolean(item))
|
||||
}
|
||||
|
||||
function getMaterialFields(order: WorkOrder): Record<string, string> {
|
||||
const material = asRecord(order.material)
|
||||
const collect = asRecord(material.collect)
|
||||
const fields = asRecord(collect.fields || material.fields)
|
||||
return Object.fromEntries(
|
||||
Object.entries(fields).map(([key, value]) => [key, String(value || '')]),
|
||||
)
|
||||
}
|
||||
|
||||
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 getAcceptanceFiles(row: WorkOrder): UploadedFile[] {
|
||||
return Array.isArray(row.acceptance?.files) ? row.acceptance.files : []
|
||||
}
|
||||
@@ -220,6 +658,20 @@ 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>)
|
||||
: {}
|
||||
}
|
||||
|
||||
function formatMoney(value: number | undefined) {
|
||||
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
||||
}
|
||||
@@ -235,6 +687,17 @@ function formatStatus(status: string) {
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
function getStatusHint(status: string) {
|
||||
const hints: Record<string, string> = {
|
||||
in_progress: '订单进行中,完成后请及时提交验收资料。',
|
||||
pending_acceptance: '已提交验收,等待后台审核。',
|
||||
problem: '后台已标记问题,请根据备注调整后重新提交。',
|
||||
accepted: '订单已完成验收,可以在个人中心查看结算记录。',
|
||||
cancelled: '订单已取消,本次流程已结束。',
|
||||
}
|
||||
return hints[status] || '请关注当前订单状态变化。'
|
||||
}
|
||||
|
||||
function resolveStatusColor(status: string) {
|
||||
if (status === 'accepted') return 'green'
|
||||
if (status === 'cancelled') return 'default'
|
||||
|
||||
@@ -1688,6 +1688,127 @@ select {
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.worker-orders-overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.worker-orders-overview-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.worker-orders-overview-card .ant-card-body {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.worker-orders-overview-card.total {
|
||||
background:
|
||||
linear-gradient(135deg, rgba(20, 50, 74, 0.96), rgba(27, 93, 115, 0.92)),
|
||||
#14324a;
|
||||
}
|
||||
|
||||
.worker-orders-overview-card.total .worker-orders-overview-label,
|
||||
.worker-orders-overview-card.total .worker-orders-overview-value,
|
||||
.worker-orders-overview-card.total .worker-orders-overview-note {
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.worker-orders-overview-card.tone-blue {
|
||||
border-top: 3px solid #2563eb;
|
||||
}
|
||||
|
||||
.worker-orders-overview-card.tone-gold {
|
||||
border-top: 3px solid #d97706;
|
||||
}
|
||||
|
||||
.worker-orders-overview-card.tone-red {
|
||||
border-top: 3px solid #dc2626;
|
||||
}
|
||||
|
||||
.worker-orders-overview-card.tone-green {
|
||||
border-top: 3px solid #059669;
|
||||
}
|
||||
|
||||
.worker-orders-overview-card.tone-slate {
|
||||
border-top: 3px solid #64748b;
|
||||
}
|
||||
|
||||
.worker-orders-overview-label {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.worker-orders-overview-value {
|
||||
color: #0f172a;
|
||||
font-size: 28px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.worker-orders-overview-note {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.worker-orders-table-card .ant-card-body {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.worker-orders-toolbar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.worker-orders-search {
|
||||
width: min(520px, 100%);
|
||||
}
|
||||
|
||||
.worker-orders-status-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.worker-orders-status-tab {
|
||||
padding: 10px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #dbe4ee;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background 0.18s ease,
|
||||
color 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.worker-orders-status-tab:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #90b7cf;
|
||||
}
|
||||
|
||||
.worker-orders-status-tab strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.worker-orders-status-tab.is-active {
|
||||
border-color: transparent;
|
||||
background: linear-gradient(135deg, #1b5d73, #237b77);
|
||||
color: #f8fafc;
|
||||
box-shadow: 0 10px 24px rgba(27, 93, 115, 0.24);
|
||||
}
|
||||
|
||||
.worker-profile-hero {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(25, 94, 125, 0.14);
|
||||
@@ -1962,6 +2083,19 @@ select {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.worker-orders-toolbar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.worker-orders-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.worker-orders-status-tab {
|
||||
flex: 1 1 calc(50% - 10px);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.worker-profile-hero-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user