完善接单平台上传与物品规则

This commit is contained in:
yml2213
2026-07-25 15:56:18 +08:00
parent f6d3f00f21
commit a2587eda01
40 changed files with 4978 additions and 1008 deletions
@@ -1,8 +1,10 @@
import {
CheckOutlined,
FormOutlined,
PlusOutlined,
ReloadOutlined,
SendOutlined,
SyncOutlined,
WarningOutlined,
} from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
@@ -26,11 +28,14 @@ import {
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import PageHeader from '@/components/admin/PageHeader'
import {
acceptAdminWorkOrder,
createAdminMockWorkOrder,
creditAdminWorkerWallet,
fetchAdminWorkCategories,
fetchAdminWorkProductRules,
fetchAdminWorkerLevels,
fetchAdminWorkerPlatformSummary,
fetchAdminWorkerUsers,
@@ -38,19 +43,36 @@ import {
markAdminWorkOrderProblem,
publishAdminWorkOrder,
reviewAdminWorkerUser,
saveAdminWorkCategory,
saveAdminWorkProductRule,
saveAdminWorkerLevel,
submitAdminWorkOrderMaterial,
syncAdminWorkerOrdersFromSource,
} from '@/services/admin'
import type { WorkOrder, WorkerLevel, WorkerUser } from '@/types/worker-platform'
import type {
CollectField,
UploadedFile,
WorkCategory,
WorkOrder,
WorkProductRule,
WorkerLevel,
WorkerUser,
} from '@/types/worker-platform'
export default function AdminWorkerPlatformPage() {
return (
<section className="page-stack">
<PageHeader title="接单平台" description="管理接单员、接单工单、等级权限和本地 Mock 工单。" />
<PageHeader
title="接单平台"
description="管理接单员、接单工单、等级权限和本地 Mock 工单。"
/>
<SummaryCards />
<Tabs
destroyOnHidden={false}
items={[
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
{ key: 'rules', label: '物品规则', children: <ProductRulesPanel /> },
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
{ key: 'workers', label: '接单员', children: <WorkersPanel /> },
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
]}
@@ -69,10 +91,16 @@ function SummaryCards() {
return (
<div className="metric-grid four">
<Card>
<Statistic title="待审核接单员" value={summary?.pendingWorkerCount || 0} />
<Statistic
title="待审核接单员"
value={summary?.pendingWorkerCount || 0}
/>
</Card>
<Card>
<Statistic title="待完善订单" value={summary?.pendingMaterialCount || 0} />
<Statistic
title="待完善订单"
value={summary?.pendingMaterialCount || 0}
/>
</Card>
<Card>
<Statistic title="大厅订单" value={summary?.openOrderCount || 0} />
@@ -89,7 +117,9 @@ function WorkOrdersPanel() {
const queryClient = useQueryClient()
const [status, setStatus] = useState('')
const [problemOrder, setProblemOrder] = useState<WorkOrder | null>(null)
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
const [problemForm] = Form.useForm()
const [materialForm] = Form.useForm()
const ordersQuery = useQuery({
queryKey: ['admin-worker-platform-orders', status],
@@ -98,12 +128,19 @@ function WorkOrdersPanel() {
async function refreshAll() {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] }),
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }),
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-orders'],
}),
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-summary'],
}),
])
}
async function runAction(action: () => Promise<unknown>, successMessage: string) {
async function runAction(
action: () => Promise<unknown>,
successMessage: string,
) {
try {
await action()
message.success(successMessage)
@@ -116,13 +153,47 @@ function WorkOrdersPanel() {
async function submitProblem(values: { note?: string }) {
if (!problemOrder) return
await runAction(
() => markAdminWorkOrderProblem(problemOrder.workOrderId, values.note || ''),
() =>
markAdminWorkOrderProblem(problemOrder.workOrderId, values.note || ''),
'已标记问题单',
)
setProblemOrder(null)
problemForm.resetFields()
}
async function submitMaterial(values: { fields?: Record<string, string> }) {
if (!materialOrder) return
await runAction(
() =>
submitAdminWorkOrderMaterial(
materialOrder.workOrderId,
values.fields || {},
),
'订单资料已保存',
)
setMaterialOrder(null)
materialForm.resetFields()
}
async function submitSourceSync(values: { platformOrderId?: string }) {
try {
const result = await syncAdminWorkerOrdersFromSource({
platformOrderId: String(values.platformOrderId || '').trim(),
})
message.success(
`同步完成,新增 ${result.data.createdCount} 个,跳过 ${result.data.skippedCount}`,
)
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '同步失败')
}
}
function openMaterialModal(row: WorkOrder) {
setMaterialOrder(row)
materialForm.setFieldsValue({ fields: getMaterialFields(row) })
}
const columns: TableColumnsType<WorkOrder> = [
{
title: '订单',
@@ -136,35 +207,93 @@ function WorkOrdersPanel() {
</div>
),
},
{ title: '金额', width: 110, render: (_, row) => formatMoney(row.rewardAmount) },
{ title: '押金', width: 110, render: (_, row) => formatMoney(row.requiredDepositAmount) },
{
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>,
render: (_, row) => (
<Tag color={resolveStatusColor(row.status)}>
{formatStatus(row.status)}
</Tag>
),
},
{
title: '接单员',
width: 140,
render: (_, row) => row.worker?.displayName || row.worker?.username || '-',
render: (_, row) =>
row.worker?.displayName || row.worker?.username || '-',
},
{
title: '资料',
width: 160,
render: (_, row) => renderMaterialSummary(row),
},
{
title: '验收图',
width: 170,
render: (_, row) => (
<ImagePreviewList
files={getAcceptanceFiles(row)}
imageUrls={getAcceptanceImageUrls(row)}
size={44}
/>
),
},
{
title: '操作',
width: 260,
width: 340,
render: (_, row) => (
<Space wrap>
{row.status === 'pending_material' ? (
<Button
icon={<FormOutlined />}
onClick={() => openMaterialModal(row)}
>
</Button>
) : null}
{row.status === 'unassigned' ? (
<Button icon={<SendOutlined />} onClick={() => runAction(() => publishAdminWorkOrder(row.workOrderId), '已发布到大厅')}>
<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
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
icon={<WarningOutlined />}
danger
onClick={() => setProblemOrder(row)}
>
</Button>
) : null}
@@ -175,6 +304,25 @@ function WorkOrdersPanel() {
return (
<section className="platform-panel-stack">
<Card title="从源订单同步" bordered={false}>
<Form layout="inline" onFinish={submitSourceSync}>
<Form.Item
label="平台订单号"
name="platformOrderId"
rules={[{ required: true, message: '请输入平台订单号' }]}
>
<Input
placeholder="用户填写链接 A 的订单号"
style={{ width: 260 }}
/>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SyncOutlined />} htmlType="submit">
</Button>
</Form.Item>
</Form>
</Card>
<Card title="生成本地接单工单 Mock" bordered={false}>
<MockWorkOrderForm onCreated={refreshAll} />
</Card>
@@ -197,7 +345,11 @@ function WorkOrdersPanel() {
]}
onChange={setStatus}
/>
<Button icon={<ReloadOutlined />} loading={ordersQuery.isFetching} onClick={() => ordersQuery.refetch()}>
<Button
icon={<ReloadOutlined />}
loading={ordersQuery.isFetching}
onClick={() => ordersQuery.refetch()}
>
</Button>
</Space>
@@ -222,11 +374,40 @@ function WorkOrdersPanel() {
destroyOnHidden
>
<Form form={problemForm} layout="vertical" onFinish={submitProblem}>
<Form.Item label="问题备注" name="note" rules={[{ required: true, message: '请填写问题备注' }]}>
<Form.Item
label="问题备注"
name="note"
rules={[{ required: true, message: '请填写问题备注' }]}
>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
<Modal
title="人工补充资料"
open={Boolean(materialOrder)}
onCancel={() => setMaterialOrder(null)}
onOk={() => materialForm.submit()}
destroyOnHidden
>
<Form form={materialForm} layout="vertical" onFinish={submitMaterial}>
{getRequirementFields(materialOrder).map((field) => (
<Form.Item
key={field.key}
label={field.label}
name={['fields', field.key]}
rules={
field.required
? [{ required: true, message: `请填写${field.label}` }]
: undefined
}
>
<Input />
</Form.Item>
))}
</Form>
</Modal>
</section>
)
}
@@ -256,7 +437,11 @@ function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void> }) {
return (
<Form
layout="inline"
initialValues={{ productName: '指挥官秘钥1个', rewardAmount: 25, materialComplete: false }}
initialValues={{
productName: '指挥官秘钥1个',
rewardAmount: 25,
materialComplete: false,
}}
onFinish={submit}
>
<Form.Item label="订单号" name="platformOrderId">
@@ -268,11 +453,20 @@ function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void> }) {
<Form.Item label="接单金额" name="rewardAmount">
<InputNumber min={0.01} step={1} addonAfter="元" />
</Form.Item>
<Form.Item label="资料完整" name="materialComplete" valuePropName="checked">
<Form.Item
label="资料完整"
name="materialComplete"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item>
<Button type="primary" icon={<PlusOutlined />} htmlType="submit" loading={loading}>
<Button
type="primary"
icon={<PlusOutlined />}
htmlType="submit"
loading={loading}
>
</Button>
</Form.Item>
@@ -280,6 +474,332 @@ function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void> }) {
)
}
function ProductRulesPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [form] = Form.useForm()
const rulesQuery = useQuery({
queryKey: ['admin-worker-platform-product-rules'],
queryFn: () => fetchAdminWorkProductRules(),
})
const categoriesQuery = useQuery({
queryKey: ['admin-worker-platform-categories'],
queryFn: () => fetchAdminWorkCategories(),
})
async function saveRule(values: {
ruleKey: string
provider?: string
platform?: string
shopId?: string
skuCode?: string
productName?: string
matchType?: string
categoryId?: number
rewardAmount?: number
requiredDepositAmount?: number
depositThresholdAmount?: number
fieldsText?: string
enabled?: boolean
autoCreate?: boolean
}) {
try {
await saveAdminWorkProductRule(values)
message.success('物品规则已保存')
form.resetFields()
await Promise.all([
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-product-rules'],
}),
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-summary'],
}),
])
} catch (error) {
message.error(error instanceof Error ? error.message : '保存失败')
}
}
const columns: TableColumnsType<WorkProductRule> = [
{
title: '规则',
minWidth: 260,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong>
{row.productName || row.skuCode}
</Typography.Text>
<Typography.Text type="secondary">{row.ruleKey}</Typography.Text>
</div>
),
},
{
title: 'SKU',
dataIndex: 'skuCode',
width: 180,
render: (value) => String(value || '-'),
},
{ title: '分类', width: 140, render: (_, row) => row.categoryName || '-' },
{
title: '接单金额',
width: 120,
render: (_, row) => formatMoney(row.rewardAmount),
},
{
title: '押金',
width: 120,
render: (_, row) => formatMoney(row.requiredDepositAmount),
},
{
title: '匹配',
width: 130,
render: (_, row) => (row.matchType === 'exact' ? '精确' : '包含'),
},
{
title: '状态',
width: 150,
render: (_, row) => (
<Space>
<Tag color={row.enabled ? 'green' : 'default'}>
{row.enabled ? '启用' : '停用'}
</Tag>
{row.autoCreate ? <Tag color="blue"></Tag> : null}
</Space>
),
},
]
return (
<section className="platform-panel-stack">
<Card title="保存物品规则" bordered={false}>
<Form
form={form}
layout="vertical"
className="worker-rule-form"
initialValues={{
platform: 'kuaishou',
matchType: 'contains',
rewardAmount: 25,
depositThresholdAmount: 200,
enabled: true,
autoCreate: false,
fieldsText:
'gameAccount:游戏账号\nserverName:区服\nroleName:角色名',
}}
onFinish={saveRule}
>
<div className="worker-rule-grid">
<Form.Item
label="规则标识"
name="ruleKey"
rules={[{ required: true, message: '请输入规则标识' }]}
>
<Input placeholder="skin-training" />
</Form.Item>
<Form.Item label="来源" name="provider">
<Input placeholder="留空通配,如 91kaquan" />
</Form.Item>
<Form.Item label="平台" name="platform">
<Input placeholder="kuaishou" />
</Form.Item>
<Form.Item label="店铺" name="shopId">
<Input placeholder="留空通配" />
</Form.Item>
<Form.Item label="SKU" name="skuCode">
<Input placeholder="优先精确匹配" />
</Form.Item>
<Form.Item label="商品名" name="productName">
<Input placeholder="用于包含/精确匹配" />
</Form.Item>
<Form.Item label="匹配方式" name="matchType">
<Select
options={[
{ value: 'contains', label: '包含' },
{ value: 'exact', label: '精确' },
]}
/>
</Form.Item>
<Form.Item label="分类" name="categoryId">
<Select
allowClear
loading={categoriesQuery.isLoading}
options={(categoriesQuery.data?.data.items || []).map(
(item) => ({
value: item.categoryId,
label: item.name,
}),
)}
/>
</Form.Item>
<Form.Item label="接单金额" name="rewardAmount">
<InputNumber
min={0.01}
step={1}
addonAfter="元"
className="full-width"
/>
</Form.Item>
<Form.Item label="押金阈值" name="depositThresholdAmount">
<InputNumber
min={0}
step={10}
addonAfter="元"
className="full-width"
/>
</Form.Item>
<Form.Item label="固定押金" name="requiredDepositAmount">
<InputNumber
min={0}
step={10}
addonAfter="元"
className="full-width"
/>
</Form.Item>
<Form.Item label="排序" name="sortOrder">
<InputNumber min={0} max={9999} className="full-width" />
</Form.Item>
<Form.Item label="启用" name="enabled" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item
label="自动创建"
name="autoCreate"
valuePropName="checked"
>
<Switch />
</Form.Item>
</div>
<Form.Item label="资料字段" name="fieldsText">
<Input.TextArea rows={4} />
</Form.Item>
<Button type="primary" htmlType="submit">
</Button>
</Form>
</Card>
<Card
title="物品规则"
extra={
<Button
icon={<ReloadOutlined />}
loading={rulesQuery.isFetching}
onClick={() => rulesQuery.refetch()}
>
</Button>
}
bordered={false}
>
<Table<WorkProductRule>
rowKey="ruleId"
loading={rulesQuery.isLoading}
dataSource={rulesQuery.data?.data.items || []}
columns={columns}
pagination={false}
scroll={{ x: 1200 }}
/>
</Card>
</section>
)
}
function CategoriesPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [form] = Form.useForm()
const categoriesQuery = useQuery({
queryKey: ['admin-worker-platform-categories'],
queryFn: () => fetchAdminWorkCategories(),
})
async function saveCategory(values: {
categoryKey: string
name: string
sortOrder?: number
status?: string
}) {
try {
await saveAdminWorkCategory(values)
message.success('分类已保存')
form.resetFields()
await queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-categories'],
})
} catch (error) {
message.error(error instanceof Error ? error.message : '保存失败')
}
}
const columns: TableColumnsType<WorkCategory> = [
{ title: '分类', dataIndex: 'name' },
{ title: '标识', dataIndex: 'categoryKey' },
{ title: '排序', dataIndex: 'sortOrder', width: 100 },
{
title: '状态',
width: 120,
render: (_, row) => (
<Tag color={row.status === 'active' ? 'green' : 'default'}>
{row.status === 'active' ? '启用' : '停用'}
</Tag>
),
},
]
return (
<section className="platform-panel-stack">
<Card title="保存分类" bordered={false}>
<Form
form={form}
layout="inline"
initialValues={{ status: 'active', sortOrder: 100 }}
onFinish={saveCategory}
>
<Form.Item
label="标识"
name="categoryKey"
rules={[{ required: true, message: '请输入标识' }]}
>
<Input />
</Form.Item>
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input />
</Form.Item>
<Form.Item label="排序" name="sortOrder">
<InputNumber min={0} max={9999} />
</Form.Item>
<Form.Item label="状态" name="status">
<Select
style={{ width: 110 }}
options={[
{ value: 'active', label: '启用' },
{ value: 'disabled', label: '停用' },
]}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
</Card>
<Card title="分类列表" bordered={false}>
<Table<WorkCategory>
rowKey="categoryId"
loading={categoriesQuery.isLoading}
dataSource={categoriesQuery.data?.data.items || []}
columns={columns}
pagination={false}
/>
</Card>
</section>
)
}
function WorkersPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
@@ -294,8 +814,12 @@ function WorkersPanel() {
async function refreshWorkers() {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-workers'] }),
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }),
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-workers'],
}),
queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-summary'],
}),
])
}
@@ -331,26 +855,44 @@ function WorkersPanel() {
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong>{row.displayName || row.username}</Typography.Text>
<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: 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>,
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}
{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>
),
@@ -374,7 +916,11 @@ function WorkersPanel() {
]}
onChange={setStatus}
/>
<Button icon={<ReloadOutlined />} loading={workersQuery.isFetching} onClick={() => workersQuery.refetch()}>
<Button
icon={<ReloadOutlined />}
loading={workersQuery.isFetching}
onClick={() => workersQuery.refetch()}
>
</Button>
</Space>
@@ -398,8 +944,17 @@ function WorkersPanel() {
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
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 />
@@ -419,11 +974,18 @@ function LevelsPanel() {
})
const [form] = Form.useForm()
async function saveLevel(values: { levelKey: string; name: string; depositFreeAmount?: number; maxActiveOrders?: number }) {
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'] })
await queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-levels'],
})
} catch (error) {
message.error(error instanceof Error ? error.message : '保存失败')
}
@@ -432,7 +994,10 @@ function LevelsPanel() {
const columns: TableColumnsType<WorkerLevel> = [
{ title: '等级', dataIndex: 'name' },
{ title: '标识', dataIndex: 'levelKey' },
{ title: '免押额度', render: (_, row) => formatMoney(row.depositFreeAmount) },
{
title: '免押额度',
render: (_, row) => formatMoney(row.depositFreeAmount),
},
{ title: '最大同时接单', dataIndex: 'maxActiveOrders' },
{ title: '状态', dataIndex: 'status' },
]
@@ -443,13 +1008,26 @@ function LevelsPanel() {
<Form
form={form}
layout="inline"
initialValues={{ levelKey: 'regular', name: '普通接单员', depositFreeAmount: 0, maxActiveOrders: 3 }}
initialValues={{
levelKey: 'regular',
name: '普通接单员',
depositFreeAmount: 0,
maxActiveOrders: 3,
}}
onFinish={saveLevel}
>
<Form.Item label="标识" name="levelKey" rules={[{ required: true, message: '请输入标识' }]}>
<Form.Item
label="标识"
name="levelKey"
rules={[{ required: true, message: '请输入标识' }]}
>
<Input />
</Form.Item>
<Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input />
</Form.Item>
<Form.Item label="免押额度" name="depositFreeAmount">
@@ -459,7 +1037,9 @@ function LevelsPanel() {
<InputNumber min={1} max={999} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit"></Button>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
</Card>
@@ -476,6 +1056,77 @@ function LevelsPanel() {
)
}
function getRequirementFields(order: WorkOrder | null): CollectField[] {
if (!order) return []
const rawFields = Array.isArray(order.requirement?.fields)
? order.requirement.fields
: []
return rawFields
.map((item) => {
const source = asRecord(item)
const key = String(source.key || '').trim()
if (!key) return null
return {
key,
label: String(source.label || key).trim(),
required: source.required !== false,
}
})
.filter((item): item is CollectField => Boolean(item))
}
function getMaterialFields(order: WorkOrder): Record<string, string> {
const material = asRecord(order.material)
const collect = asRecord(material.collect)
const fields = asRecord(collect.fields || material.fields)
return Object.fromEntries(
Object.entries(fields).map(([key, value]) => [key, String(value || '')]),
)
}
function renderMaterialSummary(order: WorkOrder) {
const fields = getMaterialFields(order)
const entries = Object.entries(fields).filter(([, value]) =>
String(value || '').trim(),
)
if (entries.length === 0) {
return <Typography.Text type="secondary">-</Typography.Text>
}
return (
<div className="cell-stack">
{entries.slice(0, 2).map(([key, value]) => (
<Typography.Text key={key} ellipsis>
{key}: {value}
</Typography.Text>
))}
{entries.length > 2 ? (
<Typography.Text type="secondary">
{entries.length - 2}
</Typography.Text>
) : null}
</div>
)
}
function getAcceptanceFiles(row: WorkOrder): UploadedFile[] {
return Array.isArray(row.acceptance?.files) ? row.acceptance.files : []
}
function getAcceptanceImageUrls(row: WorkOrder): string[] {
return Array.isArray(row.acceptance?.imageUrls)
? row.acceptance.imageUrls
.map((item) => String(item || '').trim())
.filter(Boolean)
: []
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
}
function formatMoney(value: number | undefined) {
return `${((Number(value || 0) || 0) / 100).toFixed(2)}`
}
@@ -15,8 +15,10 @@ import {
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import ImageUpload from '@/components/files/ImageUpload'
import { fetchWorkerMyOrders, submitWorkerAcceptance } from '@/services/worker'
import type { WorkOrder } from '@/types/worker-platform'
import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
const STATUS_OPTIONS = [
{ value: '', label: '全部' },
@@ -31,6 +33,7 @@ export default function WorkerOrdersPage() {
const queryClient = useQueryClient()
const [status, setStatus] = useState('')
const [submittingOrder, setSubmittingOrder] = useState<WorkOrder | null>(null)
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
const [form] = Form.useForm()
const ordersQuery = useQuery({
@@ -39,18 +42,20 @@ export default function WorkerOrdersPage() {
retry: false,
})
async function submitAcceptance(values: { note?: string; imageUrls?: string }) {
async function submitAcceptance(values: { note?: string }) {
if (!submittingOrder) return
if (acceptanceFiles.length === 0) {
message.error('请上传验收图片')
return
}
try {
await submitWorkerAcceptance(submittingOrder.workOrderId, {
note: values.note,
imageUrls: String(values.imageUrls || '')
.split('\n')
.map((item) => item.trim())
.filter(Boolean),
files: acceptanceFiles,
})
message.success('已提交验收')
setSubmittingOrder(null)
setAcceptanceFiles([])
form.resetFields()
await queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] })
} catch (error) {
@@ -65,19 +70,40 @@ export default function WorkerOrdersPage() {
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong>{row.productName}</Typography.Text>
<Typography.Text type="secondary">{row.platformOrderId}</Typography.Text>
<Typography.Text type="secondary">
{row.platformOrderId}
</Typography.Text>
</div>
),
},
{
title: '金额',
width: 130,
render: (_, row) => <Typography.Text strong>{formatMoney(row.rewardAmount)}</Typography.Text>,
render: (_, row) => (
<Typography.Text strong>
{formatMoney(row.rewardAmount)}
</Typography.Text>
),
},
{
title: '状态',
width: 140,
render: (_, row) => <Tag color={resolveStatusColor(row.status)}>{formatStatus(row.status)}</Tag>,
render: (_, row) => (
<Tag color={resolveStatusColor(row.status)}>
{formatStatus(row.status)}
</Tag>
),
},
{
title: '验收图片',
width: 170,
render: (_, row) => (
<ImagePreviewList
files={getAcceptanceFiles(row)}
imageUrls={getAcceptanceImageUrls(row)}
size={46}
/>
),
},
{
title: '问题备注',
@@ -90,7 +116,13 @@ export default function WorkerOrdersPage() {
width: 140,
render: (_, row) =>
['in_progress', 'problem'].includes(row.status) ? (
<Button icon={<CheckOutlined />} onClick={() => setSubmittingOrder(row)}>
<Button
icon={<CheckOutlined />}
onClick={() => {
setSubmittingOrder(row)
setAcceptanceFiles([])
}}
>
</Button>
) : null,
@@ -102,8 +134,17 @@ export default function WorkerOrdersPage() {
<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()}>
<Select
value={status}
options={STATUS_OPTIONS}
style={{ width: 140 }}
onChange={setStatus}
/>
<Button
icon={<ReloadOutlined />}
loading={ordersQuery.isFetching}
onClick={() => ordersQuery.refetch()}
>
</Button>
</Space>
@@ -121,7 +162,10 @@ export default function WorkerOrdersPage() {
<Modal
title="提交验收"
open={Boolean(submittingOrder)}
onCancel={() => setSubmittingOrder(null)}
onCancel={() => {
setSubmittingOrder(null)
setAcceptanceFiles([])
}}
onOk={() => form.submit()}
destroyOnHidden
>
@@ -129,12 +173,14 @@ export default function WorkerOrdersPage() {
<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 label="验收图片" required>
<ImageUpload
scene="worker-acceptance"
scope="worker"
value={acceptanceFiles}
onChange={setAcceptanceFiles}
maxCount={6}
/>
</Form.Item>
</Form>
</Modal>
@@ -142,6 +188,18 @@ export default function WorkerOrdersPage() {
)
}
function getAcceptanceFiles(row: WorkOrder): UploadedFile[] {
return Array.isArray(row.acceptance?.files) ? row.acceptance.files : []
}
function getAcceptanceImageUrls(row: WorkOrder): string[] {
return Array.isArray(row.acceptance?.imageUrls)
? row.acceptance.imageUrls
.map((item) => String(item || '').trim())
.filter(Boolean)
: []
}
function formatMoney(value: number | undefined) {
return `${((Number(value || 0) || 0) / 100).toFixed(2)}`
}