Files
yudao-ui-admin-vue3/src/views/system/menu/index.vue

403 lines
11 KiB
Vue
Raw Normal View History

<template>
<doc-alert title="功能权限" url="https://doc.iocoder.cn/resource-permission" />
2023-10-31 10:17:00 +08:00
<doc-alert title="菜单路由" url="https://doc.iocoder.cn/vue3/route/" />
2023-03-26 22:15:45 +08:00
<!-- 搜索工作栏 -->
<ContentWrap>
2023-03-26 22:15:45 +08:00
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
2023-03-26 22:15:45 +08:00
label-width="68px"
>
2023-03-25 16:47:20 +08:00
<el-form-item label="菜单名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
2023-03-25 16:47:20 +08:00
clearable
placeholder="请输入菜单名称"
2023-03-25 16:47:20 +08:00
@keyup.enter="handleQuery"
/>
</el-form-item>
2023-03-25 16:47:20 +08:00
<el-form-item label="状态" prop="status">
2023-03-26 22:15:45 +08:00
<el-select
v-model="queryParams.status"
class="!w-240px"
clearable
placeholder="请选择菜单状态"
2023-03-26 22:15:45 +08:00
>
2023-03-25 16:47:20 +08:00
<el-option
2023-03-26 22:15:45 +08:00
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
2023-03-25 16:47:20 +08:00
:label="dict.label"
2023-03-26 22:15:45 +08:00
:value="dict.value"
2023-03-25 16:47:20 +08:00
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
2023-03-26 22:15:45 +08:00
<el-button
v-hasPermi="['system:menu:create']"
2023-03-26 22:15:45 +08:00
plain
type="primary"
2023-03-26 22:15:45 +08:00
@click="openForm('create')"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button plain type="danger" @click="toggleExpandAll">
<Icon class="mr-5px" icon="ep:sort" />
展开/折叠
2023-03-25 16:47:20 +08:00
</el-button>
<el-button plain @click="refreshMenu">
<Icon class="mr-5px" icon="ep:refresh" />
刷新菜单缓存
2023-03-26 22:15:45 +08:00
</el-button>
</el-form-item>
</el-form>
2023-03-26 22:15:45 +08:00
</ContentWrap>
2023-03-25 16:47:20 +08:00
2023-03-26 22:15:45 +08:00
<!-- 列表 -->
<ContentWrap>
<el-tree-v2
v-if="refreshTable"
2023-03-25 16:47:20 +08:00
v-loading="loading"
:data="list"
:props="{
label: 'name',
children: 'children'
}"
:default-expanded-keys="isExpandAll ? list.map(item => item.id) : []"
:height="600"
:item-size="40"
:virtual-scroll-horizontal="true"
:highlight-current="true"
@current-change="handleCurrentChange"
2023-03-25 16:47:20 +08:00
>
<template #default="{ data }">
<div
class="custom-tree-node"
:class="{ 'menu-item': true }"
>
<div class="node-content">
<span class="label">{{ data.name }}</span>
<div v-if="currentNode === data" class="menu-info">
<span class="info-item" v-if="data.icon">
<span class="info-label">图标</span>
<span class="icon-preview">
<Icon :icon="data.icon" />
<span class="icon-name">{{ data.icon }}</span>
</span>
</span>
<span class="info-item">
<span class="info-label">排序</span>
<span class="info-value">{{ data.sort }}</span>
</span>
<span class="info-item" v-if="data.permission">
<span class="info-label">权限标识</span>
<span class="info-value">{{ data.permission }}</span>
</span>
<span class="info-item" v-if="data.path">
<span class="info-label">路由地址</span>
<span class="info-value">{{ data.path }}</span>
</span>
<span class="info-item" v-if="data.component">
<span class="info-label">组件路径</span>
<span class="info-value">{{ data.component }}</span>
</span>
<span class="info-item" v-if="data.componentName">
<span class="info-label">组件名称</span>
<span class="info-value">{{ data.componentName }}</span>
</span>
</div>
</div>
<div v-show="currentNode === data" class="operations">
<el-button
v-hasPermi="['system:menu:update']"
link
type="primary"
@click.stop="openForm('update', data.id)"
>
修改
</el-button>
<el-button
v-hasPermi="['system:menu:create']"
link
type="primary"
@click.stop="openForm('create', undefined, data.id)"
>
新增
</el-button>
<el-button
v-hasPermi="['system:menu:delete']"
link
type="danger"
@click.stop="handleDelete(data.id)"
>
删除
</el-button>
</div>
</div>
</template>
</el-tree-v2>
2023-03-25 16:47:20 +08:00
</ContentWrap>
2023-03-26 22:15:45 +08:00
2023-03-25 16:47:20 +08:00
<!-- 表单弹窗添加/修改 -->
2023-03-26 22:15:45 +08:00
<MenuForm ref="formRef" @success="getList" />
</template>
2023-06-21 19:14:34 +08:00
<script lang="ts" setup>
2023-03-26 22:15:45 +08:00
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
2023-03-25 16:47:20 +08:00
import { handleTree } from '@/utils/tree'
import * as MenuApi from '@/api/system/menu'
import { MenuVO } from '@/api/system/menu'
2023-03-26 22:15:45 +08:00
import MenuForm from './MenuForm.vue'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { CommonStatusEnum } from '@/utils/constants'
2023-06-21 19:14:34 +08:00
defineOptions({ name: 'SystemMenu' })
const { wsCache } = useCache()
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
2023-03-25 16:47:20 +08:00
const loading = ref(true) // 列表的加载中
const list = ref<any>([]) // 列表的数据
const queryParams = reactive({
name: undefined,
status: undefined
})
2023-03-25 16:47:20 +08:00
const queryFormRef = ref() // 搜索的表单
2023-03-26 22:15:45 +08:00
const isExpandAll = ref(false) // 是否展开,默认全部折叠
const refreshTable = ref(true) // 重新渲染表格状态
const currentNode = ref<any>(null) // 当前选中节点
2023-03-28 23:18:10 +08:00
/** 查询列表 */
2023-03-25 16:47:20 +08:00
const getList = async () => {
loading.value = true
try {
2023-03-26 22:15:45 +08:00
const data = await MenuApi.getMenuList(queryParams)
// 为每个节点添加 showInfo 属性和样式对象
const addProps = (items: any[]) => {
items.forEach(item => {
item.showInfo = false
item.popupStyle = {}
if (item.children && item.children.length > 0) {
addProps(item.children)
}
})
}
const processedData = handleTree(data)
addProps(processedData)
list.value = processedData
2023-03-25 16:47:20 +08:00
} finally {
loading.value = false
}
}
2023-03-25 16:47:20 +08:00
/** 搜索按钮操作 */
const handleQuery = () => {
getList()
}
2023-03-25 16:47:20 +08:00
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
2023-03-25 16:47:20 +08:00
/** 添加/修改操作 */
2023-03-26 22:15:45 +08:00
const formRef = ref()
const openForm = (type: string, id?: number, parentId?: number) => {
formRef.value.open(type, id, parentId)
}
2023-03-25 16:47:20 +08:00
/** 展开/折叠操作 */
const toggleExpandAll = () => {
refreshTable.value = false
isExpandAll.value = !isExpandAll.value
nextTick(() => {
refreshTable.value = true
})
}
/** 刷新菜单缓存按钮操作 */
const refreshMenu = async () => {
try {
await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存')
// 清空,从而触发刷新
wsCache.delete(CACHE_KEY.USER)
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
// 刷新浏览器
location.reload()
} catch {}
}
2023-03-25 16:47:20 +08:00
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
2023-03-25 16:47:20 +08:00
// 删除的二次确认
await message.delConfirm()
// 发起删除
2023-03-26 22:15:45 +08:00
await MenuApi.deleteMenu(id)
2023-03-25 16:47:20 +08:00
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 开启/关闭菜单的状态 */
const menuStatusUpdating = ref({}) // 菜单状态更新中的 menu 映射。key菜单编号value是否更新中
const handleStatusChanged = async (menu: MenuVO, val: number) => {
// 1. 标记 menu.id 更新中
menuStatusUpdating.value[menu.id] = true
try {
// 2. 发起更新状态
menu.status = val
await MenuApi.updateMenu(menu)
} finally {
// 3. 标记 menu.id 更新完成
menuStatusUpdating.value[menu.id] = false
}
}
const handleCurrentChange = (data: any) => {
currentNode.value = data
// 关闭所有信息面板
list.value.forEach((item: any) => {
item.showInfo = false
})
}
// 添加点击外部关闭弹出层的处理
onMounted(() => {
document.addEventListener('click', (event: MouseEvent) => {
const target = event.target as HTMLElement
if (!target.closest('.menu-info-popup') && !target.closest('.info-button')) {
list.value.forEach((item: any) => {
item.showInfo = false
})
}
})
})
2023-03-25 16:47:20 +08:00
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
:deep(.el-tree-node.is-current > .el-tree-node__content) {
background-color: var(--el-color-primary-light-7) !important;
.custom-tree-node {
background-color: var(--el-color-primary-light-7);
.operations {
background-color: var(--el-color-primary-light-7);
}
}
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
height: 40px;
position: relative;
border-bottom: 1px solid var(--el-border-color-lighter);
min-width: 800px;
transition: background-color 0.3s;
.node-content {
display: flex;
align-items: center;
gap: 12px;
height: 100%;
flex: 1;
min-width: 0;
.label {
flex-shrink: 0;
}
.menu-info {
display: flex;
align-items: center;
gap: 16px;
overflow-x: auto;
flex: 1;
margin-right: 16px;
padding: 0 4px;
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: var(--el-border-color);
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.info-item {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 4px;
.info-label {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.info-value {
color: var(--el-text-color-primary);
font-size: 13px;
}
.icon-preview {
display: flex;
align-items: center;
gap: 4px;
padding: 0 8px;
height: 24px;
border-radius: 4px;
border: 1px solid var(--el-border-color-lighter);
background-color: var(--el-bg-color);
.icon-name {
font-size: 13px;
color: var(--el-text-color-regular);
}
}
}
}
.operations {
display: flex;
gap: 8px;
height: 100%;
align-items: center;
flex-shrink: 0;
position: sticky;
right: 8px;
padding-left: 8px;
transition: background-color 0.3s;
}
}
</style>