feat: 打手分内部/外部,内部免审核;支持后台指派打手
- worker_users 新增 worker_type(internal/external,迁移 017) - 内部打手提交验收后自动通过进入待打款(accepted+结算),外部打手保持后台审核 - 打手管理页支持设为内部/外部并显示类型标签 - 新增后台指派打手:未分配/待抢单工单可指定已注册打手,进入其我的订单并冻结押金 - 指派带审计与事件记录,校验打手状态/接单上限/余额押金
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
SearchOutlined,
|
||||
SendOutlined,
|
||||
TeamOutlined,
|
||||
UserAddOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
@@ -42,6 +43,7 @@ import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import {
|
||||
acceptAdminWorkOrder,
|
||||
assignAdminWorkOrderToWorker,
|
||||
createAdminMockWorkOrder,
|
||||
creditAdminWorkerWallet,
|
||||
deductAdminWorkOrderPendingDeposit,
|
||||
@@ -125,6 +127,8 @@ export default function WorkOrdersPanel() {
|
||||
const [materialOrder, setMaterialOrder] = useState<WorkOrder | null>(null)
|
||||
const [materialScreenshots, setMaterialScreenshots] = useState<UploadedFile[]>([])
|
||||
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
||||
const [assignOrder, setAssignOrder] = useState<WorkOrder | null>(null)
|
||||
const [assignWorkerId, setAssignWorkerId] = useState<number | undefined>(undefined)
|
||||
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
|
||||
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
|
||||
const [problemForm] = Form.useForm()
|
||||
@@ -162,6 +166,30 @@ export default function WorkOrdersPanel() {
|
||||
queryFn: () => fetchAdminWorkOrderSharing(Number(sharingOrder?.workOrderId)),
|
||||
enabled: Boolean(sharingOrder),
|
||||
})
|
||||
const assignWorkersQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-workers', 'active', 1, 200],
|
||||
queryFn: () => fetchAdminWorkerUsers({ status: 'active', page: 1, pageSize: 200 }),
|
||||
enabled: Boolean(assignOrder),
|
||||
})
|
||||
|
||||
function openAssignModal(row: WorkOrder) {
|
||||
setAssignOrder(row)
|
||||
setAssignWorkerId(undefined)
|
||||
}
|
||||
|
||||
async function submitAssign() {
|
||||
if (!assignOrder || !assignWorkerId) return
|
||||
const succeeded = await runAction(
|
||||
() =>
|
||||
assignAdminWorkOrderToWorker(assignOrder.workOrderId, {
|
||||
workerId: assignWorkerId,
|
||||
}),
|
||||
'已指派打手',
|
||||
)
|
||||
if (!succeeded) return
|
||||
setAssignOrder(null)
|
||||
setAssignWorkerId(undefined)
|
||||
}
|
||||
|
||||
function openSharingConfigModal(row: WorkOrder) {
|
||||
setSharingOrder(row)
|
||||
@@ -551,6 +579,11 @@ export default function WorkOrdersPanel() {
|
||||
拼单配置
|
||||
</Button>
|
||||
) : null}
|
||||
{['unassigned', 'open'].includes(row.status) ? (
|
||||
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
|
||||
指派
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status === 'pending_material' ? (
|
||||
<Button
|
||||
icon={<FormOutlined />}
|
||||
@@ -1142,6 +1175,41 @@ export default function WorkOrdersPanel() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`指派打手 · ${assignOrder?.productName || ''}`}
|
||||
open={Boolean(assignOrder)}
|
||||
onCancel={() => {
|
||||
setAssignOrder(null)
|
||||
setAssignWorkerId(undefined)
|
||||
}}
|
||||
onOk={() => submitAssign()}
|
||||
okText="确认指派"
|
||||
okButtonProps={{ disabled: !assignWorkerId }}
|
||||
destroyOnHidden
|
||||
width={520}
|
||||
>
|
||||
<Space direction="vertical" size={12} className="full-width">
|
||||
<Typography.Paragraph type="secondary">
|
||||
指派后订单直接进入该打手的「我的订单」,不再出现在抢单大厅;所需押金按订单规则从打手余额冻结。
|
||||
</Typography.Paragraph>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择要指派的打手"
|
||||
loading={assignWorkersQuery.isLoading}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
value={assignWorkerId}
|
||||
onChange={setAssignWorkerId}
|
||||
options={(assignWorkersQuery.data?.data.items || []).map((worker) => ({
|
||||
value: worker.workerId,
|
||||
label: `${worker.displayName || worker.username}(${worker.username})${
|
||||
worker.workerType === 'internal' ? '· 内部' : ''
|
||||
}`,
|
||||
}))}
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`拼单配置 · ${sharingOrder?.productName || ''}`}
|
||||
open={Boolean(sharingOrder)}
|
||||
|
||||
@@ -84,7 +84,7 @@ import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { formatMoney, formatWorkerStatus } from './shared'
|
||||
|
||||
export default function WorkersPanel() {
|
||||
const { message } = App.useApp()
|
||||
const { message, modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [status, setStatus] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
@@ -125,6 +125,30 @@ export default function WorkersPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleWorkerType(worker: WorkerUser, workerType: 'internal' | 'external') {
|
||||
const isInternal = workerType === 'internal'
|
||||
modal.confirm({
|
||||
title: isInternal ? '设为内部打手?' : '设为外部打手?',
|
||||
content: isInternal
|
||||
? '内部打手提交验收后将免审核,直接进入待打款;请确认该打手可信。'
|
||||
: '外部打手提交验收后需后台审核通过,才进入待打款。',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await reviewAdminWorkerUser(worker.workerId, {
|
||||
status: worker.status,
|
||||
workerType,
|
||||
})
|
||||
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 {
|
||||
@@ -164,9 +188,14 @@ export default function WorkersPanel() {
|
||||
minWidth: 220,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong>
|
||||
{row.displayName || row.username}
|
||||
</Typography.Text>
|
||||
<Space size={6} wrap>
|
||||
<Typography.Text strong>
|
||||
{row.displayName || row.username}
|
||||
</Typography.Text>
|
||||
<Tag color={row.workerType === 'internal' ? 'blue' : 'default'}>
|
||||
{row.workerType === 'internal' ? '内部' : '外部'}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Typography.Text type="secondary">{row.username}</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
@@ -227,6 +256,11 @@ export default function WorkersPanel() {
|
||||
{row.status !== 'rejected' ? (
|
||||
<Button onClick={() => review(row, 'rejected')}>拒绝</Button>
|
||||
) : null}
|
||||
{row.workerType !== 'internal' ? (
|
||||
<Button onClick={() => toggleWorkerType(row, 'internal')}>设为内部</Button>
|
||||
) : (
|
||||
<Button onClick={() => toggleWorkerType(row, 'external')}>设为外部</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLevelWorker(row)
|
||||
|
||||
@@ -456,6 +456,9 @@ export default function WorkerProfilePage() {
|
||||
<Tag color={resolveWorkerStatusColor(worker.status)}>
|
||||
{formatWorkerStatus(worker.status)}
|
||||
</Tag>
|
||||
<Tag color={worker.workerType === 'internal' ? 'blue' : 'default'}>
|
||||
{worker.workerType === 'internal' ? '内部打手' : '外部打手'}
|
||||
</Tag>
|
||||
<Tag>{worker.level?.name || '未分级'}</Tag>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
@@ -124,7 +124,7 @@ export function deleteAdminWorkProductRule(ruleId: number) {
|
||||
|
||||
export function reviewAdminWorkerUser(
|
||||
workerId: number,
|
||||
payload: { status: string; reviewNote?: string; levelId?: number },
|
||||
payload: { status: string; reviewNote?: string; levelId?: number; workerType?: string },
|
||||
) {
|
||||
return apiPost<{ worker: WorkerUser }>(
|
||||
`/api/v1/admin/worker-platform/workers/${workerId}/review`,
|
||||
@@ -132,6 +132,16 @@ export function reviewAdminWorkerUser(
|
||||
)
|
||||
}
|
||||
|
||||
export function assignAdminWorkOrderToWorker(
|
||||
workOrderId: number,
|
||||
payload: { workerId: number },
|
||||
) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/assign`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function creditAdminWorkerWallet(
|
||||
workerId: number,
|
||||
payload: { amount: number; note?: string },
|
||||
|
||||
@@ -4,6 +4,7 @@ export type WorkerUser = {
|
||||
displayName: string
|
||||
phone: string
|
||||
status: string
|
||||
workerType: string
|
||||
reviewNote: string
|
||||
inviteCode: string
|
||||
inviter: null | {
|
||||
|
||||
Reference in New Issue
Block a user