修复采购审核竞态并整理管理页面展示
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import ElementPlus, { ElMessage } from 'element-plus'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
@@ -66,6 +66,32 @@ const arrangement: FleetArrangement = { sourceType: 'FLEET_CAPACITY_OFFER', busi
|
||||
|
||||
function activatePermissions() { const pinia = createPinia(); setActivePinia(pinia); const auth = useAuthStore(); auth.token = 'test-token'; auth.user = { id: '7', name: '测试操作员', account: 'tester', roles: [] }; auth.permissions = ['platform:overseas-business:purchase:list', 'platform:overseas-business:purchase:edit', 'platform:overseas-business:purchase:review', 'platform:overseas-business:truck:list', 'platform:overseas-business:truck:create', 'platform:overseas-business:truck:edit', 'platform:overseas-business:looseboard:list', 'platform:overseas-business:looseboard:create', 'platform:overseas-business:looseboard:edit']; useAccessPreviewStore().initializeAfterLogin(); return pinia }
|
||||
function mountPage(component: Parameters<typeof mount>[0], props?: Record<string, unknown>) { const pinia = activatePermissions(); return mount(component, { attachTo: document.body, props, global: { plugins: [pinia, ElementPlus] } }) }
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason: Error) => void
|
||||
const promise = new Promise<T>((done, fail) => { resolve = done; reject = fail })
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
async function mountPurchaseReviewRequests() {
|
||||
const first = deferred<Awaited<ReturnType<typeof listOverseasPurchaseWarehouseOptions>>>()
|
||||
const second = deferred<Awaited<ReturnType<typeof listOverseasPurchaseWarehouseOptions>>>()
|
||||
vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [
|
||||
{ ...purchase, id: '101', orderNo: 'OP-A', status: 'PENDING_REVIEW', procurementDirection: 'OVERSEAS_TO_DOMESTIC' },
|
||||
{ ...purchase, id: '102', orderNo: 'OP-B', status: 'PENDING_REVIEW', procurementDirection: 'DOMESTIC_TO_OVERSEAS' },
|
||||
], page: 1, size: 20, total: 2, pages: 1 })
|
||||
vi.mocked(listOverseasPurchaseWarehouseOptions).mockImplementation(direction => direction === 'OVERSEAS_TO_DOMESTIC' ? first.promise : second.promise)
|
||||
const wrapper = mountPage(PurchaseOrdersView)
|
||||
await flushPromises()
|
||||
const buttons = wrapper.findAll('button').filter(button => button.text() === '审核')
|
||||
await buttons[0]!.trigger('click')
|
||||
const dialog = wrapper.findComponent(AppDialog)
|
||||
const openSecond = async () => {
|
||||
dialog.vm.$emit('update:modelValue', false)
|
||||
await wrapper.vm.$nextTick()
|
||||
await buttons[1]!.trigger('click')
|
||||
}
|
||||
return { wrapper, dialog, first, second, openSecond }
|
||||
}
|
||||
enableAutoUnmount(afterEach); afterAll(() => vi.unstubAllGlobals())
|
||||
|
||||
describe('overseas business mounted pages', () => {
|
||||
@@ -222,6 +248,66 @@ describe('overseas business mounted pages', () => {
|
||||
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'review', expect.objectContaining({ version: 3, decision: 'REJECT', remark: '商品型号与要求不符' }))
|
||||
expect(submitProcurementCommand).not.toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.anything(), expect.objectContaining({ reason: expect.anything() }))
|
||||
})
|
||||
it('keeps the current procurement warehouses when an earlier order resolves after the new order', async () => {
|
||||
const { dialog, first, second, openSecond } = await mountPurchaseReviewRequests()
|
||||
await openSecond()
|
||||
second.resolve([{ warehouseId: 'W-B', warehouseCode: 'B', warehouseName: 'B 仓', countryCode: 'CN' }])
|
||||
await flushPromises()
|
||||
first.resolve([{ warehouseId: 'W-A', warehouseCode: 'A', warehouseName: 'A 仓', countryCode: 'US' }])
|
||||
await flushPromises()
|
||||
expect(dialog.findAllComponents({ name: 'ElOption' }).map(option => option.props('value'))).toEqual(['W-B'])
|
||||
const form = dialog.findComponent({ name: 'ElForm' }).props('model') as { purchaseWarehouseId: string }
|
||||
form.purchaseWarehouseId = 'W-B'
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-B', purchase.customerId, 'review', expect.objectContaining({ purchaseWarehouseId: 'W-B', version: purchase.version }))
|
||||
})
|
||||
it.each(['success', 'failure'])('ignores an old procurement warehouse %s while the current request is still loading', async outcome => {
|
||||
const notification = vi.spyOn(ElMessage, 'error')
|
||||
try {
|
||||
const { dialog, first, second, openSecond } = await mountPurchaseReviewRequests()
|
||||
await openSecond()
|
||||
if (outcome === 'success') first.resolve([{ warehouseId: 'W-A', warehouseCode: 'A', warehouseName: 'A 仓', countryCode: 'US' }])
|
||||
else first.reject(new Error('A 仓库查询失败'))
|
||||
await flushPromises()
|
||||
expect(dialog.props('confirmDisabled')).toBe(true)
|
||||
expect(dialog.findComponent({ name: 'ElSelect' }).props('loading')).toBe(true)
|
||||
expect(dialog.findAllComponents({ name: 'ElOption' })).toHaveLength(0)
|
||||
expect(notification).not.toHaveBeenCalled()
|
||||
second.resolve([{ warehouseId: 'W-B', warehouseCode: 'B', warehouseName: 'B 仓', countryCode: 'CN' }])
|
||||
await flushPromises()
|
||||
expect(dialog.props('confirmDisabled')).toBe(false)
|
||||
expect(dialog.findAllComponents({ name: 'ElOption' }).map(option => option.props('value'))).toEqual(['W-B'])
|
||||
} finally { notification.mockRestore() }
|
||||
})
|
||||
it.each(['close', 'unmount', 'account'])('drops late procurement warehouse failures after %s', async action => {
|
||||
const notification = vi.spyOn(ElMessage, 'error')
|
||||
try {
|
||||
const { wrapper, dialog, first } = await mountPurchaseReviewRequests()
|
||||
if (action === 'close') dialog.vm.$emit('update:modelValue', false)
|
||||
else if (action === 'unmount') wrapper.unmount()
|
||||
else { const auth = useAuthStore(); auth.user = { ...auth.user!, id: 'other-operator' } }
|
||||
await flushPromises()
|
||||
first.reject(new Error('已离开的订单查询失败'))
|
||||
await flushPromises()
|
||||
expect(notification).not.toHaveBeenCalled()
|
||||
if (action !== 'unmount') {
|
||||
expect(dialog.props('modelValue')).toBe(false)
|
||||
expect(dialog.props('confirmDisabled')).toBe(false)
|
||||
}
|
||||
} finally { notification.mockRestore() }
|
||||
})
|
||||
it('still reports a current procurement warehouse failure and releases its loading state', async () => {
|
||||
const notification = vi.spyOn(ElMessage, 'error')
|
||||
try {
|
||||
const { dialog, first } = await mountPurchaseReviewRequests()
|
||||
first.reject(new Error('当前仓库查询失败'))
|
||||
await flushPromises()
|
||||
expect(notification).toHaveBeenCalledOnce()
|
||||
expect(dialog.props('modelValue')).toBe(true)
|
||||
expect(dialog.props('confirmDisabled')).toBe(false)
|
||||
expect(dialog.findComponent({ name: 'ElSelect' }).props('loading')).toBe(false)
|
||||
} finally { notification.mockRestore() }
|
||||
})
|
||||
it('keeps long procurement identifiers and descriptive fields out of single-line truncation', async () => {
|
||||
const record = { ...purchase, orderNo: 'QLYSXDKH202610100002', customerName: '全量验收下单客户-20261010-长客户名称', productName: 'PROC-PURCHASE-20261010-长商品名称', productSpecification: '多规格描述-完整展示', purchaseCategory: 'OFFICE_SUPPLIES', purchaseWarehouseName: '洛杉矶海外采购收货仓库' }
|
||||
vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [record], page: 1, size: 20, total: 1, pages: 1 })
|
||||
@@ -300,7 +386,7 @@ describe('overseas business mounted pages', () => {
|
||||
? ['B点取货地', 'C点送达地', 'B点联系人', 'C点联系人', 'B点联系电话', 'C点联系电话', 'B点取货地址', 'C点送达地址']
|
||||
: ['起运地', '目的地', '起运地联系人', '目的地联系人', '起运地联系电话', '目的地联系电话', '起运详细地址', '目的详细地址']
|
||||
expect(labels.slice(0, domain === 'truck' ? 10 : 9)).toEqual(['客户', ...(domain === 'truck' ? ['车辆类型'] : []), ...routeLabels])
|
||||
Object.assign(model, { vehicleType: 'PALLET_15', origin: ' LA ', destination: ' ONT ', originContactName: ' 发货人 ', originContactPhone: ' 1 ', destinationContactName: ' 收货人 ', destinationContactPhone: ' 2 ', pickupAt: '2026-10-20T09:30:45', deliveryAt: '2026-10-20T15:45:12', budgetAmount: 0, currency: 'CNY' })
|
||||
Object.assign(model, { vehicleType: 'PALLET_15', estimatedPalletCount: domain === 'truck' ? 2 : 0, origin: ' LA ', destination: ' ONT ', originContactName: ' 发货人 ', originContactPhone: ' 1 ', destinationContactName: ' 收货人 ', destinationContactPhone: ' 2 ', pickupAt: '2026-10-20T09:30:45', deliveryAt: '2026-10-20T15:45:12', budgetAmount: domain === 'truck' ? 100 : 0, currency: 'CNY' })
|
||||
const submit = wrapper.findAll('button').find(button => button.text() === '提交订单')!
|
||||
await submit.trigger('click'); await flushPromises()
|
||||
expect(createUsTruckOrder).not.toHaveBeenCalled(); expect(createLocalLooseboardOrder).not.toHaveBeenCalled()
|
||||
@@ -310,7 +396,8 @@ describe('overseas business mounted pages', () => {
|
||||
expect(command).toHaveBeenCalledOnce()
|
||||
const [customer, payload] = command.mock.calls[0]!
|
||||
expect(customer).toBe('10001')
|
||||
expect(payload).toEqual(expect.objectContaining({ origin: 'LA', destination: 'ONT', originContactName: '发货人', originContactPhone: '1', destinationContactName: '收货人', destinationContactPhone: '2', totalWeightKg: 0, totalVolumeCbm: 0, budgetAmount: 0, currency: 'CNY', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00' }))
|
||||
expect(payload).toEqual(expect.objectContaining({ origin: 'LA', destination: 'ONT', originContactName: '发货人', originContactPhone: '1', destinationContactName: '收货人', destinationContactPhone: '2', totalWeightKg: 0, totalVolumeCbm: 0, budgetAmount: domain === 'truck' ? 100 : 0, currency: 'CNY', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00' }))
|
||||
if (domain === 'truck') { expect(payload).toMatchObject({ palletCount: 2 }); expect(payload).not.toHaveProperty('packageCount') }
|
||||
for (const field of ['customerId', 'tenantId', 'orderNo', 'status', 'transportMode', 'containerType']) expect(payload).not.toHaveProperty(field)
|
||||
})
|
||||
|
||||
@@ -327,7 +414,7 @@ describe('overseas business mounted pages', () => {
|
||||
it('loads and submits pending-review truck edit', async () => { routing.route.params = { orderId: 'TK-200' }; routing.route.query = { customerId: '10002' }; const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(getUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002'); expect(updateUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002', expect.objectContaining({ version: 1, vehicleType: 'PALLET_15', originContactName: 'Bob', destinationContactName: 'Alice', pickupAt: '2026-08-28T09:30:00' })) })
|
||||
it('keeps truck edit form and version unchanged after a 409', async () => { routing.route.params = { orderId: 'TK-200' }; routing.route.query = { customerId: '10002' }; vi.mocked(updateUsTruckOrder).mockRejectedValue(new ApiResponseError('订单版本冲突', 'B0409', 409)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await destination.setValue('Ontario-MARKER-EDIT'); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(updateUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002', expect.objectContaining({ version: 1, destination: 'Ontario-MARKER-EDIT' })); expect((destination.element as HTMLInputElement).value).toBe('Ontario-MARKER-EDIT'); expect(document.body.textContent).toContain('订单版本冲突'); expect(routing.push).not.toHaveBeenCalled() })
|
||||
it('mounts truck create with explicit customer selector and enabled submit', async () => { const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' }); expect(wrapper.text()).toContain('客户信息'); expect(wrapper.text()).toContain('请选择客户'); expect(wrapper.findAll('button').find((button) => button.text().includes('提交订单'))?.attributes('disabled')).toBeUndefined() })
|
||||
it('reuses one idempotency key and preserves truck create input after retries fail', async () => { vi.mocked(createUsTruckOrder).mockRejectedValue(new ApiResponseError('temporary failure', 'S0500', 500)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0].vm.$emit('update:modelValue', '10001'); selects[1].vm.$emit('update:modelValue', 'PALLET_30'); const origin = wrapper.find('input[placeholder="城市 / 港口 / 州"]'); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await origin.setValue('LA-MARKER'); await destination.setValue('ONT-MARKER'); for (const label of ['B点联系人', 'C点联系人', 'B点联系电话', 'C点联系电话']) await wrapper.findAll('.el-form-item').find(item => item.text().startsWith(label))!.find('input').setValue(label.includes('电话') ? '+1-555-0100' : '联系人'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总重量'))!.find('input').setValue('100'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总体积'))!.find('input').setValue('2.5'); wrapper.findAllComponents({ name: 'ElDatePicker' })[0].vm.$emit('update:modelValue', '2026-08-31T09:30:00'); wrapper.findAllComponents({ name: 'ElDatePicker' })[1].vm.$emit('update:modelValue', '2026-08-31T15:45:00'); await wrapper.vm.$nextTick(); const submit = wrapper.findAll('button').find((button) => button.text().includes('提交订单')); await submit?.trigger('click'); await flushPromises(); await submit?.trigger('click'); await flushPromises(); expect(createUsTruckOrder).toHaveBeenCalledTimes(2); const keys = vi.mocked(createUsTruckOrder).mock.calls.map((call) => call[1].idempotencyKey); expect(keys[0]).toBeTruthy(); expect(keys[1]).toBe(keys[0]); expect((origin.element as HTMLInputElement).value).toBe('LA-MARKER'); expect((destination.element as HTMLInputElement).value).toBe('ONT-MARKER'); expect(document.body.textContent).toContain('服务暂时不可用'); expect(routing.push).not.toHaveBeenCalled() })
|
||||
it('reuses one idempotency key and preserves truck create input after retries fail', async () => { vi.mocked(createUsTruckOrder).mockRejectedValue(new ApiResponseError('temporary failure', 'S0500', 500)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0].vm.$emit('update:modelValue', '10001'); selects[1].vm.$emit('update:modelValue', 'PALLET_30'); const model = wrapper.findComponent({ name: 'ElForm' }).props('model'); Object.assign(model, { estimatedPalletCount: 2, budgetAmount: 100 }); const origin = wrapper.find('input[placeholder="城市 / 港口 / 州"]'); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await origin.setValue('LA-MARKER'); await destination.setValue('ONT-MARKER'); for (const label of ['B点联系人', 'C点联系人', 'B点联系电话', 'C点联系电话']) await wrapper.findAll('.el-form-item').find(item => item.text().startsWith(label))!.find('input').setValue(label.includes('电话') ? '+1-555-0100' : '联系人'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总重量'))!.find('input').setValue('100'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总体积'))!.find('input').setValue('2.5'); wrapper.findAllComponents({ name: 'ElDatePicker' })[0].vm.$emit('update:modelValue', '2026-08-31T09:30:00'); wrapper.findAllComponents({ name: 'ElDatePicker' })[1].vm.$emit('update:modelValue', '2026-08-31T15:45:00'); await wrapper.vm.$nextTick(); const submit = wrapper.findAll('button').find((button) => button.text().includes('提交订单')); await submit?.trigger('click'); await flushPromises(); await submit?.trigger('click'); await flushPromises(); expect(createUsTruckOrder).toHaveBeenCalledTimes(2); const keys = vi.mocked(createUsTruckOrder).mock.calls.map((call) => call[1].idempotencyKey); expect(keys[0]).toBeTruthy(); expect(keys[1]).toBe(keys[0]); expect((origin.element as HTMLInputElement).value).toBe('LA-MARKER'); expect((destination.element as HTMLInputElement).value).toBe('ONT-MARKER'); expect(document.body.textContent).toContain('服务暂时不可用'); expect(routing.push).not.toHaveBeenCalled() })
|
||||
it('loads the real looseboard list', async () => { const wrapper = mountPage(TransportOrdersView, { domain: 'looseboard' }); await flushPromises(); expect(listLocalLooseboardOrders).toHaveBeenCalledWith({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '' }); expect(wrapper.text()).toContain('LB-300'); expect(wrapper.text()).toContain('待接单'); expect(wrapper.text()).toContain('2026-08-01 10:00'); expect(listUsTruckOrders).not.toHaveBeenCalled() })
|
||||
it('loads and submits pending-acceptance looseboard edit with PUT adapter', async () => { routing.route.params = { orderId: 'LB-300' }; routing.route.query = { customerId: '10003' }; const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(getLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003'); expect(updateLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003', expect.objectContaining({ version: 0, estimatedPalletCount: 2 })) })
|
||||
it('requires split contacts and both local times, rejecting reversed times', async () => {
|
||||
|
||||
@@ -30,6 +30,8 @@ const reviewOpen = ref(false), reviewSaving = ref(false), reviewLoading = ref(fa
|
||||
const reviewRow = ref<OverseasPurchaseSummary | null>(null), reviewWarehouses = ref<Awaited<ReturnType<typeof listOverseasPurchaseWarehouseOptions>>>([])
|
||||
const reviewRequestKey = ref('')
|
||||
const reviewForm = reactive({ decision: 'APPROVE' as 'APPROVE' | 'REJECT', purchaseWarehouseId: '', remark: '' })
|
||||
let reviewGeneration = 0
|
||||
watch(reviewOpen, value => { if (!value) invalidateReviewRequest() }, { flush: 'sync' })
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'orderNo', type: 'input', label: '订单编号', placeholder: '请输入订单编号', width: 210 },
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 210, options: customerOptions.value.map((item) => ({ label: item.customerName, value: item.customerId })), props: { filterable: true, loading: customersLoading.value } },
|
||||
@@ -53,7 +55,7 @@ const columns: DataTableColumn[] = [
|
||||
]
|
||||
function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
let generation = 0; let inFlight: Promise<void> | null = null; let requestKey = ''
|
||||
onBeforeUnmount(() => { generation++ })
|
||||
onBeforeUnmount(() => { generation++; invalidateReviewRequest() })
|
||||
function load() {
|
||||
const query: OverseasPurchaseQuery = { page: page.value, size: pageSize.value, customerId: String(applied.value.customerId || ''), orderNo: String(applied.value.orderNo || ''), purchaseCategory: String(applied.value.purchaseCategory || ''), productName: String(applied.value.productName || ''), status: String(applied.value.status || '') as OverseasPurchaseStatus | '' }
|
||||
const key = JSON.stringify([auth.user?.id, query])
|
||||
@@ -69,19 +71,29 @@ function load() {
|
||||
})
|
||||
inFlight = promise; return promise
|
||||
}
|
||||
watch(() => auth.user?.id, () => { rows.value = []; total.value = 0; void load() })
|
||||
watch(() => auth.user?.id, () => { reviewOpen.value = false; rows.value = []; total.value = 0; void load() })
|
||||
function search(model: QueryModel) { applied.value = { ...model }; if (page.value === 1) void load(); else page.value = 1 }
|
||||
function open(row: OverseasPurchaseSummary) { void router.push({ name: 'overseas-business-purchase-detail', params: { purchaseId: row.orderNo }, query: { customerId: row.customerId } }) }
|
||||
function invalidateReviewRequest() {
|
||||
reviewGeneration++
|
||||
reviewLoading.value = false; reviewWarehouses.value = []; reviewRow.value = null
|
||||
}
|
||||
async function beginReview(row: OverseasPurchaseSummary) {
|
||||
if (reviewSaving.value) return
|
||||
const current = ++reviewGeneration; const owner = auth.user?.id
|
||||
reviewRow.value = row; reviewWarehouses.value = []; reviewRequestKey.value = crypto.randomUUID(); Object.assign(reviewForm, { decision: 'APPROVE', purchaseWarehouseId: '', remark: '' }); reviewOpen.value = true
|
||||
reviewLoading.value = true
|
||||
try { reviewWarehouses.value = await listOverseasPurchaseWarehouseOptions(row.procurementDirection) }
|
||||
catch (error) { ElMessage.error(getApiErrorDetails(error, '仓库加载失败').message) }
|
||||
finally { reviewLoading.value = false }
|
||||
// 关闭、切换订单或操作员后,旧请求的结果、提示和结束回调均不得影响当前审核。
|
||||
const isCurrent = () => current === reviewGeneration && reviewOpen.value && owner === auth.user?.id
|
||||
try {
|
||||
const warehouses = await listOverseasPurchaseWarehouseOptions(row.procurementDirection)
|
||||
if (isCurrent()) reviewWarehouses.value = warehouses
|
||||
} catch (error) { if (isCurrent()) ElMessage.error(getApiErrorDetails(error, '仓库加载失败').message) }
|
||||
finally { if (isCurrent()) reviewLoading.value = false }
|
||||
}
|
||||
async function submitReview() {
|
||||
const row = reviewRow.value
|
||||
if (!row || reviewSaving.value) return
|
||||
if (!row || reviewSaving.value || reviewLoading.value || !reviewOpen.value) return
|
||||
if (reviewForm.decision === 'APPROVE' && !reviewForm.purchaseWarehouseId) { ElMessage.warning('请选择采购仓库'); return }
|
||||
if (reviewForm.decision === 'REJECT' && !reviewForm.remark.trim()) { ElMessage.warning('请填写驳回原因'); return }
|
||||
reviewSaving.value = true
|
||||
|
||||
Reference in New Issue
Block a user