447 lines
14 KiB
Vue
447 lines
14 KiB
Vue
<template>
|
||
<div class="page-content">
|
||
<ElRow>
|
||
<ElCol :xs="24" :sm="12" :lg="6">
|
||
<ElInput placeholder="角色名称"></ElInput>
|
||
</ElCol>
|
||
<div style="width: 12px"></div>
|
||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||
<ElButton v-ripple>搜索</ElButton>
|
||
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@role_add')">新增角色</ElButton>
|
||
</ElCol>
|
||
</ElRow>
|
||
|
||
<ArtTable
|
||
ref="tableRef"
|
||
row-key="id"
|
||
:loading="loading"
|
||
:data="roleList"
|
||
:currentPage="pagination.currentPage"
|
||
:pageSize="pagination.pageSize"
|
||
:total="pagination.total"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
>
|
||
<template #default>
|
||
<ElTableColumn label="角色名称" prop="name" />
|
||
<ElTableColumn label="描述" prop="desc" />
|
||
<ElTableColumn label="启用" prop="enable">
|
||
<template #default="scope">
|
||
<ElTag :type="scope.row.status == 1 ? 'primary' : 'info'">
|
||
{{ scope.row.status == 1 ? '启用' : '禁用' }}
|
||
</ElTag>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="创建时间" prop="created_at"></ElTableColumn>
|
||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||
<template #default="scope">
|
||
<ElRow>
|
||
<el-button
|
||
link
|
||
@click="showPermissionDialog(scope.row)"
|
||
v-if="hasAuth('system@role_perm')"
|
||
>
|
||
菜单权限
|
||
</el-button>
|
||
<el-button
|
||
type="success"
|
||
link
|
||
@click="showDialog('edit', scope.row)"
|
||
v-if="hasAuth('system@role_update')"
|
||
>
|
||
编辑
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
@click="deleteRole(scope.row)"
|
||
v-if="hasAuth('system@role_destroy')"
|
||
>删除
|
||
</el-button>
|
||
</ElRow>
|
||
</template>
|
||
</ElTableColumn>
|
||
</template>
|
||
</ArtTable>
|
||
|
||
<ElDialog
|
||
v-model="dialogVisible"
|
||
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||
width="30%"
|
||
align-center
|
||
>
|
||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||
<ElFormItem label="角色名称" prop="name">
|
||
<ElInput v-model="form.name" />
|
||
</ElFormItem>
|
||
<ElFormItem label="描述" prop="desc">
|
||
<ElInput v-model="form.desc" type="textarea" :rows="3" />
|
||
</ElFormItem>
|
||
<ElFormItem label="排序" prop="sort">
|
||
<ElInput type="number" v-model="form.sort" />
|
||
</ElFormItem>
|
||
<ElFormItem label="启用">
|
||
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<ElDialog
|
||
v-model="permissionDialog"
|
||
title="菜单权限"
|
||
width="520px"
|
||
align-center
|
||
class="el-dialog-border"
|
||
>
|
||
<ElScrollbar height="70vh">
|
||
<ElTree
|
||
ref="treeRef"
|
||
:data="processedMenuList"
|
||
show-checkbox
|
||
node-key="id"
|
||
:default-expand-all="isExpandAll"
|
||
:props="defaultProps"
|
||
@check="handleTreeCheck"
|
||
>
|
||
<template #default="{ data }">
|
||
<div style="display: flex; align-items: center">
|
||
<span v-if="data.isAuth">
|
||
{{ data.label }}
|
||
</span>
|
||
<span v-else>{{ defaultProps.label(data) }}</span>
|
||
</div>
|
||
</template>
|
||
</ElTree>
|
||
</ElScrollbar>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
|
||
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
|
||
isSelectAll ? '取消全选' : '全部选择'
|
||
}}</ElButton>
|
||
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import { formatMenuTitle } from '@/router/utils/utils'
|
||
import { systemService } from '@/api/systemApi'
|
||
import { useAuth } from '@/composables/useAuth'
|
||
const { hasAuth } = useAuth()
|
||
// import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
|
||
// import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
|
||
const loading = ref(false)
|
||
defineOptions({ name: 'Role' })
|
||
|
||
const dialogVisible = ref(false)
|
||
const permissionDialog = ref(false)
|
||
// 权限树数据(来自接口 getPermissionMenuList)
|
||
const permissionMenuList = ref<any[]>([])
|
||
const treeRef = ref()
|
||
const isExpandAll = ref(true)
|
||
const isSelectAll = ref(false)
|
||
const pagination = reactive({
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
total: 0
|
||
})
|
||
const formRef = ref<FormInstance>()
|
||
|
||
const rules = reactive<FormRules>({
|
||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||
des: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
|
||
})
|
||
|
||
const form = reactive<any>({
|
||
id: '',
|
||
name: '',
|
||
desc: '',
|
||
sort: 0,
|
||
status: 1
|
||
})
|
||
|
||
const roleList = ref<any[]>([])
|
||
|
||
onMounted(() => {
|
||
getTableData()
|
||
})
|
||
|
||
// const getTableData = () => {
|
||
// roleList.value = ROLE_LIST_DATA
|
||
// }
|
||
// 获取列表数据
|
||
const getTableData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const { currentPage, pageSize } = pagination
|
||
|
||
const { records, current, size, total } = await systemService.getRoleList({
|
||
current: currentPage,
|
||
size: pageSize
|
||
})
|
||
roleList.value = records.map((item: any) => ({
|
||
...item
|
||
}))
|
||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||
} catch (error) {
|
||
console.error('获取用户列表失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
// 处理表格分页变化
|
||
const handleSizeChange = (newPageSize: number) => {
|
||
pagination.pageSize = newPageSize
|
||
getTableData()
|
||
}
|
||
|
||
const handleCurrentChange = (newCurrentPage: number) => {
|
||
pagination.currentPage = 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.name = row.name
|
||
form.desc = row.desc
|
||
form.sort = row.sort
|
||
form.status = row.status
|
||
} else {
|
||
form.name = ''
|
||
form.desc = ''
|
||
form.sort = 0
|
||
form.status = 1
|
||
}
|
||
}
|
||
const roleId = ref('')
|
||
const showPermissionDialog = async (row: any) => {
|
||
permissionDialog.value = true
|
||
roleId.value = row.id
|
||
try {
|
||
const res = await systemService.getPermissionMenuList({ role_id: row.id })
|
||
// 兼容不同返回结构(data/records/直接数组)
|
||
const list = (res as any)?.data ?? (res as any)?.records ?? (res as any)
|
||
permissionMenuList.value = Array.isArray(list) ? list : []
|
||
// 根据接口返回的 selected 字段设置默认选中
|
||
nextTick(() => {
|
||
const keys = buildDefaultCheckedKeys(permissionMenuList.value)
|
||
treeRef.value?.setCheckedKeys(keys)
|
||
handleTreeCheck()
|
||
})
|
||
} catch (e) {
|
||
permissionMenuList.value = []
|
||
}
|
||
}
|
||
// 构建默认选中的 key 列表(与 node-key="id" 一致)
|
||
const buildDefaultCheckedKeys = (source: any[]): Array<string | number> => {
|
||
const keys: Array<string | number> = []
|
||
const walk = (node: any) => {
|
||
if (node?.meta?.selected) {
|
||
keys.push(node.id)
|
||
}
|
||
if (Array.isArray(node?.meta?.authList)) {
|
||
node.meta.authList.forEach((auth: any) => {
|
||
if (auth?.selected) {
|
||
keys.push(`${node.id}__auth__${auth.authMark}`)
|
||
}
|
||
})
|
||
}
|
||
if (Array.isArray(node?.children)) node.children.forEach(walk)
|
||
}
|
||
source.forEach(walk)
|
||
return keys
|
||
}
|
||
// 处理菜单数据,将 authList 转换为子节点
|
||
const processedMenuList = computed(() => {
|
||
const processNode = (node: any) => {
|
||
// 规范化 id,确保树节点唯一键与 node-key 对应
|
||
const processed: any = { ...node, id: node.id }
|
||
|
||
// 如果有 authList,将其转换为子节点
|
||
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||
const authNodes = node.meta.authList.map((auth: any) => ({
|
||
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
|
||
id: `${node.id}__auth__${auth.authMark}`,
|
||
name: `${node.name}_${auth.authMark}`,
|
||
label: auth.title,
|
||
auth_mark: auth.authMark,
|
||
auth_id: auth.id,
|
||
isAuth: true,
|
||
checked: auth.selected || false
|
||
}))
|
||
|
||
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||
}
|
||
|
||
// 递归处理子节点
|
||
if (processed.children) {
|
||
processed.children = processed.children.map(processNode)
|
||
}
|
||
|
||
return processed
|
||
}
|
||
|
||
const source = permissionMenuList.value || []
|
||
return source.map(processNode)
|
||
})
|
||
|
||
const defaultProps = {
|
||
children: 'children',
|
||
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
|
||
}
|
||
|
||
const deleteRole = async (row: any) => {
|
||
ElMessageBox.confirm('确定删除该角色吗?', '删除确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'error'
|
||
}).then(async () => {
|
||
const res = await systemService.roleDelete({ id: row.id })
|
||
ElMessage.success(res?.message || '删除成功!')
|
||
getTableData()
|
||
})
|
||
}
|
||
|
||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||
if (!formEl) return
|
||
|
||
if (dialogType.value === 'add') {
|
||
// 新增角色逻辑
|
||
const res = await systemService.roleAdd(form)
|
||
ElMessage.success(res?.message || '提交成功!')
|
||
getTableData()
|
||
} else {
|
||
// 编辑角色逻辑
|
||
const res = await systemService.roleEdit(form)
|
||
ElMessage.success(res?.message || '提交成功!')
|
||
getTableData()
|
||
}
|
||
dialogVisible.value = false
|
||
formEl.resetFields()
|
||
}
|
||
|
||
const getSelectedIds = () => {
|
||
const tree = treeRef.value
|
||
if (!tree) return { menuIds: [], authIds: [] }
|
||
|
||
const checkedNodes = tree.getCheckedNodes?.(false) || []
|
||
const halfCheckedNodes = tree.getHalfCheckedNodes?.() || []
|
||
const allNodes = [...checkedNodes, ...halfCheckedNodes]
|
||
|
||
const menuIds = Array.from(
|
||
new Set(
|
||
allNodes
|
||
.filter((n: any) => !n.isAuth && n.id !== undefined && n.id !== null)
|
||
.map((n: any) => n.id)
|
||
)
|
||
)
|
||
const authIds = Array.from(
|
||
new Set(
|
||
checkedNodes.filter((n: any) => n.isAuth && n.auth_id != null).map((n: any) => n.auth_id)
|
||
)
|
||
)
|
||
|
||
const ids = Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
|
||
return { menuIds, authIds, ids }
|
||
}
|
||
|
||
const savePermission = async () => {
|
||
const { ids } = getSelectedIds()
|
||
// console.log('选中的菜单ID:', menuIds)
|
||
// console.log('选中的权限ID:', authIds)
|
||
// console.log('合并后的全部ID:', ids)
|
||
|
||
const res = await systemService.roleAuth({
|
||
role_id: roleId.value,
|
||
resources: ids
|
||
})
|
||
ElMessage.success(res?.message || '权限保存成功')
|
||
permissionDialog.value = false
|
||
}
|
||
|
||
const toggleExpandAll = () => {
|
||
const tree = treeRef.value
|
||
if (!tree) return
|
||
|
||
// 使用store.nodesMap直接控制所有节点的展开状态
|
||
const nodes = tree.store.nodesMap
|
||
for (const node in nodes) {
|
||
nodes[node].expanded = !isExpandAll.value
|
||
}
|
||
|
||
isExpandAll.value = !isExpandAll.value
|
||
}
|
||
|
||
const toggleSelectAll = () => {
|
||
const tree = treeRef.value
|
||
if (!tree) return
|
||
|
||
if (!isSelectAll.value) {
|
||
// 全选:获取所有节点的key并设置为选中
|
||
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||
tree.setCheckedKeys(allKeys)
|
||
} else {
|
||
// 取消全选:清空所有选中
|
||
tree.setCheckedKeys([])
|
||
}
|
||
|
||
isSelectAll.value = !isSelectAll.value
|
||
}
|
||
|
||
const getAllNodeKeys = (nodes: any[]): Array<string | number> => {
|
||
const keys: Array<string | number> = []
|
||
const traverse = (nodeList: any[]) => {
|
||
nodeList.forEach((node) => {
|
||
if (node.id !== undefined && node.id !== null) {
|
||
keys.push(node.id)
|
||
}
|
||
if (node.children && node.children.length > 0) {
|
||
traverse(node.children)
|
||
}
|
||
})
|
||
}
|
||
traverse(nodes)
|
||
return keys
|
||
}
|
||
|
||
const handleTreeCheck = () => {
|
||
const tree = treeRef.value
|
||
if (!tree) return
|
||
|
||
// 使用树组件的getCheckedKeys方法获取选中的节点
|
||
const checkedKeys = tree.getCheckedKeys()
|
||
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||
|
||
// 判断是否全选:选中的节点数量等于总节点数量
|
||
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page-content {
|
||
.svg-icon {
|
||
width: 1.8em;
|
||
height: 1.8em;
|
||
overflow: hidden;
|
||
vertical-align: -8px;
|
||
fill: currentcolor;
|
||
}
|
||
}
|
||
</style>
|