Merge branch 'dev' of http://8.163.30.128:10082/html/admin-web into dev
This commit is contained in:
@@ -108,6 +108,26 @@ describe('InboundTaskList measurement scope', () => {
|
||||
expect(source).not.toContain("name: 'inbound-order-detail', params: { inboundId: row.id }")
|
||||
})
|
||||
|
||||
it('closes only after navigating an approved review to the measurement tab', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
const handler = source.slice(source.indexOf('async function handleTaskChanged'), source.indexOf('function handleReviewSelectionChange'))
|
||||
|
||||
expect(handler).toContain("name: 'unified-order-detail-by-inbound-code'")
|
||||
expect(handler).toContain('params: { inboundCode }')
|
||||
expect(handler).toContain("query: { tab: 'measurement', from: 'measurement-review' }")
|
||||
expect(handler).toContain('await router.push')
|
||||
expect(handler.indexOf('await router.push')).toBeLessThan(handler.lastIndexOf('reviewDialogOpen.value = false'))
|
||||
expect(handler).toContain("getApiErrorDetails(error, '订单详情打开失败,请重试')")
|
||||
expect(handler).toContain("name: 'unified-order-detail'")
|
||||
expect(handler).toContain("query: { tenantId: target.tenantId, tab: 'measurement', from: 'measurement-review' }")
|
||||
expect(source).toContain(':post-approve-loading="reviewNavigationLoading"')
|
||||
expect(source).toContain(':approval-completed="reviewApproved"')
|
||||
expect(source).toContain(':close-on-click-modal="!reviewNavigationLoading"')
|
||||
expect(source).toContain('reviewNavigationError')
|
||||
expect(source).toContain('重新打开订单详情')
|
||||
expect(source).not.toContain('params: { inboundId: target.id }')
|
||||
})
|
||||
|
||||
it('shows authoritative measurement totals with operation first and no internal IDs as primary columns', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
const columnsSource = source.slice(source.indexOf('const columns'), source.indexOf('function text'))
|
||||
|
||||
@@ -45,6 +45,9 @@ const warehouseOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
const selectedInbound = ref<InboundOrderSummary | null>(null)
|
||||
const measurementDialogOpen = ref(false)
|
||||
const reviewDialogOpen = ref(false)
|
||||
const reviewNavigationLoading = ref(false)
|
||||
const reviewApproved = ref(false)
|
||||
const reviewNavigationError = ref<ApiErrorDetails | null>(null)
|
||||
const relationshipDialogOpen = ref(false)
|
||||
const relationshipDialogInboundCode = ref('')
|
||||
const batchReviewLoading = ref(false)
|
||||
@@ -184,7 +187,11 @@ function resetFilters() {
|
||||
|
||||
function openTask(row: InboundOrderSummary) {
|
||||
selectedInbound.value = row
|
||||
if (isReview.value) reviewDialogOpen.value = true
|
||||
if (isReview.value) {
|
||||
reviewApproved.value = false
|
||||
reviewNavigationError.value = null
|
||||
reviewDialogOpen.value = true
|
||||
}
|
||||
else measurementDialogOpen.value = true
|
||||
}
|
||||
|
||||
@@ -200,10 +207,47 @@ function openRelationship(row: InboundOrderSummary) {
|
||||
relationshipDialogOpen.value = true
|
||||
}
|
||||
|
||||
function handleTaskChanged() {
|
||||
reviewDialogOpen.value = false
|
||||
selectedInbound.value = null
|
||||
void loadTasks()
|
||||
async function handleTaskChanged() {
|
||||
const target = selectedInbound.value
|
||||
if (!isReview.value) {
|
||||
reviewDialogOpen.value = false
|
||||
selectedInbound.value = null
|
||||
void loadTasks()
|
||||
return
|
||||
}
|
||||
if (!target || reviewNavigationLoading.value) return
|
||||
reviewApproved.value = true
|
||||
reviewNavigationError.value = null
|
||||
|
||||
const inboundCode = target.inboundCode?.trim()
|
||||
const orderId = target.orderId?.trim()
|
||||
if (!inboundCode && !orderId) {
|
||||
reviewNavigationError.value = { message: '审核已通过,但未找到对应订单,请关闭弹框后刷新列表重试' }
|
||||
return
|
||||
}
|
||||
|
||||
reviewNavigationLoading.value = true
|
||||
try {
|
||||
if (inboundCode) {
|
||||
await router.push({
|
||||
name: 'unified-order-detail-by-inbound-code',
|
||||
params: { inboundCode },
|
||||
query: { tab: 'measurement', from: 'measurement-review' },
|
||||
})
|
||||
} else {
|
||||
await router.push({
|
||||
name: 'unified-order-detail',
|
||||
params: { orderId },
|
||||
query: { tenantId: target.tenantId, tab: 'measurement', from: 'measurement-review' },
|
||||
})
|
||||
}
|
||||
reviewDialogOpen.value = false
|
||||
selectedInbound.value = null
|
||||
} catch (error) {
|
||||
reviewNavigationError.value = getApiErrorDetails(error, '订单详情打开失败,请重试')
|
||||
} finally {
|
||||
reviewNavigationLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleReviewSelectionChange(rows: InboundOrderSummary[]) {
|
||||
@@ -266,6 +310,9 @@ function handleMeasurementSubmitted() {
|
||||
|
||||
function clearSelectedInbound() {
|
||||
selectedInbound.value = null
|
||||
reviewApproved.value = false
|
||||
reviewNavigationError.value = null
|
||||
reviewNavigationLoading.value = false
|
||||
}
|
||||
|
||||
watch([page, pageSize], () => { void loadTasks() })
|
||||
@@ -367,9 +414,16 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
top="4vh"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
:close-on-click-modal="!reviewNavigationLoading"
|
||||
:close-on-press-escape="!reviewNavigationLoading"
|
||||
class="inbound-task-dialog"
|
||||
@closed="clearSelectedInbound"
|
||||
>
|
||||
<el-alert v-if="reviewNavigationError" type="error" :closable="false" show-icon class="review-navigation-error">
|
||||
<template #title>{{ reviewNavigationError.message }}</template>
|
||||
<ApiErrorMeta :details="reviewNavigationError" />
|
||||
<el-button v-if="selectedInbound?.inboundCode || selectedInbound?.orderId" link type="danger" :loading="reviewNavigationLoading" @click="handleTaskChanged">重新打开订单详情</el-button>
|
||||
</el-alert>
|
||||
<MeasurementReviewPanel
|
||||
v-if="selectedInbound"
|
||||
:tenant-id="selectedInbound.tenantId"
|
||||
@@ -377,6 +431,8 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
:customer-name="selectedInbound.customerName || selectedInbound.companyName"
|
||||
:channel-name="selectedInbound.channelName"
|
||||
:channel-code="selectedInbound.channelCode"
|
||||
:post-approve-loading="reviewNavigationLoading"
|
||||
:approval-completed="reviewApproved"
|
||||
@changed="handleTaskChanged"
|
||||
/>
|
||||
</el-dialog>
|
||||
@@ -398,6 +454,7 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
<style scoped>
|
||||
.inbound-task-page { min-width: 0; }
|
||||
.state-stack { display: grid; gap: 12px; }
|
||||
.review-navigation-error { margin-bottom: 14px; }
|
||||
.row-actions { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
||||
.row-actions .el-button { margin: 0; }
|
||||
.primary-cell { display: grid; gap: 3px; min-width: 0; }
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('InboundFeePanel', () => {
|
||||
expect(wrapper.findAll('.calculation-flow > div > span').map((node) => node.text())).toEqual([
|
||||
'实际重量',
|
||||
'实际体积',
|
||||
'折算量',
|
||||
'体积重量',
|
||||
'取大后的计费量',
|
||||
])
|
||||
expect(wrapper.findAll('.fee-detail-grid h4').map((node) => node.text())).toEqual([
|
||||
|
||||
@@ -376,13 +376,13 @@ defineExpose({ reload: loadFee })
|
||||
|
||||
<section class="calculation-section" aria-labelledby="calculation-title">
|
||||
<div class="section-title">
|
||||
<div><h4 id="calculation-title">计费依据</h4><p v-if="!compact">实际量与折算量取大后计费,金额以服务端快照为准。</p></div>
|
||||
<div><h4 id="calculation-title">计费依据</h4><p v-if="!compact">实际重量与体积重量取大后计费,金额以服务端快照为准。</p></div>
|
||||
<el-tag effect="plain">{{ taxModeLabel(snapshot.taxMode) }}</el-tag>
|
||||
</div>
|
||||
<div class="calculation-flow">
|
||||
<div><span>实际重量</span><strong>{{ formatNumber(snapshot.actualWeightKg, 3) }} KG</strong></div>
|
||||
<div><span>实际体积</span><strong>{{ formatNumber(snapshot.actualVolumeCbm, 4) }} CBM</strong></div>
|
||||
<div><span>折算量</span><strong>{{ formatNumber(snapshot.convertedQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong><small>系数 {{ formatNumber(snapshot.conversionFactor, 4) }}</small></div>
|
||||
<div><span>体积重量</span><strong>{{ formatNumber(snapshot.convertedQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong><small>系数 {{ formatNumber(snapshot.conversionFactor, 4) }}</small></div>
|
||||
<div class="is-chargeable"><span>取大后的计费量</span><strong>{{ formatNumber(snapshot.chargeableQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong></div>
|
||||
</div>
|
||||
<div class="amount-equation" aria-label="费用计算公式">
|
||||
|
||||
@@ -195,11 +195,73 @@ describe('MeasurementReviewPanel', () => {
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(3)
|
||||
expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(2)
|
||||
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 8 })
|
||||
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('emits only after approval succeeds and blocks duplicate submissions', async () => {
|
||||
let finishApproval: (() => void) | undefined
|
||||
vi.mocked(approveMeasurement).mockImplementation(() => new Promise((resolve) => {
|
||||
finishApproval = () => resolve({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'CALCULATED', initialPricingSucceeded: true, initialPricingMessage: null, initialFee: null })
|
||||
}))
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const approveButton = wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))!
|
||||
await approveButton.trigger('click')
|
||||
await approveButton.trigger('click')
|
||||
|
||||
expect(approveMeasurement).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.emitted('changed')).toBeUndefined()
|
||||
finishApproval?.()
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||
expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps the review panel open and does not emit when approval fails', async () => {
|
||||
vi.mocked(approveMeasurement).mockRejectedValue(new Error('审核写入失败'))
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.emitted('changed')).toBeUndefined()
|
||||
expect(wrapper.find('.review-panel').exists()).toBe(true)
|
||||
expect(wrapper.findAll('button').some((button) => button.text().includes('通过并进入待分柜'))).toBe(true)
|
||||
})
|
||||
|
||||
it('blocks approval while the order detail navigation is loading', async () => {
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
const wrapper = mount(MeasurementReviewPanel, {
|
||||
props: { customerId: '10001', inboundId: '90001', postApproveLoading: true },
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))!.trigger('click')
|
||||
|
||||
expect(approveMeasurement).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not submit approval again after approval succeeded but navigation needs retrying', async () => {
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
const wrapper = mount(MeasurementReviewPanel, {
|
||||
props: { customerId: '10001', inboundId: '90001', approvalCompleted: true },
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
const approveButton = wrapper.findAll('button').find((button) => button.text().includes('审核已通过'))!
|
||||
expect(approveButton.attributes('disabled')).toBeDefined()
|
||||
await approveButton.trigger('click')
|
||||
expect(approveMeasurement).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows historical rejected data read-only without new reject or release operations', async () => {
|
||||
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, processStatus: 'MEASURE_REJECTED', measureReviewStatus: 'REJECTED', reviewReason: '历史审核说明' })
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
|
||||
@@ -21,6 +21,8 @@ const props = defineProps<{
|
||||
customerName?: string | null
|
||||
channelName?: string | null
|
||||
channelCode?: string | null
|
||||
postApproveLoading?: boolean
|
||||
approvalCompleted?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ changed: [] }>()
|
||||
@@ -129,7 +131,7 @@ async function handleWeightChanged() {
|
||||
}
|
||||
|
||||
async function confirmApprove() {
|
||||
if (!detail.value || !isPending.value || !canReview.value) return
|
||||
if (!detail.value || !isPending.value || !canReview.value || actionLoading.value || props.postApproveLoading || props.approvalCompleted) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'审核通过后,入库单将进入待分柜。系统会尝试生成初算费用,但初算失败不影响审核通过,后续可由平台人员补充调整。是否继续?',
|
||||
@@ -147,7 +149,6 @@ async function confirmApprove() {
|
||||
})
|
||||
if (result.initialPricingSucceeded) ElMessage.success('审核已通过,入库单已进入待分柜,费用已完成初算')
|
||||
else ElMessage.warning(`审核已通过,入库单已进入待分柜;费用初算未完成${result.initialPricingMessage ? `:${result.initialPricingMessage}` : ''}`)
|
||||
await reload()
|
||||
emit('changed')
|
||||
} catch (error) {
|
||||
ElMessage.error(isConflict(error) ? '数据已被更新,请刷新后重新审核' : getApiErrorDetails(error, '审核通过失败').message)
|
||||
@@ -183,7 +184,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
</div>
|
||||
<div class="review-toolbar__actions">
|
||||
<el-button v-if="isPending && canReview" type="primary" plain @click="correctionMode = true"><Pencil :size="16" />修改重量</el-button>
|
||||
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve'" @click="confirmApprove"><Check :size="16" />通过并进入待分柜</el-button>
|
||||
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve' || postApproveLoading" :disabled="approvalCompleted" @click="confirmApprove"><Check :size="16" />{{ approvalCompleted ? '审核已通过' : '通过并进入待分柜' }}</el-button>
|
||||
<el-button :loading="loading" aria-label="刷新审核数据" @click="reload"><RotateCcw :size="16" />刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,26 +93,26 @@ describe('inbound weight OCR API', () => {
|
||||
data: { records, page: '1', size: '20', total: '2', pages: '1' },
|
||||
})
|
||||
|
||||
await expect(getWeightPalletOptions('10001', '20001')).resolves.toEqual({
|
||||
await expect(getWeightPalletOptions('10001', '20001', { page: 1, size: 50 })).resolves.toEqual({
|
||||
records, page: 1, size: 20, total: 2, pages: 1,
|
||||
})
|
||||
expect(http.get).toHaveBeenCalledWith(
|
||||
'/platform/v1/first-leg/inbounds/20001/weight-pallet-options',
|
||||
{ params: { tenantId: '10001' } },
|
||||
{ params: { tenantId: '10001', page: 1, size: 50, keyword: undefined } },
|
||||
)
|
||||
})
|
||||
|
||||
it('passes an abort signal without changing the frozen pallet endpoint parameters', async () => {
|
||||
it('passes remote page, keyword and abort signal through the frozen pallet endpoint', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({
|
||||
code: '200', message: 'success', data: { records: [], page: '1', size: '20', total: '0', pages: '0' },
|
||||
})
|
||||
const controller = new AbortController()
|
||||
|
||||
await getWeightPalletOptions('10001', '20001', controller.signal)
|
||||
await getWeightPalletOptions('10001', '20001', { page: 2, size: 50, keyword: ' 木托盘 ', signal: controller.signal })
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith(
|
||||
'/platform/v1/first-leg/inbounds/20001/weight-pallet-options',
|
||||
{ params: { tenantId: '10001' }, signal: controller.signal },
|
||||
{ params: { tenantId: '10001', page: 2, size: 50, keyword: '木托盘' }, signal: controller.signal },
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -74,10 +74,19 @@ export async function getWeightWorkspace(tenantId: string, inboundId: string) {
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function getWeightPalletOptions(tenantId: string, inboundId: string, signal?: AbortSignal) {
|
||||
export async function getWeightPalletOptions(tenantId: string, inboundId: string, query: {
|
||||
page?: number
|
||||
size?: number
|
||||
keyword?: string
|
||||
signal?: AbortSignal
|
||||
} = {}) {
|
||||
const keyword = query.keyword?.trim()
|
||||
const response = await http.get<ApiEnvelope<WeightPalletOptionPageResponse>, ApiEnvelope<WeightPalletOptionPageResponse>>(
|
||||
`${measurementBasePath}/${segment(inboundId)}/weight-pallet-options`,
|
||||
{ params: { tenantId }, ...(signal ? { signal } : {}) },
|
||||
{
|
||||
params: { tenantId, page: query.page ?? 1, size: query.size ?? 50, keyword: keyword || undefined },
|
||||
...(query.signal ? { signal: query.signal } : {}),
|
||||
},
|
||||
)
|
||||
const page = unwrapApiResponse(response)
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { AxiosError } from 'axios'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { DOMWrapper, enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -221,7 +222,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
await flushPromises()
|
||||
await openWeightDialog(wrapper)
|
||||
|
||||
const dialogText = document.body.textContent || ''
|
||||
const dialogText = document.body.querySelector('.el-dialog')?.textContent || ''
|
||||
expect(dialogText).toContain('当前托盘皮重')
|
||||
expect(dialogText).toContain('12.000 KG')
|
||||
expect(dialogText).toContain('OCR 识别毛重')
|
||||
@@ -360,6 +361,24 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(wrapper.text()).not.toContain('可选托盘加载失败')
|
||||
})
|
||||
|
||||
it('remote search keyword uses page 1 and size 50', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
await wrapper.find('.pallet-picker .el-select').trigger('click')
|
||||
await wrapper.find('.pallet-picker input').setValue(' 木托盘 ')
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
await flushPromises()
|
||||
|
||||
expect(getWeightPalletOptions).toHaveBeenLastCalledWith('10001', '20001', expect.objectContaining({
|
||||
page: 1, size: 50, keyword: '木托盘', signal: expect.any(AbortSignal),
|
||||
}))
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('shows pallet loading failures without hiding existing weight results', async () => {
|
||||
vi.mocked(getWeightPalletOptions).mockRejectedValueOnce(new ApiResponseError('托盘服务暂不可用', 'A0500', 503))
|
||||
const wrapper = mountPanel()
|
||||
@@ -369,6 +388,17 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(wrapper.findAll('.weight-card')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('shows a network error as a pallet candidate load failure without hiding historical snapshots', async () => {
|
||||
vi.mocked(getWeightPalletOptions).mockRejectedValueOnce(new AxiosError('Network Error', 'ERR_NETWORK'))
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('可选托盘加载失败')
|
||||
expect(wrapper.findAll('.weight-card')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('木托盘')
|
||||
expect(wrapper.text()).toContain('12.000 KG')
|
||||
})
|
||||
|
||||
it('does not load weight or pallet data without measurement view permission', async () => {
|
||||
const wrapper = mountPanel([])
|
||||
await flushPromises()
|
||||
@@ -378,13 +408,14 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(getWeightPalletOptions).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps historical pallet snapshots visible without requesting candidates for a read-only operator', async () => {
|
||||
it('keeps historical palletCode snapshots visible for a read-only operator', async () => {
|
||||
const wrapper = mountPanel(['platform:firstleg:inbound:measure:view'])
|
||||
await flushPromises()
|
||||
|
||||
expect(getWeightWorkspace).toHaveBeenCalledOnce()
|
||||
expect(getWeightPalletOptions).not.toHaveBeenCalled()
|
||||
expect(wrapper.find('.pallet-picker .el-select').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('TP-001')
|
||||
expect(wrapper.text()).toContain('木托盘')
|
||||
expect(wrapper.text()).toContain('12.000 KG')
|
||||
})
|
||||
@@ -424,7 +455,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
}))
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
const signal = vi.mocked(getWeightPalletOptions).mock.calls[0]?.[2]
|
||||
const signal = vi.mocked(getWeightPalletOptions).mock.calls[0]?.[2]?.signal
|
||||
|
||||
wrapper.unmount()
|
||||
resolveOptions?.(palletPage([{
|
||||
|
||||
@@ -45,6 +45,7 @@ const fileInput = ref<HTMLInputElement>()
|
||||
const workspace = ref<WeightWorkspaceResource | null>(null)
|
||||
const palletOptions = ref<WeightPalletOption[]>([])
|
||||
const selectedPalletId = ref('')
|
||||
const palletKeyword = ref('')
|
||||
const palletLoading = ref(false)
|
||||
const palletError = ref<ApiErrorDetails | null>(null)
|
||||
const loading = ref(false)
|
||||
@@ -61,6 +62,7 @@ const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB
|
||||
let loadSequence = 0
|
||||
let palletLoadSequence = 0
|
||||
let palletAbortController: AbortController | null = null
|
||||
const PALLET_OPTION_PAGE_SIZE = 50
|
||||
|
||||
const editableStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
|
||||
const minimumWeight = { KG: 0.001, LB: 0.002 } as const
|
||||
@@ -225,7 +227,7 @@ async function loadWorkspace() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPalletOptions() {
|
||||
async function loadPalletOptions(keyword = palletKeyword.value) {
|
||||
if (!canLoadPalletOptions.value) {
|
||||
cancelPalletOptionsLoad()
|
||||
palletOptions.value = []
|
||||
@@ -237,10 +239,16 @@ async function loadPalletOptions() {
|
||||
const sequence = ++palletLoadSequence
|
||||
const controller = new AbortController()
|
||||
palletAbortController = controller
|
||||
palletKeyword.value = keyword.trim()
|
||||
palletLoading.value = true
|
||||
palletError.value = null
|
||||
try {
|
||||
const result = await getWeightPalletOptions(customerId.value, props.inboundId, controller.signal)
|
||||
const result = await getWeightPalletOptions(customerId.value, props.inboundId, {
|
||||
page: 1,
|
||||
size: PALLET_OPTION_PAGE_SIZE,
|
||||
keyword: palletKeyword.value || undefined,
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (sequence !== palletLoadSequence || controller.signal.aborted) return
|
||||
palletOptions.value = result.records
|
||||
if (!result.records.some((option) => option.palletId === selectedPalletId.value)) selectedPalletId.value = ''
|
||||
@@ -257,6 +265,14 @@ async function loadPalletOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
function searchPalletOptions(keyword: string) {
|
||||
void loadPalletOptions(keyword)
|
||||
}
|
||||
|
||||
function reloadPalletOptions() {
|
||||
void loadPalletOptions()
|
||||
}
|
||||
|
||||
function cancelPalletOptionsLoad() {
|
||||
++palletLoadSequence
|
||||
palletAbortController?.abort()
|
||||
@@ -417,6 +433,7 @@ watch(
|
||||
workspace.value = null
|
||||
palletOptions.value = []
|
||||
selectedPalletId.value = ''
|
||||
palletKeyword.value = ''
|
||||
palletError.value = null
|
||||
weightDrafts.value = {}
|
||||
loadError.value = null
|
||||
@@ -433,6 +450,7 @@ watch(
|
||||
cancelPalletOptionsLoad()
|
||||
palletOptions.value = []
|
||||
selectedPalletId.value = ''
|
||||
palletKeyword.value = ''
|
||||
palletError.value = null
|
||||
if (canLoadPalletOptions.value) void loadPalletOptions()
|
||||
},
|
||||
@@ -472,6 +490,8 @@ defineExpose({ reload: loadWorkspace })
|
||||
aria-label="选择称重托盘"
|
||||
placeholder="选择称重托盘"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchPalletOptions"
|
||||
:loading="palletLoading"
|
||||
:disabled="loading || uploading || !editable || !canEdit || Boolean(palletError)"
|
||||
/>
|
||||
@@ -519,7 +539,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
:loading="palletLoading"
|
||||
compact
|
||||
class="weight-action-error"
|
||||
@retry="loadPalletOptions"
|
||||
@retry="reloadPalletOptions"
|
||||
>
|
||||
<template #details><ApiErrorMeta :details="palletError" /></template>
|
||||
</LoadErrorState>
|
||||
@@ -531,7 +551,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
class="weight-action-error"
|
||||
title="当前仓库暂无可选托盘,暂时不能上传称重图片。"
|
||||
>
|
||||
<el-button size="small" @click="loadPalletOptions"><RefreshCw :size="14" />重新加载</el-button>
|
||||
<el-button size="small" @click="reloadPalletOptions"><RefreshCw :size="14" />重新加载</el-button>
|
||||
</el-alert>
|
||||
<div v-loading="loading" class="weight-list" aria-live="polite">
|
||||
<div v-if="!loading && rows.length === 0" class="weight-empty">
|
||||
@@ -566,6 +586,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
@click="openPreview(row)"
|
||||
><strong>{{ cardWeight(row) }}</strong></button>
|
||||
<dl class="weight-card__facts">
|
||||
<div><dt>托盘编码</dt><dd :title="row.palletCode">{{ row.palletCode || '未记录编码' }}</dd></div>
|
||||
<div><dt>托盘类型</dt><dd>{{ row.palletType || '未记录类型' }}</dd></div>
|
||||
<div><dt>服务端确认毛重</dt><dd>{{ formatWeight(row.grossWeightKg) }}</dd></div>
|
||||
<div><dt>托盘皮重</dt><dd>{{ formatWeight(row.palletTareWeightKg) }}</dd></div>
|
||||
|
||||
Reference in New Issue
Block a user