优化
This commit is contained in:
@@ -71,6 +71,21 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).toContain('ORD-1')
|
||||
})
|
||||
|
||||
it('rounds capacity percentages and hides column settings in embedded tables', async () => {
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [{ ...container, capacityCbm: 100, maxWeightKg: 100000 }], page: 1, size: 100, total: 1, pages: 1 })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
|
||||
...plan,
|
||||
items: [{ ...plan.items[0], volumeCbm: 0.187, grossWeightKg: 13.6 }],
|
||||
})
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('0.17%')
|
||||
expect(wrapper.text()).toContain('0.01%')
|
||||
expect(wrapper.text()).not.toContain('0.169999999')
|
||||
expect(wrapper.findAll('button[aria-label="列设置"]')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps unsupported prototype filters out of the real workbench', async () => {
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
@@ -66,6 +66,8 @@ const volumeLimit = computed(() => Number(selectedContainer.value?.capacityCbm |
|
||||
const weightLimit = computed(() => Number(selectedContainer.value?.maxWeightKg || 0) * 1.1)
|
||||
const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / volumeLimit.value * 100 : 0)
|
||||
const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.value * 100 : 0)
|
||||
const displayedVolumePercent = computed(() => percentage(volumePercent.value))
|
||||
const displayedWeightPercent = computed(() => percentage(weightPercent.value))
|
||||
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'orderKeyword', type: 'input', label: '订单或货物', placeholder: '订单号、入库单、客户、货物', width: 250 },
|
||||
@@ -123,8 +125,9 @@ const itemColumns: DataTableColumn[] = [
|
||||
]
|
||||
|
||||
function formatNumber(value: number, digits = 3) {
|
||||
return Number.isFinite(value) ? value.toFixed(digits) : '0.000'
|
||||
return Number.isFinite(value) ? value.toLocaleString('zh-CN', { maximumFractionDigits: digits }) : '0'
|
||||
}
|
||||
function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 }
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||
function statusLabel(status: ContainerPlanStatus) {
|
||||
@@ -360,6 +363,7 @@ onMounted(load)
|
||||
:columns="candidateColumns"
|
||||
:loading="loading || selectionLoading || mutating"
|
||||
preference-key="global-smart-loading-candidates"
|
||||
:show-column-settings="false"
|
||||
:empty-text="selectedContainer ? '当前仓库暂无可装货物' : '暂无可用柜次'"
|
||||
>
|
||||
<template #actions="{ row }">
|
||||
@@ -428,10 +432,10 @@ onMounted(load)
|
||||
</el-empty>
|
||||
<template v-else-if="plan">
|
||||
<div class="capacity-grid">
|
||||
<div><div class="meter-title"><span>体积</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(volumePercent, 100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div>
|
||||
<div><div class="meter-title"><span>重量</span><strong>{{ formatNumber(totalWeight, 1) }} / {{ formatNumber(weightLimit, 1) }} kg</strong></div><el-progress :percentage="Math.min(weightPercent, 100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div>
|
||||
<div><div class="meter-title"><span>体积</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(displayedVolumePercent, 100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div>
|
||||
<div><div class="meter-title"><span>重量</span><strong>{{ formatNumber(totalWeight, 1) }} / {{ formatNumber(weightLimit, 1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent, 100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div>
|
||||
</div>
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="selectionLoading || mutating" preference-key="global-smart-loading-items" empty-text="当前柜次暂无已装货物">
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="selectionLoading || mutating" preference-key="global-smart-loading-items" :show-column-settings="false" empty-text="当前柜次暂无已装货物">
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable" link type="primary" :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存</el-button><el-button v-if="canDelete && editable" link type="danger" @click="remove(row)"><Trash2 :size="15" />移除</el-button></div></template>
|
||||
<template #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||
<template #goods="{ row }"><strong>{{ row.productNameZh || '未命名货物' }}</strong><small>{{ row.skuCode || '无 SKU' }}</small></template>
|
||||
|
||||
@@ -47,6 +47,8 @@ const volumeLimit = computed(() => Number(container.value?.capacityCbm || 0) * 1
|
||||
const weightLimit = computed(() => Number(container.value?.maxWeightKg || 0) * 1.1)
|
||||
const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / volumeLimit.value * 100 : 0)
|
||||
const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.value * 100 : 0)
|
||||
const displayedVolumePercent = computed(() => percentage(volumePercent.value))
|
||||
const displayedWeightPercent = computed(() => percentage(weightPercent.value))
|
||||
|
||||
const itemColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
@@ -70,7 +72,8 @@ const candidateColumns: DataTableColumn[] = [
|
||||
{ key: 'route', label: '目的仓匹配', minWidth: 170, slotName: 'route' },
|
||||
]
|
||||
|
||||
function formatNumber(value: number, digits = 3) { return Number.isFinite(value) ? value.toFixed(digits) : '0.000' }
|
||||
function formatNumber(value: number, digits = 3) { return Number.isFinite(value) ? value.toLocaleString('zh-CN', { maximumFractionDigits: digits }) : '0' }
|
||||
function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 }
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||
function routeMatches(source: object) {
|
||||
@@ -221,10 +224,10 @@ onMounted(load)
|
||||
</section>
|
||||
<template v-else>
|
||||
<section v-if="container" class="summary-band"><div><span>柜型</span><strong>{{ container.containerType }}</strong></div><div><span>装柜仓库</span><strong>{{ container.warehouseName || '—' }}</strong></div><div><span>目的仓</span><strong>{{ container.destinationWarehouseName || container.destinationWarehouseCode || '不限' }}</strong></div><div><span>计划状态</span><strong>{{ plan?.status === 'LOADING' ? '拣货中' : plan ? '预装柜中' : '待装柜' }}</strong></div></section>
|
||||
<section v-if="container" class="capacity-grid"><div><div class="meter-title"><span>体积占用(含 110% 上限)</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(volumePercent,100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div><div><div class="meter-title"><span>重量占用(含 110% 上限)</span><strong>{{ formatNumber(totalWeight,1) }} / {{ formatNumber(weightLimit,1) }} kg</strong></div><el-progress :percentage="Math.min(weightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div></section>
|
||||
<section v-if="container" class="capacity-grid"><div><div class="meter-title"><span>体积占用(含 110% 上限)</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(displayedVolumePercent,100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div><div><div class="meter-title"><span>重量占用(含 110% 上限)</span><strong>{{ formatNumber(totalWeight,1) }} / {{ formatNumber(weightLimit,1) }} kg</strong></div><el-progress :percentage="Math.min(displayedWeightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div></section>
|
||||
<el-empty v-if="noDraft" description="该货柜尚未创建排柜计划"><el-button v-if="canCreate" type="primary" :loading="mutating" @click="createPlan"><Plus :size="16" />创建排柜计划</el-button></el-empty>
|
||||
<template v-else-if="plan">
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading || mutating" preference-key="smart-loading-items" empty-text="暂无已分配货物">
|
||||
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading || mutating" preference-key="smart-loading-items" :show-column-settings="false" empty-text="暂无已分配货物">
|
||||
<template #toolbar-start><div><h2>装柜方案</h2><p>计划 {{ plan.planNo }} · 版本 {{ plan.version }};智能建议必须经人工核对。</p></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button v-if="canAssign && editable" link type="primary" :disabled="quantityDrafts[row.id] === row.packageCount" @click="adjust(row)"><Save :size="15" />保存箱数</el-button><el-button v-if="canDelete && editable" link type="danger" @click="remove(row)"><Trash2 :size="15" />移除</el-button></div></template>
|
||||
<template #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||
@@ -233,7 +236,7 @@ onMounted(load)
|
||||
<template #volume="{ row }">{{ formatNumber(Number(row.volumeCbm || 0)) }} m³</template><template #weight="{ row }">{{ formatNumber(Number(row.grossWeightKg || 0),1) }} kg</template>
|
||||
<template #destination="{ row }">{{ row.destinationWarehouseName || row.destinationWarehouseCode || '—' }}</template>
|
||||
</DataTable>
|
||||
<DataTable :data="candidates" :columns="candidateColumns" :loading="loading || mutating" preference-key="smart-loading-candidates" empty-text="当前仓库暂无可排柜货物">
|
||||
<DataTable :data="candidates" :columns="candidateColumns" :loading="loading || mutating" preference-key="smart-loading-candidates" :show-column-settings="false" empty-text="当前仓库暂无可排柜货物">
|
||||
<template #toolbar-start><div><h2>候选货物</h2><p>系统基于仓库、目的仓、可用箱数及 110% 容量上限校验。</p></div></template>
|
||||
<template #actions="{ row }"><el-button v-if="canAssign && editable" link type="primary" :disabled="!routeMatches(row) || assignedDetailIds.has(row.inboundDetailId) || row.availablePackageCount < 1" @click="openAssign(row)"><Plus :size="15" />分配</el-button></template>
|
||||
<template #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||
|
||||
@@ -104,4 +104,38 @@ describe('API error diagnostics', () => {
|
||||
expect(refresh).toHaveBeenCalledTimes(1)
|
||||
expect(expired).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('refreshes once and replays the original request with the rotated access token', async () => {
|
||||
const refresh = vi.fn(async () => 'access-rotated')
|
||||
const expired = vi.fn()
|
||||
let orderRequests = 0
|
||||
registerAuthRefreshHandler(refresh)
|
||||
registerAuthSessionExpiredHandler(expired)
|
||||
http.defaults.adapter = async (config) => {
|
||||
if (config.url === '/platform/v1/orders' && orderRequests++ === 0) {
|
||||
throw new axios.AxiosError('Unauthorized', 'ERR_BAD_REQUEST', config, undefined, {
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
headers: {},
|
||||
config,
|
||||
data: { code: 'A0401', message: '登录状态已过期' },
|
||||
})
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config,
|
||||
data: { code: '200', message: 'success', data: [{ id: 'order-1' }] },
|
||||
}
|
||||
}
|
||||
|
||||
const response = await http.get('/platform/v1/orders')
|
||||
|
||||
expect(refresh).toHaveBeenCalledTimes(1)
|
||||
expect(expired).not.toHaveBeenCalled()
|
||||
expect(orderRequests).toBe(2)
|
||||
expect(response.config.headers.Authorization).toBe('Bearer access-rotated')
|
||||
expect(response.data).toEqual([{ id: 'order-1' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -163,4 +163,18 @@ describe('DataTable column preferences', () => {
|
||||
expect(toolbarActions.find('.data-table__utility-actions').exists()).toBe(true)
|
||||
expect(toolbarActions.find('button[aria-label="列设置"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('can hide column settings for compact tables without leaving an empty toolbar', () => {
|
||||
const wrapper = shallowMount(DataTable, {
|
||||
props: { data: [], columns: configurableColumns, preferenceKey: 'summary-table', showColumnSettings: false },
|
||||
global: {
|
||||
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
||||
stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub },
|
||||
directives: { loading: {} },
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('button[aria-label="列设置"]').exists()).toBe(false)
|
||||
expect(wrapper.find('.data-table__toolbar').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -63,11 +63,13 @@ const props = withDefaults(defineProps<{
|
||||
total?: number
|
||||
pageSizes?: number[]
|
||||
preferenceKey?: string
|
||||
showColumnSettings?: boolean
|
||||
}>(), {
|
||||
loading: false,
|
||||
emptyText: '暂无数据',
|
||||
total: undefined,
|
||||
pageSizes: () => [10, 20, 50, 100],
|
||||
showColumnSettings: true,
|
||||
})
|
||||
|
||||
const page = defineModel<number>('page', { default: 1 })
|
||||
@@ -76,7 +78,7 @@ const slots = useSlots()
|
||||
const preferenceScope = inject(UI_PREFERENCE_SCOPE, computed(() => 'anonymous'))
|
||||
const preference = ref<LayoutPreferenceSnapshot>({ version: 1, order: [], hidden: [] })
|
||||
const tableRef = ref<{ doLayout: () => void }>()
|
||||
const preferenceEnabled = computed(() => Boolean(props.preferenceKey))
|
||||
const preferenceEnabled = computed(() => Boolean(props.preferenceKey && props.showColumnSettings))
|
||||
|
||||
function columnId(column: DataTableColumn) {
|
||||
const value = column.key ?? column.columnKey ?? column.prop ?? column.property
|
||||
|
||||
Reference in New Issue
Block a user