1、设置标志位
在使用 vue-element-plus-admin 进行二次开发的时候发现了一个小bug,动态设置Search组件的componentProps.options时接口被多次调用。
发现是在Form组件中,getOptions()在没有获取到接口返回值后又进行了多次调用,直到optins有值。解决方法:通过设置标识位,在获取接口状态下不再调用
const isUpdating = ref(false)
const getOptions = async (fn: Function, item: FormSchema) => {
isUpdating.value = true
try {
const options = await fn()
setSchema([
{
field: item.field,
path:
item.component === ComponentNameEnum.TREE_SELECT ||
item.component === ComponentNameEnum.TRANSFER
? 'componentProps.data'
: 'componentProps.options',
value: options
}
])
} catch (error) {
console.error('Failed to fetch options:', error)
} finally {
// 确保无论成功失败,都会重置标志
isUpdating.value = false
}
}
在renderFormItem()渲染formItem方法里添加判断!isUpdating.value时获取
if (
item.optionApi &&
(!item.componentProps?.options || !item.componentProps?.options.length) && !isUpdating.value
) {
// 内部自动调用接口,不影响其它渲染
getOptions(item.optionApi, item)
}
2、菜单结构树的修改
UI改版:先上需求图
之前样式,是很常规后台管理的菜单
处理方式:
- 递归遍历最后一级非按钮级别的菜单全部挪到一级菜单下
- 并且之前二级菜单不能点击,有下级菜单的不能加减操作
隐藏hidden=true的菜单
function filterHiddenMenus(menus) {
return menus
.filter((menu) => !menu.meta?.hidden) // 过滤掉 meta.hidden 为 true 的菜单项
.map((menu) => {
if (menu.children) {
// 递归处理子菜单
menu.children = filterHiddenMenus(menu.children);
}
return menu;
});
}
递归便利最后一级非按钮级别的菜单全部挪到一级菜单下,添加parentPath(完整路由好跳转)
function processMenu(menu) {
// 初始化第一层的 childrenUse 数组
menu.childrenUse = [];
// 递归遍历子节点,收集 meta.iscatalog=1 的节点
function collectCatalogNodes(node) {
if (node.children && node.children.length > 0) {
for (let child of node.children) {
// 递归处理子节点
child.parentPath = node.parentPath ? `${node.parentPath}/${node.path}` : node.path;
collectCatalogNodes(child);
}
}
// 如果当前节点的 meta.iscatalog=1,将其添加到第一层的 childrenUse 中
if (node.meta && node.meta.iscatalog === 1) {
if (node.children?.length) {
if (node.children[0].meta.iscatalog === 0) {
menu.childrenUse.push(node);
}
} else {
menu.childrenUse.push(node);
}
}
}
// 从第一层开始递归收集
if (menu.children && menu.children.length > 0) {
for (let child of menu.children) {
child.parentPath = menu.path;
collectCatalogNodes(child);
}
}
return menu;
}
完整代码
<script lang="tsx">
import { computed, defineComponent, ref, unref, watch } from 'vue';
import { usePermissionStore } from '@/store/modules/permission';
import { Icon } from '@/components/Icon';
import { Router, useRouter } from 'vue-router';
const permissionStore = usePermissionStore();
const routers = computed(() => {
let rou = filterHiddenMenus(permissionStore.getRouters);
rou.forEach((v) => processMenu(v));
return rou;
});
function processMenu(menu) {
// 初始化第一层的 childrenUse 数组
menu.childrenUse = [];
// 递归遍历子节点,收集 meta.iscatalog=1 的节点
function collectCatalogNodes(node) {
if (node.children && node.children.length > 0) {
for (let child of node.children) {
// 递归处理子节点
child.parentPath = node.parentPath ? `${node.parentPath}/${node.path}` : node.path;
collectCatalogNodes(child);
}
}
// 如果当前节点的 meta.iscatalog=1,将其添加到第一层的 childrenUse 中
if (node.meta && node.meta.iscatalog === 1) {
if (node.children?.length) {
if (node.children[0].meta.iscatalog === 0) {
menu.childrenUse.push(node);
}
} else {
menu.childrenUse.push(node);
}
}
}
// 从第一层开始递归收集
if (menu.children && menu.children.length > 0) {
for (let child of menu.children) {
child.parentPath = menu.path;
collectCatalogNodes(child);
}
}
return menu;
}
function filterHiddenMenus(menus) {
return menus
.filter((menu) => !menu.meta?.hidden) // 过滤掉 meta.hidden 为 true 的菜单项
.map((menu) => {
if (menu.children) {
// 递归处理子菜单
menu.children = filterHiddenMenus(menu.children);
}
return menu;
});
}
const currentPath = ref('');
const showPop = ref(false);
const currentMenu: any = ref(null);
const mouseEnterHandler = (v) => {
showPop.value = true;
currentMenu.value = v;
};
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
return routers
.filter((v) => !v.meta?.hidden)
.map((v) => {
const meta = v.meta ?? {};
const fullPath = isUrl(v.path) ? v.path : `${v.parentPath}/${v.path}`;
if (!v.childrenUse?.length) {
return (
<router-link to={fullPath}>
<div class={['ju-menu-item', currentPath.value === fullPath ? 'is-active' : '']}>{meta.title}</div>
</router-link>
);
} else {
return (
<div class="ju-menu" onMouseenter={() => mouseEnterHandler(v)}>
<div class="flex items-center ju-sub-menu">
{meta.icon ? <Icon icon={meta.icon}></Icon> : null}
<p class="flex-1 v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">{meta.title}</p>
<Icon class="ju-sub-menu__icon-arrow" icon="svg-icon:arrow" />
</div>
<div class="flex ju-menu">{renderMenuItem(v.childrenUse!, fullPath)}</div>
</div>
);
}
});
};
const renderMenuPop = (router: Router) => {
const meta = currentMenu.value.meta ?? {};
return (
<div class="jmenu-pop" onMouseleave={() => (showPop.value = false)}>
{!meta.hidden && (
<>
<h3>{currentMenu.value.meta.title}</h3>
<div class="jmemu-pop-w">
{currentMenu.value.children.map((c) => {
if (c.meta.hidden) {
return null;
} else {
return (
<div class="w-108px">
<h4 class={[c.children.length ? '' : 'cursor-pointer flex items-center justify-between']}>
<p onClick={() => popItemClick(router, currentMenu, c)}>{c.meta.title}</p>
{!c.children.length ? <Icon icon="svg-icon:add_circle" /> : null}
</h4>
{c.children.map((cc) => {
if (cc.meta.hidden) {
return null;
} else {
return (
<div class="jmenu-pop-last-item">
<p
class="cursor-pointer single-line-ellipsis"
onClick={() => popItemClick(router, currentMenu, c, cc)}
>
{cc.meta.title}
</p>
<Icon icon="svg-icon:add_circle" />
</div>
);
}
})}
</div>
);
}
})}
</div>
</>
)}
</div>
);
};
const popItemClick = (router, c1, c, cc?) => {
if (!c.children.length) {
router.push(`${unref(c1).path}/${unref(c).path}`);
} else {
if (cc) {
router.push(`${unref(c1).path}/${unref(c).path}/${unref(cc).path}`);
}
}
};
export default defineComponent({
name: 'JMenuItem',
setup() {
const router = useRouter();
watch(
() => router.currentRoute.value,
(v) => {
currentPath.value = v.path;
showPop.value = false;
},
{ immediate: true }
);
return () => (
<>
<div>
{renderMenuItem(routers.value)}
{showPop.value && renderMenuPop(router)}
</div>
</>
);
}
});
</script>
<style lang="less" scoped>
.jmenu-pop {
position: fixed;
top: calc(var(--top-tool-height) + var(--spacing-lg));
left: calc(var(--spacing-lg) + 200px);
height: calc(100vh - var(--top-tool-height) - (var(--spacing-lg)*2));
border-radius: 0px var(--l) var(--l) 0px;
border-left: 1px solid var(--colors-boder-secondary);
background: var(--colors-elevation-surface-default);
box-shadow: 12px 12px 16px -4px rgba(16, 24, 40, 0.08), 4px 0px 6px -2px rgba(16, 24, 40, 0.03);
padding: var(--spacing-2xl);
z-index: 1000;
h3 {
color: var(--colors-text-primary);
font-size: 16px;
font-weight: 500;
}
.jmemu-pop-w {
margin-top: var(--spacing-2xl);
display: flex;
align-items: flex-start;
gap: 64px;
h4 {
color: var(--colors-text-primary);
font-size: 14px;
font-weight: 500;
margin-bottom: var(--spacing-md);
}
.jmenu-pop-last-item {
margin-bottom: var(--spacing-sm);
display: flex;
justify-content: space-between;
gap: var(--spacing-sm);
align-items: center;
p {
color: var(--colors-text-secondary);
font-size: 14px;
font-weight: 400;
width: 84px;
}
}
}
}
</style>