Release/test 合并上生产 #2
@@ -46,11 +46,11 @@
|
||||
| 消息中心 | `modules/notifications/` | Backend `iam/.../notification/`;仅顶部消息中心,不进入左侧菜单 |
|
||||
| 订单列表、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;图片读取归 `file/` 的 `PlatformProductImageController`。Order 状态为 `SUBMITTED` 时统一列表必须保持 `PENDING_REVIEW/APPROVE_ORDER`,不得被已存在的 Firstleg 准备中入库投影覆盖 |
|
||||
| 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 |
|
||||
| 入库、测量、客户箱规策略与计价主数据 | `modules/inbounds/measurement/`、`modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算 |
|
||||
| 入库、测量、称重库位状态、客户箱规策略与计价主数据 | `modules/inbounds/measurement/`、`modules/inbounds/weight/components/WeightOcrPanel.vue`、`modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 |
|
||||
| 财务与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;柜费入口 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 |
|
||||
| 货柜、排柜、预装/装柜清单、真实柜号后补与整柜报关 | `modules/container-loading/`、`modules/loading-lists/`;预装详情 `pages/PreloadPlanDetailView.vue`,货柜详情 `pages/ContainerDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue`、`api/platformContainerCustoms.ts` | 创建柜次时自动生成唯一预装计划,加入入仓码即同步进入该计划,页面不提供创建计划或推送预装操作。预装详情先调用 `/loading-plans/{planId}/lifecycle`。订舱关联和预装确认允许真实集装箱号为空;首次补录可在 `EDITING/LOADING` 进行,PDA 最终完成前必须补齐;已有柜号纠正只允许 `EDITING` 且任务未领取、无物理作业事实。整柜报关逐资料完成,全买单柜使用 `complete-all-buy` |
|
||||
| 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`pages/UnifiedOrdersView.vue`;装柜消费 `modules/container-loading/pages/SmartLoadingWorkbenchView.vue` | 前端只提交订单 ID;Backend `firstleg/loading/coload/` 展开并冻结每票全部可用入仓片段。成员数是入仓片段数,不等于订单票数;只要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均整组处理 |
|
||||
| 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue` 内嵌 `pages/GlobalSmartLoadingView.vue`,柜次页 `SmartLoadingWorkbenchView.vue`,以及 `smartLoadingChannelGroups.ts`、`api/platformContainerLoading.ts`;全局页当前不注册独立路由 | Backend `firstleg/` 的 `/inbound-candidates`、`/containers`、`/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`。候选状态多选覆盖待审核、待测量、待分柜、装柜中,状态只用于筛选展示,按钮资格以服务端 `canAssign` 为准;待审核/待测量可提前占位但不得提前确认预装或启动 PDA。人工加入允许跨渠道,智能推荐仍按柜次渠道。柜次容量和限重取服务端字段;同柜组作为单卡整组分页、分配和移除,任一成员已拣时整组保留 |
|
||||
| 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue` 内嵌 `pages/GlobalSmartLoadingView.vue`,柜次页 `SmartLoadingWorkbenchView.vue`,以及 `smartLoadingChannelGroups.ts`、`smartLoadingItems.ts`、`api/platformContainerLoading.ts`;全局页当前不注册独立路由 | Backend `firstleg/` 的 `/inbound-candidates`、`/containers`、`/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`。候选状态多选覆盖待审核、待测量、待分柜、装柜中,状态只用于筛选展示,按钮资格以服务端 `canAssign` 为准;待审核/待测量可提前占位但不得提前确认预装或启动 PDA。计划箱数编辑上限、已用和未用展示只消费服务端 `totalPackageCount/remainingPackageCount`,不得从候选页或本地规格重新拼算。人工加入允许跨渠道,智能推荐仍按柜次渠道。柜次容量和限重取服务端字段;同柜组作为单卡整组分页、分配和移除,任一成员已拣时整组保留 |
|
||||
| 三期订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/` 的 `components/OceanBookingDocumentsPanel.vue`、`OceanBookingPrintDialog.vue`,以及 `modules/container-loading/`、`modules/port-schedules/` | 订舱草稿按当前版本维护资料,放舱后资料与表单只读;打印只对 `RELEASED` 开放。`platformOceanBookings.ts` 的 `/link-candidates` 只消费服务端分页,`containerCount` 是柜型需求 `quantity` 总和;Backend `firstleg/oceanbooking/`、`file/`、`print/`、`firstleg/portschedule/` |
|
||||
| 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts` 的 `/candidates`、`components/TruckAppointmentForm.vue`、`pages/TruckAppointmentCreateView.vue`、`TruckAppointmentsView.vue` | 表单只选择服务端返回的 `RELEASED` 订舱与拖车供应商,不选择或提交客户、柜次、装柜计划或真实集装箱号;提单号和仓库由订舱冻结事实带入。一个订舱最多一张非终态拖车单,重复创建转入现有单继续编辑;PDA 完成不自动推进拖车。附件为平台作用域且不重复提单文件;Backend `TruckAppointmentService`、`truck-appointments-v1.yaml` |
|
||||
| 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | `modules/customs/` 是历史报关案件/清单/代理提交与回执入口;装柜完成后的整柜退税/买单资料从货柜详情进入,不在旧模块复制;Backend `firstleg/outbound/dispatch/`、`firstleg/customs/`、`file/` |
|
||||
|
||||
@@ -925,7 +925,8 @@ describe('GlobalSmartLoadingView', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
|
||||
|
||||
expect(source).toContain('<el-checkbox-group :model-value="selectedChannelCodes"')
|
||||
expect(source).toContain('<el-checkbox-button v-for="channel in channelOptions"')
|
||||
expect(source).toContain('v-for="channel in channelOptions"')
|
||||
expect(source).toContain('@dblclick.stop.prevent="selectOnlyChannel(channel.code)"')
|
||||
expect(source).toContain('{{ channel.name }}')
|
||||
expect(source).toContain("ElMessage.warning('至少选择一个渠道')")
|
||||
expect(source).toContain('class="compact-summary" aria-label="已选渠道装柜统计"')
|
||||
|
||||
@@ -242,7 +242,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
expect(card.get('.weight-card__facts').text()).toContain('库位A01 · 待上架')
|
||||
})
|
||||
|
||||
it('renders the authoritative whole-inbound reservation summary and an empty reserved slot', async () => {
|
||||
it('keeps whole-inbound reservation guidance out of the panel and shows location on each weight card', async () => {
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue({
|
||||
...workspace,
|
||||
palletLocationGuide: {
|
||||
@@ -260,13 +260,9 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
const guide = wrapper.get('.putaway-guide')
|
||||
expect(guide.text()).toContain('整票预留库位')
|
||||
expect(guide.text()).toContain('A01 / A02 / A03')
|
||||
expect(guide.text()).toContain('预留槽位数3')
|
||||
expect(guide.text()).toContain('实际托数2')
|
||||
expect(guide.text()).toContain('预留空位数1')
|
||||
expect(guide.text()).toContain('A03预留空位本票预留空位')
|
||||
expect(wrapper.find('.putaway-guide').exists()).toBe(false)
|
||||
expect(wrapper.text()).not.toContain('整票上架引导')
|
||||
expect(wrapper.get('.weight-card__facts').text()).toContain('库位A01 · 待上架')
|
||||
})
|
||||
|
||||
it('does not render a full-width placeholder when no whole-inbound guide exists', async () => {
|
||||
@@ -296,7 +292,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
|
||||
expect(wrapper.text()).toContain('A0 临时溢出位')
|
||||
expect(wrapper.get('.weight-card__facts').text()).toContain('库位A0 · 待上架 · A0 临时溢出位')
|
||||
expect(wrapper.get('.putaway-guide__slots li').classes()).toContain('is-overflow')
|
||||
expect(wrapper.find('.putaway-guide').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
@@ -372,8 +368,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
|
||||
)
|
||||
expect(getWeightWorkspace).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.findAll('.weight-card')).toHaveLength(0)
|
||||
expect(wrapper.get('.putaway-guide').text()).toContain('预留空位数1')
|
||||
expect(wrapper.get('.putaway-guide').text()).toContain('A01预留空位本票预留空位')
|
||||
expect(wrapper.find('.putaway-guide').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the image and server workspace visible when deletion fails', async () => {
|
||||
|
||||
@@ -35,14 +35,12 @@ describe('WeightOcrPanel contract', () => {
|
||||
expect(source).toContain('货物重量')
|
||||
expect(source).toContain('palletTareWeightKg')
|
||||
expect(source).toContain('netWeightKg')
|
||||
expect(source).toContain('slot.palletSequence')
|
||||
expect(source).toContain('row.locationCode')
|
||||
expect(source).toContain('row.sharedOverflow')
|
||||
expect(source).toContain('putawayStatusLabel(row.slotStatus)')
|
||||
expect(source).toContain('workspace.palletLocationGuide.reservedSlotCount')
|
||||
expect(source).toContain('workspace.palletLocationGuide.actualPalletCount')
|
||||
expect(source).toContain('workspace.palletLocationGuide.reservedEmptySlotCount')
|
||||
expect(source).toContain('本票预留空位')
|
||||
expect(source).not.toContain('class="putaway-guide"')
|
||||
expect(source).not.toContain('整票上架引导')
|
||||
expect(source).not.toContain('整票预留库位')
|
||||
expect(source).not.toContain('尚未分配整票预留库位')
|
||||
expect(source).not.toContain('class="weight-card__putaway"')
|
||||
expect(source).toContain('<div><dt>库位</dt><dd')
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
} from '../api/weightOcr'
|
||||
import type {
|
||||
WeightPalletOption,
|
||||
WeightPutawaySlotStatus,
|
||||
WeightResultResource,
|
||||
WeightWorkspaceResource,
|
||||
} from '../types'
|
||||
@@ -194,17 +193,6 @@ function putawayStatusLabel(status: WeightResultResource['slotStatus']) {
|
||||
} as const)[status as Exclude<WeightResultResource['slotStatus'], null>] || '未分配'
|
||||
}
|
||||
|
||||
function putawayStatusType(status: WeightResultResource['slotStatus']) {
|
||||
if (status === 'TRANSFERRED') return 'success'
|
||||
if (status === 'BOUND') return 'warning'
|
||||
if (status === 'RELEASED') return 'info'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function guideSlotStatusType(status: WeightPutawaySlotStatus) {
|
||||
return putawayStatusType(status)
|
||||
}
|
||||
|
||||
function hasWeightDraftChanged(row: WeightResultResource) {
|
||||
const draft = weightDrafts.value[row.id]
|
||||
if (!draft) return false
|
||||
@@ -557,37 +545,6 @@ defineExpose({ reload: loadWorkspace })
|
||||
</LoadErrorState>
|
||||
|
||||
<template v-else>
|
||||
<section v-if="workspace?.palletLocationGuide" class="putaway-guide" aria-labelledby="putaway-guide-title">
|
||||
<div class="putaway-guide__heading">
|
||||
<div>
|
||||
<span class="putaway-guide__eyebrow">整票上架引导</span>
|
||||
<h4 id="putaway-guide-title">整票预留库位</h4>
|
||||
</div>
|
||||
<div class="putaway-guide__metrics" aria-label="整票库位汇总">
|
||||
<div><span>预留库位</span><strong>{{ workspace.palletLocationGuide.slots.map((slot) => slot.locationCode).join(' / ') }}</strong></div>
|
||||
<div><span>预留槽位数</span><strong>{{ workspace.palletLocationGuide.reservedSlotCount }}</strong></div>
|
||||
<div><span>实际托数</span><strong>{{ workspace.palletLocationGuide.actualPalletCount }}</strong></div>
|
||||
<div><span>预留空位数</span><strong>{{ workspace.palletLocationGuide.reservedEmptySlotCount }}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="putaway-guide__slots" aria-label="整票预留槽位">
|
||||
<li
|
||||
v-for="slot in workspace.palletLocationGuide.slots"
|
||||
:key="slot.reservationId"
|
||||
:class="{ 'is-empty': slot.slotStatus === 'RESERVED_EMPTY', 'is-overflow': slot.sharedOverflow }"
|
||||
>
|
||||
<div class="putaway-guide__slot-title">
|
||||
<strong>{{ slot.locationCode }}</strong>
|
||||
<el-tag size="small" effect="light" :type="guideSlotStatusType(slot.slotStatus)">
|
||||
{{ putawayStatusLabel(slot.slotStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-if="slot.palletSequence != null">第 {{ slot.palletSequence }} 托 · {{ slot.palletCode || '托盘编码未记录' }}</span>
|
||||
<span v-else>本票预留空位</span>
|
||||
<small v-if="slot.sharedOverflow">A0 临时溢出位</small>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<LoadErrorState
|
||||
v-if="palletError"
|
||||
title="可选托盘加载失败"
|
||||
@@ -734,23 +691,6 @@ defineExpose({ reload: loadWorkspace })
|
||||
.weight-summary strong { color: #067647; font-size: 16px; }
|
||||
.visually-hidden-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
||||
.weight-action-error { margin-bottom: 14px; }
|
||||
.putaway-guide { margin: 14px 0 18px; padding: 16px; border: 1px solid #84caff; border-radius: 8px; background: #f5fbff; }
|
||||
.putaway-guide__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; }
|
||||
.putaway-guide__eyebrow { color: #175cd3; font-size: 11px; font-weight: 700; letter-spacing: .08em; }
|
||||
.putaway-guide h4 { margin: 3px 0 0; color: #102a56; font-size: 18px; }
|
||||
.putaway-guide__metrics { display: grid; grid-template-columns: minmax(180px, 1.8fr) repeat(3, minmax(90px, .6fr)); gap: 10px; flex: 1; max-width: 820px; }
|
||||
.putaway-guide__metrics div { display: grid; min-width: 0; gap: 3px; padding: 9px 11px; border-radius: 6px; background: #fff; box-shadow: 0 1px 2px rgb(16 24 40 / 6%); }
|
||||
.putaway-guide__metrics span { color: #667085; font-size: 11px; }
|
||||
.putaway-guide__metrics strong { color: #101828; font-size: 15px; overflow-wrap: anywhere; }
|
||||
.putaway-guide__slots { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin: 14px 0 0; padding: 0; list-style: none; }
|
||||
.putaway-guide__slots li { display: grid; gap: 5px; min-width: 0; padding: 11px; border: 1px solid #b2ddff; border-radius: 6px; background: #fff; }
|
||||
.putaway-guide__slots li.is-empty { border-style: dashed; background: #f9fafb; }
|
||||
.putaway-guide__slots li.is-overflow { border-color: #fec84b; background: #fffaeb; }
|
||||
.putaway-guide__slot-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.putaway-guide__slot-title strong { color: #1849a9; font-size: 20px; }
|
||||
.putaway-guide__slots li.is-overflow .putaway-guide__slot-title strong { color: #93370d; }
|
||||
.putaway-guide__slots li > span { color: #475467; font-size: 12px; overflow-wrap: anywhere; }
|
||||
.putaway-guide__slots small { width: fit-content; color: #93370d; font-weight: 600; }
|
||||
.weight-list { min-height: 0; margin-top: 14px; }
|
||||
.weight-empty { display: flex; min-height: 0; align-items: center; gap: 10px; padding: 4px 0 0; color: var(--cc-muted); }
|
||||
.weight-empty strong { color: #344054; font-size: 14px; }
|
||||
@@ -783,9 +723,6 @@ defineExpose({ reload: loadWorkspace })
|
||||
.pallet-picker :deep(.el-select) { width: 100%; }
|
||||
.weight-heading__actions :deep(.el-button) { width: 100%; min-height: 44px; }
|
||||
.weight-summary-group { width: 100%; justify-content: space-between; }
|
||||
.putaway-guide__heading { flex-direction: column; }
|
||||
.putaway-guide__metrics { width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.putaway-guide__metrics div:first-child { grid-column: 1 / -1; }
|
||||
.weight-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user