付款截图
@@ -300,18 +308,21 @@ onMounted(() => { void loadAccountData() })
diff --git a/src/modules/finance/views/FinanceBillDetailView.vue b/src/modules/finance/views/FinanceBillDetailView.vue
new file mode 100644
index 0000000..5586a63
--- /dev/null
+++ b/src/modules/finance/views/FinanceBillDetailView.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+ 账单概览
- 账单金额
- ¥ {{ formatAmount(detail.totalAmount) }}
- 已收金额
- ¥ {{ formatAmount(detail.receivedAmount) }}
- 未收余额
- ¥ {{ formatAmount(detail.outstandingAmount) }}
- 客户确认
- {{ confirmationLabel(detail.customerConfirmationStatus) }}
- 确认成员
- {{ detail.customerConfirmedByName || '—' }}
- 确认时间
- {{ formatDateTime(detail.customerConfirmedAt) }}
+ 费用明细
{{ row.inboundCode }}—¥ {{ formatAmount(row.amount) }}
+ 收款记录
{{ formatDate(row.receivedAt) }}¥ {{ formatAmount(row.allocatedAmount) }}{{ paymentMethodLabel(row.method) }}
+ 如对账单内容有疑问,请先联系平台财务,核对无误后再确认。
+
+ {{ errorMessage }}重新加载
+
+
+
+
+
diff --git a/src/modules/finance/views/FinanceBillsView.test.mjs b/src/modules/finance/views/FinanceBillsView.test.mjs
index 43818db..7ad9fa5 100644
--- a/src/modules/finance/views/FinanceBillsView.test.mjs
+++ b/src/modules/finance/views/FinanceBillsView.test.mjs
@@ -5,6 +5,7 @@ import ts from 'typescript'
const routesSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
const viewSource = readFileSync(new URL('./FinanceBillsView.vue', import.meta.url), 'utf8')
+const detailViewSource = readFileSync(new URL('./FinanceBillDetailView.vue', import.meta.url), 'utf8')
const apiSource = readFileSync(new URL('../api/finance.ts', import.meta.url), 'utf8')
const adapterSource = readFileSync(new URL('../api/adapters.ts', import.meta.url), 'utf8')
const permissionSource = readFileSync(new URL('../../tenant-management/contracts.ts', import.meta.url), 'utf8')
@@ -22,6 +23,8 @@ const permissionContracts = await importTypeScriptModule(new URL('../../tenant-m
test('activates the customer finance route with its existing list and detail permission', () => {
assert.match(routesSource, /path: 'finance\/bills'/)
+ assert.match(routesSource, /path: 'finance\/bills\/:billNo\/detail'/)
+ assert.match(routesSource, /name: 'finance-bill-detail'/)
assert.match(routesSource, /FinanceBillsView\.vue/)
assert.match(routesSource, /release: 'active'/)
assert.match(routesSource, /permission: 'finance:bill:view'/)
@@ -52,8 +55,10 @@ test('adapts the Finance Java DTO contract and filters nested receipt allocation
assert.doesNotMatch(adapterSource, /bill_no|customer_name|bill_date|total_amount|outstanding_amount|receipt_no|bill_id|applied_amount/)
assert.doesNotMatch(adapterSource, /receipt\.billId|receipt\.appliedAmount/)
assert.doesNotMatch(adapterSource, /voucherNo|voucher_no/)
- assert.match(viewSource, /loadDetail\(row\.id\)/)
- assert.doesNotMatch(viewSource, /loadDetail\(row\.billNo\)/)
+ assert.match(viewSource, /name: 'finance-bill-detail', params: \{ billNo: row\.billNo \}/)
+ assert.match(detailViewSource, /getMerchantFinanceBill\(summary\.id\)/)
+ assert.match(detailViewSource, /getMerchantFinanceReceipts\(summary\.id\)/)
+ assert.doesNotMatch(detailViewSource, /getMerchantFinanceBill\(billNo\.value\)/)
})
test('does not expose draft or voided bills returned by the server', () => {
@@ -114,30 +119,44 @@ test('requires both finance statement authorities for the page permission', () =
})
test('keeps actions first and exposes list, balance, detail, confirmation, receipt, empty and error states', () => {
- const actionIndex = viewSource.indexOf('label="操作"')
- const billNumberIndex = viewSource.indexOf('label="账单编号"')
+ const tableDefinition = viewSource.slice(viewSource.indexOf('const tableColumns'))
+ const actionIndex = tableDefinition.indexOf("label: '操作'")
+ const billNumberIndex = tableDefinition.indexOf("label: '账单编号'")
assert.ok(actionIndex >= 0 && actionIndex < billNumberIndex)
- for (const expected of ['未收余额', '账单详情', '确认账单', '客户确认信息', '收款记录', '暂无账单', '暂无收款记录', 'state-panel--error']) {
+ for (const expected of ['未收余额', '确认账单', '暂无账单', 'state-panel--error']) {
assert.match(viewSource, new RegExp(expected))
}
- assert.match(viewSource, /BANK_TRANSFER: '银行转账'/)
- assert.match(viewSource, /paymentMethodLabel\(row\.method\)/)
- assert.match(viewSource, /label="入仓码"/)
- assert.match(viewSource, /openInboundAssociation\(row\.inboundCode\)/)
- assert.match(viewSource, /InboundCodeAssociationDialog/)
- assert.match(viewSource, /:inbound-code="inboundAssociationCode"/)
+ for (const expected of ['账单概览', '费用明细', '收款记录', '暂无收款记录']) assert.match(detailViewSource, new RegExp(expected))
+ assert.match(detailViewSource, /BANK_TRANSFER: '银行转账'/)
+ assert.match(detailViewSource, /paymentMethodLabel\(row\.method\)/)
+ assert.match(detailViewSource, /label: '入仓码'/)
+ assert.match(detailViewSource, /openInboundAssociation\(row\.inboundCode\)/)
+ assert.match(detailViewSource, /InboundCodeAssociationDialog/)
+ assert.match(detailViewSource, /:inbound-code="inboundAssociationCode"/)
assert.match(viewSource, /auth\.hasPermission\('finance:bill:confirm'\)/)
- assert.match(viewSource, /row\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
- assert.match(viewSource, /size="small" type="success" plain/)
- assert.match(viewSource, /confirmStatement\(row as FinanceBillSummary\)/)
- assert.match(viewSource, /detail\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
- assert.match(viewSource, /confirmStatement\(detail\)/)
+ assert.match(viewSource, /statement\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm\.value/)
+ assert.match(viewSource, /label: '确认账单'/)
+ assert.match(viewSource, /run: \(\) => confirmStatement\(statement\)/)
+ assert.match(detailViewSource, /detail\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
+ assert.match(detailViewSource, /confirmStatement\(detail\)/)
assert.match(viewSource, /confirmMerchantFinanceStatement\(statementId, statement\.version, key\)/)
assert.match(viewSource, /ElMessage\.success\('对账单已确认'\)/)
assert.match(viewSource, /ElMessage\.error\(error instanceof ApiError \? error\.message : '对账单确认失败,请重试'\)/)
for (const forbidden of ['发起争议', '新增收款', '核销', '修改金额']) {
assert.doesNotMatch(viewSource, new RegExp(forbidden))
+ assert.doesNotMatch(detailViewSource, new RegExp(forbidden))
}
+ assert.doesNotMatch(viewSource, /
{
+ assert.match(viewSource, /]*layout="grid"/)
+ assert.match(viewSource, /]*:row-actions="rowActions"/)
+ assert.match(viewSource, /v-model:page="page" v-model:page-size="pageSize"/)
+ assert.match(viewSource, /:page-sizes="\[20, 50, 100\]"/)
+ assert.doesNotMatch(viewSource, /class="filter-bar"|class="pagination-row"|class="table-empty"/)
})
test('keeps the existing customer finance view permission mapping', () => {
diff --git a/src/modules/finance/views/FinanceBillsView.vue b/src/modules/finance/views/FinanceBillsView.vue
index d4f46df..911368a 100644
--- a/src/modules/finance/views/FinanceBillsView.vue
+++ b/src/modules/finance/views/FinanceBillsView.vue
@@ -1,24 +1,21 @@
@@ -181,65 +176,29 @@ onMounted(() => { void loadBills(1) })
未收 ¥ {{ formatAmount(totals.outstanding) }}
+
+
-
-
-
-
{{ errorMessage }}
-
重试
-
-
-
- 查看确认账单
-
-
- {{ formatDate(row.issueDate) }}
- {{ formatDate(row.dueDate) }}
- ¥ {{ formatAmount(row.totalAmount) }}
- ¥ {{ formatAmount(row.receivedAmount) }}
- ¥ {{ formatAmount(row.outstandingAmount) }}
- {{ statusLabel(row.status) }}
- {{ confirmationLabel(row.customerConfirmationStatus) }}
- 暂无账单平台财务出账后,这里会显示本企业账单。
-
-
-
+
+
+ {{ formatDate(row.issueDate) }}
+ {{ formatDate(row.dueDate) }}
+ ¥ {{ formatAmount(row.totalAmount) }}
+ ¥ {{ formatAmount(row.receivedAmount) }}
+ ¥ {{ formatAmount(row.outstandingAmount) }}
+ {{ statusLabel(row.status) }}
+ {{ confirmationLabel(row.customerConfirmationStatus) }}
+
-
-
-
-
- {{ detail.billNo }}
出账 {{ formatDate(detail.issueDate) }} · 到期 {{ formatDate(detail.dueDate) }} · 未收 ¥ {{ formatAmount(detail.outstandingAmount) }}
{{ statusLabel(detail.status) }}{{ confirmationLabel(detail.customerConfirmationStatus) }}
- {{ confirmationLabel(detail.customerConfirmationStatus) }}{{ detail.customerConfirmedByName || '当前企业成员' }} · {{ formatDateTime(detail.customerConfirmedAt) }}
请核对本期金额和费用明细,确认后将记录当前登录成员与确认时间。
确认账单
- 费用明细
{{ row.inboundCode }}—¥ {{ formatAmount(row.amount) }}
- 收款记录
{{ formatDate(row.receivedAt) }}¥ {{ formatAmount(row.allocatedAmount) }}{{ paymentMethodLabel(row.method) }}暂无收款记录
- 如对账单内容有疑问,请先线下联系平台财务,核对无误后再确认。
-
-
-
-
diff --git a/src/modules/first-leg/components/FirstLegOrderEditorShell.vue b/src/modules/first-leg/components/FirstLegOrderEditorShell.vue
index d790f7b..cd8fb68 100644
--- a/src/modules/first-leg/components/FirstLegOrderEditorShell.vue
+++ b/src/modules/first-leg/components/FirstLegOrderEditorShell.vue
@@ -42,14 +42,24 @@ const emit = defineEmits<{
diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
index 46e4b2d..56962c2 100644
--- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
+++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
@@ -5,22 +5,25 @@ import test from 'node:test'
const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8')
const tableSource = readFileSync(new URL('../../../components/common/DataTable.vue', import.meta.url), 'utf8')
const querySource = readFileSync(new URL('../../../components/common/QueryToolbar.vue', import.meta.url), 'utf8')
+const clientUiSource = readFileSync(new URL('../../../styles/client-ui.css', import.meta.url), 'utf8')
const routerSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
const editorShellSource = readFileSync(new URL('../components/FirstLegOrderEditorShell.vue', import.meta.url), 'utf8')
const progressSource = readFileSync(new URL('../components/OrderProgressPanel.vue', import.meta.url), 'utf8')
const customsDialogSource = readFileSync(new URL('../components/CustomsDocumentsDialog.vue', import.meta.url), 'utf8')
const customsApiSource = readFileSync(new URL('../api/customs.ts', import.meta.url), 'utf8')
const orderStoreSource = readFileSync(new URL('../store/firstLegOrders.ts', import.meta.url), 'utf8')
+const createHubSource = readFileSync(new URL('./FirstLegOrderCreateHubView.vue', import.meta.url), 'utf8')
test('routes the confirmed first-leg path to the implemented order view', () => {
assert.match(routerSource, /path: 'orders\/first-leg'[\s\S]*?component: \(\) => import\('\.\/views\/FirstLegOrdersView\.vue'\)[\s\S]*?release: 'active'/)
})
test('opens order creation as a dedicated protected page while keeping draft editing in a large dialog', () => {
- assert.match(routerSource, /path: 'orders\/first-leg\/create'[\s\S]*?name: 'first-leg-order-create'[\s\S]*?release: 'active'[\s\S]*?permission: 'order:first-leg:create'/)
- assert.match(routerSource, /name: 'first-leg-order-create'[\s\S]*?props: \{ createPage: true \}/)
+ assert.match(routerSource, /path: 'orders\/first-leg\/create'[\s\S]*?name: 'first-leg-order-create'[\s\S]*?release: 'active'[\s\S]*?permission: 'order:first-leg:create'[\s\S]*?keepAlive: false/)
+ assert.match(routerSource, /name: 'first-leg-order-create'[\s\S]*?FirstLegOrderCreateHubView\.vue/)
+ assert.match(createHubSource, /
/)
assert.match(source, /router\.push\(\{ name: 'first-leg-order-create' \}\)/)
- assert.match(source, /defineProps<\{ createPage\?: boolean \}>/)
+ assert.match(source, /defineProps<\{ createPage\?: boolean; detailPage\?: boolean; showCreateAction\?: boolean \}>/)
assert.match(source, /const isCreatePage = computed\(\(\) => props\.createPage\)/)
assert.match(source, /