新增接单平台第一期闭环
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { App, Button, Card, Form, Input, Result, Space, Typography } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { lookupCollectOrder, submitCollectOrder } from '@/services/worker'
|
||||
import type { CollectField } from '@/types/worker-platform'
|
||||
|
||||
type LookupOrder = {
|
||||
workOrderNo: string
|
||||
platformOrderId: string
|
||||
productName: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export default function CollectPage() {
|
||||
const { message } = App.useApp()
|
||||
const [orderNo, setOrderNo] = useState('')
|
||||
const [order, setOrder] = useState<LookupOrder | null>(null)
|
||||
const [fields, setFields] = useState<CollectField[]>([])
|
||||
const [lookupLoading, setLookupLoading] = useState(false)
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [complete, setComplete] = useState(false)
|
||||
|
||||
async function submitLookup(values: { orderNo: string }) {
|
||||
setLookupLoading(true)
|
||||
setComplete(false)
|
||||
try {
|
||||
const response = await lookupCollectOrder(values.orderNo)
|
||||
setOrderNo(values.orderNo)
|
||||
setOrder(response.data.order)
|
||||
setFields(response.data.fields)
|
||||
} catch (error) {
|
||||
setOrder(null)
|
||||
setFields([])
|
||||
message.error(error instanceof Error ? error.message : '查询失败')
|
||||
} finally {
|
||||
setLookupLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitMaterial(values: Record<string, string>) {
|
||||
setSubmitLoading(true)
|
||||
try {
|
||||
const response = await submitCollectOrder({ orderNo, fields: values })
|
||||
setComplete(response.data.complete)
|
||||
message.success(response.data.complete ? '资料已完善' : '资料已提交')
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '提交失败')
|
||||
} finally {
|
||||
setSubmitLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="collect-page">
|
||||
<Card className="collect-card" bordered={false}>
|
||||
<Typography.Title level={3}>订单资料补充</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
请填写正确订单号,匹配成功后继续补充资料。
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Form layout="vertical" onFinish={submitLookup}>
|
||||
<Space.Compact className="full-width">
|
||||
<Form.Item
|
||||
name="orderNo"
|
||||
className="collect-order-input"
|
||||
rules={[{ required: true, message: '请输入订单号' }]}
|
||||
>
|
||||
<Input placeholder="订单号" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" icon={<SearchOutlined />} loading={lookupLoading}>
|
||||
查询
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form>
|
||||
|
||||
{complete ? (
|
||||
<Result status="success" title="资料已提交完成" subTitle="订单已进入待分配状态,请等待处理。" />
|
||||
) : null}
|
||||
|
||||
{order && !complete ? (
|
||||
<Card className="platform-section-gap" title={order.productName} size="small">
|
||||
<Typography.Paragraph type="secondary">
|
||||
订单号:{order.platformOrderId}
|
||||
</Typography.Paragraph>
|
||||
<Form layout="vertical" onFinish={submitMaterial}>
|
||||
{fields.map((field) => (
|
||||
<Form.Item
|
||||
key={field.key}
|
||||
label={field.label}
|
||||
name={field.key}
|
||||
rules={field.required ? [{ required: true, message: `请填写${field.label}` }] : []}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
))}
|
||||
<Button type="primary" htmlType="submit" loading={submitLoading}>
|
||||
提交资料
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
) : null}
|
||||
</Card>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { App, Button, Card, Empty, Input, Space, Spin, Tag, Typography } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { fetchWorkerHallOrders, grabWorkerOrder } from '@/services/worker'
|
||||
import type { WorkOrder } from '@/types/worker-platform'
|
||||
|
||||
export default function WorkerHallPage() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [grabbingId, setGrabbingId] = useState(0)
|
||||
|
||||
const ordersQuery = useQuery({
|
||||
queryKey: ['worker-hall-orders', keyword],
|
||||
queryFn: () => fetchWorkerHallOrders({ keyword }),
|
||||
retry: false,
|
||||
})
|
||||
|
||||
const orders = ordersQuery.data?.data.items || []
|
||||
|
||||
async function submitGrab(order: WorkOrder) {
|
||||
setGrabbingId(order.workOrderId)
|
||||
try {
|
||||
await grabWorkerOrder(order.workOrderId)
|
||||
message.success('抢单成功')
|
||||
await queryClient.invalidateQueries({ queryKey: ['worker-hall-orders'] })
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '抢单失败')
|
||||
} finally {
|
||||
setGrabbingId(0)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="worker-page-head">
|
||||
<Typography.Title level={3}>抢单大厅</Typography.Title>
|
||||
<Space.Compact>
|
||||
<Input
|
||||
allowClear
|
||||
value={keyword}
|
||||
placeholder="搜索商品或订单号"
|
||||
prefix={<SearchOutlined />}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
onPressEnter={() => void ordersQuery.refetch()}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} loading={ordersQuery.isFetching} onClick={() => ordersQuery.refetch()}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</div>
|
||||
|
||||
{ordersQuery.isLoading ? <Spin /> : null}
|
||||
{ordersQuery.error ? (
|
||||
<Card>
|
||||
<Typography.Text type="danger">
|
||||
{ordersQuery.error instanceof Error ? ordersQuery.error.message : '读取抢单大厅失败'}
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{!ordersQuery.isLoading && orders.length === 0 ? (
|
||||
<Empty description="暂无可抢订单" />
|
||||
) : (
|
||||
<div className="worker-order-grid">
|
||||
{orders.map((order) => (
|
||||
<Card key={order.workOrderId} className="worker-order-card" bordered={false}>
|
||||
<Space direction="vertical" className="full-width" size={8}>
|
||||
<Space className="worker-order-card-head">
|
||||
<Typography.Text strong ellipsis>
|
||||
{order.productName}
|
||||
</Typography.Text>
|
||||
<Tag color="blue">{order.categoryName || '默认分类'}</Tag>
|
||||
</Space>
|
||||
<Typography.Text type="secondary">订单号:{order.platformOrderId}</Typography.Text>
|
||||
<Typography.Title level={4} className="worker-order-money">
|
||||
{formatMoney(order.rewardAmount)}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
所需冻结押金:{formatMoney(order.freezeDepositAmount ?? order.requiredDepositAmount)}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={grabbingId === order.workOrderId}
|
||||
onClick={() => submitGrab(order)}
|
||||
>
|
||||
立即抢单
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function formatMoney(value: number | undefined) {
|
||||
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { LockOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { App, Button, Card, Form, Input, Tabs, Typography } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { Navigate, useNavigate } from 'react-router'
|
||||
|
||||
import { loginWorker, registerWorker } from '@/services/worker'
|
||||
import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth'
|
||||
|
||||
export default function WorkerLoginPage() {
|
||||
const navigate = useNavigate()
|
||||
const { message } = App.useApp()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
if (hasWorkerSession()) {
|
||||
return <Navigate to="/worker/hall" replace />
|
||||
}
|
||||
|
||||
async function submitLogin(values: { username: string; password: string }) {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await loginWorker(values)
|
||||
setWorkerSession(response.data.token, response.data.expiresAt, response.data.worker)
|
||||
message.success('登录成功')
|
||||
navigate('/worker/hall', { replace: true })
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '登录失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRegister(values: {
|
||||
username: string
|
||||
password: string
|
||||
displayName?: string
|
||||
phone?: string
|
||||
}) {
|
||||
setLoading(true)
|
||||
try {
|
||||
await registerWorker(values)
|
||||
message.success('注册成功,等待后台审核')
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '注册失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="worker-auth-page">
|
||||
<Card className="worker-auth-card" bordered={false}>
|
||||
<Typography.Title level={3}>接单平台</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
账号注册后需要后台审核,通过后可进入抢单大厅。
|
||||
</Typography.Paragraph>
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'login',
|
||||
label: '登录',
|
||||
children: (
|
||||
<Form layout="vertical" onFinish={submitLogin}>
|
||||
<Form.Item
|
||||
label="账号"
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入账号' }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} autoComplete="username" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="密码"
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} autoComplete="current-password" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
登录
|
||||
</Button>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'register',
|
||||
label: '注册',
|
||||
children: (
|
||||
<Form layout="vertical" onFinish={submitRegister}>
|
||||
<Form.Item
|
||||
label="账号"
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入账号' }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} autoComplete="username" />
|
||||
</Form.Item>
|
||||
<Form.Item label="昵称" name="displayName">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="手机号" name="phone">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="密码"
|
||||
name="password"
|
||||
rules={[{ required: true, min: 6, message: '密码至少 6 位' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} autoComplete="new-password" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
注册
|
||||
</Button>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { CheckOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { fetchWorkerMyOrders, submitWorkerAcceptance } from '@/services/worker'
|
||||
import type { WorkOrder } from '@/types/worker-platform'
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'in_progress', label: '代练中' },
|
||||
{ value: 'pending_acceptance', label: '待验收' },
|
||||
{ value: 'problem', label: '问题单' },
|
||||
{ value: 'accepted', label: '已验收' },
|
||||
]
|
||||
|
||||
export default function WorkerOrdersPage() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [status, setStatus] = useState('')
|
||||
const [submittingOrder, setSubmittingOrder] = useState<WorkOrder | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const ordersQuery = useQuery({
|
||||
queryKey: ['worker-my-orders', status],
|
||||
queryFn: () => fetchWorkerMyOrders({ status }),
|
||||
retry: false,
|
||||
})
|
||||
|
||||
async function submitAcceptance(values: { note?: string; imageUrls?: string }) {
|
||||
if (!submittingOrder) return
|
||||
try {
|
||||
await submitWorkerAcceptance(submittingOrder.workOrderId, {
|
||||
note: values.note,
|
||||
imageUrls: String(values.imageUrls || '')
|
||||
.split('\n')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
})
|
||||
message.success('已提交验收')
|
||||
setSubmittingOrder(null)
|
||||
form.resetFields()
|
||||
await queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] })
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '提交失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<WorkOrder> = [
|
||||
{
|
||||
title: '订单',
|
||||
minWidth: 280,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong>{row.productName}</Typography.Text>
|
||||
<Typography.Text type="secondary">{row.platformOrderId}</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
width: 130,
|
||||
render: (_, row) => <Typography.Text strong>{formatMoney(row.rewardAmount)}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 140,
|
||||
render: (_, row) => <Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '问题备注',
|
||||
dataIndex: 'problemNote',
|
||||
minWidth: 180,
|
||||
render: (value) => String(value || '-'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 140,
|
||||
render: (_, row) =>
|
||||
['in_progress', 'problem'].includes(row.status) ? (
|
||||
<Button icon={<CheckOutlined />} onClick={() => setSubmittingOrder(row)}>
|
||||
提交验收
|
||||
</Button>
|
||||
) : null,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="worker-page-head">
|
||||
<Typography.Title level={3}>我的订单</Typography.Title>
|
||||
<Space>
|
||||
<Select value={status} options={STATUS_OPTIONS} style={{ width: 140 }} onChange={setStatus} />
|
||||
<Button icon={<ReloadOutlined />} loading={ordersQuery.isFetching} onClick={() => ordersQuery.refetch()}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table<WorkOrder>
|
||||
rowKey="workOrderId"
|
||||
loading={ordersQuery.isLoading}
|
||||
dataSource={ordersQuery.data?.data.items || []}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="提交验收"
|
||||
open={Boolean(submittingOrder)}
|
||||
onCancel={() => setSubmittingOrder(null)}
|
||||
onOk={() => form.submit()}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={submitAcceptance}>
|
||||
<Form.Item label="完成说明" name="note">
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="图片链接"
|
||||
name="imageUrls"
|
||||
extra="一期先支持粘贴图片链接,每行一个;后续再接真实上传。"
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="https://..." />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function formatMoney(value: number | undefined) {
|
||||
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
function formatStatus(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
in_progress: '代练中',
|
||||
pending_acceptance: '待验收',
|
||||
problem: '问题单',
|
||||
accepted: '已验收',
|
||||
}
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
function resolveStatusColor(status: string) {
|
||||
if (status === 'accepted') return 'green'
|
||||
if (status === 'problem') return 'red'
|
||||
if (status === 'pending_acceptance') return 'gold'
|
||||
return 'blue'
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Button, Card, Descriptions, Empty, Space, Statistic, Typography } from 'antd'
|
||||
|
||||
import { fetchWorkerProfile } from '@/services/worker'
|
||||
|
||||
export default function WorkerProfilePage() {
|
||||
const profileQuery = useQuery({
|
||||
queryKey: ['worker-profile'],
|
||||
queryFn: () => fetchWorkerProfile(),
|
||||
retry: false,
|
||||
})
|
||||
|
||||
const worker = profileQuery.data?.data.worker
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="worker-page-head">
|
||||
<Typography.Title level={3}>个人中心</Typography.Title>
|
||||
<Button icon={<ReloadOutlined />} loading={profileQuery.isFetching} onClick={() => profileQuery.refetch()}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!worker && !profileQuery.isLoading ? <Empty description="暂无接单员信息" /> : null}
|
||||
|
||||
{worker ? (
|
||||
<>
|
||||
<Card className="worker-profile-card" bordered={false}>
|
||||
<Space size={32} wrap>
|
||||
<Statistic title="账户余额" value={formatMoney(worker.wallet.availableAmount)} />
|
||||
<Statistic title="冻结押金" value={formatMoney(worker.wallet.frozenDepositAmount)} />
|
||||
<Statistic title="当前等级" value={worker.level?.name || '-'} />
|
||||
<Statistic title="审核状态" value={formatStatus(worker.status)} />
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="账号信息" bordered={false}>
|
||||
<Descriptions column={2}>
|
||||
<Descriptions.Item label="账号">{worker.username}</Descriptions.Item>
|
||||
<Descriptions.Item label="昵称">{worker.displayName}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">{worker.phone || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="免押额度">
|
||||
{formatMoney(worker.level?.permissions.depositFreeAmount || 0)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最大同时接单">
|
||||
{worker.level?.permissions.maxActiveOrders || 0} 单
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="审核备注">{worker.reviewNote || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function formatMoney(value: number | undefined) {
|
||||
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
function formatStatus(status: string) {
|
||||
if (status === 'active') return '已审核'
|
||||
if (status === 'pending_review') return '待审核'
|
||||
if (status === 'rejected') return '审核未通过'
|
||||
if (status === 'disabled') return '已停用'
|
||||
return status || '-'
|
||||
}
|
||||
Reference in New Issue
Block a user