refactor:路由模块优化重构

environments/test/deployments/1
saatana 2 years ago
parent 7bde556c8a
commit 48c8b708da

@ -10,5 +10,5 @@ export default {
/** /**
* 是否使用本地路由 * 是否使用本地路由
*/ */
useLocalRouter: false, useLocalRouter: true,
}; };

@ -10,7 +10,6 @@
</template> </template>
<script setup> <script setup>
const router = useRouter();
const store = useStore(); const store = useStore();
const asideList = computed(() => store.getters['layout/asideList']); const asideList = computed(() => store.getters['layout/asideList']);
@ -29,6 +28,7 @@
); );
const handleClick = (item) => { const handleClick = (item) => {
store.commit('layout/setAutoRouter', true);
store.commit('layout/setActiveAside', item.name); store.commit('layout/setActiveAside', item.name);
}; };
</script> </script>

@ -9,6 +9,9 @@
<style lang="less" scoped> <style lang="less" scoped>
.layout-footer { .layout-footer {
width: 100%; width: 100%;
height: @layout-footer-height;
line-height: @layout-footer-height;
flex-shrink: 0;
padding: @layout-space-small 0; padding: @layout-space-small 0;
display: flex; display: flex;
align-items: center; align-items: center;

@ -25,6 +25,7 @@
<style lang="less" scoped> <style lang="less" scoped>
.layout-header { .layout-header {
width: 100%; width: 100%;
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;

@ -1,5 +1,5 @@
<template> <template>
<el-scrollbar class="layout-main" always> <el-scrollbar class="layout-main" ref="refsScrollbar" max-height="100%">
<RouterView /> <RouterView />
</el-scrollbar> </el-scrollbar>
</template> </template>
@ -10,6 +10,7 @@
<style lang="less" scoped> <style lang="less" scoped>
.layout-main { .layout-main {
flex: 1;
margin: @layout-space-large; margin: @layout-space-large;
background-color: @color-white; background-color: @color-white;
border-radius: @layout-border-radius; border-radius: @layout-border-radius;
@ -17,11 +18,11 @@
> :deep(.el-scrollbar__wrap) { > :deep(.el-scrollbar__wrap) {
> .el-scrollbar__view { > .el-scrollbar__view {
width: 100%; width: 100%;
height: 100%; min-height: 100%;
> * { > * {
// display: inline-block; // BFC // display: inline-block; // BFC
width: 100%; width: 100%;
height: 100%; height: @layout-main-height;
padding: @layout-space-large; padding: @layout-space-large;
} }
} }

@ -1,5 +1,5 @@
<template> <template>
<el-menu-item v-if="!menuItem.children?.length" :index="props.menuItem.name" :route="{ name: props.menuItem.name }"> <el-menu-item v-if="!hasChildren" :index="props.menuItem.name" :route="{ name: props.menuItem.name }">
<el-icon :name="props.menuItem.meta.icon" size="20" /> <el-icon :name="props.menuItem.meta.icon" size="20" />
<p>{{ props.menuItem.meta.title }}</p> <p>{{ props.menuItem.meta.title }}</p>
</el-menu-item> </el-menu-item>
@ -19,6 +19,9 @@
required: true, required: true,
}, },
}); });
const hasChildren = computed(
() => (props.menuItem.children || []).filter((item) => item.meta.hidden !== true).length > 0
);
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

@ -14,9 +14,10 @@
const store = useStore(); const store = useStore();
const activeAsideName = computed(() => store.getters['layout/activeAsideName']); const activeAsideName = computed(() => store.getters['layout/activeAsideName']);
const activeAside = computed(() => store.state.layout.activeAside); const activeAside = computed(() => store.state.layout.activeAside);
const menuList = computed(() => store.state.layout.menuList); const menuList = computed(() => store.getters['layout/menuList']);
const activeMenu = computed(() => store.state.layout.activeMenu); const activeMenu = computed(() => store.state.layout.activeMenu);
const collapseMenu = computed(() => store.getters['layout/collapseMenu']); const collapseMenu = computed(() => store.getters['layout/collapseMenu']);
const autoRouter = computed(() => store.state.layout.autoRouter);
const handleSelect = (index) => { const handleSelect = (index) => {
// TODO // TODO
router.push({ name: index }); router.push({ name: index });
@ -25,10 +26,15 @@
watch( watch(
() => unref(menuList), () => unref(menuList),
(value) => { (value) => {
if (value.length === 1) { if (unref(autoRouter)) {
handleSelect(value[0].name); if (value.length === 1) {
} else if (value.length === 0) { console.info('[router] menu push 1 ' + value[0].name);
handleSelect(unref(activeAside)); handleSelect(value[0].name);
} else if (value.length === 0) {
console.info('[router] menu push 0 ' + unref(activeAside));
handleSelect(unref(activeAside));
}
store.commit('layout/setAutoRouter', false);
} }
}, },
{ {

@ -32,7 +32,8 @@
() => unref(tabList), () => unref(tabList),
(value) => { (value) => {
if (!unref(activeTab) || value.findIndex((item) => item.name === unref(activeTab)) === -1) { if (!unref(activeTab) || value.findIndex((item) => item.name === unref(activeTab)) === -1) {
router.push({ name: value[0].name }); console.info('[router] tabs push ' + value[0].fullPath);
router.push(value[0].fullPath);
} }
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }
@ -67,6 +68,7 @@
<style lang="less" scoped> <style lang="less" scoped>
.layout-tabs { .layout-tabs {
width: 100%; width: 100%;
flex-shrink: 0;
height: @layout-tabs-height; height: @layout-tabs-height;
padding: 0 @layout-space-large; padding: 0 @layout-space-large;
display: flex; display: flex;
@ -145,7 +147,7 @@
} }
} }
.operation { .operation {
height: 80%; height: 100%;
margin-left: @layout-space; margin-left: @layout-space;
display: flex; display: flex;
align-items: center; align-items: center;

@ -6,6 +6,7 @@ export default [
meta: { meta: {
title: '组件示例', title: '组件示例',
icon: 'home-fill', icon: 'home-fill',
layout: true,
}, },
children: [ children: [
{ {

@ -22,10 +22,10 @@ export const globalRoutes = [
// 示例模块 // 示例模块
import demoModule from './demo'; import demoModule from './demo';
export const demeRoutes = import.meta.env.DEV ? demoModule : []; export const demoRoutes = import.meta.env.DEV ? demoModule : [];
// 动态模块 // 动态模块
const dynamicRoutes = []; export const dynamicRoutes = [];
const modules = import.meta.globEager('./modules/*.js'); const modules = import.meta.globEager('./modules/*.js');
Object.values(modules).forEach((mod) => { Object.values(modules).forEach((mod) => {
dynamicRoutes.push(...mod.default); dynamicRoutes.push(...mod.default);
@ -41,6 +41,8 @@ export const routes = [
component: () => import('@/layouts/default.vue'), component: () => import('@/layouts/default.vue'),
meta: { meta: {
layout: true, layout: true,
title: '首页',
icon: 'home-fill',
}, },
children: [ children: [
{ {
@ -55,6 +57,7 @@ export const routes = [
], ],
}, },
...dynamicRoutes, ...dynamicRoutes,
...demoRoutes,
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
redirect: '/404', redirect: '/404',
@ -100,7 +103,7 @@ router.beforeEach(async (to, from, next) => {
next({ name: childName }); next({ name: childName });
} else { } else {
next(); next();
store.commit('layout/setActiveAside', to.matched.find((item) => !item.meta?.layout).name); store.commit('layout/setActiveAside', to.matched[0].name);
store.commit('layout/setActiveMenu', to.name); store.commit('layout/setActiveMenu', to.name);
store.commit('layout/setActiveTab', to.name); store.commit('layout/setActiveTab', to.name);
store.commit('layout/setBreakcrumbList', to.matched); store.commit('layout/setBreakcrumbList', to.matched);
@ -115,23 +118,56 @@ router.beforeEach(async (to, from, next) => {
}); });
export default router; export default router;
/**
export const reset = (routers) => { * 多级嵌套路由时由于父级路由页面中没有router-view组件所以子级路由页面无法显示
* 所以需要将路由规则展平处理让所有页面路由规则都是平级
* @param {*} routes
* @param {*} parent
*/
const flatRoutes = (routes, parent = { path: '/', meta: {} }) =>
routes.flatMap((route) => {
let res = [];
if (!route.path.startsWith('/')) {
if (!parent.path.endsWith('/')) {
parent.path += '/';
}
route.path = parent.path + route.path;
}
route.meta.menu = route.path.replaceAll(/\/:[^?]+\?/g, '');
let activeMenu = route.meta.activeMenu;
if (!activeMenu && route.meta.hidden) {
activeMenu = parent.meta.menu;
}
route.meta.activeMenu = activeMenu || route.meta.menu;
const children = flatRoutes(route.children || [], route);
route.redirect = children.find((item) => !item.meta.hidden)?.menu;
if (route.meta.layout) {
route.children = children;
res = [route];
} else {
res = [route, ...children];
}
return res;
});
export const reset = (routes) => {
router.getRoutes().forEach((item) => { router.getRoutes().forEach((item) => {
router.removeRoute(item.name); router.removeRoute(item.name);
}); });
routers = [ if (!config.useLocalRouter) {
...globalRoutes, routes = [
...routers, ...globalRoutes,
{ ...routes,
path: '/:pathMatch(.*)*', ...demoRoutes,
redirect: '/404', {
name: 'NotFound', path: '/:pathMatch(.*)*',
meta: { redirect: '/404',
global: true, name: 'NotFound',
meta: {
global: true,
},
}, },
}, ];
]; }
routers.forEach(router.addRoute); flatRoutes(routes).forEach(router.addRoute);
console.info('[router] reset', router.getRoutes()); console.info('[router] reset', router.getRoutes());
}; };

@ -1,286 +0,0 @@
export default [
{
path: '/education',
name: 'Education',
component: () => import('@/layouts/default.vue'),
meta: {
title: '教务教学',
icon: 'book-open-fill',
},
children: [
{
path: 'teacher',
name: 'TeacherManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '老师管理',
icon: 'user-shared-fill',
},
},
{
path: 'student',
name: 'StudentManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '学生管理',
icon: 'user-received-fill',
},
children: [
{
path: 'list',
name: 'StudentManagementList',
component: () => import('@/views/home/index.vue'),
meta: {
title: '学生列表',
icon: 'user-received-line',
},
},
{
path: 'grade',
name: 'GradeManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '年级管理',
icon: 'account-pin-box-fill',
},
},
{
path: 'class',
name: 'ClassManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '班级管理',
icon: 'account-pin-circle-fill',
},
},
],
},
{
path: 'admission',
name: 'AdmissionManagement',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '入学',
icon: 'award-fill',
},
children: [
{
path: 'evaluation',
name: 'AdmissionEvaluation',
component: () => import('@/views/home/index.vue'),
meta: {
title: '入学评测',
icon: 'file-paper-2-fill',
},
},
{
path: 'way',
name: 'StudyWay',
component: () => import('@/views/home/index.vue'),
meta: {
title: '学习路线',
icon: 'send-plane-fill',
},
},
],
},
{
path: 'answer',
name: 'Answer',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑',
icon: 'question-answer-fill',
},
children: [
{
path: 'management',
name: 'AnswerManagement',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑管理',
icon: 'question-answer-fill',
},
children: [
{
path: 'list',
name: 'AnswerManagementList',
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑管理列表',
icon: 'question-answer-line',
},
},
{
path: 'invalid',
name: 'InvalidQuestionManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '无效追问管理',
icon: 'questionnaire-fill',
},
},
{
path: 'lock',
name: 'UserLockManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '用户锁定管理',
icon: 'lock-fill',
},
},
],
},
{
path: 'assign/:questionId',
name: 'AssignAnswerTeacher',
component: () => import('@/views/home/index.vue'),
meta: {
title: '指派老师',
icon: 'account-box-fill',
hidden: true,
activeMenu: '/education/answer/management',
},
},
{
path: 'teacher',
name: 'AnswerTeacher',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑老师管理',
icon: 'book-line',
},
children: [
{
path: 'summary',
name: 'AnswerTeacherSummary',
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑老师统计',
icon: 'book-2-line',
},
},
{
path: 'management',
name: 'AnswerTeacherManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '答疑老师管理',
icon: 'book-3-line',
},
},
{
path: 'bind/:teacher?',
name: 'AnswerTeacherUpdateBind',
component: () => import('@/views/home/index.vue'),
meta: {
title: '编辑课程绑定',
icon: 'book-fill',
hidden: true,
activeMenu: '/education/answer/teacher/management',
},
},
{
path: 'course',
name: 'AnswerTeacherBindCourse',
component: () => import('@/views/home/index.vue'),
meta: {
title: '绑定课程管理',
icon: 'book-fill',
},
},
],
},
],
},
{
path: 'question',
name: 'QuestionManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '题库管理',
icon: 'brush-fill',
},
},
{
path: 'note',
name: 'NoteManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '笔记管理',
icon: 'sticky-note-2-fill',
},
},
{
path: 'research',
name: 'CourseResearch',
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue'),
meta: {
title: '课研更新',
icon: 'contacts-book-upload-fill',
},
children: [
{
path: 'plan',
name: 'CourseResearchPlan',
component: () => import('@/views/home/index.vue'),
meta: {
title: '课研计划',
icon: 'contacts-book-upload-line',
},
},
{
path: 'log',
name: 'CourseResearchLog',
component: () => import('@/views/home/index.vue'),
meta: {
title: '课研更新日志',
icon: 'file-copy-2-line',
},
},
],
},
{
path: 'live',
name: 'LiveManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '直播管理',
icon: 'live-fill',
},
},
{
path: 'material',
name: 'MaterialManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '老师资料管理',
icon: 'database-2-fill',
},
},
{
path: 'shift',
name: 'ShiftTable',
component: () => import('@/views/home/index.vue'),
meta: {
title: '排班表',
icon: 'calendar-check-line',
},
},
{
path: 'course',
name: 'CourseTable',
component: () => import('@/views/home/index.vue'),
meta: {
title: '课程表管理',
icon: 'calendar-todo-fill',
},
},
],
},
];

@ -6,43 +6,39 @@ export default [
meta: { meta: {
title: '系统管理', title: '系统管理',
icon: 'settings-3-fill', icon: 'settings-3-fill',
layout: true,
}, },
children: [ children: [
{ {
path: 'log', path: 'user',
name: 'LogManagement', name: 'UserManagement',
component: () => import('@/views/home/index.vue'), component: () => import('@/views/system/user/index.vue'),
meta: { meta: {
title: '日志管理', title: '用户管理',
icon: 'file-chart-fill', icon: 'user-1-fill',
},
},
{
path: 'app',
name: 'AppManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: 'APP包管理',
icon: 'app-store-fill',
},
},
{
path: 'notify',
name: 'NotifyManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '通知管理',
icon: 'alarm-warning-fill',
},
},
{
path: 'file',
name: 'FileManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '文件中台',
icon: 'file-copy-fill',
}, },
children: [
{
path: 'create',
name: 'CreateUser',
component: () => import('@/views/system/user/form.vue'),
meta: {
title: '创建用户',
icon: 'user-1-fill',
hidden: true,
},
},
{
path: 'update/:id',
name: 'UpdateUser',
component: () => import('@/views/system/user/form.vue'),
meta: {
title: '编辑用户',
icon: 'user-1-fill',
hidden: true,
},
},
],
}, },
], ],
}, },

