新增打手撤单审核
This commit is contained in:
@@ -7,6 +7,7 @@ import PageHeader from '@/components/admin/PageHeader'
|
||||
import { fetchAdminWorkerPlatformSummary } from '@/services/admin'
|
||||
import { getAdminRole } from '@/utils/admin-auth'
|
||||
import CategoriesPanel from './panels/CategoriesPanel'
|
||||
import CancelRequestsPanel from './panels/CancelRequestsPanel'
|
||||
import FinancePanel from './panels/FinancePanel'
|
||||
import LevelsPanel from './panels/LevelsPanel'
|
||||
import NotificationsPanel from './panels/NotificationsPanel'
|
||||
@@ -21,8 +22,7 @@ export default function AdminWorkerPlatformPage() {
|
||||
const role = getAdminRole()
|
||||
const isAdmin = role === 'admin'
|
||||
const isSupport = role === 'support'
|
||||
const visibleActiveTab =
|
||||
isSupport || (!isAdmin && activeTab === 'notifications') ? 'orders' : activeTab
|
||||
const visibleActiveTab = !isAdmin && activeTab === 'notifications' ? 'orders' : activeTab
|
||||
|
||||
useEffect(() => {
|
||||
const tab = searchParams.get('tab')
|
||||
@@ -46,9 +46,13 @@ export default function AdminWorkerPlatformPage() {
|
||||
onChange={changeTab}
|
||||
items={
|
||||
isSupport
|
||||
? [{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> }]
|
||||
? [
|
||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
||||
]
|
||||
: [
|
||||
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
|
||||
{ key: 'cancel-requests', label: '撤单审核', children: <CancelRequestsPanel /> },
|
||||
{ key: 'rules', label: '接单模板', children: <ProductRulesPanel /> },
|
||||
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
||||
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
||||
@@ -70,6 +74,7 @@ const ACTIVE_TAB_STORAGE_KEY = 'admin-worker-platform-tab'
|
||||
function loadActiveTabPreference(): string {
|
||||
const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
|
||||
return saved === 'orders' ||
|
||||
saved === 'cancel-requests' ||
|
||||
saved === 'rules' ||
|
||||
saved === 'product-match' ||
|
||||
saved === 'categories' ||
|
||||
@@ -88,6 +93,7 @@ function loadActiveTab(tab: string | null): string {
|
||||
function isWorkerPlatformTab(tab: string | null): tab is string {
|
||||
return [
|
||||
'orders',
|
||||
'cancel-requests',
|
||||
'rules',
|
||||
'product-match',
|
||||
'categories',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -34,6 +34,7 @@ import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
|
||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import {
|
||||
fetchWorkerCancelRequests,
|
||||
fetchWorkerMyOrderOverview,
|
||||
fetchWorkerMyOrders,
|
||||
fetchWorkerProfile,
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
saveWorkerOrderNote,
|
||||
supplementWorkerAcceptedOrderEvidence,
|
||||
submitWorkerAcceptance,
|
||||
submitWorkerCancelRequest,
|
||||
} from '@/services/worker'
|
||||
import type {
|
||||
CollectField,
|
||||
@@ -73,6 +75,10 @@ type WorkerNoteFormValues = {
|
||||
note?: string
|
||||
}
|
||||
|
||||
type CancelRequestFormValues = {
|
||||
reason?: string
|
||||
}
|
||||
|
||||
function buildWorkerOrderQuery(status: string, keyword: string, page: number, pageSize: number) {
|
||||
return {
|
||||
keyword,
|
||||
@@ -194,9 +200,11 @@ export default function WorkerOrdersPage() {
|
||||
const [detailOrder, setDetailOrder] = useState<WorkOrder | null>(null)
|
||||
const [editingOrder, setEditingOrder] = useState<WorkOrder | null>(null)
|
||||
const [noteOrder, setNoteOrder] = useState<WorkOrder | null>(null)
|
||||
const [cancelRequestOrder, setCancelRequestOrder] = useState<WorkOrder | null>(null)
|
||||
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
|
||||
const [form] = Form.useForm<AcceptanceFormValues>()
|
||||
const [noteForm] = Form.useForm<WorkerNoteFormValues>()
|
||||
const [cancelRequestForm] = Form.useForm<CancelRequestFormValues>()
|
||||
|
||||
const ordersQuery = useQuery({
|
||||
queryKey: ['worker-my-orders', status, keyword, page, pageSize],
|
||||
@@ -221,6 +229,11 @@ export default function WorkerOrdersPage() {
|
||||
queryFn: fetchWorkerMyOrderOverview,
|
||||
retry: false,
|
||||
})
|
||||
const cancelRequestsQuery = useQuery({
|
||||
queryKey: ['worker-cancel-requests'],
|
||||
queryFn: fetchWorkerCancelRequests,
|
||||
retry: false,
|
||||
})
|
||||
const overviewByStatus = new Map(
|
||||
(overviewQuery.data?.data.items || []).map((item) => [item.status, item]),
|
||||
)
|
||||
@@ -229,6 +242,7 @@ export default function WorkerOrdersPage() {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-my-orders-overview'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['worker-cancel-requests'] }),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -314,6 +328,27 @@ export default function WorkerOrdersPage() {
|
||||
noteForm.setFieldsValue({ note: String(order.workerNote || '') })
|
||||
}
|
||||
|
||||
function openCancelRequestModal(order: WorkOrder) {
|
||||
markOrderAsRead(order)
|
||||
setCancelRequestOrder(order)
|
||||
cancelRequestForm.resetFields()
|
||||
}
|
||||
|
||||
async function submitCancelRequest(values: CancelRequestFormValues) {
|
||||
if (!cancelRequestOrder) return
|
||||
try {
|
||||
await submitWorkerCancelRequest(cancelRequestOrder.workOrderId, {
|
||||
reason: String(values.reason || '').trim(),
|
||||
})
|
||||
message.success('取消申请已提交,等待客服审核')
|
||||
setCancelRequestOrder(null)
|
||||
cancelRequestForm.resetFields()
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '提交取消申请失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOrderNote(values: WorkerNoteFormValues) {
|
||||
if (!noteOrder) return
|
||||
try {
|
||||
@@ -644,6 +679,11 @@ export default function WorkerOrdersPage() {
|
||||
{canRemindAcceptance(row) ? (
|
||||
<Button onClick={() => submitAcceptanceReminder(row)}>催验收</Button>
|
||||
) : null}
|
||||
{canRequestCancel(row) ? (
|
||||
<Button danger onClick={() => openCancelRequestModal(row)}>
|
||||
申请取消
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
@@ -950,6 +990,11 @@ export default function WorkerOrdersPage() {
|
||||
催验收
|
||||
</Button>
|
||||
) : null}
|
||||
{canRequestCancel(order) ? (
|
||||
<Button size="small" danger onClick={() => openCancelRequestModal(order)}>
|
||||
申请取消
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1113,6 +1158,11 @@ export default function WorkerOrdersPage() {
|
||||
{hasSubmittedAcceptance(detailOrder) ? '补充图片' : '上传图片 / 提交验收'}
|
||||
</Button>
|
||||
) : null}
|
||||
{canRequestCancel(detailOrder) ? (
|
||||
<Button danger onClick={() => openCancelRequestModal(detailOrder)}>
|
||||
申请取消
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
@@ -1229,6 +1279,46 @@ export default function WorkerOrdersPage() {
|
||||
</div>
|
||||
) : null}
|
||||
</Drawer>
|
||||
<Modal
|
||||
title={`申请取消 · ${getDisplayOrderNo(cancelRequestOrder)}`}
|
||||
open={Boolean(cancelRequestOrder)}
|
||||
width={isMobile ? '92%' : 520}
|
||||
onCancel={() => {
|
||||
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)}
|
||||
@@ -1382,6 +1472,16 @@ function canRemindAcceptance(order: WorkOrder) {
|
||||
return !order.myShare && order.status === 'pending_acceptance'
|
||||
}
|
||||
|
||||
function canRequestCancel(order: WorkOrder) {
|
||||
if (order.myShare) {
|
||||
return (
|
||||
['joined', 'submitted'].includes(order.myShare.status) &&
|
||||
['open', 'pending_acceptance'].includes(order.status)
|
||||
)
|
||||
}
|
||||
return order.status === 'in_progress'
|
||||
}
|
||||
|
||||
function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) {
|
||||
if (!workerCanAutoAcceptWithoutEvidence) return false
|
||||
if (order.myShare) return order.myShare.status === 'joined' && order.status !== 'accepted'
|
||||
|
||||
Reference in New Issue
Block a user