diff --git a/README.md b/README.md index 20b415e1..738a759a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ pnpm test 工作台已从一期路由与菜单移除;登录后按当前角色进入第一个有权限的业务页面。一期 Admin 路由聚焦登录、客户及注册审核、平台收货仓、快递公司、往来单位、头程订单、内部用户、角色管理和菜单权限。 -后台菜单按“业务管理”和“系统管理”分组。企业审核、平台角色、菜单及按钮权限资源已接入真实 IAM 接口;客户档案、基础资料和部分系统管理页面仍使用 Pinia 模拟数据,不请求尚未确定的 Java 业务接口。 +后台菜单按“业务管理”和“系统管理”分组。企业审核、仓库、头程订单、平台角色、菜单及按钮权限资源均接入真实接口;内部用户使用 IAM 的真实查询和新增接口。运行时不再提供本地演示业务数据或静态角色预览。 ## 本地 Gateway 联调 @@ -42,19 +42,8 @@ API_PROXY_TARGET=http://8.163.30.128:9000 仓库管理已接入真实平台 Warehouse API,支持按租户筛选、详情、新增、编辑和启停,并在更新时保留后端返回的地址、能力和截单时间等完整字段。停用仓库保留历史数据,但不再提供给客户新建头程订单选择;订单详情使用下单时保存的仓库编码、名称和地址快照,仓库主数据后续修改不改变历史展示。本阶段不包含 FBA/海外目的仓、库存、库位或出入库作业。 -快递公司和往来单位一期静态维护已完成:支持筛选、详情、新增、编辑、启停、唯一校验、标准模板下载和导入文件基础检查;往来单位支持六类多选角色、联系人和完整地址。两类主数据只导入不导出,导入只新增,任一行错误整批失败。工作表内容、模板版本、重复数据和整批事务必须由后端解析校验;接口提供前,前端不模拟校验或导入成功。头程订单列表、详情、状态历史、内部备注和快递信息已经接入真实平台 Order API;订单不设“已驳回”,后台修改待审核订单后仍保持待审核并写入审计。快递公司和平台往来单位仍使用 Pinia,等待后端正式契约后接入。 +头程订单列表、详情、状态历史、内部备注和快递信息已经接入真实平台 Order API;订单不设“已驳回”,后台修改待审核订单后仍保持待审核并写入审计。客户档案、快递公司和平台往来单位尚无正式平台端接口,相应页面明确展示不可用状态,不展示静态数据,也不模拟新增、编辑或导入成功。内部用户后端当前只支持查询和新增,编辑、启停、重置密码及角色分配保持不可用。 真实接口错误会保留后端返回的业务错误码、`requestId` 和 `traceId`。登录、企业审核、平台角色及权限资源的主要失败态支持直接复制追踪标识,便于与后端日志关联。 -平台联调账号和密码只在 Vite 开发模式下预填,联调提示也只在开发模式显示。生产登录表单默认留空;生产构建产物不得包含本地联调账号、密码或开发提示文案。 - -## 静态权限预览 - -管理后台支持在顶栏切换本地角色,预览该角色可见的菜单、可访问页面和可用操作。预览角色保存在浏览器本地,刷新后继续保留;退出登录时清除,下次登录重新使用默认角色。 - -该功能仅用于静态页面和交互验收: - -- 预览角色和演示业务数据来自本地 Pinia;真实登录会切换到服务端权限,并让已接入页面调用 Java 业务 API。 -- 当前不模拟角色的 `dataScope` 数据范围。 -- 菜单隐藏、页面拦截和操作禁用不能替代服务端鉴权。 -- 后续接入 Java 服务时,接口仍须独立校验登录身份、角色权限和数据范围。 +登录表单在开发和生产环境均默认留空,不包含联调账号、密码或演示提示。菜单、路由和操作权限只使用真实登录会话返回的服务端权限快照;前端控制仅改善体验,服务端仍须独立校验身份、角色权限和数据范围。接口失败时页面展示真实错误及追踪标识,不回退到假数据或模拟成功。 diff --git a/index.html b/index.html index 02879339..47c0ca33 100644 --- a/index.html +++ b/index.html @@ -3,12 +3,12 @@ - - 虫虫出海管理后台 + + + 柏悦物流管理后台
- diff --git a/public/templates/counterparties-import-template.xlsx b/public/templates/counterparties-import-template.xlsx deleted file mode 100644 index 3e5a8bc9..00000000 Binary files a/public/templates/counterparties-import-template.xlsx and /dev/null differ diff --git a/public/templates/express-companies-import-template.xlsx b/public/templates/express-companies-import-template.xlsx deleted file mode 100644 index 5b2d267f..00000000 Binary files a/public/templates/express-companies-import-template.xlsx and /dev/null differ diff --git a/src/api/platformIam.ts b/src/api/platformIam.ts index 556a167b..7ccd5870 100644 --- a/src/api/platformIam.ts +++ b/src/api/platformIam.ts @@ -76,6 +76,26 @@ export interface CreatePlatformPermissionRequest { sortNo: number } +export interface PlatformInternalUser { + membershipId: number | string + userId: number | string + authUserId: number | string + username: string + displayName: string + memberType: string + memberNo: string | null + status: 'ACTIVE' | 'DISABLED' | string + roleIds: Array +} + +export interface CreatePlatformInternalUserRequest { + authUserId: number | string + userType: string + displayName: string + phone: string + email: string +} + async function getData(path: string): Promise { const response = await http.get, ApiEnvelope>(path) return unwrapApiResponse(response) @@ -163,6 +183,14 @@ export function createPlatformPermission(payload: CreatePlatformPermissionReques return postData('/platform/v1/iam/permissions', payload) } +export function listPlatformInternalUsers() { + return getData('/platform/v1/iam/internal-users') +} + +export function createPlatformInternalUser(payload: CreatePlatformInternalUserRequest) { + return postData('/platform/v1/iam/internal-users', payload) +} + export function approveEnterpriseReview(reviewId: number, comment: string) { return postData(`/platform/v1/iam/reviews/${reviewId}/approve`, { comment }) } diff --git a/src/assets/brand/baiyue-favicon.png b/src/assets/brand/baiyue-favicon.png new file mode 100644 index 00000000..e6151c88 Binary files /dev/null and b/src/assets/brand/baiyue-favicon.png differ diff --git a/src/assets/brand/baiyue-logo-white.png b/src/assets/brand/baiyue-logo-white.png new file mode 100644 index 00000000..efa7cf17 Binary files /dev/null and b/src/assets/brand/baiyue-logo-white.png differ diff --git a/src/assets/brand/baiyue-mark-white.png b/src/assets/brand/baiyue-mark-white.png new file mode 100644 index 00000000..f5dfbcf9 Binary files /dev/null and b/src/assets/brand/baiyue-mark-white.png differ diff --git a/src/components.d.ts b/src/components.d.ts index d0a482ab..4aa94402 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -36,8 +36,6 @@ declare module 'vue' { ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElSelect: typeof import('element-plus/es')['ElSelect'] - ElStep: typeof import('element-plus/es')['ElStep'] - ElSteps: typeof import('element-plus/es')['ElSteps'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] @@ -47,10 +45,8 @@ declare module 'vue' { ElTree: typeof import('element-plus/es')['ElTree'] MetricStrip: typeof import('./components/MetricStrip.vue')['default'] PlatformFirstLegOrdersPanel: typeof import('./components/PlatformFirstLegOrdersPanel.vue')['default'] - RolePreviewSwitcher: typeof import('./components/RolePreviewSwitcher.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] - StaticBatchImportDialog: typeof import('./components/StaticBatchImportDialog.vue')['default'] StatusBadge: typeof import('./components/StatusBadge.vue')['default'] } export interface GlobalDirectives { diff --git a/src/components/PlatformFirstLegOrdersPanel.vue b/src/components/PlatformFirstLegOrdersPanel.vue index 3ea8c131..8e19d9c6 100644 --- a/src/components/PlatformFirstLegOrdersPanel.vue +++ b/src/components/PlatformFirstLegOrdersPanel.vue @@ -297,7 +297,7 @@ onMounted(() => { 重新加载 - 当前页面已直连 Order 服务。后端尚未提供平台代客创建、平台编辑及履约任务查询接口,因此真实会话不显示这些静态操作。 + 当前页面已直连 Order 服务。后端尚未提供平台代客创建、平台编辑及履约任务查询接口,因此相关操作暂不可用。
diff --git a/src/components/RolePreviewSwitcher.test.ts b/src/components/RolePreviewSwitcher.test.ts deleted file mode 100644 index 6a221d8a..00000000 --- a/src/components/RolePreviewSwitcher.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { mount } from '@vue/test-utils' -import { describe, expect, it } from 'vitest' -import type { Role } from '@/types/admin' -import RolePreviewSwitcher from './RolePreviewSwitcher.vue' - -const roles: Role[] = [ - { id: 'R001', name: '超级管理员', code: 'SUPER_ADMIN', description: '', userCount: 1, dataScope: '全部数据', status: '启用', permissionIds: [] }, - { id: 'R003', name: '操作员', code: 'OPERATOR', description: '', userCount: 2, dataScope: '本部门数据', status: '启用', permissionIds: [] }, -] - -const stubs = { - 'el-dropdown': { - name: 'ElDropdownStub', - emits: ['command'], - template: '
', - }, - 'el-dropdown-menu': { template: '
' }, - 'el-dropdown-item': { - props: ['command', 'disabled'], - template: '', - }, -} - -describe('RolePreviewSwitcher', () => { - it('announces the active role and explains the static preview boundary', () => { - const wrapper = mount(RolePreviewSwitcher, { - props: { roles, currentRole: roles[0] }, - global: { stubs }, - }) - - const trigger = wrapper.get('button[aria-label]') - expect(trigger.attributes('aria-label')).toBe('权限预览:超级管理员,数据范围未模拟') - expect(trigger.attributes('title')).toBe('权限预览:超级管理员,数据范围未模拟') - expect(wrapper.text()).toContain('权限预览:超级管理员') - expect(wrapper.text()).toContain('仅预览菜单、页面和操作;数据范围未模拟') - expect(wrapper.find('svg.lucide-user-round-cog').exists()).toBe(true) - }) - - it('disables the current role and emits a string role id for another selection', async () => { - const wrapper = mount(RolePreviewSwitcher, { - props: { roles, currentRole: roles[0] }, - global: { stubs }, - }) - - const items = wrapper.findAll('[data-role]') - expect(items[0].attributes('disabled')).toBeDefined() - expect(items[1].attributes('disabled')).toBeUndefined() - wrapper.getComponent({ name: 'ElDropdownStub' }).vm.$emit('command', 3) - - expect(wrapper.emitted('select')).toEqual([['3']]) - }) - - it('labels the trigger when no enabled role is available', () => { - const wrapper = mount(RolePreviewSwitcher, { - props: { roles: [], currentRole: null }, - global: { stubs }, - }) - - expect(wrapper.get('button[aria-label]').attributes('aria-label')) - .toBe('权限预览:无可用角色,数据范围未模拟') - }) -}) - diff --git a/src/components/RolePreviewSwitcher.vue b/src/components/RolePreviewSwitcher.vue deleted file mode 100644 index b0f677cd..00000000 --- a/src/components/RolePreviewSwitcher.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/src/components/StaticBatchImportDialog.test.ts b/src/components/StaticBatchImportDialog.test.ts deleted file mode 100644 index b56a95dc..00000000 --- a/src/components/StaticBatchImportDialog.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { readFileSync } from 'node:fs' -import { describe, expect, it } from 'vitest' - -const source = readFileSync('src/components/StaticBatchImportDialog.vue', 'utf8') - -describe('StaticBatchImportDialog source contract', () => { - it('only performs safe client-side file checks before the backend endpoint exists', () => { - expect(source).toContain("fileName.endsWith('.xlsx')") - expect(source).toContain('10 * 1024 * 1024') - expect(source).toContain('前端不会解析工作表内容') - expect(source).toContain('等待后端导入接口') - }) - - it('does not simulate server validation or a successful import', () => { - expect(source).not.toContain('demonstratesFailure') - expect(source).not.toContain('静态演示') - expect(source).not.toContain("emit('imported'") - }) -}) diff --git a/src/components/StaticBatchImportDialog.vue b/src/components/StaticBatchImportDialog.vue deleted file mode 100644 index 2e14e890..00000000 --- a/src/components/StaticBatchImportDialog.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - - - diff --git a/src/layouts/AppLayout.test.ts b/src/layouts/AppLayout.test.ts index 9d89313b..6f77c264 100644 --- a/src/layouts/AppLayout.test.ts +++ b/src/layouts/AppLayout.test.ts @@ -1,341 +1,49 @@ import { createPinia, setActivePinia } from 'pinia' import { flushPromises, mount } from '@vue/test-utils' import { createMemoryHistory, createRouter } from 'vue-router' -import { nextTick } from 'vue' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { ElMessage, ElMessageBox } from 'element-plus' -import { ApiResponseError } from '@/api/http' -import { useAccessPreviewStore } from '@/stores/accessPreview' -import { useAdminDataStore } from '@/stores/adminData' +import { ElMessageBox } from 'element-plus' import { useAuthStore } from '@/stores/auth' -import { useUnsavedWorkStore } from '@/stores/unsavedWork' import AppLayout from './AppLayout.vue' vi.mock('element-plus', async (importOriginal) => { const actual = await importOriginal() - return { - ...actual, - ElMessage: { success: vi.fn(), warning: vi.fn(), error: vi.fn() }, - ElMessageBox: { confirm: vi.fn() }, - } + return { ...actual, ElMessage: { warning: vi.fn(), error: vi.fn() }, ElMessageBox: { confirm: vi.fn() } } }) -const page = { template: '

page

' } - -function createMemoryStorage(): Storage { - const values = new Map() - return { - get length() { return values.size }, - clear: () => values.clear(), - getItem: (key) => values.get(key) ?? null, - key: (index) => [...values.keys()][index] ?? null, - removeItem: (key) => values.delete(key), - setItem: (key, value) => values.set(key, value), - } -} - -async function mountLayout(path: string) { +async function mountLayout() { const pinia = createPinia() setActivePinia(pinia) const auth = useAuthStore() - auth.token = 'test-access-token' - auth.user = { id: '1', name: '平台管理员', account: 'platform_admin', roles: ['PLATFORM_ADMIN'] } - localStorage.setItem('cc-admin-token', auth.token) + auth.token = 'token' + auth.user = { id: '1', name: '平台管理员', account: 'admin', roles: ['PLATFORM_ADMIN'] } + auth.permissions = ['platform:warehouse:list'] const router = createRouter({ history: createMemoryHistory(), - routes: [ - { path: '/', component: page, meta: { title: '管理后台' } }, - { path: '/customers', component: page, meta: { title: '客户管理', permission: 'platform:customer:list' } }, - { path: '/warehouses', component: page, meta: { title: '仓库管理', permission: 'platform:warehouse:list' } }, - { path: '/orders/first-leg', component: page, meta: { title: '头程订单', permission: 'platform:order:list' } }, - { path: '/system/users', component: page, meta: { title: '内部用户', permission: 'platform:user:list' } }, - { path: '/403', component: page, meta: { title: '权限不足' } }, - { path: '/login', component: page, meta: { title: '登录' } }, - ], + routes: [{ path: '/', component: { template: '

page

' } }, { path: '/login', component: { template: '

login

' } }], }) - await router.push(path) + await router.push('/') await router.isReady() - const wrapper = mount(AppLayout, { - attachTo: document.body, - global: { - plugins: [pinia, router], - stubs: { - AdminSidebarMenu: { props: ['items'], template: '