拆分打手订单列表

This commit is contained in:
yml2213
2026-08-21 16:18:48 +08:00
parent a37f07b788
commit 4e8fb433ef
4 changed files with 860 additions and 628 deletions
@@ -19,8 +19,11 @@ export function WorkerOrderDesktopList({
keyword, keyword,
orders, orders,
pagination, pagination,
page,
pageSize,
columns, columns,
loading, loading,
searching,
refreshing, refreshing,
error, error,
onRefresh, onRefresh,
@@ -36,8 +39,11 @@ export function WorkerOrderDesktopList({
keyword: string keyword: string
orders: WorkOrder[] orders: WorkOrder[]
pagination: Pagination | undefined pagination: Pagination | undefined
page: number
pageSize: number
columns: TableColumnsType<WorkOrder> columns: TableColumnsType<WorkOrder>
loading: boolean loading: boolean
searching: boolean
refreshing: boolean refreshing: boolean
error: unknown error: unknown
onRefresh: () => void onRefresh: () => void
@@ -91,7 +97,7 @@ export function WorkerOrderDesktopList({
onChange={(event) => onKeywordInputChange(event.target.value)} onChange={(event) => onKeywordInputChange(event.target.value)}
onPressEnter={onKeywordSearch} onPressEnter={onKeywordSearch}
/> />
<Button icon={<SearchOutlined />} loading={refreshing} onClick={onKeywordSearch}> <Button icon={<SearchOutlined />} loading={searching} onClick={onKeywordSearch}>
</Button> </Button>
</Space.Compact> </Space.Compact>
@@ -114,8 +120,8 @@ export function WorkerOrderDesktopList({
), ),
}} }}
pagination={{ pagination={{
current: pagination?.page || 1, current: pagination?.page || page,
pageSize: pagination?.pageSize || 20, pageSize: pagination?.pageSize || pageSize,
total: pagination?.total || 0, total: pagination?.total || 0,
showSizeChanger: true, showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100], pageSizeOptions: [10, 20, 50, 100],
@@ -0,0 +1,438 @@
import {
CopyOutlined,
EditOutlined,
EyeOutlined,
MessageOutlined,
PictureOutlined,
ReloadOutlined,
SearchOutlined,
} from '@ant-design/icons'
import { Button, Empty, Input, Pagination, Spin, Tag } from 'antd'
import { DeadlineCountdown } from '@/components/DeadlineCountdown'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import type {
Pagination as PaginationData,
WorkerOrderOverviewItem,
WorkOrder,
} from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import type { WorkerOrderStatusOption } from './WorkerOrderDesktopList'
import {
canEditAcceptanceImages,
canRemindAcceptance,
canRequestCancel,
canSubmitAcceptance,
canSubmitFeedback,
formatMoney,
formatStatus,
getCopyOrderInfoItems,
getDisplayOrderNo,
getMaterialDetailItems,
getMaterialScreenshots,
getSettlementLabel,
getUniqueAcceptanceFiles,
getWorkerAcceptance,
getWorkerDraft,
hasSubmittedAcceptance,
isVipEvidencePending,
resolveStatusColor,
} from './worker-order-utils'
export function WorkerOrderMobileList({
statusOptions,
status,
overviewByStatus,
keywordInput,
keyword,
orders,
pagination,
page,
pageSize,
loading,
searching,
refreshing,
error,
onStatusChange,
onKeywordInputChange,
onKeywordSearch,
onRefresh,
onPageChange,
onOpenDetail,
onOpenAcceptance,
onRemindAcceptance,
onOpenCancelRequest,
onOpenFeedback,
onOpenNote,
onCopyOrderInfo,
onCopyMaterialImages,
}: {
statusOptions: readonly WorkerOrderStatusOption[]
status: string
overviewByStatus: Map<string, WorkerOrderOverviewItem>
keywordInput: string
keyword: string
orders: WorkOrder[]
pagination: PaginationData | undefined
page: number
pageSize: number
loading: boolean
searching: boolean
refreshing: boolean
error: unknown
onStatusChange: (status: string) => void
onKeywordInputChange: (keyword: string) => void
onKeywordSearch: () => void
onRefresh: () => void
onPageChange: (page: number, pageSize: number) => void
onOpenDetail: (order: WorkOrder) => void
onOpenAcceptance: (order: WorkOrder) => void
onRemindAcceptance: (order: WorkOrder) => void
onOpenCancelRequest: (order: WorkOrder) => void
onOpenFeedback: (order: WorkOrder) => void
onOpenNote: (order: WorkOrder) => void
onCopyOrderInfo: (order: WorkOrder) => void
onCopyMaterialImages: (order: WorkOrder) => void
}) {
return (
<div className="worker-orders-mobile-page">
<div className="worker-orders-mobile-status-bar">
{statusOptions.map((item) => {
const overview = overviewByStatus.get(item.value || 'all')
const count = Number(overview?.orderCount || 0)
return (
<button
key={item.value || 'all'}
type="button"
className={`worker-orders-status-pill ${(status || '') === item.value ? 'active' : ''}`}
onClick={() => onStatusChange(item.value)}
>
<span className="worker-orders-status-pill-main">
<span>{item.label}</span>
{count > 0 ? (
<span className="worker-orders-status-pill-count">{count}</span>
) : null}
</span>
<span className="worker-orders-status-pill-settlement">
{getSettlementLabel(item.value)} {formatMoney(overview?.settlementAmount)}
</span>
</button>
)
})}
</div>
<div className="worker-orders-mobile-search-row">
<Input
allowClear
value={keywordInput}
placeholder="搜索订单号 / 商品名 / 游戏ID / 游戏昵称"
prefix={<SearchOutlined />}
className="worker-orders-mobile-search-input"
onChange={(event) => onKeywordInputChange(event.target.value)}
onPressEnter={onKeywordSearch}
/>
<Button
type="primary"
icon={<SearchOutlined />}
loading={searching}
onClick={onKeywordSearch}
/>
<Button icon={<ReloadOutlined />} loading={refreshing} onClick={onRefresh} title="刷新" />
</div>
<div className="worker-orders-mobile-list">
{loading ? (
<div className="worker-hall-loading">
<Spin size="large" />
</div>
) : error ? (
<Empty description={error instanceof Error ? error.message : '读取我的订单失败'} />
) : orders.length === 0 ? (
<Empty
description={keyword ? `没有找到与 “${keyword}” 相关的订单` : '当前筛选下暂无订单'}
/>
) : (
orders.map((order) => (
<WorkerOrderMobileCard
key={order.workOrderId}
order={order}
onOpenDetail={onOpenDetail}
onOpenAcceptance={onOpenAcceptance}
onRemindAcceptance={onRemindAcceptance}
onOpenCancelRequest={onOpenCancelRequest}
onOpenFeedback={onOpenFeedback}
onOpenNote={onOpenNote}
onCopyOrderInfo={onCopyOrderInfo}
onCopyMaterialImages={onCopyMaterialImages}
/>
))
)}
{(pagination?.total || 0) > 0 ? (
<div className="worker-orders-mobile-pagination">
<Pagination
current={pagination?.page || page}
pageSize={pagination?.pageSize || pageSize}
total={pagination?.total || 0}
simple
onChange={onPageChange}
/>
</div>
) : null}
</div>
</div>
)
}
function WorkerOrderMobileCard({
order,
onOpenDetail,
onOpenAcceptance,
onRemindAcceptance,
onOpenCancelRequest,
onOpenFeedback,
onOpenNote,
onCopyOrderInfo,
onCopyMaterialImages,
}: Omit<
Parameters<typeof WorkerOrderMobileList>[0],
| 'statusOptions'
| 'status'
| 'overviewByStatus'
| 'keywordInput'
| 'keyword'
| 'orders'
| 'pagination'
| 'page'
| 'pageSize'
| 'loading'
| 'searching'
| 'refreshing'
| 'error'
| 'onStatusChange'
| 'onKeywordInputChange'
| 'onKeywordSearch'
| 'onRefresh'
| 'onPageChange'
> & { order: WorkOrder }) {
const items = getMaterialDetailItems(order).filter((item) => String(item.value || '').trim())
const screenshots = getMaterialScreenshots(order)
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order))
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order))
const noteText = getWorkerAcceptance(order)?.note
const workerNote = String(order.workerNote || '').trim()
return (
<article className="worker-order-mobile-card">
<div className="worker-order-mobile-header">
<div className="worker-order-mobile-header-left">
{order.isUnread ? (
<Tag color="purple" className="worker-order-unread-tag">
</Tag>
) : null}
{order.categoryName ? (
<Tag color="blue" className="worker-order-mobile-tag">
{order.categoryName}
</Tag>
) : null}
{order.myShare ? (
<Tag color="purple" className="worker-order-mobile-tag">
</Tag>
) : null}
<span className="worker-order-mobile-orderno-text">{getDisplayOrderNo(order)}</span>
</div>
<div className="worker-order-mobile-header-right">
<Tag color={resolveStatusColor(order.status)}>{formatStatus(order.status)}</Tag>
{order.status === 'in_progress' && order.deadlineAt ? (
<DeadlineCountdown deadlineAt={order.deadlineAt} />
) : null}
</div>
</div>
<div className="worker-order-mobile-main-row">
<div className="worker-order-mobile-product-title">{order.productName || '未命名商品'}</div>
<div className="worker-order-mobile-price-badge">
<span className="worker-order-mobile-price-label"></span>
<strong className="worker-order-mobile-price-num">
{formatMoney(order.rewardAmount)}
</strong>
</div>
</div>
<div className="worker-order-mobile-sub-row">
<span className="worker-order-mobile-sub-item">
: {formatMoney(order.freezeDepositAmount)}
</span>
{order.myShare ? (
<span className="worker-order-mobile-sub-item">
{order.myShare.quantity} ({formatMoney(order.myShare.shareReward)})
</span>
) : null}
<span className="worker-order-mobile-sub-time">{formatDateTime(order.assignedAt)}</span>
</div>
{order.status === 'problem' && String(order.problemNote || '').trim() ? (
<div className="worker-order-mobile-problem-box">
<strong></strong>
<span>{order.problemNote}</span>
</div>
) : null}
{items.length > 0 || screenshots.length > 0 ? (
<div className="worker-order-mobile-credentials-box">
{items.length > 0 ? (
<div className="worker-order-mobile-credentials-grid">
{items.map((item) => (
<div key={item.key} className="worker-order-mobile-credential-line">
<span className="worker-order-mobile-credential-label">{item.label}:</span>
<span className="worker-order-mobile-credential-val">{item.value}</span>
</div>
))}
</div>
) : null}
{screenshots.length > 0 ? (
<div className="worker-order-mobile-screenshots-row">
<span className="worker-order-mobile-credential-label">:</span>
<ImagePreviewList files={screenshots} size={34} />
</div>
) : null}
<div className="worker-order-mobile-copy-toolbar">
<Button
size="small"
icon={<CopyOutlined />}
disabled={getCopyOrderInfoItems(order).length === 0}
onClick={() => onCopyOrderInfo(order)}
>
</Button>
{screenshots.length > 0 ? (
<Button
size="small"
icon={<PictureOutlined />}
onClick={() => onCopyMaterialImages(order)}
>
</Button>
) : null}
</div>
</div>
) : null}
{acceptanceFiles.length > 0 ||
(draftFiles.length > 0 && !hasSubmittedAcceptance(order)) ||
noteText ? (
<div className="worker-order-mobile-acceptance-strip">
{acceptanceFiles.length > 0 ? (
<div className="worker-order-mobile-acceptance-files">
<span className="worker-order-mobile-acceptance-tag">
({acceptanceFiles.length})
</span>
<ImagePreviewList files={acceptanceFiles} size={32} maxVisible={3} />
</div>
) : null}
{isVipEvidencePending(order) ? <Tag color="purple">VIP </Tag> : null}
{draftFiles.length > 0 && !hasSubmittedAcceptance(order) ? (
<div className="worker-order-mobile-draft-files">
<Tag color="processing" icon={<PictureOutlined />}>
{draftFiles.length}
</Tag>
<ImagePreviewList files={draftFiles} size={32} maxVisible={3} />
</div>
) : null}
{noteText ? (
<div className="worker-order-mobile-acceptance-text">{noteText}</div>
) : null}
</div>
) : null}
<div className="worker-order-mobile-footer-row">
<div className="worker-order-mobile-note-block">
{workerNote ? (
<div
className={`worker-note-badge theme-${resolveNoteTheme(workerNote)}`}
onClick={() => onOpenNote(order)}
>
<span className="worker-note-text" title={workerNote}>
{workerNote}
</span>
<EditOutlined className="worker-note-edit-icon" />
</div>
) : (
<button
type="button"
className="worker-order-mobile-add-note-btn"
onClick={() => onOpenNote(order)}
>
<EditOutlined />
</button>
)}
</div>
<div className="worker-order-mobile-btn-group">
<Button size="small" icon={<EyeOutlined />} onClick={() => onOpenDetail(order)}>
</Button>
{canEditAcceptanceImages(order) ? (
<Button
size="small"
type={canSubmitAcceptance(order) ? 'primary' : 'default'}
icon={<PictureOutlined />}
className="worker-order-mobile-action-submit"
onClick={() => onOpenAcceptance(order)}
>
{hasSubmittedAcceptance(order) ? '补充图片' : '提交验收'}
</Button>
) : null}
{canRemindAcceptance(order) ? (
<Button size="small" onClick={() => onRemindAcceptance(order)}>
</Button>
) : null}
{canRequestCancel(order) ? (
<Button size="small" danger onClick={() => onOpenCancelRequest(order)}>
</Button>
) : null}
{canSubmitFeedback(order) ? (
<Button size="small" icon={<MessageOutlined />} onClick={() => onOpenFeedback(order)}>
</Button>
) : null}
</div>
</div>
</article>
)
}
function resolveNoteTheme(note?: string) {
const text = String(note || '')
.trim()
.toLowerCase()
if (!text) return 'default'
if (
['问题', '异常', '密码', '封号', '错误', '无法', '投诉', '退', '失败', '被挤'].some((item) =>
text.includes(item),
)
)
return 'danger'
if (
['未安排', '未', '暂缓', '暂停', '稍后', '延后', '挂起', '待定'].some((item) =>
text.includes(item),
)
)
return 'warning'
if (['排队', '等待', '等买家', '等扫码', '等验证', '待扫'].some((item) => text.includes(item)))
return 'purple'
if (
['打单', '做单', '进行', '处理', '上号', '练级', '正在', '开打', '肝'].some((item) =>
text.includes(item),
)
)
return 'processing'
if (
['已安排', '安排', '已就绪', '就绪', '已联系', '完成', '已好', '搞定', '正常', 'ok'].some(
(item) => text.includes(item),
)
)
return 'success'
return 'default'
}
@@ -0,0 +1,349 @@
import {
CopyOutlined,
EditOutlined,
EyeOutlined,
MessageOutlined,
PictureOutlined,
} from '@ant-design/icons'
import { Button, Space, Tag, Tooltip, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { DeadlineCountdown } from '@/components/DeadlineCountdown'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import type { WorkOrder } from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import {
canEditAcceptanceImages,
canRemindAcceptance,
canRequestCancel,
canSubmitAcceptance,
canSubmitFeedback,
formatMoney,
formatStatus,
getAcceptanceSubmittedAt,
getCopyOrderInfoItems,
getDisplayOrderNo,
getMaterialDetailItems,
getMaterialScreenshots,
getUniqueAcceptanceFiles,
getWorkerAcceptance,
getWorkerDraft,
hasSubmittedAcceptance,
isVipEvidencePending,
resolveStatusColor,
} from './worker-order-utils'
type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'default'
export function buildWorkerOrderTableColumns({
onOpenDetail,
onOpenAcceptance,
onRemindAcceptance,
onOpenCancelRequest,
onOpenFeedback,
onOpenNote,
onCopyOrderInfo,
onCopyMaterialImages,
}: {
onOpenDetail: (order: WorkOrder) => void
onOpenAcceptance: (order: WorkOrder) => void
onRemindAcceptance: (order: WorkOrder) => void
onOpenCancelRequest: (order: WorkOrder) => void
onOpenFeedback: (order: WorkOrder) => void
onOpenNote: (order: WorkOrder) => void
onCopyOrderInfo: (order: WorkOrder) => void
onCopyMaterialImages: (order: WorkOrder) => void
}): TableColumnsType<WorkOrder> {
return [
{
title: '订单信息',
width: 270,
render: (_, row) => (
<div className="cell-stack">
<Space size={4} align="center">
{row.isUnread ? (
<Tag color="purple" className="worker-order-unread-tag">
</Tag>
) : null}
<Typography.Link
strong
onClick={() => onOpenDetail(row)}
title={row.productName || '未命名商品'}
ellipsis
>
{row.productName || '未命名商品'}
</Typography.Link>
</Space>
<Space size={[4, 4]} wrap>
{row.categoryName ? <Tag color="blue">{row.categoryName}</Tag> : null}
{row.myShare ? <Tag color="purple"></Tag> : null}
</Space>
<Typography.Text
type="secondary"
style={{ fontSize: 12 }}
ellipsis={{ tooltip: getDisplayOrderNo(row) }}
>
{getDisplayOrderNo(row)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatDateTime(row.assignedAt)}
</Typography.Text>
</div>
),
},
{
title: '状态',
width: 130,
render: (_, row) => (
<div className="cell-stack">
<Space wrap size={4}>
<Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>
{row.status === 'in_progress' && row.deadlineAt ? (
<DeadlineCountdown deadlineAt={row.deadlineAt} />
) : null}
</Space>
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
<Typography.Text
type="danger"
ellipsis={{ tooltip: row.problemNote }}
style={{ fontSize: 12 }}
>
{row.problemNote}
</Typography.Text>
) : null}
</div>
),
},
{
title: '奖励 / 押金',
width: 140,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong style={{ color: '#cf1322' }}>
{formatMoney(row.rewardAmount)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatMoney(row.freezeDepositAmount)}
</Typography.Text>
{row.myShare ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.myShare.quantity} · {formatMoney(row.myShare.shareReward)}
</Typography.Text>
) : null}
</div>
),
},
{
title: '验收信息',
width: 220,
render: (_, row) => <AcceptanceCell order={row} />,
},
{
title: '快捷复制',
width: 250,
render: (_, row) => {
const items = getMaterialDetailItems(row).filter((item) => String(item.value || '').trim())
const screenshots = getMaterialScreenshots(row)
return (
<div className="cell-stack">
{items.length > 0 ? (
items.map((item) => (
<Typography.Text
key={item.key}
style={{ fontSize: 12 }}
ellipsis={{ tooltip: `${item.label}: ${item.value}` }}
>
{item.label}: {item.value}
</Typography.Text>
))
) : (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
</Typography.Text>
)}
{screenshots.length > 0 ? <ImagePreviewList files={screenshots} size={34} /> : null}
<Space wrap size={4}>
<Button
size="small"
icon={<CopyOutlined />}
disabled={getCopyOrderInfoItems(row).length === 0}
onClick={() => onCopyOrderInfo(row)}
>
</Button>
{screenshots.length > 0 ? (
<Button
size="small"
icon={<PictureOutlined />}
onClick={() => onCopyMaterialImages(row)}
>
</Button>
) : null}
</Space>
</div>
)
},
},
{
title: '备注',
width: 180,
render: (_, row) => <NoteCell order={row} onOpenNote={onOpenNote} />,
},
{
title: '操作',
width: 150,
render: (_, row) => (
<div className="worker-order-actions">
<Space size={6}>
<Tooltip title="查看详情">
<Button
aria-label="查看详情"
icon={<EyeOutlined />}
onClick={() => onOpenDetail(row)}
/>
</Tooltip>
{canEditAcceptanceImages(row) ? (
<Button
icon={<PictureOutlined />}
type={canSubmitAcceptance(row) ? 'primary' : 'default'}
onClick={() => onOpenAcceptance(row)}
>
{hasSubmittedAcceptance(row) ? '补充图片' : '验收'}
</Button>
) : null}
{canRemindAcceptance(row) ? (
<Button onClick={() => onRemindAcceptance(row)}></Button>
) : null}
{canRequestCancel(row) ? (
<Button danger onClick={() => onOpenCancelRequest(row)}>
</Button>
) : null}
{canSubmitFeedback(row) ? (
<Tooltip title="反馈客户资料或订单处理问题">
<Button icon={<MessageOutlined />} onClick={() => onOpenFeedback(row)}>
</Button>
</Tooltip>
) : null}
</Space>
</div>
),
},
]
}
function AcceptanceCell({ order }: { order: WorkOrder }) {
const submittedAt = getAcceptanceSubmittedAt(order)
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order))
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order))
if (acceptanceFiles.length === 0 && !submittedAt && draftFiles.length === 0) {
return (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
</Typography.Text>
)
}
return (
<div className="cell-stack">
{acceptanceFiles.length > 0 ? (
<ImagePreviewList files={acceptanceFiles} size={38} maxVisible={2} />
) : null}
{submittedAt ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatDateTime(submittedAt)}
</Typography.Text>
) : null}
{isVipEvidencePending(order) ? <Tag color="purple">VIP </Tag> : null}
{draftFiles.length > 0 && !hasSubmittedAcceptance(order) ? (
<div className="acceptance-draft-badge-wrap">
<Tag color="processing" icon={<PictureOutlined />}>
{draftFiles.length}
</Tag>
<ImagePreviewList files={draftFiles} size={38} maxVisible={2} />
</div>
) : null}
{getWorkerAcceptance(order)?.note ? (
<Typography.Text
type="secondary"
ellipsis={{ tooltip: getWorkerAcceptance(order)?.note }}
style={{ fontSize: 12 }}
>
{getWorkerAcceptance(order)?.note}
</Typography.Text>
) : null}
</div>
)
}
function NoteCell({
order,
onOpenNote,
}: {
order: WorkOrder
onOpenNote: (order: WorkOrder) => void
}) {
const note = String(order.workerNote || '').trim()
return (
<div className="worker-note-cell">
{note ? (
<div
className={`worker-note-badge theme-${resolveNoteTheme(note)}`}
onClick={() => onOpenNote(order)}
>
<span className="worker-note-text" title={note}>
{note}
</span>
<EditOutlined className="worker-note-edit-icon" />
</div>
) : (
<Button
type="dashed"
size="small"
icon={<EditOutlined />}
className="worker-note-add-btn"
onClick={() => onOpenNote(order)}
>
</Button>
)}
</div>
)
}
function resolveNoteTheme(note?: string): NoteTheme {
const text = String(note || '')
.trim()
.toLowerCase()
if (!text) return 'default'
if (
['问题', '异常', '密码', '封号', '错误', '无法', '投诉', '退', '失败', '被挤'].some((item) =>
text.includes(item),
)
)
return 'danger'
if (
['未安排', '未', '暂缓', '暂停', '稍后', '延后', '挂起', '待定'].some((item) =>
text.includes(item),
)
)
return 'warning'
if (['排队', '等待', '等买家', '等扫码', '等验证', '待扫'].some((item) => text.includes(item)))
return 'purple'
if (
['打单', '做单', '进行', '处理', '上号', '练级', '正在', '开打', '肝'].some((item) =>
text.includes(item),
)
)
return 'processing'
if (
['已安排', '安排', '已就绪', '就绪', '已联系', '完成', '已好', '搞定', '正常', 'ok'].some(
(item) => text.includes(item),
)
)
return 'success'
return 'default'
}
@@ -1,31 +1,7 @@
import {
CopyOutlined,
EditOutlined,
EyeOutlined,
MessageOutlined,
PictureOutlined,
ReloadOutlined,
SearchOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { import { App, Form } from 'antd'
App,
Button,
Empty,
Form,
Input,
Pagination,
Space,
Spin,
Tag,
Tooltip,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useState } from 'react' import { useState } from 'react'
import { DeadlineCountdown } from '@/components/DeadlineCountdown'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import { import {
fetchWorkerCancelRequests, fetchWorkerCancelRequests,
fetchWorkerOrderFeedbacks, fetchWorkerOrderFeedbacks,
@@ -42,12 +18,12 @@ import {
submitWorkerOrderFeedback, submitWorkerOrderFeedback,
} from '@/services/worker' } from '@/services/worker'
import type { UploadedFile, WorkOrder } from '@/types/worker-platform' import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import { useIsMobile } from '@/utils/use-is-mobile' import { useIsMobile } from '@/utils/use-is-mobile'
import { buildWorkerOrderQuery } from '@/utils/worker-order-query' import { buildWorkerOrderQuery } from '@/utils/worker-order-query'
import WorkerAfterSalesSection from './WorkerAfterSalesSection' import WorkerAfterSalesSection from './WorkerAfterSalesSection'
import { WorkerOrderDesktopList } from './WorkerOrderDesktopList' import { WorkerOrderDesktopList } from './WorkerOrderDesktopList'
import { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer' import { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer'
import { WorkerOrderMobileList } from './WorkerOrderMobileList'
import { import {
type AcceptanceFormValues, type AcceptanceFormValues,
type CancelRequestFormValues, type CancelRequestFormValues,
@@ -55,30 +31,17 @@ import {
type WorkerNoteFormValues, type WorkerNoteFormValues,
WorkerOrderModals, WorkerOrderModals,
} from './WorkerOrderModals' } from './WorkerOrderModals'
import { buildWorkerOrderTableColumns } from './WorkerOrderTableColumns'
import { import {
asRecord,
canAutoAcceptOrder, canAutoAcceptOrder,
canEditAcceptanceImages,
canRemindAcceptance,
canRequestCancel,
canSubmitAcceptance,
canSubmitFeedback,
canSupplementAcceptedEvidence, canSupplementAcceptedEvidence,
formatMoney,
formatStatus,
getAcceptanceSubmittedAt,
getCopyOrderInfoItems, getCopyOrderInfoItems,
getDisplayOrderNo,
getMaterialDetailItems,
getMaterialScreenshots, getMaterialScreenshots,
getSettlementLabel,
getUniqueAcceptanceFiles, getUniqueAcceptanceFiles,
getWorkerAcceptance, getWorkerAcceptance,
getWorkerDraft, getWorkerDraft,
hasSubmittedAcceptance, hasSubmittedAcceptance,
isVipEvidencePending,
patchWorkOrderReadState, patchWorkOrderReadState,
resolveStatusColor,
toPngBlob, toPngBlob,
} from './worker-order-utils' } from './worker-order-utils'
@@ -461,598 +424,71 @@ export default function WorkerOrdersPage() {
setPage(1) setPage(1)
} }
const columns: TableColumnsType<WorkOrder> = [ const columns = buildWorkerOrderTableColumns({
{ onOpenDetail: openOrderDetail,
title: '订单信息', onOpenAcceptance: openAcceptanceModal,
width: 270, onRemindAcceptance: (order) => {
render: (_, row) => ( void submitAcceptanceReminder(order)
<div className="cell-stack">
<Space size={4} align="center">
{row.isUnread ? (
<Tag color="purple" className="worker-order-unread-tag">
</Tag>
) : null}
<Typography.Link
strong
onClick={() => openOrderDetail(row)}
title={row.productName || '未命名商品'}
ellipsis
>
{row.productName || '未命名商品'}
</Typography.Link>
</Space>
<Space size={[4, 4]} wrap>
{row.categoryName ? <Tag color="blue">{row.categoryName}</Tag> : null}
{row.myShare ? <Tag color="purple"></Tag> : null}
</Space>
<Typography.Text
type="secondary"
style={{ fontSize: 12 }}
ellipsis={{ tooltip: getDisplayOrderNo(row) }}
>
{getDisplayOrderNo(row)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatDateTime(row.assignedAt)}
</Typography.Text>
</div>
),
}, },
{ onOpenCancelRequest: openCancelRequestModal,
title: '状态', onOpenFeedback: openFeedbackModal,
width: 130, onOpenNote: openNoteModal,
render: (_, row) => ( onCopyOrderInfo: copyOrderInfo,
<div className="cell-stack"> onCopyMaterialImages: (order) => {
<Space wrap size={4}> void copyMaterialImages(order)
<Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>
{row.status === 'in_progress' && row.deadlineAt ? (
<DeadlineCountdown deadlineAt={row.deadlineAt} />
) : null}
</Space>
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
<Typography.Text
type="danger"
ellipsis={{ tooltip: row.problemNote }}
style={{ fontSize: 12 }}
>
{row.problemNote}
</Typography.Text>
) : null}
</div>
),
}, },
{ })
title: '奖励 / 押金',
width: 140,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong style={{ color: '#cf1322' }}>
{formatMoney(row.rewardAmount)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatMoney(row.freezeDepositAmount)}
</Typography.Text>
{row.myShare ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.myShare.quantity} · {formatMoney(row.myShare.shareReward)}
</Typography.Text>
) : null}
</div>
),
},
{
title: '验收信息',
width: 220,
render: (_, row) => {
const submittedAt = getAcceptanceSubmittedAt(row)
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(row))
const hasAcceptance = acceptanceFiles.length > 0
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(row))
const draftCount = draftFiles.length
const vipEvidencePending = isVipEvidencePending(row)
if (!hasAcceptance && !submittedAt && draftCount === 0) {
return (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
</Typography.Text>
)
}
return (
<div className="cell-stack">
{hasAcceptance ? (
<ImagePreviewList files={acceptanceFiles} size={38} maxVisible={2} />
) : null}
{submittedAt ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatDateTime(submittedAt)}
</Typography.Text>
) : null}
{vipEvidencePending ? <Tag color="purple">VIP </Tag> : null}
{draftCount > 0 && !hasSubmittedAcceptance(row) ? (
<div className="acceptance-draft-badge-wrap">
<Tag color="processing" icon={<PictureOutlined />}>
{draftCount}
</Tag>
<ImagePreviewList files={draftFiles} size={38} maxVisible={2} />
</div>
) : null}
{getWorkerAcceptance(row)?.note ? (
<Typography.Text
type="secondary"
ellipsis={{ tooltip: getWorkerAcceptance(row)?.note }}
style={{ fontSize: 12 }}
>
{getWorkerAcceptance(row)?.note}
</Typography.Text>
) : null}
</div>
)
},
},
{
title: '快捷复制',
width: 250,
render: (_, row) => {
const items = getMaterialDetailItems(row).filter((item) => String(item.value || '').trim())
const canCopyInfo = getCopyOrderInfoItems(row).length > 0
const screenshots = getMaterialScreenshots(row)
return (
<div className="cell-stack">
{items.length > 0 ? (
items.map((item) => (
<Typography.Text
key={item.key}
style={{ fontSize: 12 }}
ellipsis={{ tooltip: `${item.label}: ${item.value}` }}
>
{item.label}: {item.value}
</Typography.Text>
))
) : (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
</Typography.Text>
)}
{screenshots.length > 0 ? <ImagePreviewList files={screenshots} size={34} /> : null}
<Space wrap size={4}>
<Button
size="small"
icon={<CopyOutlined />}
disabled={!canCopyInfo}
onClick={() => copyOrderInfo(row)}
>
</Button>
{screenshots.length > 0 ? (
<Button
size="small"
icon={<PictureOutlined />}
onClick={() => copyMaterialImages(row)}
>
</Button>
) : null}
</Space>
</div>
)
},
},
{
title: '备注',
width: 180,
render: (_, row) => {
const note = String(row.workerNote || '').trim()
const theme = resolveNoteTheme(note)
return (
<div className="worker-note-cell">
{note ? (
<div
className={`worker-note-badge theme-${theme}`}
onClick={() => openNoteModal(row)}
>
<span className="worker-note-text" title={note}>
{note}
</span>
<EditOutlined className="worker-note-edit-icon" />
</div>
) : (
<Button
type="dashed"
size="small"
icon={<EditOutlined />}
className="worker-note-add-btn"
onClick={() => openNoteModal(row)}
>
</Button>
)}
</div>
)
},
},
{
title: '操作',
width: 150,
render: (_, row) => (
<div className="worker-order-actions">
<Space size={6}>
<Tooltip title="查看详情">
<Button
aria-label="查看详情"
icon={<EyeOutlined />}
onClick={() => openOrderDetail(row)}
/>
</Tooltip>
{canEditAcceptanceImages(row) ? (
<Button
icon={<PictureOutlined />}
type={canSubmitAcceptance(row) ? 'primary' : 'default'}
onClick={() => openAcceptanceModal(row)}
>
{hasSubmittedAcceptance(row) ? '补充图片' : '验收'}
</Button>
) : null}
{canRemindAcceptance(row) ? (
<Button onClick={() => submitAcceptanceReminder(row)}></Button>
) : null}
{canRequestCancel(row) ? (
<Button danger onClick={() => openCancelRequestModal(row)}>
</Button>
) : null}
{canSubmitFeedback(row) ? (
<Tooltip title="反馈客户资料或订单处理问题">
<Button icon={<MessageOutlined />} onClick={() => openFeedbackModal(row)}>
</Button>
</Tooltip>
) : null}
</Space>
</div>
),
},
]
return ( return (
<> <>
<WorkerAfterSalesSection isMobile={isMobile} /> <WorkerAfterSalesSection isMobile={isMobile} />
{isMobile ? ( {isMobile ? (
<div className="worker-orders-mobile-page"> <WorkerOrderMobileList
{/* 顶部水平滑动状态切换胶囊条 */} statusOptions={STATUS_OPTIONS}
<div className="worker-orders-mobile-status-bar"> status={status}
{STATUS_OPTIONS.map((item) => { overviewByStatus={overviewByStatus}
const countKey = item.value || 'all' keywordInput={keywordInput}
const overview = overviewByStatus.get(countKey) keyword={keyword}
const count = Number(overview?.orderCount || 0) orders={orders}
const settlementAmount = Number(overview?.settlementAmount || 0) pagination={pagination}
const isActive = (status || '') === item.value page={page}
const settlementLabel = getSettlementLabel(item.value) pageSize={pageSize}
return ( loading={ordersQuery.isLoading}
<button searching={ordersQuery.isFetching}
key={item.value || 'all'} refreshing={ordersQuery.isFetching || overviewQuery.isFetching}
type="button" error={ordersQuery.error}
className={`worker-orders-status-pill ${isActive ? 'active' : ''}`} onStatusChange={(nextStatus) => {
onClick={() => { setStatus(nextStatus)
setStatus(item.value) setPage(1)
setPage(1) }}
}} onKeywordInputChange={(nextValue) => {
> setKeywordInput(nextValue)
<span className="worker-orders-status-pill-main"> if (!nextValue.trim()) {
<span>{item.label}</span> setKeyword('')
{count > 0 ? ( setPage(1)
<span className="worker-orders-status-pill-count">{count}</span> }
) : null} }}
</span> onKeywordSearch={applyKeywordSearch}
<span className="worker-orders-status-pill-settlement"> onRefresh={() => {
{settlementLabel} {formatMoney(settlementAmount)} void refreshAll()
</span> }}
</button> onPageChange={(nextPage, nextPageSize) => {
) setPage(nextPage)
})} setPageSize(nextPageSize)
</div> }}
onOpenDetail={openOrderDetail}
{/* 搜索与刷新栏 */} onOpenAcceptance={openAcceptanceModal}
<div className="worker-orders-mobile-search-row"> onRemindAcceptance={(order) => {
<Input void submitAcceptanceReminder(order)
allowClear }}
value={keywordInput} onOpenCancelRequest={openCancelRequestModal}
placeholder="搜索订单号 / 商品名 / 游戏ID / 游戏昵称" onOpenFeedback={openFeedbackModal}
prefix={<SearchOutlined />} onOpenNote={openNoteModal}
className="worker-orders-mobile-search-input" onCopyOrderInfo={copyOrderInfo}
onChange={(event) => { onCopyMaterialImages={(order) => {
const nextValue = event.target.value void copyMaterialImages(order)
setKeywordInput(nextValue) }}
if (!nextValue.trim()) { />
setKeyword('')
setPage(1)
}
}}
onPressEnter={applyKeywordSearch}
/>
<Button
type="primary"
icon={<SearchOutlined />}
loading={ordersQuery.isFetching}
onClick={applyKeywordSearch}
/>
<Button
icon={<ReloadOutlined />}
loading={ordersQuery.isFetching || overviewQuery.isFetching}
onClick={() => refreshAll()}
title="刷新"
/>
</div>
{/* 移动端紧凑卡片列表 */}
<div className="worker-orders-mobile-list">
{ordersQuery.isLoading ? (
<div className="worker-hall-loading">
<Spin size="large" />
</div>
) : ordersQuery.error ? (
<Empty
description={
ordersQuery.error instanceof Error
? ordersQuery.error.message
: '读取我的订单失败'
}
/>
) : orders.length === 0 ? (
<Empty
description={keyword ? `没有找到与 “${keyword}” 相关的订单` : '当前筛选下暂无订单'}
/>
) : (
orders.map((order) => {
const items = getMaterialDetailItems(order).filter((item) =>
String(item.value || '').trim(),
)
const screenshots = getMaterialScreenshots(order)
const canCopyInfo = getCopyOrderInfoItems(order).length > 0
const _submittedAt = getAcceptanceSubmittedAt(order)
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order))
const hasAcceptance = acceptanceFiles.length > 0
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order))
const draftCount = draftFiles.length
const noteText = getWorkerAcceptance(order)?.note
const workerNote = String(order.workerNote || '').trim()
return (
<article key={order.workOrderId} className="worker-order-mobile-card">
{/* 卡片顶栏:分类 + 订单号 + 状态/倒计时 */}
<div className="worker-order-mobile-header">
<div className="worker-order-mobile-header-left">
{order.isUnread ? (
<Tag color="purple" className="worker-order-unread-tag">
</Tag>
) : null}
{order.categoryName ? (
<Tag color="blue" className="worker-order-mobile-tag">
{order.categoryName}
</Tag>
) : null}
{order.myShare ? (
<Tag color="purple" className="worker-order-mobile-tag">
</Tag>
) : null}
<span className="worker-order-mobile-orderno-text">
{getDisplayOrderNo(order)}
</span>
</div>
<div className="worker-order-mobile-header-right">
<Tag color={resolveStatusColor(order.status)}>
{formatStatus(order.status)}
</Tag>
{order.status === 'in_progress' && order.deadlineAt ? (
<DeadlineCountdown deadlineAt={order.deadlineAt} />
) : null}
</div>
</div>
{/* 商品标题与金额 */}
<div className="worker-order-mobile-main-row">
<div className="worker-order-mobile-product-title">
{order.productName || '未命名商品'}
</div>
<div className="worker-order-mobile-price-badge">
<span className="worker-order-mobile-price-label"></span>
<strong className="worker-order-mobile-price-num">
{formatMoney(order.rewardAmount)}
</strong>
</div>
</div>
{/* 辅助金额与接单时间 */}
<div className="worker-order-mobile-sub-row">
<span className="worker-order-mobile-sub-item">
: {formatMoney(order.freezeDepositAmount)}
</span>
{order.myShare ? (
<span className="worker-order-mobile-sub-item">
{order.myShare.quantity} ({formatMoney(order.myShare.shareReward)}
)
</span>
) : null}
<span className="worker-order-mobile-sub-time">
{formatDateTime(order.assignedAt)}
</span>
</div>
{/* 问题单提示 */}
{order.status === 'problem' && String(order.problemNote || '').trim() ? (
<div className="worker-order-mobile-problem-box">
<strong></strong>
<span>{order.problemNote}</span>
</div>
) : null}
{/* 买家资料与截图 */}
{(items.length > 0 || screenshots.length > 0) && (
<div className="worker-order-mobile-credentials-box">
{items.length > 0 ? (
<div className="worker-order-mobile-credentials-grid">
{items.map((item) => (
<div key={item.key} className="worker-order-mobile-credential-line">
<span className="worker-order-mobile-credential-label">
{item.label}:
</span>
<span className="worker-order-mobile-credential-val">
{item.value}
</span>
</div>
))}
</div>
) : null}
{screenshots.length > 0 ? (
<div className="worker-order-mobile-screenshots-row">
<span className="worker-order-mobile-credential-label">:</span>
<ImagePreviewList files={screenshots} size={34} />
</div>
) : null}
<div className="worker-order-mobile-copy-toolbar">
<Button
size="small"
icon={<CopyOutlined />}
disabled={!canCopyInfo}
onClick={() => copyOrderInfo(order)}
>
</Button>
{screenshots.length > 0 ? (
<Button
size="small"
icon={<PictureOutlined />}
onClick={() => copyMaterialImages(order)}
>
</Button>
) : null}
</div>
</div>
)}
{/* 验收凭据与暂存状态 */}
{(hasAcceptance ||
(draftCount > 0 && !hasSubmittedAcceptance(order)) ||
noteText) && (
<div className="worker-order-mobile-acceptance-strip">
{hasAcceptance ? (
<div className="worker-order-mobile-acceptance-files">
<span className="worker-order-mobile-acceptance-tag">
({acceptanceFiles.length})
</span>
<ImagePreviewList files={acceptanceFiles} size={32} maxVisible={3} />
</div>
) : null}
{isVipEvidencePending(order) ? (
<Tag color="purple">VIP </Tag>
) : null}
{draftCount > 0 && !hasSubmittedAcceptance(order) ? (
<div className="worker-order-mobile-draft-files">
<Tag color="processing" icon={<PictureOutlined />}>
{draftCount}
</Tag>
<ImagePreviewList files={draftFiles} size={32} maxVisible={3} />
</div>
) : null}
{noteText ? (
<div className="worker-order-mobile-acceptance-text">
{noteText}
</div>
) : null}
</div>
)}
{/* 底部备注与操作 */}
<div className="worker-order-mobile-footer-row">
<div className="worker-order-mobile-note-block">
{workerNote ? (
<div
className={`worker-note-badge theme-${resolveNoteTheme(workerNote)}`}
onClick={() => openNoteModal(order)}
>
<span className="worker-note-text" title={workerNote}>
{workerNote}
</span>
<EditOutlined className="worker-note-edit-icon" />
</div>
) : (
<button
type="button"
className="worker-order-mobile-add-note-btn"
onClick={() => openNoteModal(order)}
>
<EditOutlined />
</button>
)}
</div>
<div className="worker-order-mobile-btn-group">
<Button
size="small"
icon={<EyeOutlined />}
onClick={() => openOrderDetail(order)}
>
</Button>
{canEditAcceptanceImages(order) ? (
<Button
size="small"
type={canSubmitAcceptance(order) ? 'primary' : 'default'}
icon={<PictureOutlined />}
className="worker-order-mobile-action-submit"
onClick={() => openAcceptanceModal(order)}
>
{hasSubmittedAcceptance(order) ? '补充图片' : '提交验收'}
</Button>
) : null}
{canRemindAcceptance(order) ? (
<Button size="small" onClick={() => submitAcceptanceReminder(order)}>
</Button>
) : null}
{canRequestCancel(order) ? (
<Button size="small" danger onClick={() => openCancelRequestModal(order)}>
</Button>
) : null}
{canSubmitFeedback(order) ? (
<Button
size="small"
icon={<MessageOutlined />}
onClick={() => openFeedbackModal(order)}
>
</Button>
) : null}
</div>
</div>
</article>
)
})
)}
{(pagination?.total || 0) > 0 ? (
<div className="worker-orders-mobile-pagination">
<Pagination
current={pagination?.page || page}
pageSize={pagination?.pageSize || pageSize}
total={pagination?.total || 0}
simple
onChange={(nextPage: number, nextPageSize: number) => {
setPage(nextPage)
setPageSize(nextPageSize)
}}
/>
</div>
) : null}
</div>
</div>
) : ( ) : (
<WorkerOrderDesktopList <WorkerOrderDesktopList
statusOptions={STATUS_OPTIONS} statusOptions={STATUS_OPTIONS}
@@ -1062,8 +498,11 @@ export default function WorkerOrdersPage() {
keyword={keyword} keyword={keyword}
orders={orders} orders={orders}
pagination={pagination} pagination={pagination}
page={page}
pageSize={pageSize}
columns={columns} columns={columns}
loading={ordersQuery.isLoading} loading={ordersQuery.isLoading}
searching={ordersQuery.isFetching}
refreshing={ordersQuery.isFetching || overviewQuery.isFetching} refreshing={ordersQuery.isFetching || overviewQuery.isFetching}
error={ordersQuery.error} error={ordersQuery.error}
onRefresh={() => { onRefresh={() => {