SpringCloud+Vue+Python人工智能(fastAPI,机器学习,深度学习)前后端架构各功能实现思路——主目录(持续更新):https://blog.csdn.net/grd_java/article/details/144986730 |
---|
文章目录
注意
,因为前端业务代码重复度高,所以写多了发现,逻辑基本一样,这里直接将常用的一次引入,开发经验少的同学可能会看不懂,后面到了具体业务时,会具体讲解这些代码
1. 开发环境
1. 安装node.js的22版本。官网下载并傻瓜式安装即可,如果有更加新的版本就用最新版本之前1个版本的(例如最新是23版本,你就用22版本,最新的一般有bug)
2. 打开cmd输入
node -v
查看node.js版本,以及npm -v
查看npm的版本,如果正确显示版本号,说明安装成功
- 安装Vue Cli:是官方的vue.js项目脚手架,可以快速创建vue项目。因为vue的ui是在Vue CLI基础上封装的,所以我们必须得用
- 先安装cnpm
npm install -g cnpm --registry=https://registry.npmmirror.com/
- 再用cnpm安装vue/cli
cnpm install -g @vue/cli
2. Vue ui生成Vue项目
安装完成后,输入命令vue ui
会自动打开创建项目的页面,生成即可
包管理器使用npm,无需初始化仓库
选择手动配置
将主要功能选择上
创建项目,不保存预设
然后依次为其安装依赖
cnpm install axios
cnpm install element-plus
cnpm install sass-loader
cnpm install sass
cnpm install webpack
cnpm install svg-sprite-loader
cnpm install qs
cnpm install js-cookie
cnpm install jsencrypt
cnpm install @element-plus/icons-vue
cnpm install nprogress
cnpm install pinia
cnpm install path-browserify
cnpm install path-to-regexp
# 可选项,无需持久化,可以不引入
cnpm install pinia-plugin-persistedstate
需要的依赖(大家如果报错的话,可以和我的dependencies同步一下,避免发生冲突导致莫名其妙的报错),修改好后,删除项目中node_modules文件夹,重新执行cnpm install命令,会根据下面的依赖配置全部进行下载
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.2",
"core-js": "^3.8.3",
"element-plus": "^2.2.6",
"js-cookie": "^3.0.1",
"jsencrypt": "^3.2.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"path-to-regexp": "2.4.0",
"pinia": "^2.3.0",
"qs": "^6.10.5",
"sass": "^1.52.3",
"sass-loader": "^13.0.0",
"svg-sprite-loader": "^6.0.11",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"webpack": "^5.73.0"
},
3. 使用element plus
如何使用,其官网已经给出的很详细了
main.js中配置elementplus
//创建全局app,暴露出去
import { createApp } from 'vue'
export const app = createApp(App)
//App是主页面,必须引入
import App from './App.vue'
//路由
import router from './router'
app.use(router)
//pinia
import {createPinia} from 'pinia'
const pinia = createPinia()
// import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' //持久化
// pinia.use(piniaPluginPersistedstate)
app.use(pinia)
/**element plus start**/
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus) //引入ElementPlus
/**element plus end**/
// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
//您需要从 @element-plus/icons-vue 中导入所有图标并进行全局注册。
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
//路由导航
import '@/permission'
/**引入全局样式start**/
import '@/assets/styles/reset.css'
import '@/assets/styles/border.css'
import '@/assets/styles/index.scss'
/**引入全局样式end**/
app.mount('#app')
随便去复制点代码,测试一下
复制到About组件中
4. 常用工具类
1. auth工具类,主要是操作cookie对token进行增删改查
//使用js-cookie存储token,这里封装了很多针对cookie等浏览器存储功能
import Cookies from 'js-cookie'
//Token的key值,我们需要根据后端需要,进行修改
// const TokenKey = 'vue_admin_template_token'
const TokenKey = 'vue_admin_template_token'
//对外暴露获取token的函数
export function getToken() {
return Cookies.get(TokenKey)
}
//对外暴露设定token的函数
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
//对外暴露移除token的函数
export function removeToken() {
return Cookies.remove(TokenKey)
}
2. treeutil工具类,主要用于处理树形结构
/**
* 构造树型结构数据
* @param {*} source 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
* @param {*} rootId 根Id 默认 0
*/
export function treeData (source, id, pid, children, rootId) {
id = id || 'id'
pid = pid || 'pid'
children = children || 'children'
rootId = rootId || 0
const cloneData = JSON.parse(JSON.stringify(source))// 对源数据深度克隆
return cloneData.filter(father => {
const branchArr = cloneData.filter(child => father[id] == child[pid])// 返回每一项的子级数组
branchArr.length > 0 ? father[children] = branchArr : delete father[children]// 如果存在子级,则给父级添加一个children属性,并赋值
return father[pid] == rootId // 返回第一层
})
}
/**
* 树形结构解析,构造为普通形式
*/
export function unTreeData(source,children){
children = children || 'children'
let cloneData = JSON.parse(JSON.stringify(source))// 对源数据深度克隆
let arr = []//保存最终结果
unTreeDatadd(cloneData,children,arr)//递归解析树结构,解析结果放在arr中
return arr;//将arr返回
}
function unTreeDatadd(source,children,arr){
//遍历数组
source.filter(father=>{
if(father[children]){//如果当前元素有child,将当前元素加入arr,然后递归它的儿子加入arr,儿子都递归完,删除当前元素的children属性
arr.push(father)//将当前元素加入arr
unTreeDatadd(father[children],children,arr)//递归儿子
delete father[children]//删除children
}else{//如果当前元素没有child
arr.push(father)//直接添加到arr
}
})
}
3. validate.js: 处理Path路径相关,主要使用其提供的正则匹配url地址
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}
5. router路由配置
因为不同用户要展示不同路由,所以需要定义常规路由和异步动态路由
import { createRouter, createWebHashHistory } from 'vue-router'
//1. 将vue-router模块,挂载到Vue.VUE3中,只需要在main.js引入即可,所以这里不需要额外操作
// 2.拆分路由
//2.1.常量路由,无论何种角色,都可以访问的路由
export const constantRoutes = [
{
path: '/',
name: 'home',
redirect: '/dashboard',
component: () => import( '@/layout/index.vue'),
children: [{//这里的内容会在<router-view :key="key" />标签渲染
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'Management' }
}]
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
// {
// path: '/',
// component: Layout,
// redirect: '/dashboard',
// children: [{//这里的内容会在<router-view :key="key" />标签渲染
// path: 'dashboard',
// name: 'Dashboard',
// component: () => import('@/views/dashboard/index'),
// meta: { title: '首页', icon: 'dashboard' }
// }]
// },
]
//2.2 异步路由,根据不同角色,需要过滤的路由
export const asyncRoutes = [
{
path: '/security',
component: () => import( '@/layout/index.vue'),
redirect: '/security/user',
meta: { title: '权限管理', icon: 'Lock' },
children: [
{
path: 'user',
name: 'User',
component: () => import('@/views/security/user/index'),
meta: { title: '用户管理'}
},
{
path: 'role',
name: 'Role',
component: () => import('@/views/security/role/index'),
meta: { title: '角色管理'}
},
{
path: 'menu',
name: 'menu',
component: () => import('@/views/security/menu/index'),
meta: { title: '菜单管理'}
},
]
},
]
//2.3 任意路由,路径错误重定向到的路由
export const anyRoutes = { path: '*', redirect: '/404', hidden: true }// 404 page must be placed at the end !!!
// 3. 回调函数,封装了路由,routes封装的是上面的constantRoutes常量
const router = createRouter({
history: createWebHashHistory(),
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
// 4. 对外暴露,重置路由函数
export function resetRouter() {
const newRouter = router
router.matcher = newRouter.matcher // reset router
}
// 5. 对外暴露,router这个对象
export default router
6. pinia全局控制配置
主要帮助我们控制全局的token,登录等逻辑。后续业务功能很可能也会使用到
import {defineStore} from "pinia";
import { login, logout, getInfo } from '@/api/user' //引入了我们的api。因为我们要向后端发请求
import { getMenusByUserId } from '@/api/menu' //引入了我们的api。因为我们要向后端发请求
import {treeData,unTreeData} from '@/utils/treeutil'//处理树形结构的工具类
import { getToken, setToken, removeToken } from '@/utils/auth' //auth权限相关操作
import { resetRouter , asyncRoutes,anyRoutes,constantRoutes} from '@/router'//拆分好的路由
import router from '@/router'
//比较路由,返回需要展示的异步路由
const compareasyncRoutes = (asyncRoutes,routes)=>{
return asyncRoutes.filter(item =>{
if(routes.indexOf(item.path)!=-1){//如果当前路由,用户拥有权限,就展示
if(item.children&&item.children.length){//如果有children就递归
item.children = compareasyncRoutes(item.children,routes)
}
return true;
}
})
}
//比较路由,返回需要删除的路由
const compareDeleteRoutes = (currentRoute,oKShowRoutes)=>{
return currentRoute.filter(item =>{
if(oKShowRoutes.indexOf(item.path)===-1){//如果当前路由,是不可以展示的,就是需要删除的
if(item.children&&item.children.length){//如果有children就递归
item.children = compareasyncRoutes(item.children,oKShowRoutes)
}
return true;
}
})
}
// const defaultRoutes = (routesResult,resultAsyncRoutes)=>{
// resultAsyncRoutes = []
// routesResult = constantRoutes.concat(resultAsyncRoutes,anyRoutes);
// console.log(asyncRoutes);
// router.addRoute(routesResult)
// }
//getDefaultState常数封装了token,name,avatar
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: '',
routes:[],//服务器返回的菜单path,菜单就是type类型为menu的
roles:[],//保存当前用户拥有角色
buttons:[],//按钮权限信息,按钮就是菜单类型为button的
resultAsyncRoutes:[],//对比后的异步路由
routesResult:[],//最终要展示的路由
}
}
export const usePiniaUserStore = defineStore("piniaUserStore", {
state: ()=>{
return getDefaultState()
},
// persist:[
// {
// key: 'token',
// storage: localStorage,
// },
// {
// key: 'name',
// storage: localStorage,
// },
// {
// key: 'routesResult',
// storage: localStorage,
// }
// ],
getters: {
getName: (state) => state.name,
getToken: state => state.token,
getAvatar: state => state.avatar,
getRoutesResult: state => state.routesResult,
},
actions:{
// user login
//处理登录业务
async login(userInfo) {
//解构出用户名和密码
const { username,password} = userInfo;
//异步调用/api/user文件下的login
let result = await login({username: username.trim(),password:password});
// console.log(result)
if (result.code===20000){
//我们规定token为tokenHead + " " + token字符串
// console.log(result)
const token = result.tokenInfo.tokenValue;
//设置到state
// commit("SET_TOKEN",token);
this.token = token
//通过auth.js文件的函数,设置到cookie
setToken(token)
}else{
//登录失败
return Promise.reject(new Error(result.msg));
}
},
// get user info
async getInfo() {
//异步调用/api/user文件下的login
let result = await getInfo();
if(result.code === 20000){
const loginInfo = result.userInfo
if (!loginInfo) {
return reject('验证失败,请重新登录.');
}
//处理路由
let menusResult = await getMenusByUserId(loginInfo.id)
// console.log(menusResult)
if(menusResult.code === 20000){
const { menus } = menusResult
// console.log(menus)
if(menus.length > 0){
const _menus = menus
let routes = []
let buttons = []
_menus.forEach(e=>{
if(e.type === 'menu'){
routes.push(e.path)
}else if(e.type === 'button') {
buttons.push(e.permission)
}
})
// commit('SET_BUTTONS',buttons)
this.buttons = buttons;
// commit('SET_ROUTES',routes)
this.routes = routes;
const { roles } = loginInfo
// commit('SET_ROLES',roles)
this.roles = roles;
//对比后设置需要展示的异步路由
const resultAsyncRoutes = compareasyncRoutes(asyncRoutes,routes)
// commit('SET_RESULTASYNCROUTES',compareasyncRoutes(asyncRoutes,routes))
this.resultAsyncRoutes = resultAsyncRoutes
//合并常量、异步、路由最终展示的路由
this.routesResult = constantRoutes.concat(this.resultAsyncRoutes,anyRoutes);
// console.log("asd;kfjlk;asdjdkl;fds",JSON.parse(JSON.stringify(this.routesResult)))
// router.addRoute(JSON.parse(JSON.stringify(this.routesResult)))
// router.matcher.addRoutes(JSON.parse(JSON.stringify(this.routesResult)))
//将最终合并路由添加到路由中
for (let i = 0; i < resultAsyncRoutes.length; i++) {
router.addRoute(resultAsyncRoutes[i])
}
}else{
// defaultRoutes(this.routesResult,this.resultAsyncRoutes)
this.resultAsyncRoutes = []
this.routesResult = constantRoutes.concat(this.resultAsyncRoutes,anyRoutes);
router.addRoute(this.routesResult)
}
}else{
// commit('SET_RESULTASYNCROUTES',compareasyncRoutes(asyncRoutes,[]))
this.resultAsyncRoutes = []
this.routesResult = constantRoutes.concat(this.resultAsyncRoutes,anyRoutes);
// console.log(asyncRoutes);
router.addRoute(this.routesResult)
}
const {username} = loginInfo;
// commit('SET_NAME',username);
// commit('SET_AVATAR','https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif');
this.name = username;
this.avatar = 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif';
}else{
//登录失败
return Promise.reject(new Error(result.msg));
}
},
// user logout
async logout() {
let result = await logout(this.token);
if(result.code === 20000){
removeToken() // must remove token first
resetRouter()
// commit('RESET_STATE')
this.$reset()
}else{
//退出登录失败
return Promise.reject(new Error(result.msg));
}
},
// remove token
resetToken() {
return new Promise(resolve => {
removeToken() // must remove token first
// commit('RESET_STATE')
this.$reset()
resolve()
})
}
}
})
7. request.js工具类
是我们发送请求的一个工具类,我们需要每次发送请求时,拦截请求并添加token,以及其它一些常用操作都封装在这个文件中
import axios from 'axios'
import { ElMessageBox, ElMessage } from 'element-plus'
import {usePiniaUserStore} from "@/store/pinia_user"
import { getToken,removeToken } from '@/utils/auth'
const baseURL = 'http://localhost:7071'
// create an axios instance
export const service = axios.create({
baseURL: baseURL, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
//获取baseURL
export function getServerUrl(){return baseURL}
// request interceptor
service.interceptors.request.use(
config => {
const piniaUserStore = usePiniaUserStore();
// do something before request is sent
// if (store.getters.token) {
if (piniaUserStore.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['authorization'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const piniaUserStore = usePiniaUserStore();
//获取响应数据
const res = response.data
// console.log("response interceptor:",res)
//如果响应内容不存在,直接抛错
if(res == undefined){
return Promise.reject(new Error(res.msg || '没有获取到服务器响应,基础功能出错,无法正常运行!!!请联系管理员'))
}
//如果响应内容存在,但是没有响应码code,表示是无需关注响应码的响应体(例如验证码)
//直接放行
if(res.code == undefined){
return res;
}
// console.log(res)
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000) {
if (res.msg){
ElMessage({
message: res.msg || 'Error',
type: 'warning',
duration: 2 * 1000
})
}
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 401 || res.code === 50008 || res.code === 50012 || res.code === 50014) {
removeToken()
// to re-login。You have been logged out, you can cancel to stay on this page, or log in again
ElMessageBox.confirm('您已经被迫登出,你可以点击cancel按钮留在当前页面,或者点击Re-Login重新登录', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
ElMessage({
type: 'success',
message: 'Delete completed',
})
piniaUserStore.resetToken().then(() => {
location.reload()
})
// store.dispatch('user/resetToken').then(() => {
// location.reload()
// })
}).catch(() => {
ElMessage({
type: 'info',
message: 'Delete canceled',
})
})
return res
}
// return Promise.reject(new Error(res.msg || 'Error'))
// ElMessage.error(res.msg||"Error");
return res;
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
ElMessage({
message: error.msg,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
/**
* 文件上传请求
*/
export function fileUpload(url, params = {}) {
return new Promise((resolve, reject) => {
service({
url:url,
method:'post',
data:params,
headers:{'Content-Type':'multipart/form-data'}
}).then((response) => {
console.log(response);
resolve(response);
}).catch((error) => {
console.log('err' + error)
reject(error);
})
})
}
8. 引入全局通用css样式
网站格式应该统一一点,不可以乱七八糟,每个页面都独具一格。例如边框都是1像素的宽度,布局的比例也应该统一
引入完成后,记住在main.js中引入
border.css文件,统一边框样式
@charset "utf-8";
.border,
.border-top,
.border-right,
.border-bottom,
.border-left,
.border-topbottom,
.border-rightleft,
.border-topleft,
.border-rightbottom,
.border-topright,
.border-bottomleft {
position: relative;
}
.border::before,
.border-top::before,
.border-right::before,
.border-bottom::before,
.border-left::before,
.border-topbottom::before,
.border-topbottom::after,
.border-rightleft::before,
.border-rightleft::after,
.border-topleft::before,
.border-topleft::after,
.border-rightbottom::before,
.border-rightbottom::after,
.border-topright::before,
.border-topright::after,
.border-bottomleft::before,
.border-bottomleft::after {
content: "\0020";
overflow: hidden;
position: absolute;
}
/* border
* 因,边框是由伪元素区域遮盖在父级
* 故,子级若有交互,需要对子级设置
* 定位 及 z轴
*/
.border::before {
box-sizing: border-box;
top: 0;
left: 0;
height: 100%;
width: 100%;
border: 1px solid #eaeaea;
transform-origin: 0 0;
}
.border-top::before,
.border-bottom::before,
.border-topbottom::before,
.border-topbottom::after,
.border-topleft::before,
.border-rightbottom::after,
.border-topright::before,
.border-bottomleft::before {
left: 0;
width: 100%;
height: 1px;
}
.border-right::before,
.border-left::before,
.border-rightleft::before,
.border-rightleft::after,
.border-topleft::after,
.border-rightbottom::before,
.border-topright::after,
.border-bottomleft::after {
top: 0;
width: 1px;
height: 100%;
}
.border-top::before,
.border-topbottom::before,
.border-topleft::before,
.border-topright::before {
border-top: 1px solid #eaeaea;
transform-origin: 0 0;
}
.border-right::before,
.border-rightbottom::before,
.border-rightleft::before,
.border-topright::after {
border-right: 1px solid #eaeaea;
transform-origin: 100% 0;
}
.border-bottom::before,
.border-topbottom::after,
.border-rightbottom::after,
.border-bottomleft::before {
border-bottom: 1px solid #eaeaea;
transform-origin: 0 100%;
}
.border-left::before,
.border-topleft::after,
.border-rightleft::after,
.border-bottomleft::after {
border-left: 1px solid #eaeaea;
transform-origin: 0 0;
}
.border-top::before,
.border-topbottom::before,
.border-topleft::before,
.border-topright::before {
top: 0;
}
.border-right::before,
.border-rightleft::after,
.border-rightbottom::before,
.border-topright::after {
right: 0;
}
.border-bottom::before,
.border-topbottom::after,
.border-rightbottom::after,
.border-bottomleft::after {
bottom: 0;
}
.border-left::before,
.border-rightleft::before,
.border-topleft::after,
.border-bottomleft::before {
left: 0;
}
@media (max--moz-device-pixel-ratio: 1.49), (-webkit-max-device-pixel-ratio: 1.49), (max-device-pixel-ratio: 1.49), (max-resolution: 143dpi), (max-resolution: 1.49dppx) {
/* 默认值,无需重置 */
}
@media (min--moz-device-pixel-ratio: 1.5) and (max--moz-device-pixel-ratio: 2.49), (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 2.49), (min-device-pixel-ratio: 1.5) and (max-device-pixel-ratio: 2.49), (min-resolution: 144dpi) and (max-resolution: 239dpi), (min-resolution: 1.5dppx) and (max-resolution: 2.49dppx) {
.border::before {
width: 200%;
height: 200%;
transform: scale(.5);
}
.border-top::before,
.border-bottom::before,
.border-topbottom::before,
.border-topbottom::after,
.border-topleft::before,
.border-rightbottom::after,
.border-topright::before,
.border-bottomleft::before {
transform: scaleY(.5);
}
.border-right::before,
.border-left::before,
.border-rightleft::before,
.border-rightleft::after,
.border-topleft::after,
.border-rightbottom::before,
.border-topright::after,
.border-bottomleft::after {
transform: scaleX(.5);
}
}
@media (min--moz-device-pixel-ratio: 2.5), (-webkit-min-device-pixel-ratio: 2.5), (min-device-pixel-ratio: 2.5), (min-resolution: 240dpi), (min-resolution: 2.5dppx) {
.border::before {
width: 300%;
height: 300%;
transform: scale(.33333);
}
.border-top::before,
.border-bottom::before,
.border-topbottom::before,
.border-topbottom::after,
.border-topleft::before,
.border-rightbottom::after,
.border-topright::before,
.border-bottomleft::before {
transform: scaleY(.33333);
}
.border-right::before,
.border-left::before,
.border-rightleft::before,
.border-rightleft::after,
.border-topleft::after,
.border-rightbottom::before,
.border-topright::after,
.border-bottomleft::after {
transform: scaleX(.33333);
}
}
reset.css统一全局通用样式
@charset "utf-8";
html{font-size:12px}
body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0}
body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height:1.5;font-family:tahoma,arial,"Hiragino Sans GB",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%}
h1,h2,h3,h4,h5,h6{font-family:tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,b,strong{font-weight:normal}
address,cite,dfn,em,i,optgroup,var{font-style:normal}
table{border-collapse:collapse;border-spacing:0;text-align:left}
caption,th{text-align:inherit}
ul,ol,menu{list-style:none}
fieldset,img{border:0}
img,object,input,textarea,button,select{vertical-align:middle}
article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1}
blockquote:before,blockquote:after,q:before,q:after{content:"\0020"}
textarea{overflow:auto;resize:vertical}
input,textarea,button,select,a{outline:0 none;border: none;}
button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}
mark{background-color:transparent}
a,ins,s,u,del{text-decoration:none}
sup,sub{vertical-align:baseline}
html {overflow-x: hidden;height: 100%;font-size: 50px;-webkit-tap-highlight-color: transparent;}
body {font-family: Arial, "Microsoft Yahei", "Helvetica Neue", Helvetica, sans-serif;color: #333;font-size: .28em;line-height: 1;-webkit-text-size-adjust: none;}
hr {height: .02rem;margin: .1rem 0;border: medium none;border-top: .02rem solid #cacaca;}
a {color: #25a4bb;text-decoration: none;}
3. index.scss是css3布局用的
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
// main-container global css
.app-container {
padding: 20px;
}
9. 页面title
1. 新建settings.js,暴露全局标题
/**
* module.exports是Node.js中用于导出模块的一个特殊对象。在Node.js中,每个模块都有一个module对象,
* 该对象代表当前模块,而module.exports是module对象的一个属性,用于导出模块中的函数、对象或值。
* 其他文件可以通过require()函数来使用这些导出的内容
* **/
module.exports = {
title: 'Vue Admin Template',
/**
* @type {boolean} true | false
* @description Whether fix the header
*/
fixedHeader: false,
/**
* @type {boolean} true | false
* @description Whether show the logo in sidebar
*/
sidebarLogo: true
}
2. 生成页面title的工具类,get-page-title.js
import defaultSettings from '@/settings'
// 如果defaultSettings.title为空,默认指定为'Vue Admin Template'
const title = defaultSettings.title || 'Vue Admin Template'
//生成title,pageTitle是当前路由到的页面的title
export default function getPageTitle(pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${title}`
}
return `${title}`
}
10. 导航守卫
每次路由的改变,都会调用此守卫,如果token过期等触发,就路由到登录页面
main.js中注册,(但是硬性要求登录和获取当前登录用户信息两个接口,如果没写好的话,先不要在main.js中注册)
/**
* 路由导航守卫
**/
import router from "@/router";
// import store from "@/store";
import {usePiniaUserStore} from "@/store/pinia_user"
import {ElMessage} from "element-plus";
import {getToken} from "@/utils/auth";
import NProgress from "nprogress";
import getPageTitle from "@/utils/get-page-title";
NProgress.configure({ showSpinner: false })
//白名单
const whiteList = ['/login']
//当发生路由跳转进行异步调用,to表示要去的路由,from表示要离开的路由,next用来执行钩子,有多种重载形式,比如next(),next({})
router.beforeEach( async (to, from, next) => {
// console.log(router.getRoutes())
const piniaUserStore = usePiniaUserStore();
const toPath = to.path
// console.log(toPath)
// 开始进度条
NProgress.start()
//设置页面标题
document.title = getPageTitle(to.meta.title);
// 通过token确定用户是否已登录
const hasToken = getToken()
//如果有token,则已登录
if (hasToken) {
// console.log('已登录的用户')
/**动态路由需要解决,刷新路由丢失问题**/
//判断是否为刷新页面,vuex中无数据,则为刷新页面
// if (store.state.user.routesResult.length === 0) {
if (piniaUserStore.routesResult.length === 0) {
console.log('检测到刷新页面,重新获取用户信息')
// 获取用户信息(关于异步路由的东西我放这里了)
// store.dispatch('user/getInfo').then(()=>{
// // console.log('获取成功,开始跳转路由:',to.path)
// next({path:to.path})
// })
await piniaUserStore.getInfo().then(() => {
// console.log('获取成功,开始跳转路由:', toPath)
next({path: toPath})
})
}
// else if(piniaUserStore.routesResult.length !== 0) {
// console.log("非刷新页面")
// next()
// }
//如果用户要去的是登录页,直接从定向到主页,因为已经登录了,不用重复登录
else if (to.path === '/login') {
// console.log('正在前往login')
next({path: '/'})
NProgress.done()//进度条转态为完成
} else {//如果要前往的路由不是/login
//console.log("非login")
//获取用户名
// const hasGetUserInfo = store.getters.name
const hasGetUserInfo = piniaUserStore.name
//如果用户名存在
if (hasGetUserInfo) {
//console.log('如果用户名存在')
next()//进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
} else {//不存在
// try {
// 获取用户信息
// store.dispatch('user/getInfo')
await piniaUserStore.getInfo().then(e=>{
//console.log("获取用户信息成功!")
next()//进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
}).catch(async error=>{//如果获取用户信息出错
// 删除令牌,进入登录页面重新登录
// store.dispatch('user/resetToken')
await piniaUserStore.resetToken()
ElMessage.error(error || 'Has Error')
next(`/login?redirect=${toPath}`)//next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址
NProgress.done()//进度条结束
})
// } catch (error) {//如果获取用户信息出错
// }
}
}
} else {
/* has no token 没有登录的话*/
//是否是白名单的路径
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {//不是白名单的路径直接定位到登录页面
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})