2026-07-30 17:59:42 +08:00
< script setup lang = "ts" >
2026-08-01 12:17:33 +08:00
import { computed , nextTick , onMounted , reactive , ref } from 'vue'
2026-07-30 17:59:42 +08:00
import { useRouter } from 'vue-router'
2026-08-01 12:17:33 +08:00
import { ElMessage , ElMessageBox , type FormInstance , type FormRules } from 'element-plus'
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'
2026-07-30 17:59:42 +08:00
import { useTenantAdministrationStore , type TenantRole } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth'
const store = useTenantAdministrationStore ()
const auth = useAuthStore ()
const router = useRouter ()
2026-08-01 12:17:33 +08:00
const roleFilters = ref ({ keyword : '' })
const appliedRoleFilters = ref ({ ... roleFilters . value })
const rolePage = ref ( 1 )
const rolePageSize = ref ( 10 )
2026-07-30 17:59:42 +08:00
const editorOpen = ref ( false )
const editingId = ref ( '' )
const loading = ref ( false )
2026-07-31 15:34:22 +08:00
const loadError = ref ( '' )
2026-07-31 13:43:16 +08:00
const saving = ref ( false )
const actionRoleId = ref ( '' )
2026-08-01 12:17:33 +08:00
const formRef = ref < FormInstance >()
2026-07-30 17:59:42 +08:00
const form = reactive ({ name : '' , code : '' , description : '' })
2026-08-01 12:17:33 +08:00
const roleRules : FormRules = {
name : [{ required : true , message : '请输入角色名称' , trigger : 'blur' }],
code : [{ required : true , message : '请输入角色编码' , trigger : 'blur' }],
description : [{ required : true , message : '请填写角色说明' , trigger : 'blur' }],
}
2026-07-30 17:59:42 +08:00
2026-08-01 12:17:33 +08:00
const roleQueryFields : QueryField [] = [{ key : 'keyword' , type : 'input' , placeholder : '搜索角色名称、编码或说明' }]
const roleTableColumns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , fixed : 'left' , width : 250 , slotName : 'actions' },
{ key : 'role' , label : '角色' , minWidth : 180 , slotName : 'role' },
{ key : 'description' , prop : 'description' , label : '角色说明' , minWidth : 220 },
{ key : 'memberCount' , label : '成员数' , width : 100 , slotName : 'memberCount' },
{ key : 'permissionCount' , label : '权限数' , width : 100 , slotName : 'permissionCount' },
{ key : 'status' , label : '状态' , width : 90 , slotName : 'status' },
]
2026-07-30 17:59:42 +08:00
const filteredRoles = computed (() => {
2026-08-01 12:17:33 +08:00
const key = appliedRoleFilters . value . keyword . trim (). toLowerCase ()
2026-07-30 17:59:42 +08:00
return store . roles . filter (( role ) => ! key || [ role . name , role . code , role . description ]. some (( value ) => value . toLowerCase (). includes ( key )))
})
2026-08-01 12:17:33 +08:00
const pagedRoles = computed (() => filteredRoles . value . slice (( rolePage . value - 1 ) * rolePageSize . value , rolePage . value * rolePageSize . value ))
2026-07-30 17:59:42 +08:00
function resetForm () {
editingId . value = ''
Object . assign ( form , { name : '' , code : '' , description : '' })
2026-08-01 12:17:33 +08:00
void nextTick (() => formRef . value ? . clearValidate ())
2026-07-30 17:59:42 +08:00
}
function openCreate () {
resetForm ()
editorOpen . value = true
}
function openEdit ( role : TenantRole ) {
editingId . value = role . id
Object . assign ( form , { name : role . name , code : role . code , description : role . description })
editorOpen . value = true
}
2026-07-31 15:34:22 +08:00
async function loadRoles () {
2026-07-30 17:59:42 +08:00
loading . value = true
2026-07-31 15:34:22 +08:00
loadError . value = ''
2026-07-30 17:59:42 +08:00
try {
2026-07-30 21:23:06 +08:00
const requests = [ store . loadRoles ()]
if ( auth . hasPermission ( 'company:member:list' )) requests . push ( store . loadMembers ())
await Promise . all ( requests )
2026-07-30 17:59:42 +08:00
} catch ( error ) {
2026-07-31 15:34:22 +08:00
loadError . value = error instanceof Error ? error . message : '角色与成员统计加载失败'
2026-07-30 17:59:42 +08:00
} finally {
loading . value = false
}
2026-07-31 15:34:22 +08:00
}
onMounted (() => void loadRoles ())
2026-07-30 17:59:42 +08:00
2026-08-01 12:17:33 +08:00
function searchRoles ( filters : QueryModel ) {
appliedRoleFilters . value = { keyword : typeof filters . keyword === 'string' ? filters . keyword : '' }
rolePage . value = 1
void loadRoles ()
}
2026-07-30 17:59:42 +08:00
async function saveRole () {
2026-08-01 12:17:33 +08:00
if ( saving . value ) return
form . name = form . name . trim ()
form . code = form . code . trim (). toUpperCase ()
form . description = form . description . trim ()
const valid = await formRef . value ? . validate (). catch (() => false )
if ( ! valid ) return
2026-07-31 13:43:16 +08:00
saving . value = true
2026-07-30 17:59:42 +08:00
try {
2026-07-31 13:43:16 +08:00
if ( editingId . value ) await store . updateRole ( editingId . value , { name : form . name , description : form . description })
2026-07-30 17:59:42 +08:00
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 : '保存失败' )
2026-07-31 13:43:16 +08:00
} finally {
saving . value = false
2026-07-30 17:59:42 +08:00
}
}
async function toggleStatus ( role : TenantRole ) {
const action = role . status === 'ACTIVE' ? '停用' : '启用'
try {
await ElMessageBox . confirm ( ` ${ action } 后将影响拥有该角色的成员权限。` , ` ${ action } 角色 ${ role . name } ` , { type : 'warning' , confirmButtonText : action , cancelButtonText : '取消' })
2026-07-31 13:43:16 +08:00
actionRoleId . value = role . id
await store . toggleRoleStatus ( role . id )
2026-07-30 17:59:42 +08:00
ElMessage . success ( `角色已 ${ action } ` )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( error instanceof Error ? error . message : '操作失败' )
2026-07-31 13:43:16 +08:00
} finally {
actionRoleId . value = ''
2026-07-30 17:59:42 +08:00
}
}
async function removeRole ( role : TenantRole ) {
try {
await ElMessageBox . confirm ( '删除后不能恢复,仅未被成员使用的自定义角色可以删除。' , `删除角色 ${ role . name } ` , { type : 'warning' , confirmButtonText : '删除' , cancelButtonText : '取消' })
2026-07-31 13:43:16 +08:00
actionRoleId . value = role . id
await store . deleteRole ( role . id )
2026-07-30 17:59:42 +08:00
ElMessage . success ( '角色已删除' )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( error instanceof Error ? error . message : '删除失败' )
2026-07-31 13:43:16 +08:00
} finally {
actionRoleId . value = ''
2026-07-30 17:59:42 +08:00
}
}
function assignPermissions ( role : TenantRole ) {
router . push ({ name : 'permissions' , query : { role : role . id } })
}
</ script >
< template >
< section v-loading = "loading" class="roles-page" >
2026-07-31 15:34:22 +08:00
< div v-if = "loadError" class="client-card collection-load-error" role="alert" >
< div >< strong > 企业角色加载失败 </ strong >< span >{{ loadError }}</ span ></ div >
< el-button :loading = "loading" @click ="loadRoles" > 重新加载 </ el -button >
</ div >
< template v-else >
2026-08-01 12:17:33 +08:00
< QueryToolbar v-model = "roleFilters" ariaLabel="企业角色筛选" :fields="roleQueryFields" search-text="查询角色" @search="searchRoles" >
< template # actions >< el-button v-if = "auth.hasPermission('company:role:write')" type="primary" @click="openCreate"><Plus :size="17" /> 新增角色 </ el-button ></ template >
</ QueryToolbar >
2026-07-30 17:59:42 +08:00
2026-08-01 12:17:33 +08:00
< div class = "client-card role-table-card" >
< DataTable v -model:page = "rolePage" v -model:page-size = "rolePageSize" :data = "pagedRoles" :columns = "roleTableColumns" :loading = "loading" :total = "filteredRoles.length" row -key =" id " >
< template # actions = "{ row }" >< el-button v-if = "auth.hasPermission('company:permission:list')" link type="primary" :disabled="actionRoleId === (row as TenantRole).id" @click="assignPermissions(row as TenantRole)">{{ auth.hasPermission('company:permission:assign') && !(row as TenantRole).builtin ? '分配权限' : '查看权限' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !(row as TenantRole).builtin" link type="primary" :disabled="actionRoleId === (row as TenantRole).id" @click="openEdit(row as TenantRole)"><Pencil :size="15" />编辑</el-button><el-button v-if="auth.hasPermission('company:role:write') && !(row as TenantRole).builtin" link :type="(row as TenantRole).status === 'ACTIVE' ? 'warning' : 'success'" :loading="actionRoleId === (row as TenantRole).id" @click="toggleStatus(row as TenantRole)">{{ (row as TenantRole).status === 'ACTIVE' ? '停用' : '启用' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !(row as TenantRole).builtin" link type="danger" :disabled="actionRoleId === (row as TenantRole).id" @click="removeRole(row as TenantRole)"><Trash2 :size="15" /> 删除 </ el-button ></ template >
< template # role = "{ row }" >< div class = "role-identity" >< span class = "role-icon" >< LockKeyhole v-if = "(row as TenantRole).builtin" :size="20" /><ShieldCheck v-else :size="20" /></ span >< div >< strong >{{ ( row as TenantRole ). name }}</ strong >< code >{{ ( row as TenantRole ). code }}</ code ></ div ></ div ></ template >
< template # memberCount = "{ row }" >< span >{{ store . roleMemberCount (( row as TenantRole ). id ) }}</ span ></ template >
< template # permissionCount = "{ row }" >< span >{{ ( row as TenantRole ). permissionCodes . length }}</ span ></ template >
< template # status = "{ row }" >< el-tag v-if = "(row as TenantRole).builtin" effect="plain">内置</el-tag><el-tag :type="(row as TenantRole).status === 'ACTIVE' ? 'success' : 'info'" effect="light" >{{ ( row as TenantRole ) .status === ' ACTIVE ' ? ' 启用 ' : ' 停用 ' }} </ el -tag ></ template >
< template # empty >< div class = "client-empty" > 没有匹配的企业角色 。 </ div ></ template >
</ DataTable >
2026-07-30 17:59:42 +08:00
</ div >
< el-drawer v-model = "editorOpen" :title="editingId ? '编辑角色' : '新增角色'" size="min(500px, 94vw)" @closed="resetForm" >
2026-08-01 12:17:33 +08:00
< el-form ref = "formRef" :model = "form" :rules = "roleRules" label -position =" top " >
< el-form-item label = "角色名称" prop = "name" required >< el-input v-model = "form.name" maxlength="30" placeholder="例如 订单操作员" /></ el-form-item >
< el-form-item label = "角色编码" prop = "code" required >< el-input v-model = "form.code" :disabled="Boolean(editingId)" placeholder="例如 ORDER_OPERATOR" maxlength="40" /></ el-form-item >
< el-form-item label = "角色说明" prop = "description" required >< el-input v-model = "form.description" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="简要说明该角色的职责和适用范围" /></ el-form-item >
2026-07-30 17:59:42 +08:00
< div class = "role-form-tip" > 保存后请前往 “ 菜单权限 ” 为角色分配可见菜单和操作权限 。 </ div >
</ el-form >
2026-07-31 13:43:16 +08:00
< 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 >
2026-07-30 17:59:42 +08:00
</ el-drawer >
2026-07-31 15:34:22 +08:00
</ template >
2026-07-30 17:59:42 +08:00
</ section >
</ template >
< style scoped >
. roles - page { display : flex ; flex - direction : column ; gap : 14 px ; }
2026-07-31 15:34:22 +08:00
. collection - load - error { display : flex ; min - height : 150 px ; align - items : center ; justify - content : center ; gap : 20 px ; }
. collection - load - error > div { display : flex ; max - width : 520 px ; flex - direction : column ; gap : 6 px ; }
. collection - load - error span { color : var ( -- muted ); font - size : 12 px ; line - height : 1.6 ; }
2026-08-01 12:17:33 +08:00
. role - table - card { padding : 0 ; overflow : hidden ; }
. role - identity { display : flex ; min - width : 0 ; align - items : center ; gap : 10 px ; }
. role - icon { display : grid ; width : 34 px ; height : 34 px ; flex : 0 0 34 px ; place - items : center ; background : # e7f3f1 ; color : var ( -- primary ); }
. role - identity > div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 3 px ; }
. role - identity strong { overflow - wrap : anywhere ; }
. role - identity code { color : var ( -- muted ); font - size : 10 px ; }
2026-07-30 17:59:42 +08:00
. role - form - tip { padding : 12 px ; background : # f2f7f6 ; color : var ( -- muted ); font - size : 11 px ; }
. drawer - actions { display : flex ; justify - content : flex - end ; gap : 8 px ; }
2026-08-01 12:17:33 +08:00
. roles - page : deep (. el - form - item__error ) { line - height : 1.45 ; overflow - wrap : anywhere ; }
@ media ( max - width : 600 px ) { . drawer - actions { flex - wrap : wrap ; } }
2026-07-30 17:59:42 +08:00
</ style >