feat: 支持待分配/待抢单修改付款金额并增加价格历史
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user