Release/test 合并上生产 #2

Merged
admin merged 736 commits from release/test into main 2026-09-17 11:50:56 +08:00
6 changed files with 81 additions and 9 deletions
Showing only changes of commit 7ece30bffa - Show all commits
@@ -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>
+34
View File
@@ -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' }])
})
})
+14
View File
@@ -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)
})
})
+3 -1
View File
@@ -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