diff --git a/AGENTS.md b/AGENTS.md index 7802e242..4d9ac086 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,8 @@ ## UI 与交互 +- 已向客户讲解或确认过的产品原型是页面排版、配色、间距、尺寸、图标、按钮、表格、Tab 和组件形态的直接视觉事实来源;实现时必须按原型还原,不得脱离原型自行重新设计、添加装饰或替换风格。只有用户当前明确要求、阶段范围或可访问性/真实数据约束与原型冲突时才允许调整,并须先说明具体差异及原因。 +- 原型已经给出明确图标、字符图标或按钮形态时直接使用原型方案,不得以 Lucide、Element Plus 默认样式或个人偏好替换;仅当原型没有定义对应图标时,才按本节的默认图标语义补齐。 - 延续现有后台视觉体系:克制的企业级界面、清晰的信息层级和高密度但可扫描的数据展示。 - 路由级页面标题和说明统一由 `AppLayout` 顶部 `page-heading` 根据路由 `meta.title`、`meta.description` 展示。标准列表页正文不得重复渲染 `PageToolbar`、页面标题或说明,只保留查询区与表格区。`QueryToolbar` 只承载筛选项及其内置查询、重置按钮,不把新增、导入、导出或批量操作放入其 `actions` 插槽;重新执行查询即刷新当前结果,标准列表页任何位置都不得再提供独立刷新按钮。新增、导入、导出和批量操作统一放入 `DataTable` 的 `toolbar-actions` 插槽并靠表格工具栏右侧展示。仅没有查询组件的实时工作台或详情页在业务确实需要主动重载时,才可保留刷新;仅详情页或工作台在路由明确设置 `showPageHeading: false` 时,才可在正文使用独立详情头。 - 标准列表页的查询面板与表格面板使用 `ListPageLayout` 保持单一 `16px` 间距,不再叠加页面级 margin、空白操作区或额外容器间距。`DataTable` 必须位于连续的 `surface-panel table-panel` 中,使右侧业务按钮、列设置、表头和分页属于同一表格面板;不得使用 `toolbar-start` 重复显示列表标题、数量或说明,数量统一放在 `pagination-left`。当工具栏只有右侧操作和列设置时使用紧凑高度,不保留空白的左侧标题区域。 diff --git a/src/modules/customers/pages/CustomerDetailView.test.ts b/src/modules/customers/pages/CustomerDetailView.test.ts index f27fc77e..24cfe20e 100644 --- a/src/modules/customers/pages/CustomerDetailView.test.ts +++ b/src/modules/customers/pages/CustomerDetailView.test.ts @@ -107,7 +107,7 @@ describe('CustomerDetailView', () => { mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() - expect([...document.querySelectorAll('.detail-tabs button')].map((button) => button.textContent?.trim())).toEqual([ + expect([...document.querySelectorAll('.detail-tabs button')].map((button) => button.getAttribute('aria-label'))).toEqual([ '基本信息', '业务概况', '运单记录', '计费规则', ]) expect(document.body.textContent).toContain('企业用户1人') @@ -118,16 +118,14 @@ describe('CustomerDetailView', () => { expect(document.body.textContent).toContain('已替换') expect(document.body.textContent).toContain('12票') - const ordersTab = [...document.querySelectorAll('.detail-tabs button')] - .find((button) => button.textContent?.trim() === '运单记录') + const ordersTab = document.querySelector('.detail-tabs button[aria-label="运单记录"]') ordersTab?.click() await flushPromises() expect(document.body.textContent).toContain('BY20260807001') expect(document.body.textContent).toContain('海运整箱 FCL') expect(listPlatformFirstLegOrders).toHaveBeenCalledWith({ tenantId: tenant.id, page: 1, size: 10 }) - const billingTab = [...document.querySelectorAll('.detail-tabs button')] - .find((button) => button.textContent?.trim() === '计费规则') + const billingTab = document.querySelector('.detail-tabs button[aria-label="计费规则"]') billingTab?.click() await flushPromises() expect(document.body.textContent).toContain('暂无尺寸计算规则查看权限') @@ -143,8 +141,7 @@ describe('CustomerDetailView', () => { mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() - const ordersTab = [...document.querySelectorAll('.detail-tabs button')] - .find((button) => button.textContent?.trim() === '运单记录') + const ordersTab = document.querySelector('.detail-tabs button[aria-label="运单记录"]') ordersTab?.click() await flushPromises() @@ -174,8 +171,7 @@ describe('CustomerDetailView', () => { await flushPromises() expect(listPlatformFirstLegOrders).toHaveBeenCalledTimes(2) - const ordersTab = [...document.querySelectorAll('.detail-tabs button')] - .find((button) => button.textContent?.trim() === '运单记录') + const ordersTab = document.querySelector('.detail-tabs button[aria-label="运单记录"]') ordersTab?.click() await flushPromises() const retryButton = [...document.querySelectorAll('button')] diff --git a/src/modules/customers/pages/CustomerDetailView.vue b/src/modules/customers/pages/CustomerDetailView.vue index 23a1f1b8..bf09e278 100644 --- a/src/modules/customers/pages/CustomerDetailView.vue +++ b/src/modules/customers/pages/CustomerDetailView.vue @@ -2,9 +2,9 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { - ArrowLeft, BarChart3, Box, Building2, ClipboardList, Eye, + ArrowLeft, BarChart3, Building2, Eye, FileCheck2, FileText, Pencil, Power, - Scale, Ship, UserRound, UsersRound, + Ship, UserRound, UsersRound, } from '@lucide/vue' import { ElMessage, ElMessageBox } from 'element-plus' import { @@ -70,11 +70,11 @@ const previewUrl = ref('') const previewKind = ref<'image' | 'pdf'>('image') const tabList = ref(null) -const tabs: Array<{ key: DetailTab; label: string; icon: typeof ClipboardList }> = [ - { key: 'basic', label: '基本信息', icon: ClipboardList }, - { key: 'business', label: '业务概况', icon: BarChart3 }, - { key: 'orders', label: '运单记录', icon: Box }, - { key: 'billing', label: '计费规则', icon: Scale }, +const tabs: Array<{ key: DetailTab; label: string; icon: string }> = [ + { key: 'basic', label: '基本信息', icon: '📋' }, + { key: 'business', label: '业务概况', icon: '📊' }, + { key: 'orders', label: '运单记录', icon: '📦' }, + { key: 'billing', label: '计费规则', icon: '⚖️' }, ] const approvedQualifications = computed(() => qualifications.value.filter((item) => item.status === 'APPROVED').length) @@ -345,7 +345,7 @@ watch(orderPage, (page, previousPage) => {
- +
@@ -453,6 +453,7 @@ watch(orderPage, (page, previousPage) => { .overview-strip small { margin-left:5px; color:#475467; font-size:12px; font-weight:600; } .detail-tabs { display:flex; gap:0; overflow-x:auto; overflow-y:hidden; border-bottom:1px solid rgba(228,228,231,.5); border-radius:20px 20px 0 0; background:rgba(255,255,255,.95); box-shadow:0 -4px 16px rgba(0,0,0,.04); scrollbar-width:thin; } .detail-tabs button { display:inline-flex; min-width:max-content; min-height:0; flex:0 0 auto; align-items:center; justify-content:center; gap:8px; padding:16px 28px; border:0; border-bottom:3px solid transparent; background:transparent; color:#71717a; font-size:15px; font-weight:600; white-space:nowrap; cursor:pointer; transition:all .3s cubic-bezier(.4,0,.2,1); } +.detail-tab-icon { font-size:18px; line-height:1; } .detail-tabs button:hover { background:rgba(102,126,234,.05); color:#667eea; } .detail-tabs button.active { border-bottom-color:#667eea; background:rgba(102,126,234,.08); color:#667eea; font-weight:800; } .tab-panel { min-height:400px; padding-top:18px; } diff --git a/src/modules/customers/pages/CustomersView.vue b/src/modules/customers/pages/CustomersView.vue index 644bc729..966afa12 100644 --- a/src/modules/customers/pages/CustomersView.vue +++ b/src/modules/customers/pages/CustomersView.vue @@ -187,7 +187,7 @@ const creditCodeEstablished = ref(false) const unifiedSocialCreditCodePattern = /^[0-9A-Z]{18}$/ const customerColumns: DataTableColumn[] = [ - { key: 'actions', label: '操作', width: 620, slotName: 'actions' }, + { key: 'actions', label: '操作', width: 500, slotName: 'actions' }, { key: 'tenantCode', prop: 'tenantCode', label: '客户编号', width: 220, className: 'customer-code-column' }, { key: 'company', label: '公司名称', width: 250, slotName: 'company' }, { key: 'unifiedSocialCreditCode', prop: 'unifiedSocialCreditCode', label: '统一社会信用代码', width: 220, formatter: (row: PlatformTenantResource) => displayText(row.unifiedSocialCreditCode) },