From c188482c24c196b991fd5f7c76748fbef505f6b8 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 18 Aug 2026 17:40:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 9 + .../pages/ContainerDetailView.test.ts | 24 +++ .../pages/ContainerDetailView.vue | 168 ++++++++++++++++++ .../pages/ContainersView.test.ts | 26 ++- .../pages/ContainersView.vue | 22 +-- src/modules/container-loading/routes.ts | 6 + .../loading-lists/api/containerFees.test.ts | 4 +- .../loading-lists/api/containerFees.ts | 10 +- .../components/ContainerFeesPanel.test.ts | 20 ++- .../components/ContainerFeesPanel.vue | 78 ++++++-- .../components/LoadingListDetailDrawer.vue | 45 ++--- .../components/LoadingListsView.test.ts | 22 ++- .../pages/PreloadPlanDetailView.test.ts | 15 +- .../pages/PreloadPlanDetailView.vue | 8 - .../components/TruckAppointmentForm.test.ts | 6 +- .../components/TruckAppointmentForm.vue | 9 +- .../pages/TruckAppointmentDetailView.test.ts | 19 ++ .../pages/TruckAppointmentDetailView.vue | 29 ++- 18 files changed, 423 insertions(+), 97 deletions(-) create mode 100644 src/modules/container-loading/pages/ContainerDetailView.test.ts create mode 100644 src/modules/container-loading/pages/ContainerDetailView.vue diff --git a/AGENTS.md b/AGENTS.md index c6474ae4..3af178dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,12 +98,21 @@ - Admin 是高信息密度业务系统,默认使用紧凑的表格、键值行、分组标题和连续分区呈现信息;不将普通字段、统计值或流程节点拆成大面积卡片,不为少量信息制造大留白。一屏应优先展示完成当前业务判断所需的多项关键信息。 - 需要突出结果时,仅对总额、异常、待办或当前状态使用克制的强调;其余信息保持同一紧凑信息层级,不重复标题、说明和装饰性容器。 +- 详情页的短字段优先使用无独立卡片外框的多列键值网格或描述表;在 `1920×1080` 下通常并排展示 `4–6` 组短字段,长文本、地址、备注和异常原因再按内容跨列。不得把“一个标签 + 一个值”做成一张卡片,也不得用嵌套卡片、重复阴影和多层圆角制造层级。 +- 详情内容按真实业务域使用紧凑分组标题、细分隔线和连续白底分区;重复记录使用表格或紧凑列表,只有独立指标、明确告警、待办或需要单独操作的业务对象才使用卡片。卡片是否采用以信息结构为准,不以已有组件方便为理由机械套用。 +- 详情页顶部的返回、标题、核心状态和主要操作应尽量合并在同一紧凑头部;不得让返回按钮、状态提示、标题说明各自独占一整行。路由壳层已经展示标题时,正文不得再次重复标题或说明。 +- 同一状态或流程结论只保留一个主要视觉落点:优先在页头或概览中使用紧凑状态标签;除真实错误、阻断、危险或必须立即处理的告警外,不再叠加大面积彩色横幅、彩色卡片和重复状态条。页面主色操作通常只保留一个,其余动作使用次要或文字层级。 +- 不以“信息密度”为由使用过小字号、压缩可点击区域、删除标签或强制截断关键值;长值可以换行、跨列或提供完整悬停内容。目标是减少无效容器、重复说明和纵向留白,而不是牺牲可读性与可操作性。 +- 同一详情页必须使用一致的内容底色和容器语言。采用连续白底时,页头、概览、明细、费用和历史应位于同一主内容面内,以分隔线和间距分区;不得出现顶部透明或灰底、下方连续独立白卡的割裂布局。确需独立卡片时,各同级区域的背景、圆角、边框和外间距必须统一。 +- 在 `1920×1080` 验收视口中,如果一组柜次号、集装箱号、订单号、提单号或时间集中显示为省略号,说明列数或宽度设计失败,应减少列数、允许合理换行或让长字段跨列,而不是继续缩小字号。详情概览的标签样式应使用直接子级选择器,避免给 `StatusBadge` 等嵌套组件误加冒号、字号或截断规则。 +- 样式调整必须用真实长编号、空值、状态徽标及至少一组多字段数据核对首屏;自动化测试只能证明结构和行为,不能替代人工截图对背景连续性、有效信息量、截断和留白的视觉验收。 - 修改已有 Admin 原型页面前先读 `../../docs/Admin-Client前端原型还原验收规则.md`,按页面和操作形成原型核对矩阵;本节维护 Admin 的实现约束,不复制跨前端验收文档全文。 - 已向客户讲解或确认过的产品原型是页面视觉与布局实现的直接事实来源。字段顺序、字段分组、字段摆放、列宽、控件宽高、对齐方式、换行方式、区块层级、上下左右边距、内边距、间隙、留白、圆角、边框、阴影、背景色、文字颜色和字号均必须按原型还原,不得只参考大致风格后自行重新排版。 - 原型中的按钮位置、顺序、尺寸、圆角、边框、背景色、文字色、图标、悬停态、激活态、禁用态和危险语义必须直接按原型实现;不得擅自改为文字链接、不同颜色、不同按钮类型、折叠菜单或自创交互效果,也不得增加原型没有的上浮、缩放、渐变线、装饰图标或其他视觉元素。 - 原型已经给出明确图标、字符图标、图片资源或按钮内图标时,直接使用原型中的同一方案和资源,不得以 Lucide、Element Plus 默认图标、手绘图标或个人偏好替换。仅当原型没有定义对应图标时,才按本节的默认图标语义补齐。 - 上述原型还原规则优先于本文件中“延续现有视觉体系”、默认组件样式和默认图标库等一般性规则。只有用户当前明确要求、阶段范围、真实字段长度、响应式、可访问性或接口事实与原型冲突时才允许做最小调整,并须在修改前说明具体差异、原因和影响范围;不得以技术实现方便为由偏离原型。 +- “Admin 高信息密度”是当前已确认的产品级约束:当历史原型仅因大量字段卡片、重复状态块或过度留白与本节冲突时,应保留原型字段、顺序、业务分组和交互语义,但将承载方式收敛为上述紧凑布局;不得以机械复刻历史卡片外观覆盖当前密度要求。 - 详情、新增、编辑、审核等操作采用独立页面、居中弹框、侧滑面板或其他承载方式,必须逐页面按对应原型确认,不得全局写死为某一种容器。原型为独立页面时使用正式路由和页面布局;原型为居中弹框时使用模态弹框;只有原型明确为侧滑面板时才使用抽屉。不得因为已有公共组件或改动方便,把原型中的独立页面改成弹框、把弹框改成抽屉,或反向替换。 - 延续现有后台视觉体系:克制的企业级界面、清晰的信息层级和高密度但可扫描的数据展示。 - 路由级页面标题和说明统一由 `AppLayout` 顶部 `page-heading` 根据路由 `meta.title`、`meta.description` 展示。标准列表页正文不得重复渲染 `PageToolbar`、页面标题或说明,只保留查询区与表格区。`QueryToolbar` 只承载筛选项及其内置查询、重置按钮,不把新增、导入、导出或批量操作放入其 `actions` 插槽;重新执行查询即刷新当前结果,标准列表页任何位置都不得再提供独立刷新按钮。新增、导入、导出和批量操作统一放入 `DataTable` 的 `toolbar-actions` 插槽并靠表格工具栏右侧展示。仅没有查询组件的实时工作台或详情页在业务确实需要主动重载时,才可保留刷新;仅详情页或工作台在路由明确设置 `showPageHeading: false` 时,才可在正文使用独立详情头。 diff --git a/src/modules/container-loading/pages/ContainerDetailView.test.ts b/src/modules/container-loading/pages/ContainerDetailView.test.ts new file mode 100644 index 00000000..7e82ea31 --- /dev/null +++ b/src/modules/container-loading/pages/ContainerDetailView.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainerDetailView.vue'), 'utf8') + +describe('ContainerDetailView', () => { + it('keeps identity, status and key facts in a compact overview', () => { + expect(source).toContain('class="container-overview surface-panel"') + expect(source).toContain('class="container-overview__header"') + expect(source).toContain('class="container-overview__summary"') + expect(source).toContain('grid-template-columns:repeat(5,minmax(0,1fr))') + }) + + it('keeps real document, exception and authorized photo operations', () => { + expect(source).toContain('uploadContainerDocument(') + expect(source).toContain('downloadContainerDocument(') + expect(source).toContain('uploadContainerClearance(') + expect(source).toContain('completeContainerClearance(') + expect(source).toContain('markContainerException') + expect(source).toContain('cancelContainerException') + expect(source).toContain('downloadContainerPdaPhoto(photo.downloadPath)') + }) +}) diff --git a/src/modules/container-loading/pages/ContainerDetailView.vue b/src/modules/container-loading/pages/ContainerDetailView.vue new file mode 100644 index 00000000..7cce3ef7 --- /dev/null +++ b/src/modules/container-loading/pages/ContainerDetailView.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/src/modules/container-loading/pages/ContainersView.test.ts b/src/modules/container-loading/pages/ContainersView.test.ts index d4d0c005..9a59c66a 100644 --- a/src/modules/container-loading/pages/ContainersView.test.ts +++ b/src/modules/container-loading/pages/ContainersView.test.ts @@ -187,10 +187,11 @@ describe('ContainersView', () => { expect(source).not.toContain('@reset=') }) - it('uses centered dialogs for container detail, create and edit operations', () => { + it('opens container detail as an independent route while keeping create and edit dialogs', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') - expect(source).toContain(' { expect(source).not.toContain(' { + const routes = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/routes.ts'), 'utf8') + const detail = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainerDetailView.vue'), 'utf8') + expect(routes).toContain("path: 'container-loading/:containerId'") + expect(routes).toContain("name: 'container-detail'") + expect(routes).toContain("permission: 'platform:firstleg:container:list'") + expect(detail).toContain('getContainer(containerId.value)') + expect(detail).toContain('getContainerOverview(containerId.value)') + expect(detail).toContain(' { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') const labels = ['集装箱号', '柜次号', '装柜仓库', '提单号', '船名', '航次', '箱型(货柜类型)', '船公司', '起运港', '目的港', '目的仓', '货柜容量(m³)'] @@ -271,8 +286,9 @@ describe('ContainersView trucking supplier contract', () => { previous = current } expect(source).toContain("MISSING: '未上传', UPLOADED: '已上传', COMPLETED: '已完成'") - expect(source).toContain("detail.planNo || '—'") - expect(source).toContain("detail.exceptionActive ? '异常' : '正常'") + const detail = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainerDetailView.vue'), 'utf8') + expect(detail).toContain("detail.planNo || '—'") + expect(detail).toContain("detail.exceptionActive ? '异常' : '正常'") }) it('uses active TRUCKING suppliers without allowing free-text carrier names', () => { diff --git a/src/modules/container-loading/pages/ContainersView.vue b/src/modules/container-loading/pages/ContainersView.vue index 23fa291e..d33f3c48 100644 --- a/src/modules/container-loading/pages/ContainersView.vue +++ b/src/modules/container-loading/pages/ContainersView.vue @@ -425,24 +425,15 @@ async function loadRows() { } finally { if (sequence === listSequence) loading.value = false } } function search() { applied.value = { ...filters.value, sailingRange: [...filters.value.sailingRange] }; if (page.value === 1) void loadRows(); else page.value = 1 } -async function openDetail(source: object) { +function openDetail(source: object) { const row = source as ContainerBookingResource - detailTarget.value = row - detailOpen.value = true; detail.value = null; overview.value = null; detailError.value = null; overviewError.value = null; detailLoading.value = true - const sequence = ++detailSequence - try { const [result, aggregate] = await Promise.all([getContainer(row.id), getContainerOverview(row.id)]); if (sequence === detailSequence) { detail.value = result; overview.value = aggregate; clearanceDocument.value = null } } - catch (error) { if (sequence === detailSequence) detailError.value = getApiErrorDetails(error, '货柜详情加载失败') } - finally { if (sequence === detailSequence) detailLoading.value = false } + void router.push({ name: 'container-detail', params: { containerId: row.id } }) } function handleMoreAction(command: string, source: object) { const row = source as ContainerBookingResource if (command === 'link') { void openLink(row); return } - if (command === 'overview') { void openDetail(row); return } - detailTarget.value = row - void openDetail(row).then(() => { - if (command === 'exception') openExceptionDialog() - if (command === 'documents') openDocumentDialog('DELIVERY_ORDER') - }) + if (command === 'overview') { openDetail(row); return } + if (['exception', 'documents'].includes(command)) void router.push({ name: 'container-detail', params: { containerId: row.id }, query: { action: command } }) } function openDocumentDialog(type: string) { if (!['DELIVERY_ORDER', 'PICKUP_ORDER'].includes(type)) { @@ -607,11 +598,6 @@ onMounted(() => { void Promise.all([loadCustomers(), loadWarehouses(), loadBusin - - - - - 选择文件 选择文件 diff --git a/src/modules/container-loading/routes.ts b/src/modules/container-loading/routes.ts index 5c06b759..55c65d20 100644 --- a/src/modules/container-loading/routes.ts +++ b/src/modules/container-loading/routes.ts @@ -7,6 +7,12 @@ export const containerLoadingRoutes = [ component: () => import('./pages/ContainersView.vue'), meta: { title: '货柜与排柜', description: '维护货柜资料并进入智能排柜工作台', permission: 'platform:firstleg:container:list' }, }, + { + path: 'container-loading/:containerId', + name: 'container-detail', + component: () => import('./pages/ContainerDetailView.vue'), + meta: { title: '货柜详情', description: '查看货柜资料、履约轨迹、单证与异常', permission: 'platform:firstleg:container:list', showPageHeading: false }, + }, { path: 'container-loading/:containerId/smart-loading', name: 'smart-loading-workbench', diff --git a/src/modules/loading-lists/api/containerFees.test.ts b/src/modules/loading-lists/api/containerFees.test.ts index 765dfda5..3e572acd 100644 --- a/src/modules/loading-lists/api/containerFees.test.ts +++ b/src/modules/loading-lists/api/containerFees.test.ts @@ -10,10 +10,10 @@ describe('container fee API', () => { const values = { feeItemId: '7', direction: 'PAYABLE' as const, partnerId: '41', partnerName: '承运商A', businessDate: '2026-08-12', amount: 100, currency: 'CNY' as const, settlementIncluded: true, remark: null } await listContainerFees('9', '11'); const created = await createContainerFee('9', '11', values) await approveContainerFee('9', '11', '3'); await rejectContainerFee('9', '11', '3', '资料不全') - await adjustContainerFee('9', '11', '3', 2, values); await voidContainerFee('9', '11', '3', 3) + await adjustContainerFee('9', '11', '3', { expectedSourceVersion: 2, businessDate: '2026-08-13', amount: 120 }); await voidContainerFee('9', '11', '3', 3) expect(get).toHaveBeenCalledWith('/platform/v1/loading-plans/9/fee-details', { params: { accountId: '11' } }) expect(post).toHaveBeenCalledWith('/platform/v1/loading-plans/9/fee-details', values, { params: { accountId: '11' } }) - expect(post).toHaveBeenCalledWith('/platform/v1/loading-plans/9/fee-details/3/adjust', { expectedSourceVersion: 2, ...values }, { params: { accountId: '11' } }) + expect(post).toHaveBeenCalledWith('/platform/v1/loading-plans/9/fee-details/3/adjust', { expectedSourceVersion: 2, businessDate: '2026-08-13', amount: 120 }, { params: { accountId: '11' } }) expect(post).toHaveBeenCalledWith('/platform/v1/loading-plans/9/fee-details/3/void', { expectedSourceVersion: 3 }, { params: { accountId: '11' } }) expect(created).toMatchObject({ id: '3', loadingPlanId: '9', containerBookingId: '99', direction: 'PAYABLE', partnerId: '41', partnerName: '承运商A' }) }) diff --git a/src/modules/loading-lists/api/containerFees.ts b/src/modules/loading-lists/api/containerFees.ts index 77eaf434..0285aed3 100644 --- a/src/modules/loading-lists/api/containerFees.ts +++ b/src/modules/loading-lists/api/containerFees.ts @@ -38,6 +38,12 @@ export interface ContainerFeeSaveRequest { remark: string | null } +export interface ContainerFeeAdjustRequest { + expectedSourceVersion: number + businessDate: string + amount: number +} + export interface ApplicableFeeItem { id: string name: string @@ -109,8 +115,8 @@ export async function rejectContainerFee(planId: string, accountId: string, feeI await http.post(`${base(planId)}/${encodeURIComponent(feeId)}/reject`, { reason }, { params: params(accountId) }) } -export async function adjustContainerFee(planId: string, accountId: string, feeId: string, expectedSourceVersion: number, values: ContainerFeeSaveRequest) { - const response = await http.post>, ApiEnvelope>>(`${base(planId)}/${encodeURIComponent(feeId)}/adjust`, { expectedSourceVersion, ...values }, { params: params(accountId) }) +export async function adjustContainerFee(planId: string, accountId: string, feeId: string, payload: ContainerFeeAdjustRequest) { + const response = await http.post>, ApiEnvelope>>(`${base(planId)}/${encodeURIComponent(feeId)}/adjust`, payload, { params: params(accountId) }) return normalizeFee(unwrapApiResponse(response)) } diff --git a/src/modules/loading-lists/components/ContainerFeesPanel.test.ts b/src/modules/loading-lists/components/ContainerFeesPanel.test.ts index 4006609a..bdb5b309 100644 --- a/src/modules/loading-lists/components/ContainerFeesPanel.test.ts +++ b/src/modules/loading-lists/components/ContainerFeesPanel.test.ts @@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' -import { createContainerFee, listApplicableFeeItems, listContainerCostDetails, listContainerFeePartnerOptions, listContainerFees } from '../api/containerFees' +import { adjustContainerFee, createContainerFee, listApplicableFeeItems, listContainerCostDetails, listContainerFeePartnerOptions, listContainerFees } from '../api/containerFees' import ContainerFeesPanel from './ContainerFeesPanel.vue' vi.mock('../api/containerFees', async (importOriginal) => { @@ -32,6 +32,7 @@ beforeEach(() => { vi.mocked(listContainerFees).mockReset().mockResolvedValue([]); vi.mocked(listApplicableFeeItems).mockReset().mockResolvedValue([]) vi.mocked(listContainerFeePartnerOptions).mockReset().mockResolvedValue([]) vi.mocked(listContainerCostDetails).mockReset().mockResolvedValue([]); vi.mocked(createContainerFee).mockReset() + vi.mocked(adjustContainerFee).mockReset().mockResolvedValue({} as never) document.body.innerHTML = '' }) @@ -58,10 +59,22 @@ describe('ContainerFeesPanel contract', () => { expect(panel).toContain("row.reviewStatus === 'APPROVED'") expect(panel).toContain('adjustContainerFee') expect(panel).toContain('voidContainerFee') - expect(panel).toContain('调整后版本递增并重新进入待审核状态') + expect(panel).toContain('调整仅允许修改业务日期和金额') + expect(panel).toContain('请先作废后新增') expect(panel).toContain("row.partnerName || '服务端未返回'") }) + it('adjusts only business date and amount while preserving the approved snapshot', async () => { + const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit']) + const item = { id: '2', loadingPlanId: '9', containerBookingId: '99', containerNo: null, feeItemId: '7', feeItemName: '拖车费', direction: 'PAYABLE' as const, partnerId: '41', partnerName: '承运商A', businessDate: '2026-08-12', amount: 100, currency: 'CNY' as const, settlementIncluded: true, reviewStatus: 'APPROVED' as const, sourceStatus: 'ACTIVE' as const, sourceVersion: 3, remark: '原备注', reviewerName: null, reviewedAt: null, reviewReason: null } + const state = (wrapper.vm.$ as unknown as { setupState: { openAdjust(value: typeof item): void; save(): Promise; form: { businessDate: string; amount: number } } }).setupState + state.openAdjust(item); await flushPromises() + state.form.businessDate = '2026-08-13'; state.form.amount = 120 + await state.save() + expect(adjustContainerFee).toHaveBeenCalledWith('9', '11', '2', { expectedSourceVersion: 3, businessDate: '2026-08-13', amount: 120 }) + expect(wrapper.text()).toContain('已审核费用仍可调整日期与金额') + }) + it('shows only server allocations and supplier snapshots', () => { expect(panel).toContain('listContainerCostDetails') expect(panel).toContain('仅展示 Finance 已持久化结果') @@ -123,8 +136,7 @@ describe('ContainerFeesPanel contract', () => { { id: '2', loadingPlanId: '9', containerBookingId: '99', containerNo: null, feeItemId: '2', feeItemName: '已审费用', direction: 'PAYABLE', partnerId: '41', partnerName: '承运商A', businessDate: '2026-08-12', amount: 20, currency: 'CNY', settlementIncluded: true, reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE', sourceVersion: 1, remark: null, reviewerName: null, reviewedAt: null, reviewReason: null }, ]) const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view']) - expect(wrapper.text()).not.toContain('新增费用') - for (const label of ['通过', '驳回', '调整', '作废']) expect(wrapper.findAll('button').some((button) => button.text() === label)).toBe(false) + for (const label of ['新增费用', '通过', '驳回', '调整', '作废']) expect(wrapper.findAll('button').some((button) => button.text() === label)).toBe(false) }) it('keeps receivable usable when counterparty permission is missing', async () => { diff --git a/src/modules/loading-lists/components/ContainerFeesPanel.vue b/src/modules/loading-lists/components/ContainerFeesPanel.vue index 1a979cec..dcc04368 100644 --- a/src/modules/loading-lists/components/ContainerFeesPanel.vue +++ b/src/modules/loading-lists/components/ContainerFeesPanel.vue @@ -25,6 +25,7 @@ const partnersError = ref(null) const feeItemsLoading = ref(false) const partnersLoading = ref(false) const editorOpen = ref(false) +const editorError = ref(null) const rejectOpen = ref(false) const saving = ref(false) const target = ref(null) @@ -113,21 +114,38 @@ async function load() { finally { if (sequence === loadSequence) loading.value = false } } -function openCreate() { if (!canEdit.value || !canViewFeeItems.value || !accountId.value) return; resetForm(); editorOpen.value = true; void Promise.all([loadFeeItems(), loadPartners()]) } -function openAdjust(item: ContainerFeeResource) { if (!canEdit.value || !canViewFeeItems.value || !accountId.value) return; resetForm(item); editorOpen.value = true; void Promise.all([loadFeeItems(), loadPartners()]) } +function openCreate() { if (!canEdit.value || !canViewFeeItems.value || !accountId.value) return; resetForm(); editorError.value = null; editorOpen.value = true; void Promise.all([loadFeeItems(), loadPartners()]) } +function openAdjust(item: ContainerFeeResource) { if (!canEdit.value || !accountId.value) return; resetForm(item); editorError.value = null; editorOpen.value = true } async function save() { - if (!canEdit.value || !canViewFeeItems.value || !accountId.value) return - if (form.direction === 'PAYABLE' && (!canViewPartners.value || !form.partnerId)) { ElMessage.warning(canViewPartners.value ? '应付费用必须选择往来单位' : '当前账号缺少往来单位查看权限,无法保存应付费用'); return } + if (!canEdit.value || !accountId.value || (!target.value && !canViewFeeItems.value)) return + if (!target.value && form.direction === 'PAYABLE' && (!canViewPartners.value || !form.partnerId)) { ElMessage.warning(canViewPartners.value ? '应付费用必须选择往来单位' : '当前账号缺少往来单位查看权限,无法保存应付费用'); return } if (!await formRef.value?.validate().catch(() => false)) return + editorError.value = null + if (target.value) { + saving.value = true + try { + await adjustContainerFee(props.planId, accountId.value, target.value.id, { + expectedSourceVersion: target.value.sourceVersion, + businessDate: form.businessDate, + amount: form.amount, + }) + editorOpen.value = false; ElMessage.success('费用调整已提交审核'); await load() + } catch (cause) { + const details = getApiErrorDetails(cause, '费用调整失败') + editorError.value = details.code === 'A0409' + ? { ...details, message: `${details.message};当前输入已保留,请刷新费用明细后重试。` } + : details + } finally { saving.value = false } + return + } const partner = partners.value.find((item) => item.id === String(form.partnerId)) const payload: ContainerFeeSaveRequest = { ...form, partnerId: form.direction === 'PAYABLE' ? form.partnerId : null, partnerName: form.direction === 'PAYABLE' ? partner?.partnerName || null : null } if (form.direction === 'PAYABLE' && !payload.partnerName) { ElMessage.warning('请选择有效的往来单位'); return } saving.value = true try { - if (target.value) await adjustContainerFee(props.planId, accountId.value, target.value.id, target.value.sourceVersion, payload) - else await createContainerFee(props.planId, accountId.value, payload) - editorOpen.value = false; ElMessage.success(target.value ? '费用调整已提交审核' : '费用已新增'); await load() - } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '费用保存失败').message) } + await createContainerFee(props.planId, accountId.value, payload) + editorOpen.value = false; ElMessage.success('费用已新增'); await load() + } catch (cause) { editorError.value = getApiErrorDetails(cause, '费用保存失败') } finally { saving.value = false } } async function approve(item: ContainerFeeResource) { if (!canReview.value || !accountId.value) return; await approveContainerFee(props.planId, accountId.value, item.id).then(() => ElMessage.success('费用已审核通过')).then(load).catch((cause) => ElMessage.error(getApiErrorDetails(cause, '审核失败').message)) } @@ -162,7 +180,7 @@ onMounted(() => void load()) diff --git a/src/modules/loading-lists/components/LoadingListDetailDrawer.vue b/src/modules/loading-lists/components/LoadingListDetailDrawer.vue index 08fc500a..3b1db2f4 100644 --- a/src/modules/loading-lists/components/LoadingListDetailDrawer.vue +++ b/src/modules/loading-lists/components/LoadingListDetailDrawer.vue @@ -24,7 +24,7 @@ const props = defineProps<{ const emit = defineEmits<{ back: []; retry: []; edit: []; editWarehouseRemark: [item: LoadingItem]; confirmPreload: []; attachCustoms: []; completeCustoms: [] }>() -const actionColumn: DataTableColumn = { key: 'actions', label: '操作', fixed: 'left', width: 106, slotName: 'actions', hideable: false, movable: false } +const actionColumn: DataTableColumn = { key: 'actions', label: '操作', fixed: 'left', width: 132, slotName: 'actions', hideable: false, movable: false } const baseItemColumns: DataTableColumn[] = [ { key: 'sequence', prop: 'loadSequence', label: '装柜顺序', width: 92 }, { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 170, className: 'identifier-column', showOverflowTooltip: true }, @@ -113,7 +113,7 @@ function formatNumber(value?: number | null, digits = 3) {