前言
根据以往几篇手摸手系列文章发布,以及粉丝私信反馈,多数还是希望实例直接上代码,方便复制粘贴,这个确实是个不好的习惯哈,俗话说自己动起手来,发现问题才能解决问题嘛。
App.vue
说明:首先从app.vue开始,由于naiveui框架, 几个提示类型的组件(Dialog,Loading Bar,等),都需要把模板在app中引入,并且需要RouterView同级或者父级,方可优雅使用组件。
其次:如果你想在js中优雅地使用,也是一个问题,官方文档例子,只支持setup中使用,咋办这不科学啊,别急,上有政策下有对策,看我慢慢道来。
<template>
<NConfigProvider>
<AppProvider>
<RouterView />
</AppProvider>
</NConfigProvider>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { AppProvider } from '@/components/Application';
export default defineComponent({
name: 'App',
components: { AppProvider },
setup() {
}
})
</script>
以上用一个,AppProvider组件包裹RouterView, 为了实现组件引入嵌套问题,AppProvider组件实现代码如下:
<template>
<n-loading-bar-provider>
<n-dialog-provider>
<DialogContent />
<n-notification-provider>
<n-message-provider>
<MessageContent />
<slot slot="default"></slot>
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import {
NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
} from 'naive-ui';
import { MessageContent } from '@/components/MessageContent';
import { DialogContent } from '@/components/DialogContent';
export default defineComponent({
name: 'Application',
components: {
NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
MessageContent,
DialogContent,
},
setup() {
return {};
},
});
</script>
解释一下, 相当于app.vue中的 RouterView, 以上组件必须这么用,才能在setup中正常使用。
Layout介绍
这是整个后台骨架核心入口模板,就是登录之后的页面,通常会包含,左侧菜单导航,顶部,内容区域,如下:
实现Layout
这里直接用框架提供的,layout组件,自带了折叠,深色,固定定位,等功能,相对来说是非常方便,改动样式很少即可实现一个骨架。
<template>
<NLayout class="layout" :position="fixedMenu" has-sider>
<!-- 左侧区域 -->
<NLayoutSider>
<!-- logo -->
<Logo :collapsed="collapsed" />
<!-- 左侧菜单 -->
<AsideMenu v-model:collapsed="collapsed" v-model:location="getMenuLocation" />
</NLayoutSider>
<!-- 右侧区域-->
<NLayout>
<!-- header区域-->
<NLayoutHeader :inverted="getHeaderInverted" :position="fixedHeader">
<PageHeader v-model:collapsed="collapsed" :inverted="inverted" />
</NLayoutHeader>
<!-- 页面内容区域-->
<NLayoutContent>
<!-- 多标签组件-->
<TabsView v-if="isMultiTabs" v-model:collapsed="collapsed" />
<!-- 主内容组件-->
<MainView />
</NLayoutContent>
</NLayout>
</NLayout>
</template>
最终实现的效果如下,
以上核心骨架和组件拆分,就已经规划完成,接下来只需要填充对应的组件内容即可
AsideMenu组件
菜单组件封装,包含垂直菜单,和水平菜单,实现如下:
<template>
<NMenu
:options="menus"
:inverted="inverted"
:mode="mode"
:collapsed="collapsed"
:collapsed-width="64"
:collapsed-icon-size="20"
:indent="24"
:expanded-keys="openKeys"
:value="getSelectedKeys"
@update:value="clickMenuItem"
@update:expanded-keys="menuExpanded"
/>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, reactive, computed, watch, toRefs, unref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
import { generatorMenu, generatorMenuMix } from '@/utils';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
export default defineComponent({
name: 'Menu',
components: {},
props: {
mode: {
// 菜单模式
type: String,
default: 'vertical',
},
collapsed: {
// 侧边栏菜单是否收起
type: Boolean,
},
//位置
location: {
type: String,
default: 'left',
},
},
emits: ['update:collapsed'],
setup(props, { emit }) {
// 当前路由
const currentRoute = useRoute();
const router = useRouter();
const asyncRouteStore = useAsyncRouteStore();
const settingStore = useProjectSettingStore();
const menus = ref<any[]>([]);
const selectedKeys = ref<string>(currentRoute.name as string);
const headerMenuSelectKey = ref<string>('');
const { getNavMode } = useProjectSetting();
const navMode = getNavMode;
// 获取当前打开的子菜单
const matched = currentRoute.matched;
const getOpenKeys = matched && matched.length ? matched.map((item) => item.name) : [];
const state = reactive({
openKeys: getOpenKeys,
});
const inverted = computed(() => {
return ['dark', 'header-dark'].includes(settingStore.navTheme);
});
const getSelectedKeys = computed(() => {
let location = props.location;
return location === 'left' || (location === 'header' && unref(navMode) === 'horizontal')
? unref(selectedKeys)
: unref(headerMenuSelectKey);
});
// 监听分割菜单
watch(
() => settingStore.menuSetting.mixMenu,
() => {
updateMenu();
if (props.collapsed) {
emit('update:collapsed', !props.collapsed);
}
}
);
// 监听菜单收缩状态
watch(
() => props.collapsed,
(newVal) => {
state.openKeys = newVal ? [] : getOpenKeys;
selectedKeys.value = currentRoute.name as string;
}
);
// 跟随页面路由变化,切换菜单选中状态
watch(
() => currentRoute.fullPath,
() => {
updateMenu();
const matched = currentRoute.matched;
state.openKeys = matched.map((item) => item.name);
const activeMenu: string = (currentRoute.meta?.activeMenu as string) || '';
selectedKeys.value = activeMenu ? (activeMenu as string) : (currentRoute.name as string);
}
);
function updateMenu() {
if (!settingStore.menuSetting.mixMenu) {
menus.value = generatorMenu(asyncRouteStore.getMenus);
} else {
//混合菜单
const firstRouteName: string = (currentRoute.matched[0].name as string) || '';
menus.value = generatorMenuMix(asyncRouteStore.getMenus, firstRouteName, props.location);
const activeMenu: string = currentRoute?.matched[0].meta?.activeMenu as string;
headerMenuSelectKey.value = (activeMenu ? activeMenu : firstRouteName) || '';
}
}
// 点击菜单
function clickMenuItem(key: string) {
if (/http(s)?:/.test(key)) {
window.open(key);
} else {
router.push({ name: key });
}
}
//展开菜单
function menuExpanded(openKeys: string[]) {
if (!openKeys) return;
const latestOpenKey = openKeys.find((key) => state.openKeys.indexOf(key) === -1);
const isExistChildren = findChildrenLen(latestOpenKey as string);
state.openKeys = isExistChildren ? (latestOpenKey ? [latestOpenKey] : []) : openKeys;
}
//查找是否存在子路由
function findChildrenLen(key: string) {
if (!key) return false;
const subRouteChildren: string[] = [];
for (const { children, key } of unref(menus)) {
if (children && children.length) {
subRouteChildren.push(key as string);
}
}
return subRouteChildren.includes(key);
}
onMounted(() => {
updateMenu();
});
return {
...toRefs(state),
inverted,
menus,
selectedKeys,
headerMenuSelectKey,
getSelectedKeys,
clickMenuItem,
menuExpanded,
};
},
});
</script>
代码实现比较简单,官方提供的menu组件,会自动帮我们递归创建出子菜单,这里我们只需把数据丢给他即可,真是大快人心啊。
说一下核心的2个方法:
1、左侧普通菜单
/**
* 递归组装菜单格式
*/
export function generatorMenu(routerMap: Array<any>) {
return filterRouter(routerMap).map((item) => {
const isRoot = isRootRouter(item);
const info = isRoot ? item.children[0] : item;
const currentMenu = {
...info,
...info.meta,
label: info.meta?.title,
key: info.name,
};
// 是否有子菜单,并递归处理
if (info.children && info.children.length > 0) {
// Recursion
currentMenu.children = generatorMenu(info.children);
}
return currentMenu;
});
}
这里通过路由对象数组,组装成menu组件需要的格式,并且还可以自定义一些逻辑在这里实现
2、顶部菜单模式-混合菜单
/**
* 混合菜单
* */
export function generatorMenuMix(routerMap: Array<any>, routerName: string, location: string) {
const cloneRouterMap = cloneDeep(routerMap);
const newRouter = filterRouter(cloneRouterMap);
if (location === 'header') {
const firstRouter: any[] = [];
newRouter.forEach((item) => {
const isRoot = isRootRouter(item);
const info = isRoot ? item.children[0] : item;
info.children = undefined;
const currentMenu = {
...info,
...info.meta,
label: info.meta?.title,
key: info.name,
};
firstRouter.push(currentMenu);
});
return firstRouter;
} else {
return getChildrenRouter(newRouter.filter((item) => item.name === routerName));
}
}
/**
* 递归组装子菜单
* */
export function getChildrenRouter(routerMap: Array<any>) {
return filterRouter(routerMap).map((item) => {
const isRoot = isRootRouter(item);
const info = isRoot ? item.children[0] : item;
const currentMenu = {
...info,
...info.meta,
label: info.meta?.title,
key: info.name,
};
// 是否有子菜单,并递归处理
if (info.children && info.children.length > 0) {
// Recursion
currentMenu.children = getChildrenRouter(info.children);
}
return currentMenu;
});
}
这里也是为menu组件提供数据支持
PageHeader组件
这个组件主要是实现了这些功能,比较简单,而且不一定是你想要的,所以不说了哈~
TabsView
多标签页,实现的功能比较多,后面单独抽一起来解析,手摸手带你撸。
MainView
<template>
<RouterView>
<template #default="{ Component, route }">
<transition name="zoom-fade" mode="out-in" appear>
<keep-alive v-if="keepAliveComponents" :include="keepAliveComponents">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
<component v-else :is="Component" :key="route.fullPath" />
</transition>
</template>
</RouterView>
</template>
<script>
import { defineComponent, computed } from 'vue';
import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
export default defineComponent({
name: 'MainView',
components: {},
props: {
notNeedKey: {
type: Boolean,
default: false,
},
animate: {
type: Boolean,
default: true,
},
},
setup() {
const asyncRouteStore = useAsyncRouteStore();
// 需要缓存的路由组件
const keepAliveComponents = computed(() => asyncRouteStore.keepAliveComponents);
return {
keepAliveComponents,
};
},
});
</script>
这里主要是做一个缓存路由,和动画效果,以及公共布局风格统一处理
最后上成品
以上只展示一些核心代码,和思路,想看完整代码,可找一下,naive-ui-admin 官方仓库
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!