2026-07-30 17:59:43 +08:00
< script setup lang = "ts" >
import { computed , nextTick , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { Pencil , Plus , RotateCcw , Search , ShieldCheck , Trash2 } from '@lucide/vue'
2026-07-31 13:43:16 +08:00
import {
changePlatformRoleStatus ,
createPlatformRole ,
deletePlatformRole ,
listPlatformPermissions ,
listPlatformRoles ,
replacePlatformRolePermissions ,
updatePlatformRole ,
type PlatformRoleResource ,
} from '@/api/platformIam'
2026-07-30 17:59:43 +08:00
import { getApiErrorDetails , getApiErrorMessage , type ApiErrorDetails } from '@/api/http'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import MetricStrip from '@/components/MetricStrip.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAdminDataStore } from '@/stores/adminData'
2026-07-31 13:43:16 +08:00
import type { DataScope , Permission , Role } from '@/types/admin'
2026-07-30 17:59:43 +08:00
import { buildPermissionTree , summarizePermissionSelection } from '@/utils/permissionTree'
2026-07-31 13:43:16 +08:00
import { mapPlatformPermission } from '@/utils/platformPermission'
2026-07-30 17:59:43 +08:00
interface TreeController {
getCheckedKeys : ( leafOnly ?: boolean ) => Array < string | number >
getHalfCheckedKeys : () => Array < string | number >
setCheckedKeys : ( keys : string []) => void
}
const store = useAdminDataStore ()
const access = useAccessPreviewStore ()
const editorOpen = ref ( false )
const treeRef = ref < TreeController | null >( null )
const serverRoles = ref < Role [] >([])
2026-07-31 13:43:16 +08:00
const serverPermissions = ref < Permission [] >([])
2026-07-30 17:59:43 +08:00
const loading = ref ( false )
const saving = ref ( false )
2026-07-31 13:43:16 +08:00
const rowActionId = ref ( '' )
2026-07-30 17:59:43 +08:00
const loadError = ref < ApiErrorDetails | null >( null )
const filters = reactive ({ keyword : '' , status : '' , dataScope : '' })
const emptyForm = () : Omit < Role , ' userCount ' > => ({ id : '' , name : '' , code : '' , description : '' , dataScope : '本部门数据' , status : '启用' , permissionIds : [], builtIn : false })
const form = reactive ( emptyForm ())
const editorLabel = computed (() => form . id ? '配置角色' : '新增角色' )
2026-07-31 13:43:16 +08:00
const canEdit = computed (() => access . can ( 'platform:role:edit' ))
const canGrant = computed (() => access . can ( 'platform:role:grant' ))
const requiredPermission = computed (() => access . usesServerPermissions
? ( form . id && canEdit . value ? 'platform:role:edit' : form . id ? 'platform:role:grant' : 'platform:role:create' )
: 'system:role:grant' )
2026-07-30 17:59:43 +08:00
const { captureBaseline } = useUnsavedWorkRegistration ({
key : 'role-editor' ,
label : editorLabel ,
isOpen : editorOpen ,
model : form ,
requiredPermission ,
})
const dataScopes : DataScope [] = [ '全部数据' , '本部门数据' , '本人数据' , '自定义' ]
const roles = computed (() => access . usesServerPermissions ? serverRoles . value : store . roles )
const canCreate = computed (() => access . can ( 'platform:role:create' ))
const filteredRows = computed (() => roles . value . filter (( item ) => {
const keyword = filters . keyword . trim (). toLowerCase ()
return ( ! keyword || [ item . name , item . code , item . description ]. some (( value ) => value . toLowerCase (). includes ( keyword ))) && ( ! filters . status || item . status === filters . status ) && ( ! filters . dataScope || item . dataScope === filters . dataScope )
}))
const metrics = computed (() => [
{ label : '角色总数' , value : roles . value . length , note : access . usesServerPermissions ? 'IAM 返回的角色' : '平台角色模型' },
{ label : '启用角色' , value : roles . value . filter (( item ) => item . status === '启用' ). length , note : '当前可分配' },
{ label : '授权用户' , value : access . usesServerPermissions ? '—' : store . users . length , note : access . usesServerPermissions ? '成员关系待接口回显' : '已分配平台角色' },
2026-07-31 13:43:16 +08:00
{ label : '权限资源' , value : access . usesServerPermissions ? serverPermissions . value . length : store . permissions . length , note : '菜单、按钮与接口' },
2026-07-30 17:59:43 +08:00
])
2026-07-31 13:43:16 +08:00
const availablePermissions = computed (() => access . usesServerPermissions ? serverPermissions . value : store . permissions )
const permissionTree = computed (() => buildPermissionTree ( availablePermissions . value ))
2026-07-30 17:59:43 +08:00
watch (() => form . dataScope , () => undefined )
function resetFilters () { Object . assign ( filters , { keyword : '' , status : '' , dataScope : '' }) }
function mapPlatformRole ( role : PlatformRoleResource ) : Role {
return {
id : String ( role . id ),
name : role . roleName ,
code : role . roleCode ,
description : role . description ?? '暂无角色说明' ,
userCount : 0 ,
dataScope : '全部数据' ,
status : role . status === 'ACTIVE' ? '启用' : '停用' ,
2026-07-31 13:43:16 +08:00
permissionIds : ( role . permissionIds ?? []). map ( String ),
2026-07-30 17:59:43 +08:00
builtIn : role . roleType === 'SYSTEM' ,
}
}
async function loadRoles () {
if ( ! access . usesServerPermissions ) return
loading . value = true
loadError . value = null
serverRoles . value = []
2026-07-31 13:43:16 +08:00
serverPermissions . value = []
2026-07-30 17:59:43 +08:00
try {
2026-07-31 13:43:16 +08:00
const [ roleRecords , permissionRecords ] = await Promise . all ([ listPlatformRoles (), listPlatformPermissions ()])
serverPermissions . value = permissionRecords . map ( mapPlatformPermission )
serverRoles . value = roleRecords . map ( mapPlatformRole )
2026-07-30 17:59:43 +08:00
} catch ( error ) {
loadError . value = getApiErrorDetails ( error , '平台角色列表加载失败,请稍后重试' )
} finally {
loading . value = false
}
}
function openCreate () {
Object . assign ( form , emptyForm ())
captureBaseline ()
editorOpen . value = true
nextTick (() => treeRef . value ? . setCheckedKeys ([]))
}
function openEdit ( row : Role ) {
2026-07-31 13:43:16 +08:00
if ( access . usesServerPermissions && ! canEdit . value && ! canGrant . value ) return
2026-07-30 17:59:43 +08:00
Object . assign ( form , { ... row , permissionIds : [... row . permissionIds ] })
captureBaseline ()
editorOpen . value = true
nextTick (() => treeRef . value ? . setCheckedKeys ([... row . permissionIds ]))
}
function selectedPermissionIds () {
const checked = treeRef . value ? . getCheckedKeys ( false ). map ( String ) || []
const halfChecked = treeRef . value ? . getHalfCheckedKeys (). map ( String ) || []
return [... new Set ([... checked , ... halfChecked ])]
}
function syncPermissionIds () {
form . permissionIds = selectedPermissionIds ()
}
async function saveRole () {
if ( ! form . name . trim () || ! form . code . trim ()) { ElMessage . warning ( '请填写角色名称和角色编码' ); return }
const normalizedCode = form . code . trim (). toUpperCase ()
const duplicate = roles . value . some (( item ) => item . code . toLowerCase () === normalizedCode . toLowerCase () && item . id !== form . id )
if ( duplicate ) { ElMessage . error ( '角色编码已经存在' ); return }
if ( access . usesServerPermissions ) {
saving . value = true
try {
2026-07-31 13:43:16 +08:00
const permissionIds = selectedPermissionIds ()
if ( form . id ) {
if ( canEdit . value ) {
await updatePlatformRole ( form . id , { roleName : form . name . trim (), description : form . description . trim () })
}
if ( canGrant . value ) await replacePlatformRolePermissions ( form . id , permissionIds )
} else {
const created = await createPlatformRole ({
roleCode : normalizedCode ,
roleName : form . name . trim (),
description : form . description . trim (),
})
if ( canGrant . value && permissionIds . length > 0 ) await replacePlatformRolePermissions ( created . id , permissionIds )
}
await loadRoles ()
2026-07-30 17:59:43 +08:00
editorOpen . value = false
2026-07-31 13:43:16 +08:00
ElMessage . success ( form . id ? '角色已更新' : '角色已创建' )
2026-07-30 17:59:43 +08:00
} catch ( error ) {
ElMessage . error ( getApiErrorMessage ( error , '角色创建失败,请稍后重试' ))
} finally {
saving . value = false
}
return
}
const permissionIds = selectedPermissionIds ()
if ( permissionIds . length === 0 ) { ElMessage . warning ( '请至少选择一项菜单权限' ); return }
store . saveRole ({ ... form , id : form . id || undefined , permissionIds })
editorOpen . value = false
ElMessage . success ( form . id ? '角色及权限已更新' : '角色已创建' )
}
async function removeRole ( row : Role ) {
if ( row . builtIn ) { ElMessage . warning ( '系统内置角色不能删除' ); return }
if ( row . userCount > 0 ) { ElMessage . warning ( '请先移除该角色下的用户' ); return }
try {
await ElMessageBox . confirm ( `确认删除角色“ ${ row . name } ”吗?` , '删除角色' , { confirmButtonText : '删除' , cancelButtonText : '取消' , type : 'warning' })
2026-07-31 13:43:16 +08:00
if ( access . usesServerPermissions ) {
rowActionId . value = row . id
await deletePlatformRole ( row . id )
serverRoles . value = serverRoles . value . filter (( item ) => item . id !== row . id )
ElMessage . success ( '角色已删除' )
} else if ( store . removeRole ( row . id )) ElMessage . success ( '角色已删除' )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( getApiErrorMessage ( error , '角色删除失败,请稍后重试' ))
} finally {
rowActionId . value = ''
2026-07-30 17:59:43 +08:00
}
}
async function toggleStatus ( row : Role ) {
if ( row . builtIn ) { ElMessage . warning ( '系统内置角色不能停用' ); return }
const target = row . status === '启用' ? '停用' : '启用'
try {
await ElMessageBox . confirm ( `确认 ${ target } 角色“ ${ row . name } ”吗?` , '角色状态' , { confirmButtonText : '确认' , cancelButtonText : '取消' })
2026-07-31 13:43:16 +08:00
if ( access . usesServerPermissions ) {
rowActionId . value = row . id
const updated = await changePlatformRoleStatus ( row . id , target === '启用' ? 'ACTIVE' : 'DISABLED' )
Object . assign ( row , mapPlatformRole ( updated ))
} else store . saveRole ({ ... row , status : target })
2026-07-30 17:59:43 +08:00
ElMessage . success ( `角色已 ${ target } ` )
2026-07-31 13:43:16 +08:00
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( getApiErrorMessage ( error , '角色状态更新失败,请稍后重试' ))
} finally {
rowActionId . value = ''
2026-07-30 17:59:43 +08:00
}
}
function permissionSummary ( row : Role ) {
2026-07-31 13:43:16 +08:00
const { menuCount , actionCount } = summarizePermissionSelection ( availablePermissions . value , row . permissionIds )
2026-07-30 17:59:43 +08:00
return ` ${ menuCount } 菜单 · ${ actionCount } 操作`
}
onMounted ( loadRoles )
</ script >
< template >
< div class = "business-page" >
< MetricStrip :items = "metrics" />
2026-07-31 13:43:16 +08:00
< el-alert v-if = "access.usesServerPermissions" type="success" :closable="false" show-icon title="角色查询、新增、编辑、启停、删除及权限分配均使用 IAM 真实接口;系统内置角色继续由服务端保护。" />
2026-07-30 17:59:43 +08:00
< el-alert v-if = "loadError" type="error" :closable="false" show-icon role="alert"><template #title><div class="api-error-alert-title"><span>{{ loadError.message }}</span><ApiErrorMeta :details="loadError" /></div><el-button link type="danger" @click="loadRoles" > 重新加载 </ el -button ></ template ></ el-alert >
< section class = "surface-panel filter-panel" aria -label =" 角色筛选 " >
< div class = "filter-fields" >< el-input v-model = "filters.keyword" clearable placeholder="角色名称、编码或描述" aria-label="角色关键词"><template #prefix><Search :size="16" /></template></el-input><el-select v-if="!access.usesServerPermissions" v-model="filters.dataScope" clearable placeholder="全部数据范围" aria-label="数据范围"><el-option v-for="item in ['全部数据','本部门数据','本人数据','自定义']" :key="item" :label="item" :value="item" /></el-select><el-select v-model="filters.status" clearable placeholder="全部状态" aria-label="角色状态"><el-option label="启用" value="启用" /><el-option label="停用" value="停用" /></ el-select ></ div >
< div class = "filter-actions" >< el-button @click ="resetFilters" >< RotateCcw :size = "16" /> 重置 </ el-button >< el-button v-if = "canCreate" type="primary" @click="openCreate"><Plus :size="16" /> 新增角色 </ el-button ></ div >
</ section >
< section class = "surface-panel table-panel" >
< header class = "panel-heading" >< div >< h2 > 角色与授权 </ h2 >< p > 角色负责聚合菜单 、 操作和数据范围 , 内部用户可分配多个角色 。 </ p ></ div ></ header >
< el-table v-loading = "loading" :data="filteredRows" row-key="id" empty-text="没有符合条件的角色" >
2026-07-31 13:43:16 +08:00
< el-table-column label = "操作" width = "235" fixed = "left" >< template # default = "{ row }" >< div v-if = "access.usesServerPermissions && (canEdit || canGrant || access.can('platform:role:status') || access.can('platform:role:delete'))" class="table-actions"><el-button v-if="canEdit || canGrant" link type="primary" :disabled="rowActionId === row.id || row.builtIn" @click="openEdit(row as Role)"><Pencil :size="15" />配置</el-button><el-button v-if="access.can('platform:role:status')" link :type="row.status === '启用' ? 'danger' : 'success'" :loading="rowActionId === row.id" :disabled="row.builtIn" @click="toggleStatus(row as Role)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button><el-button v-if="access.can('platform:role:delete')" link type="danger" :disabled="row.builtIn || row.userCount > 0 || rowActionId === row.id" @click="removeRole(row as Role)"><Trash2 :size="15" />删除</el-button></div><span v-else-if="access.usesServerPermissions" class="secondary-text">仅查看</span><div v-else-if="access.can('system:role:grant')" class="table-actions"><el-button link type="primary" @click="openEdit(row as Role)"><Pencil :size="15" />配置</el-button><el-button link :type="row.status === '启用' ? 'danger' : 'success'" :disabled="row.builtIn" @click="toggleStatus(row as Role)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button><el-button link type="danger" :disabled="row.builtIn || row.userCount > 0" @click="removeRole(row as Role)"><Trash2 :size="15" /> 删除 </ el-button ></ div ></ template ></ el-table-column >
2026-07-30 17:59:43 +08:00
< el-table-column label = "角色" min -width =" 210 " >< template # default = "{ row }" >< div class = "role-cell" >< span >< ShieldCheck :size = "18" /></ span >< div >< strong >{{ row . name }}</ strong >< small >{{ row . code }}</ small ></ div ></ div ></ template ></ el-table-column >
< el-table-column prop = "description" label = "说明" min -width =" 240 " show -overflow-tooltip />
2026-07-31 13:43:16 +08:00
< el-table-column label = "授权范围" min -width =" 170 " >< template # default = "{ row }" >< div class = "primary-cell" >< strong >{{ permissionSummary ( row as Role ) }}</ strong >< span >{{ access . usesServerPermissions ? '平台 IAM 权限' : row . dataScope }}</ span ></ div ></ template ></ el-table-column >
2026-07-30 17:59:43 +08:00
< el-table-column label = "用户数" width = "86" align = "right" >< template # default = "{ row }" >{{ access . usesServerPermissions ? '—' : row . userCount }}</ template ></ el-table-column >
< el-table-column label = "类型" width = "95" >< template # default = "{ row }" >< el-tag : type = "row.builtIn ? 'warning' : 'info'" effect = "plain" size = "small" >{{ row . builtIn ? '系统内置' : '自定义' }}</ el-tag ></ template ></ el-table-column >
< el-table-column label = "状态" width = "88" >< template # default = "{ row }" >< StatusBadge :status = "row.status" /></ template ></ el-table-column >
</ el-table >
</ section >
< el-drawer v-model = "editorOpen" :title="form.id ? '配置角色' : '新增角色'" size="760px" class="business-drawer editor-drawer" :modal="false" :close-on-click-modal="false" >
< el-form label -position =" top " class = "drawer-form" >
2026-07-31 13:43:16 +08:00
< div class = "form-section" >< h3 > 基本信息 </ h3 >< div class = "form-grid two-cols" >< el-form-item label = "角色名称" required >< el-input v-model = "form.name" :disabled="access.usesServerPermissions && Boolean(form.id) && !canEdit" maxlength="64" /></el-form-item><el-form-item label="角色编码" required><el-input v-model="form.code" :disabled="Boolean(form.builtIn) || (access.usesServerPermissions && Boolean(form.id))" placeholder="如 OPS_MANAGER" maxlength="64" /></el-form-item><el-form-item v-if="!access.usesServerPermissions" label="数据范围"><el-select v-model="form.dataScope"><el-option v-for="item in dataScopes" :key="item" :label="item" :value="item" /></el-select></el-form-item><el-form-item v-if="!access.usesServerPermissions" label="状态"><el-radio-group v-model="form.status" :disabled="Boolean(form.builtIn)"><el-radio-button label="启用" value="启用" /><el-radio-button label="停用" value="停用" /></el-radio-group></el-form-item></div><el-form-item label="角色说明"><el-input v-model="form.description" :disabled="access.usesServerPermissions && Boolean(form.id) && !canEdit" type="textarea" :rows="2" maxlength="200" show -word-limit /></ el-form-item ></ div >
< div v-if = "!access.usesServerPermissions || canGrant" class="form-section permission-section"><div class="section-heading-row"><div><h3>菜单与操作权限</h3><p>保存时按后端契约全量替换当前角色权限;未勾选表示清空授权。</p></div><span>{{ form.permissionIds.length }} 项原有授权</span></div><el-tree ref="treeRef" class="permission-tree" :data="permissionTree" node-key="id" show-checkbox check-strictly default-expand-all :props="{ label: 'name', children: 'children' }" @check="syncPermissionIds"><template #default="{ data }"><div class="permission-node" >< span >{{ data . name }}</ span >< code >{{ data . code }}</ code ></ div ></ template ></ el-tree ></ div >
2026-07-30 17:59:43 +08:00
</ el-form >
2026-07-31 13:43:16 +08:00
< template # footer >< div class = "drawer-footer" >< el-button :disabled = "saving" @click ="editorOpen = false" > 取消 </ el -button >< el-button v-if = "access.usesServerPermissions ? (form.id ? canEdit || canGrant : canCreate) : access.can('system:role:grant')" type="primary" :loading="saving" @click="saveRole" > 保存角色 </ el -button ></ div ></ template >
2026-07-30 17:59:43 +08:00
</ el-drawer >
</ div >
</ template >