Merge branch 'release/test' of http://8.163.30.128:10082/html/admin-web into release/test

This commit is contained in:
2026-08-11 11:59:39 +08:00
47 changed files with 1193 additions and 236 deletions
@@ -108,6 +108,7 @@ function formatNumber(value?: number | null, digits = 3) {
<el-alert v-else-if="mode === 'confirmed'" type="info" :closable="false" show-icon title="装柜完成后清单、明细和运输信息永久只读" />
<section class="detail-section surface-panel">
<header class="detail-section__header"><h2>装柜货物明细</h2><span>{{ detail.items.length }} 条</span></header>
<DataTable
:data="detail.items"
:columns="itemColumns"
@@ -116,7 +117,6 @@ function formatNumber(value?: number | null, digits = 3) {
empty-text="暂无装柜货物明细"
aria-label="装柜货物明细"
>
<template #toolbar-start><h2>装柜货物明细</h2></template>
<template #product="{ row }"><strong>{{ row.chineseName || row.englishName || '—' }}</strong><small v-if="row.englishName && row.chineseName">{{ row.englishName }}</small></template>
<template #grossWeight="{ row }">{{ formatNumber(row.grossWeightKg) }}</template>
<template #netWeight="{ row }">{{ formatNumber(row.netWeightKg) }}</template>
@@ -153,7 +153,9 @@ function formatNumber(value?: number | null, digits = 3) {
.detail-summary__wide { grid-column: span 2; }
.detail-actions { display: flex; justify-content: flex-end; }
.detail-section, .history-section { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); background: #fff; }
.detail-section :deep(.data-table__toolbar) { min-height: 56px; }
.detail-section__header { display: flex; align-items: baseline; gap: 10px; padding: 18px 18px 0; }
.detail-section__header h2 { margin: 0; font-size: 16px; }
.detail-section__header span { color: var(--cc-muted); font-size: 12px; }
.detail-section :deep(.el-table) { width: 100%; }
.detail-section small { display: block; margin-top: 3px; color: var(--cc-muted); }
.history-section { padding: 18px; }
@@ -21,6 +21,7 @@ describe('LoadingListsView', () => {
expect(source).toContain("selectedWarehouseId.value = String(row.warehouseId || '')")
expect(source).not.toContain("ElMessage.warning('请先选择仓库')")
expect(source).not.toContain("'请先选择仓库后查询'")
expect(source).toContain('MISSING_WAREHOUSE_CONTEXT')
})
it('uses the shared query, table, drawer, dialog and error components', () => {
@@ -53,4 +54,24 @@ describe('LoadingListsView', () => {
expect(source).toContain('openEditFromRow(row)')
expect(source).toContain('await loadDetail()')
})
it('refreshes the detail version on A0409 while preserving transport input', () => {
expect(source).toContain("details.code === 'A0409'")
expect(source).toContain('await loadDetail()')
expect(source).toContain('当前输入已保留,请核对后重新保存')
})
it('opens a validated plan and warehouse deep link without mixing list modes', () => {
expect(source).toContain('useRoute()')
expect(source).toContain('route.query.planId')
expect(source).toContain('route.query.warehouseId')
expect(source).toContain("const expectedStatus = isPreload.value ? 'LOADING' : 'COMPLETED'")
expect(source).toContain('MODE_STATUS_MISMATCH')
})
it('passes translated status text directly to the badge', () => {
expect(source).toContain('<StatusBadge :status="statusLabel(row.status)" />')
expect(source).not.toContain('<StatusBadge :status="row.status" />')
expect(detailSource).toContain('<StatusBadge :status="statusLabel(summary.status)" />')
})
})
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { Eye, Pencil } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
@@ -28,6 +29,7 @@ import {
} from '../api/platformLoadingLists'
const props = defineProps<{ mode: LoadingListMode }>()
const route = useRoute()
const access = useAccessPreviewStore()
const rows = ref<LoadingListSummary[]>([])
const loading = ref(false)
@@ -47,6 +49,7 @@ const editSaving = ref(false)
const editError = ref<ApiErrorDetails | null>(null)
const editFormRef = ref<FormInstance>()
const editForm = reactive({ containerNo: '', voyageNo: '', operationReason: '' })
const handledDeepLink = ref('')
const isPreload = computed(() => props.mode === 'preload')
const title = computed(() => isPreload.value ? '预装清单' : '装柜清单')
@@ -111,6 +114,10 @@ function statusLabel(status?: string | null) {
return ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' } as Record<string, string>)[status || ''] || status || '—'
}
function queryId(value: unknown) {
return typeof value === 'string' && /^[1-9]\d*$/.test(value) ? value : ''
}
function toQuery(): LoadingListQuery {
const range = Array.isArray(query.createdRange) ? query.createdRange.map(String) : []
return {
@@ -131,6 +138,7 @@ async function load() {
const result = isPreload.value ? await listPreloadPlans(request) : await listConfirmedLoadingLists(request)
rows.value = result.records
total.value = result.total
await openDeepLinkedDetail()
} catch (cause) {
error.value = getApiErrorDetails(cause, `${title.value}加载失败,请稍后重试`)
} finally {
@@ -138,6 +146,30 @@ async function load() {
}
}
async function openDeepLinkedDetail() {
const planId = queryId(route.query.planId)
const warehouseId = queryId(route.query.warehouseId)
if (!planId || !warehouseId || !canView.value) return
const key = `${props.mode}:${planId}:${warehouseId}`
if (handledDeepLink.value === key) return
handledDeepLink.value = key
selectedPlanId.value = planId
selectedWarehouseId.value = warehouseId
detail.value = null
detailError.value = null
detailOpen.value = true
await loadDetail()
const expectedStatus = isPreload.value ? 'LOADING' : 'COMPLETED'
const loadedDetail = detail.value as LoadingDetail | null
if (loadedDetail && loadedDetail.summary.status !== expectedStatus) {
detail.value = null
detailError.value = {
code: 'MODE_STATUS_MISMATCH',
message: `该计划不属于当前${title.value}状态,请从对应清单入口查看。`,
}
}
}
function search() {
if (page.value !== 1) page.value = 1
else void load()
@@ -145,7 +177,15 @@ function search() {
async function loadDetail() {
if (!selectedPlanId.value || !selectedWarehouseId.value || !canView.value) return
if (!canView.value) return
if (!selectedPlanId.value || !selectedWarehouseId.value) {
detail.value = null
detailError.value = {
message: '该清单缺少所属仓库信息,无法安全查询详情,请联系管理员核对数据。',
code: 'MISSING_WAREHOUSE_CONTEXT',
}
return
}
detailLoading.value = true
detailError.value = null
try {
@@ -208,13 +248,23 @@ async function saveTransport() {
ElMessage.success('运输信息已更新')
await load()
} catch (cause) {
editError.value = getApiErrorDetails(cause, '运输信息保存失败,请刷新详情后重试')
const details = getApiErrorDetails(cause, '运输信息保存失败,请刷新详情后重试')
if (details.code === 'A0409' || /版本|并发|已被其他人修改/.test(details.message)) {
await loadDetail()
editError.value = {
...details,
message: '运输信息已被其他人修改,系统已刷新最新版本;当前输入已保留,请核对后重新保存。',
}
} else {
editError.value = details
}
} finally {
editSaving.value = false
}
}
watch([page, pageSize], () => void load())
watch(() => [route.query.planId, route.query.warehouseId], () => void openDeepLinkedDetail())
onMounted(() => {
void load()
})