feat:拼单明细管理加一个撤份额的功能,指定数量

This commit is contained in:
2026-10-25 13:44:34 +08:00
parent a7a8634a96
commit 9c620d39cb
16 changed files with 627 additions and 3 deletions
@@ -34,6 +34,18 @@ function formatStatusLabel(status: string) {
return STATUS_LABELS[status] || status || '未知'
}
/** 份额级问题单处置动作中文名(与后台弹窗的四个选项一一对应) */
const SHARE_PROBLEM_ACTION_LABELS: Record<string, string> = {
return_to_worker: '退回打手继续处理',
reopen: '取消该份额并退回接单大厅',
cancel_release: '取消该份额并退还押金',
cancel_deduct: '取消该份额并扣除押金',
}
function formatShareProblemAction(action: string) {
return SHARE_PROBLEM_ACTION_LABELS[action] || ''
}
function readPayload(event: WorkOrderEvent, key: string): string {
const value = event.payload?.[key]
if (value === undefined || value === null) return ''
@@ -311,6 +323,88 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
}
case 'sharing_config_updated':
return { title: '更新了拼单配置', color: 'default' }
case 'sharing_joined': {
const workerLabel = resolveEventWorkerLabel(event)
const quantity = readPayloadAmount(event, 'quantity')
const deadlineAt = readPayload(event, 'deadlineAt')
const timeoutMinutes = readPayloadAmount(event, 'timeoutMinutes')
const deadlineText = deadlineAt
? `截止 ${dayjs(deadlineAt).format('MM-DD HH:mm')}`
: timeoutMinutes > 0
? `限时 ${Math.round(timeoutMinutes)} 分钟`
: '不限时'
return {
title: workerLabel ? `打手参与拼单(${workerLabel})` : '打手参与拼单',
description: [quantity > 0 ? `参与 ${quantity} 份` : '', deadlineText]
.filter(Boolean)
.join(' · '),
color: 'purple',
}
}
case 'sharing_share_quantity_reduced': {
const workerLabel = resolveEventWorkerLabel(event)
const fromQuantity = readPayloadAmount(event, 'fromQuantity')
const toQuantity = readPayloadAmount(event, 'toQuantity')
const reduceQuantity = readPayloadAmount(event, 'reduceQuantity')
const details = [
fromQuantity > 0 && toQuantity > 0 ? `${fromQuantity} 份 → ${toQuantity} 份` : '',
reduceQuantity > 0 ? `撤销 ${reduceQuantity} 份` : '',
note ? `原因:${note}` : '',
'撤掉的份数已退回接单大厅,押金保持冻结',
].filter(Boolean)
return {
title: workerLabel ? `撤销部分拼单份额(${workerLabel})` : '撤销部分拼单份额',
description: details.join(';'),
color: 'orange',
}
}
case 'sharing_share_marked_problem': {
const workerLabel = resolveEventWorkerLabel(event)
return {
title: workerLabel ? `标记拼单份额问题单(${workerLabel})` : '标记拼单份额问题单',
description: note
? `原因:${note};仅该份额受影响,母单状态不变`
: '未填备注;仅该份额受影响,母单状态不变',
color: 'red',
}
}
case 'sharing_share_problem_disposed': {
const workerLabel = resolveEventWorkerLabel(event)
const actionLabel = formatShareProblemAction(readPayload(event, 'action'))
const depositAmount = readPayloadAmount(event, 'depositAmount')
const details = [
actionLabel ? `处理方式:${actionLabel}` : '',
depositAmount > 0 ? `涉及押金 ${formatFenAmount(depositAmount)}` : '',
note ? `备注:${note}` : '',
].filter(Boolean)
return {
title: workerLabel ? `处置拼单份额问题单(${workerLabel})` : '处置拼单份额问题单',
description: details.length > 0 ? details.join(';') : undefined,
color: 'orange',
}
}
case 'sharing_acceptance_draft_saved':
case 'sharing_acceptance_updated': {
const workerLabel = resolveEventWorkerLabel(event)
const imageCount = readPayloadAmount(event, 'imageCount')
const hasVideo = Boolean(event.payload?.hasVideo)
const details = [
imageCount > 0 ? `验收图 ${imageCount} 张` : '',
hasVideo ? '含验收视频' : '',
].filter(Boolean)
return {
title:
type === 'sharing_acceptance_draft_saved'
? workerLabel
? `保存了拼单验收草稿(${workerLabel})`
: '保存了拼单验收草稿'
: workerLabel
? `更新了拼单验收资料(${workerLabel})`
: '更新了拼单验收资料',
description: details.length > 0 ? details.join(' · ') : undefined,
color: 'purple',
}
}
case 'price_synced_from_rule': {
const rewardAmount = Number(readPayload(event, 'rewardAmount') || 0)
const unitReward = Number(readPayload(event, 'sharingUnitReward') || 0)
@@ -72,6 +72,7 @@ import {
markAdminWorkOrderShareProblem,
pinAdminWorkOrder,
publishAdminWorkOrder,
reduceAdminWorkOrderShareQuantity,
reopenAdminCancelledWorkOrder,
resetAdminWorkOrderGiftMaterial,
resolveAdminProblemWorkOrder,
@@ -306,6 +307,7 @@ export default function WorkOrdersPanel() {
const [materialPasteText, setMaterialPasteText] = useState('')
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
const [cancelShare, setCancelShare] = useState<WorkOrderShare | null>(null)
const [reduceShare, setReduceShare] = useState<WorkOrderShare | null>(null)
const [problemShare, setProblemShare] = useState<WorkOrderShare | null>(null)
const [disposeShare, setDisposeShare] = useState<WorkOrderShare | null>(null)
const [cancelSharingOrder, setCancelSharingOrder] = useState<WorkOrder | null>(null)
@@ -320,6 +322,7 @@ export default function WorkOrdersPanel() {
const [materialForm] = Form.useForm<MaterialFormValues>()
const [cancelForm] = Form.useForm<CancelOrderFormValues>()
const [cancelShareForm] = Form.useForm<CancelShareFormValues>()
const [reduceShareForm] = Form.useForm<{ reduceQuantity?: number; note?: string }>()
const [problemShareForm] = Form.useForm<{ note?: string }>()
const [disposeShareForm] = Form.useForm<{
action?: 'return_to_worker' | 'reopen' | 'cancel_release' | 'cancel_deduct'
@@ -725,6 +728,32 @@ export default function WorkOrdersPanel() {
cancelShareForm.resetFields()
}
/** 按份数撤销部分份额:减少的份数回到大厅,押金保持冻结。 */
async function submitReduceShare(values: { reduceQuantity?: number; note?: string }) {
if (!sharingOrder || !reduceShare) return
const workOrderId = sharingOrder.workOrderId
const reduceQuantity = Math.floor(Number(values.reduceQuantity || 0))
if (!Number.isFinite(reduceQuantity) || reduceQuantity <= 0) {
message.warning('请填写要撤掉的份数')
return
}
const keepQuantity = Math.max(0, reduceShare.quantity - reduceQuantity)
const succeeded = await runAction(
() =>
reduceAdminWorkOrderShareQuantity(workOrderId, reduceShare.shareId, {
reduceQuantity,
note: String(values.note || '').trim(),
}),
`已撤掉 ${reduceQuantity} 份,保留 ${keepQuantity} 份继续履约`,
)
if (!succeeded) return
await queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-order-sharing', workOrderId],
})
setReduceShare(null)
reduceShareForm.resetFields()
}
/** 按份额标记问题单:只影响该打手,母单状态不变。备注选填。 */
async function submitProblemShare(values: { note?: string }) {
if (!sharingOrder || !problemShare) return
@@ -3083,9 +3112,11 @@ export default function WorkOrdersPanel() {
onCancel={() => {
setSharingOrder(null)
setCancelShare(null)
setReduceShare(null)
setCancelSharingOrder(null)
sharingForm.resetFields()
cancelShareForm.resetFields()
reduceShareForm.resetFields()
cancelSharingForm.resetFields()
}}
onOk={() => sharingForm.submit()}
@@ -3218,7 +3249,7 @@ export default function WorkOrdersPanel() {
loading={sharingQuery.isFetching}
pagination={false}
tableLayout="fixed"
scroll={{ x: 1624 }}
scroll={{ x: 1794 }}
rowClassName={(share) =>
share.problemMarked
? 'admin-share-row-problem'
@@ -3363,6 +3394,27 @@ export default function WorkOrdersPanel() {
)
},
},
{
title: '撤单记录',
width: 170,
render: (_, share) => {
// 份数前缀由展示层拼接:接口只给结构化的份数与原因文本
const cancelledQuantity = Math.max(
0,
Math.floor(Number(share.cancelQuantity || 0)),
)
const note = String(share.cancelNote || '').trim()
if (!cancelledQuantity && !note) return '-'
const text = cancelledQuantity
? `撤 ${cancelledQuantity} 份${note ? ` · ${note}` : ''}`
: note
return (
<Typography.Text style={{ fontSize: 12 }} ellipsis={{ tooltip: text }}>
{text}
</Typography.Text>
)
},
},
{
title: '操作',
width: 300,
@@ -3451,6 +3503,24 @@ export default function WorkOrdersPanel() {
单人撤单
</Button>
) : null}
{(share.status === 'joined' || share.status === 'submitted') &&
share.quantity >= 2 ? (
<Button
danger
size="small"
icon={<UndoOutlined />}
title="按份数撤销部分份额:减少的份数立即回到大厅,押金保持冻结,验收资料保留"
onClick={() => {
setReduceShare(share)
reduceShareForm.setFieldsValue({
reduceQuantity: 1,
note: '',
})
}}
>
撤份额
</Button>
) : null}
{problemAction}
</Space>
)
@@ -3496,6 +3566,75 @@ export default function WorkOrdersPanel() {
) : null}
</Modal>
<Modal
title={`撤份额 · ${reduceShare?.worker?.displayName || reduceShare?.worker?.username || ''}`}
open={Boolean(reduceShare)}
okText="确认撤销"
cancelText="取消"
okButtonProps={{ danger: true }}
onCancel={() => {
setReduceShare(null)
reduceShareForm.resetFields()
}}
onOk={() => reduceShareForm.submit()}
destroyOnHidden
>
{reduceShare ? (
<Form
form={reduceShareForm}
layout="vertical"
initialValues={{ reduceQuantity: 1 }}
onFinish={submitReduceShare}
>
<Alert
showIcon
type="warning"
message={`将从该打手的 ${reduceShare.quantity} 份中撤掉指定份数`}
description="撤掉的份数会立刻回到抢单大厅;押金保持原冻结额不退还;验收资料保留,之后仍可单独验收剩余份数。"
style={{ marginBottom: 16 }}
/>
<Form.Item
label={`撤掉份数(1 ~ ${Math.max(1, reduceShare.quantity - 1)} 份)`}
name="reduceQuantity"
rules={[{ required: true, message: '请填写要撤掉的份数' }]}
>
<InputNumber
min={1}
max={Math.max(1, reduceShare.quantity - 1)}
precision={0}
addonAfter="份"
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{() => {
const reduceQuantity = Math.floor(
Number(reduceShareForm.getFieldValue('reduceQuantity') || 0),
)
const boundedReduce = Math.max(
0,
Math.min(Math.max(0, reduceShare.quantity - 1), reduceQuantity),
)
const keepQuantity = Math.max(0, reduceShare.quantity - boundedReduce)
return (
<Typography.Text type="secondary">
调整后保留 {keepQuantity} 份,回到大厅 {boundedReduce} 份;剩余份额报酬{' '}
{formatMoney(keepQuantity * Number(reduceShare.unitReward || 0))}
</Typography.Text>
)
}}
</Form.Item>
<Form.Item label="撤销原因(选填)" name="note">
<Input.TextArea
rows={3}
maxLength={200}
placeholder="如:打手账号实际只能赠送 5 份"
/>
</Form.Item>
</Form>
) : null}
</Modal>
<Modal
title={`标记份额问题单 · ${
problemShare?.worker?.displayName || problemShare?.worker?.username || ''
@@ -892,6 +892,18 @@ export function cancelAdminWorkOrderShare(
}>(`/api/v1/admin/worker-platform/orders/${workOrderId}/shares/${shareId}/cancel`, payload)
}
/** 按份数撤销某个打手的部分份额:减少的份数回到大厅,押金保持原冻结额。 */
export function reduceAdminWorkOrderShareQuantity(
workOrderId: number,
shareId: number,
payload: { reduceQuantity: number; note?: string },
) {
return apiPost<{ order: WorkOrder; share: WorkOrderShare }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/shares/${shareId}/reduce-quantity`,
payload,
)
}
/** 按份额标记问题单:只影响该份额持有人,母单状态不变。备注选填。 */
export function markAdminWorkOrderShareProblem(workOrderId: number, shareId: number, note = '') {
return apiPost<{ order: WorkOrder; share: WorkOrderShare }>(
@@ -572,6 +572,10 @@ export type WorkOrderShare = {
timeoutMinutes?: number
timeoutPolicy?: string
cancelledReason?: string
/** 后台撤单/撤份额时填写的原因(自由文本,选填) */
cancelNote?: string
/** 被撤掉的份数(撤份额累计 / 单人撤单为整份份数;0 = 未撤过) */
cancelQuantity?: number
/** 份额级问题单:只影响该份额持有人,母单状态不变 */
problemMarked?: boolean
problemNote?: string