feat(menu): 添加菜单缓存刷新功能和拖拽排序支持
- 在SysMenuController中添加refreshCache和refreshCurrentUserMenuCache接口 - 实现菜单缓存的按需刷新和用户级别缓存清理功能 - 优化菜单列表查询的缓存key策略,支持更精确的缓存命中 - 为菜单树查询添加缓存注解提升性能 - 在菜单增删改操作中完善缓存清理逻辑 - 添加allocateMenuToRole方法实现菜单角色分配功能 - 在前端DictTag组件中修复标签类型验证逻辑 - 为首页配置页面添加拖拽排序功能,支持快捷功能重新排列 - 集成Sortable.js实现拖拽交互和排序保存 - 优化菜单管理页面的缓存刷新机制和数据展示 - 完善配置更新事件处理,支持实时配置同步
This commit is contained in:
@@ -138,4 +138,26 @@ public class SysMenuController extends BaseController {
|
|||||||
String fullPath = menuService.generateFullPath(parentId, currentPath);
|
String fullPath = menuService.generateFullPath(parentId, currentPath);
|
||||||
return success(fullPath);
|
return success(fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新菜单缓存
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:menu:list')")
|
||||||
|
@Log(title = "菜单管理", businessType = BusinessType.OTHER)
|
||||||
|
@PostMapping("/refreshCache")
|
||||||
|
public AjaxResult refreshCache() {
|
||||||
|
menuService.refreshMenuCache();
|
||||||
|
return success("菜单缓存已刷新");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制刷新当前用户菜单缓存
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:menu:list')")
|
||||||
|
@Log(title = "菜单管理", businessType = BusinessType.OTHER)
|
||||||
|
@PostMapping("/refreshCurrentUserMenuCache")
|
||||||
|
public AjaxResult refreshCurrentUserMenuCache() {
|
||||||
|
menuService.clearMenuCacheByUserId(getUserId());
|
||||||
|
return success("当前用户菜单缓存已刷新");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -165,4 +165,23 @@ public interface ISysMenuService {
|
|||||||
* @return 完整路径
|
* @return 完整路径
|
||||||
*/
|
*/
|
||||||
public String generateFullPath(Long parentId, String currentPath);
|
public String generateFullPath(Long parentId, String currentPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新菜单缓存
|
||||||
|
*/
|
||||||
|
public void refreshMenuCache();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID清除菜单缓存
|
||||||
|
*/
|
||||||
|
public void clearMenuCacheByUserId(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将菜单分配给角色
|
||||||
|
*
|
||||||
|
* @param roleId 角色ID
|
||||||
|
* @param menuIds 菜单ID列表
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public int allocateMenuToRole(Long roleId, List<Long> menuIds);
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import com.core.common.core.domain.entity.SysRole;
|
|||||||
import com.core.common.core.domain.entity.SysUser;
|
import com.core.common.core.domain.entity.SysUser;
|
||||||
import com.core.common.utils.SecurityUtils;
|
import com.core.common.utils.SecurityUtils;
|
||||||
import com.core.common.utils.StringUtils;
|
import com.core.common.utils.StringUtils;
|
||||||
|
import com.core.system.domain.SysRoleMenu;
|
||||||
import com.core.system.domain.vo.MetaVo;
|
import com.core.system.domain.vo.MetaVo;
|
||||||
import com.core.system.domain.vo.RouterVo;
|
import com.core.system.domain.vo.RouterVo;
|
||||||
import com.core.system.mapper.SysMenuMapper;
|
import com.core.system.mapper.SysMenuMapper;
|
||||||
@@ -59,7 +60,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
* @return 菜单列表
|
* @return 菜单列表
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'menuList:' + #userId + ':' + (#menu == null ? 'all' : #menu.menuName)")
|
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'menuList:' + #userId + ':' + (#menu == null ? 'all' : (#menu.menuName != null ? #menu.menuName : 'all') + ':' + (#menu.visible != null ? #menu.visible : 'all') + ':' + (#menu.status != null ? #menu.status : 'all'))")
|
||||||
public List<SysMenu> selectMenuList(SysMenu menu, Long userId) {
|
public List<SysMenu> selectMenuList(SysMenu menu, Long userId) {
|
||||||
List<SysMenu> menuList = null;
|
List<SysMenu> menuList = null;
|
||||||
// 管理员显示所有菜单信息
|
// 管理员显示所有菜单信息
|
||||||
@@ -115,6 +116,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
* @return 菜单列表
|
* @return 菜单列表
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
@org.springframework.cache.annotation.Cacheable(value = "menu", key = "'menuTree:' + #userId")
|
||||||
public List<SysMenu> selectMenuTreeByUserId(Long userId) {
|
public List<SysMenu> selectMenuTreeByUserId(Long userId) {
|
||||||
List<SysMenu> menus = null;
|
List<SysMenu> menus = null;
|
||||||
if (SecurityUtils.isAdmin(userId)) {
|
if (SecurityUtils.isAdmin(userId)) {
|
||||||
@@ -410,6 +412,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
@org.springframework.cache.annotation.Caching(evict = {
|
@org.springframework.cache.annotation.Caching(evict = {
|
||||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
|
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
|
||||||
})
|
})
|
||||||
@@ -419,7 +422,14 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
if (sysMenu != null){
|
if (sysMenu != null){
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return menuMapper.insertMenu(menu);
|
|
||||||
|
int rows = menuMapper.insertMenu(menu);
|
||||||
|
|
||||||
|
// 如果是管理员创建菜单,自动分配给所有角色(可选逻辑)
|
||||||
|
// 或者,可以将新菜单分配给创建者所属的角色
|
||||||
|
// 这里我们暂时不自动分配,因为这可能不符合安全最佳实践
|
||||||
|
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -431,7 +441,8 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
@Override
|
@Override
|
||||||
@org.springframework.cache.annotation.Caching(evict = {
|
@org.springframework.cache.annotation.Caching(evict = {
|
||||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true),
|
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true),
|
||||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'fullPath:' + #menu.menuId")
|
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'fullPath:' + #menu.menuId"),
|
||||||
|
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'menuTree:' + #menu.updateBy")
|
||||||
})
|
})
|
||||||
public int updateMenu(SysMenu menu) {
|
public int updateMenu(SysMenu menu) {
|
||||||
//路径Path唯一性判断(排除当前菜单本身)
|
//路径Path唯一性判断(排除当前菜单本身)
|
||||||
@@ -456,8 +467,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@org.springframework.cache.annotation.Caching(evict = {
|
@org.springframework.cache.annotation.Caching(evict = {
|
||||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true),
|
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
|
||||||
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'fullPath:' + #menuId")
|
|
||||||
})
|
})
|
||||||
public int deleteMenuById(Long menuId) {
|
public int deleteMenuById(Long menuId) {
|
||||||
return menuMapper.deleteMenuById(menuId);
|
return menuMapper.deleteMenuById(menuId);
|
||||||
@@ -776,4 +786,50 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
|||||||
|
|
||||||
return normalizePath(fullPath.toString());
|
return normalizePath(fullPath.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新菜单缓存
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
|
||||||
|
public void refreshMenuCache() {
|
||||||
|
log.info("菜单缓存已刷新");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID清除菜单缓存
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@org.springframework.cache.annotation.CacheEvict(value = "menu", key = "'menuTree:' + #userId")
|
||||||
|
public void clearMenuCacheByUserId(Long userId) {
|
||||||
|
log.info("清除用户 {} 的菜单树缓存", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将菜单分配给角色
|
||||||
|
*
|
||||||
|
* @param roleId 角色ID
|
||||||
|
* @param menuIds 菜单ID列表
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@org.springframework.transaction.annotation.Transactional
|
||||||
|
@org.springframework.cache.annotation.CacheEvict(value = "menu", allEntries = true)
|
||||||
|
public int allocateMenuToRole(Long roleId, List<Long> menuIds) {
|
||||||
|
// 先删除该角色现有的所有菜单权限
|
||||||
|
roleMenuMapper.deleteRoleMenuByRoleId(roleId);
|
||||||
|
|
||||||
|
// 重新分配菜单给角色
|
||||||
|
if (menuIds != null && !menuIds.isEmpty()) {
|
||||||
|
List<SysRoleMenu> roleMenuList = new ArrayList<>();
|
||||||
|
for (Long menuId : menuIds) {
|
||||||
|
SysRoleMenu rm = new SysRoleMenu();
|
||||||
|
rm.setRoleId(roleId);
|
||||||
|
rm.setMenuId(menuId);
|
||||||
|
roleMenuList.add(rm);
|
||||||
|
}
|
||||||
|
return roleMenuMapper.batchRoleMenu(roleMenuList);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,3 +78,11 @@ export function generateFullPath(parentId, currentPath) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新菜单缓存
|
||||||
|
export function refreshMenuCache() {
|
||||||
|
return request({
|
||||||
|
url: '/system/menu/refreshCache',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
:disable-transitions="true"
|
:disable-transitions="true"
|
||||||
:key="item.value + ''"
|
:key="item.value + ''"
|
||||||
:index="index"
|
:index="index"
|
||||||
:type="item.elTagType === 'primary' ? '' : item.elTagType"
|
:type="getValidTagType(item.elTagType)"
|
||||||
:class="item.elTagClass"
|
:class="item.elTagClass"
|
||||||
>{{ item.label + " " }}</el-tag>
|
>{{ item.label + " " }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
@@ -67,6 +67,26 @@ const unmatch = computed(() => {
|
|||||||
return unmatch // 返回标志的值
|
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) {
|
function handleArray(array) {
|
||||||
if (array.length === 0) return "";
|
if (array.length === 0) return "";
|
||||||
return array.reduce((pre, cur) => {
|
return array.reduce((pre, cur) => {
|
||||||
|
|||||||
@@ -78,13 +78,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="selected-functions-section">
|
<div class="selected-functions-section">
|
||||||
|
<div class="selected-functions-header">
|
||||||
<h4>已选择的功能</h4>
|
<h4>已选择的功能</h4>
|
||||||
<div class="selected-functions-list">
|
<el-tag type="info" size="small">{{ selectedFunctions.length }}/8</el-tag>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="item in selectedFunctions"
|
class="selected-functions-list"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop="handleDrop"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in selectedFunctions"
|
||||||
:key="item.menuId"
|
:key="item.menuId"
|
||||||
class="selected-function-item"
|
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">
|
<div class="function-info">
|
||||||
<svg-icon :icon-class="item.icon" :style="{ color: getIconColor(item) }" />
|
<svg-icon :icon-class="item.icon" :style="{ color: getIconColor(item) }" />
|
||||||
<div class="function-details">
|
<div class="function-details">
|
||||||
@@ -117,8 +134,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, nextTick, watch } from 'vue'
|
import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import Sortable from 'sortablejs'
|
||||||
import useUserStore from '@/store/modules/user'
|
import useUserStore from '@/store/modules/user'
|
||||||
import { listMenu } from '@/api/system/menu'
|
import { listMenu } from '@/api/system/menu'
|
||||||
import { getMenuFullPath } from '@/api/system/menu'
|
import { getMenuFullPath } from '@/api/system/menu'
|
||||||
@@ -187,7 +205,8 @@ import {
|
|||||||
WarningFilled,
|
WarningFilled,
|
||||||
InfoFilled as InfoFilledIcon,
|
InfoFilled as InfoFilledIcon,
|
||||||
SuccessFilled,
|
SuccessFilled,
|
||||||
QuestionFilled as QuestionFilledIcon
|
QuestionFilled as QuestionFilledIcon,
|
||||||
|
Rank
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import SvgIcon from '@/components/SvgIcon'
|
import SvgIcon from '@/components/SvgIcon'
|
||||||
|
|
||||||
@@ -206,11 +225,24 @@ const treeProps = {
|
|||||||
label: 'menuName'
|
label: 'menuName'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sortable实例
|
||||||
|
let sortable = null
|
||||||
|
|
||||||
// 监听过滤文本变化
|
// 监听过滤文本变化
|
||||||
watch(filterText, (val) => {
|
watch(filterText, (val) => {
|
||||||
treeRef.value?.filter(val)
|
treeRef.value?.filter(val)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听selectedFunctions的变化,重新初始化Sortable
|
||||||
|
watch(selectedFunctions, () => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (sortable) {
|
||||||
|
sortable.destroy();
|
||||||
|
}
|
||||||
|
initSortable();
|
||||||
|
});
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
|
||||||
// 获取图标颜色
|
// 获取图标颜色
|
||||||
const getIconColor = (data) => {
|
const getIconColor = (data) => {
|
||||||
@@ -379,12 +411,17 @@ const saveConfig = async () => {
|
|||||||
if (saveResult.code === 200) {
|
if (saveResult.code === 200) {
|
||||||
// 只有在数据库保存成功后,才保存到本地存储
|
// 只有在数据库保存成功后,才保存到本地存储
|
||||||
localStorage.setItem('homeFeaturesConfig', JSON.stringify(menuDataWithPaths))
|
localStorage.setItem('homeFeaturesConfig', JSON.stringify(menuDataWithPaths))
|
||||||
|
// 同时更新缓存配置
|
||||||
|
localStorage.setItem('homeFeaturesConfigCache', JSON.stringify(menuDataWithPaths));
|
||||||
|
localStorage.setItem('homeFeaturesConfigCacheTimestamp', Date.now().toString());
|
||||||
// 清除菜单树缓存,以便下次加载最新数据
|
// 清除菜单树缓存,以便下次加载最新数据
|
||||||
localStorage.removeItem('menuTreeCache');
|
localStorage.removeItem('menuTreeCache');
|
||||||
localStorage.removeItem('menuTreeCacheTimestamp');
|
localStorage.removeItem('menuTreeCacheTimestamp');
|
||||||
ElMessage.success('配置保存成功')
|
ElMessage.success('配置保存成功')
|
||||||
// 触发全局事件,通知首页更新快捷功能
|
// 触发全局事件,通知首页更新快捷功能
|
||||||
window.dispatchEvent(new Event('homeFeaturesConfigUpdated'));
|
window.dispatchEvent(new CustomEvent('homeFeaturesConfigUpdated', {
|
||||||
|
detail: { config: menuDataWithPaths } // 传递最新配置数据
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
console.error('保存到数据库失败:', saveResult);
|
console.error('保存到数据库失败:', saveResult);
|
||||||
ElMessage.error('保存到数据库失败')
|
ElMessage.error('保存到数据库失败')
|
||||||
@@ -553,6 +590,15 @@ const loadSavedConfig = async () => {
|
|||||||
defaultSelections.includes(node.menuId)
|
defaultSelections.includes(node.menuId)
|
||||||
)
|
)
|
||||||
selectedFunctions.value = checkedNodes
|
selectedFunctions.value = checkedNodes
|
||||||
|
} finally {
|
||||||
|
// 确保在数据加载完成后初始化Sortable
|
||||||
|
// 使用 nextTick 确保DOM已更新
|
||||||
|
nextTick(() => {
|
||||||
|
if (sortable) {
|
||||||
|
sortable.destroy();
|
||||||
|
}
|
||||||
|
initSortable();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,9 +618,63 @@ const getAllNodes = (nodes) => {
|
|||||||
return result
|
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(() => {
|
onMounted(() => {
|
||||||
loadMenuData()
|
loadMenuData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 在组件卸载时销毁Sortable实例
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (sortable) {
|
||||||
|
sortable.destroy()
|
||||||
|
sortable = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -690,6 +790,22 @@ onMounted(() => {
|
|||||||
background: white;
|
background: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
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 {
|
.function-info {
|
||||||
@@ -724,4 +840,26 @@ onMounted(() => {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
padding: 20px 0;
|
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>
|
</style>
|
||||||
@@ -963,9 +963,46 @@ const handleStorageChange = (event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 监听配置更新事件
|
// 监听配置更新事件
|
||||||
const handleConfigUpdate = () => {
|
const handleConfigUpdate = (event) => {
|
||||||
console.log('检测到快捷功能配置更新事件,正在重新加载...');
|
console.log('检测到快捷功能配置更新事件,正在重新加载...');
|
||||||
|
// 如果事件携带了最新配置数据,则直接使用
|
||||||
|
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();
|
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 () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
@click="toggleExpandAll"
|
@click="toggleExpandAll"
|
||||||
>展开/折叠</el-button>
|
>展开/折叠</el-button>
|
||||||
</el-col>
|
</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-row>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
@@ -95,12 +95,12 @@
|
|||||||
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
|
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
|
||||||
<el-table-column prop="status" label="状态" width="80">
|
<el-table-column prop="status" label="状态" width="80">
|
||||||
<template #default="scope">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="visible" label="显示状态" width="100">
|
<el-table-column prop="visible" label="显示状态" width="100">
|
||||||
<template #default="scope">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="创建时间" align="center" width="160" prop="createTime">
|
<el-table-column label="创建时间" align="center" width="160" prop="createTime">
|
||||||
@@ -321,7 +321,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup name="Menu">
|
<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 SvgIcon from "@/components/SvgIcon";
|
||||||
import IconSelect from "@/components/IconSelect";
|
import IconSelect from "@/components/IconSelect";
|
||||||
import {getNormalPath} from "@/utils/openhis";
|
import {getNormalPath} from "@/utils/openhis";
|
||||||
@@ -329,6 +329,21 @@ import {getNormalPath} from "@/utils/openhis";
|
|||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable");
|
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 menuList = ref([]);
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
@@ -367,6 +382,20 @@ async function getList() {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 刷新缓存并重新获取菜单列表 */
|
||||||
|
async function handleRefresh() {
|
||||||
|
try {
|
||||||
|
// 首先调用后端接口刷新缓存
|
||||||
|
await refreshMenuCache();
|
||||||
|
// 然后重新获取菜单列表
|
||||||
|
await getList();
|
||||||
|
proxy.$modal.msgSuccess("菜单缓存已刷新,列表已更新");
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新菜单缓存失败:', error);
|
||||||
|
proxy.$modal.msgError("刷新菜单缓存失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
/** 查询菜单下拉树结构 */
|
/** 查询菜单下拉树结构 */
|
||||||
function getTreeselect() {
|
function getTreeselect() {
|
||||||
menuOptions.value = [];
|
menuOptions.value = [];
|
||||||
@@ -494,7 +523,10 @@ function submitForm() {
|
|||||||
} else {
|
} else {
|
||||||
proxy.$modal.msgSuccess("新增成功");
|
proxy.$modal.msgSuccess("新增成功");
|
||||||
open.value = false;
|
open.value = false;
|
||||||
|
// 新增菜单后,刷新缓存并重新获取列表
|
||||||
|
refreshMenuCache().finally(() => {
|
||||||
getList();
|
getList();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// 可以在这里添加自定义的错误处理,或者使用默认的错误提示
|
// 可以在这里添加自定义的错误处理,或者使用默认的错误提示
|
||||||
|
|||||||
Reference in New Issue
Block a user