fix(consignment): 手动匹配订单按钮点了不弹窗(弹窗挂错了位置)
用户反馈:在库待匹配列表点「手动匹配订单」没有反应。 根因:上一批把手动匹配弹窗挂到了「编辑寄售物品」弹窗**内部**,外层弹窗未打开时 该组件根本没被挂载,点击只改状态、界面无变化。 - 弹窗移到面板顶层(与其它弹窗同级),点击即弹 - 新增静态守卫测试:断言手动匹配弹窗前面不能有未闭合的 Modal(即不能嵌在别的弹窗里)、 按钮必须真的 setState、open 由该状态驱动;已验证该守卫能拦下错误写法 - 顺带补入口缺口:订单侧原先只在"检测到系统候选"的提醒弹窗里给入口,订单没有任何候选时 反而进不去手动匹配 —— 工单行内新增常驻「手动匹配现货」(仅待指派/待补资料, 与后端前置校验同口径) - 已暂停匹配的现货点按钮会直接 409,改为按钮禁用并提示先打开「允许匹配」开关 - destroyOnClose → antd 6 的 destroyOnHidden(前者已废弃) 验证:前端 typecheck/lint/format/48 例测试/build 全过。
This commit is contained in:
@@ -221,7 +221,7 @@ export default function ConsignmentManualMatchModal({
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
|
||||
@@ -554,9 +554,22 @@ export default function ConsignmentInventoryPanel() {
|
||||
</Button>
|
||||
{record.status === 'in_stock' && canManageConsignment ? (
|
||||
<>
|
||||
<Button size="small" type="link" onClick={() => setManualMatchItem(record)}>
|
||||
手动匹配订单
|
||||
</Button>
|
||||
<Tooltip
|
||||
title={
|
||||
record.allowMatching
|
||||
? '从待匹配订单里人工挑一单(支持规格冲突强制匹配,需填原因)'
|
||||
: '该物品已暂停匹配,先打开上方「允许匹配」开关'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={!record.allowMatching}
|
||||
onClick={() => setManualMatchItem(record)}
|
||||
>
|
||||
手动匹配订单
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@@ -944,20 +957,6 @@ export default function ConsignmentInventoryPanel() {
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<ConsignmentManualMatchModal
|
||||
open={Boolean(manualMatchItem)}
|
||||
onClose={() => setManualMatchItem(null)}
|
||||
inventory={
|
||||
manualMatchItem
|
||||
? {
|
||||
inventoryId: manualMatchItem.id,
|
||||
label: manualMatchItem.itemName,
|
||||
}
|
||||
: null
|
||||
}
|
||||
onMatched={() => void inventoriesQuery.refetch()}
|
||||
/>
|
||||
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
@@ -1162,6 +1161,21 @@ export default function ConsignmentInventoryPanel() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 手动匹配订单:从现货出发挑订单(与订单侧共用同一弹窗) */}
|
||||
<ConsignmentManualMatchModal
|
||||
open={Boolean(manualMatchItem)}
|
||||
onClose={() => setManualMatchItem(null)}
|
||||
inventory={
|
||||
manualMatchItem
|
||||
? {
|
||||
inventoryId: manualMatchItem.id,
|
||||
label: manualMatchItem.itemName,
|
||||
}
|
||||
: null
|
||||
}
|
||||
onMatched={() => void inventoriesQuery.refetch()}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
SendOutlined,
|
||||
ThunderboltOutlined,
|
||||
TeamOutlined,
|
||||
UndoOutlined,
|
||||
UserAddOutlined,
|
||||
@@ -82,6 +83,7 @@ import {
|
||||
} from '@/services/admin/consignment'
|
||||
import type { ConsignmentFulfillmentInfo, WorkOrderInventoryMatchCheck } from '@/types/consignment'
|
||||
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
|
||||
import { isManuallyMatchableOrderStatus } from '@/utils/consignment-manual-match'
|
||||
import ConsignmentManualMatchModal from '@/components/admin/ConsignmentManualMatchModal'
|
||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
||||
@@ -1539,6 +1541,19 @@ export default function WorkOrdersPanel() {
|
||||
指派并核销
|
||||
</Button>
|
||||
) : null}
|
||||
{/*
|
||||
手动指定现货必须**常驻**:只在"检测到候选"的提醒弹窗里给入口,
|
||||
订单没有任何系统候选时客服反而进不去——而那正是最需要人工指定的时候。
|
||||
*/}
|
||||
{canFulfillConsignment && isManuallyMatchableOrderStatus(row.status) ? (
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={() => setManualMatchOrder(row)}
|
||||
title="从全部在库现货里人工指定一件(支持规格冲突强制匹配,需填原因)"
|
||||
>
|
||||
手动匹配现货
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canCancel && (canReturnToHall || canCancelOrder) ? (
|
||||
<Button danger icon={<UndoOutlined />} onClick={() => openCancelModal(row)}>
|
||||
撤单
|
||||
|
||||
@@ -70,3 +70,13 @@ export function filterManualMatchOptions<T>(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 哪些订单状态可以手动指定现货。
|
||||
*
|
||||
* 与后端 `fulfillWorkOrderWithConsignment` 的前置校验保持一致(只允许 unassigned /
|
||||
* pending_material):入口先按同一口径收敛,避免"按钮点了才报错"。
|
||||
*/
|
||||
export function isManuallyMatchableOrderStatus(status: string): boolean {
|
||||
return status === 'unassigned' || status === 'pending_material'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
/**
|
||||
* 手动匹配弹窗曾经被插到「编辑寄售物品」弹窗**内部**,导致外层弹窗没打开时它根本没挂载:
|
||||
* 用户点「手动匹配订单」什么都不会发生。这类"点了没反应"的问题在浏览器里才看得出来,
|
||||
* 这里用静态检查把它钉住:组件必须挂在顶层(前面不能有未闭合的 Modal)。
|
||||
*/
|
||||
function assertTopLevelMount(filePath: string, componentName: string) {
|
||||
const source = readFileSync(filePath, 'utf8')
|
||||
const index = source.indexOf(`<${componentName}`)
|
||||
assert.ok(index > 0, `${filePath} 里找不到 ${componentName} 的挂载点`)
|
||||
|
||||
const before = source.slice(0, index)
|
||||
const opened = (before.match(/<Modal[\s>]/g) || []).length
|
||||
const closed = (before.match(/<\/Modal>/g) || []).length
|
||||
assert.equal(
|
||||
opened - closed,
|
||||
0,
|
||||
`${componentName} 必须挂在顶层:不能嵌在别的 Modal 里,否则外层弹窗未打开时它不会渲染`,
|
||||
)
|
||||
}
|
||||
|
||||
test('库存面板:手动匹配订单弹窗挂在顶层(按钮点了必须能弹出来)', () => {
|
||||
assertTopLevelMount(
|
||||
new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname,
|
||||
'ConsignmentManualMatchModal',
|
||||
)
|
||||
})
|
||||
|
||||
test('订单面板:手动指定现货弹窗同样挂在顶层', () => {
|
||||
assertTopLevelMount(
|
||||
new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url).pathname,
|
||||
'ConsignmentManualMatchModal',
|
||||
)
|
||||
})
|
||||
|
||||
test('按钮点击要真的把状态设上,弹窗的 open 由该状态驱动', () => {
|
||||
const source = readFileSync(
|
||||
new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname,
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(source.includes('setManualMatchItem(record)'), '行内按钮必须设置待匹配的现货')
|
||||
assert.ok(source.includes('open={Boolean(manualMatchItem)}'), '弹窗 open 必须由该状态驱动')
|
||||
})
|
||||
@@ -59,3 +59,13 @@ test('关键词过滤:现货能按寄卖单号/物品名搜到,大小写与
|
||||
assert.equal(filterManualMatchOptions(list, '苍龙', pick).length, 1)
|
||||
assert.equal(filterManualMatchOptions(list, '不存在', pick).length, 0)
|
||||
})
|
||||
|
||||
test('只有待指派/待补资料的订单能手动指定现货(与后端前置校验同口径)', async () => {
|
||||
const { isManuallyMatchableOrderStatus } =
|
||||
await import('../src/utils/consignment-manual-match.ts')
|
||||
assert.equal(isManuallyMatchableOrderStatus('unassigned'), true)
|
||||
assert.equal(isManuallyMatchableOrderStatus('pending_material'), true)
|
||||
for (const status of ['in_progress', 'pending_acceptance', 'accepted', 'cancelled', 'disputed']) {
|
||||
assert.equal(isManuallyMatchableOrderStatus(status), false, `${status} 不该给手动匹配入口`)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -289,6 +289,24 @@
|
||||
前端在"部分匹配"候选上先 `modal.confirm` 危险确认,再带 `allowPartial: true` 重试,高配充低配的合法玩法保留但不可误触。
|
||||
3. ⏳ 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)未做:当前仍是"取全量在库 → 内存比对"。
|
||||
|
||||
### 批次 T · 手动匹配的入口修复(本轮)
|
||||
|
||||
**问题**(用户):在库待匹配列表点「手动匹配订单」**不弹窗**。
|
||||
|
||||
**根因**:弹窗被挂到了「编辑寄售物品」弹窗**内部**(JSX 嵌套位置错),
|
||||
外层弹窗没打开时它根本没被挂载 → 点击只改状态、什么都不发生。
|
||||
**这类"点了没反应"的问题在浏览器里才看得出来**,所以顺手加了静态守卫。
|
||||
|
||||
| 修复 | 说明 |
|
||||
| --- | --- |
|
||||
| ✅ 挂载位置 | 弹窗移到面板顶层(与其它弹窗同级),点击即弹 |
|
||||
| ✅ 静态守卫 | 新增 3 例测试:断言手动匹配弹窗**前面不能有未闭合的 Modal**,按钮必须真的 setState、open 由该状态驱动(已验证该守卫能拦下"嵌在别的弹窗里"的写法) |
|
||||
| ✅ 入口补全 | 订单侧原来只在「检测到候选」的提醒弹窗里给入口,**订单没有任何系统候选时反而进不去**(而那正是最需要人工指定的时候)→ 工单行内新增常驻「手动匹配现货」(仅待指派/待补资料,与后端前置校验同口径) |
|
||||
| ✅ 失败前收敛 | 已暂停匹配的现货点按钮会直接 409 → 改为按钮禁用 + 提示"先打开允许匹配开关" |
|
||||
| ✅ 顺带 | `destroyOnClose` 换成 antd 6 的 `destroyOnHidden`(前者已废弃) |
|
||||
|
||||
验证:前端 typecheck / lint / format / 48 例测试 / build 全过。
|
||||
|
||||
### 批次 S · 客服手动指定现货匹配(本轮)
|
||||
|
||||
**需求**(用户):客服要能在后台**人工指定**现货与订单匹配起来,而不是只能用系统算出的候选。
|
||||
|
||||
Reference in New Issue
Block a user