@ -1,49 +0,0 @@
export default [
{
path: '/user',
name: 'UserManagement',
component: () => import('@/layouts/default.vue'),
meta: {
title: '用户管理',
icon: 'folder-user-fill',
},
children: [
{
path: 'consult',
name: 'ConsultManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '咨询管理',
icon: 'kakao-talk-fill',
},
},
{
path: 'course',
name: 'CourseConsultManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '咨询课程',
icon: 'chat-smile-2-fill',
},
},
{
path: 'account',
name: 'AccountManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '账号课程',
icon: 'account-box-fill',
},
},
{
path: 'feedback',
name: 'FeedbackManagement',
component: () => import('@/views/home/index.vue'),
meta: {
title: '意见反馈',
icon: 'feedback-fill',
},
},
],
},
];

@ -1,6 +1,6 @@
import { getUserInfo, getPermission, sendSmsCode, login } from '@/api/auth'; import { getUserInfo, getPermission, sendSmsCode, login } from '@/api/auth';
import { ElMessage } from '@/plugins/element-plus'; import { ElMessage } from '@/plugins/element-plus';
import router, { reset as resetRoutes, demeRoutes } from '@/router'; import router, { reset as resetRoutes } from '@/router';
import config from '@/configs'; import config from '@/configs';
const viewComponents = import.meta.glob('../../views/**/*.vue'); const viewComponents = import.meta.glob('../../views/**/*.vue');
const state = () => ({ const state = () => ({
@ -39,6 +39,7 @@ const mutations = {
if (route.component) { if (route.component) {
if (route.component === 'Layout') { if (route.component === 'Layout') {
route.component = () => import('@/layouts/default.vue'); route.component = () => import('@/layouts/default.vue');
route.meta.layout = true;
} else { } else {
let path = `../../views/${route.component}`; let path = `../../views/${route.component}`;
if (!path.endsWith('.vue')) { if (!path.endsWith('.vue')) {
@ -57,7 +58,6 @@ const mutations = {
}); });
}; };
data = config.useLocalRouter ? data : convert(data); data = config.useLocalRouter ? data : convert(data);
data.push(...demeRoutes);
state.permission = data; state.permission = data;
resetRoutes(state.permission); resetRoutes(state.permission);
}, },

@ -7,14 +7,16 @@ const state = () => ({
tabList: [], tabList: [],
activeTab: null, activeTab: null,
breakcrumbList: [], breakcrumbList: [],
autoRouter: false,
}); });
const getters = { const getters = {
asideList: (state, getters, rootState) => { asideList: (state, getters, rootState) => {
const deep = (arr) => return rootState.auth.permission.filter((item) => item.meta.global !== true);
arr.flatMap((item) => (item.meta?.layout ? deep(item.children || []) : item.meta?.global ? [] : item));
return deep(rootState.auth.permission);
}, },
collapseMenu: (state) => state.collapseMenu || state.menuList.length < 2, menuList: (state) => {
return state.menuList.filter((item) => item.meta.hidden !== true);
},
collapseMenu: (state, getters) => state.collapseMenu || getters.menuList.length < 2,
activeAsideName: (state, getters) => getters.asideList.find((item) => item.name === state.activeAside)?.meta.title, activeAsideName: (state, getters) => getters.asideList.find((item) => item.name === state.activeAside)?.meta.title,
}; };
const mutations = { const mutations = {
@ -26,6 +28,7 @@ const mutations = {
let oldIndex = state.tabList.findIndex((item) => item.name === data.name); let oldIndex = state.tabList.findIndex((item) => item.name === data.name);
let tab = null; let tab = null;
if (oldIndex !== -1) { if (oldIndex !== -1) {
tab = state.tabList[oldIndex];
tab.fullPath = data.fullPath; tab.fullPath = data.fullPath;
} else { } else {
tab = _.cloneDeep(data); tab = _.cloneDeep(data);
@ -38,16 +41,17 @@ const mutations = {
} else { } else {
if (reverse) { if (reverse) {
let tab = state.tabList[index]; let tab = state.tabList[index];
router.push({ name: tab.name }); router.push(tab.fullPath);
state.tabList = [tab]; state.tabList = [tab];
} else if (state.tabList.length > 1) { } else if (state.tabList.length > 1) {
state.tabList.splice(index, 1); state.tabList.splice(index, 1);
router.push({ name: state.tabList[Math.min(index, state.tabList.length - 1)].name }); router.push(state.tabList[Math.min(index, state.tabList.length - 1)].fullPath);
} }
} }
}, },
setActiveTab: (state, data) => (state.activeTab = data), setActiveTab: (state, data) => (state.activeTab = data),
setBreakcrumbList: (state, data) => (state.breakcrumbList = data), setBreakcrumbList: (state, data) => (state.breakcrumbList = data),
setAutoRouter: (state, data) => (state.autoRouter = data),
}; };
const actions = {}; const actions = {};
export default { export default {

@ -47,11 +47,16 @@
@layout-header-bgc: @color-white; @layout-header-bgc: @color-white;
@layout-header-fc: @color-black; @layout-header-fc: @color-black;
@layout-tabs-height: 50px; @layout-tabs-height: 40px;
@layout-footer-height: @layout-h1;
@layout-footer-bgc: @color-white; @layout-footer-bgc: @color-white;
@layout-footer-fc: @color-black; @layout-footer-fc: @color-black;
@layout-main-height: calc(
100vh - @layout-header-height - @layout-tabs-height - @layout-space-large * 2 - @layout-footer-height
);
:export { :export {
layoutAsideWidth: @layout-aside-width; layoutAsideWidth: @layout-aside-width;
layoutLogoSize: calc(@layout-header-height * 0.72); layoutLogoSize: calc(@layout-header-height * 0.72);

@ -0,0 +1,20 @@
<template>
<div class="form-container">
<el-form :model="form" :rules="rules" label-width="100px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</el-form-item>
</el-form>
</div>
</template>
<script setup>
const form = reactive({
username: null,
});
const rules = reactive({
username: [{ required: true }],
});
</script>
<style lang="less" scoped></style>

@ -21,6 +21,7 @@
</template> </template>
<script setup lang="jsx"> <script setup lang="jsx">
const router = useRouter();
const store = useStore(); const store = useStore();
const loading = computed(() => store.state.user.loading); const loading = computed(() => store.state.user.loading);
const list = computed(() => store.state.user.list); const list = computed(() => store.state.user.list);
@ -34,11 +35,11 @@
const handleSearch = (page) => { const handleSearch = (page) => {
store.dispatch('user/search', { ...page, ...state.condition }); store.dispatch('user/search', { ...page, ...state.condition });
}; };
const handleCreate = (row) => { const handleCreate = () => {
alert('create ' + row.id); router.push({ name: 'CreateUser' });
}; };
const handleUpdate = (row) => { const handleUpdate = (row) => {
alert('update ' + row.id); router.push({ name: 'UpdateUser', params: { id: row.id } });
}; };
const handleRemove = (rows) => { const handleRemove = (rows) => {
alert('delete ' + rows.map((item) => item.id).join(',')); alert('delete ' + rows.map((item) => item.id).join(','));

Loading…
Cancel
Save