From a37f07b788cb97504410482602b28b523c94f41f Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 21 Aug 2026 16:08:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E6=89=93=E6=89=8B=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/worker/WorkerOrderDesktopList.tsx | 131 +++++ .../pages/worker/WorkerOrderDetailDrawer.tsx | 180 ++++++ .../src/pages/worker/WorkerOrderModals.tsx | 271 +++++++++ .../src/pages/worker/WorkerOrdersPage.tsx | 553 +++--------------- 4 files changed, 656 insertions(+), 479 deletions(-) create mode 100644 apps/frontend/src/pages/worker/WorkerOrderDesktopList.tsx create mode 100644 apps/frontend/src/pages/worker/WorkerOrderDetailDrawer.tsx create mode 100644 apps/frontend/src/pages/worker/WorkerOrderModals.tsx diff --git a/apps/frontend/src/pages/worker/WorkerOrderDesktopList.tsx b/apps/frontend/src/pages/worker/WorkerOrderDesktopList.tsx new file mode 100644 index 00000000..d99086e2 --- /dev/null +++ b/apps/frontend/src/pages/worker/WorkerOrderDesktopList.tsx @@ -0,0 +1,131 @@ +import { ReloadOutlined, SearchOutlined } from '@ant-design/icons' +import { Button, Card, Empty, Input, Space, Table, Tabs, Typography } from 'antd' +import type { TableColumnsType } from 'antd' + +import type { Pagination, WorkerOrderOverviewItem, WorkOrder } from '@/types/worker-platform' + +import { formatMoney, getSettlementLabel } from './worker-order-utils' + +export type WorkerOrderStatusOption = { + value: string + label: string +} + +export function WorkerOrderDesktopList({ + statusOptions, + status, + overviewByStatus, + keywordInput, + keyword, + orders, + pagination, + columns, + loading, + refreshing, + error, + onRefresh, + onStatusChange, + onKeywordInputChange, + onKeywordSearch, + onPageChange, +}: { + statusOptions: readonly WorkerOrderStatusOption[] + status: string + overviewByStatus: Map + keywordInput: string + keyword: string + orders: WorkOrder[] + pagination: Pagination | undefined + columns: TableColumnsType + loading: boolean + refreshing: boolean + error: unknown + onRefresh: () => void + onStatusChange: (status: string) => void + onKeywordInputChange: (keyword: string) => void + onKeywordSearch: () => void + onPageChange: (page: number, pageSize: number) => void +}) { + return ( +
+
+ 我的订单 + +
+ + + + { + const overview = overviewByStatus.get(item.value || 'all') + const count = Number(overview?.orderCount || 0) + const settlementAmount = Number(overview?.settlementAmount || 0) + return { + key: item.value || 'all', + label: ( + + + {item.label} + {count > 0 ? {count} : null} + + + {getSettlementLabel(item.value)} {formatMoney(settlementAmount)} + + + ), + } + })} + onChange={(nextKey) => onStatusChange(nextKey === 'all' ? '' : nextKey)} + /> + + + } + onChange={(event) => onKeywordInputChange(event.target.value)} + onPressEnter={onKeywordSearch} + /> + + + + + rowKey="workOrderId" + size="small" + loading={loading} + dataSource={orders} + columns={columns} + locale={{ + emptyText: error ? ( + + ) : ( + + ), + }} + pagination={{ + current: pagination?.page || 1, + pageSize: pagination?.pageSize || 20, + total: pagination?.total || 0, + showSizeChanger: true, + pageSizeOptions: [10, 20, 50, 100], + showTotal: (total) => `共 ${total} 条`, + onChange: onPageChange, + }} + scroll={{ x: 1440 }} + /> + + +
+ ) +} diff --git a/apps/frontend/src/pages/worker/WorkerOrderDetailDrawer.tsx b/apps/frontend/src/pages/worker/WorkerOrderDetailDrawer.tsx new file mode 100644 index 00000000..ba0547bf --- /dev/null +++ b/apps/frontend/src/pages/worker/WorkerOrderDetailDrawer.tsx @@ -0,0 +1,180 @@ +import { MessageOutlined, PictureOutlined } from '@ant-design/icons' +import { Button, Card, Descriptions, Drawer, Tag, Typography } from 'antd' + +import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline' +import ImagePreviewList from '@/components/files/ImagePreviewList' +import type { WorkOrder } from '@/types/worker-platform' +import { formatDateTime } from '@/utils/date-time' + +import { + canEditAcceptanceImages, + canRequestCancel, + canSubmitAcceptance, + canSubmitFeedback, + formatMoney, + formatStatus, + getAcceptanceSubmittedAt, + getDisplayOrderNo, + getMaterialDetailItems, + getMaterialScreenshots, + getUniqueAcceptanceFiles, + getWorkerAcceptance, + getWorkerDraft, + hasSubmittedAcceptance, + renderDetailFieldValues, + resolveStatusColor, + getStatusHint, +} from './worker-order-utils' + +export function WorkerOrderDetailDrawer({ + isMobile, + order, + orderTimelineVisible, + onClose, + onOpenAcceptance, + onOpenCancelRequest, + onOpenFeedback, +}: { + isMobile: boolean + order: WorkOrder | null + orderTimelineVisible: boolean + onClose: () => void + onOpenAcceptance: (order: WorkOrder) => void + onOpenCancelRequest: (order: WorkOrder) => void + onOpenFeedback: (order: WorkOrder) => void +}) { + return ( + + {order ? ( +
+
+ {formatStatus(order.status)} + {canEditAcceptanceImages(order) ? ( + + ) : null} + {canRequestCancel(order) ? ( + + ) : null} + {canSubmitFeedback(order) ? ( + + ) : null} +
+ + + {order.productName || '-'} + {order.categoryName || '-'} + {getDisplayOrderNo(order)} + + {formatStatus(order.status)} + + {getStatusHint(order.status)} + + {formatMoney(order.rewardAmount)} + + + {formatMoney(order.freezeDepositAmount)} + + + {formatDateTime(order.assignedAt)} + + + {formatDateTime(getAcceptanceSubmittedAt(order))} + + + {formatDateTime(order.acceptedAt)} + + + {formatDateTime(order.updatedAt)} + + + + + + {renderDetailFieldValues(getMaterialDetailItems(order), '当前还没有资料内容')} + {getMaterialScreenshots(order).length > 0 ? ( +
+ 买家主页截图 +
+ +
+
+ ) : null} +
+ + + + + + {String(order.problemNote || '').trim() ? ( + + + {String(order.problemNote || '').trim()} + + + ) : null} + + {orderTimelineVisible ? ( + + + + ) : null} +
+ ) : null} +
+ ) +} + +function AcceptanceSummary({ order }: { order: WorkOrder }) { + const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order)) + const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order)) + const hasAcceptance = acceptanceFiles.length > 0 + const hasDraft = draftFiles.length > 0 && !hasSubmittedAcceptance(order) + + return ( +
+ + {acceptanceFiles.length} + + {formatDateTime(getAcceptanceSubmittedAt(order))} + + + + {String(getWorkerAcceptance(order)?.note || '').trim() || '-'} + + + + + {hasAcceptance ? ( + + ) : ( + 暂无已提交验收图片 + )} + + {hasDraft ? ( +
+ }> + 已暂存 {draftFiles.length} 张图片(未提交,刷新不丢失) + + +
+ ) : null} +
+ ) +} diff --git a/apps/frontend/src/pages/worker/WorkerOrderModals.tsx b/apps/frontend/src/pages/worker/WorkerOrderModals.tsx new file mode 100644 index 00000000..7bc87292 --- /dev/null +++ b/apps/frontend/src/pages/worker/WorkerOrderModals.tsx @@ -0,0 +1,271 @@ +import { PictureOutlined } from '@ant-design/icons' +import { Button, Form, Input, Modal, Space, Tag, Typography } from 'antd' +import type { FormInstance } from 'antd' + +import ImageUpload from '@/components/files/ImageUpload' +import type { + UploadedFile, + WorkerCancelRequestRisk, + WorkerOrderFeedback, + WorkOrder, +} from '@/types/worker-platform' + +import { + canAutoAcceptOrder, + canSubmitAcceptance, + canSupplementAcceptedEvidence, + getDisplayOrderNo, + hasSubmittedAcceptance, +} from './worker-order-utils' + +export type AcceptanceFormValues = { note?: string } +export type WorkerNoteFormValues = { note?: string } +export type CancelRequestFormValues = { reason?: string } +export type FeedbackFormValues = { content?: string } + +type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'default' + +const PRESET_NOTE_TAGS: Array<{ label: string; theme: NoteTheme }> = [ + { label: '已安排', theme: 'success' }, + { label: '打单中', theme: 'processing' }, + { label: '账号问题', theme: 'danger' }, + { label: '排队中', theme: 'purple' }, + { label: '未安排', theme: 'warning' }, + { label: '暂缓做单', theme: 'warning' }, +] + +export function WorkerOrderModals({ + isMobile, + feedbackOrder, + feedbacks, + feedbackForm, + onCloseFeedback, + onSubmitFeedback, + cancelRequestOrder, + cancelRequestRisk, + cancelRequestForm, + onCloseCancelRequest, + onSubmitCancelRequest, + noteOrder, + noteForm, + onCloseNote, + onSaveNote, + editingOrder, + acceptanceFiles, + acceptanceForm, + workerCanAutoAcceptWithoutEvidence, + onCloseAcceptance, + onAcceptanceFilesChange, + onSubmitAcceptance, + onSaveAcceptanceImages, +}: { + isMobile: boolean + feedbackOrder: WorkOrder | null + feedbacks: WorkerOrderFeedback[] + feedbackForm: FormInstance + onCloseFeedback: () => void + onSubmitFeedback: (values: FeedbackFormValues) => void | Promise + cancelRequestOrder: WorkOrder | null + cancelRequestRisk: WorkerCancelRequestRisk | undefined + cancelRequestForm: FormInstance + onCloseCancelRequest: () => void + onSubmitCancelRequest: (values: CancelRequestFormValues) => void | Promise + noteOrder: WorkOrder | null + noteForm: FormInstance + onCloseNote: () => void + onSaveNote: (values: WorkerNoteFormValues) => void | Promise + editingOrder: WorkOrder | null + acceptanceFiles: UploadedFile[] + acceptanceForm: FormInstance + workerCanAutoAcceptWithoutEvidence: boolean + onCloseAcceptance: () => void + onAcceptanceFilesChange: (files: UploadedFile[]) => void + onSubmitAcceptance: (values: AcceptanceFormValues) => void | Promise + onSaveAcceptanceImages: (values: AcceptanceFormValues) => void | Promise +}) { + const feedbackItems = feedbacks.filter((item) => item.workOrderId === feedbackOrder?.workOrderId) + return ( + <> + feedbackForm.submit()} + okText="提交反馈" + destroyOnHidden + > + + 可反馈客户资料错误、账号异常或无法继续处理等问题。客服处理后会在反馈记录中回复。 + +
+ + + +
+ {feedbackItems.length > 0 ? ( + + 该订单最近反馈 + {feedbackItems.slice(0, 3).map((item) => ( + + {item.status === 'resolved' ? '已处理' : '待处理'}:{item.content} + {item.reply ? `;客服回复:${item.reply}` : ''} + + ))} + + ) : null} +
+ + cancelRequestForm.submit()} + okText="提交申请" + destroyOnHidden + > + + + 提交后由客服审核;通过后退还本次冻结押金,订单或拼单份额将回到大厅。 + + + 今日申请:{cancelRequestRisk?.dailyUsed || 0}/{cancelRequestRisk?.dailyLimit || 5} + ;累计通过: + {cancelRequestRisk?.approvedTotal || 0} 次。 + + + 每累计 10 次通过申请最多降一级;VIP1 期间累计 5 次通过将封禁账号。 + + +
+ + + +
+
+ + noteForm.submit()} + destroyOnHidden + > +
+ +
+ {PRESET_NOTE_TAGS.map((item) => ( + noteForm.setFieldsValue({ note: item.label })} + > + {item.label} + + ))} +
+
+ + + +
+
+ + + + {canSupplementAcceptedEvidence(editingOrder) ? ( + + ) : ( + <> + + {canSubmitAcceptance(editingOrder) ? ( + + ) : null} + + )} + + ) : null + } + > +
+ {!editingOrder || !canSupplementAcceptedEvidence(editingOrder) ? ( + + + + ) : null} + + + +
+
+ + ) +} diff --git a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx index 6e1f0659..bedae9f7 100644 --- a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx @@ -11,18 +11,12 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' import { App, Button, - Card, - Descriptions, - Drawer, Empty, Form, Input, - Modal, Pagination, Space, Spin, - Table, - Tabs, Tag, Tooltip, Typography, @@ -31,9 +25,7 @@ import type { TableColumnsType } from 'antd' import { useState } from 'react' import { DeadlineCountdown } from '@/components/DeadlineCountdown' -import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline' import ImagePreviewList from '@/components/files/ImagePreviewList' -import ImageUpload from '@/components/files/ImageUpload' import { fetchWorkerCancelRequests, fetchWorkerOrderFeedbacks, @@ -54,6 +46,15 @@ import { formatDateTime } from '@/utils/date-time' import { useIsMobile } from '@/utils/use-is-mobile' import { buildWorkerOrderQuery } from '@/utils/worker-order-query' import WorkerAfterSalesSection from './WorkerAfterSalesSection' +import { WorkerOrderDesktopList } from './WorkerOrderDesktopList' +import { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer' +import { + type AcceptanceFormValues, + type CancelRequestFormValues, + type FeedbackFormValues, + type WorkerNoteFormValues, + WorkerOrderModals, +} from './WorkerOrderModals' import { asRecord, canAutoAcceptOrder, @@ -71,14 +72,12 @@ import { getMaterialDetailItems, getMaterialScreenshots, getSettlementLabel, - getStatusHint, getUniqueAcceptanceFiles, getWorkerAcceptance, getWorkerDraft, hasSubmittedAcceptance, isVipEvidencePending, patchWorkOrderReadState, - renderDetailFieldValues, resolveStatusColor, toPngBlob, } from './worker-order-utils' @@ -94,33 +93,8 @@ const STATUS_OPTIONS = [ { value: '', label: '全部订单' }, ] as const -type AcceptanceFormValues = { - note?: string -} - -type WorkerNoteFormValues = { - note?: string -} - -type CancelRequestFormValues = { - reason?: string -} - -type FeedbackFormValues = { - content?: string -} - type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'default' -const PRESET_NOTE_TAGS: Array<{ label: string; theme: NoteTheme }> = [ - { label: '已安排', theme: 'success' }, - { label: '打单中', theme: 'processing' }, - { label: '账号问题', theme: 'danger' }, - { label: '排队中', theme: 'purple' }, - { label: '未安排', theme: 'warning' }, - { label: '暂缓做单', theme: 'warning' }, -] - function resolveNoteTheme(note?: string): NoteTheme { const text = String(note || '') .trim() @@ -1080,466 +1054,87 @@ export default function WorkerOrdersPage() { ) : ( -
-
- 我的订单 - -
- - - - { - const countKey = item.value || 'all' - const overview = overviewByStatus.get(countKey) - const count = Number(overview?.orderCount || 0) - const settlementAmount = Number(overview?.settlementAmount || 0) - return { - key: item.value || 'all', - label: ( - - - {item.label} - {count > 0 ? ( - {count} - ) : null} - - - {getSettlementLabel(item.value)} {formatMoney(settlementAmount)} - - - ), - } - })} - onChange={(nextKey) => { - const nextStatus = nextKey === 'all' ? '' : nextKey - setStatus(nextStatus) - setPage(1) - }} - /> - - - } - onChange={(event) => { - const nextValue = event.target.value - setKeywordInput(nextValue) - if (!nextValue.trim()) { - setKeyword('') - setPage(1) - } - }} - onPressEnter={applyKeywordSearch} - /> - - - - - rowKey="workOrderId" - size="small" - 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: number, nextPageSize: number) => { - setPage(nextPage) - setPageSize(nextPageSize) - }, - }} - scroll={{ x: 1440 }} - /> - - -
+ { + void refreshAll() + }} + onStatusChange={(nextStatus) => { + setStatus(nextStatus) + setPage(1) + }} + onKeywordInputChange={(nextValue) => { + setKeywordInput(nextValue) + if (!nextValue.trim()) { + setKeyword('') + setPage(1) + } + }} + onKeywordSearch={applyKeywordSearch} + onPageChange={(nextPage, nextPageSize) => { + setPage(nextPage) + setPageSize(nextPageSize) + }} + /> )} - setDetailOrder(null)} - > - {detailOrder ? ( -
-
- - {formatStatus(detailOrder.status)} - - {canEditAcceptanceImages(detailOrder) ? ( - - ) : null} - {canRequestCancel(detailOrder) ? ( - - ) : null} - {canSubmitFeedback(detailOrder) ? ( - - ) : null} -
- - - {detailOrder.productName || '-'} - - {detailOrder.categoryName || '-'} - - - {getDisplayOrderNo(detailOrder)} - - - - {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), '当前还没有资料内容')} - {getMaterialScreenshots(detailOrder).length > 0 ? ( -
- 买家主页截图 -
- -
-
- ) : null} -
- - -
- {(() => { - const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(detailOrder)) - const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(detailOrder)) - const hasAcceptance = acceptanceFiles.length > 0 - const hasDraft = draftFiles.length > 0 && !hasSubmittedAcceptance(detailOrder) - - return ( - <> - - - {acceptanceFiles.length} - - - {formatDateTime(getAcceptanceSubmittedAt(detailOrder))} - - - - {String(getWorkerAcceptance(detailOrder)?.note || '').trim() || '-'} - - - - - {hasAcceptance ? ( - - ) : ( - 暂无已提交验收图片 - )} - - {hasDraft ? ( -
- }> - 已暂存 {draftFiles.length} 张图片(未提交,刷新不丢失) - - -
- ) : null} - - ) - })()} -
-
- - {String(detailOrder.problemNote || '').trim() ? ( - - - {String(detailOrder.problemNote || '').trim()} - - - ) : null} - - {orderTimelineVisible ? ( - - - - ) : null} -
- ) : null} -
- { + onOpenAcceptance={openAcceptanceModal} + onOpenCancelRequest={openCancelRequestModal} + onOpenFeedback={openFeedbackModal} + /> + { setFeedbackOrder(null) feedbackForm.resetFields() }} - onOk={() => feedbackForm.submit()} - okText="提交反馈" - destroyOnHidden - > - - 可反馈客户资料错误、账号异常或无法继续处理等问题。客服处理后会在反馈记录中回复。 - -
- - - -
- {(feedbacksQuery.data?.data.items || []).filter( - (item) => item.workOrderId === feedbackOrder?.workOrderId, - ).length > 0 ? ( - - 该订单最近反馈 - {(feedbacksQuery.data?.data.items || []) - .filter((item) => item.workOrderId === feedbackOrder?.workOrderId) - .slice(0, 3) - .map((item) => ( - - {item.status === 'resolved' ? '已处理' : '待处理'}:{item.content} - {item.reply ? `;客服回复:${item.reply}` : ''} - - ))} - - ) : null} -
- { + onSubmitFeedback={submitFeedback} + cancelRequestOrder={cancelRequestOrder} + cancelRequestRisk={cancelRequestsQuery.data?.data.risk} + cancelRequestForm={cancelRequestForm} + onCloseCancelRequest={() => { setCancelRequestOrder(null) cancelRequestForm.resetFields() }} - onOk={() => cancelRequestForm.submit()} - okText="提交申请" - destroyOnHidden - > - - - 提交后由客服审核;通过后退还本次冻结押金,订单或拼单份额将回到大厅。 - - - 今日申请:{cancelRequestsQuery.data?.data.risk.dailyUsed || 0}/ - {cancelRequestsQuery.data?.data.risk.dailyLimit || 5};累计通过: - {cancelRequestsQuery.data?.data.risk.approvedTotal || 0} 次。 - - - 每累计 10 次通过申请最多降一级;VIP1 期间累计 5 次通过将封禁账号。 - - -
- - - -
-
- { + onSubmitCancelRequest={submitCancelRequest} + noteOrder={noteOrder} + noteForm={noteForm} + onCloseNote={() => { setNoteOrder(null) noteForm.resetFields() }} - onOk={() => noteForm.submit()} - destroyOnHidden - > -
- -
- {PRESET_NOTE_TAGS.map((item) => ( - noteForm.setFieldsValue({ note: item.label })} - > - {item.label} - - ))} -
-
- - - -
-
- - { + onSaveNote={saveOrderNote} + editingOrder={editingOrder} + acceptanceFiles={acceptanceFiles} + acceptanceForm={form} + workerCanAutoAcceptWithoutEvidence={workerCanAutoAcceptWithoutEvidence} + onCloseAcceptance={() => { setEditingOrder(null) setAcceptanceFiles([]) form.resetFields() }} - destroyOnHidden - footer={ - editingOrder ? ( - - - {canSupplementAcceptedEvidence(editingOrder) ? ( - - ) : ( - <> - - {canSubmitAcceptance(editingOrder) ? ( - - ) : null} - - )} - - ) : null - } - > -
- {!editingOrder || !canSupplementAcceptedEvidence(editingOrder) ? ( - - - - ) : null} - - - -
-
+ onAcceptanceFilesChange={setAcceptanceFiles} + onSubmitAcceptance={submitAcceptance} + onSaveAcceptanceImages={saveAcceptanceImages} + /> ) }