309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
import { computed, ref } from 'vue'
|
|
import { defineStore } from 'pinia'
|
|
import {
|
|
changeMerchantMemberStatus,
|
|
changeMerchantRoleStatus,
|
|
createMerchantMember,
|
|
createMerchantRole,
|
|
deleteMerchantRole,
|
|
getMerchantMembers,
|
|
getMerchantPermissionResources,
|
|
getMerchantRoles,
|
|
getTenantProfile,
|
|
getTenantQualifications,
|
|
resetMerchantMemberPassword,
|
|
replaceMerchantRolePermissions,
|
|
updateMerchantMember,
|
|
updateMerchantRole,
|
|
updateTenantProfile,
|
|
type MerchantRole,
|
|
type MerchantMember,
|
|
type MerchantPermissionResource,
|
|
} from '@/modules/tenant-management/api/merchantIam'
|
|
import {
|
|
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
|
|
TENANT_PERMISSION_GROUPS,
|
|
type MerchantTenantStatus,
|
|
} from '@/modules/tenant-management/contracts'
|
|
|
|
export type TenantRecordStatus = 'ACTIVE' | 'INACTIVE'
|
|
|
|
export interface TenantCompanyProfile {
|
|
companyName: string
|
|
creditCode: string
|
|
legalRepresentative: string
|
|
contactName: string
|
|
contactPhone: string
|
|
contactEmail: string
|
|
auditStatus: MerchantTenantStatus
|
|
}
|
|
|
|
export interface TenantQualification {
|
|
id: string
|
|
type: string
|
|
fileName: string
|
|
status: 'VERIFIED' | 'PENDING' | 'REJECTED' | 'EXPIRED'
|
|
expiresAt: string
|
|
updatedAt: string
|
|
documentUrl: string
|
|
}
|
|
|
|
export interface TenantMember {
|
|
id: string
|
|
name: string
|
|
account: string
|
|
phone: string
|
|
email: string
|
|
roleIds: string[]
|
|
status: TenantRecordStatus
|
|
lastLoginAt: string
|
|
isAdministrator: boolean
|
|
}
|
|
|
|
export interface TenantRole {
|
|
id: string
|
|
name: string
|
|
code: string
|
|
description: string
|
|
status: TenantRecordStatus
|
|
builtin: boolean
|
|
permissionIds: string[]
|
|
permissionCodes: string[]
|
|
}
|
|
|
|
const allPermissionCodes = TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code))
|
|
const mappedMerchantPermissionCodes = new Set(Object.values(FRONTEND_TO_MERCHANT_PERMISSION_CODES).flat())
|
|
|
|
const emptyCompany: TenantCompanyProfile = {
|
|
companyName: '',
|
|
creditCode: '',
|
|
legalRepresentative: '',
|
|
contactName: '',
|
|
contactPhone: '',
|
|
contactEmail: '',
|
|
auditStatus: 'PROFILE_REQUIRED',
|
|
}
|
|
|
|
export const useTenantAdministrationStore = defineStore('tenant-administration', () => {
|
|
const company = ref<TenantCompanyProfile>({ ...emptyCompany })
|
|
const qualifications = ref<TenantQualification[]>([])
|
|
const members = ref<TenantMember[]>([])
|
|
const roles = ref<TenantRole[]>([])
|
|
const permissionResources = ref<MerchantPermissionResource[]>([])
|
|
const activeMemberCount = computed(() => members.value.filter((member) => member.status === 'ACTIVE').length)
|
|
|
|
async function loadCompanyContext() {
|
|
company.value = { ...emptyCompany }
|
|
qualifications.value = []
|
|
const [profile, qualificationRecords] = await Promise.all([
|
|
getTenantProfile(),
|
|
getTenantQualifications(),
|
|
])
|
|
company.value = {
|
|
companyName: profile.tenantName,
|
|
creditCode: profile.unifiedSocialCreditCode ?? '',
|
|
legalRepresentative: profile.legalName,
|
|
contactName: profile.contactName,
|
|
contactPhone: profile.contactPhone,
|
|
contactEmail: profile.contactEmail ?? '',
|
|
auditStatus: profile.status,
|
|
}
|
|
qualifications.value = qualificationRecords.map((item) => ({
|
|
id: String(item.id),
|
|
type: item.qualificationType === 'BUSINESS_LICENSE' ? '营业执照' : item.qualificationType,
|
|
fileName: item.documentUrl.split('/').pop() || '营业执照',
|
|
status: item.status === 'APPROVED' ? 'VERIFIED' : item.status,
|
|
expiresAt: '-',
|
|
updatedAt: '-',
|
|
documentUrl: item.documentUrl,
|
|
}))
|
|
}
|
|
|
|
function mapMerchantRole(role: MerchantRole): TenantRole {
|
|
const grantedIds = (role.permissionIds ?? []).map(String)
|
|
const grantedBackendCodes = new Set(permissionResources.value
|
|
.filter((resource) => grantedIds.includes(String(resource.id)))
|
|
.map((resource) => resource.permissionCode))
|
|
const permissionCodes = Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES)
|
|
.filter(([, backendCodes]) => backendCodes.some((code) => grantedBackendCodes.has(code)))
|
|
.map(([frontendCode]) => frontendCode)
|
|
return {
|
|
id: String(role.id),
|
|
name: role.roleName,
|
|
code: role.roleCode,
|
|
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,
|
|
}
|
|
}
|
|
|
|
function mapMerchantMember(member: MerchantMember): TenantMember {
|
|
return {
|
|
id: String(member.membershipId),
|
|
name: member.displayName || `企业成员 ${member.membershipId}`,
|
|
account: member.memberNo || `用户 ${member.authUserId}`,
|
|
phone: member.phone ?? '',
|
|
email: member.email ?? '',
|
|
roleIds: member.roleIds.map(String),
|
|
status: member.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE',
|
|
lastLoginAt: '服务端未提供',
|
|
isAdministrator: member.memberType === 'OWNER',
|
|
}
|
|
}
|
|
|
|
async function loadRoles() {
|
|
roles.value = []
|
|
permissionResources.value = []
|
|
const [roleRecords, resources] = await Promise.all([getMerchantRoles(), getMerchantPermissionResources()])
|
|
permissionResources.value = resources.filter((item) => item.status === 'ACTIVE')
|
|
roles.value = roleRecords.map(mapMerchantRole)
|
|
}
|
|
|
|
async function loadMembers() {
|
|
members.value = []
|
|
members.value = (await getMerchantMembers()).map(mapMerchantMember)
|
|
}
|
|
|
|
async function updateCompany(payload: TenantCompanyProfile) {
|
|
const updated = await updateTenantProfile({
|
|
tenantName: payload.companyName.trim(),
|
|
legalName: payload.legalRepresentative.trim(),
|
|
contactName: payload.contactName.trim(),
|
|
contactPhone: payload.contactPhone.trim(),
|
|
contactEmail: payload.contactEmail.trim(),
|
|
})
|
|
company.value = {
|
|
companyName: updated.tenantName,
|
|
creditCode: updated.unifiedSocialCreditCode ?? '',
|
|
legalRepresentative: updated.legalName,
|
|
contactName: updated.contactName,
|
|
contactPhone: updated.contactPhone,
|
|
contactEmail: updated.contactEmail ?? '',
|
|
auditStatus: updated.status,
|
|
}
|
|
}
|
|
|
|
async function createMember(payload: { name: string; account: string; password: string; phone: string; email: string; roleIds: string[]; memberNo: string }) {
|
|
const created = await createMerchantMember({
|
|
username: payload.account.trim(),
|
|
password: payload.password,
|
|
displayName: payload.name.trim(),
|
|
phone: payload.phone.trim(),
|
|
email: payload.email.trim(),
|
|
memberType: 'EMPLOYEE',
|
|
memberNo: payload.memberNo.trim(),
|
|
roleIds: payload.roleIds,
|
|
})
|
|
members.value.unshift(mapMerchantMember(created))
|
|
}
|
|
|
|
async function updateMember(id: string, payload: Pick<TenantMember, 'name' | 'phone' | 'email' | 'roleIds'> & { memberNo: string }) {
|
|
const member = members.value.find((item) => item.id === id)
|
|
if (!member) throw new Error('成员不存在')
|
|
const updated = await updateMerchantMember(id, {
|
|
displayName: payload.name.trim(),
|
|
phone: payload.phone.trim(),
|
|
email: payload.email.trim(),
|
|
memberType: member.isAdministrator ? 'OWNER' : 'EMPLOYEE',
|
|
memberNo: payload.memberNo.trim(),
|
|
roleIds: payload.roleIds,
|
|
})
|
|
Object.assign(member, mapMerchantMember(updated))
|
|
}
|
|
|
|
async function toggleMemberStatus(id: string) {
|
|
const member = members.value.find((item) => item.id === id)
|
|
if (!member) throw new Error('成员不存在')
|
|
if (member.isAdministrator) throw new Error('企业管理员不能停用')
|
|
const updated = await changeMerchantMemberStatus(id, member.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE')
|
|
Object.assign(member, mapMerchantMember(updated))
|
|
}
|
|
|
|
async function resetMemberPassword(id: string, newPassword: string) {
|
|
await resetMerchantMemberPassword(id, newPassword)
|
|
}
|
|
|
|
async function createRole(payload: Pick<TenantRole, 'name' | 'code' | 'description'>) {
|
|
if (roles.value.some((role) => role.code === payload.code)) throw new Error('角色编码已存在')
|
|
const created = await createMerchantRole({ roleCode: payload.code, roleName: payload.name, description: payload.description })
|
|
roles.value.push(mapMerchantRole(created))
|
|
}
|
|
|
|
async function updateRole(id: string, payload: Pick<TenantRole, 'name' | 'description'>) {
|
|
const role = roles.value.find((item) => item.id === id)
|
|
if (!role) throw new Error('角色不存在')
|
|
if (role.builtin) throw new Error('内置角色不能编辑')
|
|
const updated = await updateMerchantRole(id, { roleName: payload.name.trim(), description: payload.description.trim() })
|
|
Object.assign(role, mapMerchantRole(updated))
|
|
}
|
|
|
|
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('内置角色不能停用')
|
|
const updated = await changeMerchantRoleStatus(id, role.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE')
|
|
Object.assign(role, mapMerchantRole(updated))
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
async function setRolePermissions(id: string, permissionCodes: string[]) {
|
|
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) => FRONTEND_TO_MERCHANT_PERMISSION_CODES[code] ?? []))
|
|
const selectedPermissionIds = permissionResources.value
|
|
.filter((resource) => backendCodes.has(resource.permissionCode))
|
|
.map((resource) => resource.id)
|
|
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)]
|
|
}
|
|
|
|
function roleMemberCount(roleId: string) {
|
|
return members.value.filter((member) => member.roleIds.includes(roleId)).length
|
|
}
|
|
|
|
function roleNames(roleIds: readonly string[]) {
|
|
return roleIds.map((id) => roles.value.find((role) => role.id === id)?.name).filter((name): name is string => Boolean(name))
|
|
}
|
|
|
|
return {
|
|
company,
|
|
qualifications,
|
|
members,
|
|
roles,
|
|
activeMemberCount,
|
|
loadCompanyContext,
|
|
loadRoles,
|
|
loadMembers,
|
|
updateCompany,
|
|
createMember,
|
|
updateMember,
|
|
toggleMemberStatus,
|
|
resetMemberPassword,
|
|
createRole,
|
|
updateRole,
|
|
toggleRoleStatus,
|
|
deleteRole,
|
|
setRolePermissions,
|
|
roleMemberCount,
|
|
roleNames,
|
|
}
|
|
})
|