合并远程 main 最新改动

This commit is contained in:
yml2213
2026-11-05 16:11:40 +08:00
9 changed files with 393 additions and 19 deletions
@@ -40,6 +40,12 @@ export default function AdminWorkerPlatformPage() {
const location = useLocation()
const [activeTab, setActiveTab] = useState(() => loadActiveTab(searchParams.get('tab')))
const afterSalesPanelRef = useRef<AfterSalesPanelHandle>(null)
/**
* 从工单列表跳转过来时,售后面板可能尚未挂载(Tab 懒渲染),
* 此时 ref 还是 null,直接调用会被可选链静默吞掉,导致要点两次。
* 先记录待打开的工单,等面板挂载后再真正打开建单弹窗。
*/
const [pendingAfterSalesOrder, setPendingAfterSalesOrder] = useState<WorkOrder | null>(null)
const role = getAdminRole()
const isAdmin = role === 'admin'
const isSupport = role === 'support'
@@ -82,12 +88,20 @@ export default function AdminWorkerPlatformPage() {
const state = location.state as AfterSalesLocationState | null
if (state?.createOrder) {
setActiveTab('after-sales')
afterSalesPanelRef.current?.openCreateModal(state.createOrder)
setPendingAfterSalesOrder(state.createOrder)
// 清掉 state,避免刷新或后退时重复弹窗。
window.history.replaceState({}, '')
}
}, [location.state])
// 售后 Tab 挂载完成(ref 可写)后再打开建单弹窗,避免首点击丢失。
useEffect(() => {
if (visibleActiveTab !== 'after-sales' || !pendingAfterSalesOrder) return
if (!afterSalesPanelRef.current) return
afterSalesPanelRef.current.openCreateModal(pendingAfterSalesOrder)
setPendingAfterSalesOrder(null)
}, [visibleActiveTab, pendingAfterSalesOrder])
function changeTab(nextTab: string) {
if (!isWorkerPlatformTab(nextTab)) return
if (isFinance && nextTab !== 'finance') return
@@ -47,6 +47,8 @@ type AfterSalesCreateFormValues = {
type AfterSalesResolutionFormValues = {
action?: 'not_upheld' | 'remedied' | 'upheld'
resolutionNote?: string
/** 拼单售后追缴的份数;整单售后不展示该字段 */
recoveryQuantity?: number
recoveryAmount?: number
}
@@ -83,6 +85,15 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
const [afterSalesCreateForm] = Form.useForm<AfterSalesCreateFormValues>()
const [afterSalesResolutionForm] = Form.useForm<AfterSalesResolutionFormValues>()
const [afterSalesRepayForm] = Form.useForm<AfterSalesRepayFormValues>()
/** 拼单售后追缴份数(实时回显,用于金额联动展示)。 */
const resolutionRecoveryQuantityWatch = Form.useWatch(
'recoveryQuantity',
afterSalesResolutionForm,
)
const resolutionRecoveryQuantity = Math.max(
1,
Math.round(Number(resolutionRecoveryQuantityWatch || 1)),
)
const afterSalesSharingQuery = useQuery({
queryKey: ['admin-worker-platform-after-sales-sharing', afterSalesOrder?.workOrderId],
@@ -157,10 +168,24 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
function openAfterSalesResolutionModal(item: WorkerAfterSalesCase) {
setAfterSalesCase(item)
const existingRecoveryAmount = Number(item.recoveryAmount || 0)
const shareQuantity = Math.max(1, Number(item.shareQuantity || 1))
const unitRewardFen = Math.max(0, Number(item.shareUnitReward || 0))
// 已追缴过的按原金额反推份数;未处置的默认 1 份。
const recoveryQuantity =
item.shareId && existingRecoveryAmount > 0 && unitRewardFen > 0
? Math.min(shareQuantity, Math.max(1, Math.round(existingRecoveryAmount / unitRewardFen)))
: 1
afterSalesResolutionForm.setFieldsValue({
action: 'upheld',
resolutionNote: '',
recoveryAmount: Number(item.recoveryAmount || 0) / 100,
recoveryQuantity: item.shareId ? recoveryQuantity : undefined,
// 已追缴过的沿用原金额;未处置时按后端建议值:
// 整单 = 发单金额,拼单 = 该责任份额的一份价格。
recoveryAmount:
existingRecoveryAmount > 0
? existingRecoveryAmount / 100
: Number(item.suggestedRecoveryAmount || 0) / 100,
})
}
@@ -272,7 +297,7 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
setAfterSalesPageSize(nextPageSize)
},
})}
scroll={{ x: 1160 }}
scroll={{ x: 1230 }}
columns={[
{
title: '售后单',
@@ -289,13 +314,20 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
},
{
title: '责任打手',
width: 150,
width: 220,
render: (_, item) => (
<div className="cell-stack">
<Typography.Text>{item.workerName || `打手#${item.workerId}`}</Typography.Text>
{item.shareQuantity ? (
<Tag color="purple">拼单 {item.shareQuantity} 份</Tag>
) : null}
{item.shareId ? (
<Tag color="purple">
拼单 {item.shareQuantity || 1} 份 · 一份 {formatMoney(item.shareUnitReward)} ·
合计 {formatMoney(item.shareRewardAmount)}
</Tag>
) : (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
整单发单 {formatMoney(item.workOrderRewardAmount)}
</Typography.Text>
)}
</div>
),
},
@@ -447,7 +479,7 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
.filter((share) => share.status === 'accepted')
.map((share) => ({
value: share.shareId,
label: `${share.worker?.displayName || share.worker?.username || `打手#${share.workerId}`} · ${share.quantity} 份`,
label: `${share.worker?.displayName || share.worker?.username || `打手#${share.workerId}`} · ${share.quantity} 份 · 一份 ${formatMoney(share.unitReward)} · 合计 ${formatMoney(share.shareReward)}`,
}))}
/>
</Form.Item>
@@ -497,6 +529,16 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
form={afterSalesResolutionForm}
layout="vertical"
onFinish={submitAfterSalesResolution}
onValuesChange={(changedValues) => {
// 拼单:份数变化时按「一份价格 × 份数」重算追缴金额;金额仍可手动改。
if (!afterSalesCase?.shareId) return
if (changedValues.recoveryQuantity === undefined) return
const quantity = Math.max(1, Math.round(Number(changedValues.recoveryQuantity || 1)))
afterSalesResolutionForm.setFieldsValue({
recoveryAmount:
(Math.max(0, Number(afterSalesCase.shareUnitReward || 0)) * quantity) / 100,
})
}}
>
<Form.Item
label="处置结论"
@@ -517,14 +559,49 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
>
{({ getFieldValue }) =>
getFieldValue('action') === 'upheld' ? (
<Form.Item
label="追缴金额"
name="recoveryAmount"
rules={[{ required: true, message: '请输入追缴金额' }]}
extra="系统依次扣减该责任打手的本单待解冻金额、提现冻结中的金额(会自动驳回其待审核提现)、可用余额;不足部分转为欠款,可从后续验收报酬、充值入账自动抵扣,也可在此手动偿还。"
>
<InputNumber min={0} precision={2} addonAfter="元" className="full-width" />
</Form.Item>
<>
{afterSalesCase.shareId ? (
<>
<Alert
showIcon
type="info"
style={{ marginBottom: 16 }}
message={`发单金额:一份 ${formatMoney(afterSalesCase.shareUnitReward)} × ${resolutionRecoveryQuantity} 份 = ${formatMoney(Number(afterSalesCase.shareUnitReward || 0) * resolutionRecoveryQuantity)}`}
description={`该责任份额共 ${afterSalesCase.shareQuantity || 1} 份,合计 ${formatMoney(afterSalesCase.shareRewardAmount)};默认追缴 1 份,调整份数后追缴金额自动重算,也可直接修改金额。`}
/>
<Form.Item
label="追缴份数"
name="recoveryQuantity"
rules={[{ required: true, message: '请输入追缴份数' }]}
extra={`最多可追缴 ${afterSalesCase.shareQuantity || 1} 份`}
>
<InputNumber
min={1}
max={Math.max(1, Number(afterSalesCase.shareQuantity || 1))}
precision={0}
addonAfter="份"
className="full-width"
/>
</Form.Item>
</>
) : (
<Alert
showIcon
type="info"
style={{ marginBottom: 16 }}
message={`发单金额:${formatMoney(afterSalesCase.workOrderRewardAmount)}(整单)`}
description="整单售后默认按工单发单金额追缴,可自行调整。"
/>
)}
<Form.Item
label="追缴金额"
name="recoveryAmount"
rules={[{ required: true, message: '请输入追缴金额' }]}
extra="系统依次扣减该责任打手的本单待解冻金额、提现冻结中的金额(会自动驳回其待审核提现)、可用余额;不足部分转为欠款,可从后续验收报酬、充值入账自动抵扣,也可在此手动偿还。"
>
<InputNumber min={0} precision={2} addonAfter="元" className="full-width" />
</Form.Item>
</>
) : (
<Typography.Paragraph type="secondary" style={{ marginBottom: 16 }}>
选择补救完成或问题不成立时,系统自动将该工单同一责任打手此前已追缴且未退还的金额退还至其可用余额。
@@ -235,6 +235,16 @@ export type WorkerAfterSalesCase = {
shopName: string
workerName: string
shareQuantity: number | null
/** 工单发单金额(分):整单售后按此金额追缴 */
workOrderRewardAmount: number
/** 工单是否开启拼单 */
sharingEnabled: boolean
/** 责任份额的一份价格(分) */
shareUnitReward: number
/** 责任份额的合计金额(分)= 一份价格 × 份数 */
shareRewardAmount: number
/** 后端建议的默认追缴金额(分):整单 = 发单金额,拼单 = 一份价格 */
suggestedRecoveryAmount: number
problemType: string
complaintNote: string
complaintFiles: UploadedFile[]