fix: align client integration contracts
This commit is contained in:
@@ -4,9 +4,13 @@ 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')
|
||||
|
||||
test('declares the documented merchant role and member endpoints', () => {
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/roles'/)
|
||||
assert.match(source, /method: 'PUT', url: `\/merchant\/v1\/iam\/roles\/\$\{roleId\}`/)
|
||||
assert.match(source, /method: 'PUT', url: `\/merchant\/v1\/iam\/roles\/\$\{roleId\}\/status`/)
|
||||
assert.match(source, /method: 'DELETE', url: `\/merchant\/v1\/iam\/roles\/\$\{roleId\}`/)
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/roles\/permissions'/)
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/members'/)
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/members\/roles'/)
|
||||
@@ -33,8 +37,21 @@ test('clears mock records before loading server role and member lists', () => {
|
||||
assert.match(storeSource, /rolesLoadedFromApi\.value = true\s+roles\.value = \[\]/)
|
||||
assert.match(storeSource, /membersLoadedFromApi\.value = true\s+members\.value = \[\]/)
|
||||
assert.match(storeSource, /await createMerchantRole/)
|
||||
assert.match(storeSource, /await updateMerchantRole/)
|
||||
assert.match(storeSource, /await changeMerchantRoleStatus/)
|
||||
assert.match(storeSource, /await deleteMerchantRole/)
|
||||
assert.match(storeSource, /await createMerchantMember/)
|
||||
assert.match(storeSource, /await updateMerchantMember/)
|
||||
assert.match(storeSource, /await changeMerchantMemberStatus/)
|
||||
assert.match(storeSource, /await resetMerchantMemberPassword/)
|
||||
})
|
||||
|
||||
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(authSource, /FRONTEND_TO_MERCHANT_PERMISSION_CODES/)
|
||||
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\]\)\]/)
|
||||
assert.match(storeSource, /await replaceMerchantRolePermissions\(id, nextPermissionIds\)/)
|
||||
})
|
||||
|
||||
@@ -72,6 +72,11 @@ export interface CreateMerchantRoleRequest {
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface UpdateMerchantRoleRequest {
|
||||
roleName: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface MerchantMember {
|
||||
membershipId: number | string
|
||||
userId: number | string
|
||||
@@ -159,6 +164,18 @@ export function createMerchantRole(payload: CreateMerchantRoleRequest) {
|
||||
return apiRequest<MerchantRole>({ method: 'POST', url: '/merchant/v1/iam/roles', data: payload })
|
||||
}
|
||||
|
||||
export function updateMerchantRole(roleId: number | string, payload: UpdateMerchantRoleRequest) {
|
||||
return apiRequest<MerchantRole>({ method: 'PUT', url: `/merchant/v1/iam/roles/${roleId}`, data: payload })
|
||||
}
|
||||
|
||||
export function changeMerchantRoleStatus(roleId: number | string, status: 'ACTIVE' | 'DISABLED') {
|
||||
return apiRequest<MerchantRole>({ method: 'PUT', url: `/merchant/v1/iam/roles/${roleId}/status`, data: { status } })
|
||||
}
|
||||
|
||||
export function deleteMerchantRole(roleId: number | string) {
|
||||
return apiRequest<void>({ method: 'DELETE', url: `/merchant/v1/iam/roles/${roleId}` })
|
||||
}
|
||||
|
||||
export function replaceMerchantRolePermissions(roleId: number | string, permissionIds: Array<number | string>) {
|
||||
return apiRequest<void>({ method: 'PUT', url: '/merchant/v1/iam/roles/permissions', data: { roleId, permissionIds } })
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { apiRequest } from '@/api/http'
|
||||
|
||||
export type MerchantPartnerType = 'SENDER' | 'RECEIVER'
|
||||
export type MerchantPartnerType = 'CUSTOMER' | 'SUPPLIER' | 'BOTH'
|
||||
|
||||
export interface MerchantPartner {
|
||||
id: number | string
|
||||
@@ -19,7 +19,7 @@ export interface MerchantPartner {
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'DISABLED' | string
|
||||
}
|
||||
|
||||
export function getMerchantPartnerCandidates(type: MerchantPartnerType, keyword?: string) {
|
||||
export function getMerchantPartnerCandidates(type?: MerchantPartnerType, keyword?: string) {
|
||||
return apiRequest<MerchantPartner[]>({
|
||||
method: 'GET',
|
||||
url: '/merchant/v1/partners/candidates',
|
||||
|
||||
@@ -13,11 +13,15 @@ test('keeps counterparty availability behind a replaceable read-only source', ()
|
||||
assert.match(source, /async load\(\)/)
|
||||
assert.match(source, /party\.status === 'ACTIVE' && party\.roles\.includes\(role\)/)
|
||||
assert.match(source, /getMerchantPartnerCandidates/)
|
||||
assert.match(source, /roles: \['SHIPPER', 'CONSIGNEE'\]/)
|
||||
assert.match(source, /await getMerchantPartnerCandidates\(\)/)
|
||||
assert.doesNotMatch(source, /getMerchantPartnerCandidates\('SENDER'\)|getMerchantPartnerCandidates\('RECEIVER'\)/)
|
||||
assert.match(viewSource, /firstLegPartySource\.listAvailable\('SHIPPER'\)/)
|
||||
assert.match(viewSource, /firstLegPartySource\.listAvailable\('CONSIGNEE'\)/)
|
||||
assert.match(viewSource, /firstLegPartySource\.findAvailable\(id, role\)/)
|
||||
assert.match(viewSource, /firstLegPartySource\.load\(\)/)
|
||||
assert.doesNotMatch(viewSource, /DEMO_FIRST_LEG_PARTIES/)
|
||||
assert.match(apiSource, /url: '\/merchant\/v1\/partners\/candidates'/)
|
||||
assert.match(apiSource, /'CUSTOMER' \| 'SUPPLIER' \| 'BOTH'/)
|
||||
assert.match(apiSource, /url: `\/merchant\/v1\/partners\/\$\{id\}`/)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getMerchantPartnerCandidates, type MerchantPartner, type MerchantPartnerType } from '@/api/partners'
|
||||
import { getMerchantPartnerCandidates, type MerchantPartner } from '@/api/partners'
|
||||
import type { FirstLegParty, FirstLegPartyRole } from '@/data/firstLegParties'
|
||||
|
||||
export interface FirstLegPartySource {
|
||||
@@ -13,12 +13,14 @@ function isAvailableForRole(party: FirstLegParty, role: FirstLegPartyRole) {
|
||||
|
||||
const parties: FirstLegParty[] = []
|
||||
|
||||
function mapPartner(partner: MerchantPartner, role: FirstLegPartyRole): FirstLegParty {
|
||||
function mapPartner(partner: MerchantPartner): FirstLegParty {
|
||||
return {
|
||||
id: String(partner.id),
|
||||
code: partner.partnerCode,
|
||||
name: partner.partnerName,
|
||||
roles: [role],
|
||||
// 后端往来单位类型为 CUSTOMER / SUPPLIER / BOTH,不表达订单中的发货/收货角色;
|
||||
// 两类订单角色均可选择启用主数据,提交时分别保存独立快照。
|
||||
roles: ['SHIPPER', 'CONSIGNEE'],
|
||||
contactName: partner.contactName ?? '',
|
||||
phone: partner.contactPhone ?? '',
|
||||
email: partner.contactEmail ?? '',
|
||||
@@ -31,10 +33,6 @@ function mapPartner(partner: MerchantPartner, role: FirstLegPartyRole): FirstLeg
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCandidates(type: MerchantPartnerType, role: FirstLegPartyRole) {
|
||||
return (await getMerchantPartnerCandidates(type)).map((partner) => mapPartner(partner, role))
|
||||
}
|
||||
|
||||
export const firstLegPartySource: FirstLegPartySource = {
|
||||
listAvailable(role) {
|
||||
return parties.filter((party) => isAvailableForRole(party, role))
|
||||
@@ -43,9 +41,6 @@ export const firstLegPartySource: FirstLegPartySource = {
|
||||
return parties.find((party) => party.id === id && isAvailableForRole(party, role))
|
||||
},
|
||||
async load() {
|
||||
parties.splice(0, parties.length, ...await Promise.all([
|
||||
loadCandidates('SENDER', 'SHIPPER'),
|
||||
loadCandidates('RECEIVER', 'CONSIGNEE'),
|
||||
]).then((groups) => groups.flat()))
|
||||
parties.splice(0, parties.length, ...(await getMerchantPartnerCandidates()).map(mapPartner))
|
||||
},
|
||||
}
|
||||
|
||||
+9
-23
@@ -5,32 +5,18 @@ import { loginMerchant, logoutMerchant } from '@/api/auth'
|
||||
import { ApiError } from '@/api/http'
|
||||
import { getMerchantPermissionResources, getMerchantPersonalProfile, getTenantProfile, updateMerchantPersonalProfile, type MerchantPermissionResource } from '@/api/merchantIam'
|
||||
import { clearSessionTokens, getAccessToken, getRefreshToken, persistTokenPair } from '@/api/session'
|
||||
import { TENANT_PERMISSION_GROUPS } from '@/stores/tenantAdministration'
|
||||
import { FRONTEND_TO_MERCHANT_PERMISSION_CODES, TENANT_PERMISSION_GROUPS } from '@/stores/tenantAdministration'
|
||||
import { decodeMerchantToken, type MerchantJwtPayload } from '@/utils/jwt'
|
||||
|
||||
const frontendPermissionCodes = new Set(TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code)))
|
||||
const backendPermissionMapping: Readonly<Record<string, readonly string[]>> = {
|
||||
'merchant:profile:view': ['company:profile:view'],
|
||||
'merchant:profile:edit': ['company:profile:edit'],
|
||||
'merchant:qualification:upload': ['company:qualification:upload'],
|
||||
'merchant:member:list': ['company:member:list'],
|
||||
'merchant:member:create': ['company:member:write'],
|
||||
'merchant:member:edit': ['company:member:write'],
|
||||
'merchant:member:grant-role': ['company:member:write'],
|
||||
'merchant:member:status': ['company:member:write'],
|
||||
'merchant:member:reset-password': ['company:member:reset-password'],
|
||||
'merchant:role:list': ['company:role:list', 'company:permission:list'],
|
||||
'merchant:role:create': ['company:role:write'],
|
||||
'merchant:role:edit': ['company:role:write'],
|
||||
'merchant:role:status': ['company:role:write'],
|
||||
'merchant:role:delete': ['company:role:write'],
|
||||
'merchant:role:grant': ['company:permission:list', 'company:permission:assign'],
|
||||
'merchant:order:list': ['order:first-leg:list'],
|
||||
'merchant:order:view': ['order:first-leg:list'],
|
||||
'merchant:order:create': ['order:first-leg:create'],
|
||||
'merchant:order:edit': ['order:first-leg:edit'],
|
||||
'merchant:order:submit': ['order:first-leg:submit'],
|
||||
}
|
||||
const backendPermissionMapping = Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES)
|
||||
.reduce<Record<string, string[]>>((mapping, [frontendCode, backendCodes]) => {
|
||||
for (const backendCode of backendCodes) {
|
||||
if (!mapping[backendCode]) mapping[backendCode] = []
|
||||
mapping[backendCode].push(frontendCode)
|
||||
}
|
||||
return mapping
|
||||
}, {})
|
||||
|
||||
export interface ClientAccountProfile {
|
||||
username: string
|
||||
|
||||
@@ -33,6 +33,7 @@ test('sends backend-required channel, customs, destination and cargo fields', ()
|
||||
assert.match(storeSource, new RegExp(field))
|
||||
}
|
||||
assert.match(storeSource, /containerMode: draft\.transportMode === 'FCL' \? 'FULL_CONTAINER'/)
|
||||
assert.match(storeSource, /customerId: draft\.shipperPartyId/)
|
||||
})
|
||||
|
||||
test('does not expose the out-of-scope cancellation write', () => {
|
||||
|
||||
@@ -63,6 +63,9 @@ function partyPayload(snapshot: FirstLegPartySnapshot): FirstLegOrderPartyPayloa
|
||||
function savePayload(draft: FirstLegOrderDraft, warehouse: PlatformReceivingWarehouse, customerName: string): FirstLegOrderSaveRequest {
|
||||
if (!draft.shipperPartySnapshot || !draft.consigneePartySnapshot) throw new Error('请完整填写收发货信息')
|
||||
return {
|
||||
// 后端把 customerId 作为订单主往来单位校验并以服务端名称覆盖 customerName;
|
||||
// 当前商户下单以所选发货单位作为主往来单位,收发货详情仍分别保存订单快照。
|
||||
customerId: draft.shipperPartyId,
|
||||
customerName,
|
||||
transportMode: apiTransport(draft),
|
||||
tradeTerm: draft.tradeTerm || undefined,
|
||||
|
||||
@@ -2,8 +2,10 @@ import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import {
|
||||
changeMerchantMemberStatus,
|
||||
changeMerchantRoleStatus,
|
||||
createMerchantMember,
|
||||
createMerchantRole,
|
||||
deleteMerchantRole,
|
||||
getMerchantMembers,
|
||||
getMerchantPermissionResources,
|
||||
getMerchantRoles,
|
||||
@@ -12,6 +14,7 @@ import {
|
||||
resetMerchantMemberPassword,
|
||||
replaceMerchantRolePermissions,
|
||||
updateMerchantMember,
|
||||
updateMerchantRole,
|
||||
updateTenantProfile,
|
||||
type MerchantRole,
|
||||
type MerchantMember,
|
||||
@@ -67,6 +70,7 @@ export interface TenantRole {
|
||||
description: string
|
||||
status: TenantRecordStatus
|
||||
builtin: boolean
|
||||
permissionIds: string[]
|
||||
permissionCodes: string[]
|
||||
}
|
||||
|
||||
@@ -136,7 +140,7 @@ export const TENANT_PERMISSION_GROUPS: readonly TenantPermissionGroup[] = [
|
||||
|
||||
const allPermissionCodes = TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code))
|
||||
|
||||
const frontendToBackendPermissionCodes: Readonly<Record<string, readonly string[]>> = {
|
||||
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'],
|
||||
@@ -154,10 +158,12 @@ const frontendToBackendPermissionCodes: Readonly<Record<string, readonly string[
|
||||
'company:permission:assign': ['merchant:role:grant'],
|
||||
}
|
||||
|
||||
const mappedMerchantPermissionCodes = new Set(Object.values(FRONTEND_TO_MERCHANT_PERMISSION_CODES).flat())
|
||||
|
||||
const initialRoles: TenantRole[] = [
|
||||
{ id: 'ROLE-ADMIN', name: '企业管理员', code: 'TENANT_ADMIN', description: '企业内置管理员,拥有全部客户端权限', status: 'ACTIVE', builtin: true, permissionCodes: [...allPermissionCodes] },
|
||||
{ id: 'ROLE-ORDER', name: '订单专员', code: 'ORDER_OPERATOR', description: '负责头程订单创建、提交和跟进', status: 'ACTIVE', builtin: false, permissionCodes: ['account:profile:view', 'dashboard:view', 'order:first-leg:list', 'order:first-leg:create', 'order:first-leg:edit', 'order:first-leg:submit', 'order:first-leg:cancel', 'company:profile:view'] },
|
||||
{ id: 'ROLE-VIEWER', name: '业务查看员', code: 'BUSINESS_VIEWER', description: '只读查看企业资料和业务订单', status: 'ACTIVE', builtin: false, permissionCodes: ['account:profile:view', 'dashboard:view', 'order:first-leg:list', 'company:profile:view'] },
|
||||
{ id: 'ROLE-ADMIN', name: '企业管理员', code: 'TENANT_ADMIN', description: '企业内置管理员,拥有全部客户端权限', status: 'ACTIVE', builtin: true, permissionIds: [], permissionCodes: [...allPermissionCodes] },
|
||||
{ id: 'ROLE-ORDER', name: '订单专员', code: 'ORDER_OPERATOR', description: '负责头程订单创建、提交和跟进', status: 'ACTIVE', builtin: false, permissionIds: [], permissionCodes: ['account:profile:view', 'dashboard:view', 'order:first-leg:list', 'order:first-leg:create', 'order:first-leg:edit', 'order:first-leg:submit', 'order:first-leg:cancel', 'company:profile:view'] },
|
||||
{ id: 'ROLE-VIEWER', name: '业务查看员', code: 'BUSINESS_VIEWER', description: '只读查看企业资料和业务订单', status: 'ACTIVE', builtin: false, permissionIds: [], permissionCodes: ['account:profile:view', 'dashboard:view', 'order:first-leg:list', 'company:profile:view'] },
|
||||
]
|
||||
|
||||
const initialMembers: TenantMember[] = [
|
||||
@@ -228,7 +234,7 @@ export const useTenantAdministrationStore = defineStore('tenant-administration',
|
||||
const grantedBackendCodes = new Set(permissionResources.value
|
||||
.filter((resource) => grantedIds.includes(String(resource.id)))
|
||||
.map((resource) => resource.permissionCode))
|
||||
const permissionCodes = Object.entries(frontendToBackendPermissionCodes)
|
||||
const permissionCodes = Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES)
|
||||
.filter(([, backendCodes]) => backendCodes.some((code) => grantedBackendCodes.has(code)))
|
||||
.map(([frontendCode]) => frontendCode)
|
||||
return {
|
||||
@@ -238,6 +244,7 @@ export const useTenantAdministrationStore = defineStore('tenant-administration',
|
||||
description: role.description ?? '暂无角色说明',
|
||||
status: role.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE',
|
||||
builtin: role.roleType === 'SYSTEM',
|
||||
permissionIds: grantedIds,
|
||||
permissionCodes: role.roleType === 'SYSTEM' && role.roleCode === 'MERCHANT_OWNER' ? [...allPermissionCodes] : permissionCodes,
|
||||
}
|
||||
}
|
||||
@@ -352,24 +359,28 @@ export const useTenantAdministrationStore = defineStore('tenant-administration',
|
||||
roles.value.push(mapMerchantRole(created))
|
||||
}
|
||||
|
||||
function updateRole(id: string, payload: Pick<TenantRole, 'name' | 'description'>) {
|
||||
async function updateRole(id: string, payload: Pick<TenantRole, 'name' | 'description'>) {
|
||||
const role = roles.value.find((item) => item.id === id)
|
||||
if (!role) throw new Error('角色不存在')
|
||||
Object.assign(role, payload)
|
||||
if (role.builtin) throw new Error('内置角色不能编辑')
|
||||
const updated = await updateMerchantRole(id, { roleName: payload.name.trim(), description: payload.description.trim() })
|
||||
Object.assign(role, mapMerchantRole(updated))
|
||||
}
|
||||
|
||||
function toggleRoleStatus(id: string) {
|
||||
async function toggleRoleStatus(id: string) {
|
||||
const role = roles.value.find((item) => item.id === id)
|
||||
if (!role) throw new Error('角色不存在')
|
||||
if (role.builtin) throw new Error('内置角色不能停用')
|
||||
role.status = role.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
|
||||
const updated = await changeMerchantRoleStatus(id, role.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE')
|
||||
Object.assign(role, mapMerchantRole(updated))
|
||||
}
|
||||
|
||||
function deleteRole(id: string) {
|
||||
async function deleteRole(id: string) {
|
||||
const role = roles.value.find((item) => item.id === id)
|
||||
if (!role) throw new Error('角色不存在')
|
||||
if (role.builtin) throw new Error('内置角色不能删除')
|
||||
if (members.value.some((member) => member.roleIds.includes(id))) throw new Error('该角色仍有成员使用,不能删除')
|
||||
await deleteMerchantRole(id)
|
||||
roles.value = roles.value.filter((item) => item.id !== id)
|
||||
}
|
||||
|
||||
@@ -377,11 +388,18 @@ export const useTenantAdministrationStore = defineStore('tenant-administration',
|
||||
const role = roles.value.find((item) => item.id === id)
|
||||
if (!role) throw new Error('角色不存在')
|
||||
if (role.builtin) throw new Error('内置管理员权限不能修改')
|
||||
const backendCodes = new Set(permissionCodes.flatMap((code) => frontendToBackendPermissionCodes[code] ?? []))
|
||||
const permissionIds = permissionResources.value
|
||||
const backendCodes = new Set(permissionCodes.flatMap((code) => FRONTEND_TO_MERCHANT_PERMISSION_CODES[code] ?? []))
|
||||
const selectedPermissionIds = permissionResources.value
|
||||
.filter((resource) => backendCodes.has(resource.permissionCode))
|
||||
.map((resource) => resource.id)
|
||||
await replaceMerchantRolePermissions(id, permissionIds)
|
||||
const mappedResourceIds = new Set(permissionResources.value
|
||||
.filter((resource) => mappedMerchantPermissionCodes.has(resource.permissionCode))
|
||||
.map((resource) => String(resource.id)))
|
||||
// The endpoint replaces every ID, including MENU nodes this screen does not render.
|
||||
const preservedPermissionIds = role.permissionIds.filter((permissionId) => !mappedResourceIds.has(permissionId))
|
||||
const nextPermissionIds = [...new Set([...preservedPermissionIds, ...selectedPermissionIds])]
|
||||
await replaceMerchantRolePermissions(id, nextPermissionIds)
|
||||
role.permissionIds = nextPermissionIds.map(String)
|
||||
role.permissionCodes = [...new Set(permissionCodes)]
|
||||
}
|
||||
|
||||
|
||||
+17
-6
@@ -13,6 +13,8 @@ const keyword = ref('')
|
||||
const editorOpen = ref(false)
|
||||
const editingId = ref('')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const actionRoleId = ref('')
|
||||
const form = reactive({ name: '', code: '', description: '' })
|
||||
|
||||
const filteredRoles = computed(() => {
|
||||
@@ -54,13 +56,16 @@ async function saveRole() {
|
||||
ElMessage.warning('请完整填写角色名称、编码和说明')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingId.value) store.updateRole(editingId.value, { name: form.name, description: form.description })
|
||||
if (editingId.value) await store.updateRole(editingId.value, { name: form.name, description: form.description })
|
||||
else await store.createRole({ ...form, code: form.code.trim().toUpperCase() })
|
||||
editorOpen.value = false
|
||||
ElMessage.success(editingId.value ? '角色已更新' : '角色已新增')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,22 +73,28 @@ async function toggleStatus(role: TenantRole) {
|
||||
const action = role.status === 'ACTIVE' ? '停用' : '启用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`${action}后将影响拥有该角色的成员权限。`, `${action}角色 ${role.name}`, { type: 'warning', confirmButtonText: action, cancelButtonText: '取消' })
|
||||
store.toggleRoleStatus(role.id)
|
||||
actionRoleId.value = role.id
|
||||
await store.toggleRoleStatus(role.id)
|
||||
ElMessage.success(`角色已${action}`)
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
ElMessage.error(error instanceof Error ? error.message : '操作失败')
|
||||
} finally {
|
||||
actionRoleId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRole(role: TenantRole) {
|
||||
try {
|
||||
await ElMessageBox.confirm('删除后不能恢复,仅未被成员使用的自定义角色可以删除。', `删除角色 ${role.name}`, { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
|
||||
store.deleteRole(role.id)
|
||||
actionRoleId.value = role.id
|
||||
await store.deleteRole(role.id)
|
||||
ElMessage.success('角色已删除')
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||
} finally {
|
||||
actionRoleId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +106,7 @@ function assignPermissions(role: TenantRole) {
|
||||
<template>
|
||||
<section v-loading="loading" class="roles-page">
|
||||
<div class="role-guidance client-card"><ShieldCheck :size="22" /><div><strong>角色用于约束企业内部成员可见菜单和可执行操作</strong><span>企业管理员为系统内置角色,不能停用或删除;平台后台权限不会出现在客户工作台。</span></div></div>
|
||||
<div v-if="store.rolesLoadedFromApi" class="role-contract-notice client-card">角色列表、新增和已授权权限回显已接入后端;编辑、启停、删除路径尚未进入当前 Gateway OpenAPI,相关写操作暂不开放。</div>
|
||||
<div v-if="store.rolesLoadedFromApi" class="role-contract-notice client-card">角色列表、新增、编辑、启停、删除及权限回显均使用真实 IAM 接口;系统内置角色和仍有关联成员的角色继续受服务端保护。</div>
|
||||
|
||||
<div class="role-toolbar client-card">
|
||||
<el-input v-model="keyword" clearable placeholder="搜索角色名称、编码或说明"><template #prefix><Search :size="16" /></template></el-input>
|
||||
@@ -107,7 +118,7 @@ function assignPermissions(role: TenantRole) {
|
||||
<header><div class="role-icon"><LockKeyhole v-if="role.builtin" :size="20" /><ShieldCheck v-else :size="20" /></div><div class="role-heading"><div><h3>{{ role.name }}</h3><el-tag v-if="role.builtin" effect="plain">内置</el-tag><el-tag :type="role.status === 'ACTIVE' ? 'success' : 'info'" effect="light">{{ role.status === 'ACTIVE' ? '启用' : '停用' }}</el-tag></div><code>{{ role.code }}</code></div></header>
|
||||
<p>{{ role.description }}</p>
|
||||
<dl><div><dt><Users :size="15" />成员数</dt><dd>{{ store.roleMemberCount(role.id) }}</dd></div><div><dt><ShieldCheck :size="15" />权限数</dt><dd>{{ role.permissionCodes.length }}</dd></div></dl>
|
||||
<footer><el-button v-if="auth.hasPermission('company:permission:list')" link type="primary" @click="assignPermissions(role)">{{ auth.hasPermission('company:permission:assign') && !role.builtin ? '分配权限' : '查看权限' }}</el-button><el-button v-if="!store.rolesLoadedFromApi && auth.hasPermission('company:role:write')" link type="primary" @click="openEdit(role)"><Pencil :size="15" />编辑</el-button><el-button v-if="!store.rolesLoadedFromApi && auth.hasPermission('company:role:write') && !role.builtin" link :type="role.status === 'ACTIVE' ? 'warning' : 'success'" @click="toggleStatus(role)">{{ role.status === 'ACTIVE' ? '停用' : '启用' }}</el-button><el-button v-if="!store.rolesLoadedFromApi && auth.hasPermission('company:role:write') && !role.builtin" link type="danger" @click="removeRole(role)"><Trash2 :size="15" />删除</el-button></footer>
|
||||
<footer><el-button v-if="auth.hasPermission('company:permission:list')" link type="primary" :disabled="actionRoleId === role.id" @click="assignPermissions(role)">{{ auth.hasPermission('company:permission:assign') && !role.builtin ? '分配权限' : '查看权限' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !role.builtin" link type="primary" :disabled="actionRoleId === role.id" @click="openEdit(role)"><Pencil :size="15" />编辑</el-button><el-button v-if="auth.hasPermission('company:role:write') && !role.builtin" link :type="role.status === 'ACTIVE' ? 'warning' : 'success'" :loading="actionRoleId === role.id" @click="toggleStatus(role)">{{ role.status === 'ACTIVE' ? '停用' : '启用' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !role.builtin" link type="danger" :disabled="actionRoleId === role.id" @click="removeRole(role)"><Trash2 :size="15" />删除</el-button></footer>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="client-card client-empty">没有匹配的企业角色。</div>
|
||||
@@ -119,7 +130,7 @@ function assignPermissions(role: TenantRole) {
|
||||
<el-form-item label="角色说明" required><el-input v-model="form.description" type="textarea" :rows="4" maxlength="200" show-word-limit /></el-form-item>
|
||||
<div class="role-form-tip">保存后请前往“菜单权限”为角色分配可见菜单和操作权限。</div>
|
||||
</el-form>
|
||||
<template #footer><div class="drawer-actions"><el-button @click="editorOpen = false">取消</el-button><el-button type="primary" @click="saveRole">保存</el-button></div></template>
|
||||
<template #footer><div class="drawer-actions"><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveRole">保存</el-button></div></template>
|
||||
</el-drawer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -53,6 +53,9 @@ test('protects built-in roles and exposes role permission assignment', () => {
|
||||
assert.match(rolesSource, /auth\.hasPermission\('company:member:list'\).*store\.loadMembers\(\)/)
|
||||
assert.match(rolesSource, /await Promise\.all\(requests\)/)
|
||||
assert.match(rolesSource, /await store\.createRole/)
|
||||
assert.match(rolesSource, /await store\.updateRole/)
|
||||
assert.match(rolesSource, /await store\.toggleRoleStatus/)
|
||||
assert.match(rolesSource, /await store\.deleteRole/)
|
||||
})
|
||||
|
||||
test('shows company audit status and applies button permissions', () => {
|
||||
|
||||
Reference in New Issue
Block a user