拆分打手订单展示

This commit is contained in:
yml2213
2026-08-21 16:08:24 +08:00
parent b7aa20f0ad
commit a37f07b788
4 changed files with 656 additions and 479 deletions
@@ -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<string, WorkerOrderOverviewItem>
keywordInput: string
keyword: string
orders: WorkOrder[]
pagination: Pagination | undefined
columns: TableColumnsType<WorkOrder>
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 (
<section className="page-stack">
<div className="worker-page-head">
<Typography.Title level={3}></Typography.Title>
<Button icon={<ReloadOutlined />} loading={refreshing} onClick={onRefresh}>
</Button>
</div>
<Card bordered={false}>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Tabs
className="worker-order-tabs"
activeKey={status || 'all'}
items={statusOptions.map((item) => {
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: (
<span className="worker-order-tab-label">
<span className="worker-order-tab-label-main">
{item.label}
{count > 0 ? <span className="worker-order-tab-count">{count}</span> : null}
</span>
<span className="worker-order-tab-settlement">
{getSettlementLabel(item.value)} {formatMoney(settlementAmount)}
</span>
</span>
),
}
})}
onChange={(nextKey) => onStatusChange(nextKey === 'all' ? '' : nextKey)}
/>
<Space.Compact style={{ width: '100%', maxWidth: 460 }}>
<Input
allowClear
value={keywordInput}
placeholder="搜索订单号 / 商品名 / 游戏ID / 游戏昵称"
prefix={<SearchOutlined />}
onChange={(event) => onKeywordInputChange(event.target.value)}
onPressEnter={onKeywordSearch}
/>
<Button icon={<SearchOutlined />} loading={refreshing} onClick={onKeywordSearch}>
</Button>
</Space.Compact>
<Table<WorkOrder>
rowKey="workOrderId"
size="small"
loading={loading}
dataSource={orders}
columns={columns}
locale={{
emptyText: error ? (
<Empty description={error instanceof Error ? error.message : '读取我的订单失败'} />
) : (
<Empty
description={
keyword ? `没有找到与 “${keyword}” 相关的订单` : '当前筛选下暂无订单'
}
/>
),
}}
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 }}
/>
</Space>
</Card>
</section>
)
}
@@ -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 (
<Drawer
className="worker-order-detail-drawer"
title="订单详情"
width={isMobile ? '100%' : 820}
open={Boolean(order)}
destroyOnHidden
onClose={onClose}
>
{order ? (
<div className="worker-order-detail-stack">
<div className="worker-order-detail-actions">
<Tag color={resolveStatusColor(order.status)}>{formatStatus(order.status)}</Tag>
{canEditAcceptanceImages(order) ? (
<Button
type={canSubmitAcceptance(order) ? 'primary' : 'default'}
icon={<PictureOutlined />}
onClick={() => onOpenAcceptance(order)}
>
{hasSubmittedAcceptance(order) ? '补充图片' : '上传图片 / 提交验收'}
</Button>
) : null}
{canRequestCancel(order) ? (
<Button danger onClick={() => onOpenCancelRequest(order)}>
</Button>
) : null}
{canSubmitFeedback(order) ? (
<Button icon={<MessageOutlined />} onClick={() => onOpenFeedback(order)}>
</Button>
) : null}
</div>
<Card title="基础信息" size="small">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="商品">{order.productName || '-'}</Descriptions.Item>
<Descriptions.Item label="分类">{order.categoryName || '-'}</Descriptions.Item>
<Descriptions.Item label="平台订单号">{getDisplayOrderNo(order)}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={resolveStatusColor(order.status)}>{formatStatus(order.status)}</Tag>
</Descriptions.Item>
<Descriptions.Item label="阶段说明">{getStatusHint(order.status)}</Descriptions.Item>
<Descriptions.Item label="接单奖励">
{formatMoney(order.rewardAmount)}
</Descriptions.Item>
<Descriptions.Item label="冻结押金">
{formatMoney(order.freezeDepositAmount)}
</Descriptions.Item>
<Descriptions.Item label="接单时间">
{formatDateTime(order.assignedAt)}
</Descriptions.Item>
<Descriptions.Item label="提交验收">
{formatDateTime(getAcceptanceSubmittedAt(order))}
</Descriptions.Item>
<Descriptions.Item label="验收完成">
{formatDateTime(order.acceptedAt)}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{formatDateTime(order.updatedAt)}
</Descriptions.Item>
</Descriptions>
</Card>
<Card title="资料信息" size="small">
{renderDetailFieldValues(getMaterialDetailItems(order), '当前还没有资料内容')}
{getMaterialScreenshots(order).length > 0 ? (
<div style={{ marginTop: 12 }}>
<Typography.Text type="secondary"></Typography.Text>
<div style={{ marginTop: 8 }}>
<ImagePreviewList files={getMaterialScreenshots(order)} size={72} />
</div>
</div>
) : null}
</Card>
<Card title="验收信息" size="small">
<AcceptanceSummary order={order} />
</Card>
{String(order.problemNote || '').trim() ? (
<Card title="问题备注" size="small">
<span className="worker-order-detail-value">
{String(order.problemNote || '').trim()}
</span>
</Card>
) : null}
{orderTimelineVisible ? (
<Card title="流转记录" size="small">
<WorkOrderEventTimeline orderId={order.workOrderId} viewer="worker" />
</Card>
) : null}
</div>
) : null}
</Drawer>
)
}
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 (
<div className="worker-order-detail-stack">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="验收图片数量">{acceptanceFiles.length}</Descriptions.Item>
<Descriptions.Item label="提交时间">
{formatDateTime(getAcceptanceSubmittedAt(order))}
</Descriptions.Item>
<Descriptions.Item label="验收说明" span={2}>
<span className="worker-order-detail-value">
{String(getWorkerAcceptance(order)?.note || '').trim() || '-'}
</span>
</Descriptions.Item>
</Descriptions>
{hasAcceptance ? (
<ImagePreviewList files={acceptanceFiles} size={76} />
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
{hasDraft ? (
<div className="acceptance-draft-badge-wrap" style={{ marginTop: 8 }}>
<Tag color="processing" icon={<PictureOutlined />}>
{draftFiles.length}
</Tag>
<ImagePreviewList files={draftFiles} size={76} />
</div>
) : null}
</div>
)
}
@@ -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<FeedbackFormValues>
onCloseFeedback: () => void
onSubmitFeedback: (values: FeedbackFormValues) => void | Promise<void>
cancelRequestOrder: WorkOrder | null
cancelRequestRisk: WorkerCancelRequestRisk | undefined
cancelRequestForm: FormInstance<CancelRequestFormValues>
onCloseCancelRequest: () => void
onSubmitCancelRequest: (values: CancelRequestFormValues) => void | Promise<void>
noteOrder: WorkOrder | null
noteForm: FormInstance<WorkerNoteFormValues>
onCloseNote: () => void
onSaveNote: (values: WorkerNoteFormValues) => void | Promise<void>
editingOrder: WorkOrder | null
acceptanceFiles: UploadedFile[]
acceptanceForm: FormInstance<AcceptanceFormValues>
workerCanAutoAcceptWithoutEvidence: boolean
onCloseAcceptance: () => void
onAcceptanceFilesChange: (files: UploadedFile[]) => void
onSubmitAcceptance: (values: AcceptanceFormValues) => void | Promise<void>
onSaveAcceptanceImages: (values: AcceptanceFormValues) => void | Promise<void>
}) {
const feedbackItems = feedbacks.filter((item) => item.workOrderId === feedbackOrder?.workOrderId)
return (
<>
<Modal
title={`问题反馈 · ${getDisplayOrderNo(feedbackOrder)}`}
open={Boolean(feedbackOrder)}
width={isMobile ? '92%' : 540}
onCancel={onCloseFeedback}
onOk={() => feedbackForm.submit()}
okText="提交反馈"
destroyOnHidden
>
<Typography.Paragraph type="secondary">
</Typography.Paragraph>
<Form form={feedbackForm} layout="vertical" onFinish={onSubmitFeedback}>
<Form.Item
label="问题说明"
name="content"
rules={[{ required: true, whitespace: true, message: '请填写问题说明' }]}
>
<Input.TextArea
rows={5}
maxLength={500}
showCount
placeholder="例如:客户提供的游戏账号密码错误,无法登录"
/>
</Form.Item>
</Form>
{feedbackItems.length > 0 ? (
<Space direction="vertical" size={6} style={{ width: '100%' }}>
<Typography.Text type="secondary"></Typography.Text>
{feedbackItems.slice(0, 3).map((item) => (
<Typography.Text key={item.feedbackId} type="secondary">
{item.status === 'resolved' ? '已处理' : '待处理'}{item.content}
{item.reply ? `;客服回复:${item.reply}` : ''}
</Typography.Text>
))}
</Space>
) : null}
</Modal>
<Modal
title={`申请取消 · ${getDisplayOrderNo(cancelRequestOrder)}`}
open={Boolean(cancelRequestOrder)}
width={isMobile ? '92%' : 520}
onCancel={onCloseCancelRequest}
onOk={() => cancelRequestForm.submit()}
okText="提交申请"
destroyOnHidden
>
<Space direction="vertical" size={8} style={{ width: '100%', marginBottom: 16 }}>
<Typography.Text type="secondary">
退
</Typography.Text>
<Typography.Text type="secondary">
{cancelRequestRisk?.dailyUsed || 0}/{cancelRequestRisk?.dailyLimit || 5}
{cancelRequestRisk?.approvedTotal || 0}
</Typography.Text>
<Typography.Text type="warning">
10 VIP1 5
</Typography.Text>
</Space>
<Form form={cancelRequestForm} layout="vertical" onFinish={onSubmitCancelRequest}>
<Form.Item
label="申请原因"
name="reason"
rules={[{ required: true, whitespace: true, message: '请填写申请取消原因' }]}
>
<Input.TextArea
rows={4}
maxLength={300}
showCount
placeholder="请说明无法继续处理的原因"
/>
</Form.Item>
</Form>
</Modal>
<Modal
title={`订单备注 · ${getDisplayOrderNo(noteOrder)}`}
open={Boolean(noteOrder)}
width={isMobile ? '92%' : 460}
onCancel={onCloseNote}
onOk={() => noteForm.submit()}
destroyOnHidden
>
<Form form={noteForm} layout="vertical" onFinish={onSaveNote}>
<Form.Item label="快捷选择标签" style={{ marginBottom: 12 }}>
<div className="worker-note-preset-tags">
{PRESET_NOTE_TAGS.map((item) => (
<Tag
key={item.label}
className={`worker-note-preset-tag theme-${item.theme}`}
onClick={() => noteForm.setFieldsValue({ note: item.label })}
>
{item.label}
</Tag>
))}
</div>
</Form.Item>
<Form.Item label="备注内容" name="note">
<Input.TextArea
rows={3}
maxLength={100}
showCount
placeholder="请输入备注说明(如:已联系买家、账号异常中等)"
/>
</Form.Item>
</Form>
</Modal>
<Modal
title={
editingOrder &&
(canSupplementAcceptedEvidence(editingOrder) || hasSubmittedAcceptance(editingOrder))
? '补充验收图片'
: '验收图片'
}
open={Boolean(editingOrder)}
width={isMobile ? '96%' : 620}
onCancel={onCloseAcceptance}
destroyOnHidden
footer={
editingOrder ? (
<Space>
<Button onClick={onCloseAcceptance}></Button>
{canSupplementAcceptedEvidence(editingOrder) ? (
<Button type="primary" onClick={() => acceptanceForm.submit()}>
</Button>
) : (
<>
<Button
onClick={() => void onSaveAcceptanceImages(acceptanceForm.getFieldsValue())}
>
{hasSubmittedAcceptance(editingOrder) ? '保存修改' : '保存暂存'}
</Button>
{canSubmitAcceptance(editingOrder) ? (
<Button type="primary" onClick={() => acceptanceForm.submit()}>
</Button>
) : null}
</>
)}
</Space>
) : null
}
>
<Form form={acceptanceForm} layout="vertical" onFinish={onSubmitAcceptance}>
{!editingOrder || !canSupplementAcceptedEvidence(editingOrder) ? (
<Form.Item label="完成说明" name="note">
<Input.TextArea rows={3} placeholder="可补充代练结果、注意事项或截图说明。" />
</Form.Item>
) : null}
<Form.Item
label="验收图片"
required={
!editingOrder ||
canSupplementAcceptedEvidence(editingOrder) ||
!canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
}
extra={
editingOrder && canSupplementAcceptedEvidence(editingOrder)
? '仅可追加新图片,原有验收图片会保留。'
: editingOrder &&
canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
? 'VIP 免审核可先提交,验收后需在 24 小时内补充图片。'
: undefined
}
>
<ImageUpload
scene="worker-acceptance"
scope="worker"
value={acceptanceFiles}
onChange={onAcceptanceFilesChange}
maxCount={20}
showPasteHint
/>
</Form.Item>
</Form>
</Modal>
</>
)
}
@@ -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() {
</div>
</div>
) : (
<section className="page-stack">
<div className="worker-page-head">
<Typography.Title level={3}></Typography.Title>
<Button
icon={<ReloadOutlined />}
loading={ordersQuery.isFetching || overviewQuery.isFetching}
onClick={() => refreshAll()}
>
</Button>
</div>
<Card bordered={false}>
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Tabs
className="worker-order-tabs"
activeKey={status || 'all'}
items={STATUS_OPTIONS.map((item) => {
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: (
<span className="worker-order-tab-label">
<span className="worker-order-tab-label-main">
{item.label}
{count > 0 ? (
<span className="worker-order-tab-count">{count}</span>
) : null}
</span>
<span className="worker-order-tab-settlement">
{getSettlementLabel(item.value)} {formatMoney(settlementAmount)}
</span>
</span>
),
}
})}
onChange={(nextKey) => {
const nextStatus = nextKey === 'all' ? '' : nextKey
setStatus(nextStatus)
setPage(1)
}}
/>
<Space.Compact style={{ width: '100%', maxWidth: 460 }}>
<Input
allowClear
value={keywordInput}
placeholder="搜索订单号 / 商品名 / 游戏ID / 游戏昵称"
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>
<Table<WorkOrder>
rowKey="workOrderId"
size="small"
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: number, nextPageSize: number) => {
setPage(nextPage)
setPageSize(nextPageSize)
},
}}
scroll={{ x: 1440 }}
/>
</Space>
</Card>
</section>
<WorkerOrderDesktopList
statusOptions={STATUS_OPTIONS}
status={status}
overviewByStatus={overviewByStatus}
keywordInput={keywordInput}
keyword={keyword}
orders={orders}
pagination={pagination}
columns={columns}
loading={ordersQuery.isLoading}
refreshing={ordersQuery.isFetching || overviewQuery.isFetching}
error={ordersQuery.error}
onRefresh={() => {
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)
}}
/>
)}
<Drawer
className="worker-order-detail-drawer"
title="订单详情"
width={isMobile ? '100%' : 820}
open={Boolean(detailOrder)}
destroyOnHidden
<WorkerOrderDetailDrawer
isMobile={isMobile}
order={detailOrder}
orderTimelineVisible={orderTimelineVisible}
onClose={() => setDetailOrder(null)}
>
{detailOrder ? (
<div className="worker-order-detail-stack">
<div className="worker-order-detail-actions">
<Tag color={resolveStatusColor(detailOrder.status)}>
{formatStatus(detailOrder.status)}
</Tag>
{canEditAcceptanceImages(detailOrder) ? (
<Button
type={canSubmitAcceptance(detailOrder) ? 'primary' : 'default'}
icon={<PictureOutlined />}
onClick={() => openAcceptanceModal(detailOrder)}
>
{hasSubmittedAcceptance(detailOrder) ? '补充图片' : '上传图片 / 提交验收'}
</Button>
) : null}
{canRequestCancel(detailOrder) ? (
<Button danger onClick={() => openCancelRequestModal(detailOrder)}>
</Button>
) : null}
{canSubmitFeedback(detailOrder) ? (
<Button icon={<MessageOutlined />} onClick={() => openFeedbackModal(detailOrder)}>
</Button>
) : null}
</div>
<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="平台订单号">
{getDisplayOrderNo(detailOrder)}
</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), '当前还没有资料内容')}
{getMaterialScreenshots(detailOrder).length > 0 ? (
<div style={{ marginTop: 12 }}>
<Typography.Text type="secondary"></Typography.Text>
<div style={{ marginTop: 8 }}>
<ImagePreviewList files={getMaterialScreenshots(detailOrder)} size={72} />
</div>
</div>
) : null}
</Card>
<Card title="验收信息" size="small">
<div className="worker-order-detail-stack">
{(() => {
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(detailOrder))
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(detailOrder))
const hasAcceptance = acceptanceFiles.length > 0
const hasDraft = draftFiles.length > 0 && !hasSubmittedAcceptance(detailOrder)
return (
<>
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="验收图片数量">
{acceptanceFiles.length}
</Descriptions.Item>
<Descriptions.Item label="提交时间">
{formatDateTime(getAcceptanceSubmittedAt(detailOrder))}
</Descriptions.Item>
<Descriptions.Item label="验收说明" span={2}>
<span className="worker-order-detail-value">
{String(getWorkerAcceptance(detailOrder)?.note || '').trim() || '-'}
</span>
</Descriptions.Item>
</Descriptions>
{hasAcceptance ? (
<ImagePreviewList files={acceptanceFiles} size={76} />
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
{hasDraft ? (
<div className="acceptance-draft-badge-wrap" style={{ marginTop: 8 }}>
<Tag color="processing" icon={<PictureOutlined />}>
{draftFiles.length}
</Tag>
<ImagePreviewList files={draftFiles} size={76} />
</div>
) : null}
</>
)
})()}
</div>
</Card>
{String(detailOrder.problemNote || '').trim() ? (
<Card title="问题备注" size="small">
<span className="worker-order-detail-value">
{String(detailOrder.problemNote || '').trim()}
</span>
</Card>
) : null}
{orderTimelineVisible ? (
<Card title="流转记录" size="small">
<WorkOrderEventTimeline orderId={detailOrder.workOrderId} viewer="worker" />
</Card>
) : null}
</div>
) : null}
</Drawer>
<Modal
title={`问题反馈 · ${getDisplayOrderNo(feedbackOrder)}`}
open={Boolean(feedbackOrder)}
width={isMobile ? '92%' : 540}
onCancel={() => {
onOpenAcceptance={openAcceptanceModal}
onOpenCancelRequest={openCancelRequestModal}
onOpenFeedback={openFeedbackModal}
/>
<WorkerOrderModals
isMobile={isMobile}
feedbackOrder={feedbackOrder}
feedbacks={feedbacksQuery.data?.data.items || []}
feedbackForm={feedbackForm}
onCloseFeedback={() => {
setFeedbackOrder(null)
feedbackForm.resetFields()
}}
onOk={() => feedbackForm.submit()}
okText="提交反馈"
destroyOnHidden
>
<Typography.Paragraph type="secondary">
</Typography.Paragraph>
<Form form={feedbackForm} layout="vertical" onFinish={submitFeedback}>
<Form.Item
label="问题说明"
name="content"
rules={[{ required: true, whitespace: true, message: '请填写问题说明' }]}
>
<Input.TextArea
rows={5}
maxLength={500}
showCount
placeholder="例如:客户提供的游戏账号密码错误,无法登录"
/>
</Form.Item>
</Form>
{(feedbacksQuery.data?.data.items || []).filter(
(item) => item.workOrderId === feedbackOrder?.workOrderId,
).length > 0 ? (
<Space direction="vertical" size={6} style={{ width: '100%' }}>
<Typography.Text type="secondary"></Typography.Text>
{(feedbacksQuery.data?.data.items || [])
.filter((item) => item.workOrderId === feedbackOrder?.workOrderId)
.slice(0, 3)
.map((item) => (
<Typography.Text key={item.feedbackId} type="secondary">
{item.status === 'resolved' ? '已处理' : '待处理'}{item.content}
{item.reply ? `;客服回复:${item.reply}` : ''}
</Typography.Text>
))}
</Space>
) : null}
</Modal>
<Modal
title={`申请取消 · ${getDisplayOrderNo(cancelRequestOrder)}`}
open={Boolean(cancelRequestOrder)}
width={isMobile ? '92%' : 520}
onCancel={() => {
onSubmitFeedback={submitFeedback}
cancelRequestOrder={cancelRequestOrder}
cancelRequestRisk={cancelRequestsQuery.data?.data.risk}
cancelRequestForm={cancelRequestForm}
onCloseCancelRequest={() => {
setCancelRequestOrder(null)
cancelRequestForm.resetFields()
}}
onOk={() => cancelRequestForm.submit()}
okText="提交申请"
destroyOnHidden
>
<Space direction="vertical" size={8} style={{ width: '100%', marginBottom: 16 }}>
<Typography.Text type="secondary">
退
</Typography.Text>
<Typography.Text type="secondary">
{cancelRequestsQuery.data?.data.risk.dailyUsed || 0}/
{cancelRequestsQuery.data?.data.risk.dailyLimit || 5}
{cancelRequestsQuery.data?.data.risk.approvedTotal || 0}
</Typography.Text>
<Typography.Text type="warning">
10 VIP1 5
</Typography.Text>
</Space>
<Form form={cancelRequestForm} layout="vertical" onFinish={submitCancelRequest}>
<Form.Item
label="申请原因"
name="reason"
rules={[{ required: true, whitespace: true, message: '请填写申请取消原因' }]}
>
<Input.TextArea
rows={4}
maxLength={300}
showCount
placeholder="请说明无法继续处理的原因"
/>
</Form.Item>
</Form>
</Modal>
<Modal
title={`订单备注 · ${getDisplayOrderNo(noteOrder)}`}
open={Boolean(noteOrder)}
width={isMobile ? '92%' : 460}
onCancel={() => {
onSubmitCancelRequest={submitCancelRequest}
noteOrder={noteOrder}
noteForm={noteForm}
onCloseNote={() => {
setNoteOrder(null)
noteForm.resetFields()
}}
onOk={() => noteForm.submit()}
destroyOnHidden
>
<Form form={noteForm} layout="vertical" onFinish={saveOrderNote}>
<Form.Item label="快捷选择标签" style={{ marginBottom: 12 }}>
<div className="worker-note-preset-tags">
{PRESET_NOTE_TAGS.map((item) => (
<Tag
key={item.label}
className={`worker-note-preset-tag theme-${item.theme}`}
onClick={() => noteForm.setFieldsValue({ note: item.label })}
>
{item.label}
</Tag>
))}
</div>
</Form.Item>
<Form.Item label="备注内容" name="note">
<Input.TextArea
rows={3}
maxLength={100}
showCount
placeholder="请输入备注说明(如:已联系买家、账号异常中等)"
/>
</Form.Item>
</Form>
</Modal>
<Modal
title={
editingOrder
? canSupplementAcceptedEvidence(editingOrder) || hasSubmittedAcceptance(editingOrder)
? '补充验收图片'
: '验收图片'
: '验收图片'
}
open={Boolean(editingOrder)}
width={isMobile ? '96%' : 620}
onCancel={() => {
onSaveNote={saveOrderNote}
editingOrder={editingOrder}
acceptanceFiles={acceptanceFiles}
acceptanceForm={form}
workerCanAutoAcceptWithoutEvidence={workerCanAutoAcceptWithoutEvidence}
onCloseAcceptance={() => {
setEditingOrder(null)
setAcceptanceFiles([])
form.resetFields()
}}
destroyOnHidden
footer={
editingOrder ? (
<Space>
<Button
onClick={() => {
setEditingOrder(null)
setAcceptanceFiles([])
form.resetFields()
}}
>
</Button>
{canSupplementAcceptedEvidence(editingOrder) ? (
<Button type="primary" onClick={() => form.submit()}>
</Button>
) : (
<>
<Button onClick={() => void saveAcceptanceImages(form.getFieldsValue())}>
{hasSubmittedAcceptance(editingOrder) ? '保存修改' : '保存暂存'}
</Button>
{canSubmitAcceptance(editingOrder) ? (
<Button type="primary" onClick={() => form.submit()}>
</Button>
) : null}
</>
)}
</Space>
) : null
}
>
<Form form={form} layout="vertical" onFinish={submitAcceptance}>
{!editingOrder || !canSupplementAcceptedEvidence(editingOrder) ? (
<Form.Item label="完成说明" name="note">
<Input.TextArea rows={3} placeholder="可补充代练结果、注意事项或截图说明。" />
</Form.Item>
) : null}
<Form.Item
label="验收图片"
required={
!editingOrder ||
canSupplementAcceptedEvidence(editingOrder) ||
!canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
}
extra={
editingOrder && canSupplementAcceptedEvidence(editingOrder)
? '仅可追加新图片,原有验收图片会保留。'
: editingOrder &&
canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
? 'VIP 免审核可先提交,验收后需在 24 小时内补充图片。'
: undefined
}
>
<ImageUpload
scene="worker-acceptance"
scope="worker"
value={acceptanceFiles}
onChange={setAcceptanceFiles}
maxCount={20}
showPasteHint
/>
</Form.Item>
</Form>
</Modal>
onAcceptanceFilesChange={setAcceptanceFiles}
onSubmitAcceptance={submitAcceptance}
onSaveAcceptanceImages={saveAcceptanceImages}
/>
</>
)
}