diff --git a/.codex-customer-detail-desktop.png b/.codex-customer-detail-desktop.png new file mode 100644 index 00000000..649813ec Binary files /dev/null and b/.codex-customer-detail-desktop.png differ diff --git a/.codex-customer-detail-mobile-viewport.png b/.codex-customer-detail-mobile-viewport.png new file mode 100644 index 00000000..ea328e01 Binary files /dev/null and b/.codex-customer-detail-mobile-viewport.png differ diff --git a/.codex-customer-detail-mobile.png b/.codex-customer-detail-mobile.png new file mode 100644 index 00000000..02cac8ab Binary files /dev/null and b/.codex-customer-detail-mobile.png differ diff --git a/README.md b/README.md index 3191b817..05f43336 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ pnpm test 工作台已从一期菜单和默认入口移除;登录后按当前角色进入第一个有权限的业务页面。历史工作台仍保留为不展示菜单的原型路由。一期 Admin 正式路由聚焦登录、客户及注册审核、平台收货仓、快递公司、往来单位、头程订单、内部用户、角色管理和菜单权限;企业注册审核已合并到客户列表,不再保留独立菜单。 -后台菜单仅展示已接入的一期模块;其余平台原型菜单保留为导航配置中的注释,待对应业务契约接入后再恢复展示。企业审核、仓库、头程订单、平台角色、菜单及按钮权限资源均接入真实接口;用户管理通过 IAM 统一维护平台账号和企业租户账号。运行时不再提供本地演示业务数据或静态角色预览。 +后台菜单仅展示已接入的一期模块;其余平台原型菜单保留为导航配置中的注释,待对应业务契约接入后再恢复展示。企业审核、仓库、头程订单、平台角色、菜单及按钮权限资源均接入真实接口;内部用户管理只维护平台员工账号,企业租户成员由商户成员边界管理。运行时不再提供本地演示业务数据或静态角色预览。 ## 本地 Gateway 联调 @@ -52,7 +52,7 @@ API_PROXY_TARGET=http://8.163.30.128:9000 仓库管理已接入真实平台 Warehouse API,支持按租户筛选、详情、新增、编辑和启停,并在更新时保留后端返回的地址、能力和截单时间等完整字段。停用仓库保留历史数据,但不再提供给客户新建头程订单选择;订单详情使用下单时保存的仓库编码、名称和地址快照,仓库主数据后续修改不改变历史展示。本阶段不包含 FBA/海外目的仓、库存、库位或出入库作业。 -头程订单列表、详情、状态历史、内部备注和快递信息已经接入真实平台 Order API;订单不设“已驳回”,后台修改待审核订单后仍保持待审核并写入审计。快递公司已经接入 Warehouse Service 的真实分页查询、详情、新增、编辑、启停、标准模板下载和 XLSX 整批导入接口,按租户与平台权限访问;订单履约接口当前仍按公司名称和快递单号保存。平台往来单位已经接入 Warehouse Service 的真实分页、新增、编辑和启停接口,当前后端类型契约为客户、供应商及客户/供应商双重类型;批量导入接口尚未提供,不模拟导入成功。用户管理已接入统一查询、新增、编辑、所属租户与角色调整、启停、最后登录展示和密码重置接口;账号凭据仍由 Auth Service 独占保存。 +头程订单列表、详情、状态历史、内部备注和快递信息已经接入真实平台 Order API;订单不设“已驳回”,后台修改待审核订单后仍保持待审核并写入审计。快递公司已经接入 Warehouse Service 的真实分页查询、详情、新增、编辑、启停、标准模板下载和 XLSX 整批导入接口,按租户与平台权限访问;订单履约接口当前仍按公司名称和快递单号保存。平台往来单位已经接入 Warehouse Service 的真实分页、新增、编辑和启停接口,当前后端类型契约为客户、供应商及客户/供应商双重类型;批量导入接口尚未提供,不模拟导入成功。内部用户管理仅维护平台员工账号,已接入服务端分页查询、关键词与状态筛选、新增、编辑、平台角色调整、启停、最后登录展示和密码重置接口;账号凭据仍由 Auth Service 独占保存。 真实接口错误会保留后端返回的业务错误码、`requestId` 和 `traceId`。登录、企业审核、平台角色及权限资源的主要失败态支持直接复制追踪标识,便于与后端日志关联。 diff --git a/src/modules/customers/pages/CustomerDetailView.test.ts b/src/modules/customers/pages/CustomerDetailView.test.ts new file mode 100644 index 00000000..3c549b66 --- /dev/null +++ b/src/modules/customers/pages/CustomerDetailView.test.ts @@ -0,0 +1,98 @@ +import ElementPlus from 'element-plus' +import { createPinia, setActivePinia } from 'pinia' +import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + downloadTenantQualification, + getPlatformTenant, + listPlatformTenantOrderCounts, + listTenantQualifications, + type PlatformTenantResource, +} from '@/modules/iam' +import { useAccessPreviewStore } from '@/core/access/accessPreview' +import { useAuthStore } from '@/core/auth/auth' +import CustomerDetailView from './CustomerDetailView.vue' + +const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() })) + +vi.mock('vue-router', async (importOriginal) => ({ + ...await importOriginal(), + useRoute: () => ({ params: { tenantId: '8' } }), + useRouter: () => ({ push: routerPush }), +})) + +vi.mock('@/modules/iam', () => ({ + changePlatformTenantStatus: vi.fn(), + downloadTenantQualification: vi.fn(), + getPlatformTenant: vi.fn(), + listPlatformTenantOrderCounts: vi.fn(), + listTenantQualifications: vi.fn(), + qualificationFileName: vi.fn((documentUrl: string) => documentUrl.split('/').at(-1) || '资质文件'), +})) + +class ResizeObserverStub implements ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +const tenant: PlatformTenantResource = { + id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'ACTIVE', legalName: '柏悦物流有限公司', + legalRepresentative: '张三', unifiedSocialCreditCode: '91440300MA5ABCDE12', contactName: '张三', + contactPhone: '13800138000', contactEmail: 'ops@example.com', servicePackage: '基础版', contactWechat: 'baiyue-ops', + salesOwnerName: '李四', paymentTermDays: 30, enterpriseType: '有限责任公司', establishedDate: '2020-01-01', + registeredCapital: 1000, registeredCapitalCurrency: 'CNY', registeredAddress: '深圳市宝安区', internalRemark: '重点客户', + createdAt: '2026-08-01T10:00:00', approvedAt: '2026-08-02T10:00:00', +} + +enableAutoUnmount(afterEach) + +describe('CustomerDetailView', () => { + let pinia: ReturnType + + beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('ResizeObserver', ResizeObserverStub) + Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:qualification-preview') }) + Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() }) + document.body.innerHTML = '' + pinia = createPinia() + setActivePinia(pinia) + const auth = useAuthStore() + auth.token = 'token' + auth.permissions = ['platform:customer:view', 'platform:user:list', 'platform:order:list'] + useAccessPreviewStore().initializeAfterLogin() + vi.mocked(getPlatformTenant).mockResolvedValue(tenant) + vi.mocked(listTenantQualifications).mockResolvedValue([{ + id: 'qualification-1', tenantId: tenant.id, qualificationType: 'BUSINESS_LICENSE', certificateNo: tenant.unifiedSocialCreditCode, + documentUrl: '/files/license.png', status: 'SUPERSEDED', reviewComment: '已替换', validTo: null, + }]) + vi.mocked(listPlatformTenantOrderCounts).mockResolvedValue([{ tenantId: tenant.id, orderCount: 12 }]) + vi.mocked(downloadTenantQualification).mockResolvedValue(new Blob(['image'], { type: 'image/png' })) + }) + + it('renders all six modules without inventing unavailable tenant users', async () => { + 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.body.textContent).toContain('企业成员数据暂不可用') + expect(document.body.textContent).not.toContain('0 人正常使用') + expect(document.body.textContent).toContain('已替换') + expect(document.body.textContent).toContain('12票') + }) + + it('opens an authenticated qualification preview', async () => { + mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) + await flushPromises() + + const fileButton = document.querySelector('.qualification-list button') + fileButton?.click() + await flushPromises() + + expect(downloadTenantQualification).toHaveBeenCalledWith(tenant.id, '/files/license.png') + expect(document.querySelector('img[alt="license.png预览"]')).not.toBeNull() + }) +}) diff --git a/src/modules/customers/pages/CustomerDetailView.vue b/src/modules/customers/pages/CustomerDetailView.vue new file mode 100644 index 00000000..369e5101 --- /dev/null +++ b/src/modules/customers/pages/CustomerDetailView.vue @@ -0,0 +1,347 @@ + + +