新增接单平台第一期闭环

This commit is contained in:
yml2213
2026-07-25 13:58:53 +08:00
parent 06decc51ff
commit f6d3f00f21
27 changed files with 3987 additions and 0 deletions
@@ -0,0 +1,513 @@
import {
CheckOutlined,
PlusOutlined,
ReloadOutlined,
SendOutlined,
WarningOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
App,
Button,
Card,
Form,
Input,
InputNumber,
Modal,
Select,
Space,
Statistic,
Switch,
Table,
Tabs,
Tag,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import PageHeader from '@/components/admin/PageHeader'
import {
acceptAdminWorkOrder,
createAdminMockWorkOrder,
creditAdminWorkerWallet,
fetchAdminWorkerLevels,
fetchAdminWorkerPlatformSummary,
fetchAdminWorkerUsers,
fetchAdminWorkOrders,
markAdminWorkOrderProblem,
publishAdminWorkOrder,
reviewAdminWorkerUser,
saveAdminWorkerLevel,
} from '@/services/admin'
import type { WorkOrder, WorkerLevel, WorkerUser } from '@/types/worker-platform'
export default function AdminWorkerPlatformPage() {
return (
<section className="page-stack">
<PageHeader title="接单平台" description="管理接单员、接单工单、等级权限和本地 Mock 工单。" />
<SummaryCards />
<Tabs
destroyOnHidden={false}
items={[
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
{ key: 'workers', label: '接单员', children: <WorkersPanel /> },
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
]}
/>
</section>
)
}
function SummaryCards() {
const summaryQuery = useQuery({
queryKey: ['admin-worker-platform-summary'],
queryFn: () => fetchAdminWorkerPlatformSummary(),
})
const summary = summaryQuery.data?.data
return (
<div className="metric-grid four">
<Card>
<Statistic title="待审核接单员" value={summary?.pendingWorkerCount || 0} />
</Card>
<Card>
<Statistic title="待完善订单" value={summary?.pendingMaterialCount || 0} />
</Card>
<Card>
<Statistic title="大厅订单" value={summary?.openOrderCount || 0} />
</Card>
<Card>
<Statistic title="代练中" value={summary?.inProgressOrderCount || 0} />
</Card>
</div>
)
}
function WorkOrdersPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [status, setStatus] = useState('')
const [problemOrder, setProblemOrder] = useState<WorkOrder | null>(null)
const [problemForm] = Form.useForm()
const ordersQuery = useQuery({
queryKey: ['admin-worker-platform-orders', status],
queryFn: () => fetchAdminWorkOrders({ status }),
})
async function refreshAll() {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] }),
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }),
])
}
async function runAction(action: () => Promise<unknown>, successMessage: string) {
try {
await action()
message.success(successMessage)
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '操作失败')
}
}
async function submitProblem(values: { note?: string }) {
if (!problemOrder) return
await runAction(
() => markAdminWorkOrderProblem(problemOrder.workOrderId, values.note || ''),
'已标记问题单',
)
setProblemOrder(null)
problemForm.resetFields()
}
const columns: TableColumnsType<WorkOrder> = [
{
title: '订单',
minWidth: 300,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong>{row.productName}</Typography.Text>
<Typography.Text type="secondary">
{row.platformOrderId} · {row.workOrderNo}
</Typography.Text>
</div>
),
},
{ title: '金额', width: 110, render: (_, row) => formatMoney(row.rewardAmount) },
{ title: '押金', width: 110, render: (_, row) => formatMoney(row.requiredDepositAmount) },
{
title: '状态',
width: 130,
render: (_, row) => <Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>,
},
{
title: '接单员',
width: 140,
render: (_, row) => row.worker?.displayName || row.worker?.username || '-',
},
{
title: '操作',
width: 260,
render: (_, row) => (
<Space wrap>
{row.status === 'unassigned' ? (
<Button icon={<SendOutlined />} onClick={() => runAction(() => publishAdminWorkOrder(row.workOrderId), '已发布到大厅')}>
</Button>
) : null}
{row.status === 'pending_acceptance' ? (
<Button icon={<CheckOutlined />} type="primary" onClick={() => runAction(() => acceptAdminWorkOrder(row.workOrderId), '已验收通过')}>
</Button>
) : null}
{['in_progress', 'pending_acceptance'].includes(row.status) ? (
<Button icon={<WarningOutlined />} danger onClick={() => setProblemOrder(row)}>
</Button>
) : null}
</Space>
),
},
]
return (
<section className="platform-panel-stack">
<Card title="生成本地接单工单 Mock" bordered={false}>
<MockWorkOrderForm onCreated={refreshAll} />
</Card>
<Card
title="接单工单"
extra={
<Space>
<Select
value={status}
style={{ width: 150 }}
options={[
{ value: '', label: '全部状态' },
{ value: 'pending_material', label: '待完善' },
{ value: 'unassigned', label: '未分配' },
{ value: 'open', label: '待抢单' },
{ value: 'in_progress', label: '代练中' },
{ value: 'pending_acceptance', label: '待验收' },
{ value: 'problem', label: '问题单' },
{ value: 'accepted', label: '已验收' },
]}
onChange={setStatus}
/>
<Button icon={<ReloadOutlined />} loading={ordersQuery.isFetching} onClick={() => ordersQuery.refetch()}>
</Button>
</Space>
}
bordered={false}
>
<Table<WorkOrder>
rowKey="workOrderId"
loading={ordersQuery.isLoading}
dataSource={ordersQuery.data?.data.items || []}
columns={columns}
pagination={false}
scroll={{ x: 1100 }}
/>
</Card>
<Modal
title="标记问题单"
open={Boolean(problemOrder)}
onCancel={() => setProblemOrder(null)}
onOk={() => problemForm.submit()}
destroyOnHidden
>
<Form form={problemForm} layout="vertical" onFinish={submitProblem}>
<Form.Item label="问题备注" name="note" rules={[{ required: true, message: '请填写问题备注' }]}>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
</section>
)
}
function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void> }) {
const { message } = App.useApp()
const [loading, setLoading] = useState(false)
async function submit(values: {
platformOrderId?: string
productName?: string
rewardAmount?: number
materialComplete?: boolean
}) {
setLoading(true)
try {
await createAdminMockWorkOrder(values)
message.success('Mock 工单已生成')
await onCreated()
} catch (error) {
message.error(error instanceof Error ? error.message : '生成失败')
} finally {
setLoading(false)
}
}
return (
<Form
layout="inline"
initialValues={{ productName: '指挥官秘钥1个', rewardAmount: 25, materialComplete: false }}
onFinish={submit}
>
<Form.Item label="订单号" name="platformOrderId">
<Input placeholder="留空自动生成" style={{ width: 180 }} />
</Form.Item>
<Form.Item label="商品" name="productName">
<Input style={{ width: 220 }} />
</Form.Item>
<Form.Item label="接单金额" name="rewardAmount">
<InputNumber min={0.01} step={1} addonAfter="元" />
</Form.Item>
<Form.Item label="资料完整" name="materialComplete" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item>
<Button type="primary" icon={<PlusOutlined />} htmlType="submit" loading={loading}>
</Button>
</Form.Item>
</Form>
)
}
function WorkersPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [status, setStatus] = useState('')
const [creditWorker, setCreditWorker] = useState<WorkerUser | null>(null)
const [creditForm] = Form.useForm()
const workersQuery = useQuery({
queryKey: ['admin-worker-platform-workers', status],
queryFn: () => fetchAdminWorkerUsers({ status }),
})
async function refreshWorkers() {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-workers'] }),
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }),
])
}
async function review(worker: WorkerUser, nextStatus: string) {
try {
await reviewAdminWorkerUser(worker.workerId, { status: nextStatus })
message.success('接单员状态已更新')
await refreshWorkers()
} catch (error) {
message.error(error instanceof Error ? error.message : '操作失败')
}
}
async function submitCredit(values: { amount?: number; note?: string }) {
if (!creditWorker) return
try {
await creditAdminWorkerWallet(creditWorker.workerId, {
amount: Number(values.amount || 0),
note: values.note,
})
message.success('余额已增加')
setCreditWorker(null)
creditForm.resetFields()
await refreshWorkers()
} catch (error) {
message.error(error instanceof Error ? error.message : '充值失败')
}
}
const columns: TableColumnsType<WorkerUser> = [
{
title: '接单员',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong>{row.displayName || row.username}</Typography.Text>
<Typography.Text type="secondary">{row.username}</Typography.Text>
</div>
),
},
{ title: '等级', width: 140, render: (_, row) => row.level?.name || '-' },
{ title: '余额', width: 120, render: (_, row) => formatMoney(row.wallet.availableAmount) },
{ title: '冻结', width: 120, render: (_, row) => formatMoney(row.wallet.frozenDepositAmount) },
{
title: '状态',
width: 130,
render: (_, row) => <Tag color={row.status === 'active' ? 'green' : 'orange'}>{formatWorkerStatus(row.status)}</Tag>,
},
{
title: '操作',
width: 260,
render: (_, row) => (
<Space wrap>
{row.status !== 'active' ? <Button onClick={() => review(row, 'active')}></Button> : null}
{row.status !== 'rejected' ? <Button onClick={() => review(row, 'rejected')}></Button> : null}
<Button onClick={() => setCreditWorker(row)}></Button>
</Space>
),
},
]
return (
<Card
title="接单员"
extra={
<Space>
<Select
value={status}
style={{ width: 150 }}
options={[
{ value: '', label: '全部状态' },
{ value: 'pending_review', label: '待审核' },
{ value: 'active', label: '已通过' },
{ value: 'rejected', label: '已拒绝' },
{ value: 'disabled', label: '已停用' },
]}
onChange={setStatus}
/>
<Button icon={<ReloadOutlined />} loading={workersQuery.isFetching} onClick={() => workersQuery.refetch()}>
</Button>
</Space>
}
bordered={false}
>
<Table<WorkerUser>
rowKey="workerId"
loading={workersQuery.isLoading}
dataSource={workersQuery.data?.data.items || []}
columns={columns}
pagination={false}
scroll={{ x: 1000 }}
/>
<Modal
title="人工增加余额"
open={Boolean(creditWorker)}
onCancel={() => setCreditWorker(null)}
onOk={() => creditForm.submit()}
destroyOnHidden
>
<Form form={creditForm} layout="vertical" onFinish={submitCredit}>
<Form.Item label="金额" name="amount" rules={[{ required: true, message: '请输入金额' }]}>
<InputNumber min={0.01} step={10} addonAfter="元" className="full-width" />
</Form.Item>
<Form.Item label="备注" name="note">
<Input />
</Form.Item>
</Form>
</Modal>
</Card>
)
}
function LevelsPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const levelsQuery = useQuery({
queryKey: ['admin-worker-platform-levels'],
queryFn: () => fetchAdminWorkerLevels(),
})
const [form] = Form.useForm()
async function saveLevel(values: { levelKey: string; name: string; depositFreeAmount?: number; maxActiveOrders?: number }) {
try {
await saveAdminWorkerLevel(values)
message.success('等级已保存')
await queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-levels'] })
} catch (error) {
message.error(error instanceof Error ? error.message : '保存失败')
}
}
const columns: TableColumnsType<WorkerLevel> = [
{ title: '等级', dataIndex: 'name' },
{ title: '标识', dataIndex: 'levelKey' },
{ title: '免押额度', render: (_, row) => formatMoney(row.depositFreeAmount) },
{ title: '最大同时接单', dataIndex: 'maxActiveOrders' },
{ title: '状态', dataIndex: 'status' },
]
return (
<section className="platform-panel-stack">
<Card title="保存等级" bordered={false}>
<Form
form={form}
layout="inline"
initialValues={{ levelKey: 'regular', name: '普通接单员', depositFreeAmount: 0, maxActiveOrders: 3 }}
onFinish={saveLevel}
>
<Form.Item label="标识" name="levelKey" rules={[{ required: true, message: '请输入标识' }]}>
<Input />
</Form.Item>
<Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
<Input />
</Form.Item>
<Form.Item label="免押额度" name="depositFreeAmount">
<InputNumber min={0} step={10} addonAfter="元" />
</Form.Item>
<Form.Item label="最大同时接单" name="maxActiveOrders">
<InputNumber min={1} max={999} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit"></Button>
</Form.Item>
</Form>
</Card>
<Card title="等级列表" bordered={false}>
<Table<WorkerLevel>
rowKey="levelId"
loading={levelsQuery.isLoading}
dataSource={levelsQuery.data?.data.items || []}
columns={columns}
pagination={false}
/>
</Card>
</section>
)
}
function formatMoney(value: number | undefined) {
return `${((Number(value || 0) || 0) / 100).toFixed(2)}`
}
function formatStatus(status: string) {
const labels: Record<string, string> = {
pending_material: '待完善',
unassigned: '未分配',
open: '待抢单',
in_progress: '代练中',
pending_acceptance: '待验收',
problem: '问题单',
accepted: '已验收',
cancelled: '已取消',
}
return labels[status] || status
}
function resolveStatusColor(status: string) {
if (status === 'accepted') return 'green'
if (status === 'problem') return 'red'
if (status === 'pending_acceptance') return 'gold'
if (status === 'open') return 'blue'
return 'default'
}
function formatWorkerStatus(status: string) {
const labels: Record<string, string> = {
pending_review: '待审核',
active: '已通过',
rejected: '已拒绝',
disabled: '已停用',
}
return labels[status] || status
}