优化构架
This commit is contained in:
@@ -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'],
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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/)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user