Files
order_site/apps/frontend/src/pages/admin/panels/CancelRequestsPanel.tsx
T

204 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.platformOrderId || '-'}</Typography.Text>
<Typography.Text type="secondary">{request.shopName || '-'}</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>
</>
)
}