feat(menu): 添加菜单缓存刷新功能和拖拽排序支持

- 在SysMenuController中添加refreshCache和refreshCurrentUserMenuCache接口
- 实现菜单缓存的按需刷新和用户级别缓存清理功能
- 优化菜单列表查询的缓存key策略,支持更精确的缓存命中
- 为菜单树查询添加缓存注解提升性能
- 在菜单增删改操作中完善缓存清理逻辑
- 添加allocateMenuToRole方法实现菜单角色分配功能
- 在前端DictTag组件中修复标签类型验证逻辑
- 为首页配置页面添加拖拽排序功能,支持快捷功能重新排列
- 集成Sortable.js实现拖拽交互和排序保存
- 优化菜单管理页面的缓存刷新机制和数据展示
- 完善配置更新事件处理,支持实时配置同步
This commit is contained in:
2026-02-05 23:07:31 +08:00
parent cd6c015d8f
commit f3d56bff45
8 changed files with 356 additions and 24 deletions

View File

@@ -77,4 +77,12 @@ export function generateFullPath(parentId, currentPath) {
currentPath: currentPath
}
})
}
// 刷新菜单缓存
export function refreshMenuCache() {
return request({
url: '/system/menu/refreshCache',
method: 'post'
})
}

View File

@@ -13,7 +13,7 @@
:disable-transitions="true"
:key="item.value + ''"
:index="index"
:type="item.elTagType === 'primary' ? '' : item.elTagType"
:type="getValidTagType(item.elTagType)"
:class="item.elTagClass"
>{{ item.label + " " }}</el-tag>
</template>
@@ -67,6 +67,26 @@ const unmatch = computed(() => {
return unmatch // 返回标志的值
});
// 验证并返回有效的tag类型
function getValidTagType(tagType) {
// 定义有效的tag类型
const validTypes = ['', 'success', 'warning', 'danger', 'info', 'primary'];
// 如果tagType为null、undefined或不在有效类型中则返回默认值('')
if (tagType === null || tagType === undefined || !validTypes.includes(tagType)) {
// 如果是primary类型转换为空字符串默认样式
if (tagType === 'primary') {
return '';
}
// 其他无效类型统一返回空字符串(默认样式)
return '';
}
// 如果是primary类型也转换为空字符串默认样式
if (tagType === 'primary') {
return '';
}
return tagType;
}
function handleArray(array) {
if (array.length === 0) return "";
return array.reduce((pre, cur) => {

View File

@@ -78,13 +78,30 @@
</div>
<div class="selected-functions-section">
<h4>已选择的功能</h4>
<div class="selected-functions-list">
<div class="selected-functions-header">
<h4>已选择的功能</h4>
<el-tag type="info" size="small">{{ selectedFunctions.length }}/8</el-tag>
</div>
<div
class="selected-functions-list"
@dragover.prevent
@drop="handleDrop"
>
<div
v-for="item in selectedFunctions"
v-for="(item, index) in selectedFunctions"
:key="item.menuId"
class="selected-function-item"
draggable
@dragstart="handleDragStart($event, index)"
@dragover="handleDragOver"
@dragenter="handleDragEnter"
@dragleave="handleDragLeave"
@drop="handleDrop"
@dragend="handleDragEnd"
>
<div class="drag-handle">
<el-icon><Rank /></el-icon>
</div>
<div class="function-info">
<svg-icon :icon-class="item.icon" :style="{ color: getIconColor(item) }" />
<div class="function-details">
@@ -117,8 +134,9 @@
</template>
<script setup>
import { ref, onMounted, nextTick, watch } from 'vue'
import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import Sortable from 'sortablejs'
import useUserStore from '@/store/modules/user'
import { listMenu } from '@/api/system/menu'
import { getMenuFullPath } from '@/api/system/menu'
@@ -187,7 +205,8 @@ import {
WarningFilled,
InfoFilled as InfoFilledIcon,
SuccessFilled,
QuestionFilled as QuestionFilledIcon
QuestionFilled as QuestionFilledIcon,
Rank
} from '@element-plus/icons-vue'
import SvgIcon from '@/components/SvgIcon'
@@ -206,11 +225,24 @@ const treeProps = {
label: 'menuName'
}
// Sortable实例
let sortable = null
// 监听过滤文本变化
watch(filterText, (val) => {
treeRef.value?.filter(val)
})
// 监听selectedFunctions的变化重新初始化Sortable
watch(selectedFunctions, () => {
nextTick(() => {
if (sortable) {
sortable.destroy();
}
initSortable();
});
}, { deep: true })
// 获取图标颜色
const getIconColor = (data) => {
@@ -379,12 +411,17 @@ const saveConfig = async () => {
if (saveResult.code === 200) {
// 只有在数据库保存成功后,才保存到本地存储
localStorage.setItem('homeFeaturesConfig', JSON.stringify(menuDataWithPaths))
// 同时更新缓存配置
localStorage.setItem('homeFeaturesConfigCache', JSON.stringify(menuDataWithPaths));
localStorage.setItem('homeFeaturesConfigCacheTimestamp', Date.now().toString());
// 清除菜单树缓存,以便下次加载最新数据
localStorage.removeItem('menuTreeCache');
localStorage.removeItem('menuTreeCacheTimestamp');
ElMessage.success('配置保存成功')
// 触发全局事件,通知首页更新快捷功能
window.dispatchEvent(new Event('homeFeaturesConfigUpdated'));
window.dispatchEvent(new CustomEvent('homeFeaturesConfigUpdated', {
detail: { config: menuDataWithPaths } // 传递最新配置数据
}));
} else {
console.error('保存到数据库失败:', saveResult);
ElMessage.error('保存到数据库失败')
@@ -553,6 +590,15 @@ const loadSavedConfig = async () => {
defaultSelections.includes(node.menuId)
)
selectedFunctions.value = checkedNodes
} finally {
// 确保在数据加载完成后初始化Sortable
// 使用 nextTick 确保DOM已更新
nextTick(() => {
if (sortable) {
sortable.destroy();
}
initSortable();
});
}
}
@@ -572,9 +618,63 @@ const getAllNodes = (nodes) => {
return result
}
// 初始化Sortable
const initSortable = () => {
const container = document.querySelector('.selected-functions-list')
if (container && selectedFunctions.value.length > 0) {
sortable = Sortable.create(container, {
animation: 150, // 动画时长
ghostClass: 'sortable-ghost', // 拖拽时的样式
chosenClass: 'sortable-chosen', // 选中时的样式
dragClass: 'sortable-drag', // 拖拽过程中的样式
onEnd: function(evt) {
// 拖拽结束时更新数组顺序
const oldIndex = evt.oldIndex
const newIndex = evt.newIndex
if (oldIndex !== newIndex) {
// 移动数组元素
const movedItem = selectedFunctions.value.splice(oldIndex, 1)[0]
selectedFunctions.value.splice(newIndex, 0, movedItem)
// 更新树形控件的选中状态
updateTreeCheckState()
}
}
})
}
}
// 更新树形控件的选中状态
const updateTreeCheckState = () => {
// 获取当前所有选中的菜单ID
const checkedIds = selectedFunctions.value.map(item => item.menuId)
// 更新 checkedKeys 和树形控件的选中状态
checkedKeys.value = checkedIds
// 如果 treeRef 存在,手动设置选中状态
if (treeRef.value) {
// 清除当前选中状态
treeRef.value.setCheckedKeys([])
// 设置新的选中状态
checkedIds.forEach(id => {
treeRef.value.setChecked(id, true, false)
})
}
}
onMounted(() => {
loadMenuData()
})
// 在组件卸载时销毁Sortable实例
onUnmounted(() => {
if (sortable) {
sortable.destroy()
sortable = null
}
})
</script>
<style scoped lang="scss">
@@ -690,6 +790,22 @@ onMounted(() => {
background: white;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
cursor: move; /* 添加拖拽光标 */
transition: all 0.2s ease;
}
.selected-function-item.drag-over {
border: 2px dashed #409eff;
background-color: #ecf5ff;
}
.drag-handle {
cursor: move;
padding: 0 8px;
color: #909399;
display: flex;
align-items: center;
justify-content: center;
}
.function-info {
@@ -724,4 +840,26 @@ onMounted(() => {
font-style: italic;
padding: 20px 0;
}
.selected-functions-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
/* Sortable相关样式 */
.sortable-ghost {
opacity: 0.4;
}
.sortable-chosen {
background-color: #ecf5ff;
border: 1px solid #409eff;
box-shadow: 0 0 8px rgba(64, 158, 255, 0.3);
}
.sortable-drag {
user-select: none;
}
</style>

View File

@@ -963,9 +963,46 @@ const handleStorageChange = (event) => {
};
// 监听配置更新事件
const handleConfigUpdate = () => {
const handleConfigUpdate = (event) => {
console.log('检测到快捷功能配置更新事件,正在重新加载...');
loadUserQuickAccessConfig();
// 如果事件携带了最新配置数据,则直接使用
if (event && event.detail && event.detail.config) {
console.log('使用事件传递的最新配置数据:', event.detail.config);
// 直接更新本地存储中的配置
localStorage.setItem('homeFeaturesConfig', JSON.stringify(event.detail.config));
// 更新缓存时间戳
localStorage.setItem('homeFeaturesConfigCache', JSON.stringify(event.detail.config));
localStorage.setItem('homeFeaturesConfigCacheTimestamp', Date.now().toString());
// 使用最新配置更新首页显示
updateQuickAccessData(event.detail.config);
} else {
// 否则重新加载配置
loadUserQuickAccessConfig();
}
};
// 直接更新快捷访问数据
const updateQuickAccessData = async (configData) => {
quickAccessLoading.value = true;
try {
console.log('使用最新配置数据更新快捷访问...', configData);
// 转换菜单ID为快捷访问格式
const convertedFeatures = await convertMenuIdsToQuickAccess(configData);
console.log('转换后的功能:', convertedFeatures);
if (convertedFeatures && convertedFeatures.length > 0) {
quickAccessData.value = convertedFeatures;
} else {
// 如果转换失败,使用默认配置
quickAccessData.value = getDefaultQuickAccessConfig();
}
} catch (error) {
console.error('更新快捷访问数据失败:', error);
// 出错时使用默认配置
quickAccessData.value = getDefaultQuickAccessConfig();
} finally {
quickAccessLoading.value = false;
}
};
onMounted(async () => {

View File

@@ -54,7 +54,7 @@
@click="toggleExpandAll"
>展开/折叠</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
<right-toolbar v-model:showSearch="showSearch" @queryTable="handleRefresh"></right-toolbar>
</el-row>
<el-table
@@ -95,12 +95,12 @@
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" class="dict-tag" />
<dict-tag :options="processedSysNormalDisable" :value="scope.row.status" class="dict-tag" />
</template>
</el-table-column>
<el-table-column prop="visible" label="显示状态" width="100">
<template #default="scope">
<dict-tag :options="sys_show_hide" :value="scope.row.visible" class="dict-tag" />
<dict-tag :options="processedSysShowHide" :value="scope.row.visible" class="dict-tag" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" width="160" prop="createTime">
@@ -321,7 +321,7 @@
</style>
<script setup name="Menu">
import {addMenu, delMenu, getMenu, listMenu, updateMenu, treeselect} from "@/api/system/menu";
import {addMenu, delMenu, getMenu, listMenu, updateMenu, treeselect, refreshMenuCache} from "@/api/system/menu";
import SvgIcon from "@/components/SvgIcon";
import IconSelect from "@/components/IconSelect";
import {getNormalPath} from "@/utils/openhis";
@@ -329,6 +329,21 @@ import {getNormalPath} from "@/utils/openhis";
const { proxy } = getCurrentInstance();
const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable");
// 处理字典数据确保elTagType字段不为null
const processedSysShowHide = computed(() => {
return sys_show_hide.value.map(item => ({
...item,
elTagType: item.elTagType || '' // 如果elTagType为null或undefined则设为空字符串
}));
});
const processedSysNormalDisable = computed(() => {
return sys_normal_disable.value.map(item => ({
...item,
elTagType: item.elTagType || '' // 如果elTagType为null或undefined则设为空字符串
}));
});
const menuList = ref([]);
const open = ref(false);
const loading = ref(true);
@@ -367,6 +382,20 @@ async function getList() {
loading.value = false;
}
}
/** 刷新缓存并重新获取菜单列表 */
async function handleRefresh() {
try {
// 首先调用后端接口刷新缓存
await refreshMenuCache();
// 然后重新获取菜单列表
await getList();
proxy.$modal.msgSuccess("菜单缓存已刷新,列表已更新");
} catch (error) {
console.error('刷新菜单缓存失败:', error);
proxy.$modal.msgError("刷新菜单缓存失败");
}
}
/** 查询菜单下拉树结构 */
function getTreeselect() {
menuOptions.value = [];
@@ -494,7 +523,10 @@ function submitForm() {
} else {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
// 新增菜单后,刷新缓存并重新获取列表
refreshMenuCache().finally(() => {
getList();
});
}
}).catch(() => {
// 可以在这里添加自定义的错误处理,或者使用默认的错误提示