423 lines
17 KiB
TypeScript
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,
|
|
}
|
|
})
|