feat: 支持待分配/待抢单修改付款金额并增加价格历史

This commit is contained in:
2026-11-03 10:31:46 +08:00
parent 232ef6ce79
commit 96676841b5
22 changed files with 626 additions and 78 deletions
@@ -1,9 +1,28 @@
import { EditOutlined } from '@ant-design/icons'
import { App, Input, InputNumber, Modal, Space, Tag, Typography } from 'antd'
import { EditOutlined, HistoryOutlined } from '@ant-design/icons'
import {
App,
Drawer,
Empty,
Input,
InputNumber,
List,
Modal,
Space,
Spin,
Tag,
Tooltip,
Typography,
} from 'antd'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useRef, useState } from 'react'
import { updateAdminWorkOrder, updateAdminWorkOrderPaymentAmount } from '@/services/admin'
import {
fetchAdminWorkOrderPaymentAmountHistory,
updateAdminWorkOrder,
updateAdminWorkOrderPaymentAmount,
} from '@/services/admin'
import type { WorkOrder } from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import { appendProductNameSourceSuffix } from '@/utils/product-name-source-suffix'
import { formatMoney } from './shared'
@@ -111,13 +130,37 @@ export function UserPaymentAmountEditableCell({
onSaved: () => Promise<void> | void
}) {
const { message } = App.useApp()
const queryClient = useQueryClient()
const editable = canEditUserPaymentAmount(order)
const currentFen = getUserPaymentFen(order)
const lastModifiedText = order.userPaymentAmountUpdatedAt
? `上次修改:${
order.userPaymentAmountPreviousFen === null ||
order.userPaymentAmountPreviousFen === undefined
? '-'
: formatMoney(order.userPaymentAmountPreviousFen)
} → ${currentFen === null ? '-' : formatMoney(currentFen)} · ${formatDateTime(
order.userPaymentAmountUpdatedAt,
)}${order.userPaymentAmountUpdatedBy ? ` · ${order.userPaymentAmountUpdatedBy}` : ''}`
: ''
// 三个可编辑状态共用同一套弹窗;没有修改记录时也保留占位,避免高度和内容随状态变化。
const lastModifiedDisplay = lastModifiedText || '上次修改:暂无记录'
const cellTitle = [editable ? '点击修改用户付款金额' : '', lastModifiedText]
.filter(Boolean)
.join('\n')
const [open, setOpen] = useState(false)
const [draft, setDraft] = useState<number | null>(null)
const [reason, setReason] = useState('')
const [saving, setSaving] = useState(false)
const savingRef = useRef(false)
const [historyOpen, setHistoryOpen] = useState(false)
const historyQuery = useQuery({
queryKey: ['work-order-payment-history', order.workOrderId],
queryFn: () => fetchAdminWorkOrderPaymentAmountHistory(order.workOrderId),
enabled: historyOpen,
retry: false,
})
const historyItems = historyQuery.data?.data?.items || []
function openEditor() {
if (!editable || savingRef.current) return
@@ -152,6 +195,9 @@ export function UserPaymentAmountEditableCell({
})
message.success('用户付款金额已更新')
setOpen(false)
await queryClient.invalidateQueries({
queryKey: ['work-order-payment-history', order.workOrderId],
})
await onSaved()
} catch (error) {
message.error(error instanceof Error ? error.message : '保存失败')
@@ -164,7 +210,6 @@ export function UserPaymentAmountEditableCell({
return (
<>
<div
title={editable ? '点击修改用户付款金额' : undefined}
style={{ cursor: editable ? 'pointer' : 'default', whiteSpace: 'nowrap' }}
onClick={(event) => {
event.stopPropagation()
@@ -172,10 +217,24 @@ export function UserPaymentAmountEditableCell({
}}
>
<Space size={4} align="center">
<Typography.Text style={editable ? { color: 'rgba(0,0,0,0.65)' } : undefined}>
<Typography.Text
title={cellTitle || undefined}
style={editable ? { color: 'rgba(0,0,0,0.65)' } : undefined}
>
{currentFen === null ? '-' : formatMoney(currentFen)}
</Typography.Text>
{editable ? <EditOutlined style={{ color: 'rgba(0,0,0,0.35)', fontSize: 12 }} /> : null}
{order.hasPaymentAmountHistory ? (
<Tooltip title="查看价格历史" placement="topLeft">
<HistoryOutlined
style={{ color: 'rgba(0,0,0,0.35)', fontSize: 12, cursor: 'pointer' }}
onClick={(event) => {
event.stopPropagation()
setHistoryOpen(true)
}}
/>
</Tooltip>
) : null}
</Space>
</div>
@@ -197,6 +256,9 @@ export function UserPaymentAmountEditableCell({
<Typography.Text type="secondary">
当前金额:{currentFen === null ? '-' : formatMoney(currentFen)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{lastModifiedDisplay}
</Typography.Text>
<InputNumber
autoFocus
min={0.01}
@@ -219,10 +281,67 @@ export function UserPaymentAmountEditableCell({
onChange={(event) => setReason(event.target.value)}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
修改会记录到工单流转记录与后台审计日志;财务统计按下一次定时同步更新。
修改会同步来源订单金额,并记录到工单流转记录与后台审计日志;财务统计会同步更新,不会改变发单金额或已加入份额的报酬。
</Typography.Text>
</div>
</Modal>
<Drawer
title="价格历史记录"
open={historyOpen}
width={480}
destroyOnHidden
onClose={() => setHistoryOpen(false)}
>
<div style={{ display: 'grid', gap: 16 }}>
<Typography.Text type="secondary">
工单 {order.workOrderNo} · 共 {historyItems.length} 次修改
</Typography.Text>
{historyQuery.isLoading ? (
<Spin />
) : historyQuery.error ? (
<Typography.Text type="secondary">
{historyQuery.error instanceof Error
? historyQuery.error.message
: '读取价格历史失败'}
</Typography.Text>
) : historyItems.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无价格历史" />
) : (
<List
dataSource={historyItems}
renderItem={(item, index) => (
<List.Item style={{ display: 'block', paddingInline: 0 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
}}
>
<Typography.Text strong>
{item.previousAmountFen === null
? `- → ${formatMoney(item.amountFen)}`
: `${formatMoney(item.previousAmountFen)} → ${formatMoney(item.amountFen)}`}
</Typography.Text>
{index === 0 ? <Tag color="gold">最近一次</Tag> : null}
</div>
<Typography.Text
type="secondary"
style={{ display: 'block', marginTop: 4, fontSize: 12 }}
>
{formatDateTime(item.changedAt)} · {item.changedBy || '未知'}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
原因:{item.reason || '未填写'}
</Typography.Text>
</List.Item>
)}
/>
)}
</div>
</Drawer>
</>
)
}
@@ -1760,7 +1760,7 @@ export default function WorkOrdersPanel() {
},
{
title: '用户付款金额',
width: 130,
width: 160,
render: (_, row) => <UserPaymentAmountEditableCell order={row} onSaved={refreshAll} />,
},
{
@@ -33,9 +33,13 @@ export function getUserPaymentFen(order: PaymentSourceOrder): number | null {
return fromColumn !== null && fromColumn > 0 ? fromColumn : null
}
/** 用户付款金额仅在「待完善」阶段可编辑,与后端校验同口径。 */
const USER_PAYMENT_AMOUNT_EDITABLE_STATUSES = new Set(['pending_material', 'unassigned', 'open'])
/** 用户付款金额在「待完善 / 待分配 / 待抢单」可编辑,与后端校验同口径。 */
export function canEditUserPaymentAmount(order: { status?: string; rawStatus?: string }): boolean {
return String(order.rawStatus || order.status || '').trim() === 'pending_material'
// 运营态优先:拼单母单 rawStatus 仍是 open,但满额后界面展示为代练中/待验收,不能继续改。
const effectiveStatus = String(order.status || order.rawStatus || '').trim()
return USER_PAYMENT_AMOUNT_EDITABLE_STATUSES.has(effectiveStatus)
}
/** 发单金额高于用户付款时返回两边金额,否则 null(付款未知时不提示) */
@@ -6,6 +6,7 @@ import type {
WorkCategory,
WorkOrder,
WorkOrderEvent,
WorkOrderPaymentAmountHistoryItem,
WorkOrderShare,
WorkOrderStatistics,
WorkProductRule,
@@ -726,6 +727,12 @@ export function fetchAdminWorkOrderEvents(workOrderId: number) {
)
}
export function fetchAdminWorkOrderPaymentAmountHistory(workOrderId: number) {
return apiGet<{ items: WorkOrderPaymentAmountHistoryItem[] }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/payment-amount-history`,
)
}
export function fetchAdminMockCustomShops() {
return apiGet<{ items: string[] }>('/api/v1/admin/worker-platform/orders/mock/custom-shops')
}
@@ -684,6 +684,14 @@ export type WorkOrder = {
rewardAmount: number
/** 来源订单实付金额(分):取消部分时用于预填退款金额。 */
sourceOrderAmount?: number
/** 最近一次修改用户付款金额前的金额(分);NULL 表示修改前金额未知,仅后台可见。 */
userPaymentAmountPreviousFen?: number | null
/** 最近一次修改用户付款金额的时间,仅后台可见。 */
userPaymentAmountUpdatedAt?: string | null
/** 最近一次修改用户付款金额的操作人展示名/用户名快照,仅后台可见。 */
userPaymentAmountUpdatedBy?: string
/** 是否存在用户付款金额修改历史,仅后台列表返回;用于显示「价格历史」入口。 */
hasPaymentAmountHistory?: boolean
/** 当前打手生效的免押额度,仅打手端订单数据返回。 */
depositFreeAmount?: number
requiredDepositAmount?: number
@@ -771,6 +779,16 @@ export type WorkOrderStatistics = {
statusCounts: Record<string, number>
}
/** 用户付款金额的一次修改记录,仅后台可见。 */
export type WorkOrderPaymentAmountHistoryItem = {
id: number
previousAmountFen: number | null
amountFen: number
reason: string
changedBy: string
changedAt: string
}
/** 工单流转事件(订单详情中的操作日志) */
export type WorkOrderEvent = {
id: number
@@ -318,7 +318,16 @@ export function patchAdminWorkOrderListItems(
)
if (index >= 0) {
previousRow = items[index]
nextItems = items.map((item, itemIndex) => (itemIndex === index ? snapshot : item))
nextItems = items.map((item, itemIndex) =>
itemIndex === index
? {
...(snapshot as Record<string, unknown>),
// 列表专用派生字段不在实时快照里,沿用旧值,避免价格历史入口消失。
hasPaymentAmountHistory:
(item as { hasPaymentAmountHistory?: boolean }).hasPaymentAmountHistory === true,
}
: item,
)
} else if (options.insert) {
nextItems = [snapshot, ...items]
} else {