前端项目初始化
初始化前端项目,并开发一个通用模板。
使用的前端脚手架是 Vue Cli:https://cli.vuejs.org/zh/
初始化
安装Vue Cli
npm install -g @vue/cli
检测版本
vue -V
创建项目
vue create ojsystem-frontend
手动选择
按照图中的选择。
尝试运行项目
运行成功界面
前端项目工程化配置
Vue Cli 脚手架已经配置了代码美化、校验、格式化等插件,无需自行配置,但是需要自己在编辑器WebStorm中开启代码美化插件,否则使用快捷键格式化代码后,vue cli 脚手架 会提示格式错误,是因为 vue cli 和 目前编辑器的格式不一样。
prettier
eslint
自己整合 代码规范:https://eslint.org/docs/latest/use/getting-started 代码美化:https://prettier.io/docs/en/install.html 直接整合:https://github.com/prettier/eslint-plugin-prettier#recommended-configuration(包括了 https://github.com/prettier/eslint-config-prettier#installation)
Git 配置
vue cli 初始化这个项目的时候已经初始化了本地仓库,只需要在 github/gitee 上创建对应的远程仓库,然后配置一下就好。
在 gitee 上创建对应的 ojsystem-frontend 远程仓库
给本地仓库添加远程仓库
git remote add origin git@gitee.com:miykah/ojsystem-frontend.git
引入组件
这次组件库使用字节开源的 Acro Design:https://arco.design/
使用其中的vue组件库:https://arco.design/vue/docs/start
参考文档安装
npm install --save-dev @arco-design/web-vue
引入项目
可以选择全局引入(项目需要瘦身优化的时候,可以选择按需引入)
测试是否引入成功
随便找一个组件引入到页面。
引入一个日历组件到 App.vue
项目初始化模板设计
项目通用布局
创建一个layouts目录,及一个
BasicLayout.vue
文件。引入 Acro Design 中最基础的 layout组件。
<template>
<div id="basicLayout">
<a-layout style="height: 400px">
<a-layout-header class="header">
<GlobalHeader />
</a-layout-header>
<a-layout-content class="content">
<router-view />
</a-layout-content>
<a-layout-footer class="footer">
<a href="https://blog.miykah.top" target="_blank">miykah's blog</a>
</a-layout-footer>
</a-layout>
</div>
</template>
<style>
#basicLayout {
}
#basicLayout .header {
margin-bottom: 16px;
box-shadow: #eee 1px 1px 5px;
}
#basicLayout .content {
background: linear-gradient(to right, #fefefe, #fff);
margin-bottom: 16px;
padding: 20px;
}
#basicLayout .footer {
/*background: #efefef;*/
padding: 12px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
box-shadow: #aaa 1px 1px 5px;
}
</style>
<script>
import GlobalHeader from "@/components/GlobalHeader";
export default {
components: { GlobalHeader },
};
</script>
删除
App.vue
中多余的代码。引入这个 BasicLayout
<template>
<div id="app">
<BasicLayout />
</div>
</template>
<style>
#app {
}
</style>
<script>
import BasicLayout from "@/layouts/BasicLayout";
export default {
components: { BasicLayout },
};
</script>
通用的路由菜单栏
提取通用的路由文件
vue cli 的路由默认是写在了 index.ts
中,可以单独抽取出其中的路由部分,新建一个routes.ts
文件。
import { RouteRecordRaw } from "vue-router";
import HomeView from "@/views/HomeView.vue";
export const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "题库",
component: HomeView,
},
{
path: "/about",
name: "关于",
component: () =>
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
},
];
创建 GlobalHeader.vue
在 components 目录创建GlobalHeader.vue
,作为通用的菜单栏,里面引入 Acro 的菜单栏组件。并实现动态路由,可以动态高亮菜单。
<template>
<div id="globalHeader">
<a-menu
mode="horizontal"
:selected-keys="selectedKeys"
@menu-item-click="doMenuClick"
>
<!--这里是logo以及title-->
<a-menu-item
key="0"
:style="{ padding: 0, marginRight: '38px' }"
disabled
>
<div class="title-bar">
<img class="logo" src="../assets/oj-logo.png" />
<div class="title">OJ-System</div>
</div>
</a-menu-item>
<!--菜单项,动态的,使用v-for遍历-->
<a-menu-item v-for="item in routes" :key="item.path">
{{ item.name }}
</a-menu-item>
</a-menu>
</div>
</template>
<script setup lang="ts">
import { routes } from "../router/routes";
import { useRouter } from "vue-router";
import { ref } from "vue";
const router = useRouter();
// 选中的菜单项,默认选中主页"/"
const selectedKeys = ref(["/"]);
// 路由跳转后,高亮对应的菜单项
router.afterEach((to, from, failure) => {
selectedKeys.value = [to.path];
});
// 点击菜单,跳转
const doMenuClick = (key: string) => {
router.push({
path: key,
});
};
</script>
<style scoped>
.title-bar {
display: flex;
align-items: center;
}
.title {
color: #444;
margin-left: 16px;
}
.logo {
height: 48px;
}
</style>
全局状态管理
保存页面全局共享的一些变量,比如用户登陆了,需要保存用户的一些基本信息。
使用 vuex,vue cli 已经整合了 vuex
state:存储的状态信息,比如用户信息 mutation(尽量同步):定义了对变量进行增删改(更新)的方法 actions(支持异步):执行异步操作,并且触发 mutation 的更改(actions 调用 mutation) modules(模块):把一个大的 state(全局变量)划分为多个小模块,比如 user 专门存用户的状态信息
在 store 目录下 新建
user.ts
文件,保存用户信息。
// initial state
import { StoreOptions } from "vuex";
export default {
namespaced: true,
state: () => ({
loginUser: {
userName: "未登录",
role: "not login",
},
}),
actions: {
getLoginUser({ commit, state }, payload) {
commit("updateUser", payload);
},
},
mutations: {
updateUser(state, payload) {
state.loginUser = payload;
},
},
} as StoreOptions<any>;
在 store 目录下定义 index.ts 文件,导入 user 模块
import { createStore } from "vuex";
import user from "./user";
export default createStore({
mutations: {},
actions: {},
modules: {
user,
},
});
在 Vue 页面中可以获取已存储的状态变量
const store = useStore();
store.state.user?.loginUser
在 Vue 页面中可以修改状态变量
使用 dispatch 来调用之前定义好的 actions
store.dispatch("user/getLoginUser", {
userName: "miykah",
role: "admin",
});
权限管理
有的页面,必须要管理员才能访问,如果不是管理员,则需要拦截或跳转到指定页面。
给保存用户信息的全局状态添加一个 role 字段
routes.ts 中,需要管理员访问的页面,添加一个 meta,里面指定权限 access 为 admin
在全局 App.vue 中,每次路由前,判断路由的页面是否需要管理员权限 (后面优化了,不放在 App.vue中)
<template>
<div id="app">
<BasicLayout />
</div>
</template>
<style>
#app {
}
</style>
<script setup lang="ts">
import BasicLayout from "@/layouts/BasicLayout";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
const router = useRouter();
const store = useStore();
router.beforeEach((to, from, next) => {
// 判断路由的页面是否需要管理员权限
if (to.meta?.access === "admin") {
if (store.state.user.loginUser?.role !== "admin") {
next("/no-auth"); // 无权限,跳转到 no auth 页面
return;
}
next();
}
next();
});
</script>
菜单显隐
目前的效果是:
但有一些菜单,比如管理员的菜单,当用户是管理员时,才会显示出来菜单按钮,不是管理员则隐藏。
给路由增加一个meta属性,标志该路由菜单是否显示
在
GlobalHeader.vue
中,菜单项不在遍历 routes, 而是遍历过滤之后的 routes
<a-menu-item v-for="item in routes" :key="item.path">
{{ item.name }}
</a-menu-item>
改为:
<a-menu-item v-for="item in visibleRoutes" :key="item.path">
{{ item.name }}
</a-menu-item>
其中:
// 未被隐藏的菜单路由
const visibleRoutes = routes.filter((item, index) => {
return !item.meta?.hideInMenu;
});
根据权限隐藏菜单
和上一节类似,过滤掉用户没有权限的路由就可以。
这里同时优化一下权限管理,不放在
App.vue
中,单独抽取出来。
创建一个 access 目录
创建
accessEnum.ts
/**
* 权限定义
*/
const ACCESS_ENUM = {
NOT_LOGIN: "not login",
USER: "user",
ADMIN: "admin",
};
export default ACCESS_ENUM;
定义通用的权限校验方法,而不放在
App.vue
中。抽离成checkAccess.ts
import ACCESS_ENUM from "@/access/accessEnum";
/**
* 检查权限(判断当前登录用户是否具有某个权限)
* @param loginUser 当前登录用户
* @param needAccess 需要有的权限
* @return boolean 有无权限
*/
const checkAccess = (loginUser: any, needAccess = ACCESS_ENUM.NOT_LOGIN) => {
// 获取当前登录用户具有的权限(如果没有 loginUser,则表示未登录)
const loginUserAccess = loginUser?.userRole ?? ACCESS_ENUM.NOT_LOGIN;
if (needAccess === ACCESS_ENUM.NOT_LOGIN) {
return true;
}
// 如果用户登录才能访问
if (needAccess === ACCESS_ENUM.USER) {
// 如果用户没登录,那么表示无权限
if (loginUserAccess === ACCESS_ENUM.NOT_LOGIN) {
return false;
}
}
// 如果需要管理员权限
if (needAccess === ACCESS_ENUM.ADMIN) {
// 如果不为管理员,表示无权限
if (loginUserAccess !== ACCESS_ENUM.ADMIN) {
return false;
}
}
return true;
};
export default checkAccess;
根据权限隐藏菜单
// 未被隐藏的菜单路由,过滤掉被隐藏的,且没权限的菜单
const visibleRoutes = computed(() => {
return routes.filter((item, index) => {
// 路由是隐藏的
if (item.meta?.hideInMenu) {
return false;
}
// 用户无权限访问的菜单,也隐藏
if (!checkAccess(loginUser, item?.meta?.access as string)) {
return false;
}
return true;
});
});
评论区