Skip to content

项目演示

pnpm安装后,插件路径错误

  • yarn/npm 安装时,不会有这个问题~~~
  1. 直接找到完整路径导入
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目录
  1. 配置别名
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表单的校验

image.png

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 输入的内容双向数据绑定
   -->

image.png

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

image.png

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模块

  1. 写action
  2. 写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,在组件中请求数据

image.png

组件中dispatch action,在action中请求数据,然后提交

image.png

同步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)
    }
)