refactor: 重构角色管理

This commit is contained in:
2025-02-06 20:25:52 +08:00
parent 1c743fb097
commit cfa20ac765
19 changed files with 1037 additions and 713 deletions

View File

@@ -6,7 +6,7 @@ export type * from './type'
const BASE_URL = '/system/menu'
/** @desc 查询菜单列表 */
export function listMenu(query: T.MenuQuery) {
export function listMenu(query?: T.MenuQuery) {
return http.get<T.MenuResp[]>(`${BASE_URL}/tree`, query)
}

View File

@@ -6,8 +6,8 @@ export type * from './type'
const BASE_URL = '/system/role'
/** @desc 查询角色列表 */
export function listRole(query: T.RolePageQuery) {
return http.get<PageRes<T.RoleResp[]>>(`${BASE_URL}`, query)
export function listRole(query: T.RoleQuery) {
return http.get<T.RoleResp[]>(`${BASE_URL}/list`, query)
}
/** @desc 查询角色详情 */
@@ -30,12 +30,27 @@ export function deleteRole(ids: string | Array<string>) {
return http.del(`${BASE_URL}/${ids}`)
}
/** @desc 修改角色权限 */
export function updateRolePermission(id: string, data: any) {
return http.put(`${BASE_URL}/${id}/permission`, data)
}
/** @desc 查询角色关联用户 */
export function listRoleUsers(id: string) {
return http.get(`${BASE_URL}/${id}/user`)
export function listRoleUser(id: string, query: T.RoleUserPageQuery) {
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL}/${id}/user`, query)
}
/** @desc 分配角色给用户 */
export function assignToUsers(id: string, userIds: Array<string>) {
return http.post(`${BASE_URL}/${id}/user`, userIds)
}
/** @desc 取消分配角色给用户 */
export function unassignFromUsers(userRoleIds: Array<string | number>) {
return http.del(`${BASE_URL}/user`, userRoleIds)
}
/** @desc 查询角色关联用户 ID */
export function listRoleUserId(id: string) {
return http.get(`${BASE_URL}/${id}/user/id`)
}

View File

@@ -38,6 +38,7 @@ export interface UserQuery {
deptId?: string
sort: Array<string>
userIds?: Array<string>
excludeUserIds?: Array<string>
}
export interface UserPageQuery extends UserQuery, PageQuery {}
@@ -62,11 +63,29 @@ export type RoleDetailResp = RoleResp & {
menuCheckStrictly: boolean
deptCheckStrictly: boolean
}
export interface RoleUserResp {
id: string
username: string
nickname: string
gender: number
description: string
status: 1 | 2
isSystem?: boolean
deptId: string
deptName: string
roleIds: Array<number>
roleNames: Array<string>
disabled: boolean
}
export interface RoleQuery {
description?: string
sort: Array<string>
}
export interface RolePageQuery extends RoleQuery, PageQuery {}
export interface RoleUserQuery {
description?: string
sort: Array<string>
}
export interface RoleUserPageQuery extends RoleUserQuery, PageQuery {}
/** 菜单类型 */
export interface MenuResp {

View File

@@ -219,7 +219,7 @@ onUnmounted(() => {
overflow: hidden;
:deep(.arco-table-border .arco-table-container) {
border: none;
// do nothing
}
}

View File

@@ -94,11 +94,13 @@ const emit = defineEmits<{
interface Props {
multiple?: boolean
value: string | string[]
excludeValue?: string[]
}
// 查询表单
const queryForm = reactive<UserQuery>({
sort: ['t1.createTime,desc', 't1.id,desc'],
excludeUserIds: props.excludeValue,
})
// 用户列表

View File

@@ -313,6 +313,31 @@
}
}
.gi_tabs {
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
.arco-tabs-content {
flex: 1;
overflow: hidden;
.arco-tabs-content-list {
height: 100%;
}
.arco-tabs-content-item {
height: 100%;
}
.arco-tabs-pane {
height: 100%;
overflow: hidden;
}
}
}
.detail{
height: 100%;
display: flex;
@@ -517,4 +542,4 @@
::view-transition-new(root),
.dark::view-transition-old(root) {
z-index: 9999;
}
}

View File

@@ -132,7 +132,7 @@ const reset = () => {
// 删除
const onDelete = (record: DictItemResp) => {
return handleDelete(() => deleteDictItem(record.id), {
content: `是否确定删除字典「${record.label}」?`,
content: `是否确定删除字典${record.label}」?`,
showModal: true,
})
}

View File

@@ -125,7 +125,7 @@ const onMenuItemClick = (mode: string, node: DictResp) => {
} else if (mode === 'delete') {
Modal.warning({
title: '提示',
content: `是否确定删除 [${node.name}]`,
content: `是否确定删除字典「${node.name}`,
hideCancel: false,
okButtonProps: { status: 'danger' },
onBeforeOk: async () => {

View File

@@ -183,7 +183,7 @@ const handleRightMenuClick = async (mode: string, fileInfo: FileItem) => {
if (mode === 'delete') {
Modal.warning({
title: '提示',
content: `是否确定删除文件 [${fileInfo.name}]`,
content: `是否确定删除文件${fileInfo.name}`,
hideCancel: false,
okButtonProps: { status: 'danger' },
onOk: async () => {

View File

@@ -1,7 +1,7 @@
<template>
<a-drawer
v-model:visible="visible"
title="修改角色"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
@@ -15,7 +15,7 @@
<a-input v-model.trim="form.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="编码" field="code">
<a-input v-model.trim="form.code" placeholder="请输入编码" :disabled="true" />
<a-input v-model.trim="form.code" placeholder="请输入编码" :disabled="isUpdate" />
</a-form-item>
<a-form-item label="排序" field="sort">
<a-input-number v-model="form.sort" placeholder="请输入排序" :min="1" mode="button" />
@@ -30,26 +30,6 @@
/>
</a-form-item>
</fieldset>
<fieldset>
<legend>功能权限</legend>
<a-form-item hide-label :disabled="form.isSystem">
<a-space>
<a-checkbox v-model="isMenuExpanded" @change="onExpanded('menu')">展开/折叠</a-checkbox>
<a-checkbox v-model="isMenuCheckAll" @change="onCheckAll('menu')">全选/全不选</a-checkbox>
<a-checkbox v-model="form.menuCheckStrictly">父子联动</a-checkbox>
</a-space>
<template #extra>
<a-tree
ref="menuTreeRef"
class="menu-tree"
:data="menuList"
:default-expand-all="isMenuExpanded"
:check-strictly="!form.menuCheckStrictly"
checkable
/>
</template>
</a-form-item>
</fieldset>
<fieldset>
<legend>数据权限</legend>
<a-form-item hide-label field="dataScope">
@@ -84,9 +64,11 @@
<script setup lang="ts">
import { type FormInstance, Message, type TreeNodeData } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { getRole, updateRole } from '@/apis/system/role'
import type { GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import { useDept, useDict, useMenu } from '@/hooks/app'
import { useDept, useDict } from '@/hooks/app'
import { addRole, getRole, updateRole } from '@/apis'
const emit = defineEmits<{
(e: 'save-success'): void
@@ -96,10 +78,11 @@ const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const formRef = ref<FormInstance>()
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改角色' : '新增角色'))
const formRef = ref<InstanceType<typeof GiForm>>()
const { data_scope_enum } = useDict('data_scope_enum')
const { deptList, getDeptList } = useDept()
const { menuList, getMenuList } = useMenu()
const rules: FormInstance['rules'] = {
name: [{ required: true, message: '请输入名称' }],
@@ -108,44 +91,24 @@ const rules: FormInstance['rules'] = {
}
const [form, resetForm] = useResetReactive({
menuCheckStrictly: true,
deptCheckStrictly: true,
sort: 999,
dataScope: 4,
})
const menuTreeRef = ref()
const deptTreeRef = ref()
const isMenuExpanded = ref(false)
const isDeptExpanded = ref(true)
const isMenuCheckAll = ref(false)
const isDeptCheckAll = ref(false)
//
const reset = () => {
isMenuExpanded.value = false
isMenuCheckAll.value = false
isDeptExpanded.value = true
isDeptCheckAll.value = false
menuTreeRef.value?.expandAll(isMenuExpanded.value)
menuTreeRef.value?.checkAll(false)
deptTreeRef.value?.expandAll(isDeptExpanded.value)
deptTreeRef.value?.checkAll(false)
formRef.value?.resetFields()
resetForm()
}
//
const getMenuAllCheckedKeys = () => {
//
const checkedNodes = menuTreeRef.value?.getCheckedNodes()
const checkedKeys = checkedNodes.map((item: TreeNodeData) => item.key)
//
const halfCheckedNodes = menuTreeRef.value?.getHalfCheckedNodes()
const halfCheckedKeys = halfCheckedNodes.map((item: TreeNodeData) => item.key)
checkedKeys.unshift(...halfCheckedKeys)
return checkedKeys
}
//
const getDeptAllCheckedKeys = () => {
if (!deptTreeRef.value) {
@@ -162,32 +125,28 @@ const getDeptAllCheckedKeys = () => {
}
// /
const onExpanded = (type: string) => {
if (type === 'menu') {
menuTreeRef.value?.expandAll(isMenuExpanded.value)
} else if (type === 'dept') {
deptTreeRef.value?.expandAll(isDeptExpanded.value)
}
const onExpanded = () => {
deptTreeRef.value?.expandAll(isDeptExpanded.value)
}
// /
const onCheckAll = (type: string) => {
if (type === 'menu') {
menuTreeRef.value?.checkAll(isMenuCheckAll.value)
} else if (type === 'dept') {
deptTreeRef.value?.checkAll(isDeptCheckAll.value)
}
const onCheckAll = () => {
deptTreeRef.value?.checkAll(isDeptCheckAll.value)
}
//
const save = async () => {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return false
try {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return false
form.menuIds = getMenuAllCheckedKeys()
form.deptIds = getDeptAllCheckedKeys()
await updateRole(form, dataId.value)
Message.success('修改成功')
if (isUpdate.value) {
await updateRole(form, dataId.value)
Message.success('修改成功')
} else {
await addRole(form)
Message.success('新增成功')
}
emit('save-success')
return true
} catch (error) {
@@ -195,23 +154,25 @@ const save = async () => {
}
}
//
const onOpen = async (id: string) => {
//
const onAdd = async () => {
reset()
dataId.value = id
if (!menuList.value.length) {
await getMenuList()
}
if (!deptList.value.length) {
await getDeptList()
}
dataId.value = ''
visible.value = true
}
//
const onUpdate = async (id: string) => {
reset()
if (!deptList.value.length) {
await getDeptList()
}
dataId.value = id
const { data } = await getRole(id)
Object.assign(form, data)
data.menuIds?.forEach((node) => {
nextTick(() => {
menuTreeRef.value?.checkNode(node, true, true)
})
})
data.deptIds?.forEach((node) => {
nextTick(() => {
deptTreeRef.value?.checkNode(node, true, true)
@@ -220,7 +181,7 @@ const onOpen = async (id: string) => {
visible.value = true
}
defineExpose({ onOpen })
defineExpose({ onAdd, onUpdate })
</script>
<style scoped lang="scss">
@@ -236,12 +197,4 @@ fieldset legend {
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.menu-tree{
:deep(.arco-tree-node-is-leaf) {
display: inline-flex;
}
:deep(.arco-tree-node-indent-block){
width: 10px;
}
}
</style>

View File

@@ -1,299 +0,0 @@
<template>
<a-modal
v-model:visible="visible"
title="新增角色"
:mask-closable="false"
:esc-to-close="true"
draggable
:width="width >= 600 ? 600 : '100%'"
@close="reset"
>
<a-steps :current="current" class="mb-15" @change="onChangeCurrent">
<a-step>基础信息</a-step>
<a-step>功能权限</a-step>
<a-step>数据权限</a-step>
</a-steps>
<a-form ref="formRef" :model="form" :rules="rules" size="large" auto-label-width>
<fieldset v-show="current === 1">
<a-form-item label="名称" field="name">
<a-input v-model.trim="form.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="编码" field="code">
<a-input v-model.trim="form.code" placeholder="请输入编码" />
</a-form-item>
<a-form-item label="排序" field="sort">
<a-input-number v-model="form.sort" placeholder="请输入排序" :min="1" mode="button" />
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea
v-model.trim="form.description"
placeholder="请输入描述"
show-word-limit
:max-length="200"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
</a-form-item>
</fieldset>
<fieldset v-show="current === 2">
<a-form-item hide-label :disabled="form.isSystem" class="w-full">
<a-space>
<a-checkbox v-model="isMenuExpanded" @change="onExpanded('menu')">展开/折叠</a-checkbox>
<a-checkbox v-model="isMenuCheckAll" @change="onCheckAll('menu')">全选/全不选</a-checkbox>
<a-checkbox v-model="form.menuCheckStrictly">父子联动</a-checkbox>
</a-space>
<template #extra>
<a-tree
ref="menuTreeRef"
v-model:checked-keys="form.menuIds"
class="w-full menu-tree"
:data="menuList"
:default-expand-all="isMenuExpanded"
:check-strictly="!form.menuCheckStrictly"
:virtual-list-props="{ height: 400 }"
checkable
/>
</template>
</a-form-item>
</fieldset>
<fieldset v-show="current === 3">
<a-form-item hide-label field="dataScope">
<a-select
v-model.trim="form.dataScope"
:options="data_scope_enum"
placeholder="请选择数据权限"
:disabled="form.isSystem"
/>
</a-form-item>
<a-form-item v-if="form.dataScope === 5" hide-label :disabled="form.isSystem">
<a-space>
<a-checkbox v-model="isDeptExpanded" @change="onExpanded('dept')">展开/折叠</a-checkbox>
<a-checkbox v-model="isDeptCheckAll" @change="onCheckAll('dept')">全选/全不选</a-checkbox>
<a-checkbox v-model="form.deptCheckStrictly">父子联动</a-checkbox>
</a-space>
<template #extra>
<a-tree
ref="deptTreeRef"
v-model:checked-keys="form.deptIds"
class="w-full"
:data="deptList"
:default-expand-all="isDeptExpanded"
:check-strictly="!form.deptCheckStrictly"
:virtual-list-props="{ height: 350 }"
checkable
/>
</template>
</a-form-item>
</fieldset>
</a-form>
<template #footer>
<a-space size="large">
<a-button :disabled="current === 1" type="secondary" @click="onPrev">
<IconLeft />
上一步
</a-button>
<a-button v-if="current !== 3" :disabled="current === 3" type="primary" @click="onNext">
下一步
<IconRight />
</a-button>
<a-button v-if="current === 3" type="primary" @click="onClickOk">确定</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { type FormInstance, Message, type TreeNodeData } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addRole } from '@/apis/system/role'
import { useResetReactive } from '@/hooks'
import { useDept, useDict, useMenu } from '@/hooks/app'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const formRef = ref<FormInstance>()
const { data_scope_enum } = useDict('data_scope_enum')
const { deptList, getDeptList } = useDept()
const { menuList, getMenuList } = useMenu()
const rules: FormInstance['rules'] = {
name: [{ required: true, message: '请输入名称' }],
code: [{ required: true, message: '请输入编码' }],
dataScope: [{ required: true, message: '请选择数据权限' }],
}
const [form, resetForm] = useResetReactive({
menuCheckStrictly: true,
deptCheckStrictly: true,
sort: 999,
dataScope: 4,
})
const menuTreeRef = ref()
const deptTreeRef = ref()
const isMenuExpanded = ref(false)
const isDeptExpanded = ref(true)
const isMenuCheckAll = ref(false)
const isDeptCheckAll = ref(false)
const current = ref<number>(1)
// 重置
const reset = () => {
isMenuExpanded.value = false
isMenuCheckAll.value = false
isDeptExpanded.value = true
isDeptCheckAll.value = false
menuTreeRef.value?.expandAll(isMenuExpanded.value)
deptTreeRef.value?.expandAll(isDeptExpanded.value)
current.value = 1
formRef.value?.resetFields()
resetForm()
}
// 上一步
const onPrev = () => {
current.value = Math.max(1, current.value - 1)
}
// 下一步
const onNext = async () => {
try {
if (current.value === 1) {
const isInvalid = await formRef.value?.validateField(['name', 'code', 'sort', 'description'])
if (isInvalid) return
}
current.value = Math.min(3, current.value + 1)
} catch (error) {
console.error(error)
}
}
// 当前页
const onChangeCurrent = (page: number) => {
current.value = page
}
// 获取所有选中的菜单
const getMenuAllCheckedKeys = () => {
// 获取目前被选中的菜单
const checkedNodes = menuTreeRef.value?.getCheckedNodes()
const checkedKeys = checkedNodes.map((item: TreeNodeData) => item.key)
// 获取半选中的菜单
const halfCheckedNodes = menuTreeRef.value?.getHalfCheckedNodes()
const halfCheckedKeys = halfCheckedNodes.map((item: TreeNodeData) => item.key)
checkedKeys.unshift(...halfCheckedKeys)
return checkedKeys
}
// 获取所有选中的部门
const getDeptAllCheckedKeys = () => {
if (!deptTreeRef.value) {
return []
}
// 获取目前被选中的部门
const checkedNodes = deptTreeRef.value?.getCheckedNodes()
const checkedKeys = checkedNodes.map((item: TreeNodeData) => item.key)
// 获取半选中的部门
const halfCheckedNodes = deptTreeRef.value?.getHalfCheckedNodes()
const halfCheckedKeys = halfCheckedNodes.map((item: TreeNodeData) => item.key)
checkedKeys.unshift(...halfCheckedKeys)
return checkedKeys
}
// 操作树
const handleTreeAction = (type, action) => {
const refMap = {
menu: menuTreeRef,
dept: deptTreeRef,
}
const ref = refMap[type]
if (ref && action === 'expand') {
ref.value?.expandAll(type === 'menu' ? isMenuExpanded.value : isDeptExpanded.value)
} else if (ref && action === 'check') {
ref.value?.checkAll(type === 'menu' ? isMenuCheckAll.value : isDeptCheckAll.value)
}
}
// 调用时
const onExpanded = (type) => handleTreeAction(type, 'expand')
const onCheckAll = (type) => handleTreeAction(type, 'check')
// 保存
const save = async () => {
try {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return false
form.menuIds = getMenuAllCheckedKeys()
form.deptIds = getDeptAllCheckedKeys()
await addRole(form)
Message.success('新增成功')
emit('save-success')
return true
} catch (error) {
return false
}
}
// 确认
const onClickOk = () => {
if (unref(current) === 3) {
save()
visible.value = false
}
}
// 打开
const onOpen = async () => {
reset()
if (!menuList.value.length) {
await getMenuList()
}
if (!deptList.value.length) {
await getDeptList()
}
dataId.value = ''
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 10px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
height: 440px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.mb-15 {
margin-bottom: 15px
}
:deep(.arco-form-item-extra) {
width: 100%;
}
:deep(.arco-modal-footer){
margin-top: -20px;
}
.menu-tree{
:deep(.arco-tree-node-is-leaf) {
display: inline-flex;
}
:deep(.arco-tree-node-indent-block){
width: 10px;
}
}
</style>

View File

@@ -9,20 +9,25 @@
@before-ok="save"
@close="reset"
>
<UserSelect v-if="visible" ref="UserSelectRef" v-model:value="selectedUsers" @select-user="onSelectUser" />
<UserSelect v-if="visible" ref="UserSelectRef" v-model:value="selectedUsers" :exclude-value="excludeUsers" @select-user="onSelectUser" />
</a-modal>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { assignToUsers, listRoleUsers } from '@/apis/system/role'
import { assignToUsers, listRoleUserId } from '@/apis/system/role'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const selectedUsers = ref<string[]>([])
const excludeUsers = ref<string[]>([])
// 用户选择回调
const onSelectUser = (value: string[]) => {
@@ -48,6 +53,7 @@ const save = async () => {
await assignToUsers(dataId.value, selectedUsers.value)
Message.success('分配成功')
reset()
emit('save-success')
return true
} catch (error) {
return false
@@ -58,8 +64,9 @@ const save = async () => {
const onOpen = async (id: string) => {
dataId.value = id
// 初始化选择的用户
const { data } = await listRoleUsers(id)
selectedUsers.value = data
const { data } = await listRoleUserId(id)
excludeUsers.value = data
selectedUsers.value = []
visible.value = true
}

View File

@@ -1,94 +0,0 @@
<template>
<a-drawer v-model:visible="visible" title="角色详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-descriptions title="基础信息" :column="2" size="large" class="general-description">
<a-descriptions-item label="ID">{{ dataDetail?.id }}</a-descriptions-item>
<a-descriptions-item label="数据权限">
<GiCellTag :value="dataDetail?.dataScope" :dict="data_scope_enum" />
</a-descriptions-item>
<a-descriptions-item label="名称">{{ dataDetail?.name }}</a-descriptions-item>
<a-descriptions-item label="编码">{{ dataDetail?.code }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ dataDetail?.createUserString }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ dataDetail?.createTime }}</a-descriptions-item>
<a-descriptions-item label="修改人">{{ dataDetail?.updateUserString }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ dataDetail?.updateTime }}</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">{{ dataDetail?.description }}</a-descriptions-item>
</a-descriptions>
<a-descriptions
title="功能权限"
:column="2"
size="large"
class="permission general-description"
style="margin-top: 20px; position: relative"
>
<a-descriptions-item :span="2">
<a-tree
:checked-keys="dataDetail?.menuIds"
:data="menuList"
default-expand-all
check-strictly
checkable
/>
</a-descriptions-item>
</a-descriptions>
<a-descriptions
v-if="dataDetail?.dataScope === 5"
title="数据权限"
:column="2"
size="large"
class="general-description"
style="margin-top: 20px; position: relative"
>
<a-descriptions-item :span="2">
<a-tree
:checked-keys="dataDetail?.deptIds"
:data="deptList"
default-expand-all
check-strictly
checkable
/>
</a-descriptions-item>
</a-descriptions>
</a-drawer>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { type RoleDetailResp, getRole as getDetail } from '@/apis/system/role'
import { useDept, useDict, useMenu } from '@/hooks/app'
const { width } = useWindowSize()
const dataId = ref('')
const dataDetail = ref<RoleDetailResp>()
const visible = ref(false)
const { data_scope_enum } = useDict('data_scope_enum')
const { deptList, getDeptList } = useDept()
const { menuList, getMenuList } = useMenu()
// 查询详情
const getDataDetail = async () => {
const { data } = await getDetail(dataId.value)
dataDetail.value = data
}
// 打开
const onOpen = async (id: string) => {
dataId.value = id
if (!menuList.value.length) {
await getMenuList()
}
if (!deptList.value.length) {
await getDeptList()
}
await getDataDetail()
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
.permission :deep(.arco-descriptions-item-label-block) {
padding-right: 0;
}
</style>

View File

@@ -0,0 +1,338 @@
<template>
<GiTable
ref="tableRef"
row-key="id"
:data="tableData"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="false"
:disabled-tools="['fullscreen', 'size', 'setting']"
:row-selection="{ type: 'checkbox', showCheckedAll, selectRowKeys: selectedKeys }"
@select="select"
@select-all="selectAll"
@refresh="refresh"
>
<template #toolbar-left>
<a-button v-permission="['system:role:updatePermission']" type="primary" :disabled="disabled" @click="save">
<template #icon><icon-save /></template>保存权限
</a-button>
</template>
<template #toolbar-right>
<a-tooltip :content="isCascade ? '取消父子联动' : '父子联动'">
<a-button @click="isCascade = !isCascade">
<template #icon>
<icon-check v-if="!isCascade" />
<icon-close v-else />
</template>
</a-button>
</a-tooltip>
<a-tooltip :content="isExpanded ? '折叠' : '展开'">
<a-button @click="onExpanded">
<template #icon>
<icon-mind-mapping v-if="!isExpanded" />
<icon-list v-else />
</template>
</a-button>
</a-tooltip>
</template>
<template #expand-icon="{ expanded }">
<IconDown v-if="expanded" />
<IconRight v-else />
</template>
<template #title="{ record }">
<GiSvgIcon :name="record.icon" :size="15" />
<span style="margin-left: 5px; vertical-align: middle">{{ record.title }}</span>
</template>
<template #permissions="{ record }">
<div v-if="record.permissions && record.permissions.length > 0">
<a-checkbox-group v-model="record.checkedPermissions" :disabled="disabled" @change="selectPermission(record)">
<a-checkbox v-for="permission in record.permissions" :key="permission.id" :value="permission.id">
{{ permission.title }}
</a-checkbox>
</a-checkbox-group>
</div>
</template>
</GiTable>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { Message, type TableInstance } from '@arco-design/web-vue'
import { type MenuResp, listMenu } from '@/apis/system/menu'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { isMobile } from '@/utils'
import type GiTable from '@/components/GiTable/index.vue'
import { useTable } from '@/hooks'
import { getRole, updateRolePermission } from '@/apis'
import has from '@/utils/has'
const props = withDefaults(defineProps<Props>(), {
roleId: '',
})
interface Props {
roleId: string
}
const tableRef = ref<InstanceType<typeof GiTable>>()
// 是否父子联动
const isCascade = ref(true)
const isExpanded = ref(true)
// 是否禁用
const disabled = ref(false)
// 展开/折叠
const onExpanded = () => {
isExpanded.value = !isExpanded.value
tableRef.value?.tableRef?.expandAll(isExpanded.value)
}
/**
* 递归处理菜单数据,并进行类型转换
*
* @param menus 菜单数据
*/
const transformMenu = (menus: MenuResp[]) => {
return menus.map((item) => {
// 如果当前项有子项,递归处理子项
if (item.children && item.children.length > 0) {
// 过滤出 type 为 3 的按钮权限
const permissions = item.children.filter((child) => child.type === 3 || child.permission).map((child) => ({
id: child.id,
title: child.title,
parentId: child.parentId,
permission: child.permission,
}))
// 过滤出 type 不为 3 的子项
item.children = item.children.filter((child) => child.type !== 3 && !child.permission)
// 如果有权限,将其添加到当前项的 permissions 属性中
if (permissions.length > 0) {
item.permissions = permissions
item.checkedPermissions = permissions.filter((permission) => permission.isChecked)
}
// 递归处理剩余的子项
item.children = transformMenu(item.children)
// 如果 children 为空数组,移除 children 属性
if (item.children.length === 0) {
delete item.children
}
}
return item
})
}
// 更新表格数据的选中状态
const updateTableDataCheckedStatus = (data: MenuResp[], selectedKeys: (string | number)[]) => {
data.forEach((item) => {
item.disabled = disabled.value
// 设置菜单项的选中状态
item.isChecked = selectedKeys.includes(item.id)
// 设置权限的选中状态
if (item.permissions) {
item.checkedPermissions = item.permissions
.filter((permission) => selectedKeys.includes(permission.id))
.map((permission) => permission.id)
}
// 递归处理子菜单
if (item.children) {
updateTableDataCheckedStatus(item.children, selectedKeys)
}
})
}
const selectedKeys = ref<Set<string | number>>(new Set())
const {
tableData,
loading,
search,
} = useTable(() => listMenu(), {
immediate: true,
formatResult(data) {
return transformMenu(data)
},
onSuccess: () => {
nextTick(() => {
tableRef.value?.tableRef?.expandAll(true)
})
// 初始加载时应用已选中的权限
if (selectedKeys.value.size > 0) {
updateTableDataCheckedStatus(tableData.value, Array.from(selectedKeys.value))
}
},
})
const columns: TableInstanceColumns[] = [
{ title: '菜单', dataIndex: 'title', slotName: 'title', width: 170, fixed: !isMobile() ? 'left' : undefined },
{ title: '权限', dataIndex: 'permissions', slotName: 'permissions' },
]
// 级联选中子项
const cascadeSelectChild = (record: MenuResp, isCascade: boolean) => {
if (isCascade && record.children && record.children.length > 0) {
record.children.forEach((child) => {
child.isChecked = record.isChecked
tableRef.value?.tableRef?.select(child.id, child.isChecked)
child.isChecked
? selectedKeys.value.add(child.id)
: selectedKeys.value.delete(child.id)
if ((child.children && child.children.length > 0) || (child.permissions && child.permissions.length > 0)) {
cascadeSelectChild(child, isCascade)
}
})
}
// 递归选中权限
if (isCascade && record.permissions && record.permissions.length > 0) {
record.permissions.forEach((permission) => {
permission.isChecked = record.isChecked
permission.isChecked
? selectedKeys.value.add(permission.id)
: selectedKeys.value.delete(permission.id)
})
record.checkedPermissions = record.permissions.filter((permission) => permission.isChecked).map((permission) => permission.id)
}
}
// 查找指定菜单
const findItem = (id: string, data: MenuResp[]) => {
for (const item of data) {
if (item.id === id) return item
if (item.children?.length) {
const found = findItem(id, item.children)
if (found) return found
}
}
return null
}
// 级联选中父项目
const cascadeSelectParent = (record: MenuResp, isCascade: boolean) => {
if (isCascade && record.parentId && record.parentId !== '0') {
const parent = findItem(record.parentId, tableData.value)
if (parent) {
// 如果父项目的某个子项被选中了,它就依然保持选中状态
parent.isChecked = parent.children?.some((child) => child.isChecked)
tableRef.value?.tableRef?.select(parent.id, parent.isChecked)
if (!parent.isChecked && !record.isChecked) {
selectedKeys.value.delete(parent.id)
} else {
selectedKeys.value.add(parent.id)
}
if (parent.parentId && parent.parentId !== 0) {
cascadeSelectParent(parent, isCascade)
}
}
}
}
// 选中
const select: TableInstance['onSelect'] = (rowKeys, checked, record) => {
const isChecked = rowKeys.includes(checked)
isChecked
? selectedKeys.value.add(record.id)
: selectedKeys.value.delete(record.id)
record.isChecked = isChecked
// 级联选中子项
cascadeSelectChild(record, isCascade.value)
// 级联选中父项
cascadeSelectParent(record, isCascade.value)
}
// 全选
const selectAll: TableInstance['onSelectAll'] = (checked) => {
tableData.value.forEach((item) => {
item.isChecked = checked
cascadeSelectChild(item, true)
})
}
// 选中权限
const selectPermission = (record) => {
const checkPermissions = record.checkedPermissions
// 取消选中
if (checkPermissions.length === 0) {
if (isCascade.value) {
record.isChecked = false
selectedKeys.value.delete(record.id)
tableRef.value?.tableRef?.select(record.id, record.isChecked)
cascadeSelectParent(record, isCascade.value)
}
record.permissions.forEach((permission) => {
permission.isChecked = false
selectedKeys.value.delete(permission.id)
})
return
}
// 选中
if (checkPermissions.length > 0) {
if (isCascade.value) {
record.isChecked = true
selectedKeys.value.add(record.id)
tableRef.value?.tableRef?.select(record.id, record.isChecked)
cascadeSelectParent(record, isCascade.value)
}
record.permissions.forEach((permission) => {
permission.isChecked = checkPermissions.includes(permission.id)
permission.isChecked
? selectedKeys.value.add(permission.id)
: selectedKeys.value.delete(permission.id)
})
}
}
// 保存
const save = async () => {
await updateRolePermission(props.roleId, {
menuIds: Array.from(selectedKeys.value),
menuCheckStrictly: isCascade.value,
})
Message.success('保存成功')
}
const showCheckedAll = ref(true)
// 加载角色详情
const fetchRole = async (id: string) => {
disabled.value = !has.hasPermOr(['system:role:updatePermission'])
// 查询角色详情
const { data } = await getRole(id)
if (!disabled.value) {
disabled.value = data.isSystem
}
isCascade.value = data.menuCheckStrictly
// 更新选中键集合
selectedKeys.value = new Set(data.menuIds)
// 更新表格数据的选中状态
updateTableDataCheckedStatus(tableData.value, data.menuIds)
// 手动设置表格行的选中状态,确保组件响应
await nextTick(() => {
tableRef.value?.tableRef?.selectAll(false)
tableRef.value?.tableRef?.select(data.menuIds, true)
showCheckedAll.value = !disabled.value
})
}
// 刷新
const refresh = () => {
search()
fetchRole(props.roleId)
}
// 监听 roleId 的变化
watch(
() => props.roleId,
async (newRoleId) => {
if (newRoleId) {
await fetchRole(newRoleId)
}
},
{ immediate: true },
)
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,179 @@
<template>
<GiTable
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 600 }"
:pagination="pagination"
:disabled-tools="['size', 'setting', 'fullscreen']"
:disabled-column-keys="['nickname']"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
:selected-keys="selectedKeys"
@select="select"
@select-all="selectAll"
@refresh="reset"
>
<template #toolbar-left>
<a-button v-permission="['system:role:assign']" type="primary" @click="onAssign">
<template #icon><icon-plus /></template>
<template #default>分配角色</template>
</a-button>
<a-button v-permission="['system:role:unassign']" type="primary" status="danger" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onMulDelete">
<template #icon><icon-delete /></template>
<template #default>取消分配</template>
</a-button>
</template>
<template #toolbar-right>
<a-input-search v-model="queryForm.description" placeholder="搜索用户名/昵称/描述" allow-clear @search="search" />
<a-button @click="reset">
<template #icon>
<icon-refresh />
</template>
<template #default>重置</template>
</a-button>
</template>
<template #gender="{ record }">
<GiCellGender :gender="record.gender" />
</template>
<template #roleNames="{ record }">
<GiCellTags :data="record.roleNames" />
</template>
<template #status="{ record }">
<GiCellStatus :status="record.status" />
</template>
<template #action="{ record }">
<a-space>
<a-link
v-permission="['system:role:unassign']"
status="danger"
:disabled="record.disabled"
:title="record.disabled ? '该用户为系统内置用户不能取消分配' : '取消分配'"
@click="onDelete(record)"
>
取消分配
</a-link>
</a-space>
</template>
</GiTable>
<RoleAssignModal ref="RoleAssignModalRef" @save-success="search" />
</template>
<script lang='tsx' setup>
import { Message, Modal } from '@arco-design/web-vue'
import RoleAssignModal from '../RoleAssignModal.vue'
import { useResetReactive, useTable } from '@/hooks'
import { type RoleUserQuery, type RoleUserResp, listRoleUser, unassignFromUsers } from '@/apis/system/role'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { isMobile } from '@/utils'
import has from '@/utils/has'
const props = withDefaults(defineProps<Props>(), {
roleId: '',
})
interface Props {
roleId: string
}
const [queryForm, resetForm] = useResetReactive<RoleUserQuery>({
sort: ['t1.id,desc'],
})
const {
tableData: dataList,
loading,
pagination,
search,
selectedKeys,
select,
selectAll,
handleDelete,
} = useTable((page) => listRoleUser(props.roleId, { ...queryForm, ...page }), { immediate: false })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '昵称',
dataIndex: 'nickname',
slotName: 'nickname',
minWidth: 130,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{ title: '用户名', dataIndex: 'username', slotName: 'username', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center' },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 140, ellipsis: true, tooltip: true },
{ title: '角色', dataIndex: 'roleNames', slotName: 'roleNames', minWidth: 165 },
{ title: '描述', dataIndex: 'description', minWidth: 130, ellipsis: true, tooltip: true },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 100,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr([
'system:role:unassign',
]),
},
]
// 重置
const reset = () => {
resetForm()
search()
}
// 批量删除
const onMulDelete = () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
Modal.warning({
title: '提示',
content: `是否确定取消分配角色给所选的${selectedKeys.value.length}个用户?`,
hideCancel: false,
onOk: async () => {
await unassignFromUsers(selectedKeys.value)
Message.success('取消成功')
search()
},
})
}
// 删除
const onDelete = (record: RoleUserResp) => {
return handleDelete(() => unassignFromUsers([record.id]), {
content: `是否确定取消分配角色给用户「${record.nickname}(${record.username})」?`,
successTip: '取消成功',
showModal: true,
})
}
const RoleAssignModalRef = ref<InstanceType<typeof RoleAssignModal>>()
// 分配
const onAssign = () => {
RoleAssignModalRef.value?.onOpen(props.roleId)
}
// 监听 roleId 的变化
watch(
() => props.roleId,
async (newRoleId) => {
if (newRoleId) {
search()
}
},
{ immediate: true },
)
</script>
<style scoped lang="scss"></style>

View File

@@ -1,160 +1,39 @@
<template>
<div class="gi_table_page">
<GiTable
title=""
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:bordered="false"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.description" placeholder="搜索名称/编码/描述" allow-clear @search="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
<div class="gi_page">
<SplitPanel>
<template #left>
<RoleTree @node-click="handleSelectRole" />
</template>
<template #toolbar-right>
<a-button v-permission="['system:role:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
</a-button>
<template #main>
<a-tabs v-model:activeKey="activeTab" class="gi_tabs" size="large">
<a-tab-pane key="1" title="功能权限">
<component :is="Pane1" v-if="activeTab === '1'" :role-id="roleId" />
</a-tab-pane>
<a-tab-pane key="2" title="角色用户">
<component :is="Pane2" v-if="activeTab === '2'" :role-id="roleId" />
</a-tab-pane>
</a-tabs>
</template>
<template #dataScope="{ record }">
<GiCellTag :value="record.dataScope" :dict="data_scope_enum" />
</template>
<template #isSystem="{ record }">
<a-tag v-if="record.isSystem" color="red" size="small"></a-tag>
<a-tag v-else color="arcoblue" size="small"></a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['system:role:detail']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['system:role:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link v-permission="['system:role:assign']" title="分配" @click="onAssign(record)">分配</a-link>
<a-link
v-permission="['system:role:delete']"
status="danger"
:disabled="record.isSystem"
:title="record.isSystem ? '系统内置数据不能删除' : '删除'"
@click="onDelete(record)"
>
删除
</a-link>
</a-space>
</template>
</GiTable>
<RoleAddModal ref="RoleAddModalRef" @save-success="search" />
<RoleUpdateDrawer ref="RoleUpdateDrawerRef" @save-success="search" />
<RoleDetailDrawer ref="RoleDetailDrawerRef" />
<RoleAssignModal ref="RoleAssignModalRef" />
</SplitPanel>
</div>
</template>
<script setup lang="ts">
import RoleAddModal from './RoleAddModal.vue'
import RoleUpdateDrawer from './RoleUpdateDrawer.vue'
import RoleDetailDrawer from './RoleDetailDrawer.vue'
import RoleAssignModal from './RoleAssignModal.vue'
import { type RoleQuery, type RoleResp, deleteRole, listRole } from '@/apis/system/role'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import RoleTree from './tree/index.vue'
import Pane1 from './components/Pane1.vue'
import Pane2 from './components/Pane2.vue'
defineOptions({ name: 'SystemRole' })
const { data_scope_enum } = useDict('data_scope_enum')
const activeTab = ref('1')
const queryForm = reactive<RoleQuery>({
sort: ['id,desc'],
})
const {
tableData: dataList,
loading,
pagination,
search,
handleDelete,
} = useTable((page) => listRole({ ...queryForm, ...page }), { immediate: true })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '名称', dataIndex: 'name', slotName: 'name', ellipsis: true, tooltip: true },
{ title: '编码', dataIndex: 'code', ellipsis: true, tooltip: true },
{ title: '数据权限', dataIndex: 'dataScope', slotName: 'dataScope', ellipsis: true, tooltip: true },
{ title: '排序', dataIndex: 'sort', align: 'center', show: false },
{ title: '系统内置', dataIndex: 'isSystem', slotName: 'isSystem', align: 'center', show: false },
{ title: '描述', dataIndex: 'description', ellipsis: true, tooltip: true },
{ title: '创建人', dataIndex: 'createUserString', ellipsis: true, tooltip: true, show: false },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{ title: '修改人', dataIndex: 'updateUserString', ellipsis: true, tooltip: true, show: false },
{ title: '修改时间', dataIndex: 'updateTime', width: 180, show: false },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 200,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr([
'system:role:detail',
'system:role:update',
'system:role:delete',
'system:role:assign',
]),
},
]
// 重置
const reset = () => {
queryForm.description = undefined
search()
}
// 删除
const onDelete = (record: RoleResp) => {
return handleDelete(() => deleteRole(record.id), {
content: `是否确定删除角色「${record.name}(${record.code})」?`,
showModal: true,
})
}
const RoleAddModalRef = ref<InstanceType<typeof RoleAddModal>>()
// 新增
const onAdd = () => {
RoleAddModalRef.value?.onOpen()
}
const RoleUpdateDrawerRef = ref<InstanceType<typeof RoleUpdateDrawer>>()
// 修改
const onUpdate = (record: RoleResp) => {
RoleUpdateDrawerRef.value?.onOpen(record.id)
}
const RoleDetailDrawerRef = ref<InstanceType<typeof RoleDetailDrawer>>()
// 详情
const onDetail = (record: RoleResp) => {
RoleDetailDrawerRef.value?.onOpen(record.id)
}
const RoleAssignModalRef = ref<InstanceType<typeof RoleAssignModal>>()
// 分配
const onAssign = (record: RoleResp) => {
RoleAssignModalRef.value?.onOpen(record.id)
const roleId = ref('')
// 根据选中角色查询
const handleSelectRole = (keys: Array<any>) => {
roleId.value = keys.length === 1 ? keys[0] : undefined
}
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,69 @@
<template>
<a-menu class="right-menu">
<a-menu-item v-permission="['system:role:update']" title="修改" @click="onClick('update')">
<span>修改</span>
</a-menu-item>
<a-menu-item
v-permission="['system:role:delete']"
class="danger"
:disabled="data.isSystem"
:title="data.isSystem ? '该角色为系统内置角色' : '删除'"
@click="onClick('delete')"
>
<span>删除</span>
</a-menu-item>
</a-menu>
</template>
<script setup lang="ts">
import type { RoleResp } from '@/apis/system/role'
interface Props {
data: RoleResp
}
const props = withDefaults(defineProps<Props>(), {})
const emit = defineEmits<{
(e: 'on-menu-item-click', mode: string, data: RoleResp): void
}>()
// 点击菜单项
const onClick = (mode: string) => {
emit('on-menu-item-click', mode, props.data)
}
</script>
<style scoped lang="scss">
:deep(.arco-menu-inner) {
padding: 4px;
.arco-menu-item {
height: 34px;
&:last-child {
margin-bottom: 0;
}
}
.danger {
color: rgb(var(--danger-6));
}
.danger.arco-menu-disabled {
color: var(--color-danger-light-3);
}
}
.right-menu {
width: 120px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 4px;
border: 1px solid var(--color-border-2);
box-sizing: border-box;
.arrow-icon {
margin-right: 0;
}
}
</style>

View File

@@ -0,0 +1,235 @@
<template>
<div class="container">
<div class="search">
<a-input v-model="searchKey" placeholder="搜索名称/编码" allow-clear>
<template #prefix><icon-search /></template>
</a-input>
<a-button v-permission="['system:role:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
</a-button>
</div>
<div class="tree-wrapper">
<div class="tree">
<a-tree
:data="(treeData as unknown as TreeNodeData[])"
:field-names="{ key: 'id' }"
block-node
:selected-keys="selectedKeys"
@select="select"
>
<template #title="node">
<a-typography-paragraph
:ellipsis="{
rows: 1,
showTooltip: true,
css: true,
}"
>
{{ node.name }} ({{ node.code }})
</a-typography-paragraph>
</template>
<template #extra="node">
<a-trigger trigger="click" align-point animation-name="slide-dynamic-origin" auto-fit-transform-origin position="bl" scroll-to-close>
<icon-more-vertical v-if="has.hasPermOr(['system:role:update', 'system:role:delete'])" class="action" />
<template #content>
<RightMenu :data="node" @on-menu-item-click="onMenuItemClick" />
</template>
</a-trigger>
</template>
</a-tree>
</div>
</div>
<RoleAddDrawer ref="RoleAddDrawerRef" @save-success="getTreeData" />
</div>
</template>
<script setup lang="ts">
import { Message, Modal } from '@arco-design/web-vue'
import type { TreeNodeData } from '@arco-design/web-vue'
import { mapTree } from 'xe-utils'
import RoleAddDrawer from '../RoleAddDrawer.vue'
import RightMenu from './RightMenu.vue'
import { type RoleResp, deleteRole, listRole } from '@/apis/system/role'
import has from '@/utils/has'
const emit = defineEmits<{
(e: 'node-click', keys: Array<any>): void
}>()
const selectedKeys = ref()
// 选中节点
const select = (keys: Array<any>) => {
if (selectedKeys.value && selectedKeys.value[0] === keys[0]) {
return
}
selectedKeys.value = keys
emit('node-click', keys)
}
interface TreeItem extends RoleResp {
popupVisible: boolean
}
const dataList = ref<TreeItem[]>([])
const loading = ref(false)
// 查询树列表
const getTreeData = async () => {
try {
loading.value = true
const { data } = await listRole({ sort: ['sort,asc'] })
dataList.value = mapTree(data, (i) => ({
...i,
popupVisible: false,
icon: () => {
return null
},
}))
await nextTick(() => {
select([dataList.value[0]?.id])
})
} finally {
loading.value = false
}
}
// 过滤树
const searchKey = ref('')
const search = (keyword: string) => {
const loop = (data: TreeItem[]) => {
const result = [] as TreeItem[]
data.forEach((item: TreeItem) => {
if (item.name?.toLowerCase().includes(keyword) || item.code?.toLowerCase().includes(keyword)) {
result.push({ ...item })
}
})
return result
}
return loop(dataList.value)
}
const treeData = computed(() => {
if (!searchKey.value) return dataList.value
return search(searchKey.value.toLowerCase())
})
const RoleAddDrawerRef = ref<InstanceType<typeof RoleAddDrawer>>()
// 新增
const onAdd = () => {
RoleAddDrawerRef.value?.onAdd()
}
// 点击菜单项
const onMenuItemClick = (mode: string, node: RoleResp) => {
if (mode === 'update') {
RoleAddDrawerRef.value?.onUpdate(node.id)
} else if (mode === 'delete') {
Modal.warning({
title: '提示',
content: `是否确定删除角色「${node.name}」?`,
hideCancel: false,
okButtonProps: { status: 'danger' },
onBeforeOk: async () => {
try {
const res = await deleteRole(node.id)
if (res.success) {
Message.success('删除成功')
await getTreeData()
}
return res.success
} catch (error) {
return false
}
},
})
}
}
onMounted(() => {
getTreeData()
})
</script>
<style scoped lang="scss">
:deep(.arco-tree-node) {
line-height: normal;
border-radius: var(--border-radius-medium);
margin: 5px 0;
.action {
opacity: 0;
}
&:hover {
background-color: var(--color-secondary-hover);
.action {
opacity: 1;
}
}
.arco-tree-node-switcher {
width: 0;
margin-right: 0;
}
.arco-tree-node-title {
&:hover {
background-color: transparent;
}
}
.arco-tree-node-title-text {
width: 100%;
white-space: normal;
overflow-wrap: anywhere;
}
}
:deep(.arco-tree-node-selected) {
font-weight: bold;
background-color: rgba(var(--primary-6), 0.1);
&:hover {
background-color: rgba(var(--primary-6), 0.1);
}
.arco-typography {
color: rgb(var(--primary-6));
}
.action {
opacity: 1;
}
}
.container {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
box-sizing: border-box;
height: 100%;
.search {
display: flex;
justify-content: start;
margin-bottom: 10px;
.arco-btn {
margin-left: 8px;
padding: 0 15px;
}
}
.tree-wrapper {
flex: 1;
overflow: hidden;
background-color: var(--color-bg-1);
position: relative;
height: 100%;
/* margin-bottom:10px;*/
.tree {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: auto
}
}
}
</style>

View File

@@ -10,83 +10,79 @@
<DeptTree @node-click="handleSelectDept" />
</template>
<template #main>
<a-row align="stretch" :gutter="14" class="h-full page_content">
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" :xxl="24" class="h-full overflow-hidden">
<GiTable
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['nickname']"
@refresh="search"
>
<template #top>
<GiForm v-model="queryForm" :options="options" :columns="queryFormColumns" @search="search" @reset="reset"></GiForm>
</template>
<template #toolbar-left>
<a-button v-permission="['system:user:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
<GiTable
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['nickname']"
@refresh="search"
>
<template #top>
<GiForm v-model="queryForm" :options="options" :columns="queryFormColumns" @search="search" @reset="reset"></GiForm>
</template>
<template #toolbar-left>
<a-button v-permission="['system:user:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
</a-button>
<a-button v-permission="['system:user:import']" @click="onImport">
<template #icon><icon-upload /></template>
<template #default>导入</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['system:user:export']" @click="onExport">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
</template>
<template #nickname="{ record }">
<GiCellAvatar :avatar="record.avatar" :name="record.nickname" />
</template>
<template #gender="{ record }">
<GiCellGender :gender="record.gender" />
</template>
<template #roleNames="{ record }">
<GiCellTags :data="record.roleNames" />
</template>
<template #status="{ record }">
<GiCellStatus :status="record.status" />
</template>
<template #isSystem="{ record }">
<a-tag v-if="record.isSystem" color="red" size="small"></a-tag>
<a-tag v-else color="arcoblue" size="small"></a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['system:user:detail']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['system:user:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:user:delete']"
status="danger"
:disabled="record.isSystem"
:title="record.isSystem ? '系统内置数据不能删除' : '删除'"
@click="onDelete(record)"
>
删除
</a-link>
<a-dropdown>
<a-button v-if="has.hasPermOr(['system:user:resetPwd', 'system:user:updateRole'])" type="text" size="mini" title="更多">
<template #icon>
<icon-more :size="16" />
</template>
</a-button>
<a-button v-permission="['system:user:import']" @click="onImport">
<template #icon><icon-upload /></template>
<template #default>导入</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['system:user:export']" @click="onExport">
<template #icon><icon-download /></template>
<template #default>导出</template>
</a-button>
</template>
<template #nickname="{ record }">
<GiCellAvatar :avatar="record.avatar" :name="record.nickname" />
</template>
<template #gender="{ record }">
<GiCellGender :gender="record.gender" />
</template>
<template #roleNames="{ record }">
<GiCellTags :data="record.roleNames" />
</template>
<template #status="{ record }">
<GiCellStatus :status="record.status" />
</template>
<template #isSystem="{ record }">
<a-tag v-if="record.isSystem" color="red" size="small"></a-tag>
<a-tag v-else color="arcoblue" size="small"></a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['system:user:detail']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['system:user:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:user:delete']"
status="danger"
:disabled="record.isSystem"
:title="record.isSystem ? '系统内置数据不能删除' : '删除'"
@click="onDelete(record)"
>
删除
</a-link>
<a-dropdown>
<a-button v-if="has.hasPermOr(['system:user:resetPwd', 'system:user:updateRole'])" type="text" size="mini" title="更多">
<template #icon>
<icon-more :size="16" />
</template>
</a-button>
<template #content>
<a-doption v-permission="['system:user:resetPwd']" title="重置密码" @click="onResetPwd(record)">重置密码</a-doption>
<a-doption v-permission="['system:user:updateRole']" title="分配角色" @click="onUpdateRole(record)">分配角色</a-doption>
</template>
</a-dropdown>
</a-space>
</template>
</GiTable>
</a-col>
</a-row>
<template #content>
<a-doption v-permission="['system:user:resetPwd']" title="重置密码" @click="onResetPwd(record)">重置密码</a-doption>
<a-doption v-permission="['system:user:updateRole']" title="分配角色" @click="onUpdateRole(record)">分配角色</a-doption>
</template>
</a-dropdown>
</a-space>
</template>
</GiTable>
</template>
</SplitPanel>