拆分打手订单展示
This commit is contained in:
@@ -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 {
|
import {
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
Descriptions,
|
|
||||||
Drawer,
|
|
||||||
Empty,
|
Empty,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
|
||||||
Pagination,
|
Pagination,
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Table,
|
|
||||||
Tabs,
|
|
||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -31,9 +25,7 @@ import type { TableColumnsType } from 'antd'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { DeadlineCountdown } from '@/components/DeadlineCountdown'
|
import { DeadlineCountdown } from '@/components/DeadlineCountdown'
|
||||||
import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
|
|
||||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||||
import ImageUpload from '@/components/files/ImageUpload'
|
|
||||||
import {
|
import {
|
||||||
fetchWorkerCancelRequests,
|
fetchWorkerCancelRequests,
|
||||||
fetchWorkerOrderFeedbacks,
|
fetchWorkerOrderFeedbacks,
|
||||||
@@ -54,6 +46,15 @@ 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 { WorkerOrderDetailDrawer } from './WorkerOrderDetailDrawer'
|
||||||
|
import {
|
||||||
|
type AcceptanceFormValues,
|
||||||
|
type CancelRequestFormValues,
|
||||||
|
type FeedbackFormValues,
|
||||||
|
type WorkerNoteFormValues,
|
||||||
|
WorkerOrderModals,
|
||||||
|
} from './WorkerOrderModals'
|
||||||
import {
|
import {
|
||||||
asRecord,
|
asRecord,
|
||||||
canAutoAcceptOrder,
|
canAutoAcceptOrder,
|
||||||
@@ -71,14 +72,12 @@ import {
|
|||||||
getMaterialDetailItems,
|
getMaterialDetailItems,
|
||||||
getMaterialScreenshots,
|
getMaterialScreenshots,
|
||||||
getSettlementLabel,
|
getSettlementLabel,
|
||||||
getStatusHint,
|
|
||||||
getUniqueAcceptanceFiles,
|
getUniqueAcceptanceFiles,
|
||||||
getWorkerAcceptance,
|
getWorkerAcceptance,
|
||||||
getWorkerDraft,
|
getWorkerDraft,
|
||||||
hasSubmittedAcceptance,
|
hasSubmittedAcceptance,
|
||||||
isVipEvidencePending,
|
isVipEvidencePending,
|
||||||
patchWorkOrderReadState,
|
patchWorkOrderReadState,
|
||||||
renderDetailFieldValues,
|
|
||||||
resolveStatusColor,
|
resolveStatusColor,
|
||||||
toPngBlob,
|
toPngBlob,
|
||||||
} from './worker-order-utils'
|
} from './worker-order-utils'
|
||||||
@@ -94,33 +93,8 @@ const STATUS_OPTIONS = [
|
|||||||
{ value: '', label: '全部订单' },
|
{ value: '', label: '全部订单' },
|
||||||
] as const
|
] 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'
|
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 {
|
function resolveNoteTheme(note?: string): NoteTheme {
|
||||||
const text = String(note || '')
|
const text = String(note || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -1080,466 +1054,87 @@ export default function WorkerOrdersPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<section className="page-stack">
|
<WorkerOrderDesktopList
|
||||||
<div className="worker-page-head">
|
statusOptions={STATUS_OPTIONS}
|
||||||
<Typography.Title level={3}>我的订单</Typography.Title>
|
status={status}
|
||||||
<Button
|
overviewByStatus={overviewByStatus}
|
||||||
icon={<ReloadOutlined />}
|
keywordInput={keywordInput}
|
||||||
loading={ordersQuery.isFetching || overviewQuery.isFetching}
|
keyword={keyword}
|
||||||
onClick={() => refreshAll()}
|
orders={orders}
|
||||||
>
|
pagination={pagination}
|
||||||
刷新
|
columns={columns}
|
||||||
</Button>
|
loading={ordersQuery.isLoading}
|
||||||
</div>
|
refreshing={ordersQuery.isFetching || overviewQuery.isFetching}
|
||||||
|
error={ordersQuery.error}
|
||||||
<Card bordered={false}>
|
onRefresh={() => {
|
||||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
void refreshAll()
|
||||||
<Tabs
|
}}
|
||||||
className="worker-order-tabs"
|
onStatusChange={(nextStatus) => {
|
||||||
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)
|
setStatus(nextStatus)
|
||||||
setPage(1)
|
setPage(1)
|
||||||
}}
|
}}
|
||||||
/>
|
onKeywordInputChange={(nextValue) => {
|
||||||
|
|
||||||
<Space.Compact style={{ width: '100%', maxWidth: 460 }}>
|
|
||||||
<Input
|
|
||||||
allowClear
|
|
||||||
value={keywordInput}
|
|
||||||
placeholder="搜索订单号 / 商品名 / 游戏ID / 游戏昵称"
|
|
||||||
prefix={<SearchOutlined />}
|
|
||||||
onChange={(event) => {
|
|
||||||
const nextValue = event.target.value
|
|
||||||
setKeywordInput(nextValue)
|
setKeywordInput(nextValue)
|
||||||
if (!nextValue.trim()) {
|
if (!nextValue.trim()) {
|
||||||
setKeyword('')
|
setKeyword('')
|
||||||
setPage(1)
|
setPage(1)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPressEnter={applyKeywordSearch}
|
onKeywordSearch={applyKeywordSearch}
|
||||||
/>
|
onPageChange={(nextPage, nextPageSize) => {
|
||||||
<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)
|
setPage(nextPage)
|
||||||
setPageSize(nextPageSize)
|
setPageSize(nextPageSize)
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
scroll={{ x: 1440 }}
|
|
||||||
/>
|
/>
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Drawer
|
<WorkerOrderDetailDrawer
|
||||||
className="worker-order-detail-drawer"
|
isMobile={isMobile}
|
||||||
title="订单详情"
|
order={detailOrder}
|
||||||
width={isMobile ? '100%' : 820}
|
orderTimelineVisible={orderTimelineVisible}
|
||||||
open={Boolean(detailOrder)}
|
|
||||||
destroyOnHidden
|
|
||||||
onClose={() => setDetailOrder(null)}
|
onClose={() => setDetailOrder(null)}
|
||||||
>
|
onOpenAcceptance={openAcceptanceModal}
|
||||||
{detailOrder ? (
|
onOpenCancelRequest={openCancelRequestModal}
|
||||||
<div className="worker-order-detail-stack">
|
onOpenFeedback={openFeedbackModal}
|
||||||
<div className="worker-order-detail-actions">
|
/>
|
||||||
<Tag color={resolveStatusColor(detailOrder.status)}>
|
<WorkerOrderModals
|
||||||
{formatStatus(detailOrder.status)}
|
isMobile={isMobile}
|
||||||
</Tag>
|
feedbackOrder={feedbackOrder}
|
||||||
{canEditAcceptanceImages(detailOrder) ? (
|
feedbacks={feedbacksQuery.data?.data.items || []}
|
||||||
<Button
|
feedbackForm={feedbackForm}
|
||||||
type={canSubmitAcceptance(detailOrder) ? 'primary' : 'default'}
|
onCloseFeedback={() => {
|
||||||
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={() => {
|
|
||||||
setFeedbackOrder(null)
|
setFeedbackOrder(null)
|
||||||
feedbackForm.resetFields()
|
feedbackForm.resetFields()
|
||||||
}}
|
}}
|
||||||
onOk={() => feedbackForm.submit()}
|
onSubmitFeedback={submitFeedback}
|
||||||
okText="提交反馈"
|
cancelRequestOrder={cancelRequestOrder}
|
||||||
destroyOnHidden
|
cancelRequestRisk={cancelRequestsQuery.data?.data.risk}
|
||||||
>
|
cancelRequestForm={cancelRequestForm}
|
||||||
<Typography.Paragraph type="secondary">
|
onCloseCancelRequest={() => {
|
||||||
可反馈客户资料错误、账号异常或无法继续处理等问题。客服处理后会在反馈记录中回复。
|
|
||||||
</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={() => {
|
|
||||||
setCancelRequestOrder(null)
|
setCancelRequestOrder(null)
|
||||||
cancelRequestForm.resetFields()
|
cancelRequestForm.resetFields()
|
||||||
}}
|
}}
|
||||||
onOk={() => cancelRequestForm.submit()}
|
onSubmitCancelRequest={submitCancelRequest}
|
||||||
okText="提交申请"
|
noteOrder={noteOrder}
|
||||||
destroyOnHidden
|
noteForm={noteForm}
|
||||||
>
|
onCloseNote={() => {
|
||||||
<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={() => {
|
|
||||||
setNoteOrder(null)
|
setNoteOrder(null)
|
||||||
noteForm.resetFields()
|
noteForm.resetFields()
|
||||||
}}
|
}}
|
||||||
onOk={() => noteForm.submit()}
|
onSaveNote={saveOrderNote}
|
||||||
destroyOnHidden
|
editingOrder={editingOrder}
|
||||||
>
|
acceptanceFiles={acceptanceFiles}
|
||||||
<Form form={noteForm} layout="vertical" onFinish={saveOrderNote}>
|
acceptanceForm={form}
|
||||||
<Form.Item label="快捷选择标签" style={{ marginBottom: 12 }}>
|
workerCanAutoAcceptWithoutEvidence={workerCanAutoAcceptWithoutEvidence}
|
||||||
<div className="worker-note-preset-tags">
|
onCloseAcceptance={() => {
|
||||||
{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={() => {
|
|
||||||
setEditingOrder(null)
|
setEditingOrder(null)
|
||||||
setAcceptanceFiles([])
|
setAcceptanceFiles([])
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
}}
|
}}
|
||||||
destroyOnHidden
|
onAcceptanceFilesChange={setAcceptanceFiles}
|
||||||
footer={
|
onSubmitAcceptance={submitAcceptance}
|
||||||
editingOrder ? (
|
onSaveAcceptanceImages={saveAcceptanceImages}
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user