第六篇-项目展示基本框架

在后台管理的项目中来说,一般会分 头部、侧边栏、和内容区域三个部分

在项目根目录下创建 layout 文件夹,完成这个三个部分的组件,并将这些组件组合在一起

新建 layout/index.vue 文件,搭建项目的基本框架,为了方便查看效果,把 src/App.vuetemplate 只保留 el-config-providerrouter-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-containerel-headerel-asideel-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>

打开浏览器就可以看到划分好的区域了

image-20210913133624904

接下来就把头部和侧边栏提取到单独的组件中,内容区域展示的子路由的内容所以要换成 router-view

src/layout 下新建 Header.vueAside.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 文件中,会出现下列字段

字段名数据类型备注
namestring路由的 pathname,子路由的 name 是 父路由子父路由拼接起来的
titlestring左侧菜单展示的名称,页面 title
childrenarray子路由,子菜单
showboolean是否展示在左侧菜单中
redirectstring重定向的路由

示例

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
# or
yarn add eslint --dev

生成views下目录和文件

  1. 需要一个方法创建不存在的文件夹
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");

/**
* 工厂函数,创建以恶搞创建文件夹的方法
* @param {string} basePath 创建文件夹的基础路径
* @returns { function } 返回一个创建文件夹的具体方法
*/
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 });
}
};
};

/**
* 创建 views 下的文件夹
*/
const createViewFolder = getCreateFolder(viewsPath);
  1. 创建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. 需要一个方法创建不存在的文件
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
/**
* 工厂函数返回实际创建文件的方法
* @param {string} basePath 创建文件的基本路径
* @param {string} ext 文件后缀名
* @returns {function} 返回实际创建文件的方法
*/
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");
  1. 递归配置的模块创建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 下的文件夹和文件夹
* @param {{
* name: "product",
* title: "项目",
* children: []
* }} views 创建的模块
* @param {string} basePath 基于src/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 下的文件

  1. 创建 routes 文件夹
1
2
3
4
/**
* 创建 router/routes 下的文件夹
*/
const createRouteFolder = getCreateFolder(routesPath);
  1. 创建 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
/**
* 拼接字符串,从第二个参数开始,首字母大写
* @param {string} str
* @param {...string} arg
* @returns string
*/
const joinStr = (str, ...arg) => {
return (str ?? "") + arg.map((item) => item[0].toUpperCase() + item.slice(1));
};

/**
* 获取单个route的配置
* @param {{
* name: "product",
* title: "项目",
* children: []
* }} routes 创建的模块
* @param isParent {boolean} 是否是顶级的路由
* @param parentName {string} 父路由的 name/path
*/
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 下的文件
* @param {{
* name: "product",
* title: "项目",
* children: []
* }} routes 创建的模块
*/
const createRouteFile = async (routes) => {
const filePath = path.join(routesPath, routes.name + ".ts");
const str = `export default ${getRouteTmp(routes, true)};`;
const write = async () => {
// 先用 eslint 修改一下代码格式
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();
}
};
  1. 创建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
/**
* 创建 routes 下的index.ts 导出所有的路由
*/
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(",")}
]
`;
// 先用 eslint 修改一下代码格式
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
# or
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(/* webpackChunkName: "about" */ "../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