From c7334b2cf88216bc8e71ba19ffb54c2c7a79a29d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 17 Aug 2026 17:49:10 +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 --- .../downloads/api/platformExportTasks.ts | 12 +- .../pages/DownloadCenterView.test.ts | 24 +++ .../downloads/pages/DownloadCenterView.vue | 19 ++- .../pages/FinanceCustomerAccountsView.test.ts | 74 +++++++- .../pages/FinanceCustomerAccountsView.vue | 56 ++++-- .../pages/FinanceCustomerArapView.test.ts | 38 +++++ .../finance/pages/FinanceCustomerArapView.vue | 38 ++++- .../FinanceCustomerStatementsView.test.ts | 39 ++++- .../pages/FinanceCustomerStatementsView.vue | 60 ++++--- .../FinanceFormalSettlementExport.test.ts | 160 ++++++++++++++++++ .../pages/FinanceFormalSettlementPage.vue | 82 +++++---- .../pages/FinanceG3G4Operations.test.ts | 2 +- .../finance/pages/FinanceG3G4Views.test.ts | 4 +- .../api/platformFirstLegOrders.test.ts | 17 ++ .../api/platformFirstLegOrders.ts | 22 +++ .../PlatformFirstLegOrdersPanel.test.ts | 21 +++ .../PlatformFirstLegOrdersPanel.vue | 80 ++++++++- .../components/InboundTaskList.test.ts | 18 +- .../inbounds/components/InboundTaskList.vue | 63 +++---- .../InboundWorkflowWorkspace.test.ts | 12 +- .../components/InboundWorkflowWorkspace.vue | 55 ++---- src/modules/inbounds/index.ts | 2 + .../InboundMeasurementDialog.test.ts | 48 ++++++ .../measurement/InboundMeasurementDialog.vue | 145 ++++++++++++++++ .../measurement/InboundMeasurementPanel.vue | 6 +- .../MeasurementEvidenceThumbnail.vue | 79 +++++++++ src/modules/inbounds/measurement/index.ts | 3 +- .../components/WeightImageThumbnail.vue | 61 ++----- .../pages/UnifiedOrdersView.test.ts | 9 +- .../pages/UnifiedOrdersView.vue | 43 +++-- 30 files changed, 1043 insertions(+), 249 deletions(-) create mode 100644 src/modules/finance/pages/FinanceFormalSettlementExport.test.ts create mode 100644 src/modules/inbounds/measurement/InboundMeasurementDialog.test.ts create mode 100644 src/modules/inbounds/measurement/InboundMeasurementDialog.vue create mode 100644 src/modules/inbounds/measurement/components/MeasurementEvidenceThumbnail.vue diff --git a/src/modules/downloads/api/platformExportTasks.ts b/src/modules/downloads/api/platformExportTasks.ts index ff52f809..bfbcb2fb 100644 --- a/src/modules/downloads/api/platformExportTasks.ts +++ b/src/modules/downloads/api/platformExportTasks.ts @@ -2,7 +2,17 @@ import axios from 'axios' import { ApiResponseError, http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' export type ExportTaskStatus = 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'EXPIRED' -export type ExportTaskType = 'CUSTOMER_LIST' | string +export type KnownExportTaskType = + | 'CUSTOMER_LIST' + | 'FINANCE_CUSTOMER_ACCOUNT_LIST' + | 'FINANCE_CUSTOMER_ACCOUNT_LEDGER' + | 'FINANCE_FEE_SOURCE_LIST' + | 'FINANCE_RECONCILIATION_STATEMENT_LIST' + | 'FINANCE_RECEIVABLE_SETTLEMENT_LIST' + | 'FINANCE_RECEIVABLE_SETTLEMENT_DETAIL' + | 'FINANCE_PAYABLE_SETTLEMENT_LIST' + | 'FINANCE_PAYABLE_SETTLEMENT_DETAIL' +export type ExportTaskType = KnownExportTaskType | (string & {}) export interface PlatformExportTask { id: string diff --git a/src/modules/downloads/pages/DownloadCenterView.test.ts b/src/modules/downloads/pages/DownloadCenterView.test.ts index bb8b16ca..c60865a7 100644 --- a/src/modules/downloads/pages/DownloadCenterView.test.ts +++ b/src/modules/downloads/pages/DownloadCenterView.test.ts @@ -101,6 +101,30 @@ describe('DownloadCenterView', () => { expect(retryPlatformExportTask).toHaveBeenCalledWith('9') }) + it('labels and retries Finance tasks with their exact source export permission', async () => { + const financeTask = { + ...task, + exportType: 'FINANCE_RECONCILIATION_STATEMENT_LIST', + taskName: '客户对账单', + status: 'FAILED' as const, + } + vi.mocked(listPlatformExportTasks).mockResolvedValue({ records: [financeTask], page: 1, size: 20, total: 1, pages: 1 }) + const pinia = setActivePinia(createPinia()) + const auth = useAuthStore() + auth.token = 'token' + auth.permissions = ['platform:download:list', 'platform:finance:customer-statement:export'] + useAccessPreviewStore().initializeAfterLogin() + mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) + await flushPromises() + + expect(document.body.textContent).toContain('客户对账单') + const retry = [...document.querySelectorAll('button')] + .find((item) => item.textContent?.trim() === '重试') + retry?.click() + await flushPromises() + expect(retryPlatformExportTask).toHaveBeenCalledWith('9') + }) + it('loads a fresh task detail from the backend', async () => { const wrapper = mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } }) await flushPromises() diff --git a/src/modules/downloads/pages/DownloadCenterView.vue b/src/modules/downloads/pages/DownloadCenterView.vue index 45e8780b..3b9110d5 100644 --- a/src/modules/downloads/pages/DownloadCenterView.vue +++ b/src/modules/downloads/pages/DownloadCenterView.vue @@ -25,6 +25,18 @@ type DownloadFilters = QueryModel & { requestedRange: string[] } +const exportTypeMeta: Record = { + CUSTOMER_LIST: { label: '客户列表', retryPermission: 'platform:customer:export' }, + FINANCE_CUSTOMER_ACCOUNT_LIST: { label: '客户账户', retryPermission: 'platform:finance:customer-account:export' }, + FINANCE_CUSTOMER_ACCOUNT_LEDGER: { label: '客户账户流水', retryPermission: 'platform:finance:customer-account:export' }, + FINANCE_FEE_SOURCE_LIST: { label: '客户应收应付费用来源', retryPermission: 'platform:finance:customer-arap:export' }, + FINANCE_RECONCILIATION_STATEMENT_LIST: { label: '客户对账单', retryPermission: 'platform:finance:customer-statement:export' }, + FINANCE_RECEIVABLE_SETTLEMENT_LIST: { label: '应收结算单列表', retryPermission: 'platform:finance:settlement-ar:export' }, + FINANCE_RECEIVABLE_SETTLEMENT_DETAIL: { label: '应收结算单详情', retryPermission: 'platform:finance:settlement-ar:export' }, + FINANCE_PAYABLE_SETTLEMENT_LIST: { label: '应付结算单列表', retryPermission: 'platform:finance:settlement-ap:export' }, + FINANCE_PAYABLE_SETTLEMENT_DETAIL: { label: '应付结算单详情', retryPermission: 'platform:finance:settlement-ap:export' }, +} + const access = useAccessPreviewStore() const page = ref(1) const pageSize = ref(20) @@ -46,7 +58,7 @@ let detailRequestId = 0 const queryFields: QueryField[] = [ { key: 'exportType', type: 'select', label: '任务类型', placeholder: '全部任务类型', options: [ - { label: '客户列表', value: 'CUSTOMER_LIST' }, + ...Object.entries(exportTypeMeta).map(([value, meta]) => ({ label: meta.label, value })), ] }, { key: 'status', type: 'select', label: '任务状态', placeholder: '全部任务状态', options: [ { label: '等待处理', value: 'PENDING' }, { label: '处理中', value: 'RUNNING' }, @@ -196,7 +208,7 @@ function safeFileName(value: string) { return safe || '导出文件.xlsx' } -function typeLabel(type: string) { return type === 'CUSTOMER_LIST' ? '客户列表' : type } +function typeLabel(type: string) { return exportTypeMeta[type]?.label || type } function statusMeta(status: ExportTaskStatus) { const values = { @@ -223,7 +235,8 @@ function formatSize(bytes: number | null) { } function canRetry(task: PlatformExportTask) { - return task.status === 'FAILED' && task.exportType === 'CUSTOMER_LIST' && access.can('platform:customer:export') + const permission = exportTypeMeta[task.exportType]?.retryPermission + return task.status === 'FAILED' && permission !== undefined && access.can(permission) } watch([page, pageSize], () => { void loadTasks() }) diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts index 2a627eef..c9795b34 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts @@ -1,13 +1,15 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' -import ElementPlus from 'element-plus' +import ElementPlus, { ElMessage } from 'element-plus' import { createPinia, setActivePinia } from 'pinia' import { flushPromises, mount } from '@vue/test-utils' import { createMemoryHistory, createRouter } from 'vue-router' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' +import { createPlatformExportTask } from '@/modules/downloads' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' +import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog' import { listFinanceAccountLedger, listFinanceAccounts } from '../api/finance' import FinanceCustomerAccountsView from './FinanceCustomerAccountsView.vue' @@ -18,6 +20,8 @@ vi.mock('../api/finance', () => ({ updateFinanceCustomerTerms: vi.fn(), })) vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() })) +vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() })) +vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() })) class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} } @@ -26,6 +30,21 @@ function memoryStorage(): Storage { return { get length() { return values.size }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null, key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value) } } +async function mountAccountsView(permissions: string[], initialPath = '/finance/customer-accounts') { + const pinia = createPinia(); setActivePinia(pinia) + const auth = useAuthStore(); auth.token = 'token'; auth.permissions = permissions + useAccessPreviewStore().initializeAfterLogin() + const router = createRouter({ history: createMemoryHistory(), routes: [ + { path: '/finance/customer-accounts', name: 'finance-customer-accounts', component: FinanceCustomerAccountsView }, + { path: '/finance/customer-accounts/:customerId', name: 'finance-customer-account-detail', component: FinanceCustomerAccountsView }, + { path: '/downloads', name: 'download-center', component: { template: '
下载中心
' } }, + ] }) + await router.push(initialPath); await router.isReady() + const wrapper = mount(FinanceCustomerAccountsView, { global: { plugins: [pinia, ElementPlus, router] } }) + await flushPromises() + return Object.assign(wrapper, { router }) +} + describe('FinanceCustomerAccountsView delivery', () => { const page = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerAccountsView.vue'), 'utf8') const routes = readFileSync(resolve(process.cwd(), 'src/modules/finance/routes.ts'), 'utf8') @@ -92,6 +111,8 @@ describe('FinanceCustomerAccountsView delivery', () => { vi.mocked(listFinanceAccounts).mockReset().mockResolvedValue({ records: [{ customerId: '10001', customerCode: 'C-001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 100, pendingBilling: 20, receivedInPeriod: 30, outstandingPayable: 40, status: 'ACTIVE', lastChangedAt: '2026-08-13 10:00:00' }], total: 1, pages: 1 }) vi.mocked(listFinanceAccountLedger).mockReset().mockResolvedValue({ records: [{ ledgerId: '1', occurredAt: '2026-08-13T10:00:00', businessType: 'REC', documentType: 'RECEIPT', documentId: '2', documentNo: 'REC-2', receivableChange: -10, payableChange: 0, outstandingReceivableAfter: 90, operatorName: 'operator-1000001', remark: '收款' }], total: 1, pages: 1 }) vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '客户A', status: 'ACTIVE' } as PlatformTenantResource], page: 1, size: 200, total: 1, pages: 1 }) + vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ id: 'exp-1', taskNo: 'EXP-1', taskName: '客户账户', status: 'PENDING' } as never) + vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false) }) it('restores account summary and ledger after opening the detail URL directly', async () => { @@ -133,6 +154,57 @@ describe('FinanceCustomerAccountsView delivery', () => { expect(wrapper.text()).not.toContain('2026-08-13T10:00:00') wrapper.unmount() }) + + it('creates account and ledger export tasks from the complete active filters without page parameters', async () => { + const permissions = ['platform:finance:customer-account:list', 'platform:finance:customer-account:export', 'platform:download:list'] + const listWrapper = await mountAccountsView(permissions) + const listVm = listWrapper.vm as unknown as { filters: Record; exportAccounts(): Promise } + Object.assign(listVm.filters, { customerId: '10001', status: 'ACTIVE' }) + await listVm.exportAccounts() + expect(createPlatformExportTask).toHaveBeenLastCalledWith({ + exportType: 'FINANCE_CUSTOMER_ACCOUNT_LIST', format: 'XLSX', taskName: '客户账户', + parameters: { customerId: '10001', status: 'ACTIVE' }, + }) + expect(createPlatformExportTask).not.toHaveBeenCalledWith(expect.objectContaining({ parameters: expect.objectContaining({ page: expect.anything() }) })) + listWrapper.unmount() + + vi.mocked(createPlatformExportTask).mockResolvedValueOnce({ id: 'exp-2', taskNo: 'EXP-2', taskName: '客户账户流水', status: 'PENDING' } as never) + const detailWrapper = await mountAccountsView(permissions, '/finance/customer-accounts/10001') + const detailVm = detailWrapper.vm as unknown as { ledgerFilters: Record; exportLedger(): Promise } + Object.assign(detailVm.ledgerFilters, { businessType: 'RECEIPT', occurredRange: ['2026-08-01', '2026-08-31'] }) + await detailVm.exportLedger() + expect(createPlatformExportTask).toHaveBeenLastCalledWith({ + exportType: 'FINANCE_CUSTOMER_ACCOUNT_LEDGER', format: 'XLSX', taskName: '客户账户流水', + parameters: { customerId: '10001', businessType: 'RECEIPT', startDate: '2026-08-01', endDate: '2026-08-31' }, + }) + expect(showExportTaskCreatedDialog).toHaveBeenLastCalledWith({ taskName: '客户账户流水', taskNo: 'EXP-2' }) + detailWrapper.unmount() + }) + + it('requires both the finance export permission and download-center access', async () => { + const wrapper = await mountAccountsView(['platform:finance:customer-account:list', 'platform:finance:customer-account:export']) + expect(wrapper.findAll('button').map((button) => button.text())).not.toContain('导出Excel') + expect(createPlatformExportTask).not.toHaveBeenCalled() + wrapper.unmount() + }) + + it('opens the download center after confirmation and reports task creation failures', async () => { + const permissions = ['platform:finance:customer-account:list', 'platform:finance:customer-account:export', 'platform:download:list'] + vi.mocked(showExportTaskCreatedDialog).mockResolvedValueOnce(true) + const wrapper = await mountAccountsView(permissions) + await (wrapper.vm as unknown as { exportAccounts(): Promise }).exportAccounts() + expect(wrapper.router.currentRoute.value.name).toBe('download-center') + wrapper.unmount() + + const errorSpy = vi.spyOn(ElMessage, 'error').mockImplementation(() => undefined as never) + vi.mocked(createPlatformExportTask).mockRejectedValueOnce(new Error('导出服务不可用')) + const failedWrapper = await mountAccountsView(permissions) + await (failedWrapper.vm as unknown as { exportAccounts(): Promise }).exportAccounts() + expect(errorSpy).toHaveBeenCalledWith('客户账户导出任务创建失败') + expect(showExportTaskCreatedDialog).toHaveBeenCalledTimes(1) + failedWrapper.unmount() + errorSpy.mockRestore() + }) }) afterAll(() => vi.unstubAllGlobals()) diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.vue b/src/modules/finance/pages/FinanceCustomerAccountsView.vue index 8d1aa96f..3fced65e 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.vue +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.vue @@ -4,8 +4,9 @@ import { useRoute, useRouter } from 'vue-router' import { ArrowLeft, Download, Eye, Pencil, Save } from '@lucide/vue' import { ElMessage } from 'element-plus' import { useAccessPreviewStore } from '@/core/access/accessPreview' +import { createPlatformExportTask } from '@/modules/downloads' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' -import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' +import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http' import AdminPage from '@/shared/components/AdminPage.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' @@ -14,6 +15,7 @@ import FormSection from '@/shared/components/FormSection.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' +import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog' import { getFinanceCustomerTerms, listFinanceAccountLedger, listFinanceAccounts, updateFinanceCustomerTerms } from '../api/finance' import { financeBusinessTypeLabel } from '../status' import type { FinanceAccountSummary, FinanceCustomerTerms, FinanceLedgerEntry } from '../types' @@ -44,11 +46,14 @@ const ledgerFilters = ref({ businessType: '', occurredRange: [] }) const ledgerPage = ref(1) const ledgerPageSize = ref(20) const ledgerTotal = ref(0) +const accountExportBusy = ref(false) +const ledgerExportBusy = ref(false) const canView = computed(() => access.can('platform:finance:customer-account:list')) const canListCustomers = computed(() => canView.value || access.can('platform:customer:list')) const canViewTerms = computed(() => access.can('platform:finance:customer-terms:view')) const canUpdateTerms = computed(() => canViewTerms.value && access.can('platform:finance:customer-terms:update')) +const canExport = computed(() => access.can('platform:finance:customer-account:export') && access.can('platform:download:list')) const stateError = computed(() => customersError.value || loadError.value) const queryFields = computed(() => [ { key: 'customerId', type: 'select', label: '客户名称', width: 260, placeholder: '全部客户', props: { 'aria-label': '客户名称', filterable: true, clearable: true, loading: customersLoading.value }, options: customers.value.filter((item) => item.status === 'ACTIVE').map((item) => ({ label: item.tenantName, value: String(item.id) })) }, @@ -173,14 +178,45 @@ function formatDateTime(value: unknown) { function formatLastChangedAt(value: unknown) { return String(value || '').trim() ? formatDateTime(value) : '暂无变动' } function displayDocumentNo(value: unknown) { return String(value || '').trim() || '关联单据缺失' } function displayLedgerId(value: unknown) { return String(value || '').trim() || '流水号缺失' } -function csvCell(value: unknown) { return `"${String(value ?? '').replaceAll('"', '""')}"` } -function downloadCsv(fileName: string, headers: string[], data: unknown[][]) { - const content = `\uFEFF${[headers, ...data].map((line) => line.map(csvCell).join(',')).join('\r\n')}` - const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' })) - const link = document.createElement('a'); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url) +async function finishExport(task: Awaited>) { + const openDownloadCenter = await showExportTaskCreatedDialog({ taskName: task.taskName, taskNo: task.taskNo }) + if (openDownloadCenter) await router.push({ name: 'download-center' }) +} +async function exportAccounts() { + const customerId = String(filters.value.customerId || '').trim() + const status = String(filters.value.status || '').trim() + accountExportBusy.value = true + try { + const task = await createPlatformExportTask({ + exportType: 'FINANCE_CUSTOMER_ACCOUNT_LIST', format: 'XLSX', taskName: '客户账户', + parameters: { + ...(customerId ? { customerId } : {}), + ...(status ? { status } : {}), + }, + }) + await finishExport(task) + } catch (error) { ElMessage.error(getApiErrorMessage(error, '客户账户导出任务创建失败')) } + finally { accountExportBusy.value = false } +} +async function exportLedger() { + if (!selected.value) return + const range = Array.isArray(ledgerFilters.value.occurredRange) ? ledgerFilters.value.occurredRange.map(String) : [] + const businessType = String(ledgerFilters.value.businessType || '').trim() + ledgerExportBusy.value = true + try { + const task = await createPlatformExportTask({ + exportType: 'FINANCE_CUSTOMER_ACCOUNT_LEDGER', format: 'XLSX', taskName: '客户账户流水', + parameters: { + customerId: selected.value.customerId, + ...(businessType ? { businessType } : {}), + ...(range[0] ? { startDate: range[0] } : {}), + ...(range[1] ? { endDate: range[1] } : {}), + }, + }) + await finishExport(task) + } catch (error) { ElMessage.error(getApiErrorMessage(error, '客户账户流水导出任务创建失败')) } + finally { ledgerExportBusy.value = false } } -function exportAccounts() { downloadCsv('客户账户.csv', columns.slice(1).map((item) => String(item.label)), rows.value.map((row) => [customerName(row.customerId, row.customerName), row.outstandingReceivable, row.pendingBilling, row.receivedInPeriod, row.outstandingPayable, row.status, row.lastChangedAt])) } -function exportLedger() { downloadCsv(`${selected.value ? customerName(selected.value.customerId, selected.value.customerName) : '客户'}-账户流水.csv`, ledgerColumns.map((item) => String(item.label)), ledger.value.map((row) => [row.ledgerId, formatDateTime(row.occurredAt), ledgerBusinessType(row.businessType), row.documentNo, row.receivableChange, row.payableChange, row.outstandingReceivableAfter, displayOperator(row.operatorName), row.remark])) } async function closeDetail() { detailOpen.value = false; selected.value = null; editing.value = false; ledger.value = [] if (route.name === 'finance-customer-account-detail') await router.push({ name: 'finance-customer-accounts' }) @@ -218,7 +254,7 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
- + @@ -236,7 +272,7 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome diff --git a/src/modules/finance/pages/FinanceCustomerArapView.test.ts b/src/modules/finance/pages/FinanceCustomerArapView.test.ts index 28021c5b..ce17878d 100644 --- a/src/modules/finance/pages/FinanceCustomerArapView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerArapView.test.ts @@ -5,13 +5,17 @@ import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' +import { createPlatformExportTask } from '@/modules/downloads' import { ApiResponseError } from '@/shared/api/http' +import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { createReconciliationStatement, getFinanceFeeSource, listFinanceFeePool, reviewFinanceFeeSource, updateFinanceFeeSourceSettlementInclusion } from '../api/finance' import type { FinanceFeeSourceDetail } from '../types' import FinanceCustomerArapView from './FinanceCustomerArapView.vue' vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() })) +vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() })) +vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() })) vi.mock('../api/finance', () => ({ listFinanceFeePool: vi.fn(), getFinanceFeeSource: vi.fn(), @@ -80,6 +84,8 @@ describe('FinanceCustomerArapView G2 operations', () => { vi.mocked(reviewFinanceFeeSource).mockReset().mockResolvedValue({ ...detail, reviewStatus: 'APPROVED', operationVersion: 4 }) vi.mocked(updateFinanceFeeSourceSettlementInclusion).mockReset().mockResolvedValue({ ...detail, reviewStatus: 'APPROVED', settlementIncluded: true, operationVersion: 4 }) vi.mocked(createReconciliationStatement).mockReset().mockResolvedValue({} as never) + vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ id: 'exp-3', taskNo: 'EXP-3', taskName: '客户应收应付费用来源', status: 'PENDING' } as never) + vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false) }) it('shows detail from the list permission but keeps G2 operations independent', async () => { @@ -231,4 +237,36 @@ describe('FinanceCustomerArapView G2 operations', () => { await vm.submitGeneration() expect(createReconciliationStatement).toHaveBeenCalledWith({ customerId: '10001', currency: 'CNY', generationMode: 'SELECTED', feeSourceIds: ['42'], periodStart: '2026-08-01', periodEnd: '2026-08-31', remark: '八月费用' }) }) + + it('creates a fee-source export task with the complete active filters and typed booleans', async () => { + const wrapper = await mountView(['platform:finance:customer-arap:list', 'platform:finance:customer-arap:export', 'platform:download:list']) + const vm = wrapper.vm as unknown as { filters: Record; exportFeeSources(): Promise } + Object.assign(vm.filters, { + customerId: '10001', direction: 'RECEIVABLE', reviewStatus: 'APPROVED', settlementIncluded: 'true', + statementGenerated: 'false', businessRange: ['2026-08-01', '2026-08-31'], keyword: ' 运费 ', + }) + await vm.exportFeeSources() + expect(createPlatformExportTask).toHaveBeenCalledWith({ + exportType: 'FINANCE_FEE_SOURCE_LIST', format: 'XLSX', taskName: '客户应收应付费用来源', + parameters: { + customerId: '10001', direction: 'RECEIVABLE', reviewStatus: 'APPROVED', settlementIncluded: true, + statementGenerated: false, businessDateFrom: '2026-08-01', businessDateTo: '2026-08-31', keyword: '运费', + }, + }) + expect(showExportTaskCreatedDialog).toHaveBeenCalledWith({ taskName: '客户应收应付费用来源', taskNo: 'EXP-3' }) + }) + + it('does not expose fee-source export without download-center access', async () => { + const wrapper = await mountView(['platform:finance:customer-arap:list', 'platform:finance:customer-arap:export']) + expect(wrapper.findAll('button').map((button) => button.text())).not.toContain('导出Excel') + expect(createPlatformExportTask).not.toHaveBeenCalled() + }) + + it('omits every empty fee-source filter instead of exporting the current table page', async () => { + const wrapper = await mountView(['platform:finance:customer-arap:list', 'platform:finance:customer-arap:export', 'platform:download:list']) + await (wrapper.vm as unknown as { exportFeeSources(): Promise }).exportFeeSources() + expect(createPlatformExportTask).toHaveBeenCalledWith({ + exportType: 'FINANCE_FEE_SOURCE_LIST', format: 'XLSX', taskName: '客户应收应付费用来源', parameters: {}, + }) + }) }) diff --git a/src/modules/finance/pages/FinanceCustomerArapView.vue b/src/modules/finance/pages/FinanceCustomerArapView.vue index 76a7862a..57ee83a4 100644 --- a/src/modules/finance/pages/FinanceCustomerArapView.vue +++ b/src/modules/finance/pages/FinanceCustomerArapView.vue @@ -1,11 +1,12 @@ + + + + + + diff --git a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue index 862ae20d..66f64f85 100644 --- a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue +++ b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue @@ -24,6 +24,8 @@ const props = defineProps<{ const emit = defineEmits<{ changed: [] updated: [] + saved: [] + submitted: [] }>() interface EditableMeasurementItem extends SaveInboundMeasurementItemRequest { @@ -360,6 +362,7 @@ async function saveDraft() { form.version = version ElMessage.success('确认箱数已保存,计算结果已由服务端更新') await loadMeasurement() + emit('saved') emit('changed') } catch (error) { const details = setOperationError(error, '测量草稿保存失败,请检查后重试') @@ -394,6 +397,7 @@ async function submitForReview() { }) ElMessage.success('测量数据已提交审核') await loadMeasurement() + emit('submitted') emit('changed') } catch (error) { const details = setOperationError(error, '测量数据提交失败,请检查图片和称重结果后重试') @@ -459,7 +463,7 @@ watch( onBeforeUnmount(releaseImagePreview) -defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit }) +defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })