From cdfee71c977c60d55b70110a3412e17550e5f22e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 28 Jul 2026 11:06:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=88=91=E7=9A=84=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/worker/WorkerOrdersPage.tsx | 631 +++++++++++++++--- apps/frontend/src/styles/main.css | 134 ++++ 2 files changed, 681 insertions(+), 84 deletions(-) diff --git a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx index a45aa0ca..2e3cebc1 100644 --- a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx @@ -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(null) const [submittingOrder, setSubmittingOrder] = useState(null) const [acceptanceFiles, setAcceptanceFiles] = useState([]) - const [form] = Form.useForm() + const [form] = Form.useForm() 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 + }, + 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 = [ { - title: '订单', - minWidth: 280, + title: '订单信息', + minWidth: 320, render: (_, row) => (
- {row.productName} + setDetailOrder(row)}> + {row.productName || row.workOrderNo} + + + {row.categoryName ? {row.categoryName} : null} + + 工单号:{row.workOrderNo} + + - {row.platformOrderId} + 订单号:{row.platformOrderId || '-'} + + + 接单时间:{formatDateTime(row.assignedAt)}
), }, { - title: '金额', - width: 130, + title: '奖励 / 押金', + width: 150, render: (_, row) => ( - - {formatMoney(row.rewardAmount)} - +
+ {formatMoney(row.rewardAmount)} + + 冻结押金:{formatMoney(row.freezeDepositAmount)} + +
), }, { - title: '状态', - width: 140, + title: '当前状态', + width: 180, render: (_, row) => ( - - {formatStatus(row.status)} - +
+ {formatStatus(row.status)} + + {getStatusHint(row.status)} + +
), }, { - title: '验收图片', - width: 170, + title: '验收信息', + minWidth: 210, render: (_, row) => ( - +
+ + + 提交时间:{formatDateTime(getAcceptanceSubmittedAt(row))} + + {row.acceptance?.note ? ( + {row.acceptance.note} + ) : ( + 暂无验收说明 + )} +
), }, { title: '问题备注', - dataIndex: 'problemNote', minWidth: 180, - render: (value) => String(value || '-'), + render: (_, row) => ( + + {String(row.problemNote || '').trim() || '-'} + + ), }, { title: '操作', - width: 140, - render: (_, row) => - ['in_progress', 'problem'].includes(row.status) ? ( - - ) : null, + {canSubmitAcceptance(row) ? ( + + ) : null} + + ), }, ] @@ -138,45 +261,250 @@ export default function WorkerOrdersPage() {
我的订单 - - } + onChange={(event) => { + const nextValue = event.target.value + setKeywordInput(nextValue) + if (!nextValue.trim()) { + setKeyword('') + setPage(1) + } + }} + onPressEnter={applyKeywordSearch} + /> + + + + 当前筛选:{STATUS_OPTIONS.find((item) => item.value === status)?.label || '全部订单'} + +
+ +
+ {STATUS_OPTIONS.map((item) => { + const active = status === item.value + const countKey = item.value || 'all' + return ( + + ) + })} +
+ + + rowKey="workOrderId" + loading={ordersQuery.isLoading} + dataSource={orders} + columns={columns} + locale={{ + emptyText: ordersQuery.error ? ( + + ) : ( + + ), + }} + 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 }} + /> + + + setDetailOrder(null)} + extra={ + detailOrder ? ( + + + {formatStatus(detailOrder.status)} + + {canSubmitAcceptance(detailOrder) ? ( + + ) : null} + + ) : null + } + > + {detailOrder ? ( +
+ + + + {detailOrder.productName || '-'} + + + {detailOrder.categoryName || '-'} + + + {detailOrder.workOrderNo} + + + {detailOrder.platformOrderId || '-'} + + + + {formatStatus(detailOrder.status)} + + + + {getStatusHint(detailOrder.status)} + + + {formatMoney(detailOrder.rewardAmount)} + + + {formatMoney(detailOrder.freezeDepositAmount)} + + + {formatDateTime(detailOrder.assignedAt)} + + + {formatDateTime(getAcceptanceSubmittedAt(detailOrder))} + + + {formatDateTime(detailOrder.acceptedAt)} + + + {formatDateTime(detailOrder.updatedAt)} + + + + + + {renderDetailFieldValues( + getMaterialDetailItems(detailOrder), + '当前还没有资料内容', + )} + + + + {renderRequirementFieldValues(detailOrder)} + + + +
+ + + {getAcceptanceFiles(detailOrder).length + + getAcceptanceImageUrls(detailOrder).length || 0} + + + {formatDateTime(getAcceptanceSubmittedAt(detailOrder))} + + + + {String(detailOrder.acceptance?.note || '').trim() || '-'} + + + + {getAcceptanceFiles(detailOrder).length || + getAcceptanceImageUrls(detailOrder).length ? ( + + ) : ( + 暂无验收图片 + )} +
+
+ + {String(detailOrder.problemNote || '').trim() ? ( + + + {String(detailOrder.problemNote || '').trim()} + + + ) : null} +
+ ) : null} +
- + { + 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 { + 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() + 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 {emptyText} + } + + return ( + + {items.map((item) => ( + + {item.label} + {item.required ? 必填 : null} + + } + > + + {String(item.value || '').trim() || '-'} + + + ))} + + ) +} + +function renderRequirementFieldValues(order: WorkOrder) { + const fields = getRequirementFields(order) + if (fields.length === 0) { + return 当前工单未配置资料要求 + } + + return ( + + {fields.map((field) => ( + + + {field.key} + + {field.required ? '必填' : '选填'} + + + + ))} + + ) +} + 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 { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : {} +} + 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 = { + in_progress: '订单进行中,完成后请及时提交验收资料。', + pending_acceptance: '已提交验收,等待后台审核。', + problem: '后台已标记问题,请根据备注调整后重新提交。', + accepted: '订单已完成验收,可以在个人中心查看结算记录。', + cancelled: '订单已取消,本次流程已结束。', + } + return hints[status] || '请关注当前订单状态变化。' +} + function resolveStatusColor(status: string) { if (status === 'accepted') return 'green' if (status === 'cancelled') return 'default' diff --git a/apps/frontend/src/styles/main.css b/apps/frontend/src/styles/main.css index 214f882b..abae1f7f 100644 --- a/apps/frontend/src/styles/main.css +++ b/apps/frontend/src/styles/main.css @@ -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; }