在后台管理的项目中来说,一般会分 头部、侧边栏、和内容区域三个部分
在项目根目录下创建 layout
文件夹,完成这个三个部分的组件,并将这些组件组合在一起
新建 layout/index.vue
文件,搭建项目的基本框架,为了方便查看效果,把 src/App.vue
的 template
只保留 el-config-provider
和 router-view
,其他删除,script
也做对应的删除,style
内设置 #app
的样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <el-config-provider :locale="zhCn"> <router-view /> </el-config-provider> </template>
<script setup lang="ts"> import zhCn from "element-plus/lib/locale/lang/zh-cn"; </script>
<style lang="scss"> #app { width: 100vw; height: 100vh; } </style>
|
在 src/router/index.ts
中把 path: "/"
的路由的 component
改成 Layout
src/router/index.ts
1 2 3 4 5 6 7 8
| import Layout from "@/layout/index.vue"; const routes: Array<RouteRecordRaw> = [ { path: "/", name: "Home", component: Layout, } ];
|
layout – 布局
在 src/layout/index.vue
中,使用 element-plus的Container 布局容器
需要用到的组件有:el-container
、el-header
、el-aside
、el-main
,在 src/theme/index.ts
中引入这些组件。然后就可以在 src/layout/index.vue
中使用了。
src/theme/index.ts
1 2 3 4 5 6 7 8
| import { ElContainer } from "element-plus"; export default (app: App): void => { app.use(ElContainer); };
|
在 src/layout/index.vue
中,应该把头部、侧边栏和内容区域的宽高都划分好
src/layout/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <el-container class="layout" style="background: red"> <el-header style="background: green"></el-header> <el-container> <el-aside style="background: blue"></el-aside> <el-main style="background: yellow"></el-main> </el-container> </el-container> </template>
<script lang="ts" setup> </script>
<style lang="scss" scoped> .layout { width: 100%; height: 100%; } </style>
|
打开浏览器就可以看到划分好的区域了

