优化构架

This commit is contained in:
2026-08-03 18:52:30 +08:00
parent 35c713a7ca
commit fed46f42ef
59 changed files with 691 additions and 325 deletions
@@ -3,8 +3,9 @@ import { readFileSync } from 'node:fs'
import test from 'node:test'
const source = readFileSync(new URL('./merchantIam.ts', import.meta.url), 'utf8')
const storeSource = readFileSync(new URL('../stores/tenantAdministration.ts', import.meta.url), 'utf8')
const authSource = readFileSync(new URL('../stores/auth.ts', import.meta.url), 'utf8')
const storeSource = readFileSync(new URL('../store/tenantAdministration.ts', import.meta.url), 'utf8')
const contractsSource = readFileSync(new URL('../contracts.ts', import.meta.url), 'utf8')
const authSource = readFileSync(new URL('../../auth/store/auth.ts', import.meta.url), 'utf8')
test('declares the documented merchant role and member endpoints', () => {
assert.match(source, /url: '\/merchant\/v1\/iam\/roles'/)
@@ -50,9 +51,11 @@ test('starts empty and only loads server role and member records', () => {
})
test('shares the merchant permission map and preserves resources not rendered by the role editor', () => {
assert.match(storeSource, /export const FRONTEND_TO_MERCHANT_PERMISSION_CODES/)
assert.match(storeSource, /'order:first-leg:cancel': \['merchant:order:cancel'\]/)
assert.match(contractsSource, /export const FRONTEND_TO_MERCHANT_PERMISSION_CODES/)
assert.match(contractsSource, /'order:first-leg:cancel': \['merchant:order:cancel'\]/)
assert.match(storeSource, /from '@\/modules\/tenant-management\/contracts'/)
assert.match(authSource, /FRONTEND_TO_MERCHANT_PERMISSION_CODES/)
assert.match(authSource, /from '@\/modules\/tenant-management\/public'/)
assert.match(authSource, /Object\.entries\(FRONTEND_TO_MERCHANT_PERMISSION_CODES\)/)
assert.match(storeSource, /const preservedPermissionIds = role\.permissionIds\.filter/)
assert.match(storeSource, /const nextPermissionIds = \[\.\.\.new Set\(\[\.\.\.preservedPermissionIds, \.\.\.selectedPermissionIds\]\)\]/)
@@ -1,6 +1,5 @@
import { apiBlobRequest, apiRequest } from '@/api/http'
export type MerchantTenantStatus = 'PROFILE_REQUIRED' | 'PENDING' | 'ACTIVE' | 'REJECTED' | 'DISABLED'
import type { MerchantTenantStatus } from '../contracts'
export type QualificationStatus = 'PENDING' | 'APPROVED' | 'REJECTED'
export interface MerchantTenantProfile {
@@ -0,0 +1,83 @@
export type MerchantTenantStatus = 'PROFILE_REQUIRED' | 'PENDING' | 'ACTIVE' | 'REJECTED' | 'DISABLED'
export interface TenantPermissionItem {
code: string
label: string
description: string
}
export interface TenantPermissionGroup {
key: string
label: string
items: TenantPermissionItem[]
}
export const TENANT_PERMISSION_GROUPS: readonly TenantPermissionGroup[] = [
{
key: 'account',
label: '个人账号',
items: [{ code: 'account:profile:view', label: '维护我的信息', description: '查看并维护当前成员自己的资料和登录密码' }],
},
{
key: 'dashboard',
label: '业务总览',
items: [{ code: 'dashboard:view', label: '查看总览', description: '查看企业统计和最近订单' }],
},
{
key: 'orders',
label: '头程订单',
items: [
{ code: 'order:first-leg:list', label: '查看订单', description: '查看本企业头程订单列表和详情' },
{ code: 'order:first-leg:create', label: '创建订单', description: '新建并保存订单草稿' },
{ code: 'order:first-leg:edit', label: '编辑订单', description: '编辑本企业尚未提交的订单草稿' },
{ code: 'order:first-leg:submit', label: '提交订单', description: '提交草稿进入平台审核' },
{ code: 'order:first-leg:cancel', label: '取消订单', description: '在允许的状态下取消订单' },
],
},
{
key: 'company',
label: '企业资料',
items: [
{ code: 'company:profile:view', label: '查看资料', description: '查看企业、联系人和资质资料' },
{ code: 'company:profile:edit', label: '编辑资料', description: '维护企业联系信息和业务地址' },
{ code: 'company:qualification:upload', label: '更新资质', description: '上传或更新企业资质文件' },
],
},
{
key: 'members',
label: '企业成员',
items: [
{ code: 'company:member:list', label: '查看成员', description: '查看本企业成员列表' },
{ code: 'company:member:write', label: '维护成员', description: '新增、编辑、启停企业成员' },
{ code: 'company:member:reset-password', label: '重置密码', description: '重置企业成员登录密码' },
],
},
{
key: 'roles',
label: '角色与权限',
items: [
{ code: 'company:role:list', label: '查看角色', description: '查看本企业角色列表' },
{ code: 'company:role:write', label: '维护角色', description: '新增、编辑、启停企业角色' },
{ code: 'company:permission:list', label: '查看权限', description: '查看角色拥有的菜单和操作权限' },
{ code: 'company:permission:assign', label: '分配权限', description: '为本企业角色分配菜单及操作权限' },
],
},
]
export const FRONTEND_TO_MERCHANT_PERMISSION_CODES: Readonly<Record<string, readonly string[]>> = {
'order:first-leg:list': ['merchant:order:list', 'merchant:order:view'],
'order:first-leg:create': ['merchant:order:create'],
'order:first-leg:edit': ['merchant:order:edit'],
'order:first-leg:submit': ['merchant:order:submit'],
'order:first-leg:cancel': ['merchant:order:cancel'],
'company:profile:view': ['merchant:profile:view'],
'company:profile:edit': ['merchant:profile:edit'],
'company:qualification:upload': ['merchant:qualification:upload'],
'company:member:list': ['merchant:member:list'],
'company:member:write': ['merchant:member:create', 'merchant:member:edit', 'merchant:member:grant-role', 'merchant:member:status'],
'company:member:reset-password': ['merchant:member:reset-password'],
'company:role:list': ['merchant:role:list'],
'company:role:write': ['merchant:role:create', 'merchant:role:edit', 'merchant:role:status', 'merchant:role:delete'],
'company:permission:list': ['merchant:role:list'],
'company:permission:assign': ['merchant:role:grant'],
}
+13
View File
@@ -0,0 +1,13 @@
export {
getMerchantPermissionResources,
getMerchantPersonalProfile,
getTenantProfile,
updateMerchantPersonalProfile,
type MerchantPermissionResource,
} from './api/merchantIam'
export {
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
TENANT_PERMISSION_GROUPS,
type MerchantTenantStatus,
} from './contracts'
export { useTenantAdministrationStore } from './store/tenantAdministration'
+41
View File
@@ -0,0 +1,41 @@
import { Building2, KeyRound, Settings, ShieldCheck, Users } from '@lucide/vue'
import type { ClientModuleManifest } from '../types.ts'
export const tenantManagementModule = {
key: 'company',
navigationGroup: { title: '企业管理', icon: Settings, dividerBefore: true },
routes: [
{
path: 'company/profile',
name: 'company-profile',
component: () => import('./views/CompanyProfileView.vue'),
release: 'active',
meta: { title: '企业资料', description: '维护企业基本信息、联系人和资质文件', permission: 'company:profile:view' },
navigation: { key: 'company-profile', icon: Building2 },
},
{
path: 'organization/members',
name: 'members',
component: () => import('./views/MembersView.vue'),
release: 'active',
meta: { title: '企业成员', description: '管理本企业成员账号、状态和角色', permission: 'company:member:list' },
navigation: { key: 'company-members', icon: Users },
},
{
path: 'organization/roles',
name: 'roles',
component: () => import('./views/RolesView.vue'),
release: 'active',
meta: { title: '角色管理', description: '建立符合企业分工的成员角色', permission: 'company:role:list' },
navigation: { key: 'company-roles', icon: ShieldCheck },
},
{
path: 'organization/permissions',
name: 'permissions',
component: () => import('./views/PermissionsView.vue'),
release: 'active',
meta: { title: '菜单权限', description: '为本企业角色分配菜单及操作权限', permission: 'company:permission:list' },
navigation: { key: 'company-permissions', icon: KeyRound },
},
],
} satisfies ClientModuleManifest
@@ -19,8 +19,12 @@ import {
type MerchantRole,
type MerchantMember,
type MerchantPermissionResource,
} from '@/modules/tenant-management/api/merchantIam'
import {
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
TENANT_PERMISSION_GROUPS,
type MerchantTenantStatus,
} from '@/api/merchantIam'
} from '@/modules/tenant-management/contracts'
export type TenantRecordStatus = 'ACTIVE' | 'INACTIVE'
@@ -67,90 +71,7 @@ export interface TenantRole {
permissionCodes: string[]
}
export interface TenantPermissionItem {
code: string
label: string
description: string
}
export interface TenantPermissionGroup {
key: string
label: string
items: TenantPermissionItem[]
}
export const TENANT_PERMISSION_GROUPS: readonly TenantPermissionGroup[] = [
{
key: 'account',
label: '个人账号',
items: [{ code: 'account:profile:view', label: '维护我的信息', description: '查看并维护当前成员自己的资料和登录密码' }],
},
{
key: 'dashboard',
label: '业务总览',
items: [{ code: 'dashboard:view', label: '查看总览', description: '查看企业统计和最近订单' }],
},
{
key: 'orders',
label: '头程订单',
items: [
{ code: 'order:first-leg:list', label: '查看订单', description: '查看本企业头程订单列表和详情' },
{ code: 'order:first-leg:create', label: '创建订单', description: '新建并保存订单草稿' },
{ code: 'order:first-leg:edit', label: '编辑订单', description: '编辑本企业尚未提交的订单草稿' },
{ code: 'order:first-leg:submit', label: '提交订单', description: '提交草稿进入平台审核' },
{ code: 'order:first-leg:cancel', label: '取消订单', description: '在允许的状态下取消订单' },
],
},
{
key: 'company',
label: '企业资料',
items: [
{ code: 'company:profile:view', label: '查看资料', description: '查看企业、联系人和资质资料' },
{ code: 'company:profile:edit', label: '编辑资料', description: '维护企业联系信息和业务地址' },
{ code: 'company:qualification:upload', label: '更新资质', description: '上传或更新企业资质文件' },
],
},
{
key: 'members',
label: '企业成员',
items: [
{ code: 'company:member:list', label: '查看成员', description: '查看本企业成员列表' },
{ code: 'company:member:write', label: '维护成员', description: '新增、编辑、启停企业成员' },
{ code: 'company:member:reset-password', label: '重置密码', description: '重置企业成员登录密码' },
],
},
{
key: 'roles',
label: '角色与权限',
items: [
{ code: 'company:role:list', label: '查看角色', description: '查看本企业角色列表' },
{ code: 'company:role:write', label: '维护角色', description: '新增、编辑、启停企业角色' },
{ code: 'company:permission:list', label: '查看权限', description: '查看角色拥有的菜单和操作权限' },
{ code: 'company:permission:assign', label: '分配权限', description: '为本企业角色分配菜单及操作权限' },
],
},
]
const allPermissionCodes = TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code))
export const FRONTEND_TO_MERCHANT_PERMISSION_CODES: Readonly<Record<string, readonly string[]>> = {
'order:first-leg:list': ['merchant:order:list', 'merchant:order:view'],
'order:first-leg:create': ['merchant:order:create'],
'order:first-leg:edit': ['merchant:order:edit'],
'order:first-leg:submit': ['merchant:order:submit'],
'order:first-leg:cancel': ['merchant:order:cancel'],
'company:profile:view': ['merchant:profile:view'],
'company:profile:edit': ['merchant:profile:edit'],
'company:qualification:upload': ['merchant:qualification:upload'],
'company:member:list': ['merchant:member:list'],
'company:member:write': ['merchant:member:create', 'merchant:member:edit', 'merchant:member:grant-role', 'merchant:member:status'],
'company:member:reset-password': ['merchant:member:reset-password'],
'company:role:list': ['merchant:role:list'],
'company:role:write': ['merchant:role:create', 'merchant:role:edit', 'merchant:role:status', 'merchant:role:delete'],
'company:permission:list': ['merchant:role:list'],
'company:permission:assign': ['merchant:role:grant'],
}
const mappedMerchantPermissionCodes = new Set(Object.values(FRONTEND_TO_MERCHANT_PERMISSION_CODES).flat())
const emptyCompany: TenantCompanyProfile = {
@@ -4,10 +4,10 @@ import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
import { Building2, CheckCircle2, Clock3, FileCheck2, FileText, Pencil, Trash2, Upload, UserRound } from '@lucide/vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQualification } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth'
import { formatQualificationFileSize, validateQualificationFile } from '@/utils/qualificationFile'
import { downloadQualificationImage, uploadBusinessLicense } from '@/api/merchantIam'
import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQualification } from '@/modules/tenant-management/store/tenantAdministration'
import { useAuthStore } from '@/modules/auth/public'
import { formatQualificationFileSize, validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile'
import { downloadQualificationImage, uploadBusinessLicense } from '@/modules/tenant-management/api/merchantIam'
import { ApiError } from '@/api/http'
const store = useTenantAdministrationStore()
@@ -4,8 +4,8 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem
import { KeyRound, Pencil, Plus } from '@lucide/vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import { useTenantAdministrationStore, type TenantMember, type TenantRecordStatus } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth'
import { useTenantAdministrationStore, type TenantMember, type TenantRecordStatus } from '@/modules/tenant-management/store/tenantAdministration'
import { useAuthStore } from '@/modules/auth/public'
const store = useTenantAdministrationStore()
const auth = useAuthStore()
@@ -3,8 +3,9 @@ import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { LockKeyhole, RotateCcw, Save, ShieldCheck } from '@lucide/vue'
import { TENANT_PERMISSION_GROUPS, useTenantAdministrationStore } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth'
import { useTenantAdministrationStore } from '@/modules/tenant-management/store/tenantAdministration'
import { TENANT_PERMISSION_GROUPS } from '@/modules/tenant-management/contracts'
import { useAuthStore } from '@/modules/auth/public'
const store = useTenantAdministrationStore()
const auth = useAuthStore()
@@ -5,8 +5,8 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem
import { LockKeyhole, Pencil, Plus, ShieldCheck, Trash2 } from '@lucide/vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import { useTenantAdministrationStore, type TenantRole } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth'
import { useTenantAdministrationStore, type TenantRole } from '@/modules/tenant-management/store/tenantAdministration'
import { useAuthStore } from '@/modules/auth/public'
const store = useTenantAdministrationStore()
const auth = useAuthStore()
@@ -2,18 +2,19 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import test from 'node:test'
const routerSource = fs.readFileSync(new URL('../router/index.ts', import.meta.url), 'utf8')
const routerSource = fs.readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
const profileSource = fs.readFileSync(new URL('./CompanyProfileView.vue', import.meta.url), 'utf8')
const membersSource = fs.readFileSync(new URL('./MembersView.vue', import.meta.url), 'utf8')
const rolesSource = fs.readFileSync(new URL('./RolesView.vue', import.meta.url), 'utf8')
const permissionsSource = fs.readFileSync(new URL('./PermissionsView.vue', import.meta.url), 'utf8')
const storeSource = fs.readFileSync(new URL('../stores/tenantAdministration.ts', import.meta.url), 'utf8')
const storeSource = fs.readFileSync(new URL('../store/tenantAdministration.ts', import.meta.url), 'utf8')
test('routes tenant administration modules to implemented views instead of the placeholder', () => {
assert.match(routerSource, /company\/profile'.*CompanyProfileView\.vue/)
assert.match(routerSource, /organization\/members'.*MembersView\.vue/)
assert.match(routerSource, /organization\/roles'.*RolesView\.vue/)
assert.match(routerSource, /organization\/permissions'.*PermissionsView\.vue/)
assert.match(routerSource, /path: 'company\/profile'[\s\S]*?CompanyProfileView\.vue/)
assert.match(routerSource, /path: 'organization\/members'[\s\S]*?MembersView\.vue/)
assert.match(routerSource, /path: 'organization\/roles'[\s\S]*?RolesView\.vue/)
assert.match(routerSource, /path: 'organization\/permissions'[\s\S]*?PermissionsView\.vue/)
assert.equal((routerSource.match(/release: 'active'/g) ?? []).length, 4)
assert.doesNotMatch(routerSource, /ModuleView\.vue/)
})