优化
This commit is contained in:
@@ -71,6 +71,21 @@ describe('GlobalSmartLoadingView', () => {
|
|||||||
expect(wrapper.text()).toContain('ORD-1')
|
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 () => {
|
it('keeps unsupported prototype filters out of the real workbench', async () => {
|
||||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ const volumeLimit = computed(() => Number(selectedContainer.value?.capacityCbm |
|
|||||||
const weightLimit = computed(() => Number(selectedContainer.value?.maxWeightKg || 0) * 1.1)
|
const weightLimit = computed(() => Number(selectedContainer.value?.maxWeightKg || 0) * 1.1)
|
||||||
const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / volumeLimit.value * 100 : 0)
|
const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / volumeLimit.value * 100 : 0)
|
||||||
const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.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[]>(() => [
|
const queryFields = computed<QueryField[]>(() => [
|
||||||
{ key: 'orderKeyword', type: 'input', label: '订单或货物', placeholder: '订单号、入库单、客户、货物', width: 250 },
|
{ key: 'orderKeyword', type: 'input', label: '订单或货物', placeholder: '订单号、入库单、客户、货物', width: 250 },
|
||||||
@@ -123,8 +125,9 @@ const itemColumns: DataTableColumn[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function formatNumber(value: number, digits = 3) {
|
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 candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||||
function statusLabel(status: ContainerPlanStatus) {
|
function statusLabel(status: ContainerPlanStatus) {
|
||||||
@@ -360,6 +363,7 @@ onMounted(load)
|
|||||||
:columns="candidateColumns"
|
:columns="candidateColumns"
|
||||||
:loading="loading || selectionLoading || mutating"
|
:loading="loading || selectionLoading || mutating"
|
||||||
preference-key="global-smart-loading-candidates"
|
preference-key="global-smart-loading-candidates"
|
||||||
|
:show-column-settings="false"
|
||||||
:empty-text="selectedContainer ? '当前仓库暂无可装货物' : '暂无可用柜次'"
|
:empty-text="selectedContainer ? '当前仓库暂无可装货物' : '暂无可用柜次'"
|
||||||
>
|
>
|
||||||
<template #actions="{ row }">
|
<template #actions="{ row }">
|
||||||
@@ -428,10 +432,10 @@ onMounted(load)
|
|||||||
</el-empty>
|
</el-empty>
|
||||||
<template v-else-if="plan">
|
<template v-else-if="plan">
|
||||||
<div class="capacity-grid">
|
<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(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(weightPercent, 100)" :status="weightPercent > 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>
|
</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 #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 #order="{ row }"><strong>{{ row.orderNo || '—' }}</strong><small>{{ row.inboundCode || '—' }}</small></template>
|
||||||
<template #goods="{ row }"><strong>{{ row.productNameZh || '未命名货物' }}</strong><small>{{ row.skuCode || '无 SKU' }}</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 weightLimit = computed(() => Number(container.value?.maxWeightKg || 0) * 1.1)
|
||||||
const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / volumeLimit.value * 100 : 0)
|
const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / volumeLimit.value * 100 : 0)
|
||||||
const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.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[] = [
|
const itemColumns: DataTableColumn[] = [
|
||||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||||
@@ -70,7 +72,8 @@ const candidateColumns: DataTableColumn[] = [
|
|||||||
{ key: 'route', label: '目的仓匹配', minWidth: 170, slotName: 'route' },
|
{ 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 candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||||
function routeMatches(source: object) {
|
function routeMatches(source: object) {
|
||||||
@@ -221,10 +224,10 @@ onMounted(load)
|
|||||||
</section>
|
</section>
|
||||||
<template v-else>
|
<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="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>
|
<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">
|
<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 #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 #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 #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 #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>
|
<template #destination="{ row }">{{ row.destinationWarehouseName || row.destinationWarehouseCode || '—' }}</template>
|
||||||
</DataTable>
|
</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 #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 #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>
|
<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(refresh).toHaveBeenCalledTimes(1)
|
||||||
expect(expired).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('.data-table__utility-actions').exists()).toBe(true)
|
||||||
expect(toolbarActions.find('button[aria-label="列设置"]').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
|
total?: number
|
||||||
pageSizes?: number[]
|
pageSizes?: number[]
|
||||||
preferenceKey?: string
|
preferenceKey?: string
|
||||||
|
showColumnSettings?: boolean
|
||||||
}>(), {
|
}>(), {
|
||||||
loading: false,
|
loading: false,
|
||||||
emptyText: '暂无数据',
|
emptyText: '暂无数据',
|
||||||
total: undefined,
|
total: undefined,
|
||||||
pageSizes: () => [10, 20, 50, 100],
|
pageSizes: () => [10, 20, 50, 100],
|
||||||
|
showColumnSettings: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const page = defineModel<number>('page', { default: 1 })
|
const page = defineModel<number>('page', { default: 1 })
|
||||||
@@ -76,7 +78,7 @@ const slots = useSlots()
|
|||||||
const preferenceScope = inject(UI_PREFERENCE_SCOPE, computed(() => 'anonymous'))
|
const preferenceScope = inject(UI_PREFERENCE_SCOPE, computed(() => 'anonymous'))
|
||||||
const preference = ref<LayoutPreferenceSnapshot>({ version: 1, order: [], hidden: [] })
|
const preference = ref<LayoutPreferenceSnapshot>({ version: 1, order: [], hidden: [] })
|
||||||
const tableRef = ref<{ doLayout: () => void }>()
|
const tableRef = ref<{ doLayout: () => void }>()
|
||||||
const preferenceEnabled = computed(() => Boolean(props.preferenceKey))
|
const preferenceEnabled = computed(() => Boolean(props.preferenceKey && props.showColumnSettings))
|
||||||
|
|
||||||
function columnId(column: DataTableColumn) {
|
function columnId(column: DataTableColumn) {
|
||||||
const value = column.key ?? column.columnKey ?? column.prop ?? column.property
|
const value = column.key ?? column.columnKey ?? column.prop ?? column.property
|
||||||
|
|||||||
Reference in New Issue
Block a user