diff --git a/src/modules/customers/pages/CustomerDetailView.test.ts b/src/modules/customers/pages/CustomerDetailView.test.ts index e23c7fcb..b1ec158a 100644 --- a/src/modules/customers/pages/CustomerDetailView.test.ts +++ b/src/modules/customers/pages/CustomerDetailView.test.ts @@ -108,7 +108,7 @@ describe('CustomerDetailView', () => { await flushPromises() expect([...document.querySelectorAll('.detail-tabs button')].map((button) => button.textContent?.trim())).toEqual([ - '基本信息', '业务概况', '运单记录', '财务账单', '操作日志', '计费规则', + '基本信息', '业务概况', '运单记录', '计费规则', ]) expect(document.body.textContent).toContain('企业用户1人') expect(document.body.textContent).toContain('1 人正常使用') diff --git a/src/modules/customers/pages/CustomerDetailView.vue b/src/modules/customers/pages/CustomerDetailView.vue index e1b3c599..84766e78 100644 --- a/src/modules/customers/pages/CustomerDetailView.vue +++ b/src/modules/customers/pages/CustomerDetailView.vue @@ -2,8 +2,8 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { - ArrowLeft, BarChart3, Box, Building2, CircleDollarSign, ClipboardList, Eye, - FileCheck2, FileText, History, Pencil, Power, ReceiptText, + ArrowLeft, BarChart3, Box, Building2, ClipboardList, Eye, + FileCheck2, FileText, Pencil, Power, Scale, Ship, UserRound, UsersRound, } from '@lucide/vue' import { ElMessage, ElMessageBox } from 'element-plus' @@ -38,7 +38,7 @@ import { useAccessPreviewStore } from '@/core/access/accessPreview' import CustomerBillingRulesPanel from '@/modules/customers/components/CustomerBillingRulesPanel.vue' import CustomerOverseasStorageFeePanel from '@/modules/customers/CustomerOverseasStorageFeePanel.vue' -type DetailTab = 'basic' | 'business' | 'orders' | 'finance' | 'logs' | 'billing' +type DetailTab = 'basic' | 'business' | 'orders' | 'billing' type PreviewState = { url: string; kind: 'image' | 'pdf'; loading: boolean; failed: boolean } const route = useRoute() @@ -74,8 +74,6 @@ 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: 'finance', label: '财务账单', icon: CircleDollarSign }, - { key: 'logs', label: '操作日志', icon: History }, { key: 'billing', label: '计费规则', icon: Scale }, ] @@ -385,7 +383,7 @@ watch(orderPage, (page, previousPage) => {
-

企业用户

当前客户租户下的登录账号

+

企业用户

当前客户的登录账号

@@ -427,8 +425,6 @@ watch(orderPage, (page, previousPage) => {
暂无运单查看权限

请联系管理员申请头程订单查看权限。

-

财务账单

客户应收、回款与对账记录

财务账单尚未接入

当前阶段没有可用的客户账单 API,页面不会使用原型中的演示金额。

-

操作日志

客户档案与状态变更记录

操作日志尚未接入

等待审计服务提供按租户查询的只读接口后展示真实操作记录。

@@ -455,7 +451,7 @@ watch(orderPage, (page, previousPage) => { .overview-strip p,.overview-strip span { margin:0; color:var(--cc-muted); font-size:12px; } .overview-strip strong { display:block; margin:5px 0 4px; color:#172033; font-size:26px; line-height:1; } .overview-strip small { margin-left:5px; color:#475467; font-size:12px; font-weight:600; } -.detail-tabs { display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); overflow-x:auto; border-radius:14px 14px 0 0; background:#fff; box-shadow:0 8px 22px rgba(31,43,74,.06); } +.detail-tabs { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); overflow-x:auto; border-radius:14px 14px 0 0; background:#fff; box-shadow:0 8px 22px rgba(31,43,74,.06); } .detail-tabs button { position:relative; display:flex; min-width:128px; min-height:64px; align-items:center; justify-content:center; gap:8px; padding:0 14px; border:0; background:transparent; color:#667085; font-weight:650; cursor:pointer; } .detail-tabs button::after { position:absolute; right:0; bottom:0; left:0; height:3px; background:transparent; content:""; } .detail-tabs button:hover { background:#fafbff; color:#344054; }.detail-tabs button.active { background:#f2f3ff; color:var(--cc-primary); }.detail-tabs button.active::after { background:var(--cc-primary); } @@ -484,7 +480,7 @@ table { width:100%; min-width:720px; border-collapse:collapse; font-size:12px; } .business-summary { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); overflow:hidden; border:1px solid var(--cc-border); border-radius:12px; }.business-summary > div { display:grid; min-height:130px; place-items:center; align-content:center; gap:8px; border-right:1px solid var(--cc-border); }.business-summary > div:last-child { border-right:0; }.business-summary svg { color:var(--cc-primary); }.business-summary span { color:var(--cc-muted); font-size:11px; }.business-summary strong { color:#25324a; font-size:20px; } .contract-note { margin-top:18px; padding:16px 18px; border-radius:10px; background:#f8fafc; }.contract-note strong { color:#344054; font-size:12px; }.contract-note p { margin:5px 0 0; color:var(--cc-muted); font-size:11px; line-height:1.7; } .qualification-preview { display:grid; min-height:320px; max-height:72vh; overflow:auto; place-items:center; border-radius:10px; background:#f8fafc; }.qualification-preview img { display:block; max-width:100%; height:auto; }.qualification-preview iframe { width:100%; min-height:68vh; border:0; background:#fff; } -@media (max-width:960px) { .basic-layout { grid-template-columns:1fr; }.contact-panel,.user-panel { grid-column:auto; }.detail-tabs { grid-template-columns:repeat(6,150px); }.qualification-panel { order:3; }.user-panel { order:4; } } +@media (max-width:960px) { .basic-layout { grid-template-columns:1fr; }.contact-panel,.user-panel { grid-column:auto; }.detail-tabs { grid-template-columns:repeat(4,150px); }.qualification-panel { order:3; }.user-panel { order:4; } } @media (max-width:720px) { .detail-header { align-items:flex-start; flex-wrap:wrap; }.detail-page-toolbar { width:100%; flex-basis:100%; }.company-mark { width:40px; height:40px; }.header-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); width:100%; }.header-actions :deep(.el-tag) { grid-column:1 / -1; justify-self:start; }.header-actions :deep(.el-button) { min-height:44px; }.overview-strip { grid-template-columns:1fr; }.overview-strip article { border-right:0; border-bottom:1px solid var(--cc-border); }.overview-strip article:last-child { border-bottom:0; }.detail-panel { padding:16px; }.info-grid { grid-template-columns:1fr; }.info-grid > div.wide { grid-column:auto; }.business-summary { grid-template-columns:1fr; }.business-summary > div { min-height:100px; border-right:0; border-bottom:1px solid var(--cc-border); }.business-summary > div:last-child { border-bottom:0; } } @media (max-width:420px) { .company-mark { display:none; }.header-actions { grid-template-columns:1fr; }.header-actions :deep(.el-tag) { grid-column:auto; }.qualification-list button { grid-template-columns:36px minmax(0,1fr) auto; }.qualification-list .file-date,.qualification-list button > svg { display:none; } } diff --git a/src/modules/customers/pages/CustomersView.vue b/src/modules/customers/pages/CustomersView.vue index d0c45fc8..c3777eb8 100644 --- a/src/modules/customers/pages/CustomersView.vue +++ b/src/modules/customers/pages/CustomersView.vue @@ -120,7 +120,7 @@ const qualificationPreviewKind = ref<'image' | 'pdf' | ''>('') const filters = ref({ keyword: '', status: '' }) const queryFields: QueryField[] = [ - { key: 'keyword', type: 'input', label: '客户关键词', placeholder: '公司名称、客户ID或联系人' }, + { key: 'keyword', type: 'input', label: '客户关键词', placeholder: '公司名称、客户编号或联系人' }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: [ { label: '待完善', value: 'PROFILE_REQUIRED' }, { label: '待审核', value: 'PENDING' }, { label: '启用', value: 'ACTIVE' }, { label: '已驳回', value: 'REJECTED' }, @@ -188,7 +188,7 @@ const unifiedSocialCreditCodePattern = /^[0-9A-Z]{18}$/ const customerColumns: DataTableColumn[] = [ { key: 'actions', label: '操作', minWidth: 280, slotName: 'actions' }, - { key: 'tenantCode', prop: 'tenantCode', label: '客户ID', minWidth: 170 }, + { key: 'tenantCode', prop: 'tenantCode', label: '客户编号', minWidth: 170 }, { key: 'company', label: '公司名称', minWidth: 230, slotName: 'company' }, { key: 'unifiedSocialCreditCode', prop: 'unifiedSocialCreditCode', label: '统一社会信用代码', minWidth: 205, formatter: (row: PlatformTenantResource) => displayText(row.unifiedSocialCreditCode) }, { key: 'contactName', prop: 'contactName', label: '联系人', minWidth: 120, formatter: (row: PlatformTenantResource) => displayText(row.contactName) }, @@ -1062,7 +1062,7 @@ onMounted(() => { void loadCustomers() }) -
正在重置企业管理员登录密码{{ passwordTenant.tenantName }}
客户ID:{{ passwordTenant.tenantCode }}
+
正在重置企业管理员登录密码{{ passwordTenant.tenantName }}
客户编号:{{ passwordTenant.tenantCode }}
随机生成
@@ -1074,7 +1074,7 @@ onMounted(() => { void loadCustomers() })
-
{{ impersonationTenant.tenantName }}将以企业管理员身份打开客户工作台
客户ID:{{ impersonationTenant.tenantCode }}
+
{{ impersonationTenant.tenantName }}将以企业管理员身份打开客户工作台
客户编号:{{ impersonationTenant.tenantCode }}
本操作会生成一次性、短时有效的客户会话,并记录代登录原因;不会替换当前 Admin 会话。 diff --git a/src/modules/customers/pages/OverseasStorageFeesView.vue b/src/modules/customers/pages/OverseasStorageFeesView.vue index e3e5197e..32574632 100644 --- a/src/modules/customers/pages/OverseasStorageFeesView.vue +++ b/src/modules/customers/pages/OverseasStorageFeesView.vue @@ -28,12 +28,11 @@ const form = reactive({ currency: 'USD', version: 0, tiers: [] as OverseasStorag const storageTiers = computed(() => form.tiers.filter((tier) => tier.feeType === 'STORAGE')) const returnTiers = computed(() => form.tiers.filter((tier) => tier.feeType === 'RETURN_STORAGE')) -const filters = reactive({ keyword: '' }) +const filters = ref({ keyword: '' }) const queryFields = [{ key: 'keyword', label: '客户', type: 'input' as const, placeholder: '搜索客户名称' }] const columns = [ { key: 'actions', slotName: 'actions', label: '操作', width: 100, fixed: 'left' as const }, { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 180 }, - { key: 'customerId', prop: 'customerId', label: '客户ID', minWidth: 180 }, { key: 'currency', prop: 'currency', label: '币种', width: 90 }, { key: 'storage', slotName: 'storage', label: '仓租(CBM/天)', minWidth: 300 }, { key: 'returnStorage', slotName: 'returnStorage', label: '退货仓储费(托/天)', minWidth: 300 }, @@ -48,7 +47,7 @@ async function load() { catch (cause) { error.value = getApiErrorDetails(cause, '海外仓阶梯费用加载失败,请稍后重试') } finally { loading.value = false } } -function search() { keyword.value = filters.keyword; page.value = 1; void load() } +function search() { keyword.value = filters.value.keyword; page.value = 1; void load() } function openEdit(row: OverseasStorageFeeConfig) { editing.value = row; form.currency = row.currency || 'USD'; form.version = row.version; form.tiers = clone(row.tiers); dialogOpen.value = true } function addTier(feeType: OverseasStorageFeeTier['feeType']) { const message = addFeeTier(form.tiers, feeType); if (message) ElMessage.error(message) } function removeTier(tier: OverseasStorageFeeTier) { const message = removeFeeTier(form.tiers, tier); if (message) ElMessage.error(message) } diff --git a/src/modules/customs/pages/CustomsCasesView.test.ts b/src/modules/customs/pages/CustomsCasesView.test.ts index 09643d44..ea240095 100644 --- a/src/modules/customs/pages/CustomsCasesView.test.ts +++ b/src/modules/customs/pages/CustomsCasesView.test.ts @@ -4,7 +4,7 @@ import { resolve } from 'node:path' const source = readFileSync(resolve(process.cwd(), 'src/modules/customs/pages/CustomsCasesView.vue'), 'utf8') describe('CustomsCasesView', () => { it('uses shared list and dialog components', () => { expect(source).toContain('QueryToolbar'); expect(source).toContain('DataTable'); expect(source).toContain('AppDialog'); expect(source).toContain("fixed: 'left'") }) - it('supports create, detail, history and agent submission', () => { expect(source).toContain('createCustomsCase'); expect(source).toContain('getCustomsCase'); expect(source).toContain('submitCustomsCase'); expect(source).toContain('detail.history') }) + it('supports detail, history and agent submission', () => { expect(source).toContain('getCustomsCase'); expect(source).toContain('submitCustomsCase'); expect(source).toContain('detail.history') }) it('localizes backend statuses and timeline actions throughout the page', () => { expect(source).toContain('customsCaseStatusOptions') expect(source).toContain(':status="customsStatusLabel(row.status)"') @@ -14,19 +14,11 @@ describe('CustomsCasesView', () => { expect(source).toContain("detail.value?.customsCase.status === 'READY_TO_SUBMIT'") expect(source).not.toContain("['DRAFT', 'COLLECTING', 'READY', 'SUBMITTED', 'RELEASED', 'REJECTED']") }) - it('selects business resources instead of asking operators for customer and order ids', () => { - expect(source).toContain('listPlatformTenants') - expect(source).toContain('listPlatformUnifiedOrders') - expect(source).toContain('listContainers') - expect(source).toContain('selectOrder') - expect(source).toContain('重新加载选项') - expect(source).not.toContain('label="客户 ID"') - expect(source).not.toContain('label="订单 ID"') - expect(source).not.toContain('label="入库单 ID"') - }) - it('only exposes active customers in create selectors', () => { - expect(source).toContain("customers.value.filter((customer) => customer.status === 'ACTIVE')") - expect(source).toContain('v-for="customer in activeCustomers"') - expect(source).not.toContain(':disabled="customer.status !== \'ACTIVE\'"') + it('does not expose internal id inputs while resource query APIs are unavailable', () => { + expect(source).not.toContain('createCustomsCase') + expect(source).not.toContain('新增报关') + expect(source).not.toContain('资料清单版本 ID') + expect(source).not.toContain("key: 'orderId', type: 'input'") + expect(source).not.toContain("key: 'inboundId', type: 'input'") }) }) diff --git a/src/modules/customs/pages/CustomsCasesView.vue b/src/modules/customs/pages/CustomsCasesView.vue index f56f5480..d4c003ee 100644 --- a/src/modules/customs/pages/CustomsCasesView.vue +++ b/src/modules/customs/pages/CustomsCasesView.vue @@ -1,7 +1,7 @@ -
账号成员类型联系方式角色状态