Files
client-web/src/stores/tenantAdministration.ts
T
2026-07-30 17:59:42 +08:00

423 lines
17 KiB
TypeScript

import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import {
changeMerchantMemberStatus,
createMerchantMember,
createMerchantRole,
getMerchantMembers,
getMerchantPermissionResources,
getMerchantRoles,
getTenantProfile,
getTenantQualifications,
resetMerchantMemberPassword,
replaceMerchantRolePermissions,
updateMerchantMember,
updateTenantProfile,
type MerchantRole,
type MerchantMember,
type MerchantPermissionResource,
type MerchantTenantStatus,
} from '@/api/merchantIam'
export type TenantRecordStatus = 'ACTIVE' | 'INACTIVE'
export interface TenantCompanyProfile {
companyName: string
creditCode: string
companyType: string
registeredAddress: string
establishedDate: string
registeredCapital: string
legalRepresentative: string
legalRepresentativePhone: string
contactName: string
contactPhone: string
contactEmail: string
contactWechat: string
businessAddress: 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
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))
const frontendToBackendPermissionCodes: 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 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'] },
]
const initialMembers: TenantMember[] = [
{ id: 'MEMBER-001', name: '企业管理员', account: 'customer', phone: '138****6601', email: 'admin@example.com', roleIds: ['ROLE-ADMIN'], status: 'ACTIVE', lastLoginAt: '2026-07-28 16:42', isAdministrator: true },
{ id: 'MEMBER-002', name: '张伟', account: 'zhangwei', phone: '138****6789', email: 'zhangwei@example.com', roleIds: ['ROLE-ORDER'], status: 'ACTIVE', lastLoginAt: '2026-07-28 09:18', isAdministrator: false },
{ id: 'MEMBER-003', name: '李娜', account: 'lina', phone: '137****2210', email: 'lina@example.com', roleIds: ['ROLE-VIEWER'], status: 'INACTIVE', lastLoginAt: '2026-07-12 11:30', isAdministrator: false },
]
export const useTenantAdministrationStore = defineStore('tenant-administration', () => {
const company = ref<TenantCompanyProfile>({
companyName: '示例贸易有限公司',
creditCode: '91330100MA2XXXXXXX',
companyType: '有限责任公司',
registeredAddress: '浙江省杭州市滨江区',
establishedDate: '2018-06-15',
registeredCapital: '500 万人民币',
legalRepresentative: '陈明',
legalRepresentativePhone: '139****8866',
contactName: '张伟',
contactPhone: '138****6789',
contactEmail: 'business@example.com',
contactWechat: 'cc_business',
businessAddress: '浙江省杭州市滨江区演示大道 88 号',
auditStatus: 'PROFILE_REQUIRED',
})
const qualifications = ref<TenantQualification[]>([
{ id: 'QUAL-001', type: '营业执照', fileName: '示例贸易有限公司营业执照.pdf', status: 'VERIFIED', expiresAt: '2028-06-15', updatedAt: '2026-07-27 14:20', documentUrl: '' },
{ id: 'QUAL-002', type: '法人身份证', fileName: '法人身份证明.pdf', status: 'VERIFIED', expiresAt: '2030-01-01', updatedAt: '2026-07-27 14:22', documentUrl: '' },
{ id: 'QUAL-003', type: '货代资质证书', fileName: '货代资质证书.pdf', status: 'PENDING', expiresAt: '2027-03-01', updatedAt: '2026-07-28 10:08', documentUrl: '' },
])
const members = ref<TenantMember[]>(structuredClone(initialMembers))
const roles = ref<TenantRole[]>(structuredClone(initialRoles))
const rolesLoadedFromApi = ref(false)
const membersLoadedFromApi = ref(false)
const permissionResources = ref<MerchantPermissionResource[]>([])
const activeMemberCount = computed(() => members.value.filter((member) => member.status === 'ACTIVE').length)
async function loadCompanyContext() {
const [profile, qualificationRecords] = await Promise.all([
getTenantProfile(),
getTenantQualifications(),
])
company.value = {
...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(frontendToBackendPermissionCodes)
.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',
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() {
rolesLoadedFromApi.value = true
roles.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() {
membersLoadedFromApi.value = true
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 = {
...company.value,
companyName: updated.tenantName,
creditCode: updated.unifiedSocialCreditCode ?? '',
legalRepresentative: updated.legalName,
contactName: updated.contactName,
contactPhone: updated.contactPhone,
contactEmail: updated.contactEmail ?? '',
auditStatus: updated.status,
}
}
function setCompanyAuditStatusForPreview(status: TenantCompanyProfile['auditStatus']) {
company.value.auditStatus = status
}
function upsertQualification(payload: Omit<TenantQualification, 'id' | 'status' | 'updatedAt'>) {
const existing = qualifications.value.find((item) => item.type === payload.type)
const next = {
...payload,
status: 'PENDING' as const,
updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
}
if (existing) Object.assign(existing, next)
else qualifications.value.unshift({ id: `QUAL-${Date.now()}`, ...next })
company.value.auditStatus = 'PENDING'
}
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))
}
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)
}
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'
}
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('该角色仍有成员使用,不能删除')
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) => frontendToBackendPermissionCodes[code] ?? []))
const permissionIds = permissionResources.value
.filter((resource) => backendCodes.has(resource.permissionCode))
.map((resource) => resource.id)
await replaceMerchantRolePermissions(id, permissionIds)
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,
rolesLoadedFromApi,
membersLoadedFromApi,
loadCompanyContext,
loadRoles,
loadMembers,
updateCompany,
setCompanyAuditStatusForPreview,
upsertQualification,
createMember,
updateMember,
toggleMemberStatus,
resetMemberPassword,
createRole,
updateRole,
toggleRoleStatus,
deleteRole,
setRolePermissions,
roleMemberCount,
roleNames,
}
})