项目演示
pnpm安装后,插件路径错误
- yarn/npm 安装时,不会有这个问题~~~
- 直接找到完整路径导入
javascript
import '.pnpm/quill@1.3.7/node_modules/quill/dist/quill.core.css'
import '.pnpm/quill@1.3.7/node_modules/quill/dist/quill.snow.css'
import '.pnpm/quill@1.3.7/node_modules/quill/dist/quill.bubble.css'
// vue中导入模块时,可以省略最外层的node_moduels目录
- 配置别名
javascript
const { defineConfig } = require("@vue/cli-service");
const path = require("path");
// commonjs require => node
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
configureWebpack: {
resolve: {
alias: {
"@pnpm": path.join(
__dirname,
"node_modules/.pnpm/quill@1.3.7/node_modules"
),
},
},
},
});
article.vue
vue
import "@pnpm/quill/dist/quill.core.css";
import "@pnpm/quill/dist/quill.snow.css";
import "@pnpm/quill/dist/quill.bubble.css";
Vite创建项目
bash
# 1. pnpm create vite
# vite-vuex => 项目命名 pc-element-ui
# Vanilla 选择普通模式
# JavaScript
# 2. 安装vue2插件 / 安装vue@2.7.14
# pnpm i @vitejs/plugin-vue2 -D
# pnpm i vue@2.7.14
# 3. 根目录下新建vite.config.js 文件,并配置别名@等
import vue from '@vitejs/plugin-vue2'
import { resolve } from 'path'
export default {
plugins: [vue()],
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
}
}
# 4. 新建src文件夹,把main.js移入进去
# => 记得修改index.html中main.js的路径 src/main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
# 5. src中新建App.vue,写一些代码,测试启动 pnpm dev
<template>
<div id="app">
<h2>Vite-Vuex-Learning</h2>
</div>
</template>
<script>
export default {}
</script>
# 6. 安装 less / vuex (如果要用vue-router也要装)
# pnpm i less less-loader -D => pnpm i sass sass-loader -D
# pnpm i vuex@3.6.2
# pnpm i vue-router@3.6.5 用不到可以不装
scss - less 等css预解析
css
<style scoped lang="scss">
/*
sass 旧 scss 新
1. scss 是 sass3新增的, css的超集
2. scss 和 less 都支持嵌套语法,都可以定义变量,混入等操作
less => @变量名
scss => $变量名
3. stylus 和 sass 语法很像,严格保持缩进,没有分号,没有{}
4. 现在主流 scss / less, stylus也有用到,看公司。。。
*/
// 1. 全局的
$color: skyblue; // less ==> @
$width: 300px;
.box {
/* 局部的变量 */
$width: 100px;
width: 200px;
height: 200px;
background: orange;
.son1 {
/* 找就近的 */
width: $width;
height: 100px;
background: $color;
}
}
混合 mixins 函数
css
// 2. 定义一个scss的混合 mixin
// ==》 相当于是定义一大段公共的代码,一下子导入
// @mixin + @include 使用
// 1. 不带参数
@mixin orangeTheme {
background-color: orange;
color: orange;
border: 1px solid orange;
}
// 2.传参数 $color 默认值
@mixin theme($color: red) {
background-color: $color;
color: $color;
border: 1px solid $color;
}
.box {
$width: 100px;
width: 200px;
height: 200px;
background: black;
.son1 {
width: $width;
height: 100px;
background: $color;
}
.son2 {
width: $width;
height: 100px;
// @include orangeTheme;
@include theme(pink); // 不要加引号!
}
}
调整目录结构-准备
router/index.js
javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
routes: []
})
export default router
store/index.js
javascript
// 1. 导入Vue / Vuex
import Vue from 'vue'
import Vuex from 'vuex'
// 2. Vue.use注册vuex
Vue.use(Vuex)
// 3. 创建一个仓库store
const store = new Vuex.Store({
modules:{
user
}
})
// 4. 导出store,挂载到vue根实例上
export default store
App.vue
javascript
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<style lang="scss">
</style>
新增三个目录
- /api : 存储请求函数模块
- /styles: 样式文件模块
- /utils: 工具函数模块
安装element-ui,导入使用
Element - The world’s most popular Vue UI framework
安装
javascript
pnpm i element-ui
全局导入
javascript
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
// 后台管理系统,公司自己用的。。。全局导入没关系
按需导入
javascript
import 'element-ui/lib/theme-chalk/index.css'
import { Button, Select, Row } from 'element-ui';
Vue.use(Button).use(Row).use(Select)
vue
<el-row>
<el-button plain>朴素按钮</el-button>
<el-button type="primary" plain>主要按钮</el-button>
<el-button type="success" plain>成功按钮</el-button>
<el-button type="info" plain>信息按钮</el-button>
<el-button type="warning" plain>警告按钮</el-button>
<el-button type="danger" plain>危险按钮</el-button>
</el-row>
封装request和storage模块
utils目录下新建 request.js
javascript
pnpm i axios
javascript
/* 封装axios用于发送请求 */
import axios from 'axios'
// 创建一个新的axios实例
const request = axios.create({
baseURL: 'http://interview-api-t.itheima.net/',
timeout: 5000
})
// 添加请求拦截器
request.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 添加响应拦截器
request.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response.data
},
function (error) {
// 对响应错误做点什么
return Promise.reject(error)
}
)
export default request
storage.js
javascript
// 以前 token 令牌,如果存到了本地,每一次都写这么长,太麻烦
// localStorage.setItem(键, 值)
// localStorage.getItem(键)
// localStorage.removeItem(键)
const KEY = 'my-token-element-pc'
// 直接用按需导出,可以导出多个
// 但是按需导出,导入时必须 import { getToken } from '模块名导入'
// 获取
export const getToken = () => {
return localStorage.getItem(KEY)
}
// 设置
export const setToken = (newToken) => {
localStorage.setItem(KEY, newToken)
}
// 删除
export const delToken = () => {
localStorage.removeItem(KEY)
}
路由配置
javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
// 默认打开Layout,就不用配置动态导入了
import Layout from '@/views/layout/index.vue'
// const Layout = () => import('@/views/layout/index.vue')
const Login = () => import('@/views/login/index.vue')
const Dashboard = () => import('@/views/dashboard/index.vue')
const Article = () => import('@/views/article/index.vue')
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
// 1. /dashboard ==> 完整路径的写法
// 2. dashboard 相对路径写法 会自动的拼接父级路由上的路径
path: '/dashboard',
component: Dashboard
},
{
path: '/article',
component: Article
}
]
}
]
})
export default router
登录
form表单
vue
<template>
<div>
<el-card class="my-card">
<!-- header插槽 -->
<template v-slot:header>
<h2>黑马面经运营后台</h2>
</template>
<!-- 默认插槽 -->
<!--
el-form 包裹整个form表单
el-form-item => 每一行,里面包含一些表单元素input button
-->
<el-form label-position="top" label-width="80px">
<el-form-item label="用户名">
<el-input></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary">登录</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
export default {
name: 'LoginIndex'
}
</script>
<style lang="scss" scoped>
.my-card {
margin: 0 auto;
width: 420px;
::v-deep .el-card__header {
background: rgba(114, 124, 245);
text-align: center;
color: white;
padding: 5px 20px;
}
::v-deep .el-form-item__content {
display: flex;
justify-content: center;
.el-button--primary {
background: rgba(114, 124, 245);
border-color: rgba(114, 124, 245);
}
}
// ::v-deep .el-button--primary {
// background: rgba(114, 124, 245);
// border-color: rgba(114, 124, 245);
// }
}
</style>
form表单的校验
javascript
<!--
el-form 包裹整个form表单
el-form-item => 每一行,里面包含一些表单元素input button
1. el-form model & rules
:model 需要绑定一个大对象, 里面有一些各个表单的属性
:rules 绑定一个大对象,里面是每个属性的一些校验规则
{
字段1:[{},{}],
字段2:[{},{}]
}
2. el-form-item prop => 指定这一行是校验的哪个规则,username
3. el-input v-model 输入的内容双向数据绑定
-->
vue
data() {
return {
form: {
username: '',
password: ''
},
rules: {
username: [
// 1. required 非空校验,true必须写
// 2. message 如果不成功,提示什么
// 3. trigger 触发条件 blur / change
{
required: true,
message: '请输入用户名',
trigger: 'blur'
}
],
password: []
}
}
}
javascript
<template>
<div>
<el-card class="my-card">
<!-- header插槽 -->
<template v-slot:header>
<h2>黑马面经运营后台</h2>
</template>
<!-- 默认插槽 -->
<!--
el-form 包裹整个form表单
el-form-item => 每一行,里面包含一些表单元素input button
1. el-form model & rules
:model 需要绑定一个大对象, 里面有一些各个表单的属性
:rules 绑定一个大对象,里面是每个属性的一些校验规则
{
字段1:[{},{}],
字段2:[{},{}]
}
2. el-form-item prop => 指定这一行是校验的哪个规则,username
3. el-input v-model 输入的内容双向数据绑定
-->
<el-form
:model="form"
:rules="rules"
label-position="top"
label-width="80px"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名"
></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary">登录</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
export default {
name: 'LoginIndex',
data() {
return {
form: {
username: '',
password: ''
},
rules: {
username: [
// 1. required 非空校验,true必须写
// 2. message 如果不成功,提示什么
// 3. trigger 触发条件 blur / change
{
required: true,
message: '请输入用户名',
trigger: 'blur'
},
{
min: 3,
max: 5,
message: '长度在 3 到 5 个字符',
trigger: ['change']
}
],
password: [
{
required: true,
message: '请输入密码',
// 这里的change类似于input
trigger: ['blur']
},
{
pattern: /^\w{5,11}$/,
message: '输入5-11位密码',
// 这里的change类似于input
trigger: ['blur']
}
]
}
}
}
}
</script>
<style lang="scss" scoped>
.my-card {
margin: 0 auto;
width: 420px;
::v-deep .el-card__header {
background: rgba(114, 124, 245);
text-align: center;
color: white;
padding: 5px 20px;
}
::v-deep .el-form-item__content {
display: flex;
justify-content: center;
.el-button--primary {
background: rgba(114, 124, 245);
border-color: rgba(114, 124, 245);
}
}
// ::v-deep .el-button--primary {
// background: rgba(114, 124, 245);
// border-color: rgba(114, 124, 245);
// }
}
</style>
表单校验JS
javascript
async login() {
try {
// 校验表单中所有规则
await this.$refs.myForm.validate()
// 下面代码,一定是校验成功才会执行
console.log('校验成功~~')
} catch (e) {
console.log(e)
}
},
reset() {
this.$refs.myForm.resetFields()
}
登录存token
接口: https://www.apifox.cn/apidoc/project-934563/api-19465917
api中封装user.js
javascript
import axios from '@/utils/request'
// 按需导出
export const login = ({ username, password }) => {
return axios.post('/auth/login', {
username,
password
})
}
modules/user.js 封装vuex模块
- 写action
- 写mutation
javascript
import { login } from '@/api/user'
export default {
namespaced: true,
state: () => ({
token: ''
}),
getters: {},
mutations: {
updateUserToken(state, payload) {
state.token = payload
}
},
actions: {
// 1. context ctx 当前store,
// 2. payload
async updateUserToken(ctx, payload) {
const { data } = await login(payload)
console.log(data)
// 如果成功了,应该有个token,提交一个mutation存
ctx.commit('updateUserToken', data.token)
}
}
}
user.js要在store/index中挂载
javascript
// 1. 导入Vue / Vuex
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
// 2. Vue.use注册vuex
Vue.use(Vuex)
// 3. 创建一个仓库store
const store = new Vuex.Store({
modules: {
user // 挂载模块
}
})
// 4. 导出store,挂载到vue根实例上
export default store
login/index.vue中dispatch action
vue
methods: {
async login() {
try {
// 用户名 admin1-10 密码 admin
// 校验表单中所有规则
await this.$refs.myForm.validate()
// 下面代码,一定是校验成功才会执行
// 1. 以前的逻辑,校验成功之后,就可以发送请求
// const res = await login(this.form)
// 2. 现在,我们又可以dispatch一个action,在action中
// 发送请求 => login
this.$store.dispatch('user/updateUserToken', this.form)
console.log('校验成功~~')
} catch (e) {
console.log(e)
}
},
reset() {
this.$refs.myForm.resetFields()
}
}
存token的逻辑
javascript
// 1. token 存vuex中, 方便各个组件模块访问,token属于个人信息
// 2. token 也会存localStorage中, 为了做数据持久化管理
// Vuex中的数据时在内存中的,一旦刷新页面,vuex中数据会丢失,
// 实际开发, 个人信息,token等,从vuex中区
// 存localStorage中的目的,保证页面刷新时,vuex中token等信息还在
// (刷新的时候从localStorage同步到vuex)
修改token
直接通过mutation,在组件中请求数据
组件中dispatch action,在action中请求数据,然后提交
同步token,登录
javascript
import { login } from '@/api/user'
import { setToken } from '@/utils/storage'
export default {
namespaced: true,
state: () => ({
// 当没有的时候,从本地取token
token: getToken() || ''
}),
getters: {},
mutations: {
updateUserToken(state, payload) {
// 更新vuex中token
state.token = payload
// 存到localStorage 一起更新
setToken(payload)
}
},
actions: {
// 在action中操作异步请求
// 1. context ctx 当前store,
// 2. payload
async updateUserToken(ctx, payload) {
const { data } = await login(payload)
console.log(data)
// 如果成功了,应该有个token,提交一个mutation存
ctx.commit('updateUserToken', data.token)
}
}
}
login/index.vue
javascript
async login() {
try {
// 用户名 admin1-10 密码 admin
// 校验表单中所有规则
await this.$refs.myForm.validate()
// 下面代码,一定是校验成功才会执行
// 1. 以前的逻辑,校验成功之后,就可以发送请求
// const { data } = await login(this.form)
// this.$store.commit('user/updateUserToken', data.token)
// 2. 现在,我们又可以dispatch一个action,在action中
// 发送请求 => login
await this.$store.dispatch('user/updateUserToken', this.form)
this.$message.success('恭喜登录成功')
this.$router.push('/')
} catch (e) {
console.log(e)
}
},
请求头中带上token
request.js
javascript
import store from '@/store'
request.interceptors.request.use(
function (config) {
// 取token,
const token = store.state.user.token
// 请求头,token放到请求头中
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 在发送请求之前做些什么
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
导航守卫根据token拦截
router/index.js
如果没有token或者去的路由不是/login, 拦截到登录
javascript
// 登录页可以正常访问,其他页面 && 没有token,都拦截到登录页中
import store from '@/store'
// 全局前置守卫,拦截
router.beforeEach((to, from, next) => {
// to 到哪儿去
const token = store.state.user.token
// console.log(token)
// 没有token 或者|| 去的不是login页 ==> 拦截 Error 不能写或者 ||
if (to.path !== '/login' && !token) {
// 去的页面不是login,并且没有token,==> 拦截
next('/login')
} else {
next()
}
// from 从哪儿来
// next 是否放行
})
layout/index.vue
api/user.js
javascript
export const getUser = () => {
return request.get('/auth/currentUser')
}
vue
<template>
<el-container class="layout-page">
<el-aside width="200px">
<div class="logo">黑马面经</div>
<el-menu
router
:default-active="$route.path"
background-color="#313a46"
text-color="#8391a2"
active-text-color="#FFF"
>
<el-menu-item index="/dashboard">
<i class="el-icon-pie-chart"></i>
<span>数据看板</span>
</el-menu-item>
<el-menu-item index="/article">
<i class="el-icon-notebook-1"></i>
<span>面经管理</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header>
<div class="user">
<el-avatar :size="36" :src="avatar"></el-avatar>
<el-link :underline="false">{{ name }}</el-link>
</div>
<div class="logout">
<el-popconfirm
title="您确认退出黑马面运营后台吗?"
@confirm="handleConfirm"
>
<i
slot="reference"
title="logout"
class="el-icon-switch-button"
></i>
</el-popconfirm>
</div>
</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script>
import { getUser } from '@/api/user'
export default {
name: 'layout-page',
data() {
return {
avatar: '',
name: ''
}
},
created() {
this.initData()
},
methods: {
async initData() {
const { data } = await getUser()
this.avatar = data.avatar
this.name = data.name
},
handleConfirm() {
this.$router.push('/login')
}
}
}
</script>
<style lang="scss" scoped>
.layout-page {
height: 100vh;
.el-aside {
background: #313a46;
.logo {
color: #fff;
font-size: 20px;
height: 60px;
line-height: 60px;
text-align: center;
}
.el-menu {
border-right: none;
margin-top: 20px;
&-item {
background-color: transparent !important;
> span,
i {
padding-left: 5px;
}
}
}
}
.el-header {
box-shadow: 0px 0px 35px 0px rgba(154, 161, 171, 0.15);
background: #fff;
display: flex;
justify-content: flex-end;
align-items: center;
z-index: 999;
.user {
display: flex;
align-items: center;
background: #fafbfd;
height: 60px;
border: 1px solid #f1f3fa;
padding: 0 15px;
.el-avatar {
margin-right: 15px;
}
}
.logout {
font-size: 20px;
color: #999;
cursor: pointer;
padding: 0 15px;
}
}
.el-footer {
display: flex;
justify-content: space-between;
align-items: center;
color: #aaa;
border-top: 1px solid rgba(152, 166, 173, 0.2);
font-size: 14px;
}
}
</style>
相应拦截器,token过期拦截到登录
javascript
import router from '@/router'
import { Message } from 'element-ui'
// 添加响应拦截器
request.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response.data
},
function (error) {
// token过期或者说错误,拦截到登录
if (error.response.status === 401) {
// console.log(error.response)
Message({
message: '你的token过期啦,重新登录一下吧',
duration: 1000
})
router.push('/login')
} else {
// this.$message
Message(error.response.data.message)
}
// 对响应错误做点什么
return Promise.reject(error)
}
)