feat(worker): support batch sharing cancellation

This commit is contained in:
yml2213
2026-08-28 19:40:49 +08:00
parent e189811617
commit d91d7180ad
13 changed files with 633 additions and 96 deletions
@@ -313,6 +313,22 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
color: 'red',
}
}
case 'sharing_unaccepted_shares_returned_to_hall_by_admin': {
const count = readPayload(event, 'cancelledShareCount')
return {
title: `批量撤回未验收拼单份额${count ? `${count} 个)` : ''}`,
description: note ? `原因:${note}` : '对应数量已重新开放到接单大厅',
color: 'orange',
}
}
case 'sharing_order_cancelled_by_admin': {
const count = readPayload(event, 'cancelledShareCount')
return {
title: `取消拼单订单${count ? `(已撤销 ${count} 个参与份额)` : ''}`,
description: note ? `原因:${note}` : undefined,
color: 'red',
}
}
case 'pending_deposit_deducted':
return { title: '扣减了待解冻押金', color: 'default' }
case 'voucher_backfilled':
@@ -49,6 +49,7 @@ import {
assignAdminWorkOrderToWorker,
cancelAdminWorkOrder,
cancelAdminWorkOrderShare,
cancelAdminWorkOrderSharing,
confirmAdminWorkOrderFriend,
deductAdminWorkOrderPendingDeposit,
deleteAdminWorkOrder,
@@ -146,6 +147,11 @@ type CancelShareFormValues = {
reason?: string
}
type CancelSharingFormValues = {
action?: 'return_to_hall' | 'cancel_order'
reason?: string
}
export default function WorkOrdersPanel() {
const { message, modal } = App.useApp()
const navigate = useNavigate()
@@ -221,6 +227,7 @@ export default function WorkOrdersPanel() {
const [materialPasteText, setMaterialPasteText] = useState('')
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
const [cancelShare, setCancelShare] = useState<WorkOrderShare | null>(null)
const [cancelSharingOrder, setCancelSharingOrder] = 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)
@@ -232,6 +239,7 @@ export default function WorkOrdersPanel() {
const [materialForm] = Form.useForm<MaterialFormValues>()
const [cancelForm] = Form.useForm<CancelOrderFormValues>()
const [cancelShareForm] = Form.useForm<CancelShareFormValues>()
const [cancelSharingForm] = Form.useForm<CancelSharingFormValues>()
const [deductForm] = Form.useForm<{ amount?: number; note?: string }>()
const [editForm] = Form.useForm<{
productName?: string
@@ -335,6 +343,11 @@ export default function WorkOrdersPanel() {
queryFn: () => fetchAdminWorkOrderSharing(Number(sharingOrder?.workOrderId)),
enabled: Boolean(sharingOrder),
})
const sharingShares = sharingQuery.data?.data.shares || []
const hasCancellableSharingShares = sharingShares.some((share) =>
['joined', 'submitted'].includes(share.status),
)
const hasAcceptedSharingShares = sharingShares.some((share) => share.status === 'accepted')
const assignWorkersQuery = useQuery({
queryKey: ['admin-worker-platform-workers', 'assign', assignWorkerKeyword],
queryFn: () =>
@@ -498,6 +511,28 @@ export default function WorkOrdersPanel() {
cancelShareForm.resetFields()
}
async function submitCancelSharing(values: CancelSharingFormValues) {
if (!cancelSharingOrder || !values.action) return
const action = values.action
const succeeded = await runAction(
() =>
cancelAdminWorkOrderSharing(cancelSharingOrder.workOrderId, {
action,
reason: String(values.reason || '').trim(),
}),
(result) =>
action === 'return_to_hall'
? `已撤回 ${result.data.cancelledShareCount} 个未验收份额并退回大厅`
: `拼单订单已取消,已撤销 ${result.data.cancelledShareCount} 个参与份额`,
)
if (!succeeded) return
await queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-order-sharing', cancelSharingOrder.workOrderId],
})
setCancelSharingOrder(null)
cancelSharingForm.resetFields()
}
async function refreshAll() {
await Promise.all([
queryClient.invalidateQueries({
@@ -1097,7 +1132,7 @@ export default function WorkOrdersPanel() {
(row.capabilities?.canConfigureSharing && canConfigureSharing) ? (
<Button icon={<TeamOutlined />} onClick={() => openSharingConfigModal(row)}>
{row.sharing?.enabled && !row.capabilities?.canConfigureSharing
? '拼单明细'
? '拼单管理'
: '拼单配置'}
</Button>
) : null}
@@ -2148,8 +2183,10 @@ export default function WorkOrdersPanel() {
onCancel={() => {
setSharingOrder(null)
setCancelShare(null)
setCancelSharingOrder(null)
sharingForm.resetFields()
cancelShareForm.resetFields()
cancelSharingForm.resetFields()
}}
onOk={() => sharingForm.submit()}
footer={
@@ -2237,114 +2274,152 @@ export default function WorkOrdersPanel() {
) : (sharingQuery.data?.data.shares || []).length === 0 ? (
<Typography.Text type="secondary"></Typography.Text>
) : (
<Table<WorkOrderShare>
rowKey="shareId"
size="small"
dataSource={sharingQuery.data?.data.shares || []}
loading={sharingQuery.isFetching}
pagination={false}
columns={[
{
title: '打手',
render: (_, share) =>
share.worker?.displayName || share.worker?.username || '-',
},
{ title: '数量', dataIndex: 'quantity' },
{
title: '份额报酬',
render: (_, share) => formatMoney(share.shareReward),
},
{
title: '状态',
render: (_, share) => (
<Tag color={resolveSharingShareStatusColor(share.status)}>
{formatSharingShareStatus(share.status)}
</Tag>
),
},
{
title: '提交时间',
render: (_, share) =>
share.submittedAt ? formatAdminDateTime(share.submittedAt) : '-',
},
{
title: '验收图',
width: 150,
render: (_, share) => (
<ImagePreviewList
files={
Array.isArray(share.acceptance?.files) ? share.acceptance.files : []
}
imageUrls={
Array.isArray(share.acceptance?.imageUrls)
? share.acceptance.imageUrls
: []
}
size={44}
maxVisible={2}
/>
),
},
{
title: '备注',
render: (_, share) => String(share.acceptance?.note || '').trim() || '-',
},
{
title: '操作',
width: 190,
render: (_, share) => {
if (share.status === 'accepted') return <Tag color="green"></Tag>
if (share.status === 'cancelled') return <Tag></Tag>
return (
<Space size={6}>
{share.status === 'submitted' ? (
<Button
type="primary"
size="small"
onClick={() => {
void runAction(
() =>
acceptAdminWorkOrderShare(
sharingOrder.workOrderId,
share.shareId,
),
'该拼单参与者已验收',
).then((succeeded) => {
if (succeeded) {
void queryClient.invalidateQueries({
queryKey: [
'admin-worker-platform-order-sharing',
<>
{sharingOrder.capabilities?.canCancelSharing &&
hasCancellableSharingShares &&
(canReturnToHall || canCancelOrder) ? (
<Space
direction="vertical"
size={8}
className="full-width"
style={{ marginBottom: 12 }}
>
<Alert
showIcon
type="warning"
message="拼单撤单会批量处理所有未验收份额"
description={
hasAcceptedSharingShares
? '已验收份额会保留;如需退款,请从售后处理。'
: '可选择把未验收份额退回大厅,或取消整个拼单订单。'
}
/>
<div>
<Button
danger
onClick={() => {
setCancelSharingOrder(sharingOrder)
cancelSharingForm.setFieldsValue({
action: canReturnToHall ? 'return_to_hall' : 'cancel_order',
reason: '',
})
}}
>
</Button>
</div>
</Space>
) : null}
<Table<WorkOrderShare>
rowKey="shareId"
size="small"
dataSource={sharingShares}
loading={sharingQuery.isFetching}
pagination={false}
columns={[
{
title: '打手',
render: (_, share) =>
share.worker?.displayName || share.worker?.username || '-',
},
{ title: '数量', dataIndex: 'quantity' },
{
title: '份额报酬',
render: (_, share) => formatMoney(share.shareReward),
},
{
title: '状态',
render: (_, share) => (
<Tag color={resolveSharingShareStatusColor(share.status)}>
{formatSharingShareStatus(share.status)}
</Tag>
),
},
{
title: '提交时间',
render: (_, share) =>
share.submittedAt ? formatAdminDateTime(share.submittedAt) : '-',
},
{
title: '验收图',
width: 150,
render: (_, share) => (
<ImagePreviewList
files={
Array.isArray(share.acceptance?.files) ? share.acceptance.files : []
}
imageUrls={
Array.isArray(share.acceptance?.imageUrls)
? share.acceptance.imageUrls
: []
}
size={44}
maxVisible={2}
/>
),
},
{
title: '备注',
render: (_, share) => String(share.acceptance?.note || '').trim() || '-',
},
{
title: '操作',
width: 190,
render: (_, share) => {
if (share.status === 'accepted') return <Tag color="green"></Tag>
if (share.status === 'cancelled') return <Tag></Tag>
return (
<Space size={6}>
{share.status === 'submitted' ? (
<Button
type="primary"
size="small"
onClick={() => {
void runAction(
() =>
acceptAdminWorkOrderShare(
sharingOrder.workOrderId,
],
})
}
})
}}
>
</Button>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
share.shareId,
),
'该拼单参与者已验收',
).then((succeeded) => {
if (succeeded) {
void queryClient.invalidateQueries({
queryKey: [
'admin-worker-platform-order-sharing',
sharingOrder.workOrderId,
],
})
}
})
}}
>
</Button>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
{canCancelShare ? (
<Button
danger
size="small"
icon={<UndoOutlined />}
title="只撤销当前参与者的拼单份额,其他参与者不受影响"
onClick={() => {
setCancelShare(share)
cancelShareForm.setFieldsValue({ reason: '' })
}}
>
</Button>
) : null}
</Space>
)
</Space>
)
},
},
},
]}
/>
]}
/>
</>
)}
</Card>
</Space>
@@ -2381,6 +2456,66 @@ export default function WorkOrdersPanel() {
</Form>
) : null}
</Modal>
<Modal
title={`拼单撤单处理 · ${getDisplayOrderNo(cancelSharingOrder)}`}
open={Boolean(cancelSharingOrder)}
onCancel={() => {
setCancelSharingOrder(null)
cancelSharingForm.resetFields()
}}
onOk={() => cancelSharingForm.submit()}
destroyOnHidden
>
{cancelSharingOrder ? (
<Form form={cancelSharingForm} layout="vertical" onFinish={submitCancelSharing}>
<Alert
showIcon
type="warning"
message="将批量处理所有未验收的拼单份额"
description="退回大厅会释放未验收份额的押金并重新开放对应数量;取消整单仅在不存在已验收份额时可用。"
style={{ marginBottom: 16 }}
/>
<Form.Item
label="处理方式"
name="action"
rules={[{ required: true, message: '请选择拼单撤单处理方式' }]}
>
<Radio.Group>
<Space direction="vertical">
{canReturnToHall ? (
<Radio value="return_to_hall">
退
</Radio>
) : null}
{canCancelOrder ? (
<Radio value="cancel_order">
</Radio>
) : null}
</Space>
</Radio.Group>
</Form.Item>
<Form.Item
label="处理原因"
name="reason"
dependencies={['action']}
rules={[
({ getFieldValue }) => ({
validator(_, value) {
if (getFieldValue('action') !== 'cancel_order' || String(value || '').trim()) {
return Promise.resolve()
}
return Promise.reject(new Error('取消整单时请填写退款或撤单原因'))
},
}),
]}
>
<Input.TextArea rows={3} placeholder="取消整单时请填写退款原因;退回大厅可选填" />
</Form.Item>
</Form>
) : null}
</Modal>
</section>
)
}
@@ -728,6 +728,21 @@ export function cancelAdminWorkOrderShare(
}>(`/api/v1/admin/worker-platform/orders/${workOrderId}/shares/${shareId}/cancel`, payload)
}
export function cancelAdminWorkOrderSharing(
workOrderId: number,
payload: {
action: 'return_to_hall' | 'cancel_order'
reason?: string
},
) {
return apiPost<{
order: WorkOrder
action: 'return_to_hall' | 'cancel_order'
cancelledShareCount: number
releasedDepositAmount: number
}>(`/api/v1/admin/worker-platform/orders/${workOrderId}/sharing/cancel`, payload)
}
export function acceptAdminWorkOrders(workOrderIds: number[]) {
return apiPost<{ orders: WorkOrder[] }>('/api/v1/admin/worker-platform/orders/accept-batch', {
workOrderIds,
@@ -550,6 +550,7 @@ export type WorkOrder = {
canReopen: boolean
canPublish: boolean
canCancel: boolean
canCancelSharing?: boolean
canResolveProblem: boolean
canDeduct: boolean
canConfirmFriend?: boolean