1216 lines
36 KiB
Vue
1216 lines
36 KiB
Vue
<template>
|
||
<ArtTableFullScreen>
|
||
<div class="role-page" id="table-full-screen">
|
||
<!-- 搜索栏 -->
|
||
<ArtSearchBar
|
||
v-model:filter="searchForm"
|
||
:items="searchFormItems"
|
||
:show-expand="false"
|
||
@reset="handleReset"
|
||
@search="handleSearch"
|
||
></ArtSearchBar>
|
||
|
||
<ElCard shadow="never" class="art-table-card">
|
||
<!-- 表格头部 -->
|
||
<ArtTableHeader
|
||
:columnList="columnOptions"
|
||
v-model:columns="columnChecks"
|
||
@refresh="handleRefresh"
|
||
>
|
||
<template #left>
|
||
<ElButton @click="showDialog('add')" v-permission="'role:add'">新增角色</ElButton>
|
||
</template>
|
||
</ArtTableHeader>
|
||
|
||
<!-- 表格 -->
|
||
<ArtTable
|
||
ref="tableRef"
|
||
row-key="ID"
|
||
:loading="loading"
|
||
:data="roleList"
|
||
:currentPage="pagination.page"
|
||
:pageSize="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:marginTop="10"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
>
|
||
<template #default>
|
||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||
</template>
|
||
</ArtTable>
|
||
|
||
<!-- 新增/编辑对话框 -->
|
||
<ElDialog
|
||
v-model="dialogVisible"
|
||
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||
width="30%"
|
||
>
|
||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||
<ElFormItem label="角色名称" prop="role_name">
|
||
<ElInput v-model="form.role_name" placeholder="请输入角色名称" />
|
||
</ElFormItem>
|
||
<ElFormItem label="角色描述" prop="role_desc">
|
||
<ElInput
|
||
v-model="form.role_desc"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入角色描述"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="角色类型" prop="role_type">
|
||
<ElSelect
|
||
v-model="form.role_type"
|
||
placeholder="请选择角色类型"
|
||
style="width: 100%"
|
||
:disabled="dialogType === 'edit'"
|
||
>
|
||
<ElOption
|
||
v-for="item in ROLE_TYPE_OPTIONS"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="状态">
|
||
<ElSwitch
|
||
v-model="form.status"
|
||
:active-value="CommonStatus.ENABLED"
|
||
:inactive-value="CommonStatus.DISABLED"
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
|
||
提交
|
||
</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 分配权限对话框 -->
|
||
<ElDialog v-model="permissionDialogVisible" width="800px">
|
||
<template #header>
|
||
<div class="dialog-header">
|
||
<span class="dialog-title">分配权限</span>
|
||
<div class="role-info">
|
||
<ElTag :type="currentRole.role_type === 1 ? 'primary' : 'success'" size="small">
|
||
{{ currentRole.role_type === 1 ? '平台角色' : '客户角色' }}
|
||
</ElTag>
|
||
<span class="role-name">{{ currentRole.role_name }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div class="permission-tree-transfer-container">
|
||
<!-- 左侧:可分配权限树 -->
|
||
<div class="transfer-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">可分配权限</span>
|
||
<ElInput
|
||
v-model="leftTreeFilter"
|
||
placeholder="搜索权限"
|
||
clearable
|
||
size="small"
|
||
style="width: 180px"
|
||
/>
|
||
</div>
|
||
<div class="panel-body">
|
||
<ElTree
|
||
ref="leftTreeRef"
|
||
:data="availablePermissions"
|
||
:props="{ children: 'children', label: 'label' }"
|
||
node-key="id"
|
||
show-checkbox
|
||
check-strictly
|
||
:filter-node-method="filterNode"
|
||
:default-expand-all="false"
|
||
:check-on-click-node="false"
|
||
@check="handleLeftTreeCheck"
|
||
class="permission-tree"
|
||
>
|
||
<template #default="{ node, data }">
|
||
<span style="display: flex; gap: 8px; align-items: center">
|
||
<span>{{ node.label }}</span>
|
||
<ElTag :type="data.perm_type === 1 ? 'info' : 'success'" size="small">
|
||
{{ data.perm_type === 1 ? '菜单' : '按钮' }}
|
||
</ElTag>
|
||
</span>
|
||
</template>
|
||
</ElTree>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中间:操作按钮 -->
|
||
<div class="transfer-buttons">
|
||
<ElButton
|
||
type="primary"
|
||
:icon="'ArrowRight'"
|
||
@click="addPermissions"
|
||
:disabled="getLeftCheckedKeys().length === 0"
|
||
>
|
||
添加
|
||
</ElButton>
|
||
</div>
|
||
|
||
<!-- 右侧:已分配权限树 -->
|
||
<div class="transfer-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">已分配权限</span>
|
||
<ElInput
|
||
v-model="rightTreeFilter"
|
||
placeholder="搜索权限"
|
||
clearable
|
||
size="small"
|
||
style="width: 180px"
|
||
/>
|
||
</div>
|
||
<div class="panel-body">
|
||
<ElTree
|
||
ref="rightTreeRef"
|
||
:data="assignedPermissions"
|
||
:props="{ children: 'children', label: 'label' }"
|
||
node-key="id"
|
||
:filter-node-method="filterNode"
|
||
:default-expand-all="false"
|
||
class="permission-tree permission-tree-with-action"
|
||
>
|
||
<template #default="{ node, data }">
|
||
<span class="tree-node-content">
|
||
<span class="tree-node-label">
|
||
<span>{{ node.label }}</span>
|
||
<ElTag :type="data.perm_type === 1 ? 'info' : 'success'" size="small">
|
||
{{ data.perm_type === 1 ? '菜单' : '按钮' }}
|
||
</ElTag>
|
||
</span>
|
||
<ElButton
|
||
type="danger"
|
||
size="small"
|
||
link
|
||
@click="removeSinglePermission(data)"
|
||
>
|
||
移除
|
||
</ElButton>
|
||
</span>
|
||
</template>
|
||
</ElTree>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="permissionDialogVisible = false">关闭</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
</ElCard>
|
||
</div>
|
||
</ArtTableFullScreen>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { h } from 'vue'
|
||
import { RoleService, PermissionService } from '@/api/modules'
|
||
import {
|
||
ElMessage,
|
||
ElMessageBox,
|
||
ElTag,
|
||
ElTree,
|
||
ElSwitch,
|
||
ElButton,
|
||
ElInput,
|
||
ElSelect,
|
||
ElOption
|
||
} from 'element-plus'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import type { PlatformRole, PermissionTreeNode } from '@/types/api'
|
||
import type { SearchFormItem } from '@/types'
|
||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||
import { useAuth } from '@/composables/useAuth'
|
||
import { formatDateTime } from '@/utils/business/format'
|
||
import {
|
||
CommonStatus,
|
||
getStatusText,
|
||
ROLE_TYPE_OPTIONS,
|
||
getRoleTypeLabel
|
||
} from '@/config/constants'
|
||
|
||
defineOptions({ name: 'Role' })
|
||
|
||
const { hasAuth } = useAuth()
|
||
|
||
const dialogVisible = ref(false)
|
||
const permissionDialogVisible = ref(false)
|
||
const loading = ref(false)
|
||
const submitLoading = ref(false)
|
||
const tableRef = ref()
|
||
const leftTreeRef = ref()
|
||
const rightTreeRef = ref()
|
||
const currentRoleId = ref<number>(0)
|
||
const currentRole = ref<{ role_name: string; role_type: number }>({
|
||
role_name: '',
|
||
role_type: 1
|
||
})
|
||
const selectedPermissions = ref<number[]>([])
|
||
const originalPermissions = ref<number[]>([]) // 保存原始权限,用于对比
|
||
const availablePermissions = ref<any[]>([]) // 左侧可分配权限树数据
|
||
const assignedPermissions = ref<any[]>([]) // 右侧已分配权限树数据
|
||
const allPermissionsMap = ref<Map<number, any>>(new Map()) // 所有权限的映射表
|
||
const leftTreeFilter = ref('') // 左侧树搜索关键字
|
||
const rightTreeFilter = ref('') // 右侧树搜索关键字
|
||
const isHandlingCheck = ref(false) // 标志位:是否正在处理勾选事件
|
||
|
||
// 搜索表单初始值
|
||
const initialSearchState = {
|
||
role_name: '',
|
||
role_type: undefined as number | undefined,
|
||
status: undefined as number | undefined
|
||
}
|
||
|
||
// 搜索表单
|
||
const searchForm = reactive({ ...initialSearchState })
|
||
|
||
// 搜索表单配置
|
||
const searchFormItems: SearchFormItem[] = [
|
||
{
|
||
label: '角色名称',
|
||
prop: 'role_name',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入角色名称'
|
||
}
|
||
},
|
||
{
|
||
label: '角色类型',
|
||
prop: 'role_type',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请选择角色类型'
|
||
},
|
||
options: () => ROLE_TYPE_OPTIONS
|
||
},
|
||
{
|
||
label: '状态',
|
||
prop: 'status',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请选择状态'
|
||
},
|
||
options: () => [
|
||
{ label: '启用', value: 1 },
|
||
{ label: '禁用', value: 0 }
|
||
]
|
||
}
|
||
]
|
||
|
||
// 分页
|
||
const pagination = reactive({
|
||
page: 1,
|
||
pageSize: 20,
|
||
total: 0
|
||
})
|
||
|
||
// 列配置
|
||
const columnOptions = [
|
||
{ label: '角色名称', prop: 'role_name' },
|
||
{ label: '角色描述', prop: 'role_desc' },
|
||
{ label: '角色类型', prop: 'role_type' },
|
||
{ label: '状态', prop: 'status' },
|
||
{ label: '创建时间', prop: 'CreatedAt' }
|
||
]
|
||
|
||
const formRef = ref<FormInstance>()
|
||
|
||
const rules = reactive<FormRules>({
|
||
role_name: [
|
||
{ required: true, message: '请输入角色名称', trigger: 'blur' },
|
||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||
],
|
||
role_desc: [{ max: 200, message: '描述不能超过200个字符', trigger: 'blur' }],
|
||
role_type: [{ required: true, message: '请选择角色类型', trigger: 'change' }]
|
||
})
|
||
|
||
const form = reactive<any>({
|
||
id: 0,
|
||
role_name: '',
|
||
role_desc: '',
|
||
role_type: 1,
|
||
status: CommonStatus.ENABLED
|
||
})
|
||
|
||
const roleList = ref<PlatformRole[]>([])
|
||
|
||
// 动态列配置
|
||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||
{
|
||
prop: 'role_name',
|
||
label: '角色名称',
|
||
width: 180
|
||
},
|
||
{
|
||
prop: 'role_type',
|
||
label: '角色类型',
|
||
width: 100,
|
||
formatter: (row: any) => {
|
||
return h(ElTag, { type: row.role_type === 1 ? 'primary' : 'success' }, () =>
|
||
getRoleTypeLabel(row.role_type)
|
||
)
|
||
}
|
||
},
|
||
{
|
||
prop: 'status',
|
||
label: '状态',
|
||
width: 100,
|
||
formatter: (row: any) => {
|
||
return h(ElSwitch, {
|
||
modelValue: row.status,
|
||
activeValue: CommonStatus.ENABLED,
|
||
inactiveValue: CommonStatus.DISABLED,
|
||
activeText: getStatusText(CommonStatus.ENABLED),
|
||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||
inlinePrompt: true,
|
||
disabled: !hasAuth('role:update_status'),
|
||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||
handleStatusChange(row, val as number)
|
||
})
|
||
}
|
||
},
|
||
{
|
||
prop: 'role_desc',
|
||
label: '角色描述'
|
||
},
|
||
{
|
||
prop: 'CreatedAt',
|
||
label: '创建时间',
|
||
width: 180,
|
||
formatter: (row: any) => formatDateTime(row.CreatedAt)
|
||
},
|
||
{
|
||
prop: 'actions',
|
||
label: '操作',
|
||
width: 220,
|
||
fixed: 'right',
|
||
formatter: (row: any) => {
|
||
const buttons: any[] = []
|
||
|
||
if (hasAuth('role:permission')) {
|
||
buttons.push(
|
||
h(
|
||
ElButton,
|
||
{
|
||
type: 'primary',
|
||
link: true,
|
||
onClick: () => showPermissionDialog(row)
|
||
},
|
||
() => '分配权限'
|
||
)
|
||
)
|
||
}
|
||
|
||
if (hasAuth('role:edit')) {
|
||
buttons.push(
|
||
h(
|
||
ElButton,
|
||
{
|
||
type: 'primary',
|
||
link: true,
|
||
onClick: () => showDialog('edit', row)
|
||
},
|
||
() => '编辑'
|
||
)
|
||
)
|
||
}
|
||
|
||
if (hasAuth('role:delete')) {
|
||
buttons.push(
|
||
h(
|
||
ElButton,
|
||
{
|
||
type: 'danger',
|
||
link: true,
|
||
onClick: () => deleteRole(row)
|
||
},
|
||
() => '删除'
|
||
)
|
||
)
|
||
}
|
||
|
||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||
}
|
||
}
|
||
])
|
||
|
||
onMounted(() => {
|
||
getTableData()
|
||
})
|
||
|
||
// 监听搜索关键字变化
|
||
watch(leftTreeFilter, (val) => {
|
||
leftTreeRef.value?.filter(val)
|
||
})
|
||
|
||
watch(rightTreeFilter, (val) => {
|
||
rightTreeRef.value?.filter(val)
|
||
})
|
||
|
||
// 树节点过滤方法
|
||
const filterNode = (value: string, data: any) => {
|
||
if (!value) return true
|
||
return data.label.toLowerCase().includes(value.toLowerCase())
|
||
}
|
||
|
||
// 更新父节点的勾选状态(计算应该半选的父节点)
|
||
const updateParentCheckStatus = (checkedKeys: number[]) => {
|
||
const checkedSet = new Set(checkedKeys)
|
||
const parentIdsToHalfCheck = new Set<number>()
|
||
|
||
// 递归检查节点的子节点勾选状态
|
||
const checkNode = (node: any): { allChecked: boolean; someChecked: boolean } => {
|
||
if (!node.children || node.children.length === 0) {
|
||
// 叶子节点
|
||
return {
|
||
allChecked: checkedSet.has(node.id),
|
||
someChecked: checkedSet.has(node.id)
|
||
}
|
||
}
|
||
|
||
// 有子节点的节点
|
||
let allChecked = true
|
||
let someChecked = false
|
||
|
||
for (const child of node.children) {
|
||
const childStatus = checkNode(child)
|
||
if (!childStatus.allChecked) {
|
||
allChecked = false
|
||
}
|
||
if (childStatus.someChecked) {
|
||
someChecked = true
|
||
}
|
||
}
|
||
|
||
// 当前节点被勾选
|
||
const currentNodeChecked = checkedSet.has(node.id)
|
||
|
||
// 判断父节点应该显示的状态:
|
||
// 1. 如果当前节点被勾选,但不是所有子节点都被勾选 -> 半选
|
||
// 2. 如果有子节点被勾选,但当前节点未被勾选 -> 半选
|
||
// 3. 如果所有子节点都被勾选且当前节点也被勾选 -> 全选
|
||
if (currentNodeChecked) {
|
||
// 当前节点被勾选
|
||
if (!allChecked) {
|
||
// 但不是所有子节点都被勾选 -> 半选
|
||
parentIdsToHalfCheck.add(node.id)
|
||
}
|
||
// 如果 allChecked,则为全选,不需要设置半选
|
||
} else if (someChecked) {
|
||
// 当前节点未勾选,但有子节点被勾选 -> 半选
|
||
parentIdsToHalfCheck.add(node.id)
|
||
}
|
||
|
||
return {
|
||
allChecked: allChecked && currentNodeChecked,
|
||
someChecked: someChecked || currentNodeChecked
|
||
}
|
||
}
|
||
|
||
// 检查所有顶层节点
|
||
availablePermissions.value.forEach((node) => checkNode(node))
|
||
|
||
return Array.from(parentIdsToHalfCheck)
|
||
}
|
||
|
||
// 判断节点是否为一级菜单(没有父节点或parent_id为0/null)
|
||
const isFirstLevelMenu = (nodeId: number): boolean => {
|
||
const node = allPermissionsMap.value.get(nodeId)
|
||
if (!node) return false
|
||
return !node.parent_id || node.parent_id === 0
|
||
}
|
||
|
||
// 获取节点的所有子节点ID(递归)
|
||
const getAllChildrenIds = (nodeId: number): number[] => {
|
||
const childrenIds: number[] = []
|
||
|
||
const collectChildren = (id: number) => {
|
||
const node = allPermissionsMap.value.get(id)
|
||
if (!node || !node.children || node.children.length === 0) return
|
||
|
||
node.children.forEach((child: any) => {
|
||
childrenIds.push(child.id)
|
||
collectChildren(child.id)
|
||
})
|
||
}
|
||
|
||
collectChildren(nodeId)
|
||
return childrenIds
|
||
}
|
||
|
||
// 上一次勾选的keys,用于判断是勾选还是取消勾选
|
||
const previousCheckedKeys = ref<number[]>([])
|
||
|
||
// 处理左侧树的勾选事件
|
||
const handleLeftTreeCheck = (data: any, checked: any) => {
|
||
if (isHandlingCheck.value) return
|
||
|
||
isHandlingCheck.value = true
|
||
|
||
try {
|
||
// 获取当前勾选的keys
|
||
let currentChecked = checked.checkedKeys as number[]
|
||
|
||
// 判断是勾选还是取消勾选(通过对比当前和之前的keys)
|
||
const isChecking =
|
||
currentChecked.includes(data.id) && !previousCheckedKeys.value.includes(data.id)
|
||
const isUnchecking =
|
||
!currentChecked.includes(data.id) && previousCheckedKeys.value.includes(data.id)
|
||
|
||
if (isChecking) {
|
||
// 正在勾选节点
|
||
// 判断当前节点是否为一级菜单
|
||
if (isFirstLevelMenu(data.id)) {
|
||
// 一级菜单:自动勾选所有子节点(包括所有子菜单和按钮)
|
||
const childrenIds = getAllChildrenIds(data.id)
|
||
const newCheckedKeys = [...new Set([...currentChecked, ...childrenIds])]
|
||
|
||
// 设置新的勾选状态
|
||
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
|
||
currentChecked = newCheckedKeys
|
||
} else {
|
||
// 二级及以下菜单/按钮:只勾选自己,不勾选子节点
|
||
const childrenIds = getAllChildrenIds(data.id)
|
||
// 从当前勾选中移除所有子节点(如果有的话)
|
||
const newCheckedKeys = currentChecked.filter((id) => !childrenIds.includes(id))
|
||
|
||
// 设置新的勾选状态
|
||
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
|
||
currentChecked = newCheckedKeys
|
||
}
|
||
} else if (isUnchecking) {
|
||
// 正在取消勾选节点
|
||
// 判断当前节点是否为一级菜单
|
||
if (isFirstLevelMenu(data.id)) {
|
||
// 一级菜单:自动取消所有子节点(包括所有子菜单和按钮)
|
||
const childrenIds = getAllChildrenIds(data.id)
|
||
const newCheckedKeys = currentChecked.filter((id) => !childrenIds.includes(id))
|
||
|
||
// 设置新的勾选状态
|
||
leftTreeRef.value?.setCheckedKeys(newCheckedKeys, false)
|
||
currentChecked = newCheckedKeys
|
||
}
|
||
// 二级及以下菜单/按钮:直接取消勾选,不需要额外处理
|
||
}
|
||
|
||
// 更新上一次勾选的keys
|
||
previousCheckedKeys.value = [...currentChecked]
|
||
|
||
// 计算应该半选的父节点
|
||
const halfCheckedIds = updateParentCheckStatus(currentChecked)
|
||
|
||
// 使用内部API直接操作树的半选状态
|
||
nextTick(() => {
|
||
if (leftTreeRef.value && leftTreeRef.value.store) {
|
||
// 清除所有半选状态
|
||
Object.values(leftTreeRef.value.store.nodesMap).forEach((node: any) => {
|
||
node.indeterminate = false
|
||
})
|
||
|
||
// 设置应该半选的节点
|
||
halfCheckedIds.forEach((id: number) => {
|
||
const node = leftTreeRef.value.store.nodesMap[id]
|
||
if (node) {
|
||
node.indeterminate = true
|
||
}
|
||
})
|
||
}
|
||
})
|
||
} finally {
|
||
isHandlingCheck.value = false
|
||
}
|
||
}
|
||
|
||
// 构建权限树数据结构(只包含启用的权限)
|
||
const buildTreeData = (treeNodes: PermissionTreeNode[]): any[] => {
|
||
return treeNodes
|
||
.filter((node) => node.status === 1) // 只显示启用的权限
|
||
.map((node) => ({
|
||
id: node.id,
|
||
label: node.perm_name,
|
||
perm_type: node.perm_type,
|
||
status: node.status ?? 1,
|
||
children:
|
||
node.children && node.children.length > 0 ? buildTreeData(node.children) : undefined
|
||
}))
|
||
}
|
||
|
||
// 构建权限映射表(包括所有节点和子节点)
|
||
const buildPermissionMap = (treeNodes: PermissionTreeNode[], map: Map<number, any>) => {
|
||
treeNodes.forEach((node) => {
|
||
map.set(node.id, node)
|
||
if (node.children && node.children.length > 0) {
|
||
buildPermissionMap(node.children, map)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 根据权限ID列表过滤树节点(包含指定ID的节点)
|
||
const filterTreeByIds = (treeNodes: any[], ids: number[]): any[] => {
|
||
const result: any[] = []
|
||
|
||
treeNodes.forEach((node) => {
|
||
if (ids.includes(node.id)) {
|
||
const newNode = { ...node }
|
||
if (node.children && node.children.length > 0) {
|
||
newNode.children = filterTreeByIds(node.children, ids)
|
||
}
|
||
result.push(newNode)
|
||
} else if (node.children && node.children.length > 0) {
|
||
const filteredChildren = filterTreeByIds(node.children, ids)
|
||
if (filteredChildren.length > 0) {
|
||
result.push({
|
||
...node,
|
||
children: filteredChildren
|
||
})
|
||
}
|
||
}
|
||
})
|
||
|
||
return result
|
||
}
|
||
|
||
// 根据权限ID列表设置树节点的禁用状态
|
||
// 同时禁用状态为禁用(status=0)的权限
|
||
const setTreeNodesDisabled = (treeNodes: any[], idsToDisable: number[]): any[] => {
|
||
return treeNodes.map((node) => {
|
||
const newNode = {
|
||
...node,
|
||
// 如果已分配或状态为禁用,则禁用该节点
|
||
disabled: idsToDisable.includes(node.id) || node.status === 0
|
||
}
|
||
|
||
if (node.children && node.children.length > 0) {
|
||
newNode.children = setTreeNodesDisabled(node.children, idsToDisable)
|
||
}
|
||
|
||
return newNode
|
||
})
|
||
}
|
||
|
||
// 保存原始权限树数据
|
||
const originalPermissionTree = ref<PermissionTreeNode[]>([])
|
||
|
||
// 加载所有权限树
|
||
const loadAllPermissions = async () => {
|
||
try {
|
||
const res = await PermissionService.getPermissionTree()
|
||
if (res.code === 0) {
|
||
const treeData = res.data || []
|
||
|
||
// 保存原始树数据
|
||
originalPermissionTree.value = treeData
|
||
|
||
// 构建权限映射表
|
||
const map = new Map()
|
||
buildPermissionMap(treeData, map)
|
||
allPermissionsMap.value = map
|
||
|
||
// 构建树形数据
|
||
const fullTreeData = buildTreeData(treeData)
|
||
|
||
// 左侧显示所有权限,但已分配的权限设置为禁用
|
||
availablePermissions.value = setTreeNodesDisabled(
|
||
JSON.parse(JSON.stringify(fullTreeData)),
|
||
selectedPermissions.value
|
||
)
|
||
|
||
// 右侧显示已分配的权限
|
||
if (selectedPermissions.value.length > 0) {
|
||
assignedPermissions.value = filterTreeByIds(fullTreeData, selectedPermissions.value)
|
||
} else {
|
||
assignedPermissions.value = []
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取权限树失败:', error)
|
||
}
|
||
}
|
||
|
||
// 获取左侧树完全勾选的节点(不包括半选节点)
|
||
const getLeftCheckedKeys = (): number[] => {
|
||
if (!leftTreeRef.value) return []
|
||
return leftTreeRef.value.getCheckedKeys(false)
|
||
}
|
||
|
||
// 获取节点的所有父节点ID
|
||
const getParentNodeIds = (nodeId: number): number[] => {
|
||
const parentIds: number[] = []
|
||
|
||
// 在原始权限树中递归查找父节点
|
||
const findInTree = (
|
||
treeNodes: PermissionTreeNode[],
|
||
targetId: number,
|
||
parentId?: number
|
||
): number | null => {
|
||
for (const node of treeNodes) {
|
||
if (node.id === targetId) {
|
||
return parentId || null
|
||
}
|
||
if (node.children && node.children.length > 0) {
|
||
const found = findInTree(node.children, targetId, node.id)
|
||
if (found !== null) return found
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 递归向上查找所有父节点
|
||
const findAllParents = (currentId: number) => {
|
||
const parentId = findInTree(originalPermissionTree.value, currentId)
|
||
if (parentId) {
|
||
parentIds.push(parentId)
|
||
findAllParents(parentId)
|
||
}
|
||
}
|
||
|
||
findAllParents(nodeId)
|
||
return parentIds
|
||
}
|
||
|
||
// 获取左侧树勾选的节点(包括必要的父节点,用于提交服务器)
|
||
const getLeftCheckedKeysWithHalf = (): number[] => {
|
||
if (!leftTreeRef.value) return []
|
||
const checkedKeys = leftTreeRef.value.getCheckedKeys(false)
|
||
const allIds = new Set<number>(checkedKeys)
|
||
|
||
// 为每个勾选的节点找到所有父节点
|
||
checkedKeys.forEach((key: number) => {
|
||
const parents = getParentNodeIds(key)
|
||
parents.forEach((parentId) => allIds.add(parentId))
|
||
})
|
||
|
||
return Array.from(allIds)
|
||
}
|
||
|
||
// 添加权限
|
||
const addPermissions = async () => {
|
||
const checkedKeys = getLeftCheckedKeys()
|
||
if (checkedKeys.length === 0) return
|
||
|
||
try {
|
||
// 提交到服务器时包含勾选和半选节点
|
||
const keysToSubmit = getLeftCheckedKeysWithHalf()
|
||
await RoleService.assignPermissions(currentRoleId.value, keysToSubmit)
|
||
|
||
// 本地只记录完全勾选的节点
|
||
selectedPermissions.value = [...new Set([...selectedPermissions.value, ...checkedKeys])]
|
||
|
||
// 重新构建左右两侧树
|
||
const fullTreeData = buildTreeData(originalPermissionTree.value)
|
||
|
||
// 左侧显示所有权限,但已分配的权限设置为禁用
|
||
availablePermissions.value = setTreeNodesDisabled(
|
||
JSON.parse(JSON.stringify(fullTreeData)),
|
||
selectedPermissions.value
|
||
)
|
||
|
||
// 右侧显示已分配的权限
|
||
assignedPermissions.value = filterTreeByIds(fullTreeData, selectedPermissions.value)
|
||
|
||
// 清空左侧树的勾选状态
|
||
leftTreeRef.value?.setCheckedKeys([], false)
|
||
previousCheckedKeys.value = []
|
||
|
||
ElMessage.success('权限添加成功')
|
||
} catch (error) {
|
||
console.error('添加权限失败:', error)
|
||
ElMessage.error('权限添加失败')
|
||
}
|
||
}
|
||
|
||
// 检查节点是否有子节点(子菜单或按钮)
|
||
const hasChildren = (data: any): boolean => {
|
||
return data.children && data.children.length > 0
|
||
}
|
||
|
||
// 移除单个权限
|
||
const removeSinglePermission = async (data: any) => {
|
||
// 检查是否有子菜单或按钮
|
||
if (hasChildren(data)) {
|
||
ElMessage.warning('该权限下还有子菜单或按钮,请先移除子项后再移除此权限')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 保存右侧树的展开节点
|
||
const expandedKeys = rightTreeRef.value?.store?.nodesMap
|
||
? Object.keys(rightTreeRef.value.store.nodesMap)
|
||
.filter((key) => rightTreeRef.value.store.nodesMap[key].expanded)
|
||
.map((key) => Number(key))
|
||
: []
|
||
|
||
await RoleService.removePermission(currentRoleId.value, data.id)
|
||
|
||
// 更新已选权限列表
|
||
selectedPermissions.value = selectedPermissions.value.filter((id) => id !== data.id)
|
||
|
||
// 重新构建左右两侧树
|
||
const fullTreeData = buildTreeData(originalPermissionTree.value)
|
||
|
||
// 左侧显示所有权限,但已分配的权限设置为禁用
|
||
availablePermissions.value = setTreeNodesDisabled(
|
||
JSON.parse(JSON.stringify(fullTreeData)),
|
||
selectedPermissions.value
|
||
)
|
||
|
||
// 右侧显示已分配的权限
|
||
if (selectedPermissions.value.length > 0) {
|
||
assignedPermissions.value = filterTreeByIds(fullTreeData, selectedPermissions.value)
|
||
} else {
|
||
assignedPermissions.value = []
|
||
}
|
||
|
||
// 等待DOM更新后恢复展开状态
|
||
await nextTick()
|
||
if (rightTreeRef.value && expandedKeys.length > 0) {
|
||
expandedKeys.forEach((key) => {
|
||
const node = rightTreeRef.value.store.nodesMap[key]
|
||
if (node && !node.isLeaf) {
|
||
node.expanded = true
|
||
}
|
||
})
|
||
}
|
||
|
||
ElMessage.success('权限移除成功')
|
||
} catch (error) {
|
||
console.error('移除权限失败:', error)
|
||
ElMessage.error('权限移除失败')
|
||
}
|
||
}
|
||
|
||
// 显示分配权限对话框
|
||
const showPermissionDialog = async (row: PlatformRole) => {
|
||
currentRoleId.value = row.ID
|
||
currentRole.value = {
|
||
role_name: row.role_name,
|
||
role_type: row.role_type
|
||
}
|
||
selectedPermissions.value = []
|
||
originalPermissions.value = []
|
||
previousCheckedKeys.value = []
|
||
leftTreeFilter.value = ''
|
||
rightTreeFilter.value = ''
|
||
|
||
try {
|
||
// 加载当前角色的权限
|
||
const res = await RoleService.getRolePermissions(row.ID)
|
||
|
||
if (res.code === 0 || Array.isArray(res.data)) {
|
||
// 提取权限ID数组
|
||
const permissions = res.data || []
|
||
|
||
if (Array.isArray(permissions) && permissions.length > 0) {
|
||
// 如果返回的是权限对象数组,提取ID或id字段
|
||
if (typeof permissions[0] === 'object') {
|
||
// 优先使用ID字段,如果没有则使用id字段
|
||
if ('ID' in permissions[0]) {
|
||
selectedPermissions.value = permissions.map((perm: any) => perm.ID)
|
||
} else if ('id' in permissions[0]) {
|
||
selectedPermissions.value = permissions.map((perm: any) => perm.id)
|
||
}
|
||
} else {
|
||
// 如果返回的是ID数组
|
||
selectedPermissions.value = permissions
|
||
}
|
||
}
|
||
|
||
// 保存原始权限,用于后续对比
|
||
originalPermissions.value = [...selectedPermissions.value]
|
||
}
|
||
|
||
// 每次打开对话框时重新加载最新的权限列表
|
||
await loadAllPermissions()
|
||
|
||
// 数据加载完成后再打开对话框
|
||
permissionDialogVisible.value = true
|
||
} catch (error) {
|
||
console.error('获取角色权限失败:', error)
|
||
}
|
||
}
|
||
|
||
// 获取角色列表
|
||
const getTableData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const params = {
|
||
page: pagination.page,
|
||
pageSize: pagination.pageSize,
|
||
role_name: searchForm.role_name || undefined,
|
||
role_type: searchForm.role_type,
|
||
status: searchForm.status
|
||
}
|
||
const res = await RoleService.getRoles(params)
|
||
if (res.code === 0) {
|
||
roleList.value = res.data.items || []
|
||
pagination.total = res.data.total || 0
|
||
}
|
||
} catch (error) {
|
||
console.error(error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 重置搜索
|
||
const handleReset = () => {
|
||
Object.assign(searchForm, { ...initialSearchState })
|
||
pagination.page = 1
|
||
getTableData()
|
||
}
|
||
|
||
// 搜索
|
||
const handleSearch = () => {
|
||
pagination.page = 1
|
||
getTableData()
|
||
}
|
||
|
||
// 刷新表格
|
||
const handleRefresh = () => {
|
||
getTableData()
|
||
}
|
||
|
||
// 处理表格分页变化
|
||
const handleSizeChange = (newPageSize: number) => {
|
||
pagination.pageSize = newPageSize
|
||
getTableData()
|
||
}
|
||
|
||
const handleCurrentChange = (newCurrentPage: number) => {
|
||
pagination.page = newCurrentPage
|
||
getTableData()
|
||
}
|
||
|
||
const dialogType = ref('add')
|
||
|
||
// 显示新增/编辑对话框
|
||
const showDialog = (type: string, row?: any) => {
|
||
dialogVisible.value = true
|
||
dialogType.value = type
|
||
|
||
if (type === 'edit' && row) {
|
||
form.id = row.ID
|
||
form.role_name = row.role_name
|
||
form.role_desc = row.role_desc
|
||
form.role_type = row.role_type
|
||
form.status = row.status
|
||
} else {
|
||
form.id = 0
|
||
form.role_name = ''
|
||
form.role_desc = ''
|
||
form.role_type = 1
|
||
form.status = CommonStatus.ENABLED
|
||
}
|
||
}
|
||
|
||
// 删除角色
|
||
const deleteRole = (row: any) => {
|
||
ElMessageBox.confirm(`确定删除角色 ${row.role_name} 吗?`, '删除确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'error'
|
||
})
|
||
.then(async () => {
|
||
try {
|
||
await RoleService.deleteRole(row.ID)
|
||
ElMessage.success('删除成功')
|
||
await getTableData()
|
||
} catch (error) {
|
||
console.error(error)
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 用户取消删除
|
||
})
|
||
}
|
||
|
||
// 提交表单
|
||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||
if (!formEl) return
|
||
|
||
await formEl.validate(async (valid) => {
|
||
if (valid) {
|
||
submitLoading.value = true
|
||
try {
|
||
if (dialogType.value === 'add') {
|
||
const data = {
|
||
role_name: form.role_name,
|
||
role_desc: form.role_desc,
|
||
role_type: form.role_type,
|
||
status: form.status
|
||
}
|
||
await RoleService.createRole(data)
|
||
ElMessage.success('新增成功')
|
||
} else {
|
||
// 更新角色时只发送允许的字段
|
||
const data = {
|
||
role_name: form.role_name,
|
||
role_desc: form.role_desc,
|
||
role_type: form.role_type,
|
||
status: form.status
|
||
}
|
||
await RoleService.updateRole(form.id, data)
|
||
ElMessage.success('修改成功')
|
||
}
|
||
|
||
dialogVisible.value = false
|
||
formEl.resetFields()
|
||
await getTableData()
|
||
} catch (error) {
|
||
console.error(error)
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 状态切换
|
||
const handleStatusChange = async (row: any, newStatus: number) => {
|
||
const oldStatus = row.status
|
||
// 先更新UI
|
||
row.status = newStatus
|
||
try {
|
||
await RoleService.updateRoleStatus(row.ID, newStatus as 0 | 1)
|
||
ElMessage.success('状态切换成功')
|
||
} catch (error) {
|
||
// 切换失败,恢复原状态
|
||
row.status = oldStatus
|
||
console.error(error)
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.dialog-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
|
||
.dialog-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.role-info {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
|
||
.role-name {
|
||
font-size: 14px;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
}
|
||
}
|
||
|
||
.permission-tree-transfer-container {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: stretch;
|
||
justify-content: space-between;
|
||
min-height: 500px;
|
||
padding: 20px 0;
|
||
|
||
.transfer-panel {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
max-width: 340px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--el-border-color);
|
||
border-radius: 4px;
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
background: var(--el-fill-color-light);
|
||
border-bottom: 1px solid var(--el-border-color);
|
||
|
||
.panel-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
}
|
||
|
||
.panel-body {
|
||
flex: 1;
|
||
padding: 12px;
|
||
overflow-y: auto;
|
||
|
||
.permission-tree {
|
||
:deep(.el-tree-node) {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
:deep(.el-tree-node__content) {
|
||
height: 36px;
|
||
padding-right: 8px;
|
||
line-height: 36px;
|
||
}
|
||
|
||
:deep(.el-tree-node__label) {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.permission-tree-with-action {
|
||
:deep(.el-tree-node__content) {
|
||
height: auto;
|
||
min-height: 36px;
|
||
padding: 4px 8px 4px 0;
|
||
}
|
||
|
||
.tree-node-content {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
|
||
.tree-node-label {
|
||
display: flex;
|
||
flex: 1;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.el-button {
|
||
flex-shrink: 0;
|
||
margin-left: auto;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.transfer-buttons {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 10px;
|
||
|
||
.el-button {
|
||
width: 100px;
|
||
}
|
||
}
|
||
}
|
||
</style>
|