This commit is contained in:
2026-09-14 15:33:35 +08:00
parent 23d956ba15
commit b0de9c026f
30 changed files with 270 additions and 240 deletions
@@ -1,4 +1,5 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed } from 'vue'
import { ArrowLeft, Check, Pencil } from '@lucide/vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -146,7 +147,7 @@ function formatNumber(value?: number | null, digits = 3) {
</script>
<template>
<section v-loading="loading" class="loading-detail-content" :aria-label="title" :aria-busy="loading">
<section v-loading="loading" class="loading-detail-content" :class="{ 'admin-density preload-density': mode === 'preload' }" :aria-label="title" :aria-busy="loading">
<section class="detail-overview" aria-label="清单概览">
<header class="detail-page-header" :class="{ 'detail-page-header--embedded': embedded }">
<el-button link type="primary" class="detail-page-header__back" @click="emit('back')"><ArrowLeft :size="16" />返回列表</el-button>
@@ -155,7 +156,7 @@ function formatNumber(value?: number | null, digits = 3) {
<p>{{ mode === 'preload' ? '核对运输信息、确认预装并维护逐条仓库备注' : '查看已完成装柜明细和柜次费用' }}</p>
</div>
<div v-if="detail && summary" class="detail-page-header__actions">
<PalletLabelPrintDialog v-if="summary.warehouseId" :plan-id="summary.planId" :warehouse-id="summary.warehouseId" :disabled="!summary.containerNo || summary.status === 'EDITING'" />
<PalletLabelPrintDialog :dense="mode === 'preload'" v-if="summary.warehouseId" :plan-id="summary.planId" :warehouse-id="summary.warehouseId" :disabled="!summary.containerNo || summary.status === 'EDITING'" />
<template v-if="canEditItemRemark">
<el-button v-if="canEditTransport" @click="emit('edit')"><Pencil :size="16" />{{ summary.containerNo ? '纠正集装箱号 / 航次' : '补录集装箱号 / 航次' }}</el-button>
<el-button type="primary" :loading="confirmSaving" :disabled="!preloadSummary?.readiness.canConfirm" @click="emit('confirmPreload')"><Check :size="16" />确认预装</el-button>
@@ -219,16 +220,13 @@ function formatNumber(value?: number | null, digits = 3) {
<section class="history-section" aria-label="操作历史">
<header><h2>操作历史</h2><span>{{ visibleHistories.length }} 条</span></header>
<el-empty v-if="visibleHistories.length === 0" description="暂无操作历史" :image-size="72" />
<el-timeline v-else>
<el-timeline-item v-for="history in visibleHistories" :key="historyEventKey(history) || history.historyId" :timestamp="formatDateTime(history.occurredAt)" placement="top">
<div class="history-entry">
<strong>{{ actionLabel(history.actionType) }}</strong>
<span v-if="history.fromStatus || history.toStatus">{{ loadingListStatusLabel(history.fromStatus) }} → {{ loadingListStatusLabel(history.toStatus) }}</span>
<p>{{ history.operatorName || '操作人信息缺失' }}<template v-if="history.operationReason"> · {{ history.operationReason }}</template></p>
</div>
</el-timeline-item>
</el-timeline>
<el-table :data="visibleHistories" border stripe empty-text="暂无操作历史" aria-label="清单操作历史">
<el-table-column label="时间" width="170"><template #default="{ row }">{{ formatDateTime(row.occurredAt) }}</template></el-table-column>
<el-table-column label="事件" width="140"><template #default="{ row }">{{ actionLabel(row.actionType) }}</template></el-table-column>
<el-table-column label="状态变化" min-width="200"><template #default="{ row }"><span v-if="row.fromStatus || row.toStatus">{{ loadingListStatusLabel(row.fromStatus) }} → {{ loadingListStatusLabel(row.toStatus) }}</span><span v-else>—</span></template></el-table-column>
<el-table-column label="操作人" min-width="160"><template #default="{ row }">{{ row.operatorName || '操作人信息缺失' }}</template></el-table-column>
<el-table-column label="原因 / 备注" min-width="240"><template #default="{ row }">{{ row.operationReason || '—' }}</template></el-table-column>
</el-table>
</section>
</DetailPageLayout>
</div>
@@ -273,7 +271,21 @@ function formatNumber(value?: number | null, digits = 3) {
.history-section header { display: flex; align-items: baseline; gap: 10px; margin-bottom: 18px; }
.history-section h2 { margin: 0; font-size: 16px; }
.history-section header span { color: var(--cc-muted); font-size: 12px; }
.history-entry { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.history-entry > span { color: var(--cc-muted); font-size: 12px; }
.history-entry p { width: 100%; margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; }
.preload-density { gap: 12px; padding: 12px; border-radius: 4px; }
.preload-density .detail-page-header { gap: 12px; padding: 0 0 8px; }
.preload-density .detail-page-header__identity h1 { font-size: 16px; overflow-wrap: anywhere; }
.preload-density .detail-page-header__actions { flex-wrap: wrap; }
.preload-density .detail-summary { gap: 0; padding: 0; border-left: 1px solid var(--cc-border); }
.preload-density .detail-summary > div { gap: 0; align-items: stretch; min-height: 36px; border-bottom: 1px solid var(--cc-border); border-right: 1px solid var(--cc-border); }
.preload-density .detail-summary > div > span:first-child { display: flex; align-items: center; flex: 0 0 96px; padding: 6px 8px; background: #f8fafc; }
.preload-density .detail-summary > div > span:first-child::after { content: none; }
.preload-density .detail-summary strong, .preload-density .detail-summary :deep(.status-badge) { align-self: center; margin: 6px 8px; font-weight: 400; }
.preload-density .detail-summary__wide { grid-column: span 2; }
.preload-density .detail-layout :deep(.detail-page-layout__content) { gap: 12px; }
.preload-density .detail-section, .preload-density .history-section { padding-top: 8px; }
.preload-density .detail-section__heading h2 { font-size: 14px; }
.preload-density .detail-section__heading p, .preload-density small { font-size: 12px; }
.preload-density .readiness-note { align-items: baseline; }
.preload-density .readiness-note span { white-space: normal; overflow: visible; overflow-wrap: anywhere; }
@media (max-width: 720px) { .preload-density .detail-summary { grid-template-columns: 1fr; } .preload-density .detail-summary__wide { grid-column: auto; } }
</style>
@@ -229,9 +229,9 @@ describe('LoadingListsView', () => {
expect(source).toContain("item.customerName || '客户信息缺失'")
expect(source).not.toContain('item.customerName || item.accountId')
expect(detailSource).toContain("item.customerName || '客户信息缺失'")
expect(detailSource).toContain("history.operatorName || '操作人信息缺失'")
expect(detailSource).toContain("row.operatorName || '操作人信息缺失'")
expect(detailSource).not.toContain('item.customerName || item.accountId')
expect(detailSource).not.toContain('history.operatorName || history.operatorId')
expect(detailSource).not.toContain('row.operatorName || row.operatorId')
})
it('does not retain the retired loading-list customs entry', () => {
@@ -244,7 +244,7 @@ describe('LoadingListsView', () => {
it('filters pure status no-ops and counts identity-distinct visible history events', () => {
for (const marker of ['visibleHistories', 'STATUS_UPDATE', 'operationId', 'PUSH_PRELOAD']) expect(detailSource).toContain(marker)
expect(detailSource).toContain('visibleHistories.length }} 条')
expect(detailSource).toContain('v-for="history in visibleHistories"')
expect(detailSource).toContain(':data="visibleHistories"')
expect(detailSource).not.toContain('detail.histories.length }} 条')
expect(detailSource).not.toContain('v-for="history in detail.histories"')
expect(detailSource).toContain('seen.has(eventKey)')
@@ -1,4 +1,5 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ContainerSimulationCreate } from '@/modules/container-loading'
@@ -181,7 +182,7 @@ onMounted(() => {
<template>
<AdminPage>
<div class="business-page loading-lists-page">
<div class="business-page loading-lists-page" :class="{ 'admin-density': isPreload }">
<ListPageLayout :ariaLabel="title">
<template #toolbar>
<QueryToolbar
@@ -215,7 +216,7 @@ onMounted(() => {
:aria-label="title"
>
<template #toolbar-actions>
<ContainerSimulationCreate v-if="canCreate" success-message="模拟柜及预装清单已创建" @created="onSimulationCreated" />
<ContainerSimulationCreate :dense="isPreload" v-if="canCreate" success-message="模拟柜及预装清单已创建" @created="onSimulationCreated" />
</template>
<template #actions="{ row }">
<div class="row-actions">
@@ -1,4 +1,5 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, ref } from 'vue'
import { Printer } from '@lucide/vue'
import { ElMessage } from 'element-plus'
@@ -7,7 +8,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { preparePalletLabels, printPalletLabels, type PalletLabelManifest } from '../api/platformLoadingLists'
const props = defineProps<{ planId: string; warehouseId: string; disabled?: boolean }>()
const props = defineProps<{ planId: string; warehouseId: string; disabled?: boolean; dense?: boolean }>()
const open = ref(false)
const loading = ref(false)
const printing = ref(false)
@@ -47,7 +48,7 @@ async function print() {
<template>
<el-button :disabled="disabled" @click="show"><Printer :size="16" />打印托盘码</el-button>
<AppDialog v-model="open" title="散货托盘码" width="min(760px, calc(100vw - 28px))" confirm-text="生成 PDF" :loading="loading" :confirm-loading="printing" :confirm-disabled="disabledConfirm" @confirm="print">
<AppDialog :class="{ 'admin-density-dialog': dense }" v-model="open" title="散货托盘码" width="min(760px, calc(100vw - 28px))" confirm-text="生成 PDF" :loading="loading" :confirm-loading="printing" :confirm-disabled="disabledConfirm" @confirm="print">
<LoadErrorState v-if="error" compact title="托盘码操作失败" :message="error.message" :show-retry="false" />
<template v-if="manifest">
<el-alert type="info" :closable="false" :title="`编号规则:${manifest.containerNo}-FBA地址内部编号-三位序号;一件代发托盘不在此清单。`" />
@@ -1,4 +1,5 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
@@ -244,12 +245,12 @@ watch([planId, warehouseId], () => void loadDetail())
<template>
<AdminPage>
<div class="preload-detail-page">
<div class="preload-detail-page admin-density">
<OperationErrorToast :details="remarkError || editError || confirmError" />
<LoadingListDetailDrawer :mode="'preload'" :loading="loading" :detail="detail" :error="error" :can-edit="canEdit" :can-edit-transport="canCorrectTransport" :transport-note="transportCorrectionNote" :confirm-saving="confirmSaving" @back="goBack" @retry="loadDetail" @edit="openEdit" @edit-warehouse-remark="openWarehouseRemark" @confirm-preload="openConfirmPreload" />
</div>
<AppDialog v-model="editOpen" class="loading-transport-dialog" :title="editDialogTitle" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="editSaving" :confirm-disabled="!canEdit" @confirm="saveTransport">
<AppDialog v-model="editOpen" class="loading-transport-dialog admin-density-dialog" :title="editDialogTitle" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="editSaving" :confirm-disabled="!canEdit" @confirm="saveTransport">
<el-form ref="editFormRef" class="transport-form" :model="editForm" :rules="editRules" label-position="top">
<el-form-item label="真实集装箱号" prop="containerNo"><el-input v-model="editForm.containerNo" maxlength="64" placeholder="请输入提柜后取得的真实集装箱号" @blur="editForm.containerNo = normalizedContainerNo(editForm.containerNo)" /></el-form-item>
<el-form-item label="航次" prop="voyageNo"><el-input v-model="editForm.voyageNo" maxlength="64" placeholder="请输入航次" /></el-form-item>
@@ -257,7 +258,7 @@ watch([planId, warehouseId], () => void loadDetail())
</el-form>
</AppDialog>
<AppDialog v-model="remarkOpen" class="warehouse-remark-dialog" title="编辑仓库备注" width="min(560px, calc(100vw - 28px))" confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveWarehouseRemark">
<AppDialog v-model="remarkOpen" class="warehouse-remark-dialog admin-density-dialog" title="编辑仓库备注" width="min(560px, calc(100vw - 28px))" confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveWarehouseRemark">
<el-alert type="info" :closable="false" show-icon title="仓库备注将随装柜完成资料下发海外仓" />
<el-form label-position="top" class="warehouse-remark-form">
<el-form-item label="客户备注(只读)"><el-input :model-value="remarkTarget?.customerRemark || '—'" disabled /><small>来源:客户下单</small></el-form-item>
@@ -267,7 +268,7 @@ watch([planId, warehouseId], () => void loadDetail())
<LoadErrorState v-if="remarkError" compact title="仓库备注保存失败" :message="remarkError.message" :show-retry="false"><template #details><ApiErrorMeta :details="remarkError" /></template></LoadErrorState>
</AppDialog>
<AppDialog v-model="confirmOpen" class="preload-confirm-dialog" title="确认整份预装计划" width="min(520px, calc(100vw - 28px))" confirm-text="确认预装" :confirm-loading="confirmSaving" :confirm-disabled="!detail?.summary.readiness.canConfirm" @confirm="submitConfirmPreload">
<AppDialog v-model="confirmOpen" class="preload-confirm-dialog admin-density-dialog" title="确认整份预装计划" width="min(520px, calc(100vw - 28px))" confirm-text="确认预装" :confirm-loading="confirmSaving" :confirm-disabled="!detail?.summary.readiness.canConfirm" @confirm="submitConfirmPreload">
<div class="confirm-content">
<el-alert type="warning" :closable="false" show-icon title="本操作将确认整份预装计划" description="真实集装箱号可在提柜或预装详情补录,缺号不阻断此前流程;PDA 确认装柜完成前由服务端执行最终门禁。系统会原子确认本计划包含的全部入库预装票据,并由服务端按真实条件创建唯一 PDA 任务;当前页面不会伪造 PDA 成功。" />
<el-descriptions v-if="detail" :column="1" border>
@@ -281,10 +282,10 @@ watch([planId, warehouseId], () => void loadDetail())
</template>
<style scoped>
.preload-detail-page { display: grid; min-width: 0; max-width: 1180px; gap: 16px; }
.preload-detail-page { display: grid; min-width: 0; width: 100%; gap: 12px; }
.transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
.confirm-content { display: grid; gap: 16px; }
.confirm-content { display: grid; gap: 12px; }
.warehouse-remark-form { margin-top: 14px; }
.warehouse-remark-form small { display: block; margin-top: 4px; color: var(--cc-muted); }
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }