新增打手撤单审核

This commit is contained in:
yml2213
2026-08-20 19:43:03 +08:00
parent d76d1e8d90
commit e504b3a207
20 changed files with 1331 additions and 8 deletions
@@ -0,0 +1,203 @@
import { ReloadOutlined } from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { App, Button, Card, Form, Input, Modal, Space, Table, Tag, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useEffect, useState } from 'react'
import { useSearchParams } from 'react-router'
import { fetchAdminWorkerCancelRequests, reviewAdminWorkerCancelRequest } from '@/services/admin'
import type { WorkerCancelRequest } from '@/types/worker-platform'
import { formatAdminDateTime } from '@/utils/admin-time'
type ReviewFormValues = {
reviewNote?: string
}
export default function CancelRequestsPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [searchParams, setSearchParams] = useSearchParams()
const [reviewTarget, setReviewTarget] = useState<{
request: WorkerCancelRequest
approved: boolean
} | null>(null)
const [form] = Form.useForm<ReviewFormValues>()
const requestsQuery = useQuery({
queryKey: ['admin-worker-cancel-requests', 'pending'],
queryFn: () => fetchAdminWorkerCancelRequests('pending'),
})
useEffect(() => {
const requestId = Number(searchParams.get('cancelRequestId') || 0)
if (!Number.isInteger(requestId) || requestId <= 0) return
const request = (requestsQuery.data?.data.items || []).find(
(item) => item.requestId === requestId,
)
if (!request) return
setReviewTarget({ request, approved: true })
form.resetFields()
setSearchParams(
(current) => {
current.delete('cancelRequestId')
return current
},
{ replace: true },
)
}, [form, requestsQuery.data, searchParams, setSearchParams])
async function refreshAll() {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['admin-worker-cancel-requests'] }),
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] }),
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }),
])
}
function openReview(request: WorkerCancelRequest, approved: boolean) {
setReviewTarget({ request, approved })
form.resetFields()
}
async function submitReview(values: ReviewFormValues) {
if (!reviewTarget) return
try {
const response = await reviewAdminWorkerCancelRequest(reviewTarget.request.requestId, {
approved: reviewTarget.approved,
reviewNote: String(values.reviewNote || '').trim(),
})
const result = response.data
const resultText = result.accountDisabled
? ',该打手已封禁'
: result.levelChanged
? ',该打手已降一级 VIP'
: ''
message.success(`${result.approved ? '已同意' : '已驳回'}撤单申请${resultText}`)
setReviewTarget(null)
form.resetFields()
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '审核撤单申请失败')
}
}
const columns: TableColumnsType<WorkerCancelRequest> = [
{
title: '订单',
width: 260,
render: (_, request) => (
<div className="cell-stack">
<Typography.Text strong>{request.productName || '-'}</Typography.Text>
<Typography.Text type="secondary">{request.workOrderNo || '-'}</Typography.Text>
</div>
),
},
{
title: '打手 / 份数',
width: 180,
render: (_, request) => (
<div className="cell-stack">
<Typography.Text>{request.workerName || `打手 #${request.workerId}`}</Typography.Text>
{request.shareQuantity ? (
<Tag color="purple"> {request.shareQuantity} </Tag>
) : (
<Tag></Tag>
)}
</div>
),
},
{
title: '申请原因',
render: (_, request) => (
<Typography.Text ellipsis={{ tooltip: request.reason }}>{request.reason}</Typography.Text>
),
},
{
title: '申请时间',
width: 180,
render: (_, request) => formatAdminDateTime(request.createdAt),
},
{
title: '操作',
width: 180,
render: (_, request) => (
<Space>
<Button type="primary" size="small" onClick={() => openReview(request, true)}>
</Button>
<Button danger size="small" onClick={() => openReview(request, false)}>
</Button>
</Space>
),
},
]
return (
<>
<Card
title={`待审核撤单申请(${requestsQuery.data?.data.items.length || 0}`}
extra={
<Button
icon={<ReloadOutlined />}
loading={requestsQuery.isFetching}
onClick={() => requestsQuery.refetch()}
>
</Button>
}
bordered={false}
>
<Table<WorkerCancelRequest>
rowKey="requestId"
size="small"
loading={requestsQuery.isLoading}
dataSource={requestsQuery.data?.data.items || []}
columns={columns}
pagination={false}
locale={{ emptyText: '暂无待审核撤单申请' }}
scroll={{ x: 900 }}
/>
</Card>
<Modal
title={reviewTarget?.approved ? '同意撤单申请' : '驳回撤单申请'}
open={Boolean(reviewTarget)}
onCancel={() => {
setReviewTarget(null)
form.resetFields()
}}
onOk={() => form.submit()}
okText={reviewTarget?.approved ? '确认同意' : '确认驳回'}
okButtonProps={reviewTarget?.approved ? undefined : { danger: true }}
destroyOnHidden
>
{reviewTarget ? (
<Space direction="vertical" size={12} style={{ width: '100%' }}>
<Typography.Text>
{reviewTarget.request.workerName || `打手 #${reviewTarget.request.workerId}`}
{reviewTarget.request.reason}
</Typography.Text>
<Typography.Text type={reviewTarget.approved ? 'warning' : 'secondary'}>
{reviewTarget.approved
? '同意后将退还冻结押金,并将整单或对应拼单份额回到大厅;该申请会计入撤单处罚次数。'
: '驳回后不会计入累计次数,且会释放本次申请的当天额度。'}
</Typography.Text>
<Form form={form} layout="vertical" onFinish={submitReview}>
<Form.Item
label="审核备注"
name="reviewNote"
rules={[{ required: true, whitespace: true, message: '请填写审核备注' }]}
>
<Input.TextArea
rows={3}
maxLength={300}
showCount
placeholder="请填写审核结论或说明"
/>
</Form.Item>
</Form>
</Space>
) : null}
</Modal>
</>
)
}
@@ -24,6 +24,7 @@ const notificationEventRows: NotificationEventRow[] = [
{ key: 'acceptance_submitted', name: '提交验收', description: '打手提交订单验收资料' },
{ key: 'acceptance_reminded', name: '催验收', description: '打手在验收后催促处理' },
{ key: 'material_required', name: '待补资料工单', description: '新工单等待后台补充资料' },
{ key: 'cancel_requested', name: '撤单申请', description: '打手提交新的撤单申请,等待客服审核' },
]
export default function NotificationsPanel() {
@@ -267,7 +267,6 @@ export default function WorkOrdersPanel() {
}),
enabled: Boolean(assignOrder),
})
useEffect(() => {
setSelectedAcceptanceOrderIds([])
}, [statuses, keyword, workerId, workerLevelIds, targetWorkOrderId, page, pageSize])
@@ -1327,7 +1326,6 @@ export default function WorkOrdersPanel() {
</div>
) : null}
</Drawer>
<Modal
title={`编辑工单 · ${getDisplayOrderNo(editOrder)}`}
open={Boolean(editOrder)}
@@ -2012,7 +2010,10 @@ function renderWorkOrderWorkers(order: WorkOrder) {
const names = order.sharingWorkers
.map((worker) => worker.displayName || worker.username || `打手#${worker.workerId}`)
.join('、')
const quantity = order.sharingWorkers.reduce((sum, worker) => sum + Number(worker.quantity || 0), 0)
const quantity = order.sharingWorkers.reduce(
(sum, worker) => sum + Number(worker.quantity || 0),
0,
)
return (
<div className="cell-stack">
<Typography.Text ellipsis={{ tooltip: names }}>{names}</Typography.Text>