安装基础包npm create vite@latest
# 这里选择的是Vue+Typescript的组合
cd vue-admin
npm install
# 先安装基础包
npm install vue-router@4
npm i pinia
npm i axios
npm install sass --save-dev
npm install element-plus --save
npm install @element-plus/icons-vue
npm install -D unplugin-vue-components unplugin-auto-import
npm i eslint -D
# 提交规范
npm i lint-staged husky
--save-dev
npm install @commitlint/cli @commitlint/config-conventional -D
代码规范npm init @eslint/config
接下来会有一堆提示,选择如下:Need to install the following packages:
@eslint/create-config
Ok to proceed? (y)
√ How would you like to use ESLint? · style
√ What type of modules does your project use? · esm
√ Which framework does your project use? · vue
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · browser
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · standard-with-typescript
√ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-standard-with-typescript@latest
The config that you've selected requires the following dependencies:
eslint-plugin-vue@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.50.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@*
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · npm
Installing eslint-plugin-vue@latest, eslint-config-standard-with-typescript@latest, @typescript-eslint/eslint-plugin@^5.50.0, eslint@^8.0.1, eslint-plugin-import@^2.25.2, eslint-plugin-n@^15.0.0, eslint-plugin-promise@^6.0.0, typescript@*
在项目中就会生成一个.eslintrc.cjs文件,接下来配置一下脚本验证一下:"lint": "eslint src/**/*.{js,jsx,vue,ts,tsx} --fix"
然而运行的时候报错了,由于我当前的"typescript": "^5.1.3",而@typescript-eslint/typescript-estree支持的ts版本范围为:=3.3.1 <5.1.0,所以我得降级一下:typescript@5.0.4,在配置eslint路上出现了很多问题,直接提供解决方案:首先是修改.eslintrc.cjs:module.exports = {
env: {
browser: true,
es2021: true
},
extends: [
'plugin:vue/vue3-essential',
'standard-with-typescript'
],
parser: "vue-eslint-parser",
overrides: [
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ["./tsconfig.json"],
parser: "@typescript-eslint/parser",
extraFileExtensions: ['.vue']
},
plugins: [
'vue'
],
rules: {
'space-before-function-paren': [2, {
anonymous: 'always',
named: 'never',
asyncArrow: 'always'
}],
'vue/multi-word-component-names': 0,
"space-before-function-paren": 0,
"@typescript-eslint/consistent-type-assertions": 0,
"@typescript-eslint/ban-types": [
"error",
{
"extendDefaults": true,
"types": {
"{}": false
}
}
]
}
}
在vite-env.d.ts加上一行注释,忽略检查:// eslint-disable-next-line @typescript-eslint/triple-slash-reference
///
关于eslint配置中遇到的问题,可以参考这个大佬写的,更详细些:Eslint:vue3项目添加eslint(standard规则)commit规范git init
在package.json中新增如下代码,利用它来调用 eslint 和 stylelint 去检查暂存区内的代码"lint-staged": {
"*.{vue,js}": [
"npm run lint"
]
}
执行:npm pkg set scripts.postinstall="husky install"
# 等同于执行npm i,执行过程中会生成.husky文件夹
npm run postinstall
npx husky add .husky/pre-commit "npm lint"
git add .husky/pre-commit
这样我们执行git commit的时候就会自动执行npm lint。很尴尬,在跑的过程中,报错了node不是内部或外部命令。node -v是木有问题的,大抵是nvm这个工具的问题,所以后面就换了volta来做node的版本控制。npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"
新建commitlint.config.cjs:module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', // 新增功能
'update', // 更新功能
'ui', // 样式改动
'fix', // 修复功能bug
'merge', // 合并分支
'refactor', // 重构功能
'perf', // 性能优化
'revert', // 回退提交
'style', // 不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等)
'build', // 修改项目构建工具(例如 glup,webpack,rollup 的配置等)的提交
'docs', // 文档新增、改动
'test', // 增加测试、修改测试
'chore' // 不修改src或者test的其余修改,例如构建过程或辅助工具的变动
]],
'scope-empty': [0],
// 'scope-empty': [2, 'never'], 作用域不为空
'scope-case': [0],
'subject-full-stop': [0],
'subject-case': [0]
}
}
修改tsconfig.json:"include": [
//...
"commitlint.config.cjs"
],
修改.eslintrc.cjs:project: ["./tsconfig.json", "./commitlint.config.cjs"],
git add .
# 失败
git commit -m "commit校验"
# 成功
git commit -m "feat: commit校验"
设置路径别名import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
const resolve = (dist) => path.resolve(__dirname, dist)
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve('src')
},
// 顺便把可以省略的后缀配置一下,在vite中不支持省略.vue
extensions: [".js", ".ts", ".tsx", ".jsx"]
}
})
修改tsconfig.json,新增:"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
重置样式在 assets文件夹下新建styles/reset.css:/**
* Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
* http://cssreset.com
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video{
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
font-weight: normal;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section{
display: block;
}
ol, ul, li{
list-style: none;
}
blockquote, q{
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after{
content: '';
content: none;
}
table{
border-collapse: collapse;
border-spacing: 0;
}
/* custom */
a{
color: #7e8c8d;
text-decoration: none;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
::-webkit-scrollbar{
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track-piece{
background-color: rgba(0, 0, 0, 0.2);
border-radius: 6px;
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:vertical{
height: 5px;
background-color: rgba(125, 125, 125, 0.7);
border-radius: 6px;
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:horizontal{
width: 5px;
background-color: rgba(125, 125, 125, 0.7);
border-radius: 6px;
-webkit-border-radius: 6px;
}
html, body{
width: 100%;
font-family: "Arial", "Microsoft YaHei", "黑体", "宋体", "微软雅黑", sans-serif;
}
body{
line-height: 1;
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
html{
overflow-y: scroll;
}
/*清除浮动*/
.clearfix:before,
.clearfix:after{
content: " ";
display: inline-block;
height: 0;
clear: both;
visibility: hidden;
}
.clearfix{
*zoom: 1;
}
/*隐藏*/
.dn{
display: none;
}
使用ScssVite 提供了对 .scss, .sass, .less, .styl 和 .stylus 文件的内置支持。没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖。一般我们会在项目中定义一些主题色:// variable.scss
$font-color-gray:rgb(147,147,147);
或者是一些封装好的集合样式:// mixins.scss
@mixin line-clamp($lines) {
word-break: break-all;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
}
@mixin ellipsis() {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
然后我们在vite.config.js中配置:css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
}
}
}
删除目录中的style.css,新建styles/common.scss :// common.scss
@import url('./reset.css');
然后在main.ts中引入:import '@/assets/styles/common.scss'
这样全局样式就初始化了,接下来测试一下variable.scss和mixins.scss是否起作用,修改HelloWorld.vue查看是否为灰色且两行省略:
没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。
配置路由现在要来配置路由,希望有这样的结果:- 登录页
- 带菜单栏的框架
- 主页
- 人员管理
- 客户管理
- 员工管理
- 404页
所以新建以下文件:在这个过程中我们会遇到几个问题:在编写路由的时候我们引入组件,必须有.vue后缀;import xxx from '@/xxx'时会报错,是因为你没有在上面提到的那样在tsconfg.json中设置baseUrl和paths值。由于layout文件中要嵌套子路由,所以layout中要加入router-view:
布局
其他的文件只要写成这样就行:
主页
新建router文件夹:- router
-hooks # 后期做登录校验和鉴权用的
- routes
- index.ts # 总输出文件
- others.ts # 不需要layout这一层的路由均可以放在这里
- person.ts # 人员管理模块
- index.ts
# 总输出文件
接下里是各个文件的内容:// person.ts
export default [
{
path: '/person',
name: 'Person',
meta: { title: '人员管理' },
redirect: '/person/customer',
children: [
{
path: '/person/customer',
name: 'PersonCustomer',
meta: { title: '客户管理' },
component: () => import('@/views/person/customer/index.vue')
},
{
path: '/person/staff',
name: 'PersonStaff',
meta: { title: '员工管理' },
component: () => import('@/views/person/staff/index.vue')
}
]
}
];
// others.ts
export default [
{
path: '/login',
name: 'Login',
meta: { title: '登录' },
component: () => import('@/views/login/index.vue')
}
];
// router/routes/index.ts
import Layout from '@/views/layout/index.vue';
import personRoutes from './person';
import otherRoutes from './others';
export default [
{
path: '/',
name: 'Layout',
component: Layout,
children: [
{
path: '/',
name: 'Index',
meta: { title: '主页' },
component: () => import('@/views/index/index.vue')
},
...personRoutes,
]
},
...otherRoutes,
{
path: '/404',
name: 'NotFound',
meta: { title: '404' },
component: () => import('@/views/404/index.vue')
},
{
path: "/:pathMatch(.*)",
redirect: "/404",
name:'ErrorPage',
meta: { title: '' },
}
];
先抛开hooks文件夹,简单的写一下index.ts:// router/index.ts
import routes from "./routes";
export default routes;
新建一个src/plugins/index.ts,之前我们注册内容的时候都是直接放在main.ts中,不太容易维护,所以以后统一在这里挂载:// src/plugins/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import routes from '@/router/index';
export default (app: any) => {
// 注册路由
const router = createRouter({
history: createWebHashHistory(),
routes
})
app.use(router);
}
不要忘了修改App.vue:
import { createApp } from 'vue'
import '@/assets/styles/common.scss'
import App from './App.vue'
import installPlugins from '@/plugins';
const app = createApp(App);
installPlugins(app);
app.mount('#app')
这样就可以测试了:http://127.0.0.1:5173/#/
http://127.0.0.1:5173/#/login
http://127.0.0.1:5173/#/person
http://127.0.0.1:5173/#/person/customer
http://127.0.0.1:5173/#/person/staff
如果在配置过程中发现报错:找不到模块“xxx.vue”或其相应的类型声明,则在vite-env.d.ts中新增:declare module '*.vue' {
import type { DefineComponent } from 'vue';
const vueComponent: DefineComponent<{}, {}, any>;
export default vueComponent;
}
使用element plus如果您使用 Volar,请在 tsconfig.json 中通过 compilerOptions.type 指定全局组件类型。// tsconfig.json
{
"compilerOptions": {
// ...
// 然而这个配置在后期打包的时候报错了...
"types": ["element-plus/global"]
}
}
这里采用了按需引入的方式,如果对体积不追求的,可以采用完整引入:// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// 新增
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
const resolve = (dist) => path.resolve(__dirname, dist)
export default defineConfig({
plugins: [
vue(),
// 新增
AutoImport({
resolvers: [ElementPlusResolver()]
}),
// 新增
Components({
resolvers: [ElementPlusResolver()]
})
],
resolve: {
alias: {
'@': resolve('./src')
},
extensions: [".js", ".ts", ".tsx", ".jsx"]
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
}
}
}
})
element plus中日期等组件默认是英文,所以我们把组件改为中文:修改App.vue:
这样就会出现中文了。关于引入的两个插件,这里解释一下:unplugin-vue-components用于自动识别Vue模板中使用的组件,自动按需导入和注册;unplugin-auto-import可以在vite、webpack等环境下自动按需导入配置库常用的API,如Vue的ref,不需要手动import,所以我们可以配置一下,并删除一些API的引入:export default defineConfig({
plugins: [
// ...
AutoImport({
imports: [
'vue',
'vue-router',
'pinia'
],
eslintrc: {
enabled: true,
filepath: './.eslintrc-auto-import.json',
globalsPropValue: true
},
resolvers: [ElementPlusResolver()]
}),
// ...
],
})
保存生效后,auto-imports.d.ts 会自动填充内容,并且会在项目根目录生成 .eslintrc-auto-import.json eslint 全局变量配置。然后修改tsconfg.json和.eslintrc.cjs:// tsconfg.json
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "commitlint.config.cjs", "auto-imports.d.ts"],
// .eslintrc.cjs
project: ["./tsconfig.json", "./commitlint.config.cjs", './.eslintrc-auto-import.json'],
忽略 auto-imports.d.ts ESLint 校验# .eslintignore
auto-imports.d.ts
这里需要注意一下:不是全部 API,例如 Vue Router 的 createRouter 就不会导入。具体可以自动导入的 API 参考 unplugin-auto-import/src/presets生成 .eslintrc-auto-import.json 文件后如不需要增加配置建议将 enabled: true 设置为 false,否则每次都会生成这个文件。配置完可以删除页面中的一些引用,发现是没有问题的。
测试一下组件:
测试
这样页面上就会显示按钮了。自动按需引入的原理是通过识别
中使用的组件自动导入,那类似ElMessage 这类直接在 JS 中调用方法的组件,插件并不会识别并完成自动导入,所以还是需要自己手动导入一下(建议按需引入的方式,仍然引入完整的样式文件,避免这类边界问题):修改vite-env.d.ts,不然在ts中引入element plus会报错:declare module "element-plus";
在plugins/element-plus.ts中小试一下:import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'
const options = {
size: 'small',
zIndex: 3000
}
const components = [
ElLoading,
ElMessage,
ElMessageBox,
ElNotification
]
export default function install (app: any): void {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
components.forEach((component) => {
app.use(component, options)
})
}
// plugins/index.ts
export default (app: any) => {
// ...
// 注册element-plus
installElementPlus(app);
}
测试一下:
测试
globalProperties按照以前的习惯,loading的调用肯定不用上面的方式,而是挂载在全局Vue.prototype上,然而在这个项目中,我们用的是按需引入,且在Vue3中,写法变了,你可能想这么写:app.config.globalProperties.$loading = ElLoading;
app.config.globalProperties.$message = ElMessage;
app.config.globalProperties.$msgBox = ElMessageBox;
app.config.globalProperties.$notification = ElNotification;
然后再使用的过程中:
但是查阅了官方文档,并没有getCurrentInstance该方法,大概是不符合规范吧。所以全局方法的注入,我采用了provide/inject。App.vue:
修改element-plus.ts:import { App } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'
// const options = {
//
size: 'small',
//
zIndex: 3000
// }
// const components = [
//
ElLoading,
//
ElMessage,
//
ElMessageBox,
//
ElNotification
// ]
export default function install (app: App): void {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// components.forEach((component) => {
//
app.use(component, options)
// })
}
新增src/types/global.d.ts:import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
export interface ComponentCustomProperties {
$message: typeof ElMessage
$msgBox: typeof ElMessageBox
$loading: typeof ElLoading
$notification: typeof ElNotification
}
测试:
测试
复习一些常见的ts写法开发服务器和打包器将只会对ts文件执行语法转义,而不会执行任何类型检查,保证了vite开发服务器在使用ts时也能保持飞快的速度。下面是一些常见的例子:
登录静态页面在登录页面中会使用到el-input组件,一般对于这种表单组件,后管使用的频率还是很高的,所以倾向于进行二次封装再使用,一般情况下会优化它的前后空格问题,并将它与textarea拆解出来:
在plugins/components.ts下全局注册:import type { Component } from 'vue'
import ArInput from '@/components/form/input/index.vue';
const componentObj: {[propName: string]: Component} = {
ArInput
};
export default function install(app: any) {
Object.keys(componentObj).forEach((key) => {
app.component(key, componentObj[key])
})
}
记得在plugins/index.ts中加入:import installComponents from './components'
export default (app: any) => {
// ...
// 注册自定义组件
installComponents(app);
}
login.vue的源码:
最后出来的页面效果即:环境变量在完成页面提交动作之前,先解决环境变量的问题,在测服、预发布或者生产中我们总有些变量是不一样的,所以需要做环境区分新建env文件夹,内部新增三个四个文件:.env
# 所有情况下都会加载
.env.development
# 开发环境
.env.release
# 预发布环境
.env.production
# 正服环境
举个例子:# .env.development
VITE_ENV = devalopment
# 请求接口
VITE_API_URL = https://api.vvhan.com/testapi/saorao
这样就可以在不同的环境中设置变量,然后我们修改一下脚本命令,区分一下环境:"scripts": {
"watch": "vite",
"watch:release": "vite --mode release",
"watch:production": "vite --mode production",
"build:development": "vue-tsc && vite build --mode development",
"build:release": "vue-tsc && vite build --mode release",
"build:production": "vue-tsc && vite build --mode production",
// ...
},
在根目录新建build/utils.ts文件:// Read all environment variable configuration files to process.env
export function wrapperEnv(envConf: Recordable) {
const result: any = {}
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, '\n')
realName =
realName === 'true' ? true : realName === 'false' ? false : realName
result[envName] = realName
if (typeof realName === 'string') {
process.env[envName] = realName
} else if (typeof realName === 'object') {
process.env[envName] = JSON.stringify(realName)
}
}
return result
}
然后修改vite.config.js,将我们定义的变量注入进去:import { defineConfig, loadEnv, UserConfig, ConfigEnv
} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { wrapperEnv } from './build/utils'
const resolve = (dist) => path.resolve(__dirname, dist)
// https://vitejs.dev/config/
export default ({ command, mode }: ConfigEnv): UserConfig => {
const env = loadEnv(mode, './env')
wrapperEnv(env)
return {
plugins: [
vue(),
AutoImport({
imports: [
'vue',
'vue-router',
'pinia'
],
eslintrc: {
enabled: false,
filepath: './.eslintrc-auto-import.json',
globalsPropValue: true
},
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
],
resolve: {
alias: {
'@': resolve('./src')
},
extensions: [".js", ".ts", ".tsx", ".jsx"]
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
}
}
}
}
}
这样我们就可以在下面的axios封装中使用了。哦对可能会有点ts的报错,修改tsconfig.node.json:"include": ["vite.config.ts", "build**/*.ts"]
axios封装新建src/utils/request.ts:import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'
const request: AxiosInstance
any = axios.create({
timeout: 100000,
headers: {
post: {
'Content-Type': 'application/x-www-form-urlencoded'
}
},
withCredentials: true
});
request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
let token = localStorage.getItem("token");
if (token && token !== '') {
config.headers['Authorization'] = token;
}
// 获取环境变量!!!
const projectUrlPrefix = import.meta.env.VITE_API_URL;
// 这样更支持多域名接口的情况
if (config && config.url && !/^(http(|s):\/\/)|^\/\//.test(config.url)) {
config.url = projectUrlPrefix + config.url;
}
return config;
});
request.interceptors.response.use((res: any) => {
if (res.data.status && res.data.status !== 200) {
ElMessage.error(res.data.msg
'请求失败,请稍后重试')
return Promise.reject(res.data)
}
// 如果这里是登录信息过期,那么应该给个弹窗提示什么的,最后都应该重定向到登录页面
return res.data
}, (error: any) => {
console.log(`%c 接口异常 `, 'background-color:orange;color: #FFF;border-radius: 4px;', error);
})
export default request;
export const $get = (url: string, params = {}) => {
return request.get(url, {
params
})
}
export const $post = (url: string, params = {}) => {
return request.post(url, params)
}
去登录页面做一下测试:
测试不同环境下的域名前缀,如果不一样,则说明配置成功了~https://api.vvhan.com/testapi/saorao/login
https://api.vvhan.com/releaseapi/saorao/login
https://api.vvhan.com/api/saorao/login
整体布局将路由模块中的路径转为菜单显示在左边;上面部分是用户信息,以及可以退出;切换路由,会出现一个类似浏览器的tab,可以点击tab切换,也可以关闭当前页面;最后是页面的主体内容显示。先看看菜单组件:
接下来是顶部栏信息:
接下来是关于导航栏tab的开发了,这里我们运用到了pinia来做状态管理,安装之后我们先在plugins/index.ts中注册:import { createPinia } from 'pinia';
// ...
// 注册store (建议这个放在所有注册的首位,方便其他插件可能会用到它)
app.use(createPinia());
声明关于导航啦tab的store,在src/store/tagViews.ts中:import { defineStore } from 'pinia';
import { IRouterItem } from '@/types/menu'
export const useTagViewsStore = defineStore('tagViews', {
state: () => {
return {
// 访问过的页面
visitedViews: [] as IRouterItem[],
// 当前访问的页面
activitedView: {} as IRouterItem
}
},
actions: {
// 新增页面
addVisitedViews(view: IRouterItem){
const item = this.visitedViews.find((item) => item.path === view.path)
if(item) return;
this.visitedViews.push(view);
},
// 删除页面
deleteVisitedViews(index: number) {
this.visitedViews.splice(index, 1);
},
// 高亮某个页面
setActivitedView(view: IRouterItem) {
this.activitedView = view
}
}
})
tagViews组件的源码如下:
pinia的使用比vuex简单了很多,最大的区别就是*mutations*不再存在了。路由拦截一般我们登录之后会将pinia中关于用户的信息进行更新,还会将一些信息进行加密之后存放在localstorage中。未登录的用户我们得拦截他们进入系统,并重定向到登录页面。(实际项目中还得考虑页面权限的拦截问题)// router/hooks/index.ts
import type { Router } from 'vue-router'
import { USERINFO } from '@/constants/localstorage'
const routerHook = (router: Router) => {
router.beforeEach(to => {
if(to.path === '/login') {
// 可以做一些清空登录信息的操作, 比如跟pinia相关的等操作
localStorage.removeItem(USERINFO);
return true;
}else{
// 在这里可以判断用户是否登录,跳转的某个页面是否有权限,这里只是粗略写一下
const info = localStorage.getItem(USERINFO);
if(!info) {
return { name: 'Login' }
}
}
})
}
export default routerHook;
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import routerHook from './hooks/index'
// 注册路由
const router = createRouter({
history: createWebHashHistory(),
routes
})
routerHook(router)
export default router
修改一下登录页面,模拟一下:const onSubmit = async () => {
await formRef.value.validate(async (valid: boolean, fields: {[key: string]: any}) => {
if (valid) {
// 一般这种情况下,localStorage中存储的信息不能太重要,且需要加密,还应该更新pinia中的用户信息
const info = {
name: 'Armouy'
}
localStorage.setItem(USERINFO, JSON.stringify(info))
router.push('/')
} else {
console.log('error submit!', fields)
}
})
}
未登录情况下都会重定向到登录页面。打包npm run build:production
npm run preview
参考链接Eslint:vue3项目添加eslint(standard规则)vite官网如有错误,欢迎指出,感谢阅读~
相关文章:
C#获取前置窗体的句柄,并通过句柄获取到前置进程信息。
汇编输入int 16h
前端减少代码量的技巧与实践
【pytorch】nn.utils.rnn.pad_sequence的使用
深入浅出设计模式 - 观察者模式
JAVA开发(外部接口调用授权问题记录总结)
大数据云计算,人工智能概念兴起,多相Buck电源不容错过
K8s是什么?
大数据赋能交通业务管理——远眺智慧交通集成管控系统
Flutter中导航栏和状态栏设置成透明
重磅发布
《银行业跨网数据安全交换白皮书》免费下载!
什么是首选域设置
react antd阻止Checkbox事件冒泡(折叠面板标题中增加复选框,阻止点击复选框折叠面板展开/折叠)
Neo4j的简单使用
上色技巧笔记
一个基于 SpringCloud 微服务架构的前后端分离博客系统
机器学习之朴素贝叶斯(Naive Bayes)
你给企业创建百科了吗?5分钟带你看懂创建企业百度百科的实用技巧和注意事项
360手机驱动提取 360手机驱动安装 360手机高通驱动
【已解决】执行apt-get update报错404 Not Found的解决方案——docker 镜像下安装报错
JSONUtil.toJsonStr 时间变成了时间戳
如何使用ChatGPT处理excel
Ubuntu TensorRT安装
服务器设置tomcat开机自启动(cmd命令行语句,tomcat注册到服务里)
Redis之哨兵模式以及RedisTemplate的使用
JAVA BigDecimal 常用来解决精确的浮点数运算不精确的问题
Spring 2023面试题(1)--事务的隔离级别
微服务架构介绍2
datav和echarts一起使用时,在datav的组件里获取不到dom元素,导致无法渲染echarts
数据结构与算法:滑动窗口题目的解题框架
20230524 taro+vue3+webpack5+pdfjs时打包pdfjs进不来的问题
智能优化算法改进策略之局部搜索算子(二)—模式搜索(以正余弦算法和灰狼算法为研究对象)
二维码名片制作:MECARD 和 vCard
echarts折线图背景空白太大
Apache HttpComponents 5
python中的切片
继骨传导耳机之后,新发布开放式耳机又成断货王!2年3代爆款,南卡怎么吸引年轻人?
MTR命令:网络诊断的得力助手
基于遗传算法(GA)的多旅行商问题(MTSP)
ubuntu上升级python从python3.7到python3.8
c++等待/睡眠函数
Linux下std::ifstream成员函数对应系统调用验证
企业管理中最大的难点和痛点是什么?怎么解决?
将当前conda环境导出为yaml文件
在Ubuntu系统上安装带有http_ssl_module模块的完整nginx版本的教程
使用预计算的纹理替换Hololens 2屏幕的内容
轻松搞定 Git
【力扣】144、二叉树的前序遍历
魔兽世界自己架设私人服登录不了服务器
短视频seo源码部署打包分享---开源
Oracle select 和read的权限
Uniapp 版本更新
Java正则表达式捕获组
快速小巧的粘贴应用程序Hasty Paste
C++ multimap 的使用
你哪来这么多事(六):职工信息查找
hql调用mysql存储过程_hibernate调用mysql存储过程
ALD技术,相机去噪,图像传感器
文章目录搭建后台管理系统模板项目的资源地址项目初始化2.1.1环境准备2.1.2初始化项目2.2项目配置一、eslint配置1.1vue3环境代码校验插件1.2修改.eslintrc.cjs配置文件1.3.eslintignore忽略文件1.4运行脚本二、配置**prettier**2.1安装依赖包2.2.prettierrc.json添加规则2.3.prettierignore忽略文件三、配置stylelint3.1`.stylelintrc.cjs`**配置文件**3.2.stylelintignore忽略文件3.3运行脚本四、配置husky五、配置commitlint六、强制使用pnpm包管理器工具三、项目集成3.1集成element-plus3.2src别名的配置3.3环境变量的配置3.4SVG图标配置3.4.1svg封装为全局组件3.5集成sass3.6mock数据3.7axios二次封装3.8API接口统一管理3.9 路由模板配置P303.10登录页面 p31pinia使用 P333.11 布局layout静态组件3.12 logo封装组件3.13 根据数据实现菜单动态显示 P39-42关于一级二级路由显示的理解3.14 顶部tabbar组件 P433.14 菜单折叠 P443.15 顶部面包屑动态展示 P453.16 刷新 以及全屏 P46-474 业务逻辑4.1 Token 退出登录 路由鉴权 P48-51路由鉴权使用真实数据而不是mock的,需要配置代理服务器4.2 品牌管理组件 P54-63el-table-column的插槽分页器使用文件上传细节el-form校验4.3 平台属性组件 P64-75三级分类全局组件SPU模块 P76项目的资源地址贾成豪老师代码仓库地址:https://gitee.com/jch1011/vue3_admin_template-bj1.git项目在线文档:服务器域名:http://sph-api.atguigu.cnswagger文档:http://139.198.104.58:8209/swagger-ui.htmlhttp://139.198.104.58:8212/swagger-ui.html#/echarts:国内镜像网站https://www.isqqw.com/echarts-doc/zh/option.html#titlehttp://datav.aliyun.com/portal/school/atlas/area_selector说明: 具体代码可以在贾成豪老师代码仓库git clone,笔记只记录做的过程中踩坑或者比较重要的知识点项目初始化今天来带大家从0开始搭建一个vue3版本的后台管理系统。一个项目要有统一的规范,需要使用eslint+stylelint+prettier来对我们的代码质量做检测和修复,需要使用husky来做commit拦截,需要使用commitlint来统一提交规范,需要使用preinstall来统一包管理工具。下面我们就用这一套规范来初始化我们的项目,集成一个规范的模版。2.1.1环境准备node v16.14.2 使用vite必须保证Node版本16+node -v查看node版本pnpm 8.0.02.1.2初始化项目本项目使用vite进行构建,vite官方中文文档参考:cn.vitejs.dev/guide/pnpm:performant npm ,意味“高性能的 npm”。pnpm由npm/yarn衍生而来,解决了npm/yarn内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为“最先进的包管理工具”pnpm安装指令npm i -g pnpm使用pnpm创建vite项目 项目初始化命令:pnpm create viteSelect a framework:: 用什么框架,选vueSelect a variant: 选择语法 TS项目初始化完毕,需要运行cd sgg-vue3 进入项目根目录pnpm install 项目内部没有node_modules,需要下载安装全部依赖pnpm run dev 启动项目运行在 http://127.0.0.1:5173/,它运行起来不会自动在浏览器端打开如何自动打开?项目根目录下package,json文件, 添加在scripts脚本dev运行添加--open即可2.2项目配置一、eslint配置eslint中文官网:http://eslint.cn/ESLint最初是由Nicholas C. Zakas 于2013年6月创建的开源项目。它的目标是提供一个插件化的javascript代码检测工具首先安装eslint√ How would you like to use ESLint? · 以检查eslint语法并且发现problems发方式使用eslint√ What type of modules does your project use? · 项目采用什么模块式开发,选择JavsScript modules esm√ Which framework does your project use? · 使用vue框架开发√ Does your project use TypeScript? ·使用TS 语法Yes√ Where does your code run? · 运行在browser,而不是Node端√ What format do you want your config file to be in? · 配置文件格式是 JavaScript文件The config that you’ve selected requires the following dependencies:检查语法之类需要安装三个插件,选择yes这些插件之类选择pnpm 包管理进行下载安装项目根目录下会多一个配置文件.eslintrc.cjs,如下/module.exports = {//运行环境配置: 在浏览器端运行,以es2021标准检查语法"env": { "browser": true,//浏览器端"es2021": true,//es2021},//规则继承"extends": [ //全部规则默认是关闭的,这个配置项开启推荐规则,推荐规则参照文档// 开启推荐规则,即eslint推荐的语法,我们都使用//比如:函数不能重名、对象不能出现重复key"eslint:recommended",//vue3语法规则"plugin:vue/vue3-essential",//ts语法规则"plugin:@typescript-eslint/recommended"],//要为特定类型的文件指定处理器"overrides": [],//指定解析器:解析器//Esprima 默认解析器//Babel-ESLint babel解析器//@typescript-eslint/parser ts解析器"parser": "@typescript-eslint/parser",//指定解析器选项"parserOptions": {"ecmaVersion": "latest",//校验ECMA最新版本"sourceType": "module"//设置为"script"(默认),或者"module"代码在ECMAScript模块中},//ESLint支持使用第三方插件。在使用插件之前,您必须使用npm安装它//该eslint-plugin-前缀可以从插件名称被省略"plugins": ["vue","@typescript-eslint"],//eslint规则"rules": {}
}
1.1vue3环境代码校验插件检测vue# 让所有与prettier规则存在冲突的Eslint rules失效,并使用prettier进行代码检查
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
# 运行更漂亮的Eslint,使prettier规则优先级更高,Eslint优先级低
"eslint-plugin-prettier": "^4.2.1",
# vue.js的Eslint插件(查找vue语法错误,发现错误指令,查找违规风格指南
"eslint-plugin-vue": "^9.9.0",
# 该解析器允许使用Eslint校验所有babel code
"@babel/eslint-parser": "^7.19.1",
安装指令直接复制命令pnpm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel/eslint-parser
1.2修改.eslintrc.cjs配置文件我们需要对生成的.eslintrc.cjs配置文件进行一些修改// @see https://eslint.bootcss.com/docs/rules/module.exports = {env: {browser: true,es2021: true,node: true,jest: true,},/* 指定如何解析语法 */parser: 'vue-eslint-parser',/** 优先级低于 parse 的语法解析配置 */parserOptions: {ecmaVersion: 'latest',sourceType: 'module',parser: '@typescript-eslint/parser',jsxPragma: 'React',ecmaFeatures: {jsx: true,},},/* 继承已有的规则 */extends: ['eslint:recommended','plugin:vue/vue3-essential','plugin:@typescript-eslint/recommended','plugin:prettier/recommended',],plugins: ['vue', '@typescript-eslint'],/** "off" 或 0
==>
关闭规则* "warn" 或 1
==>
打开的规则作为警告(不影响代码执行)* "error" 或 2
==>
规则作为一个错误(代码不能执行,界面报错)*/rules: {// eslint(https://eslint.bootcss.com/docs/rules/)'no-var': 'error', // 要求使用 let 或 const 而不是 var'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off','no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off','no-unexpected-multiline': 'error', // 禁止空余的多行'no-useless-escape': 'off', // 禁止不必要的转义字符// typeScript (https://typescript-eslint.io/rules)'@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量'@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore'@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型'@typescript-eslint/no-non-null-assertion': 'off','@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。'@typescript-eslint/semi': 'off',// eslint-plugin-vue (https://eslint.vuejs.org/rules/)'vue/multi-word-component-names': 'off', // 要求组件名称始终为 “-” 链接的单词'vue/script-setup-uses-vars': 'error', // 防止
main.ts引入注册import SvgIcon from '@/components/SvgIcon/index.vue';
app.component('SvgIcon', SvgIcon)
但是如果我们的插件特别多,需要这样引入注册很多次,因此下面使用插件在src\components文件夹目录下创建一个index.ts文件:用于注册components文件夹内部全部全局组件!!!// 引入需要注册为全局的组件
import SvgIcon from './SvgIcon/index.vue';
import type { App, Component } from 'vue';
// 定义一个对象,属性是字符串,属性值是组件
const components: { [name: string]: Component } = { SvgIcon };
export default {install(app: App) {// 遍历对象,进行注册组件Object.keys(components).forEach((key: string) => {app.component(key, components[key]);})}
}在入口文件引入src/index.ts文件,通过app.use方法安装自定义插件import gloablComponent from './components/index';
app.use(gloablComponent);
3.5集成sass我们目前在组件内部已经可以使用scss样式,因为在配置styleLint工具的时候,项目当中已经安装过sass sass-loader,因此我们再组件内可以使用scss语法!!!需要加上lang=“scss”
接下来我们为项目添加一些全局的样式在src/styles目录下创建一个index.scss文件,当然项目中需要用到清除默认样式,因此在index.scss引入reset.scss,// 引入清除样式
@import './reset.scss'
reset.scss中的代码是下图的code中,去该网站粘贴即可在入口文件引入import '@/styles'
但是你会发现在src/styles/index.scss全局样式文件中没有办法使用$变量.因此需要给项目中引入全局变量$.在style/variable.scss创建一个variable.scss文件!// 给项目提供全局变量$base-color: red
在vite.config.ts文件配置如下:export default defineConfig((config) => {// scss全局变量配置 全局变量配置在/variable.scsscss: {preprocessorOptions: {scss: {javascriptEnabled: true,additionalData: '@import "./src/styles/variable.scss";',},},},}
}
@import "./src/styles/variable.less";后面的;不要忘记,不然会报错!配置完毕你会发现scss提供这些全局变量可以在组件样式中使用了!!!3.6mock数据这是之前的配置项,先保存记录一下import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),createSvgIconsPlugin({// Specify the icon folder to be cached 以后矢量图标就放在src/assets/iconsiconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],// Specify symbolId formatsymbolId: 'icon-[dir]-[name]',}),],resolve: {alias: {'@': path.resolve('./src'), // // 相对路径别名配置,使用 @ 代替 src},},// scss全局变量配置css: {preprocessorOptions: {scss: {javascriptEnabled: true,additionalData: '@import "./src/styles/variable.scss";',},},},
})
安装依赖:https://www.npmjs.com/package/vite-plugin-mockpnpm install -D vite-plugin-mock mockjs
解决过程(尝试但失败):更改stylelint的版本,降到14pnpm uninstall stylelint 安装低版本pnpm i stylelint@14.16.1后面发现其他依赖于stylelint的需要stylelint的15版本,于是降低其他的版本,最后还是没成功解决最终:上面说是styleint-config-prettier需要stylelint的[11.x,15)版本,我改变了styleint-config-prettier的版本(9.05—>9.0.4)pnpm install stylelint-config-prettier@9.0.4在 vite.config.js 配置文件启用插件。import { UserConfigExport, ConfigEnv } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
import vue from '@vitejs/plugin-vue'
export default ({ command })=> {return {plugins: [vue(),viteMockServe({localEnabled: command === 'serve',}),],}
}
大概原因:(在路径node_modules/vite-plugin-mock/dist/index.mjs下)文件中的代码中使用了require关键字,但是在浏览器环境下,这个关键字是不被支持的。解决这个问题的方法是确保在运行vite的时候,vite-plugin-mock插件没有被引入。你可以尝试在vite.config.js配置文件中排除掉这个插件,或者卸载这个插件,这样就可以解决这个错误。卸载了我还怎么用!!!!! ReferenceError: require is not definedat cleanRequireCache (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite-plugin-mock@3.0.0_esbuild@0.17.19_mockjs@1.1.0_vite@4.3.9/node_modules/vite-plugin-mock/dist/index.mjs:128:3)at getMockConfig (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite-plugin-mock@3.0.0_esbuild@0.17.19_mockjs@1.1.0_vite@4.3.9/node_modules/vite-plugin-mock/dist/index.mjs:157:3)at createMockServer (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite-plugin-mock@3.0.0_esbuild@0.17.19_mockjs@1.1.0_vite@4.3.9/node_modules/vite-plugin-mock/dist/index.mjs:52:20)at configResolved (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite-plugin-mock@3.0.0_esbuild@0.17.19_mockjs@1.1.0_vite@4.3.9/node_modules/vite-plugin-mock/dist/index.mjs:246:16)at file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite@4.3.9_@types+node@20.2.5_sass@1.62.1/node_modules/vite/dist/node/chunks/dep-e8f070e8.js:64256:28at Array.map ()at resolveConfig (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite@4.3.9_@types+node@20.2.5_sass@1.62.1/node_modules/vite/dist/node/chunks/dep-e8f070e8.js:64256:14)at async createServer (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite@4.3.9@types+node@20.2.5_sass@1.62.1/node_modules/vite/dist/node/chunks/dep-e8f070e8.js:63274:20)at async restartServer (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite@4.3.9_@types+node@20.2.5_sass@1.62.1/node_modules/vite/dist/node/chunks/dep-e8f070e8.js:63667:21)at async handleHMRUpdate (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite@4.3.9_@types+node@20.2.5_sass@1.62.1/node_modules/vite/dist/node/chunks/dep-e8f070e8.js:39941:13)at async onHMRUpdate (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite@4.3.9_@types+node@20.2.5_sass@1.62.1/node_modules/vite/dist/node/chunks/dep-e8f070e8.js:63419:17)at async FSWatcher. (file:///C:/Users/ytm/Desktop/sgg-glzx-vue3/sgg-vue3/node_modules/.pnpm/vite@4.3.9_@types+node@20.2.5_sass@1.62.1/node_modules/vite/dist/node/chunks/dep-e8f070e8.js:63438:9)
解决:降低版本!!!import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { viteMockServe } from 'vite-plugin-mock'// https://vitejs.dev/config/
export default defineConfig(({ command }) => {return {plugins: [vue(),createSvgIconsPlugin({// Specify the icon folder to be cached 以后矢量图标就放在src/assets/iconsiconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],// Specify symbolId formatsymbolId: 'icon-[dir]-[name]',}),viteMockServe({localEnabled: command === 'serve',}),],resolve: {alias: {'@': path.resolve('./src'), // // 相对路径别名配置,使用 @ 代替 src},},// scss全局变量配置css: {preprocessorOptions: {scss: {javascriptEnabled: true,additionalData: '@import "./src/styles/variable.scss";',},},},}
})在根目录创建mock文件夹:去创建我们需要mock数据与接口!!!在mock文件夹内部创建一个user.ts文件//用户信息数据
function createUserList() {return [{userId: 1,avatar:'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',username: 'admin',password: '111111',desc: '平台管理员',roles: ['平台管理员'],buttons: ['cuser.detail'],routes: ['home'],token: 'Admin Token',},{userId: 2,avatar:'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',username: 'system',password: '111111',desc: '系统管理员',roles: ['系统管理员'],buttons: ['cuser.detail', 'cuser.user'],routes: ['home'],token: 'System Token',},]
}export default [// 用户登录接口{url: '/api/user/login',//请求地址method: 'post',//请求方式response: ({ body }) => {//获取请求体携带过来的用户名与密码const { username, password } = body;//调用获取用户信息函数,用于判断是否有此用户const checkUser = createUserList().find((item) => item.username === username && item.password === password,)//没有用户返回失败信息if (!checkUser) {return { code: 201, data: { message: '账号或者密码不正确' } }}//如果有返回成功信息const { token } = checkUserreturn { code: 200, data: { token } }},},// 获取用户信息{url: '/api/user/info',method: 'get',response: (request) => {//获取请求头携带tokenconst token = request.headers.token;//查看用户信息是否包含有次token用户const checkUser = createUserList().find((item) => item.token === token)//没有返回失败的信息if (!checkUser) {return { code: 201, data: { message: '获取用户信息失败' } }}//如果有返回成功信息return { code: 200, data: {checkUser} }},},
]
安装axiospnpm install axios
最后通过axios测试接口!!!Mock成功在main.tsimport axios from 'axios'
// 登录
axios({url: '/api/user/info',method: 'get',data: {username: 'admin',password: '111111',},
})
3.7axios二次封装在开发项目的时候避免不了与后端进行交互,因此我们需要使用axios插件实现发送网络请求。在开发项目的时候我们经常会把axios进行二次封装。目的:1:使用请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)2:使用响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)在根目录下创建utils/request.tsiimport axios from 'axios'
import { ElMessage } from 'element-plus'
//创建axios实例
let request = axios.create({// 基础路径会携带/apibaseURL: import.meta.env.VITE_APP_BASE_API,timeout: 5000,
})
//请求拦截器
request.interceptors.request.use((config) => {// config配置对象,headers请求头经常给度武器携带公共参数return config
})
//响应拦截器
request.interceptors.response.use(// 成功的回调,可以把data取出来(response) => {return response.data},(error) => {//失败回调,处理网络错误let msg = ''let status = error.response.statusswitch (status) {case 401:msg = 'token过期'breakcase 403:msg = '无权访问'breakcase 404:msg = '请求地址错误'breakcase 500:msg = '服务器出现问题'breakdefault:msg = '无网络'}ElMessage({type: 'error',message: msg,})return Promise.reject(error)},
)
export default request
axios封装测试app.vue
需要修改的有一个地方,进行mock的url是/api/xxx而不是/dev-api/xxx3.8API接口统一管理在开发项目的时候,接口可能很多需要统一管理。在src目录下去创建api文件夹去统一管理项目的接口;在api下新建一些文件夹如user, product,不同模块下写相关的请求/api/user/type.ts使用改文件对请求和响应相关的参数进行类型的限定,主要参考后端的接口文档进行限定/api/user/type.ts对数据进行了修改,和上图不一致// 登录接口需要携带参数类型
export interface loginFormData {username: stringpassword: string
}
//定义全部接口返回数据都拥有ts类型
export interface ResponseData {code: numbermessage: stringok: boolean
}
//定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {data: string
}//定义获取用户信息返回数据类型
export interface userInfoReponseData extends ResponseData {data: {routes: string[]buttons: string[]roles: string[]name: stringavatar: string}
}/api/user/index.ts对请求响应的参数设定限制,使用到了type.ts文件中的数据类型// 统一管理用户相关的接口
// 统一管理用户相关的接口
import request from '@/utils/request'
import type {loginFormData,loginResponseData,userInfoReponseData,
} from './type'
// 统一接口管理
enum API {LOGIN_URL = '/user/login',USERINFO_URL = '/user/info',
}
// 暴露请求函数
export const reqLogin = (data: loginFormData) =>request.post
(API.LOGIN_URL, data)
export const reqUserInfo = () =>request.post(API.USERINFO_URL)
3.9 路由模板配置P30路由组件有user, 404,商品管理等,所有路由组件都放在src/views下,另外需要配置路由,根据匹配对应的路由组件以及一些路由导航守卫1-下载 pnpm i vue-router2- 创建路由组件(参考尚硅谷源码),这些路由组件的页面暂时写上如登录等信息区分3- 创建路由,这个路由负责地址栏如出现/login就跳转login页面src/router/index.tsimport { createRouter, createWebHashHistory } from 'vue-router'
import { constantRoute } from './routes'
// 创建路由
const router = createRouter({// 路由模式history: createWebHashHistory(),routes: constantRoute,scrollBehavior() {//
路由跳转时,滚动条在最上侧(显示最上面的),而不是return {left: 0,top: 0,}},
})
export default router常量路由是所有用户都可以访问,后面会根据用户的角色决定哪些路由可以访问(动态路由)// 对外暴露配置路由(常量路由)全部用户可以访问
// 对外暴露配置路由(常量路由)全部用户可以访问
export const constantRoute = [// 登录{path: '/login',component: () => import('@/views/login/index.vue'),name: 'login',meta: {title: '登录', //菜单标题hidden: true, //代表路由标题在菜单中是否隐藏
true:隐藏 false:不隐藏},},{//
登录成功展示数据的路由path: '/',component: () => import('@/layout/index.vue'),name: 'layout',meta: {title: '',hidden: false,},},{path: '/404',component: () => import('@/views/404/index.vue'),meta: {title: '404',hidden: true,},},{path: '/screen',component: () => import('@/views/screen/index.vue'),name: 'screen',meta: {hidden: false,title: '数据大屏',},},{path: '/:pathMatch(.*)',redirect: '/404',name: 'any',},
]
4-main.ts注册import router from './router'
app.use(router)
3.10登录页面 p31登录组件的静态页面Hello
欢迎来到硅谷甄选
登录
pinia使用 P33登录成功后,后端返回TOKEN,需要存储,使用pinia(非持久化) + 本地持久化存储。此外还有其他的信息都需要存储在pinia安装pnpm i pinia分为大仓库src/store/index.cs//仓库大仓库
import { createPinia } from 'pinia';
//创建大仓库
const pinia = createPinia();
//对外暴露:入口文件需要安装仓库
export default pinia;
在main.ts引入注册大仓库import pinia from '@/store'
app.use(pinia)
小仓库src/store/modules/XXX,当前处理user相关的,建立小仓库src/store/modules/user.tsimport { defineStore } from 'pinia'
//创建用户小仓库
const useUserStore = defineStore('User', {// 小仓库存储数据state: () => {return {token: localStorage.getItem('TOKEN'),}},// 异步|逻辑 地方actions: {//用户登录的方法async userLogin() {},},getters: {},
})
//对外暴露获取小仓库方法
export default useUserStore
在user/index.vuelet login = async () => {try {await useStore.userLogin(loginForm)console.log('登录成功')}catch {console.log('登录失败')}
}
登录的逻辑代码如下:在user/index.vueimport { reactive } from 'vue'
import { Lock, User } from '@element-plus/icons-vue'
import useUserStore from '@/store/modules/user.ts'
import { inputEmits } from 'element-plus/es/components/index.js'
import { useRoute, useRouter } from 'vue-router'
const useStore = useUserStore()
let loginForm = reactive({ username: 'admin', password: '111111' })
let $route = useRoute()
let $router = useRouter()
let login = async () => {try {console.log(loginForm)await useStore.userLogin(loginForm)console.log('登录成功')$router.push({ path: '/' })} catch(error) {console.log(error)}
}
src/store/modules/user.ts//创建用户相关的小仓库
import { defineStore } from 'pinia'
// 引入接口
import { reqLogin } from '@/api/user'
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
import type { loginResponseData } from '@/api/user/type.ts'
//创建用户小仓库
const useUserStore = defineStore('User', {// 小仓库存储数据state: () => {return {token: localStorage.getItem('TOKEN'),}},// 异步|逻辑 地方actions: {//用户登录的方法async userLogin(data: any) {//登录请求const result: loginResponseData = await reqLogin(data)console.log(result)//登录请求:成功200->token//登录请求:失败201->登录失败错误的信息if (result.code == 200) {//pinia仓库存储一下token//由于pinia|vuex存储数据其实利用js对象this.token = result.data//本地存储持久化存储一份SET_TOKEN(result.data as string)//能保证当前async函数返回一个成功的promisereturn Promise.resolve('ok')} else {return Promise.reject(result.data.message)}},},getters: {},
})
//对外暴露获取小仓库方法
export default useUserStore存在一个问题:登录成功后,路由也跳转了,也在主页,但背景图仍然是登录页面,刷新一下背景图就没了解决:下面进行登录的细节处理:校验用户名、密码,登录成功后给提示表单验证部分Hello
欢迎来到硅谷甄选
登录
utils/time//封装一个函数:获取一个结果:当前早上|上午|下午|晚上
export const getTime = () => {let message = '';//通过内置构造函数Datelet hours = new Date().getHours();//情况的判断if (hours <= 9) {message = '早上'} else if (hours <= 12) {message = '上午'} else if (hours <= 18) {message = '下午';} else {message = '晚上'}return message;
}
3.11 布局layout静态组件layout/index.vue布局分三部分:左侧菜单, 右侧上部导航,右侧下部是内容
variable.scss//左侧的菜单的宽度
$base-menu-width:260px;
//左侧菜单的背景颜色
$base-menu-background:#001529;
$base-menu-min-width:50px;// 顶部导航的高度
$base-tabbar-height:50px;//左侧菜单logo高度设置
$base-menu-logo-height:50px;//左侧菜单logo右侧文字大小
$base-logo-title-fontSize:20px;
3.12 logo封装组件比较简单:用到:display: flex; align-items: center;{{ setting.title }}
3.13 根据数据实现菜单动态显示 P39-42{{ item.meta.title }}{{ item.children[0].meta.title }}{{ item.meta.title }}
关于一级二级路由显示的理解一级路由在children外侧,二级路由在chilren内部(针对路由匹配而言)。一级路由在App.vue中通过router-view显示一级路由对应的组件二级路由的显示一般嵌套在某个组件内部显示,也是通过router-view显示二级路由的内容。如我们在layout组件中显示二级路由的内容–如商品管理3.14 顶部tabbar组件 P43
3.14 菜单折叠 P44菜单折叠,顶部导航和内容区宽度增加,同时,两者使用定位实现的,所以也需要改变left偏移量。在折叠时增加动画效果。另外el-menu本身有个折叠属性。折叠时鼠标放菜单上会显示折叠的子菜单。是否折叠,通过变量来控制,将变量存储在仓库中(不用仓库实现通信,用其他的组件间通信也可以,但麻烦)src\layout\tabbar\breadcrumb\index.vuecomponent可以动态显示哪个图标组件
3.15 顶部面包屑动态展示 P45路由对象身上有一些信息如params参数 元信息meta、matched匹配的路由 等信息面包屑主要用matched.点击面包屑会进行跳转对应的路由,利用面包屑的:to=“item.path”另外涉及一些重定向,在路由匹配的配置里修改sgg-vue3\src\layout\tabbar\breadcrumb\index.vue
{{ item.meta.title }}
3.16 刷新 以及全屏 P46-47刷新业务涉及组件间通信,使用pinia进行通信仓库存储refsh=false,点击刷新按钮将refsh=true, 同时将正在显示的组件(Main组件内某个组件如属性管理)销毁。销毁后还需要重建,利用watch + nextTick 。watch检测refsh, 在nextTick中重建(是否重根据另一个变量而不是refsh)import { nextTick, ref, watch } from 'vue'
import useLayOutSettingStore from '@/store/modules/setting'
let layOutSettingStore = useLayOutSettingStore()
//控制当前组件是否销毁重建
let flag = ref(true)
watch(() => layOutSettingStore.refsh,() => {flag.value = falsenextTick(() => {flag.value = true})},
)
全屏const fullScreen = () => {//DOM对象的一个属性:可以用来判断当前是不是全屏模式[全屏:true,不是全屏:false]let full = document.fullscreenElementif (full) {// 本身是全屏则退出全屏document.exitFullscreen()} else {// 进入全屏document.documentElement.requestFullscreen()}
}
4 业务逻辑4.1 Token 退出登录 路由鉴权 P48-51登录成功后,后端返回token,需要进行存储(pinia + localStorage)以后发送请求将token带上(请求拦截器加在请求头),凭借token去后端拿数据如用户头像账号,用户权限等。将用户信息存储在pinia中 + sessionStorage, 展示用户信息如头像,同时根据用户信息决定用户能访问哪些路由当用户退出登录,需要将token清除(pina中的 + localStorage中remove)登录成功,保存token用户登录成功,在首页src\views\home\index.vue的Mounted生命周期中发送网路请求获取数据
sgg-vue3\src\store\modules\user.ts
// 获取用户信息async userInfo() {const result: userInfoReponseData = await reqUserInfo()if (result.code == 200) {this.username = result.data.namethis.avatar = result.data.avatarthis.buttons = result.data.buttons}},// 退出登录async userLogout() {// //退出登录请求const result = await reqLogout()if (result.code == 200) {//后台真实接口this.token = ''this.username = ''this.avatar = ''REMOVE_TOKEN()return 'ok'} else {return Promise.reject(new Error(result.message))}},
后台接口对应APIsgg-vue3\src\api\user\index.ts// 统一管理用户相关的接口
import request from '@/utils/request'
import type {loginFormData,loginResponseData,userInfoReponseData,
} from './type'
// 统一接口管理
enum API {LOGIN_URL = '/admin/acl/index/login',USERINFO_URL = '/admin/acl/index/info',LOGOUT_URL = '/admin/acl/index/logout',
}
// 暴露请求函数
export const reqLogin = (data: loginFormData) =>request.post(API.LOGIN_URL, data)
export const reqUserInfo = () =>request.get(API.USERINFO_URL)
//退出登录
export const reqLogout = () => request.post(API.LOGOUT_URL)
路由鉴权注意:一定在main.ts引入permission.ts文件import './permission.ts'
sgg-vue3\src\permission.tsimport router from '@/router'
import setting from './setting'import nprogress from 'nprogress'
//引入进度条样式
import 'nprogress/nprogress.css'import pinia from './store'
import useUserStore from './store/modules/user';
const userStore = useUserStore(pinia)// import useUserStore from '@/store/modules/user.ts'
// const userStore = useUserStore()
//
router.beforeEach(async (to: any, from: any, next: any) => {document.title = `${setting.title} - ${to.meta.title}`nprogress.start()// token 作为用户登录的唯一凭证,有token就代表登录成功const token = userStore.token// 根据token获取用户相关的信息(如果token过期则获取用户信息失败,需要重新登录)const username = userStore.username// 已经登录if (token) {// 登录成功,访问login,不能访问,指向首页if (to.path == '/login') {next({ path: '/' })} else {//登录成功访问其余六个路由(登录排除)//有用户信息if (username) {//放行next()} else {try {//如果没有用户信息,在守卫这里发请求获取到了用户信息再放行await userStore.userInfo()//放行next()} catch (error) {//token过期:获取不到用户信息了//用户手动修改本地存储token//退出登录->用户相关的数据清空await userStore.userLogout()next({ path: '/login', query: { redirect: to.path } })}}}} else {//用户未登录判断if (to.path == '/login') {next()} else {next({ path: '/login', query: { redirect: to.path } })}}
})router.afterEach((to: any, from: any) => {nprogress.done()
})使用真实数据而不是mock的,需要配置代理服务器4.2 品牌管理组件 P54-63先自己做的,遇到不确定的看了源代码,再不懂看了部分视频el-table-column的插槽el-table-column的插槽内部添加编辑、删除按钮:使用细节为什么使用插槽呢?可以直接在el-table-column内部添加el-button等组件,但是我需要给el-button的如点击事件传递参数分页器使用let pageNo = ref(1)
let limit = ref(3)
//存储已有品牌数据总数
let total = ref(0), total, sizes":total="total":background="true"@size-change="sizeChange"@current-change="getHasTrademark"/>//获取已有品牌的接口封装为一个函数:在任何情况下向获取数据,调用次函数即可
const getHasTrademark = async (pager: number = 1) => {// pageNo 1
---pager 4
pageNo是之前的当前页码, pager是用户点击某页的页码// console.log("pageNo",pageNo.value, " ---pager", pager)pageNo.value = pagerlet result: TradeMarkResponseData = await reqHasTrademark(pageNo.value,limit.value,)if (result.code == 200) {total.value = result.data.totaltrademarkArr.value = result.data.records}
}
const sizeChange = (paseSize) => {// console.log(paseSize)getHasTrademark()
}
文件上传细节
// 品牌LOGO上传成功的回调
const handleAvatarSuccess = (response: any, uploadFile: UploadFile) => {trademarkParams.logoUrl = response.dataconsole.log('品牌LOGO上传成功')formRef.value.clearValidate('logoUrl')
}
//上传图片组件->上传图片之前触发的钩子函数
const beforeAvatarUpload = (rawFile: any) => {console.log('上传图片之前触发的钩子函数')console.log(rawFile)if (rawFile.type == 'image/png'
rawFile.type == 'image/jpeg'
rawFile.type == 'image/gif') {if (rawFile.size / 1024 / 1024 < 4) {return true} else {ElMessage({type: 'error',message: '上传文件大小小于4M',})return false}} else {ElMessage({type: 'error',message: '上传文件格式务必PNG|JPG|GIF',})return false}
}
el-form校验
取消确定
const validatorTmName = (rule: any, value: any, callBack: any) => {if (value.trim().length >= 2) {callBack()} else {callBack(new Error('品牌名称位数大于等于两位'))}
}
const validatorLogoUrl = (rule: any, value: any, callBack: any) => {//如果图片上传if (value) {callBack()} else {callBack(new Error('LOGO图片务必上传'))}
}
const rules = {tmName: [{ required: true, trigger: 'blur', validator: validatorTmName }],logoUrl: [{required: true,validator: validatorLogoUrl,},],
}
const formRef = ref()
const confirm = async () => {await formRef.value.validate()let result: any = await reqAddOrUpdateTrademark(trademarkParams)console.log(result)if (result.code == 200) {//关闭对话框dialogFormVisible.value = falseElMessage({type: 'success',message: trademarkParams.id ? '修改品牌成功' : '添加品牌成功',})getHasTrademark(trademarkParams.id ? pageNo.value : 1)}
}校验规则的清除注意事项:为什么要清除校验规则的提示:某次校验是给出提示失败的提示信息后直接关闭diglog,下次打开dialog这些错误的提示信息还存在。所以需要在打开dialog之前清除清除时机:保证打开dialog之前清除清除错误校验可选的具体代码位置:1、打开dialog时(校验规则在el-from组件上,存在打开dialog时el-form还未创建,如下图)2 关闭dialog时,有关闭的回调其他解决方案: 使用ts的?或者nextTick回调:在下一次DOM更新完执行nextTick回调函数const addTrademark = () => {trademarkParams.id = 0trademarkParams.logoUrl = ''trademarkParams.tmName = ''dialogFormVisible.value = true// formRef.value.clearValidate('tmName')// formRef.value.clearValidate('logoUrl')// 方案1// formRef.value?.clearValidate('tmName')// formRef.value?.clearValidate('logoUrl')// 方案2nextTick(() => {formRef.value.clearValidate('tmName')formRef.value.clearValidate('logoUrl')})
}
const updateTrademark = (row: TradeMark) => {// todo 清空校验规则错误提示信息 ????nextTick(() => {formRef.value.clearValidate('tmName')formRef.value.clearValidate('logoUrl')})console.log(row)//对话框显示Object.assign(trademarkParams, row)dialogFormVisible.value = true
}
4.3 平台属性组件 P64-75三级分类全局组件Category组件挂在完毕,发送网络请求获取一级分类数据,当用户通过el-select选择某个一级分类检测el-select的change事件获取二级分类数据同理 获取三级分类数据。这些数据存储在仓库中一旦获取完三级分类数据,就需要展示该三级分类的属性,属性值等,如何监测呢?答: 获取到三级分类是在三级分类组件中(子组件),而展示三级分类的属性是在attr组件(父组件),通过wacth监测三级分类id(attr组件中监视)//监听仓库三级分类ID变化
watch(() => categoryStore.c3Id,() => {//清空上一次查询的属性与属性值attrArr.value = []//保证三级分类得有才能发请求if (!categoryStore.c3Id) return//获取分类的IDgetAttr()},
)
编辑和查看状态的切换中,获得焦点和失去焦点事件element-ui框架中的el-input: 使用v-if控制el-input和div(或者span等不可编辑的)没有触发失去焦点事件原生input的获得焦点和失去焦点:结论:点击某个input是会导致其他input自动失去焦点,然后该元素获得焦点SPU模块 P76SPU:电商术语, 代表一个标准化产品单元华为公司: 品牌名称 华为->产品单元SPU组成:SPU产品品牌名称 如华为SPU描述 华为公司等SPU公司下产品图片SPU销售属性SKU库存量最小单元