Release/test 合并上生产 #2
@@ -82,6 +82,14 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
expect(page).toContain('white-space: nowrap')
|
||||
})
|
||||
|
||||
it('keeps account detail and term editing readable across the 1920 workspace', () => {
|
||||
expect(page).toContain('width: min(100%, 1920px)')
|
||||
expect(page).toContain('@media (min-width: 1920px)')
|
||||
expect(page).toContain('.finance-account-detail__layout :deep(.detail-page-layout__content)')
|
||||
expect(page).toContain('padding: 0 14px 10px')
|
||||
expect(page).not.toContain('max-width: 520px')
|
||||
})
|
||||
|
||||
it('shows only derived CNY summaries and no wallet actions', () => {
|
||||
for (const marker of ['未收', '待出账', '本期已收', '应付', 'CNY']) expect(page).toContain(marker)
|
||||
for (const forbidden of ['手动充值', '钱包', '外币', '发票', '利润']) expect(page).not.toContain(forbidden)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import ElementPlus from 'element-plus'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
@@ -189,6 +191,15 @@ describe('FinanceCustomerArapView G2 operations', () => {
|
||||
expect(wrapper.find('.arap-information').text()).not.toContain('T00:00:00')
|
||||
})
|
||||
|
||||
it('keeps the fee-source detail readable across the 1920 workspace', () => {
|
||||
const page = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerArapView.vue'), 'utf8')
|
||||
expect(page).toContain('width: min(100%, 1920px)')
|
||||
expect(page).toContain('@media (min-width: 1920px)')
|
||||
expect(page).toContain('grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr)')
|
||||
expect(page).toContain('.finance-arap-detail__layout :deep(.detail-page-layout__content)')
|
||||
expect(page).not.toContain('max-width: 520px')
|
||||
})
|
||||
|
||||
it('resolves blank detail customer names and keeps source labels business-facing', async () => {
|
||||
vi.mocked(getFinanceFeeSource).mockResolvedValueOnce({ ...detail, customerName: '', sourceType: 'NEW_SOURCE' })
|
||||
const wrapper = await mountView(['platform:finance:customer-arap:list'], '/finance/customer-arap/42')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import ElementPlus from 'element-plus'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
@@ -99,12 +101,20 @@ describe('FinanceCustomerStatementsView', () => {
|
||||
expect(document.querySelector('[aria-label="客户对账单列表"]')).toBeNull()
|
||||
expect(document.querySelector('[aria-label="所选对账单联动明细"]')).toBeNull()
|
||||
expect(document.querySelector('[aria-label="对账单摘要"]')?.textContent).toContain('200.00 CNY')
|
||||
expect(document.querySelectorAll('.statement-summary-grid .statement-summary-item')).toHaveLength(11)
|
||||
expect(document.querySelector('.statement-detail-head')?.textContent).toContain('REC-501')
|
||||
expect(document.querySelector('[aria-label="客户对账单账单明细"]')).toBeTruthy()
|
||||
expect(document.querySelector('.detail-table-scroll [aria-label="客户对账单账单明细"]')).toBeTruthy()
|
||||
expect(document.querySelector('.el-drawer')).toBeNull()
|
||||
})
|
||||
|
||||
it('uses the prototype-style wide summary grid at 1920px', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerStatementsView.vue'), 'utf8')
|
||||
expect(source).toContain('grid-template-columns: repeat(auto-fit, minmax(220px, 1fr))')
|
||||
expect(source).toContain('grid-template-columns: repeat(5, minmax(0, 1fr))')
|
||||
expect(source).not.toContain('<el-descriptions :column="3" border>')
|
||||
})
|
||||
|
||||
it('keeps the payment history in the desktop detail layout without duplicating a card container', async () => {
|
||||
const { wrapper } = await mountPage()
|
||||
const vm = wrapper.vm as unknown as { openDetail(row: FinanceStatementReadModel): Promise<void>; activeDetailTab: 'lines' | 'payments' }
|
||||
|
||||
@@ -456,19 +456,19 @@ watch(detailRouteId, (statementId) => {
|
||||
<el-tag :type="financeStatusTone(detail.status)">{{ financeStatusLabel(detail.status) }}</el-tag>
|
||||
</div>
|
||||
<FormSection class="statement-summary-section" title="对账单概要" aria-label="对账单摘要">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="总应收金额">{{ detail.totalAmount.toFixed(2) }} CNY</el-descriptions-item>
|
||||
<el-descriptions-item label="已收金额">{{ detail.paidAmount.toFixed(2) }} CNY</el-descriptions-item>
|
||||
<el-descriptions-item label="未收金额"><strong class="statement-outstanding">{{ detail.outstandingAmount.toFixed(2) }} CNY</strong></el-descriptions-item>
|
||||
<el-descriptions-item label="账期">{{ detail.creditDays }} 天</el-descriptions-item>
|
||||
<el-descriptions-item label="货币">{{ detail.currency }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否结清">{{ detail.settled ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人">{{ operatorDisplayName(detail.createdByName) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDateTime(detail.createdAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新人">{{ operatorDisplayName(detail.updatedByName) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDateTime(detail.updatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="statement-summary-grid">
|
||||
<div class="statement-summary-item"><span>总应收金额</span><strong>{{ detail.totalAmount.toFixed(2) }} CNY</strong></div>
|
||||
<div class="statement-summary-item"><span>已收金额</span><strong>{{ detail.paidAmount.toFixed(2) }} CNY</strong></div>
|
||||
<div class="statement-summary-item"><span>未收金额</span><strong class="statement-outstanding">{{ detail.outstandingAmount.toFixed(2) }} CNY</strong></div>
|
||||
<div class="statement-summary-item"><span>账期</span><strong>{{ detail.creditDays }} 天</strong></div>
|
||||
<div class="statement-summary-item"><span>货币</span><strong>{{ detail.currency }}</strong></div>
|
||||
<div class="statement-summary-item"><span>是否结清</span><strong>{{ detail.settled ? '是' : '否' }}</strong></div>
|
||||
<div class="statement-summary-item"><span>创建人</span><strong>{{ operatorDisplayName(detail.createdByName) }}</strong></div>
|
||||
<div class="statement-summary-item"><span>创建时间</span><strong>{{ formatDateTime(detail.createdAt) }}</strong></div>
|
||||
<div class="statement-summary-item"><span>更新人</span><strong>{{ operatorDisplayName(detail.updatedByName) }}</strong></div>
|
||||
<div class="statement-summary-item"><span>更新时间</span><strong>{{ formatDateTime(detail.updatedAt) }}</strong></div>
|
||||
<div class="statement-summary-item statement-summary-item--remark"><span>备注</span><strong>{{ detail.remark || '—' }}</strong></div>
|
||||
</div>
|
||||
</FormSection>
|
||||
</template>
|
||||
<el-tabs v-model="activeDetailTab" class="statement-detail-tabs">
|
||||
@@ -566,6 +566,12 @@ watch(detailRouteId, (statementId) => {
|
||||
.statement-detail-head h2 { margin: 2px 0 0; font-size: 16px; line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.statement-detail-head p { margin: 5px 0 0; color: var(--cc-text-secondary); font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
|
||||
.statement-summary-section { padding: 10px 16px; border-bottom: 1px solid var(--cc-border-subtle); }
|
||||
.statement-summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px 24px; }
|
||||
.statement-summary-item { min-width: 0; }
|
||||
.statement-summary-item span, .statement-summary-item strong { display: block; overflow-wrap: anywhere; }
|
||||
.statement-summary-item span { margin-bottom: 3px; color: var(--cc-text-secondary); font-size: 12px; }
|
||||
.statement-summary-item strong { color: var(--cc-text-primary); font-size: 14px; line-height: 1.45; }
|
||||
.statement-summary-item--remark { grid-column: span 2; }
|
||||
.statement-outstanding { color: var(--el-color-warning-dark-2); }
|
||||
.statement-detail-tabs { min-width: 0; }
|
||||
.statement-detail-table { min-width: 0; }
|
||||
@@ -601,6 +607,15 @@ watch(detailRouteId, (statementId) => {
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.statement-summary-grid {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px 24px;
|
||||
}
|
||||
|
||||
.statement-summary-item--remark {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.statement-detail-tabs :deep(.el-tabs__header) {
|
||||
padding: 0 14px;
|
||||
}
|
||||
@@ -624,5 +639,7 @@ watch(detailRouteId, (statementId) => {
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.statement-detail-page__header { align-items: stretch; flex-direction: column; }
|
||||
.statement-summary-grid { grid-template-columns: 1fr; }
|
||||
.statement-summary-item--remark { grid-column: auto; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,4 +9,19 @@ const settlementId = computed(() => String(route.params.settlementId || ''))
|
||||
function openDetail(id: string) { void router.push({ name: 'finance-settlement-payable-detail', params: { settlementId: id } }) }
|
||||
function back() { void router.push({ name: 'finance-settlement-payables' }) }
|
||||
</script>
|
||||
<template><FinanceFormalSettlementPage kind="payable" :settlement-id="settlementId" @open-detail="openDetail" @back="back" /></template>
|
||||
<template><FinanceFormalSettlementPage class="finance-settlement-route finance-settlement-route--payable" kind="payable" :settlement-id="settlementId" @open-detail="openDetail" @back="back" /></template>
|
||||
|
||||
<style scoped>
|
||||
@media (min-width: 1920px) {
|
||||
.finance-settlement-route :deep(.settlement-detail-page) { width: 100%; max-width: none; }
|
||||
.finance-settlement-route :deep(.settlement-detail-info .el-descriptions__table > tbody) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(110px, .35fr) minmax(0, 1fr) minmax(110px, .35fr) minmax(0, 1fr);
|
||||
column-gap: 20px;
|
||||
row-gap: 10px;
|
||||
}
|
||||
.finance-settlement-route :deep(.settlement-detail-info .el-descriptions__table > tbody > tr) { display: contents; }
|
||||
.finance-settlement-route :deep(.settlement-detail-info .el-descriptions__label) { min-width: 0; white-space: nowrap; }
|
||||
.finance-settlement-route :deep(.settlement-detail-info .el-descriptions__content) { min-width: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,4 +9,19 @@ const settlementId = computed(() => String(route.params.settlementId || ''))
|
||||
function openDetail(id: string) { void router.push({ name: 'finance-settlement-receivable-detail', params: { settlementId: id } }) }
|
||||
function back() { void router.push({ name: 'finance-settlement-receivables' }) }
|
||||
</script>
|
||||
<template><FinanceFormalSettlementPage kind="receivable" :settlement-id="settlementId" @open-detail="openDetail" @back="back" /></template>
|
||||
<template><FinanceFormalSettlementPage class="finance-settlement-route finance-settlement-route--receivable" kind="receivable" :settlement-id="settlementId" @open-detail="openDetail" @back="back" /></template>
|
||||
|
||||
<style scoped>
|
||||
@media (min-width: 1920px) {
|
||||
.finance-settlement-route :deep(.settlement-detail-page) { width: 100%; max-width: none; }
|
||||
.finance-settlement-route :deep(.settlement-detail-info .el-descriptions__table > tbody) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(110px, .35fr) minmax(0, 1fr) minmax(110px, .35fr) minmax(0, 1fr);
|
||||
column-gap: 20px;
|
||||
row-gap: 10px;
|
||||
}
|
||||
.finance-settlement-route :deep(.settlement-detail-info .el-descriptions__table > tbody > tr) { display: contents; }
|
||||
.finance-settlement-route :deep(.settlement-detail-info .el-descriptions__label) { min-width: 0; white-space: nowrap; }
|
||||
.finance-settlement-route :deep(.settlement-detail-info .el-descriptions__content) { min-width: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const receivableSource = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceSettlementReceivablesView.vue'), 'utf8')
|
||||
const payableSource = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceSettlementPayablesView.vue'), 'utf8')
|
||||
|
||||
describe('Finance settlement route 1920px layout', () => {
|
||||
it.each([
|
||||
['应收', receivableSource, 'finance-settlement-route--receivable'],
|
||||
['应付', payableSource, 'finance-settlement-route--payable'],
|
||||
])('uses the full detail workspace and a readable two-field information grid for %s', (_label, source, routeClass) => {
|
||||
expect(source).toContain(routeClass)
|
||||
expect(source).toContain('@media (min-width: 1920px)')
|
||||
expect(source).toContain('.finance-settlement-route :deep(.settlement-detail-page) { width: 100%; max-width: none; }')
|
||||
expect(source).toContain('grid-template-columns: minmax(110px, .35fr) minmax(0, 1fr) minmax(110px, .35fr) minmax(0, 1fr)')
|
||||
expect(source).toContain('> tbody > tr) { display: contents; }')
|
||||
})
|
||||
})
|
||||
@@ -95,6 +95,31 @@ describe('InboundTaskList measurement scope', () => {
|
||||
expect(source).not.toContain("name: 'inbound-order-detail', params: { inboundId: row.id }")
|
||||
})
|
||||
|
||||
it('shows authoritative measurement totals with operation first and no internal IDs as primary columns', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
const columnsSource = source.slice(source.indexOf('const columns'), source.indexOf('function text'))
|
||||
|
||||
expect(columnsSource.indexOf("key: 'actions'")).toBeLessThan(columnsSource.indexOf("key: 'inboundCode'"))
|
||||
expect(columnsSource).toContain("key: 'customerName', label: '客户'")
|
||||
expect(columnsSource).toContain("key: 'channelName', label: '渠道'")
|
||||
expect(columnsSource).toContain("key: 'totalPackageCount', label: '总箱数'")
|
||||
expect(columnsSource).toContain("key: 'palletCount', label: '托盘数'")
|
||||
expect(columnsSource).toContain("key: 'actualGrossWeightKg', label: '实测毛重(kg)'")
|
||||
expect(columnsSource).toContain("key: 'actualNetWeightKg', label: '货物重量(kg)'")
|
||||
expect(columnsSource).toContain("key: 'actualVolumeCbm', label: '实测体积(m³)'")
|
||||
expect(columnsSource).toContain("key: 'measureCompletedAt', label: '提交时间'")
|
||||
expect(columnsSource).not.toContain("key: 'id'")
|
||||
expect(columnsSource).not.toContain("key: 'orderId'")
|
||||
})
|
||||
|
||||
it('renders missing list facts as an explicit dash instead of guessing values', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
|
||||
expect(source).toContain("value === null || value === undefined || value === '' ? '—'")
|
||||
expect(source).toContain("value === null || value === undefined ? '—'")
|
||||
expect(source).toContain("if (!value) return '—'")
|
||||
})
|
||||
|
||||
it('refreshes cached task lists when users return from measurement work', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
|
||||
|
||||
@@ -93,9 +93,11 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'businessType', label: '业务类型', minWidth: 120, slotName: 'businessType' },
|
||||
{ key: 'channelName', label: '渠道', minWidth: 140, slotName: 'channelName' },
|
||||
{ key: 'totalPackageCount', label: '总箱数', width: 92, slotName: 'totalPackageCount' },
|
||||
{ key: 'palletCount', label: '托盘数', width: 92, slotName: 'palletCount' },
|
||||
{ key: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 128, slotName: 'actualGrossWeightKg' },
|
||||
{ key: 'actualNetWeightKg', label: '货物重量(kg)', minWidth: 128, slotName: 'actualNetWeightKg' },
|
||||
{ key: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 128, slotName: 'actualVolumeCbm' },
|
||||
{ key: 'measureCompletedAt', label: '测量完成时间', minWidth: 164, slotName: 'measureCompletedAt' },
|
||||
{ key: 'measureCompletedAt', label: '提交时间', minWidth: 164, slotName: 'measureCompletedAt' },
|
||||
])
|
||||
|
||||
function text(value: unknown) {
|
||||
@@ -334,7 +336,9 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
<template #businessType="{ row }">{{ text((row as InboundOrderSummary).businessType) }}</template>
|
||||
<template #channelName="{ row }">{{ text((row as InboundOrderSummary).channelName || (row as InboundOrderSummary).channelCode) }}</template>
|
||||
<template #totalPackageCount="{ row }">{{ text((row as InboundOrderSummary).totalPackageCount) }}</template>
|
||||
<template #palletCount="{ row }">{{ text((row as InboundOrderSummary).palletCount) }}</template>
|
||||
<template #actualGrossWeightKg="{ row }">{{ formatNumber((row as InboundOrderSummary).actualGrossWeightKg) }}</template>
|
||||
<template #actualNetWeightKg="{ row }">{{ formatNumber((row as InboundOrderSummary).actualNetWeightKg) }}</template>
|
||||
<template #actualVolumeCbm="{ row }">{{ formatNumber((row as InboundOrderSummary).actualVolumeCbm) }}</template>
|
||||
<template #measureCompletedAt="{ row }">{{ formatDate((row as InboundOrderSummary).measureCompletedAt) }}</template>
|
||||
<template #empty><el-empty :description="emptyText" /></template>
|
||||
|
||||
@@ -30,7 +30,11 @@ describe('platform inbound query API', () => {
|
||||
it('uses the implemented non-v1 list path and normalizes page values and IDs', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({
|
||||
code: '200', message: 'success', data: {
|
||||
records: [{ id: 19001, tenantId: 10002, orderId: 18001, customerId: 10001, totalPackageCount: '6', holdFlag: 'false' }],
|
||||
records: [{
|
||||
id: 19001, tenantId: 10002, orderId: 18001, customerId: 10001,
|
||||
totalPackageCount: '6', palletCount: '2', actualGrossWeightKg: '21.5',
|
||||
actualNetWeightKg: '18.25', actualVolumeCbm: '0.456', holdFlag: 'false',
|
||||
}],
|
||||
page: '1', size: '20', total: '1', pages: '1',
|
||||
},
|
||||
})
|
||||
@@ -38,7 +42,11 @@ describe('platform inbound query API', () => {
|
||||
await expect(listPlatformInbounds('10001', {
|
||||
page: 1, size: 20, tab: 'WAIT_MEASURE', inboundCode: 'RK', holdFlag: false,
|
||||
})).resolves.toMatchObject({
|
||||
records: [{ id: '19001', tenantId: '10002', orderId: '18001', customerId: '10001', totalPackageCount: 6, holdFlag: false }],
|
||||
records: [{
|
||||
id: '19001', tenantId: '10002', orderId: '18001', customerId: '10001',
|
||||
totalPackageCount: 6, palletCount: 2, actualGrossWeightKg: 21.5,
|
||||
actualNetWeightKg: 18.25, actualVolumeCbm: 0.456, holdFlag: false,
|
||||
}],
|
||||
total: 1, pages: 1,
|
||||
})
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/inbounds', {
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('InboundFeePanel', () => {
|
||||
|
||||
expect(getLatestInboundFee).toHaveBeenCalledWith('10001', '88')
|
||||
expect(document.body.textContent).not.toContain('租户')
|
||||
expect(document.body.textContent).toContain('对客费用总额 · CNY')
|
||||
expect(document.body.textContent).toContain('头程自动费用 · CNY')
|
||||
expect(document.body.textContent).toContain('¥1,235.60')
|
||||
expect(document.body.textContent).toContain('基础运费小计 ¥1,135.60')
|
||||
expect(document.body.textContent).toContain('附加费 ¥100.00')
|
||||
@@ -130,8 +130,10 @@ describe('InboundFeePanel', () => {
|
||||
|
||||
const content = wrapper.find('.fee-content').text()
|
||||
expect(wrapper.findAll('.fee-summary > div > span').map((node) => node.text())).toEqual([
|
||||
'对客费用总额 · CNY',
|
||||
'头程自动费用 · CNY',
|
||||
'最终计费量',
|
||||
'单价',
|
||||
'附加费',
|
||||
'快照版本',
|
||||
])
|
||||
expect(wrapper.findAll('.calculation-flow > div > span').map((node) => node.text())).toEqual([
|
||||
@@ -169,12 +171,13 @@ describe('InboundFeePanel', () => {
|
||||
expect(document.querySelector('[role="alert"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows adjustment only for authorized published inbound fees', async () => {
|
||||
it('shows adjustment only for authorized calculated inbound fees', async () => {
|
||||
mountPanel(['firstleg:fba-pricing:view'])
|
||||
await flushPromises()
|
||||
expect(document.querySelector('[data-testid="adjust-fee-button"]')).toBeNull()
|
||||
|
||||
document.body.innerHTML = ''
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
mountPanel()
|
||||
await flushPromises()
|
||||
expect(document.querySelector('[data-testid="adjust-fee-button"]')).not.toBeNull()
|
||||
@@ -189,6 +192,7 @@ describe('InboundFeePanel', () => {
|
||||
})
|
||||
|
||||
it('submits a reasoned adjustment and emits the refreshed snapshot', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
;(document.querySelector('[data-testid="adjust-fee-button"]') as HTMLButtonElement).click()
|
||||
@@ -233,6 +237,7 @@ describe('InboundFeePanel', () => {
|
||||
})
|
||||
|
||||
it('refreshes the latest snapshot after a conflict while retaining the dialog', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
vi.mocked(adjustPublishedInboundFee).mockRejectedValue(
|
||||
new ApiResponseError('费用已被其他操作员修改,请刷新后重试', 'A0409', 409),
|
||||
)
|
||||
|
||||
@@ -22,10 +22,12 @@ const props = defineProps<{
|
||||
customerId?: string
|
||||
tenantId?: string
|
||||
inboundId: string
|
||||
compact?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
changed: [snapshot: InboundFeeSnapshot]
|
||||
loaded: [snapshot: InboundFeeSnapshot | null]
|
||||
}>()
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -64,7 +66,7 @@ const canView = computed(() => access.can('firstleg:fba-pricing:view'))
|
||||
const canAdjust = computed(() => access.can('firstleg:fba-pricing:adjust'))
|
||||
const customerContextId = computed(() => props.customerId || props.tenantId || '')
|
||||
const adjustmentEligible = computed(() => (
|
||||
['CALCULATED', 'CONFIRMED', 'SENT'].includes(snapshot.value?.feeStatus || '')
|
||||
snapshot.value?.feeStatus === 'CALCULATED'
|
||||
&& snapshot.value?.nextProcessStatus === 'WAIT_SPLIT'
|
||||
))
|
||||
const chargeModeLabel = computed(() => snapshot.value?.chargeMode === 'KG' ? '按重量计费' : '按体积计费')
|
||||
@@ -133,10 +135,12 @@ async function loadFee(options: { preserveAdjustment?: boolean } = {}) {
|
||||
const result = await getLatestInboundFee(customerContextId.value, props.inboundId)
|
||||
if (sequence !== loadSequence) return
|
||||
snapshot.value = result
|
||||
emit('loaded', result)
|
||||
if (!options.preserveAdjustment && adjustOpen.value) fillAdjustmentForm(result)
|
||||
} catch (error) {
|
||||
if (sequence !== loadSequence) return
|
||||
snapshot.value = null
|
||||
emit('loaded', null)
|
||||
if (error instanceof ApiResponseError && error.code === 'A0404') notFound.value = true
|
||||
else loadError.value = getApiErrorDetails(error, '费用快照加载失败,请稍后重试')
|
||||
} finally {
|
||||
@@ -228,8 +232,8 @@ defineExpose({ reload: loadFee })
|
||||
|
||||
<template>
|
||||
<section class="inbound-fee-panel" :aria-busy="loading" aria-labelledby="inbound-fee-title">
|
||||
<header class="fee-heading">
|
||||
<div class="fee-heading-copy">
|
||||
<header class="fee-heading" :class="{ 'fee-heading--compact': compact }">
|
||||
<div v-if="!compact" class="fee-heading-copy">
|
||||
<span class="fee-heading-icon" aria-hidden="true"><CircleDollarSign :size="20" /></span>
|
||||
<div>
|
||||
<h3 id="inbound-fee-title">入库费用快照</h3>
|
||||
@@ -279,7 +283,7 @@ defineExpose({ reload: loadFee })
|
||||
<div v-else-if="snapshot" class="fee-content">
|
||||
<div class="fee-summary" aria-label="费用关键结果">
|
||||
<div class="fee-total">
|
||||
<span>对客费用总额 · {{ snapshot.currency }}</span>
|
||||
<span>头程自动费用 · {{ snapshot.currency }}</span>
|
||||
<strong>{{ formatMoney(snapshot.totalAmount, snapshot.currency) }}</strong>
|
||||
<small>基础运费小计 {{ formatMoney(snapshot.subtotalAmount, snapshot.currency) }} · 附加费 {{ formatMoney(snapshot.additionalAmount, snapshot.currency) }}</small>
|
||||
</div>
|
||||
@@ -288,6 +292,16 @@ defineExpose({ reload: loadFee })
|
||||
<strong>{{ formatNumber(snapshot.chargeableQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong>
|
||||
<small>{{ chargeModeLabel }}</small>
|
||||
</div>
|
||||
<div class="fee-summary-item">
|
||||
<span>单价</span>
|
||||
<strong>{{ formatMoney(snapshot.unitPrice, snapshot.currency) }} / {{ chargeUnitLabel }}</strong>
|
||||
<small>{{ snapshot.channelCode }}</small>
|
||||
</div>
|
||||
<div class="fee-summary-item">
|
||||
<span>附加费</span>
|
||||
<strong>{{ formatMoney(snapshot.additionalAmount, snapshot.currency) }}</strong>
|
||||
<small>{{ snapshot.additionalAmount ? '已计入总额' : '无附加费' }}</small>
|
||||
</div>
|
||||
<div class="fee-summary-item">
|
||||
<span>快照版本</span>
|
||||
<div class="version-status"><strong>V{{ snapshot.feeVersion }}</strong><el-tag :type="statusInfo.type" effect="light">{{ statusInfo.label }}</el-tag></div>
|
||||
@@ -297,7 +311,7 @@ defineExpose({ reload: loadFee })
|
||||
|
||||
<section class="calculation-section" aria-labelledby="calculation-title">
|
||||
<div class="section-title">
|
||||
<div><h4 id="calculation-title">计费过程</h4><p>实际量与折算量取大后计费,金额以服务端快照为准。</p></div>
|
||||
<div><h4 id="calculation-title">计费依据</h4><p v-if="!compact">实际量与折算量取大后计费,金额以服务端快照为准。</p></div>
|
||||
<el-tag effect="plain">{{ taxModeLabel(snapshot.taxMode) }}</el-tag>
|
||||
</div>
|
||||
<div class="calculation-flow">
|
||||
@@ -397,6 +411,7 @@ defineExpose({ reload: loadFee })
|
||||
<style scoped>
|
||||
.inbound-fee-panel { min-width: 0; color: var(--cc-text); }
|
||||
.fee-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 16px; }
|
||||
.fee-heading--compact { justify-content: flex-end; padding-bottom: 6px; }
|
||||
.fee-heading-copy { display: flex; min-width: 0; align-items: flex-start; gap: 12px; }
|
||||
.fee-heading-icon { display: grid; width: 38px; height: 38px; flex: 0 0 38px; place-items: center; border-radius: 10px; background: #eef4ff; color: var(--cc-primary); }
|
||||
.fee-heading h3, .section-title h4 { margin: 0; }
|
||||
@@ -409,24 +424,26 @@ defineExpose({ reload: loadFee })
|
||||
.inbound-fee-panel :deep(.el-alert .el-button) { margin-top: 8px; }
|
||||
.empty-note { margin: 0; color: var(--cc-muted); font-size: 12px; }
|
||||
.fee-content { border-top: 1px solid var(--cc-border); }
|
||||
.fee-summary { display: grid; grid-template-columns: minmax(240px, 1.25fr) repeat(2, minmax(160px, 1fr)); border-bottom: 1px solid var(--cc-border); }
|
||||
.fee-summary { display: grid; grid-template-columns: minmax(230px, 1.3fr) repeat(4, minmax(135px, 1fr)); border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.fee-total, .fee-summary-item { display: flex; min-width: 0; flex-direction: column; justify-content: center; gap: 3px; padding: 11px 14px; }
|
||||
.fee-summary-item { border-left: 1px solid var(--cc-border); }
|
||||
.fee-summary span, .fee-summary small { color: var(--cc-muted); font-size: 11px; }
|
||||
.fee-summary strong { font-size: 18px; line-height: 1.25; overflow-wrap: anywhere; }
|
||||
.fee-total strong { color: #0f5c49; font-size: 21px; letter-spacing: -.01em; }
|
||||
.fee-summary-item strong { font-size: 15px; }
|
||||
.version-status { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.calculation-section, .fee-timeline { padding: 14px 0; border-bottom: 1px solid var(--cc-border); }
|
||||
.calculation-section, .fee-timeline { padding: 10px 0; border-bottom: 1px solid var(--cc-border); }
|
||||
.section-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||||
.section-title h4 { font-size: 14px; }
|
||||
.calculation-flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 10px; border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.calculation-flow > div { display: flex; min-width: 0; min-height: 58px; flex-direction: column; justify-content: center; gap: 3px; padding: 9px 12px; border-right: 1px solid var(--cc-border); }
|
||||
.calculation-flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 7px; border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.calculation-flow > div { display: flex; min-width: 0; min-height: 46px; flex-direction: row; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; border-right: 1px solid var(--cc-border); }
|
||||
.calculation-flow > div:last-child { border-right: 0; }
|
||||
.calculation-flow span, .calculation-flow small { color: var(--cc-muted); font-size: 11px; }
|
||||
.calculation-flow strong { font-size: 15px; overflow-wrap: anywhere; }
|
||||
.calculation-flow strong { font-size: 13px; overflow-wrap: anywhere; }
|
||||
.calculation-flow small { display: none; }
|
||||
.calculation-flow .is-chargeable { background: #f7fbfa; }
|
||||
.calculation-flow .is-chargeable strong { color: #087d70; }
|
||||
.amount-equation { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; padding: 7px 0; color: #475467; font-size: 12px; }
|
||||
.amount-equation { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-top: 5px; padding: 4px 0 0; color: #475467; font-size: 12px; }
|
||||
.amount-equation i { color: #98a2b3; font-style: normal; }
|
||||
.amount-equation strong { color: var(--cc-text); font-size: 15px; }
|
||||
.fee-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-bottom: 1px solid var(--cc-border); }
|
||||
|
||||
@@ -65,6 +65,7 @@ describe('LoadingListsView', () => {
|
||||
expect(preloadDetailSource).toContain('router.back()')
|
||||
expect(confirmedDetailSource).toContain('返回列表')
|
||||
expect(confirmedDetailSource).toContain('router.back()')
|
||||
expect(confirmedDetailSource).toContain('@media (min-width: 1920px) { .confirmed-loading-detail-page { width: 100%; max-width: none; gap: 12px; } }')
|
||||
expect(routesSource).toContain("path: 'loading-lists/:planId'")
|
||||
expect(routesSource).toContain("name: 'loading-list-detail'")
|
||||
})
|
||||
|
||||
@@ -181,4 +181,5 @@ onMounted(() => void loadDetail())
|
||||
<style scoped>
|
||||
.confirmed-loading-detail-page { display: grid; min-width: 0; max-width: 1280px; gap: 16px; }
|
||||
.back-button { width: fit-content; }
|
||||
@media (min-width: 1920px) { .confirmed-loading-detail-page { width: 100%; max-width: none; gap: 12px; } }
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('PreloadPlanDetailView prototype contract', () => {
|
||||
expect(source).toContain('返回列表')
|
||||
expect(source).toContain('<ArrowLeft')
|
||||
expect(source).toContain("router.push({ name: 'loading-plans' })")
|
||||
expect(source).toContain('@media (min-width: 1920px) { .preload-detail-page { width: 100%; max-width: none; gap: 12px; } }')
|
||||
expect(source).not.toContain('AppDrawer')
|
||||
})
|
||||
|
||||
|
||||
@@ -177,5 +177,6 @@ onMounted(() => void loadDetail())
|
||||
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
|
||||
.confirm-content { display: grid; gap: 16px; }
|
||||
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
|
||||
@media (min-width: 1920px) { .preload-detail-page { width: 100%; max-width: none; gap: 12px; } }
|
||||
@media (max-width: 720px) { .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } }
|
||||
</style>
|
||||
|
||||
@@ -70,11 +70,11 @@ describe('unified orders API contract', () => {
|
||||
|
||||
it('normalizes C02 row IDs and keeps inboundId for measurement routing', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ data: {
|
||||
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', recommendedPalletType: 'STANDARD', locationCodes: 'A0-01, B2-03', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }],
|
||||
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', recommendedPalletType: 'STANDARD', locationCodes: 'A0-01, B2-03', actualGrossWeightKg: '12.5', actualVolumeCbm: '0.45', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }],
|
||||
page: 1, size: 20, total: 1, pages: 1,
|
||||
} })
|
||||
await expect(listPlatformUnifiedOrders({ page: 1, size: 20 })).resolves.toMatchObject({
|
||||
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, recommendedPalletType: 'STANDARD', locationCodes: 'A0-01, B2-03', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }],
|
||||
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, recommendedPalletType: 'STANDARD', locationCodes: 'A0-01, B2-03', actualGrossWeightKg: 12.5, actualVolumeCbm: 0.45, customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ export interface UnifiedOrderResource {
|
||||
inboundCode: string | null
|
||||
expectedShipDate: string | null
|
||||
totalPackageCount: number | null
|
||||
recommendedPalletType: string | null
|
||||
locationCodes: string | null
|
||||
actualGrossWeightKg: number | null
|
||||
actualVolumeCbm: number | null
|
||||
measurement: UnifiedOrderMeasurementSummary | null
|
||||
preload: UnifiedOrderPreloadSummary | null
|
||||
loading: UnifiedOrderLoadingSummary | null
|
||||
@@ -112,8 +116,6 @@ export interface UnifiedOrderResource {
|
||||
taxMode?: string | null
|
||||
fullContainer?: boolean | null
|
||||
customsInspection?: boolean | null
|
||||
recommendedPalletType?: string | null
|
||||
locationCodes?: string | null
|
||||
/** Legacy aliases retained while older order responses are still deployed. */
|
||||
totalPalletCount?: number | null
|
||||
chargeableVolumeCbm?: number | null
|
||||
@@ -334,6 +336,8 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource {
|
||||
childCount: integer(raw.relation.childCount, 0),
|
||||
} : null,
|
||||
totalPackageCount: nullableInteger(raw.totalPackageCount),
|
||||
actualGrossWeightKg: numberValue(raw.actualGrossWeightKg),
|
||||
actualVolumeCbm: numberValue(raw.actualVolumeCbm),
|
||||
totalPalletCount: nullableInteger(raw.totalPalletCount),
|
||||
recommendedPalletType: raw.recommendedPalletType ?? null,
|
||||
locationCodes: raw.locationCodes ?? raw.location ?? null,
|
||||
|
||||
@@ -71,6 +71,16 @@ describe('InboundFeeDetailManager', () => {
|
||||
expect(source).toMatch(/size="small" plain type="danger"[^>]+[^>]*>\s*<X/)
|
||||
})
|
||||
|
||||
it('keeps FBA_PRICING rows derived and read-only regardless of write permissions', async () => {
|
||||
vi.mocked(listInboundFeeDetails).mockResolvedValue([{ ...approved, sourceType: 'FBA_PRICING' }])
|
||||
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:inbound:fee-detail:delete', 'firstleg:inbound:fee-detail:review'])
|
||||
await flushPromises()
|
||||
const row = wrapper.find('.el-table__row')
|
||||
expect(row.text()).toContain('头程派生')
|
||||
const buttons = row.findAll('button').map((button) => button.text())
|
||||
for (const action of ['编辑', '审核', '驳回', '调整', '作废', '删除']) expect(buttons).not.toContain(action)
|
||||
})
|
||||
|
||||
it('does not request or expose fee rows without view permission', async () => {
|
||||
mountManager([]); await flushPromises()
|
||||
expect(listInboundFeeDetails).not.toHaveBeenCalled()
|
||||
|
||||
@@ -19,7 +19,8 @@ import type {
|
||||
} from '@/modules/unified-orders/api/inboundFeeDetails'
|
||||
|
||||
const props = defineProps<{ accountId: string; inboundId: string }>()
|
||||
const emit = defineEmits<{ changed: [] }>()
|
||||
interface OtherFeeSummary { currency: string; approvedReceivable: number; approvedPayable: number }
|
||||
const emit = defineEmits<{ changed: []; summary: [value: OtherFeeSummary] }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const rows = ref<InboundFeeDetail[]>([])
|
||||
const feeItems = ref<InboundFeeItemOption[]>([])
|
||||
@@ -62,18 +63,27 @@ const directionOptions = computed(() => [
|
||||
])
|
||||
const applicableItems = computed(() => feeItems.value.filter((item) => form.direction === 'RECEIVABLE' ? item.warehouseReceivable : item.warehousePayable))
|
||||
|
||||
function canEdit(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:edit') && row.reviewStatus !== 'APPROVED' }
|
||||
function canDelete(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:delete') && row.reviewStatus !== 'APPROVED' }
|
||||
function canReview(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'PENDING' }
|
||||
function canAdjust(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:edit') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
|
||||
function canVoid(row: InboundFeeDetail) { return access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
|
||||
function isManual(row: InboundFeeDetail) { return row.sourceType === 'MANUAL' }
|
||||
function canEdit(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:edit') && row.reviewStatus === 'PENDING' }
|
||||
function canDelete(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:delete') && row.reviewStatus === 'PENDING' }
|
||||
function canReview(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'PENDING' }
|
||||
function canAdjust(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:edit') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
|
||||
function canVoid(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
|
||||
function reviewLabel(value: string) { return ({ PENDING: '待审核', APPROVED: '已审核', REJECTED: '已驳回' } as Record<string, string>)[value] || value }
|
||||
function directionLabel(value: string) { return value === 'RECEIVABLE' ? '应收' : '应付' }
|
||||
|
||||
async function load() {
|
||||
if (!canView.value) return
|
||||
loading.value = true; loadError.value = null
|
||||
try { rows.value = await listInboundFeeDetails(props.accountId, props.inboundId) }
|
||||
try {
|
||||
rows.value = await listInboundFeeDetails(props.accountId, props.inboundId)
|
||||
const approved = rows.value.filter((row) => isManual(row) && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' && row.settlementIncluded)
|
||||
emit('summary', {
|
||||
currency: approved.find((row) => row.currency)?.currency || 'CNY',
|
||||
approvedReceivable: approved.filter((row) => row.direction === 'RECEIVABLE').reduce((sum, row) => sum + Number(row.amount || 0), 0),
|
||||
approvedPayable: approved.filter((row) => row.direction === 'PAYABLE').reduce((sum, row) => sum + Number(row.amount || 0), 0),
|
||||
})
|
||||
}
|
||||
catch (error) { loadError.value = getApiErrorDetails(error, '费用明细加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
@@ -208,6 +218,7 @@ onMounted(() => void load())
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核状态" width="120"><template #default="{ row }"><StatusBadge :status="reviewLabel(row.reviewStatus)" /></template></el-table-column>
|
||||
<el-table-column label="来源" width="120"><template #default="{ row }">{{ row.sourceType === 'FBA_PRICING' ? '头程派生' : '人工录入' }}</template></el-table-column>
|
||||
<el-table-column label="应收/应付" width="110"><template #default="{ row }">{{ directionLabel(row.direction) }}</template></el-table-column>
|
||||
<el-table-column prop="feeTypeNameSnapshot" label="费用类型" min-width="150" />
|
||||
<el-table-column label="往来单位/客户" min-width="170"><template #default="{ row }">{{ row.direction === 'PAYABLE' ? row.partnerName : row.customerName || '—' }}</template></el-table-column>
|
||||
|
||||
@@ -95,21 +95,21 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('v-model:page-size="pageSize"')
|
||||
const columnSource = source.slice(source.indexOf('const columns ='), source.indexOf('function emptyFilters'))
|
||||
const expectedColumnKeys = [
|
||||
'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
||||
'destinationWarehouseName', 'totalPackageCount', 'measurementReviewStatus', 'actualGrossWeightKg',
|
||||
'actions', 'orderNo', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName',
|
||||
'destinationWarehouseName', 'channelName', 'totalPackageCount', 'recommendedPalletType', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg',
|
||||
'actualVolumeCbm', 'chargeableWeightKg', 'expectedShipDate', 'loadingStatus', 'containerNo', 'cabinetNo',
|
||||
'sailingAt', 'portArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt',
|
||||
]
|
||||
for (const key of expectedColumnKeys) expect(columnSource).toContain(`key: '${key}'`)
|
||||
for (const uncontractedKey of [
|
||||
'internalOrderStatus', 'expectedArrivalAt', 'feeReviewStatus', 'holdCargo', 'companyName',
|
||||
'pickupMethod', 'chargeableVolumeCbm', 'recommendedPalletType', 'locationCodes', 'associatedFiles', 'createdBy',
|
||||
'pickupMethod', 'chargeableVolumeCbm', 'associatedFiles', 'createdBy',
|
||||
]) expect(columnSource).not.toContain(`key: '${uncontractedKey}'`)
|
||||
expect(columnSource).not.toContain("key: 'orderNo'")
|
||||
expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'"))
|
||||
expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'orderNo'"))
|
||||
expect(columnSource.indexOf("key: 'orderNo'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'"))
|
||||
expect(columnSource).toContain("{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态'")
|
||||
expect(columnSource).toContain("{ key: 'loadingStatus', prop: 'loadingStatus', label: '配载状态', minWidth: 130, slotName: 'field', defaultVisible: false }")
|
||||
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(8)
|
||||
expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(10)
|
||||
expect(source).toContain('.table-card { overflow: hidden; }')
|
||||
expect(source).toContain('slotName: \'measurementReviewStatus\'')
|
||||
expect(source).toContain('unifiedOrderReviewStatusLabel(value, \'未录入\')')
|
||||
@@ -288,6 +288,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('InboundFeePanel')
|
||||
expect(source).toContain('InboundFeeDetailManager')
|
||||
expect(source).toContain(':customer-id="String(detail.summary.tenantId)"')
|
||||
expect(source).toContain('compact')
|
||||
expect(source).toContain('头程自动初算')
|
||||
expect(source).toContain('其他费用明细')
|
||||
expect(source).toContain('@changed="loadDetail(true)"')
|
||||
|
||||
@@ -103,6 +103,18 @@ const detail = ref<UnifiedOrderDetailResource | null>(null)
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref<ApiErrorDetails | null>(null)
|
||||
const activeDetailTab = ref('basic')
|
||||
const automaticFee = ref<AutomaticFeeSummary | null>(null)
|
||||
const otherFeeSummary = ref<OtherFeeSummary>({ currency: 'CNY', approvedReceivable: 0, approvedPayable: 0 })
|
||||
const customerFeeTotal = computed(() => (
|
||||
(automaticFee.value?.feeStatus === 'SENT' ? Number(automaticFee.value.totalAmount || 0) : 0)
|
||||
+ otherFeeSummary.value.approvedReceivable
|
||||
))
|
||||
function feeMoney(value: number) {
|
||||
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: otherFeeSummary.value.currency || 'CNY' }).format(value)
|
||||
}
|
||||
|
||||
type OtherFeeSummary = { currency: string; approvedReceivable: number; approvedPayable: number }
|
||||
type AutomaticFeeSummary = { feeStatus: string; totalAmount: number }
|
||||
const approvalOpen = ref(false)
|
||||
const approvalLoadingId = ref('')
|
||||
const approvalSaving = ref(false)
|
||||
@@ -226,12 +238,16 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
key: 'selection', type: 'selection', width: 48, fixed: 'left',
|
||||
selectable: (row: UnifiedOrderResource) => canApprove(row) && !batchApprovalSaving.value,
|
||||
} satisfies DataTableColumn] : []),
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '订单号', minWidth: 180, slotName: 'field' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, slotName: 'inboundCode' },
|
||||
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 130, slotName: 'mainStatus' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '平台收货仓', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '目的仓', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'channelName', prop: 'channelName', label: '渠道', minWidth: 140, slotName: 'field' },
|
||||
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', minWidth: 100, slotName: 'field' },
|
||||
{ key: 'recommendedPalletType', prop: 'recommendedPalletType', label: '推荐托盘类型', minWidth: 140, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'locationCodes', prop: 'locationCodes', label: '库位位置', minWidth: 160, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'measurementReviewStatus', prop: 'measurementReviewStatus', label: '测量数据审核状态', minWidth: 170, slotName: 'measurementReviewStatus' },
|
||||
{ key: 'actualGrossWeightKg', prop: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'actualVolumeCbm', prop: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 130, slotName: 'field' },
|
||||
@@ -351,7 +367,6 @@ function displayField(row: UnifiedOrderResource, property: string | undefined) {
|
||||
measurementReviewStatus: 'measurement.reviewStatus', expectedArrivalAt: 'expectedArrivalAt',
|
||||
loadingStatus: 'transport.loadingStatus', containerNo: 'transport.containerNo', cabinetNo: 'transport.cabinetNo',
|
||||
sailingAt: 'transport.sailingAt', portArrivalAt: 'transport.portArrivalAt',
|
||||
actualGrossWeightKg: 'measurement.actualGrossWeightKg', actualVolumeCbm: 'measurement.actualVolumeCbm',
|
||||
chargeableWeightKg: 'measurement.chargeableWeightKg', measurementCompletedAt: 'measurement.completedAt',
|
||||
expressTrackingNo: 'delivery.trackingNo',
|
||||
}
|
||||
@@ -978,16 +993,23 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template v-else-if="activeDetailTab === 'fees'">
|
||||
<div class="panel-title"><div><h2>费用信息</h2><p>头程自动初算与其他人工费用在同一页维护</p></div><WalletCards :size="19" /></div>
|
||||
<dl class="fee-overview" aria-label="费用汇总">
|
||||
<div><dt>对客费用合计</dt><dd>{{ feeMoney(customerFeeTotal) }}</dd><small>已发布头程费用 + 已审核其他应收</small></div>
|
||||
<div><dt>已审核其他应付</dt><dd>{{ feeMoney(otherFeeSummary.approvedPayable) }}</dd><small>单列展示,不计入对客费用</small></div>
|
||||
</dl>
|
||||
<h3 class="fee-section-title">头程自动初算</h3>
|
||||
<InboundFeePanel
|
||||
:customer-id="String(detail.summary.tenantId)"
|
||||
:inbound-id="String(detail.summary.inboundId)"
|
||||
compact
|
||||
@loaded="automaticFee = $event"
|
||||
@changed="loadDetail(true)"
|
||||
/>
|
||||
<h3 class="fee-section-title fee-section-title--secondary">其他费用明细</h3>
|
||||
<InboundFeeDetailManager
|
||||
:account-id="String(detail.summary.tenantId)"
|
||||
:inbound-id="String(detail.summary.inboundId)"
|
||||
@summary="otherFeeSummary = $event"
|
||||
@changed="loadDetail(true)"
|
||||
/>
|
||||
</template>
|
||||
@@ -1295,6 +1317,12 @@ onBeforeUnmount(() => {
|
||||
.panel-title p { margin: 5px 0 0; color: var(--cc-muted); font-size: 11px; }
|
||||
.panel-title > svg { color: var(--cc-primary); }
|
||||
.fee-section-title { margin: 14px 0 8px; color: #344054; font-size: 13px; line-height: 1.4; }
|
||||
.fee-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 8px 0 0; border: 1px solid var(--cc-border); }
|
||||
.fee-overview > div { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: 4px 12px; padding: 10px 12px; }
|
||||
.fee-overview > div + div { border-left: 1px solid var(--cc-border); }
|
||||
.fee-overview dt, .fee-overview small { color: var(--cc-muted); font-size: 11px; }
|
||||
.fee-overview dd { margin: 0; font-size: 18px; font-weight: 700; text-align: right; }
|
||||
.fee-overview small { grid-column: 1 / -1; }
|
||||
.fee-section-title--secondary { margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--cc-border); }
|
||||
.order-info-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; margin: 0; overflow: hidden; border: 1px solid #edf0f4; background: #fff; }
|
||||
.order-info-grid > div, .measurement-grid > div { display: flex; min-width: 0; min-height: 38px; align-items: baseline; gap: 8px; padding: 8px 12px; border-right: 1px solid #edf0f4; border-bottom: 1px solid #edf0f4; background: #fff; }
|
||||
@@ -1346,5 +1374,6 @@ onBeforeUnmount(() => {
|
||||
@media (max-width: 960px) { .detail-overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } .detail-overview article:nth-child(2) { border-right: 0; } .detail-overview article:nth-child(-n+2) { border-bottom: 1px solid var(--cc-border); } .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { display: grid; grid-template-columns: auto 36px minmax(0, 1fr) auto; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .detail-overview, .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .detail-overview article { border-right: 0; border-bottom: 1px solid var(--cc-border); } .detail-overview article:last-child { border-bottom: 0; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs button { padding: 0 18px; } .detail-panel { padding: 16px; } .measurement-section-title { align-items: flex-start; flex-wrap: wrap; padding: 12px; } .measurement-section-summary { width: 100%; flex-basis: calc(100% - 34px); flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; white-space: normal; } .measurement-section-title > svg { margin-left: auto; } .measurement-total { flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; } .measurement-total strong { width: 100%; } }
|
||||
@media (max-width: 720px) { .weight-evidence__heading { flex-direction: column; } .weight-evidence__heading dl { text-align: left; } }
|
||||
@media (max-width: 720px) { .fee-overview { grid-template-columns: 1fr; } .fee-overview > div + div { border-top: 1px solid var(--cc-border); border-left: 0; } }
|
||||
@media (max-width: 420px) { .order-detail-heading { grid-template-columns: auto minmax(0, 1fr) auto; } .order-mark { display: none; } .weight-evidence { padding: 10px; } .weight-evidence-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user