diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index 3e891b8a..68c90bac 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -121,9 +121,10 @@ describe('adminNavigation', () => { it('shows the five formal finance pages with their real backend permissions', () => { const filtered = filterAdminNavigation(adminNavigation, new Set([ + 'platform:finance:account:view', 'platform:finance:bill:view', - 'platform:finance:receipt:view', - 'platform:finance:cost:view', + 'platform:finance:settlement-ar:view', + 'platform:finance:settlement-ap:view', ])) expect(getNavigationLeaves(filtered).map((item) => item.path).filter((path) => path.startsWith('/finance/'))).toEqual([ diff --git a/src/modules/customs/pages/CustomsCasesView.test.ts b/src/modules/customs/pages/CustomsCasesView.test.ts index 09ee2bd0..d75d5e28 100644 --- a/src/modules/customs/pages/CustomsCasesView.test.ts +++ b/src/modules/customs/pages/CustomsCasesView.test.ts @@ -33,4 +33,8 @@ describe('CustomsCasesView', () => { expect(source).toContain('max-height:90vh') expect(source).toContain('overflow-y:auto') }) + it('uses a bordered view action instead of a text link', () => { + expect(source).toContain('size="small" type="primary" plain') + expect(source).not.toContain("customs:view')\" link") + }) }) diff --git a/src/modules/customs/pages/CustomsCasesView.vue b/src/modules/customs/pages/CustomsCasesView.vue index c000f2a6..225d618c 100644 --- a/src/modules/customs/pages/CustomsCasesView.vue +++ b/src/modules/customs/pages/CustomsCasesView.vue @@ -27,6 +27,6 @@ async function openDetail(id: string) { detailOpen.value = true; detailLoading.v async function submit() { if (!detail.value || !submitForm.customsAgentName.trim()) return; actionError.value = null; try { await submitCustomsCase(detail.value.customsCase.id, { ...submitForm, expectedVersion: detail.value.customsCase.version }); ElMessage.success('已登记线下提交批次'); detail.value = await getCustomsCase(detail.value.customsCase.id) } catch (cause) { actionError.value = getApiErrorDetails(cause); if (actionError.value.code === 'A0409') detail.value = await getCustomsCase(detail.value.customsCase.id) } } watch([page, pageSize], () => void load()); onMounted(load) -查看 +查看 {{ detail.customsCase.customsNo }}{{ detail.customsCase.checklistCode }} / {{ detail.customsCase.checklistVersionNo }}{{ detail.documents.length }} / {{ detail.receipts.length }}登记线下提交状态历史{{ customsHistoryActionLabel(entry.action) }}:{{ customsStatusLabel(entry.fromStatus) }} → {{ customsStatusLabel(entry.toStatus) }}{{ entry.note || '' }} diff --git a/src/modules/fee-items/pages/FeeItemsView.test.ts b/src/modules/fee-items/pages/FeeItemsView.test.ts index d4ebeb3f..3f0c3335 100644 --- a/src/modules/fee-items/pages/FeeItemsView.test.ts +++ b/src/modules/fee-items/pages/FeeItemsView.test.ts @@ -100,9 +100,14 @@ describe('FeeItemsView public master data and access', () => { }) it('keeps all three row actions visible without a premature more menu', () => { + expect(source).toContain('size="small" plain type="primary"') + expect(source).toContain('size="small"\n plain\n :type=') + expect(source).toContain('size="small" plain type="danger"') expect(source).toContain('编辑') expect(source).toContain('') expect(source).toContain('删除') expect(source).not.toContain('MoreHorizontal') + expect(source).not.toContain("access.can('firstleg:fee-item:edit')\" link") + expect(source).not.toContain("access.can('firstleg:fee-item:delete')\" link") }) }) diff --git a/src/modules/fee-items/pages/FeeItemsView.vue b/src/modules/fee-items/pages/FeeItemsView.vue index 66b1acb4..a02bab15 100644 --- a/src/modules/fee-items/pages/FeeItemsView.vue +++ b/src/modules/fee-items/pages/FeeItemsView.vue @@ -372,16 +372,17 @@ onMounted(() => { - 编辑 + 编辑 {{ (row as PlatformFeeItemResource).status === 'ACTIVE' ? '停用' : '启用' }} - 删除 + 删除 仅查看 diff --git a/src/modules/iam/pages/InternalUsersView.test.ts b/src/modules/iam/pages/InternalUsersView.test.ts index f54f17ae..1d6afc75 100644 --- a/src/modules/iam/pages/InternalUsersView.test.ts +++ b/src/modules/iam/pages/InternalUsersView.test.ts @@ -74,6 +74,14 @@ enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) describe('InternalUsersView platform internal user management', () => { + it('renders row business actions as colored outlined buttons with prototype icons', () => { + expect(internalUsersSource).toContain('size="small" plain type="primary" @click="openEdit') + expect(internalUsersSource).toContain('size="small" plain :type="(row as PlatformManagedUser).status') + expect(internalUsersSource).toContain('编辑') + expect(internalUsersSource).toContain('') + expect(internalUsersSource).not.toContain('v-if="canChangeStatus" link') + }) + it('uses the prototype-sized centered editor with a scrollable body and mobile single column', () => { expect(internalUsersSource).toContain('width="min(650px, calc(100vw - 28px))"') expect(internalUsersSource).toContain(':global(.internal-user-editor-dialog .el-dialog__body) { min-height: 0; overflow-y: auto; }') diff --git a/src/modules/iam/pages/InternalUsersView.vue b/src/modules/iam/pages/InternalUsersView.vue index 4ee9f115..b19b6604 100644 --- a/src/modules/iam/pages/InternalUsersView.vue +++ b/src/modules/iam/pages/InternalUsersView.vue @@ -324,10 +324,10 @@ onMounted(loadInitialData) 新增用户 - + 编辑 - { + it('uses outlined business action buttons while keeping tree view helpers as links', () => { + expect(permissionsSource).toContain('size="small" plain type="primary" @click="openCreate((row as Permission).id)"') + expect(permissionsSource).toContain('size="small" plain type="primary" :disabled="rowActionId') + expect(permissionsSource).toContain('size="small" plain :type="(row as Permission).status') + expect(permissionsSource).toContain('size="small" plain type="danger"') + expect(permissionsSource).toContain('size="small" plain :aria-label="moreActionsLabel(row as Permission)"') + expect(permissionsSource).toContain('添加下级') + expect(permissionsSource).toContain('编辑') + expect(permissionsSource).toContain('') + expect(permissionsSource).toContain('删除') + expect(permissionsSource).toContain('') + expect(permissionsSource).toContain('展开全部') + expect(permissionsSource).not.toContain('canAddChild(row as Permission)" link') + expect(permissionsSource).not.toContain('canEditRow(row as Permission)" link') + expect(permissionsSource).not.toContain('canChangeStatusRow(row as Permission)" link') + }) + beforeEach(() => { vi.mocked(listPlatformPermissions).mockReset() }) diff --git a/src/modules/iam/pages/PermissionsView.vue b/src/modules/iam/pages/PermissionsView.vue index e2b31f27..38a2eb9d 100644 --- a/src/modules/iam/pages/PermissionsView.vue +++ b/src/modules/iam/pages/PermissionsView.vue @@ -281,12 +281,12 @@ onMounted(loadPermissions) 新增权限 - 添加下级 - 编辑 - {{ (row as Permission).status === '启用' ? '停用' : '启用' }} - 删除 + 添加下级 + 编辑 + {{ (row as Permission).status === '启用' ? '停用' : '启用' }} + 删除 command === 'delete' && removePermission(row as Permission)"> - + 删除 diff --git a/src/modules/master-data/counterparties/pages/PartnersView.test.ts b/src/modules/master-data/counterparties/pages/PartnersView.test.ts index df2671db..d5578f8b 100644 --- a/src/modules/master-data/counterparties/pages/PartnersView.test.ts +++ b/src/modules/master-data/counterparties/pages/PartnersView.test.ts @@ -247,9 +247,13 @@ describe('PartnersView real API integration', () => { }) it('keeps all three row actions visible without a premature more menu', () => { + expect(source).toContain('详情') expect(source).toContain('编辑') expect(source).toContain('') expect(source).not.toContain('MoreHorizontal') + expect(source).not.toContain(' { 批量导入新增往来单位 - 详情 - 编辑 - {{ (row as PlatformPartnerResource).status === 'ACTIVE' ? '停用' : '启用' }} + 详情 + 编辑 + {{ (row as PlatformPartnerResource).status === 'ACTIVE' ? '停用' : '启用' }} {{ (row as PlatformPartnerResource).partnerName }} diff --git a/src/modules/master-data/express-companies/pages/ExpressCompaniesView.test.ts b/src/modules/master-data/express-companies/pages/ExpressCompaniesView.test.ts index 8efd458a..7808b3c1 100644 --- a/src/modules/master-data/express-companies/pages/ExpressCompaniesView.test.ts +++ b/src/modules/master-data/express-companies/pages/ExpressCompaniesView.test.ts @@ -73,6 +73,15 @@ enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) describe('ExpressCompaniesView query and table migration', () => { + it('renders edit and status as colored outlined buttons with prototype icons', () => { + expect(expressCompaniesSource).toContain('size="small"\n plain\n type="primary"') + expect(expressCompaniesSource).toContain('size="small"\n plain\n :disabled="loading"\n :type=') + expect(expressCompaniesSource).toContain('编辑') + expect(expressCompaniesSource).toContain('') + expect(expressCompaniesSource).not.toContain("access.can('platform:express:edit')\"\n link") + expect(expressCompaniesSource).not.toContain("access.can('platform:express:status')\"\n link") + }) + it('keeps operation first and exposes the prototype business columns separately', () => { const actionsIndex = expressCompaniesSource.indexOf("{ key: 'actions', label: '操作'") const nameIndex = expressCompaniesSource.indexOf("{ key: 'company', label: '公司名称'") diff --git a/src/modules/master-data/express-companies/pages/ExpressCompaniesView.vue b/src/modules/master-data/express-companies/pages/ExpressCompaniesView.vue index a7d9b2d3..b372fdc2 100644 --- a/src/modules/master-data/express-companies/pages/ExpressCompaniesView.vue +++ b/src/modules/master-data/express-companies/pages/ExpressCompaniesView.vue @@ -314,7 +314,8 @@ onMounted(() => { { >编辑 { expect(source).toContain('max-height: 90vh') expect(source).toContain('overflow-y: auto') }) + it('uses semantic bordered and filled business buttons instead of text links', () => { + const source = readFileSync(resolve(process.cwd(), 'src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue'), 'utf8') + expect(source).toContain('size="small" type="primary" plain') + expect(source).toContain('size="small" type="primary" @click="action') + expect(source).toContain('size="small" type="danger" plain') + expect(source).not.toContain("outbound-dispatch:view')\" link") + }) }) diff --git a/src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue b/src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue index 9bb361fa..09b4d360 100644 --- a/src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue +++ b/src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue @@ -189,7 +189,7 @@ onBeforeUnmount(() => { disposed = true; listRequestId += 1; optionRequestId += - 查看确认撤销 + 查看确认撤销 {{ row.gate?.passed ? '通过' : '未通过' }} @@ -206,6 +206,7 @@ onBeforeUnmount(() => { disposed = true; listRequestId += 1; optionRequestId += h3 { margin: 20px 0 12px; } small { display: block; color: var(--cc-muted); } .detail-loading { padding: 12px 0; } +.table-actions { display: flex; align-items: center; gap: 6px; }.table-actions :deep(.el-button) { margin-left: 0; } :global(.outbound-detail-dialog.el-dialog) { max-height: 90vh; overflow: hidden; } :global(.outbound-detail-dialog .el-dialog__body) { max-height: calc(90vh - 120px); overflow-y: auto; } diff --git a/src/modules/port-schedules/pages/PortSchedulesView.test.ts b/src/modules/port-schedules/pages/PortSchedulesView.test.ts index fb6b4ae8..5b6e5f18 100644 --- a/src/modules/port-schedules/pages/PortSchedulesView.test.ts +++ b/src/modules/port-schedules/pages/PortSchedulesView.test.ts @@ -73,4 +73,10 @@ describe('PortSchedulesView', () => { expect(source).toContain('width:min(820px,calc(100vw - 28px))!important') expect(source).toContain('max-height:90vh') }) + it('uses bordered business action buttons while keeping retry actions as links', () => { + expect(source).toContain('size="small" type="primary" plain') + expect(source).toContain('size="small" @click="openSchedule') + expect(source).not.toContain("port-schedule:view')\" link") + expect(source).toContain(' void load()); onMounted(load) 新增船期 - 查看编辑 + 查看编辑 {{ row.schedule.vesselName }} / {{ row.schedule.voyageNo }}{{ formatDateTime(row.schedule.observedAt) }}{{ formatDateTime(row.schedule.updatedAt) }}{{ row.expired ? '已过期' : '有效' }} diff --git a/src/modules/trucking/pages/TruckAppointmentsView.test.ts b/src/modules/trucking/pages/TruckAppointmentsView.test.ts index 7774b30e..5b675496 100644 --- a/src/modules/trucking/pages/TruckAppointmentsView.test.ts +++ b/src/modules/trucking/pages/TruckAppointmentsView.test.ts @@ -119,4 +119,9 @@ describe('TruckAppointmentsView', () => { expect(source).not.toContain("key: 'payableAmount'") expect(source).not.toContain("key: 'receivableAmount'") }) + it('uses bordered list actions instead of text links', () => { + expect(source).toContain('size="small" type="primary" plain') + expect(source).toContain('size="small" :aria-label="appointmentActionLabel(\'编辑\'') + expect(source).not.toContain("truck-appointment:view')\" link") + }) }) diff --git a/src/modules/trucking/pages/TruckAppointmentsView.vue b/src/modules/trucking/pages/TruckAppointmentsView.vue index dd968a42..827b383a 100644 --- a/src/modules/trucking/pages/TruckAppointmentsView.vue +++ b/src/modules/trucking/pages/TruckAppointmentsView.vue @@ -65,7 +65,7 @@ function openDetail(row: TruckAppointmentResource, edit = false) { router.push({ {{ row.driverName || '—' }}{{ row.driverPhone || '—' }} {{ row.storeName || '—' }} - 详情编辑 + 详情编辑 @@ -75,4 +75,5 @@ function openDetail(row: TruckAppointmentResource, edit = false) { router.push({ .trucking-page { min-width: 0; } .table-panel { overflow:hidden; } small { display:block; color:var(--el-text-color-secondary); margin-top:3px; } +.table-actions { display:flex; align-items:center; gap:6px; }.table-actions :deep(.el-button) { margin-left:0; } diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 01b9746b..40a89852 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -275,4 +275,12 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('真实集装箱号') expect(source).toContain('内部柜次号') }) + + it('uses outlined semantic buttons for row business actions while keeping detail navigation explicit', () => { + const rowActions = source.slice(source.indexOf('详情') + expect(source).toContain(' { 详情 - {{ unifiedOrderActionLabel(action) }} + {{ unifiedOrderActionLabel(action) }} diff --git a/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.test.ts b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.test.ts index e1d65699..8597df71 100644 --- a/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.test.ts +++ b/src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.test.ts @@ -1,4 +1,6 @@ import ElementPlus, { ElMessageBox } from 'element-plus' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' import { createPinia, setActivePinia } from 'pinia' import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -59,6 +61,17 @@ describe('UnifiedOrderWorkflowPanel preload editor', () => { vi.mocked(voidEvidence).mockReset() }) + it('keeps workflow business actions as outlined semantic buttons with icons', () => { + const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/workflows/components/UnifiedOrderWorkflowPanel.vue'), 'utf8') + expect(source).toContain('size="small" plain type="primary"') + expect(source).toContain('size="small" plain type="danger"') + expect(source).toContain('编辑') + expect(source).toContain('查看') + expect(source).toContain('作废') + expect(source).not.toContain(' - 编辑 - 删除 + 编辑 + 删除 {{ formatVolume((row as PreloadItem).volumeCbm) }} 新增明细 @@ -516,8 +516,8 @@ onMounted(() => { void refresh() }) {{ item.fileName || `证据 #${item.id}` }} · 锁码 {{ item.sealNo }} - 查看 - 作废 + 查看 + 作废 尚未上传有效照片 diff --git a/src/shared/components/DataTable.test.ts b/src/shared/components/DataTable.test.ts index 87c4896e..7aa4a16c 100644 --- a/src/shared/components/DataTable.test.ts +++ b/src/shared/components/DataTable.test.ts @@ -8,8 +8,8 @@ import { UI_PREFERENCE_SCOPE, preferenceStorageKey } from '@/shared/utils/uiLayo const TableStub = defineComponent({ template: '', methods: { doLayout() {} } }) const ColumnStub = defineComponent({ inheritAttrs: false, - props: { label: String }, - template: '', + props: { label: String, prop: String }, + template: '', }) const ButtonStub = defineComponent({ template: '' }) const PopoverStub = defineComponent({ template: '' }) @@ -69,6 +69,27 @@ describe('DataTable pagination helpers', () => { describe('DataTable column preferences', () => { beforeEach(() => localStorage.clear()) + it('uses key as the data property for ordinary columns only', () => { + const wrapper = shallowMount(DataTable, { + props: { + data: [{ customerName: '客户A', explicitName: '显式字段' }], + columns: [ + { key: 'actions', label: '操作', slotName: 'actions' }, + { key: 'customerName', label: '客户' }, + { key: 'name', prop: 'explicitName', label: '显式字段' }, + { key: 'selection', type: 'selection', label: '选择' }, + ], + }, + global: { + stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub }, + directives: { loading: {} }, + }, + }) + + const columns = wrapper.findAll('.column-stub') + expect(columns.map((column) => column.attributes('data-prop'))).toEqual([undefined, 'customerName', 'explicitName', undefined]) + }) + it('restores column order and visibility while locking the actions column', async () => { localStorage.setItem(preferenceStorageKey('table', 'user-1', 'orders-table'), JSON.stringify({ version: 1, diff --git a/src/shared/components/DataTable.vue b/src/shared/components/DataTable.vue index eb1977ad..02e076a6 100644 --- a/src/shared/components/DataTable.vue +++ b/src/shared/components/DataTable.vue @@ -232,7 +232,19 @@ watch([page, pageCount], ([value, count]) => { }, { immediate: true }) function columnAttrs({ key, slotName, headerSlotName, hideable, movable, defaultVisible, ...attrs }: DataTableColumn) { - return attrs + const hasDataProperty = attrs.prop !== undefined || attrs.property !== undefined + const isSpecialColumn = slotName || ['selection', 'expand', 'index'].includes(String(attrs.type || '')) + if (hasDataProperty || isSpecialColumn || key === undefined || key === null || key === '') return attrs + const label = String(attrs.label || '') + return { + ...attrs, + prop: String(key), + formatter: attrs.formatter || ((_row: unknown, _column: unknown, value: unknown) => { + if (value === null || value === undefined || value === '') return '—' + if (typeof value === 'number' && /CNY|金额|应收|应付|已收|已付|未收|待收|待付|开账/.test(label)) return value.toFixed(2) + return String(value) + }), + } } function columnRenderKey(column: DataTableColumn, index: number) {