接下来就把头部和侧边栏提取到单独的组件中,内容区域展示的子路由的内容所以要换成 router-view
在 src/layout
下新建 Header.vue
和 Aside.vue
src/layout/Header.vue
1 2 3 4 5 6 7 8 9
| <template> <el-header></el-header> </template>
<script> export default { name: "LayoutHeader", }; </script>
|
src/layout/Aside.vue
1 2 3 4 5 6 7 8 9
| <template> <el-aside></el-aside> </template>
<script> export default { name: "LayoutAside", }; </script>
|
在 src/layout/index.vue
中引入这两个组件
src/layout/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <el-container class="layout" direction="vertical"> <layout-header style="background: green"></layout-header> <el-container> <layout-aside style="background: blue"></layout-aside> <el-main> <router-view /> </el-main> </el-container> </el-container> </template>
<script lang="ts" setup> import LayoutAside from "./Aside.vue"; import LayoutHeader from "./Header.vue"; </script>
<style lang="scss" scoped> .layout { width: 100%; height: 100%; } </style>
|
Aside – 侧边栏
在侧边栏中需要提供模块菜单,这里需要用到 el-menu
组件,在 src/theme/index.ts
中引入
src/theme/index.ts
1 2 3 4 5 6 7 8
| import { ElMenu, } from "element-plus"; export default (app: App): void => { app.use(ElMenu); };
|
src/layout/aside.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| <template> <el-aside> <el-menu :default-active="defaultActive" @open="handleOpen" @close="handleClose" > <template v-for="(item, index) in menus" :key="index"> <el-sub-menu :index="`${index}`" v-if="item.children"> <template #title> <i v-if="item.icon" :class="item.icon"></i> <span>{{ item.name }}</span> </template> <el-menu-item v-for="(elItem, elIndex) in item.children" :key="elIndex" :index="`${index}-${elIndex}`" > <i v-if="elItem.icon" :class="item.icon"></i> {{ elItem.name }} </el-menu-item> </el-sub-menu> <el-menu-item v-else> <i v-if="item.icon" :class="item.icon"></i> {{ item.name }} </el-menu-item> </template> </el-menu> </el-aside> </template>
<script lang="ts"> export default { name: "LayoutAside", }; </script> <script lang="ts" setup> import { computed } from "vue"; const menus = [ { name: "导航一", icon: "el-icon-location", children: [ { name: "导航五", icon: "el-icon-document", }, { name: "导航六", icon: "el-icon-settings", }, ], }, { name: "导航二", icon: "el-icon-menu", children: [ { name: "导航三", icon: "el-icon-document", }, { name: "导航四", icon: "el-icon-settings", }, ], }, ];
const hasChildren = (item: Record<string, any>) => !!item.children; const getDefaultActive = (list: Record<string, any>[], result = "0") => { const [item] = list; if (hasChildren(item)) { result += "-0"; getDefaultActive(item.children, result); } return result; };
const defaultActive = computed(() => getDefaultActive(menus)); </script>
|
然后需要在 views
下创建菜单对应的文件夹,在 router
下也要创建对应的文件,每个模块都需要做这样重复的工作,这样的工作,应该让程序自动来做,写一个脚本来完成这样的工作。
create-module脚本
在项目根目录下创建一个 scripts
文件夹存放自定义的脚本
在 scripts
下创建 create-module
文件夹,在文件夹中创建 create-module.js
,在这个文件中处理模块,需要知道有哪些模块,所以还需要创建一个 module.json
文件
在 module.json
文件中,会出现下列字段
字段名 | 数据类型 | 备注 |
---|
name | string | 路由的 path 和 name ,子路由的 name 是 父路由子父路由拼接起来的 |
title | string | 左侧菜单展示的名称,页面 title |
children | array | 子路由,子菜单 |
show | boolean | 是否展示在左侧菜单中 |
redirect | string | 重定向的路由 |
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| [ { "name": "project", "title": "项目列表", "redirect": "/project/index", "children": [ { "name": "index", "title": "项目列表", "show": false }, { "name": "lookTests", "title": "查看关联项目集", "show": false } ] } ]
|
需要在脚本中使用 eslint
所以需要安装 eslint
版本 7.0
以上的
1 2 3
| npm install eslint --save-dev
yarn add eslint --dev
|
生成views下目录和文件
- 需要一个方法创建不存在的文件夹
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| const fs = require("fs"); const path = require("path"); const { ESLint } = require("eslint"); const modules = require("./module.json");
const viewsPath = path.join(__dirname, "../../src/views/"); const routerPath = path.join(__dirname, "../../src/router"); const routesPath = path.join(routerPath, "/routes");
const getCreateFolder = (basePath) => { return async (folderName) => { const folderPath = path.join(basePath, folderName); try { const res = await fs.promises.stat(folderPath); if (!res.isDirectory()) { fs.promises.mkdir(folderPath); } } catch (err) { fs.promises.mkdir(folderPath, { recursive: true }); } }; };
const createViewFolder = getCreateFolder(viewsPath);
|
- 创建vue模版
scripts/create-module/template/vue
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <div>#{name}</div> </template>
<script> export default { name: "#{name}", }; </script>
<style lang="scss" scoped></style>
|
- 需要一个方法创建不存在的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
const getCreateFile = (basePath, ext) => { const templatePath = path.join(__dirname, "/template/", ext.slice(1)); return async (fileName) => { const filePath = path.join(basePath, `${fileName}${ext}`); const name = fileName.replace(/\/\w/gi, (res) => res.slice(1).toUpperCase() ); const write = async () => { const templateStr = await fs.promises.readFile(templatePath, { encoding: "utf8", }); await fs.promises.writeFile( filePath, templateStr.replace(/#{name}/g, name) ); }; try { const res = await fs.promises.stat(filePath); if (!res.isFile()) { write(); } } catch (err) { write(); } }; };
const createViewFile = getCreateFile(viewsPath, ".vue");
|
- 递归配置的模块创建views下的目录和文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
} views 创建的模块
const createViews = (views, basePath = "/") => { const isFolder = !!views.children; if (isFolder) { createViewFolder(basePath + views.name); } else { createViewFile(basePath + views.name); } if (views.children) { for (let i = 0; i < views.children.length; i++) { const element = views.children[i]; createViews(element, basePath + views.name + "/"); } } };
|
生成router/routes 下的文件
- 创建 routes 文件夹
1 2 3 4
|
const createRouteFolder = getCreateFolder(routesPath);
|
- 创建 routes 下文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
|
const joinStr = (str, ...arg) => { return (str ?? "") + arg.map((item) => item[0].toUpperCase() + item.slice(1)); };
} routes 创建的模块
const getRouteTmp = (route, isParent, parentName) => { const path = isParent ? "/" + route.name : route.name; const name = isParent ? route.name : joinStr(parentName, route.name); const componentPath = isParent ? route.name : `${parentName}/${route.name}`; const isRedirect = route.redirect; const component = `component: () => import(/* webpackName: "${name}" */"@/views/${componentPath}.vue"),`; const redirect = `redirect: "${isRedirect}",component: () => import(/* webpackName: "Layout" */"@/layout/index.vue"),`; return `{ path: "${path}", name: "${name}", ${isRedirect ? redirect : component} meta: { title: "${route.title}" }, ${ route.children?.length ? `children: [ ${route.children ?.map((item) => getRouteTmp(item, false, route.name)) .join(",")} ] ` : "" } } `; };
} routes 创建的模块
const createRouteFile = async (routes) => { const filePath = path.join(routesPath, routes.name + ".ts"); const str = `export default ${getRouteTmp(routes, true)};`; const write = async () => { const results = await eslint.lintText(str); await fs.promises.writeFile(filePath, results[0].output); }; try { const res = await fs.promises.stat(filePath); if (!res.isFile()) { write(); } } catch (err) { write(); } };
|
- 创建routes/index.ts 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
const createRouteIndexFile = async () => { let files = await fs.promises.readdir(routesPath); files = files .filter((file) => { return file !== "index.ts" && file.endsWith(".ts"); }) .map((file) => file.slice(0, -3)); let str = files .map((file) => `import ${file}Routes from "./${file}";`) .join(""); str += ` export default [ ${files.map((file) => file + "Routes").join(",")} ] `; const results = await eslint.lintText(str); await fs.promises.writeFile( path.join(routesPath, "./index.ts"), results[0].output ); };
|
生成侧边栏的菜单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const menuPath = path.join(__dirname, "../../src/layout"); const getMenu = (menu, parentMenu = "/") => { const children = menu.children?.filter((item) => item.show !== false) || []; const route = parentMenu + menu.name; return { name: menu.title, route: menu.redirect || parentMenu + menu.name, icon: menu.icon, children: children.length ? children.map((item) => getMenu(item, `${route}/`)) : null, }; };
const createMenuFile = async () => { const filePath = path.join(menuPath, "./menu.json"); const menus = modules .filter((menu) => menu.show !== false) .map((menu) => getMenu(menu)); await fs.promises.writeFile(filePath, JSON.stringify(menus, null, 4)); };
|
创建一个入口函数
1 2 3 4 5 6 7 8 9 10 11
| const run = async () => { await createRouteFolder("/"); for (const item of modules) { createViews(item); await createRoutes(item); } await createRouteIndexFile(); createMenuFile(); };
run();
|
在 packgae.json
中添加一个 script
命令
1
| "create:module": "node ./scripts/create-module/create-module.js"
|
执行命令
1 2 3
| yarn create:module
npm run create:module
|
在 src/router/index.ts
中引入创建的路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import Routes from "./routes"; const routes: Array<RouteRecordRaw> = [ { path: "/", name: "Home", component: Layout, }, { path: "/about", name: "About", component: () => import( "../views/About.vue"), }, ...Routes, ];
|
在 src/layout/Aside.vue
中引入创建的菜单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <script lang="ts" setup> import { computed } from "vue"; import menus from "./menu.json";
const hasChildren = (item: Record<string, any>) => !!item.children; const getDefaultActive = (list: Record<string, any>[], result = "0") => { const [item] = list; if (hasChildren(item)) { result += "-0"; getDefaultActive(item.children, result); } return result; };
const defaultActive = computed(() => getDefaultActive(menus)); </script>
|
在 tsconfig.json
中添加 resolveJsonModule
1 2 3 4 5
| { "compilerOptions": { "resolveJsonModule": true } }
|
现在就可以看到页面的菜单了,但是点击菜单没有路由跳转
添加菜单跳转
Aside.vue 文件中, el-menu
组件需要添加 router
prop,el-menu-item
需要 route
prop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <template> <el-aside> <el-menu :default-active="defaultActive" router > <template v-for="(item, index) in menus" :key="index"> <el-sub-menu :index="`${index}`" v-if="item.children"> <template #title> <i v-if="item.icon" :class="item.icon"></i> <span>{{ item.name }}</span> </template> <el-menu-item v-for="(elItem, elIndex) in item.children" :key="elIndex" :index="`${index}-${elIndex}`" :route="elItem.route" > <i v-if="elItem.icon" :class="item.icon"></i> {{ elItem.name }} </el-menu-item> </el-sub-menu> <el-menu-item v-else :index="`${index}`" :route="item.route"> <i v-if="item.icon" :class="item.icon"></i> {{ item.name }} </el-menu-item> </template> </el-menu> </el-aside> </template>
|
在配置一下路由 /
重定向到 /project/index
src/router/index.ts
1 2 3 4 5 6 7 8 9
| const routes: Array<RouteRecordRaw> = [ { path: "/", name: "Home", component: Layout, redirect: "/project/index", }, ];
|
完结
项目已经上传到 github 和 gitee
GitHub: https://github.com/wukang0718/cli-create-project
Gitee: https://gitee.com/wu_kang0718/cli-create-project