feat(menu): 添加菜单缓存刷新功能和拖拽排序支持
- 在SysMenuController中添加refreshCache和refreshCurrentUserMenuCache接口 - 实现菜单缓存的按需刷新和用户级别缓存清理功能 - 优化菜单列表查询的缓存key策略,支持更精确的缓存命中 - 为菜单树查询添加缓存注解提升性能 - 在菜单增删改操作中完善缓存清理逻辑 - 添加allocateMenuToRole方法实现菜单角色分配功能 - 在前端DictTag组件中修复标签类型验证逻辑 - 为首页配置页面添加拖拽排序功能,支持快捷功能重新排列 - 集成Sortable.js实现拖拽交互和排序保存 - 优化菜单管理页面的缓存刷新机制和数据展示 - 完善配置更新事件处理,支持实时配置同步
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user