From 24fa3d1c9d78c8083d6c13f7c5be7d5f58d0246b Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 18 Sep 2026 16:48:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/ContainersView.test.ts | 10 +-- .../pages/ContainersView.vue | 28 ++++++++- .../customers/pages/CustomersView.test.ts | 2 +- src/modules/customers/pages/CustomersView.vue | 48 +++++++------- .../pages/FinanceCustomerAccountsView.test.ts | 18 +++--- .../pages/FinanceCustomerAccountsView.vue | 20 +++++- .../pages/FinanceCustomerArapView.test.ts | 16 ++--- .../finance/pages/FinanceCustomerArapView.vue | 63 +++++++++++-------- .../FinanceCustomerStatementsView.test.ts | 20 +++--- .../pages/FinanceCustomerStatementsView.vue | 42 ++++++++----- src/modules/iam/pages/PermissionsView.test.ts | 17 +++-- src/modules/iam/pages/PermissionsView.vue | 43 +++++-------- .../pages/LastMileOrdersView.behavior.test.ts | 2 +- .../pages/LastMileOrdersView.vue | 37 ++++++----- .../components/InboundFeeDetailManager.vue | 39 ++++++------ .../pages/UnifiedOrdersView.test.ts | 18 +++--- .../pages/UnifiedOrdersView.vue | 40 ++++++------ src/shared/components/RowActions.test.ts | 27 ++++++++ src/shared/components/RowActions.vue | 6 +- 19 files changed, 286 insertions(+), 210 deletions(-) diff --git a/src/modules/container-loading/pages/ContainersView.test.ts b/src/modules/container-loading/pages/ContainersView.test.ts index 63ce2d6e..e1334e6d 100644 --- a/src/modules/container-loading/pages/ContainersView.test.ts +++ b/src/modules/container-loading/pages/ContainersView.test.ts @@ -374,7 +374,7 @@ describe('ContainersView', () => { await flushPromises() expect(wrapper.find('button[aria-label="查看 CB-1"]').exists()).toBe(true) - expect(wrapper.find('.el-table__body button[aria-label="更多货柜操作"]').exists()).toBe(true) + expect(wrapper.find('.el-table__body button[aria-label="更多货柜操作"]').exists()).toBe(false) expect(wrapper.findAll('button').some((button) => button.text().trim() === '编辑')).toBe(false) }) @@ -453,14 +453,16 @@ describe('ContainersView', () => { it('renders container business actions as buttons without changing the transport gate', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8') - expect(source).toContain('size="small" type="primary" plain :aria-label="containerViewLabel') - expect(source).toContain("hasAction(row as ContainerWorkbenchRow, 'EDIT_CONTAINER')") + expect(source).toContain("import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'") + expect(source).toContain("{ key: 'view', label: '查看', ariaLabel: containerViewLabel(row), priority: 500") + expect(source).toContain("hasAction(row, 'EDIT_CONTAINER')") + expect(source).toContain('); return } if (command === 'documents' && isOverseas.value && hasAction(row as ContainerWorkbenchRow, 'MANAGE_DESTINATION_DOCUMENTS')) void router.push({ name: 'overseas-container-detail', params: { containerId: row.id }, query: { action: command } }) } +function containerRowActions(row: ContainerWorkbenchRow): RowActionItem[] { + const processLabel = row.planStatus === 'EDITING' ? '装柜' : row.planStatus === 'LOADING' ? '预装清单' : row.planStatus === 'COMPLETED' ? '装柜清单' : '' + const canOpenProcess = row.planStatus === 'EDITING' ? canSmartLoading.value : row.planStatus === 'LOADING' ? canViewPreload.value : row.planStatus === 'COMPLETED' ? canViewLoadingList.value : false + return [ + { key: 'view', label: '查看', ariaLabel: containerViewLabel(row), priority: 500, visible: canView.value }, + { key: 'edit', label: '编辑', priority: 450, visible: isDomestic.value && canEdit.value && hasAction(row, 'EDIT_CONTAINER') }, + { key: 'process', label: processLabel, priority: 400, tone: 'primary', visible: isDomestic.value && Boolean(processLabel) && canOpenProcess }, + { key: 'empty-return', label: '确认还柜', priority: 350, tone: 'primary', visible: canDrayage(row, 'RECORD_EMPTY_RETURN') }, + { key: 'send-email', label: '发送邮件', priority: 300, tone: 'primary', visible: canDrayage(row, 'SEND_PICKUP_EMAIL'), loading: drayageSavingIds.value.includes(row.id) }, + { key: 'overview', label: '轨迹与日志', priority: 200, visible: canView.value }, + { key: 'email-history', label: '邮件记录', priority: 150, visible: canView.value && isOverseas.value }, + { key: 'drayage-history', label: '码头拖车记录', priority: 140, visible: canView.value && isOverseas.value }, + { key: 'documents', label: '清关 / DO / PU', priority: 130, visible: canView.value && isOverseas.value && canOverseasDocumentEdit.value && hasAction(row, 'MANAGE_DESTINATION_DOCUMENTS') }, + { key: 'link', label: '关联已放舱订舱', priority: 120, visible: canView.value && isDomestic.value && canEdit.value && hasAction(row, 'EDIT_CONTAINER') && row.sourceType === 'SIMULATION' && !row.oceanBookingId }, + ] +} +function handleContainerRowAction(command: string, row: ContainerWorkbenchRow) { + if (command === 'view') openDetail(row) + else if (command === 'edit') void openEdit(row) + else if (command === 'process') goContainerProcess(row) + else if (command === 'empty-return') openDrayageAction(row, 'RECORD_EMPTY_RETURN') + else if (command === 'send-email') void sendPickupEmail(row) + else handleMoreAction(command, row) +} async function openEdit(source: object) { const row = source as ContainerBookingResource editorError.value = null @@ -721,7 +745,7 @@ onMounted(async () => { await Promise.all([loadCustomers(), loadRows()]) })
- + diff --git a/src/modules/customers/pages/CustomersView.test.ts b/src/modules/customers/pages/CustomersView.test.ts index 604fbf32..486bf67c 100644 --- a/src/modules/customers/pages/CustomersView.test.ts +++ b/src/modules/customers/pages/CustomersView.test.ts @@ -822,7 +822,7 @@ describe('CustomersView', () => { await flushPromises() expect(document.querySelectorAll('.el-table__body button[aria-label^="更多客户操作:"]').length).toBe(3) - expect(document.querySelectorAll('.el-table__body .table-actions > .el-button').length).toBe(9) + expect(document.querySelectorAll('.el-table__body .row-actions > .el-button').length).toBe(9) }) it('keeps the action row usable when all customer identifiers are missing', async () => { diff --git a/src/modules/customers/pages/CustomersView.vue b/src/modules/customers/pages/CustomersView.vue index 61558dba..c2ed4ba4 100644 --- a/src/modules/customers/pages/CustomersView.vue +++ b/src/modules/customers/pages/CustomersView.vue @@ -3,9 +3,10 @@ import '@/styles/adminDensity.css' import '@/modules/customers/customerDensity.css' import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { Building2, CheckCircle2, CircleCheck, FileCheck2, FileText, MoreHorizontal, ShieldCheck, ShieldX } from '@lucide/vue' +import { Building2, CheckCircle2, CircleCheck, FileCheck2, FileText, ShieldCheck, ShieldX } from '@lucide/vue' import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus' import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http' +import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue' import { createPlatformExportTask } from '@/modules/downloads' import { approveEnterpriseReview, @@ -678,17 +679,6 @@ function customerSecondaryActions(row: PlatformTenantResource): CustomerRowActio ...(canArchive(row) ? ['delete' as const] : []), ] } -function customerPrimaryActionCount(row: PlatformTenantResource) { - return Number(access.can('platform:customer:list')) - + Number(access.can('platform:customer:edit') && row.status !== 'ARCHIVED') - + Number(hasReviewAction(row)) -} -function customerDirectSecondaryActions(row: PlatformTenantResource) { - return customerSecondaryActions(row).slice(0, Math.max(0, 3 - customerPrimaryActionCount(row))) -} -function customerOverflowActions(row: PlatformTenantResource) { - return customerSecondaryActions(row).slice(Math.max(0, 3 - customerPrimaryActionCount(row))) -} function customerMoreActionsLabel(row: PlatformTenantResource) { const identity = row.tenantName || row.tenantCode || String(row.id || '当前客户') return `更多客户操作:${identity}` @@ -710,6 +700,27 @@ function customerActionDisabled(action: CustomerRowAction) { if (action === 'delete') return archiveSubmitting.value return false } +function customerRowActions(row: PlatformTenantResource): RowActionItem[] { + return [ + { key: 'detail', label: '详情', priority: 500, visible: access.can('platform:customer:list') }, + { key: 'edit', label: '编辑', priority: 400, visible: access.can('platform:customer:edit') && row.status !== 'ARCHIVED' }, + { key: 'review', label: '审核', priority: 300, tone: 'warning', visible: hasReviewAction(row), disabled: reviewSubmitting.value }, + ...customerSecondaryActions(row).map((action, index): RowActionItem => ({ + key: action, + label: customerActionLabel(row, action), + priority: 200 - index, + tone: customerActionType(row, action), + disabled: customerActionDisabled(action), + divided: action === 'delete', + })), + ] +} +function handleCustomerRowAction(row: PlatformTenantResource, command: string) { + if (command === 'detail') showDetail(row) + else if (command === 'edit') openEdit(row) + else if (command === 'review') openDecision(row, access.can('platform:customer:approve') ? 'approve' : 'reject') + else handleMoreAction(row, command) +} function handleMoreAction(row: PlatformTenantResource, command: string) { if (command === 'reset-password') openResetPassword(row) if (command === 'impersonate') openImpersonation(row) @@ -1059,18 +1070,7 @@ onMounted(() => { void loadCustomers() }) - + diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts index c0d5b51c..04678b36 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts @@ -107,15 +107,13 @@ describe('FinanceCustomerAccountsView delivery', () => { it('keeps high-frequency account actions visible and groups the remaining actions', () => { expect(page).toContain("{ key: 'actions', label: '操作', width: 290, fixed: 'left', slotName: 'actions' }") - expect(page).toContain('@click="openAccount(row, false)">详情') - expect(page).toContain("openAdjustment(row as FinanceAccountSummary, 'RECHARGE')") - expect(page).toContain("openAdjustment(row as FinanceAccountSummary, 'DEDUCTION')") - expect(page).toContain('aria-label="更多客户账户操作"') - expect(page).toContain('授信') - expect(page).toContain('账期') - expect(page).not.toContain('') - expect(page).toContain('class="row-actions density-actions"') - expect(page).toContain('white-space: nowrap') + expect(page).toContain("import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'") + expect(page).toContain("{ key: 'detail', label: '详情', priority: 300 }") + expect(page).toContain("{ key: 'recharge', label: '充值', priority: 250") + expect(page).toContain("{ key: 'deduction', label: '扣款', priority: 200") + expect(page).toContain("{ key: 'credit', label: '授信', priority: 100") + expect(page).toContain("{ key: 'terms', label: '账期', visible: canUpdateTerms.value") + expect(page).toContain(' { @@ -134,7 +132,7 @@ describe('FinanceCustomerAccountsView delivery', () => { expect(actions.findAll('button').map(button => button.text().trim())).toEqual(labels) expect(actions.find('.el-dropdown').exists()).toBe(overflow) if (overflow) { - const trigger = actions.get('button[aria-label="更多客户账户操作"]') + const trigger = actions.get('button[aria-label^="更多客户账户操作"]') expect(trigger.text()).toBe('') expect(trigger.find('svg').exists()).toBe(true) } diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.vue b/src/modules/finance/pages/FinanceCustomerAccountsView.vue index f161c4b5..76a2356a 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.vue +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.vue @@ -3,7 +3,7 @@ import '@/styles/adminDensity.css' import '../financeDensity.css' import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { ArrowLeft, CreditCard, Download, MinusCircle, MoreHorizontal, PlusCircle, Save } from '@lucide/vue' +import { ArrowLeft, CreditCard, Download, MinusCircle, PlusCircle, Save } from '@lucide/vue' import { ElMessage } from 'element-plus' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { createPlatformExportTask } from '@/modules/downloads' @@ -18,6 +18,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 RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue' import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog' import { adjustFinanceAccount, downloadFinanceRechargeVoucher, getFinanceCustomerTerms, listFinanceAccountLedger, listFinanceAccounts, listFinanceRechargeRequests, reviewFinanceRechargeRequest, updateFinanceCreditLimit, updateFinanceCustomerTerms } from '../api/finance' import { financeBusinessTypeLabel } from '../status' @@ -270,6 +271,21 @@ function handleAccountMore(command: string, account: FinanceAccountSummary) { if (command === 'credit') openCreditLimit(account) if (command === 'terms') void openAccount(account as unknown as Record, true) } +function accountActions(account: FinanceAccountSummary): RowActionItem[] { + return [ + { key: 'detail', label: '详情', priority: 300 }, + { key: 'recharge', label: '充值', priority: 250, visible: canAdjust.value }, + { key: 'deduction', label: '扣款', priority: 200, tone: 'danger', visible: canAdjust.value }, + { key: 'credit', label: '授信', priority: 100, visible: canAdjust.value }, + { key: 'terms', label: '账期', visible: canUpdateTerms.value, divided: canAdjust.value }, + ] +} +function handleAccountAction(command: string, account: FinanceAccountSummary) { + if (command === 'detail') void openAccount(account as unknown as Record, false) + else if (command === 'recharge') openAdjustment(account, 'RECHARGE') + else if (command === 'deduction') openAdjustment(account, 'DEDUCTION') + else handleAccountMore(command, account) +} async function submitCreditLimit() { if (!adjustmentAccount.value) return const raw = creditLimitForm.amount.trim() @@ -425,7 +441,7 @@ onBeforeUnmount(clearVoucherPreview)
- + diff --git a/src/modules/finance/pages/FinanceCustomerArapView.test.ts b/src/modules/finance/pages/FinanceCustomerArapView.test.ts index 3885197f..ceb2f5eb 100644 --- a/src/modules/finance/pages/FinanceCustomerArapView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerArapView.test.ts @@ -113,7 +113,7 @@ describe('FinanceCustomerArapView G2 operations', () => { expect(actions.findAll('button').map(button => button.text().trim())).toEqual(labels) expect(actions.find('.el-dropdown').exists()).toBe(overflow) if (overflow) { - const trigger = actions.get('button[title="更多费用来源操作"]') + const trigger = actions.get('button[title^="更多费用来源操作"]') expect(trigger.text()).toBe('') await trigger.trigger('click') await flushPromises() @@ -268,18 +268,18 @@ describe('FinanceCustomerArapView G2 operations', () => { expect(page).not.toContain('max-width: 520px') }) - it('renders permitted fee-source row operations as shared outlined text buttons', () => { + it('renders permitted fee-source row operations through the shared row-actions component', () => { const page = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerArapView.vue'), 'utf8') - expect(page).toContain('@click="openDetail(row)">详情') - expect(page).toContain("@click=\"openMutation(row, 'SUBMIT')\"") - expect(page).toContain("@click=\"openMutation(row, 'ADJUST')\"") + expect(page).toContain("import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'") + expect(page).toContain("{ key: 'detail', label: '详情', priority: 300") + expect(page).toContain("{ key: 'submit', label: '提交审核', priority: 200") + expect(page).toContain("{ key: 'adjust', label: '调整', priority: 150") + expect(page).toContain("{ key: 'void', label: '作废', tone: 'danger'") expect(page).toContain('!isUpstreamSource(row)') expect(page).toContain('!isUpstreamSource(detail)') expect(page).toContain("['INBOUND_FEE', 'CONTAINER_FEE'].includes(type)") expect(page).toContain("=== 'LASTLEG'") - const actions = page.slice(page.indexOf('