四、项目搭建

241 阅读11分钟

贾成豪老师代码仓库地址:gitee.com/jch1011/vue…

项目在线文档:

服务器域名:sph-api.atguigu.cn

swagger文档:

http://139.198.104.58:8209/swagger-ui.html

http://139.198.104.58:8212/swagger-ui.html#/

echarts:国内镜像网站

www.isqqw.com/echarts-doc…

datav.aliyun.com/portal/scho…

路由搭建

1、安装路由

pnpm install vue-router

分别新建 views 目录,并在其下面新建对应的一级路由的vue文件

image-20230819213334164.png

2、定义路由组件

404>index.vue

<template>
  <div>
    <h1> 我是一级路由 404 </h1>
  </div>
</template>
<script setup lang="ts">
import {ref,reactive} from "vue"</script>
<style lang="scss" scoped>
​
​
</style>

home>index.vue

<template>
  <div>
    <h1> 我是一级路由 home </h1>
  </div>
</template>
<script setup lang="ts">
import {ref,reactive} from "vue"</script>
<style lang="scss" scoped>
​
​
</style>

login>index.vue

<template>
  <div>
    <h1> 我是一级路由 login </h1>
  </div>
</template>
<script setup lang="ts">
import {ref,reactive} from "vue"</script>
<style lang="scss" scoped>
​
​
</style>

在src目录下新建 router文件

image-20230819213415460.png

3、定义配置并在main.js 中引入

routes.ts

// 配置对外暴露的路由(配置成常量)
export const constantRoute = [
​
    {
        // 登入
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login',//命名路由
    },
    {
        // 登入成功以后展示数据的路由
        path: '/',
        component: () => import('@/views/home/index.vue'),
        name: 'layout',//命名路由
    },
    {
        // 404页面
        path: '/404',
        component: () => import('@/views/404/index.vue'),
        name: '404',//命名路由
    },
    {
        // 任意路由,就是当路由匹配从上往下时都有没有匹配上的默认路由
        path: '/:pathMatch(.*)*',
        redirect: '/404',
        name: 'Any',//命名路由
    }
​
​
]

router>index.ts

//通过vue-router插件实现模板路由配置
import { createRouter, createWebHashHistory } from 'vue-router';
import { constantRoute } from './routes';
​
// 创建路由器
let router = createRouter({
    //路由模式hash
    history: createWebHashHistory(),
    routes: constantRoute,
    //滚动行为
    scrollBehavior() {
        return {
            left: 0,
            top: 0
        }
    }
})
​
export default router;

在 main.ts 中注入

import { createApp } from 'vue'
import App from './App.vue'
// 引入 element-plus 插件与样式
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css'
//@ts-ignore忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
// svg 插件需要引入的配置代码
import 'virtual:svg-icons-register'
// 引入自定义插件对象:注册整个项目的全局组件
import gloablComponent from '@/components/index';
// 引入模板的全局样式
import '@/styles/index.scss'// 引入路由 +++++++++
import router from '@/router'// 获取应用实列对象
const app = createApp(App)
// 安装element-plus插件
app.use(ElementPlus, {
    locale: zhCn
});
// 安装自定义插件--> 会调用@/components/index.js中的方法
app.use(gloablComponent);
// 注册模板路由 +++++++
app.use(router);
​
// 将应用挂载到挂载点
app.mount('#app')
​

4、通过浏览器测试路由配置

在 App.vue中渲染路由出口

<template>
  <div>
   <router-view> </router-view>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'</script>
<style lang="scss" scoped>
​
​
</style>

image-20230819213742950.png

登入功能实现

1、登入路由静态页面的搭建

1、复制老师的images到 assets下

2、编写views > login.vue

<template>
  <div class="login_container">
    <el-row>
      <el-col :span="12" :xs="0"></el-col>
      <el-col :span="12" :xs="24">
        <!-- 登录的表单 -->
        <el-form class="login_form">
          <h1>Hello</h1>
          <h2>欢迎来到硅谷甄选</h2>
          <el-form-item prop="username">
            <el-input :prefix-icon="User" v-model="loginForm.username"></el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input type="password" :prefix-icon="Lock" v-model="loginForm.password" show-password></el-input>
          </el-form-item>
          <el-form-item>
            <el-button  class="login_btn" type="primary" size="default" @click="login">登录</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </div>
</template>
<script setup lang="ts">
import {ref,reactive} from "vue"
import { User, Lock } from '@element-plus/icons-vue';
import { ElNotification } from 'element-plus';
​
//收集账号与密码的数据
let loginForm = reactive({ username: 'admin', password: 'atguigu123' });
​
// 登入按钮回调
const login = async () =>{
​
console.log('请求登入=========> ',loginForm)
​
}
​
​
</script><style lang="scss" scoped>.login_container {
  width: 100%;
  height: 100vh;
  background: url('@/assets/images/background.jpg') no-repeat;
  background-size: cover;
​
  .login_form {
    position: relative;
    width: 80%;
    top: 30vh;
    background: url("@/assets/images/login_form.png") no-repeat;
    background-size: cover;
    padding: 40px;
​
    h1 {
      color: white;
      font-size: 40px;
    }
​
    h2 {
      font-size: 20px;
      color: white;
      margin: 20px 0px;
    }
​
    .login_btn {
      width: 100%;
    }
  }
}
​
​
</style>

启动测试

2、 处理登入请求(接入pinia)

首先先集成状态管理pinia,在pinia中实现登入的请求处理

安装pinia

pnpm i pinia

在 utils 下 引入老师的 time.ts

在 src 下新建 store, store>modules>user.ts, store>index.ts

image-20230819234121895.png

index.ts

// 引入大仓库
import { createPinia } from 'pinia'
// 创建大仓库
let pinia = createPinia();
// 对外暴露大仓库:入口文件需要安装仓库
export default pinia;

user.ts

//  创建用户相关的小仓库
import { defineStore } from 'pinia';
// 引入登入接口
import { reqLogin } from '@/api/user';
// 引入数据类型
import type { loginFormData } from '@/api/user/type';
​
// 创建用户小仓库
let useUserStore = defineStore('User', {
    // 小仓库存储数据的地方
​
    state: () => {
        return {
            token: localStorage.getItem("TOKEN"), //用户的唯一标识
        }
    },
    // 异步|逻辑的地方
    actions: {
        // 用户登入的方法
        async  userLogin(data: loginFormData) {
            console.log('pinia 触发了登入请求 =======>', data)
            // 发送登入请求
            let result = await reqLogin(data)
            // 登入成功 200 -> token
            // 登入失败 201 -> 登入失败的信息
            if (result.code == 200) {
                console.log("获取登入成功的数据====", result)
                // 由于 pinia | vuex 存储的数据其实是利用的 js 对象
                this.token = (result.data as string);
                // 本地持久化一份数据
                localStorage.setItem("TOKEN", result.data.token as string)
                //能保证当前async函数返回一个成功的promise
                return 'ok';
            } else {
                return Promise.reject(new Error(result.data.message));
            }
        }
​
    },
    getters: {
​
​
    }
​
})
​
// 对外暴露获取小仓库
export default useUserStore;
​

在入口文件main.ts 引入仓库

import { createApp } from 'vue'
import App from './App.vue'
// 引入 element-plus 插件与样式
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css'
//@ts-ignore忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
// svg 插件需要引入的配置代码
import 'virtual:svg-icons-register'
// 引入自定义插件对象:注册整个项目的全局组件
import gloablComponent from '@/components/index';
// 引入模板的全局样式
import '@/styles/index.scss'// 引入路由
import router from '@/router'
// 引入仓库 pinia +++++++
import pinia from '@/store'// 获取应用实列对象
const app = createApp(App)
// 安装element-plus插件
app.use(ElementPlus, {
    locale: zhCn
});
// 安装自定义插件--> 会调用@/components/index.js中的方法
app.use(gloablComponent);
// 注册模板路由
app.use(router);
// 注册仓库 ++++++
app.use(pinia);
​
// 将应用挂载到挂载点
app.mount('#app')
​

在login.vue处理登入逻辑

<template>
  <div class="login_container">
    <el-row>
      <el-col :span="12" :xs="0"></el-col>
      <el-col :span="12" :xs="24">
        <!-- 登录的表单 -->
        <el-form class="login_form">
          <h1>Hello</h1>
          <h2>欢迎来到硅谷甄选</h2>
          <el-form-item prop="username">
            <el-input
              :prefix-icon="User"
              v-model="loginForm.username"
            ></el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input
              type="password"
              :prefix-icon="Lock"
              v-model="loginForm.password"
              show-password
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-button
              :loading="loading"
              class="login_btn"
              type="primary"
              size="default"
              @click="login"
            >
              登录
            </el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { User, Lock } from '@element-plus/icons-vue'
import { ElNotification } from 'element-plus'
import useUserStore from '@/store/modules/user'
import { useRouter, useRoute } from 'vue-router'
//引入获取当前时间的函数
import { getTime } from '@/utils/time'
let useStore = useUserStore()
//获取路由器
let $router = useRouter()
//路由对象
let $route = useRoute()
//收集账号与密码的数据
let loginForm = reactive({ username: 'admin', password: 'atguigu123' })
//定义变量控制按钮加载效果
let loading = ref(false)
​
// 登入按钮回调
const login = async () => {
  // 点击登入时,请求仓库pinia发送登入请求
  // 请求成功跳转到首页数据显示
  // 如果登入失败在登入页提示失败的信息
  console.log('请求登入=========> ', loginForm)
  try {
    //保证登录成功
    await useStore.userLogin(loginForm)
    //编程式导航跳转到展示数据首页
    //判断登录的时候,路由路径当中是否有query参数,如果有就往query参数挑战,没有跳转到首页
    let redirect: any = $route.query.redirect
    $router.push({ path: redirect || '/' })
    //登录成功提示信息
    ElNotification({
      type: 'success',
      message: '欢迎回来',
      title: `HI,${getTime()}好`,
    })
    //登录成功加载效果也消失
    loading.value = false
  } catch (error) {
    //登录失败加载效果消息
    loading.value = false
    //登录失败的提示信息
    ElNotification({
      type: 'error',
      message: (error as Error).message,
    })
  }
}
</script><style lang="scss" scoped>
.login_container {
  width: 100%;
  height: 100vh;
  background: url('@/assets/images/background.jpg') no-repeat;
  background-size: cover;
​
  .login_form {
    position: relative;
    width: 80%;
    top: 30vh;
    background: url('@/assets/images/login_form.png') no-repeat;
    background-size: cover;
    padding: 40px;
​
    h1 {
      color: white;
      font-size: 40px;
    }
​
    h2 {
      font-size: 20px;
      color: white;
      margin: 20px 0px;
    }
​
    .login_btn {
      width: 100%;
    }
  }
}
</style>

登入用户相关类型定义

我们使用的是ts,之前在部分的代码中没有定义数据的类型,如储存state的类型,登入成功后接收的参数类型等。

在store>modules>新建一个types>type.ts 存放状态管理中的state的类型数据

image-20230820165129217.png

type.ts

import type { RouteRecordRaw } from "vue-router";
//定义小仓库数据state类型
export interface UserState {
    token: string | null;
}

将本地存储的方法封装的utils的工具类中去,新建token.ts 内容如下

//封装本地存储存储数据与读取数据方法
//存储数据
export const SET_TOKEN = (token: string) => {
    localStorage.setItem("TOKEN", token);
}
//本地存储获取数据
export const GET_TOKEN = () => {
    return localStorage.getItem('TOKEN');
}
//本地存储删除数据方法
export const REMOVE_TOKEN = () => {
    localStorage.removeItem('TOKEN');
}

在store>modules>user.ts 的仓库中引用上面 token.ts 和 type.ts

//  创建用户相关的小仓库
import { defineStore } from 'pinia';
// 引入登入接口
import { reqLogin } from '@/api/user';
// 引入数据类型 +++++++++
import type { loginFormData, loginResponseData } from '@/api/user/type';
// 引入用户的状态  +++++++++
import type { UserState } from '@/store/modules/types/type'
//引入操作本地存储的工具方法 +++++++++
import { SET_TOKEN, GET_TOKEN } from '@/utils/token';
​
// 创建用户小仓库
let useUserStore = defineStore('User', {
    // 小仓库存储数据的地方 +++++++++
    state: (): UserState => {
        return {
            token: GET_TOKEN(), //用户的唯一标识
        }
    },
    // 异步|逻辑的地方
    actions: {
        // 用户登入的方法
        async  userLogin(data: loginFormData) {
            console.log('pinia 触发了登入请求 =======>', data)
            // 发送登入请求 +++++++++
            let result: loginResponseData = await reqLogin(data)
            // 登入成功 200 -> token
            // 登入失败 201 -> 登入失败的信息
            if (result.code == 200) {
                console.log("获取登入成功的数据====", result)
                // 由于 pinia | vuex 存储的数据其实是利用的 js 对象
                this.token = (result.data as string);
                // 本地持久化一份数据 +++++++++
                SET_TOKEN((result.data as string));
                //能保证当前async函数返回一个成功的promise
                return 'ok';
            } else {
                return Promise.reject(new Error(result.data.message));
            }
        }
​
    },
    getters: {
​
​
    }
​
})
​
// 对外暴露获取小仓库
export default useUserStore;
​

3、登入时间的判断与封装

在 utils > time.ts

//封装一个函数:获取一个结果:当前早上|上午|下午|晚上
export const getTime = () => {
  let message = ''
  //通过内置构造函数Date
  const hours = new Date().getHours()
  //情况的判断
  if (hours <= 9) {
    message = '早上'
  } else if (hours <= 12) {
    message = '上午'
  } else if (hours <= 18) {
    message = '下午'
  } else {
    message = '晚上'
  }
  return message
}
​

在login.vue中引用

<template>
 .........................
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { User, Lock } from '@element-plus/icons-vue'
import { ElNotification } from 'element-plus'
import useUserStore from '@/store/modules/user'
import { useRouter, useRoute } from 'vue-router'
//引入获取当前时间的函数  ++++++++++++++
import { getTime } from '@/utils/time'
let useStore = useUserStore()
//获取路由器
let $router = useRouter()
//路由对象
let $route = useRoute()
//收集账号与密码的数据
let loginForm = reactive({ username: 'admin', password: 'atguigu123' })
//定义变量控制按钮加载效果
let loading = ref(false)
​
// 登入按钮回调
const login = async () => {
  // 点击登入时,请求仓库pinia发送登入请求
  // 请求成功跳转到首页数据显示
  // 如果登入失败在登入页提示失败的信息
  console.log('请求登入=========> ', loginForm)
  try {
    //保证登录成功
    await useStore.userLogin(loginForm)
    //编程式导航跳转到展示数据首页
    //判断登录的时候,路由路径当中是否有query参数,如果有就往query参数挑战,没有跳转到首页
    let redirect: any = $route.query.redirect
    $router.push({ path: redirect || '/' })
    //登录成功提示信息
    ElNotification({
      type: 'success',
      message: '欢迎回来',
      // ++++++++++++++
      title: `HI,${getTime()}好`,
    })
    //登录成功加载效果也消失
    loading.value = false
  } catch (error) {
    //登录失败加载效果消息
    loading.value = false
    //登录失败的提示信息
    ElNotification({
      type: 'error',
      message: (error as Error).message,
    })
  }
}
</script><style lang="scss" scoped>
 ......
</style>

4、登入表单校验

表单登入我们使用的是element-plus 提供的表单校验组件

需要修改的步骤:

  1. 在el-form 上加上 :model="loginForm" :rules="rules" ref="loginForms",其中loginForm为我们接受的用户输入账号密码的对象,rules 规则校验,loginForms 为 el-form 对象的引用,loginForms 需要在下面声明

  2. 声明loginForms

  3. 在el-form 中的需要校验的输入框加上prop,值为接收对象的属性名

  4. 定义规则rules

  5. 在提交方法中使用

    <template>
      <div class="login_container">
        <el-row>
          <el-col :span="12" :xs="0"></el-col>
          <el-col :span="12" :xs="24">
            <!-- 登录的表单 -->
            <el-form class="login_form" :model="loginForm" :rules="rules" ref="loginForms">      // +++++++++++
              <h1>Hello</h1>
              <h2>欢迎来到硅谷甄选</h2>
              <el-form-item prop="username"> // ++++++++++
                <el-input :prefix-icon="User" v-model="loginForm.username"></el-input>
              </el-form-item>
              <el-form-item prop="password">
                <el-input type="password" :prefix-icon="Lock" v-model="loginForm.password" show-password></el-input>
              </el-form-item>
              <el-form-item>
                <el-button :loading="loading" class="login_btn" type="primary" size="default" @click="login"> 登录 </el-button>
              </el-form-item>
            </el-form>
          </el-col>
        </el-row>
      </div>
    </template>
    <script setup lang="ts">
    import { ref, reactive } from 'vue'
    import { User, Lock } from '@element-plus/icons-vue'
    import { ElNotification } from 'element-plus'
    import useUserStore from '@/store/modules/user'
    import { useRouter, useRoute } from 'vue-router'
    //引入获取当前时间的函数
    import { getTime } from '@/utils/time'
    let useStore = useUserStore()
    //获取路由器
    let $router = useRouter()
    //路由对象
    let $route = useRoute()
    //收集账号与密码的数据
    let loginForm = reactive({ username: 'admin', password: 'atguigu123' })
    //定义变量控制按钮加载效果
    let loading = ref(false)
    //获取el-form组件 +++++++++++++++++++
    let loginForms = ref();
    ​
    // 登入按钮回调
    const login = async () => {
    ​
       //保证全部表单相校验通过再发请求 +++++++++++++
      await loginForms.value.validate();
      //加载效果:开始加载
      let loading = ref(false)
      // 点击登入时,请求仓库pinia发送登入请求
      // 请求成功跳转到首页数据显示
      // 如果登入失败在登入页提示失败的信息
      console.log('请求登入=========> ', loginForm)
      try {
        //保证登录成功
        await useStore.userLogin(loginForm)
        //编程式导航跳转到展示数据首页
        //判断登录的时候,路由路径当中是否有query参数,如果有就往query参数挑战,没有跳转到首页
        let redirect: any = $route.query.redirect
        $router.push({ path: redirect || '/' })
        //登录成功提示信息
        ElNotification({
          type: 'success',
          message: '欢迎回来',
          title: `HI,${getTime()}好`,
        })
        //登录成功加载效果也消失
        loading.value = false
      } catch (error) {
        //登录失败加载效果消息
        loading.value = false
        //登录失败的提示信息
        ElNotification({
          type: 'error',
          message: (error as Error).message,
        })
      }
    }
    ​
    //定义表单校验需要配置对象 ++++++++++
    const rules = {
      //规则对象属性:
      //required,代表这个字段务必要校验的
      //min:文本长度至少多少位
      //max:文本长度最多多少位
      //message:错误的提示信息
      //trigger:触发校验表单的时机 change->文本发生变化触发校验,blur:失去焦点的时候触发校验规则
      username: [
         { required: true, min: 6, max: 10, message: '账号长度至少六位', trigger: 'change' }
      ],
      password: [
         { required: true, min: 6, max: 15, message: '密码长度至少6位', trigger: 'change' }
      ]
    }
    ​
    </script><style lang="scss" scoped>
    ......
    }
    </style>

5、登入表单自定义校验表单

以上的校验方法实际中不太满足需求,实际中可能需要更复杂的校验,比如必须包含三种不同的字符,大小写等,所以element还提供了可以自定义的表单校验。

只需要定义两个自定义校验的规则的函数,然后使用即可

<script setup lang="ts">
.................
​
//自定义校验规则函数-用户名 +++++++++++++++++++
const validatorUserName = (rule: any, value: any, callback: any) => {
  //rule:即为校验规则对象
  //value:即为表单元素文本内容
  //函数:如果符合条件callBack放行通过即为
  //如果不符合条件callBack方法,注入错误提示信息
  if (value.length >= 5) {
    callback();
  } else {
    callback(new Error('账号长度至少五位'));
  }
}
//自定义校验规则函数-密码 +++++++++++++++++++
const validatorPassword = (rule: any, value: any, callback: any) => {
  if (value.length >= 6) {
    callback();
  } else {
    callback(new Error('密码长度至少六位'));
  }
}
​
//定义表单校验需要配置对象
const rules = {
  //规则对象属性:
  //required,代表这个字段务必要校验的
  //min:文本长度至少多少位
  //max:文本长度最多多少位
  //message:错误的提示信息
  //trigger:触发校验表单的时机 change->文本发生变化触发校验,blur:失去焦点的时候触发校验规则
  username: [
    // { required: true, min: 6, max: 10, message: '账号长度至少六位', trigger: 'change' }
      { trigger: 'change', validator: validatorUserName } // ++++++++++++++
  ],
  password: [
    //  { required: true, min: 6, max: 15, message: '密码长度至少6位', trigger: 'change' }
      { trigger: 'change', validator: validatorPassword } // ++++++++++++++
  ]
}
​
</script>

页面布局划分(Layout)

image-20230902214749272.png

1、 layout 组件静态页面的搭建

在src 下面新建一个Layout组件来显示登入成功后的显示页面,将之前的路由表中的home页面组件替换为Layout

整个Layout组件分为三个部分,左侧导航区,顶部面包屑,以及右侧的动态显示区域,

  1. 新建 src > layout > index.vue
<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider">
            
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar">
​
        </div>
        <!-- 内容展示区域 -->
        <div class="layout_main">
        </div>
    </div>
</template>
<script setup lang="ts">
import {ref,reactive} from "vue"
​
​
​
​
</script>
<style lang="scss" scoped>
.layout_container {
    width: 100%;
    height: 100vh;
​
    .layout_slider {
        color: white;
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-background;
        transition: all 0.3s;
​
    }
​
    .layout_tabbar {
        position: fixed;
        width: calc(100% - $base-menu-width);
        height: $base-tabbar-height;
        top: 0px;
        left: $base-menu-width;
        transition: all 0.3s;
        background: pink;
   
    }
​
    .layout_main {
        position: absolute;
        width: calc(100% - $base-menu-width);
        height: calc(100vh - $base-tabbar-height);
        left: $base-menu-width;
        top: $base-tabbar-height;
        padding: 20px;
        overflow: auto;
        transition: all 0.3s;
        background: #92DD22;
​
    }
}
​
​
​
</style>

2、路由表修改

// 配置对外暴露的路由(配置成常量)
export const constantRoute = [
​
    {
        // 登入
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login',//命名路由
    },
    {
        // 登入成功以后展示数据的路由
        path: '/',
        component: () => import('@/layout/index.vue'), // ++++++++++++++
        name: 'layout',//命名路由
    },
    {
        // 404页面
        path: '/404',
        component: () => import('@/views/404/index.vue'),
        name: '404',//命名路由
    },
    {
        // 任意路由,就是当路由匹配从上往下时都有没有匹配上的默认路由
        path: '/:pathMatch(.*)*',
        redirect: '/404',
        name: 'Any',//命名路由
    }
​
​
]

3、定义全局的变量(颜色,尺寸)

styles>variable.scss

​
//项目提供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;
  1. styles>index.scss 中设置全局样式(滚动条的样式)

    // 引入清楚的默认样式
    @import './reset.scss';
    ​
    //滚动条外观设置
    ::-webkit-scrollbar {
        width: 10px;
    }
    ​
    ::-webkit-scrollbar-track {
        background: $base-menu-background;
    }
    ​
    ::-webkit-scrollbar-thumb {
        width: 10px;
        background-color: yellowgreen;
        border-radius: 10px;
    }
    

4、左侧导航栏

4.1、封装左侧导航栏上的LOGO
  1. 老师资料中的logo图标cope到public下

  2. styles>variable.scss定义logo组件的全局的变量(上一步就已经完成了)

  3. 在src下定义setting.ts 文件,方便后期用户扩展logo图标和文字

    //用于项目logo|标题配置
    export default {
        title: '硅谷甄选运营平台',//项目的标题
        logo: '/logo.png',//项目logo设置
        logoHidden:true,//logo组件是否隐藏设置
    }
    
  4. src>layout>logo新建一个index.vue

<template>
    <div class="logo" v-if="setting.logoHidden">
        <img :src="setting.logo" alt="">
        <p>{{setting.title}}</p>
    </div>
</template><script setup lang="ts">
//引入设置标题与logo这配置文件
import setting from '@/setting';
</script>
<script lang="ts">
export default{
    name:"Logo"
}
</script>
<style scoped lang="scss">
.logo {
    width: 100%;
    height: $base-menu-logo-height;
    color: white;
    display: flex;
    align-items: center;
    padding: 10px;
    img{
        width: 40px;
        height: 40px;
    }
    p{
       font-size: $base-logo-title-fontSize; 
       margin-left: 10px;
    }
}
</style>
  1. 在layout>index.vue中引入logo组件
<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider">
        <Logo></Logo> // ++++++++++++++
            
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar">
​
        </div>
        <!-- 内容展示区域 -->
        <div class="layout_main">
        </div>
    </div>
</template>
<script setup lang="ts">
import {ref,reactive} from "vue"
//引入左侧菜单logo子组件
import Logo from './logo/index.vue' // +++++++++++++++++++++++
​
​
​
</script>
<style lang="scss" scoped>
........
</style>
4.2 左侧菜单的静态页面的搭建
  1. 左侧导航栏的页面分析, el-scrollbar

在我们的左侧导航栏是一个不定层级和不定数量的,假设导航项有,首页,数据大屏,权限控制,商品管理 ........,如果有 几百个那么屏幕就会撑起来,下面出现白边,所以需要使用 el-scrollbar 组件,让不管多少个都在屏幕内滚动。

  1. 引入 el-scrollbar 和 el-menu 并写样式
<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider">
        <Logo></Logo>
        <!-- 展示菜单 -->
            <!-- 滚动组件 -->
            <el-scrollbar class="scrollbar"> // +++++++++++++
                 <!-- 菜单组件 -->
                <el-menu background-color = "#001529" text-color= "white">
                   <el-menu-item index = "1">首页</el-menu-item>
                   <el-menu-item index = "2">数据大屏</el-menu-item>
                    <!-- 折叠菜单 -->
                    <el-sub-menu index="3">
                        <template #title>
                            <span>权限管理</span>
                        </template>
                         <el-menu-item index = "2-1">用户管理</el-menu-item>
                         <el-menu-item index = "2-2">角色管理</el-menu-item>
                         <el-menu-item index = "2-3">菜单管理</el-menu-item>
                    </el-sub-menu>
                
                </el-menu>
            
             </el-scrollbar>
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar">
​
        </div>
        <!-- 内容展示区域 -->
        <div class="layout_main">
        </div>
    </div>
</template>
<script setup lang="ts">
import {ref,reactive} from "vue"
//引入左侧菜单logo子组件
import Logo from './logo/index.vue'
​
​
​
</script>
<style lang="scss" scoped>
.layout_container {
    width: 100%;
    height: 100vh;
​
    .layout_slider {
        color: white;
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-background;
        transition: all 0.3s;
        .scrollbar { // +++++++++
            width: 100%;
            height: calc(100vh - $base-menu-logo-height);
​
            .el-menu {
                border-right: none;
            }
        }
​
    }
​
........
}
​
​
​
</style>
4.3 递归组件生成动态的菜单

以上的写法都是静态写死的,实际中我们是需要根据后端请求过来的哪些菜单然后经行动态渲染的,所以需要将这部分抽取i成一个组件。

  1. 在左侧菜单栏上,每个菜单上都会引用到图标,先在src/components/index.ts 中将图标注册为全局的组件
//引入项目中全部的全局组件
import SvgIcon from './SvgIcon/index.vue';
​
//引入element-plus提供全部图标组件 +++++++++
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
//全局对象
const allGloablComponent: any = { SvgIcon };
//对外暴露插件对象
export default {
    //务必叫做install方法
    install(app: any) {
        //注册项目全部的全局组件
        Object.keys(allGloablComponent).forEach(key => {
            //注册为全局组件
            app.component(key, allGloablComponent[key]);
        });
        //将element-plus提供图标注册为全局组件  +++++++++++++++
        for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
            app.component(key, component)
        }
    }
}
  1. 重新 配置路由常量 router>router.ts

    //对外暴露配置路由(常量路由):全部用户都可以访问到的路由
    export const constantRoute = [
        {
            //登录
            path: '/login',
            component: () => import('@/views/login/index.vue'),
            name: 'login',
            meta: {
                title: '登录',//菜单标题
                hidden: true,//代表路由标题在菜单中是否隐藏  true:隐藏 false:不隐藏
                icon: "Promotion",//菜单文字左侧的图标,支持element-plus全部图标
            }
        }
        ,
        {
            //登录成功以后展示数据的路由
            path: '/',
            component: () => import('@/layout/index.vue'),
            name: 'layout',
            meta: {
                title: '',
                hidden: false,
                icon: ''
            },
            redirect: '/home',
            children: [
                {
                    path: '/home',
                    component: () => import('@/views/home/index.vue'),
                    meta: {
                        title: '首页',
                        hidden: false,
                        icon: 'HomeFilled'
                    }
                }
            ]
        },
        {
            //404
            path: '/404',
            component: () => import('@/views/404/index.vue'),
            name: '404',
            meta: {
                title: '404',
                hidden: true,
                icon: 'DocumentDelete'
            }
        },
        {
            path: '/screen',
            component: () => import('@/views/screen/index.vue'),
            name: 'Screen',
            meta: {
                hidden: false,
                title: '数据大屏',
                icon: 'Platform'
            }
        }
        ,
        {
            path: '/acl',
            component: () => import('@/layout/index.vue'),
            name: 'Acl',
            meta: {
                title: '权限管理',
                icon: 'Lock'
            },
            redirect: '/acl/user',
            children: [
                {
                    path: '/acl/user',
                    component: () => import('@/views/acl/user/index.vue'),
                    name: 'User',
                    meta: {
                        title: '用户管理',
                        icon: 'User'
                    }
                },
                {
                    path: '/acl/role',
                    component: () => import('@/views/acl/role/index.vue'),
                    name: 'Role',
                    meta: {
                        title: '角色管理',
                        icon: 'UserFilled'
                    }
                },
                {
                    path: '/acl/permission',
                    component: () => import('@/views/acl/permission/index.vue'),
                    name: 'Permission',
                    meta: {
                        title: '菜单管理',
                        icon: 'Monitor'
                    }
                }
            ]
        }
        ,
        {
            path: '/product',
            component: () => import('@/layout/index.vue'),
            name: 'Product',
            meta: {
                title: '商品管理',
                icon: 'Goods',
            },
            redirect: '/product/trademark',
            children: [
                {
                    path: '/product/trademark',
                    component: () => import('@/views/product/trademark/index.vue'),
                    name: "Trademark",
                    meta: {
                        title: '品牌管理',
                        icon: 'ShoppingCartFull',
                    }
                },
                {
                    path: '/product/attr',
                    component: () => import('@/views/product/attr/index.vue'),
                    name: "Attr",
                    meta: {
                        title: '属性管理',
                        icon: 'ChromeFilled',
                    }
                },
                {
                    path: '/product/spu',
                    component: () => import('@/views/product/spu/index.vue'),
                    name: "Spu",
                    meta: {
                        title: 'SPU管理',
                        icon: 'Calendar',
                    }
                },
                {
                    path: '/product/sku',
                    component: () => import('@/views/product/sku/index.vue'),
                    name: "Sku",
                    meta: {
                        title: 'SKU管理',
                        icon: 'Orange',
                    }
                },
            ]
        },
        {
            //任意路由
            path: '/:pathMatch(.*)*',
            redirect: '/404',
            name: 'Any',
            meta: {
                title: '任意路由',
                hidden: true,
                icon: 'DataLine'
            }
        }
    ]
    ​
    
    1. 根据上面配置的路由新建对应的组件

image-20230821223806878.png

  1. 在layout下新建menu > index.vue 组件
<template>
    <template v-for="(item, index) in menuList" :key="item.path">
        <!--没有子路由-->
        <template v-if="!item.children">
            <el-menu-item :index="item.path" v-if="!item.meta.hidden" @click="goRoute">
                <el-icon>
                 <!--component 来取出全局icon 组件-->
                    <component :is="item.meta.icon"></component>
                </el-icon>
                <template #title>
                    <span>{{ item.meta.title }}</span>
                </template>
            </el-menu-item>
        </template>
        <!-- 有子路由但是只有一个子路由 -->
        <template v-if="item.children && item.children.length == 1">
            <el-menu-item :index="item.children[0].path" v-if="!item.children[0].meta.hidden" @click="goRoute">
                <el-icon>
                    <component :is="item.children[0].meta.icon"></component>
                </el-icon>
                <template #title>
                    <span>{{ item.children[0].meta.title }}</span>
                </template>
            </el-menu-item>
        </template>
        <!-- 有子路由且个数大于一个1 -->
        <el-sub-menu :index="item.path" v-if="item.children && item.children.length > 1">
            <template #title>
                <el-icon>
                    <component :is="item.meta.icon"></component>
                </el-icon>
                <span>{{ item.meta.title }}</span>
            </template>
            <Menu :menuList="item.children"></Menu>
        </el-sub-menu>
    </template>
</template><script setup lang="ts">
import { useRouter } from 'vue-router';
//获取父组件传递过来的全部路由数组
defineProps(['menuList']);
​
//获取路由器对象
let $router = useRouter();
//点击菜单的回调
const goRoute = (vc: any) => {
    //路由跳转
    $router.push(vc.index);
}
</script>
<script lang="ts">
export default {
    // 递归调用一定要给组件取一个名字,否则递归不生效
    name: 'Menu'
}
</script><style scoped></style>
  1. 将路由常量放到仓库管理中的去,放到store>modules>user.ts

  2. src>store>modules>types>type.ts

    import type { RouteRecordRaw } from "vue-router";
    import type { CategoryObj } from "@/api/product/attr/type";
    //定义小仓库数据state类型
    export interface UserState {
        token: string | null;
        menuRoutes: RouteRecordRaw[],
        username: string,
        avatar: string,
        buttons:string[]
    }
    
    1. store>modules>user.ts

      //  创建用户相关的小仓库
      import { defineStore } from 'pinia';
      // 引入登入接口
      import { reqLogin } from '@/api/user';
      // 引入数据类型
      import type { loginFormData, loginResponseData } from '@/api/user/type';
      import type { UserState } from '@/store/modules/types/type'
      //引入操作本地存储的工具方法
      import { SET_TOKEN, GET_TOKEN } from '@/utils/token';
      ​
      //引入路由(常量路由) +++++++++++++++++
      import { constantRoute } from '@/router/routes';
      ​
      // 创建用户小仓库
      let useUserStore = defineStore('User', {
          // 小仓库存储数据的地方
      ​
          state: (): UserState => {
              return {
                  token: GET_TOKEN(), //用户的唯一标识
                  menuRoutes: constantRoute,//仓库存储生成菜单需要数组(路由) +++++++++++++++++
              }
          },
          // 异步|逻辑的地方
          actions: {
              // 用户登入的方法
              async  userLogin(data: loginFormData) {
                  console.log('pinia 触发了登入请求 =======>', data)
                  // 发送登入请求
                  let result: loginResponseData = await reqLogin(data)
                  // 登入成功 200 -> token
                  // 登入失败 201 -> 登入失败的信息
                  if (result.code == 200) {
                      console.log("获取登入成功的数据====", result)
                      // 由于 pinia | vuex 存储的数据其实是利用的 js 对象
                      this.token = (result.data as string);
                      // 本地持久化一份数据
                      SET_TOKEN((result.data as string));
                      //能保证当前async函数返回一个成功的promise
                      return 'ok';
                  } else {
                      return Promise.reject(new Error(result.data.message));
                  }
              }
      ​
          },
          getters: {
      ​
      ​
          }
      ​
      })
      ​
      // 对外暴露获取小仓库
      export default useUserStore;
      ​
      
  3. 定义src>main>index.vue

<template>
    <!-- 路由组件出口的位置 -->
    <router-view v-slot="{ Component }">
        <transition name="fade">
            <!-- 渲染layout一级路由组件的子路由 -->
            <component :is="Component"  />
        </transition>
    </router-view>
</template><script setup lang="ts">
​
​
</script>
<script lang="ts">
export default {
    name: "Main"
}
</script><style scoped>
.fade-enter-from {
    opacity: 0;
    transform: scale(0);
}
​
.fade-enter-active {
    transition: all .3s;
}
​
.fade-enter-to {
    opacity: 1;
    transform: scale(1);
}
</style>
  1. 在layout>index.vue 中获取仓库中的路由常量,并且将获取到的值作为props传给子组件 menu > index.vue 组件,以及引入main组件
<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider">
        <Logo></Logo>
        <!-- 展示菜单 -->
            <!-- 滚动组件 -->
            <el-scrollbar class="scrollbar">
                 <!-- 菜单组件 -->
                  <!-- 菜单组件-->
                <el-menu :default-active="$route.path" background-color="#001529" text-color="white"
                    active-text-color="yellowgreen">
                    <!--根据路由动态生成菜单-->
                    <Menu :menuList="userStore.menuRoutes"></Menu>
                </el-menu>
            
             </el-scrollbar>
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar">
​
        </div>
        <!-- 内容展示区域 -->
        <div class="layout_main">
             <Main></Main>
        </div>
    </div>
</template>
<script setup lang="ts">
import {ref,reactive} from "vue"
//获取路由对象
import { useRoute } from 'vue-router'
//引入左侧菜单logo子组件
import Logo from './logo/index.vue'
//引入菜单组件
import Menu from './menu/index.vue'
//获取用户相关的小仓库
import useUserStore from '@/store/modules/user';
//右侧内容展示区域
import Main from './main/index.vue';
​
let userStore = useUserStore();
​
//获取路由对象
let $route = useRoute();
​
</script>
<style lang="scss" scoped>
.layout_container {
    width: 100%;
    height: 100vh;
​
    .layout_slider {
        color: white;
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-background;
        transition: all 0.3s;
        .scrollbar {
            width: 100%;
            height: calc(100vh - $base-menu-logo-height);
​
            .el-menu {
                border-right: none;
            }
        }
​
    }
​
    .layout_tabbar {
        position: fixed;
        width: calc(100% - $base-menu-width);
        height: $base-tabbar-height;
        top: 0px;
        left: $base-menu-width;
        transition: all 0.3s;
        background: pink;
   
    }
​
    .layout_main {
        position: absolute;
        width: calc(100% - $base-menu-width);
        height: calc(100vh - $base-tabbar-height);
        left: $base-menu-width;
        top: $base-tabbar-height;
        padding: 20px;
        overflow: auto;
        transition: all 0.3s;
        background: #92DD22;
​
    }
}
​
​
</style>

5、顶部区域

5.1 顶部tabbar静态组件的搭建与拆分
  1. 新建layout> tabbar> breadcrumb> index.vue

    <template>
        <!-- 顶部左侧静态 -->
        <el-icon style="margin-right:10px">
            <Expand />
        </el-icon>
        <!-- 左侧面包屑 -->
        <el-breadcrumb separator-icon="ArrowRight">
            <!-- 面包动态展示路由名字与标题 -->
            <el-breadcrumb-item >权限管理</el-breadcrumb-item>
            <el-breadcrumb-item >用户管理</el-breadcrumb-item>
        </el-breadcrumb>
    </template><script setup lang="ts">
    import { useRoute } from 'vue-router';
    import { ref } from 'vue';
    ​
    //获取路由对象
    let $route = useRoute();
    ​
    </script>
    <script lang="ts">
    export default {
        name: "Breadcrumb"
    }
    </script><style scoped></style>
    
  2. 新建layout> tabbar> setting> index.vue

    <template>
        <el-button size="small" icon="Refresh" circle ></el-button>
        <el-button size="small" icon="FullScreen" circle ></el-button>
        <el-button size="small" icon="Setting" circle ></el-button>
    ​
        
        <img src="../../../../public/logo.png" style="width: 24px;height: 24px;margin:0px 10px;border-radius: 50%;">
        <!-- 下拉菜单 -->
        <el-dropdown>
            <span class="el-dropdown-link">
                admin
                <el-icon class="el-icon--right">
                    <arrow-down />
                </el-icon>
            </span>
            <template #dropdown>
                <el-dropdown-menu>
                    <el-dropdown-item>退出登录</el-dropdown-item>
                </el-dropdown-menu>
            </template>
        </el-dropdown>
    </template><script setup lang="ts">
    import { ref } from 'vue'
    ​
    ​
    </script><script lang="ts">
    export default {
        name: "Setting"
    }
    </script>
    <style scoped></style>
    
  3. 将 layout> tabbar> breadcrumb> index.vue 和 layout> tabbar> setting> index.vue 引入layout> tabbar> index.vue 中

    <template>
      <div class="tabbar">
        <div class="tabbar_left">
          <Breadcrumb />
        </div>
        <div class="tabbar_right">
          <Setting />
        </div>
      </div>
    </template>
    <script setup lang="ts">
    import { ref, reactive } from 'vue'
    import Breadcrumb from './breadcrumb/index.vue'
    import Setting from './setting/index.vue'
    ​
    ​
    ​
    </script>
    <style lang="scss" scoped>.tabbar {
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: space-between;
       // background-image: linear-gradient(to right, rgb(232, 223, 223), rgb(201, 178, 178), rgb(197, 165, 165));
    ​
        .tabbar_left {
            display: flex;
            align-items: center;
            margin-left: 20px;
    ​
        }
    ​
        .tabbar_right {
            display: flex;
            align-items: center;
        }
    ​
    }
    ​
    </style>
  4. 将layout> tabbar> index.vue 引入到 layout>index.vue中

<template>
  <div class="layout_container">
    <!-- 左侧菜单 -->
    <div class="layout_slider">
      <Logo></Logo>
      <!-- 展示菜单 -->
      <!-- 滚动组件 -->
      <el-scrollbar class="scrollbar">
        <!-- 菜单组件 -->
        <!-- 菜单组件-->
        <el-menu
          :default-active="$route.path"
          background-color="#001529"
          text-color="white"
          active-text-color="yellowgreen"
        >
          <!--根据路由动态生成菜单-->
          <Menu :menuList="userStore.menuRoutes"></Menu>
        </el-menu>
      </el-scrollbar>
    </div>
    <!-- 顶部导航 -->
    <div class="layout_tabbar">
      <!-- layout组件的顶部导航tabbar +++++++++++++++++ -->
      <Tabbar></Tabbar>
    </div>
    <!-- 内容展示区域 -->
    <div class="layout_main">
      <Main></Main>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
//获取路由对象
import { useRoute } from 'vue-router'
//引入左侧菜单logo子组件
import Logo from './logo/index.vue'
//引入菜单组件
import Menu from './menu/index.vue'
//获取用户相关的小仓库
import useUserStore from '@/store/modules/user'
//右侧内容展示区域
import Main from './main/index.vue'
//引入顶部tabbar组件 +++++++++++++++++++++++++++++++
import Tabbar from './tabbar/index.vue'
let userStore = useUserStore()
​
//获取路由对象
let $route = useRoute()
</script>
<style lang="scss" scoped>
.layout_container {
  width: 100%;
  height: 100vh;
​
  .layout_slider {
    color: white;
    width: $base-menu-width;
    height: 100vh;
    background: $base-menu-background;
    transition: all 0.3s;
    .scrollbar {
      width: 100%;
      height: calc(100vh - $base-menu-logo-height);
​
      .el-menu {
        border-right: none;
      }
    }
  }
​
  .layout_tabbar {
    position: fixed;
    width: calc(100% - $base-menu-width);
    height: $base-tabbar-height;
    top: 0px;
    left: $base-menu-width;
    transition: all 0.3s;
    background: pink;
  }
​
  .layout_main {
    position: absolute;
    width: calc(100% - $base-menu-width);
    height: calc(100vh - $base-tabbar-height);
    left: $base-menu-width;
    top: $base-tabbar-height;
    padding: 20px;
    overflow: auto;
    transition: all 0.3s;
    background: #92dd22;
  }
}
</style>

静态效果

image-20230823214939542.png

5.2 菜单折叠效果实现

image-20230823220441377.png

分析:通过store来维护一个全局变量fold, 点击折叠按钮来改变fold的值为true或者false,来切换图标的同时来给layout下index.vue中的左侧菜单组件,顶部组件,右侧显示组件来动态添加样式从而达到折叠的效果。

  1. 新建store>modules>setting.ts

    //小仓库:layout组件相关配置仓库
    import { defineStore } from "pinia";
    ​
    let useLayOutSettingStore = defineStore('SettingStore', {
        state: () => {
            return {
                fold: false,//用户控制菜单折叠还是收起控制
            }
        }
    })
    ​
    export default useLayOutSettingStore;
    
  2. 在layout> tabbar> breadcrumb> index.vue中使用store中的全局变量

<template>
    <!-- 顶部左侧静态 -->
    <el-icon style="margin-right:10px" @click="changeIcon"> // +++++++++++++++++++++++
         <component :is="LayOutSettingStore.fold ? 'Fold' : 'Expand'"></component> // +++++++++++++++++++++++
    </el-icon>
    <!-- 左侧面包屑 -->
    <el-breadcrumb separator-icon="ArrowRight">
        <!-- 面包动态展示路由名字与标题 -->
        <el-breadcrumb-item >权限管理</el-breadcrumb-item>
        <el-breadcrumb-item >用户管理</el-breadcrumb-item>
    </el-breadcrumb>
</template><script setup lang="ts">
import { useRoute } from 'vue-router';
import { ref } from 'vue';
// +++++++++++++++++++++++
import useLayOutSettingStore from '@/store/modules/setting';
//获取layout配置相关的仓库 +++++++++++++++++++++++
let LayOutSettingStore = useLayOutSettingStore();
//点击图标的方法 +++++++++++++++++++++++
const changeIcon = () => {
    //图标进行切换
    LayOutSettingStore.fold = !LayOutSettingStore.fold
}
​
</script>
<script lang="ts">
export default {
    name: "Breadcrumb"
}
</script><style scoped></style>
  1. 在layout>index.vue中引入store中的全局变量并动态绑定style
<template>
  <div class="layout_container">
    <!-- 左侧菜单 -->
    <div class="layout_slider">
      <Logo></Logo>
      <!-- 展示菜单 -->
      <!-- 滚动组件 -->
      <el-scrollbar class="scrollbar">
        <!-- 菜单组件 -->
        <!-- 菜单组件-->
        <el-menu
          :collapse="LayOutSettingStore.fold ? true : false" //++++++++++++++++++++++++++
          :default-active="$route.path"
          background-color="#001529"
          text-color="white"
          active-text-color="yellowgreen"
        >
          <!--根据路由动态生成菜单-->
          <Menu :menuList="userStore.menuRoutes"></Menu>
        </el-menu>
      </el-scrollbar>
    </div>
    <!-- 顶部导航 -->
    <div
      class="layout_tabbar"
      :class="{ fold: LayOutSettingStore.fold ? true : false }" //++++++++++++++++++++++++++
    >
      <!-- layout组件的顶部导航tabbar -->
      <Tabbar></Tabbar>
    </div>
    <!-- 内容展示区域 -->
    <div
      class="layout_main"
      :class="{ fold: LayOutSettingStore.fold ? true : false }" //++++++++++++++++++++++++++
    >
      <Main></Main>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
//获取路由对象
import { useRoute } from 'vue-router'
//引入左侧菜单logo子组件
import Logo from './logo/index.vue'
//引入菜单组件
import Menu from './menu/index.vue'
//获取用户相关的小仓库
import useUserStore from '@/store/modules/user'
//右侧内容展示区域
import Main from './main/index.vue'
//引入顶部tabbar组件
import Tabbar from './tabbar/index.vue'
//++++++++++++++++++++++++++
import useLayOutSettingStore from '@/store/modules/setting'
//++++++++++++++++++++++++++
let LayOutSettingStore = useLayOutSettingStore()
​
let userStore = useUserStore()
​
//获取路由对象
let $route = useRoute()
</script><script lang="ts">
export default {
  name: 'Layout',
}
</script><style lang="scss" scoped>
.layout_container {
  width: 100%;
  height: 100vh;
​
  .layout_slider {
    color: white;
    width: $base-menu-width;
    height: 100vh;
    background: $base-menu-background;
    transition: all 0.3s;
    .scrollbar {
      width: 100%;
      height: calc(100vh - $base-menu-logo-height);
​
      .el-menu {
        border-right: none;
      }
    }
  }
​
  .layout_tabbar {
    position: fixed;
    width: calc(100% - $base-menu-width);
    height: $base-tabbar-height;
    top: 0px;
    left: $base-menu-width;
    transition: all 0.3s;
    background: pink;
    &.fold { //++++++++++++++++++++++++++
      width: calc(100vw - $base-menu-min-width);
      left: $base-menu-min-width;
    }
  }
​
  .layout_main {
    position: absolute;
    width: calc(100% - $base-menu-width);
    height: calc(100vh - $base-tabbar-height);
    left: $base-menu-width;
    top: $base-tabbar-height;
    padding: 20px;
    overflow: auto;
    transition: all 0.3s;
    background: #92dd22;
    &.fold {  //++++++++++++++++++++++++++
      width: calc(100vw - $base-menu-min-width);
      left: $base-menu-min-width;
    }
  }
}
</style>
5.3顶部面包屑动态展示

image-20230823233307741.png

分析: 目前我们顶部的组件是写死的,我们需要根据用户点击左侧的菜单栏动态的 将左侧点击的一级二级的菜单动态的切换到顶部组件的左边区域,实现思路就是从当前的路由对象中获取当前的path中的name,然后切换。

  1. 编辑 layout > tabbar > breadcrumb> index.vue

    <template>
        <!-- 顶部左侧静态 -->
        <el-icon style="margin-right:10px" @click="changeIcon">
             <component :is="LayOutSettingStore.fold ? 'Fold' : 'Expand'"></component>
        </el-icon>
        <!-- 左侧面包屑 -->
        <!-- ++++++++++++++++ start ++++++++++++++++++ -->
        <el-breadcrumb separator-icon="ArrowRight">
             <!-- 面包动态展示路由名字与标题 -->
            <el-breadcrumb-item v-for="(item, index) in $route.matched" :key="index" v-show="item.meta.title" :to="item.path">
                <!-- 图标 -->
                <el-icon>
                    <component :is="item.meta.icon"></component>
                </el-icon>
                <!-- 面包屑展示匹配路由的标题 -->
                <span>{{ item.meta.title }}</span>
            </el-breadcrumb-item>
        </el-breadcrumb>
    <!-- ++++++++++++++++ end ++++++++++++++++++ -->
    </template><script setup lang="ts">
      // +++++++ ++++
    import { useRoute } from 'vue-router';
    import { ref } from 'vue';
    import useLayOutSettingStore from '@/store/modules/setting';
    //获取layout配置相关的仓库
    let LayOutSettingStore = useLayOutSettingStore();
    //点击图标的方法
    const changeIcon = () => {
        //图标进行切换
        LayOutSettingStore.fold = !LayOutSettingStore.fold
    }
    ​
    //获取路由对象 +++++++ ++++
    let $route = useRoute();
    ​
    </script>
    <script lang="ts">
    export default {
        name: "Breadcrumb"
    }
    </script><style scoped></style>
    
5.4 顶部刷新按钮实现

顶部组件中的刷新按钮实现分析:因为刷新按钮在Breadscrumb组件中,而需要刷新的数据在Main组件中,他们之前属于叔侄关系,方便通信可在store中的setting.ts小仓库中维护一个refresh字段,点击刷新按钮后来改变这个值,进行取反操作,然后在Main组件中监听这个值,当这个值发生变化时通过v-if 来卸载整个页面,卸载完成后重启挂载这个页面来达到刷新的功能

store> modules > setting.ts

//小仓库:layout组件相关配置仓库
import { defineStore } from "pinia";
​
let useLayOutSettingStore = defineStore('SettingStore', {
    state: () => {
        return {
            fold: false,//用户控制菜单折叠还是收起控制
            refsh:false,//仓库这个属性用于控制刷新效果 +++++++++++++++++
        }
    }
})
​
export default useLayOutSettingStore;

编辑 layout > tabbar > breadscrumb > imdex.vue 的刷新按钮绑定 updateRefresh 事件

<template>
    <el-button size="small" icon="Refresh" circle @click="updateRefsh"></el-button> // +++++++++++++++++++
    <el-button size="small" icon="FullScreen" circle ></el-button>
    <el-button size="small" icon="Setting" circle ></el-button>
​
    
    <img src="../../../../public/logo.png" style="width: 24px;height: 24px;margin:0px 10px;border-radius: 50%;">
    <!-- 下拉菜单 -->
    <el-dropdown>
        <span class="el-dropdown-link">
            admin
            <el-icon class="el-icon--right">
                <arrow-down />
            </el-icon>
        </span>
        <template #dropdown>
            <el-dropdown-menu>
                <el-dropdown-item>退出登录</el-dropdown-item>
            </el-dropdown-menu>
        </template>
    </el-dropdown>
</template><script setup lang="ts">
import { ref } from 'vue'
// 获取配置的小仓库 +++++++++++++++++++++  start
import useLayOutSettingStore from '@/store/modules/setting';
let layoutSettingStore = useLayOutSettingStore();
​
const updateRefsh = () =>{
​
 layoutSettingStore.refsh = !layoutSettingStore.refsh;
​
}
​
// +++++++++++++++++++++  end
</script><script lang="ts">
export default {
    name: "Setting"
}
</script>
<style scoped></style>

编辑 layout > main > index.vue 中监听小仓库的 refresh的值

<template>
    <!-- 路由组件出口的位置 -->
    <router-view v-slot="{ Component }">
        <transition name="fade">
            <!-- 渲染layout一级路由组件的子路由 ++++++++++++++++++++ -->
            <component :is="Component"  v-if="flag" />
        </transition>
    </router-view>
</template><script setup lang="ts">
import { watch, ref, nextTick } from 'vue';
  
 // +++++++++++++++++++ 
import useLayOutSettingStore from '@/store/modules/setting';
let layOutSettingStore = useLayOutSettingStore();
​
//控制当前组件是否销毁重建 +++++++++++++++++++++
let flag = ref(true);
​
//监听仓库内部数据是否发生变化,如果发生变化,说明用户点击过刷新按钮 ++++++++++++++++++
watch(() => layOutSettingStore.refsh, () => {
    //点击刷新按钮:路由组件销毁
    flag.value = false;
    nextTick(() => {
        flag.value = true;
    })
})
​
</script>
<script lang="ts">
export default {
    name: "Main"
}
</script>
​
..............
5.5 顶部全屏模式切换按钮

功能分析:全屏功能我们利用原生的DOM 来实现

编辑 layout > tabbar> setting> index.vue

<template>
    <el-button size="small" icon="Refresh" circle @click="updateRefsh"></el-button>
    <el-button size="small" icon="FullScreen" circle @click="fullScreen"></el-button> // +++++++++++++
    <el-button size="small" icon="Setting" circle ></el-button>
​
    
    <img src="../../../../public/logo.png" style="width: 24px;height: 24px;margin:0px 10px;border-radius: 50%;">
    <!-- 下拉菜单 -->
    <el-dropdown>
        <span class="el-dropdown-link">
            admin
            <el-icon class="el-icon--right">
                <arrow-down />
            </el-icon>
        </span>
        <template #dropdown>
            <el-dropdown-menu>
                <el-dropdown-item>退出登录</el-dropdown-item>
            </el-dropdown-menu>
        </template>
    </el-dropdown>
</template><script setup lang="ts">
import { ref } from 'vue'
// 获取配置的小仓库
import useLayOutSettingStore from '@/store/modules/setting';
let layoutSettingStore = useLayOutSettingStore();
​
const updateRefsh = () =>{
​
 layoutSettingStore.refsh = !layoutSettingStore.refsh;
​
}
​
//全屏按钮点击的回调 ++++++++++++++++++++
const fullScreen = () => {
    //DOM对象的一个属性:可以用来判断当前是不是全屏模式[全屏:true,不是全屏:false]
    let full = document.fullscreenElement;
    //切换为全屏模式
    if (!full) {
        //文档根节点的方法requestFullscreen,实现全屏模式
        document.documentElement.requestFullscreen();
    } else {
        //变为不是全屏模式->退出全屏模式
        document.exitFullscreen();
    }
}
​
</script><script lang="ts">
export default {
    name: "Setting"
}
</script>
<style scoped></style>

用户信息与路由鉴权

1、获取用户信息与token理解

实现思路分析:登入请求成功后,就将用户的token存储到 store 和 localstorage中, 然后在路由的全局拦截器中为每次请求加上token,.登入成功后跳转到home页,在home的 mounted的钩子中调用store中的方法请求用户的详情,然后将用户的name 和 头像的url 放到user的仓库中,这样即可实现在任意地方显示用户的name 和头像。

  1. 在 utils>request.ts中的拦截器中为登入成功后的请求添加上token

    .........
    //第二步:request实列添加请求拦截器
    request.interceptors.request.use(config => {
        // config 配置对象,包含 headers 属性经常给服务端携带公共参数,如token等
        //获取用户相关的小仓库:获取仓库内部token,登录成功以后携带给服务器
        let userStore = useUserStore();
        console.log("===========>",userStore.token)
        if (userStore.token) {
            config.headers.token = userStore.token
        }
        // 返回一个配置对象
        return config;
    });
    .............
    
  2. 在store>modules>types>type.ts中对象添加username和avatar

    import type { RouteRecordRaw } from "vue-router";
    //定义小仓库数据state类型
    export interface UserState {
        token: string | null;
        menuRoutes: RouteRecordRaw[],
        username: '',  // ++++++++++++++++++
        avatar: '' // ++++++++++++++++++
    }
    
    1. 在store > modules>user.ts 的state添加 username和avatar 和获取用户详情的 userInfo ()
    //  创建用户相关的小仓库
    import { defineStore } from 'pinia';
    // 引入登入接口
    import { reqLogin,reqUserInfo } from '@/api/user';
    // 引入数据类型 +++++++++++
    import type { loginFormData, loginResponseData,userInfoReponseData } from '@/api/user/type';
    import type { UserState } from '@/store/modules/types/type'
    //引入操作本地存储的工具方法
    import { SET_TOKEN, GET_TOKEN } from '@/utils/token';
    ​
    //引入路由(常量路由)
    import { constantRoute } from '@/router/routes';
    ​
    // 创建用户小仓库 
    const useUserStore = defineStore('User', {
        // 小仓库存储数据的地方
    ​
        state: (): UserState => {
            return {
                token: GET_TOKEN(), //用户的唯一标识
                menuRoutes: constantRoute,//仓库存储生成菜单需要数组(路由)
                username: '', //++++++++++++++
                avatar: '' // +++++++++++++++
            }
        },
        // 异步|逻辑的地方
        actions: {
            // 用户登入的方法
            async  userLogin(data: loginFormData) {
                console.log('pinia 触发了登入请求 =======>', data)
                // 发送登入请求
                const result: loginResponseData = await reqLogin(data)
                // 登入成功 200 -> token
                // 登入失败 201 -> 登入失败的信息
                if (result.code == 200) {
                    console.log("获取登入成功的数据====", result)
                    // 由于 pinia | vuex 存储的数据其实是利用的 js 对象
                    this.token = (result.data.token as string);
                    // 本地持久化一份数据
                    SET_TOKEN((result.data.token as string));
                    //能保证当前async函数返回一个成功的promise
                    return 'ok';
                } else {
                    return Promise.reject(new Error(result.data.message));
                }
            },
    ​
    ​
            //获取用户信息方法 +++++++++++++++++++
            async userInfo() {
                //获取用户信息进行存储仓库当中[用户头像、名字]
                let result: userInfoReponseData = await reqUserInfo();
    ​
                console.log("yonghu",result)
                //如果获取用户信息成功,存储一下用户信息
                if (result.code == 200) {
                    this.username = result.data.checkUser.username;
                    this.avatar = result.data.checkUser.avatar;
                    return 'ok';
                } else {
                    return Promise.reject(new Error(result.message));
                }
            },
    ​
    ​
    ​
        },
        getters: {
    ​
    ​
        }
    ​
    })
    ​
    // 对外暴露获取小仓库
    export default useUserStore;
    ​
    
    1. 在 views> home > index.vue 的onMounted() 方法中请求user仓库中的获取用户详情的方法
    <template>
      <div>
        <h1>我是一级路由 home</h1>
      </div>
    </template>
    <script setup lang="ts">
    import { ref, reactive, onMounted } from 'vue'
    // 获取仓库 ++++++++++++++++ start
    import useUserStore from '@/store/modules/user'
    let userStore = useUserStore()
    onMounted(() => {
      userStore.userInfo()
    })
      // 获取仓库 ++++++++++++++++ end
    </script>
    <style lang="scss" scoped></style>
    1. 在layout > tabbar > setting > index.vue 中取出仓库中的用户名和头像图标并在组件中显示
    <template>
        <el-button size="small" icon="Refresh" circle @click="updateRefsh"></el-button>
        <el-button size="small" icon="FullScreen" circle @click="fullScreen"></el-button>
        <el-button size="small" icon="Setting" circle ></el-button>
    ​
        <!-- ++++++++++++++ -->
        <img :src="userStore.avatar" style="width: 24px;height: 24px;margin:0px 10px;border-radius: 50%;">
        <!-- 下拉菜单 -->
        <el-dropdown>
            <span class="el-dropdown-link">
                {{ userStore.username }}  <!-- ++++++++++++++ -->
                <el-icon class="el-icon--right">
                    <arrow-down />
                </el-icon>
            </span>
            <template #dropdown>
                <el-dropdown-menu>
                    <el-dropdown-item>退出登录</el-dropdown-item>
                </el-dropdown-menu>
            </template>
        </el-dropdown>
    </template><script setup lang="ts">
    import { ref } from 'vue'
    // 获取配置的小仓库
    import useLayOutSettingStore from '@/store/modules/setting';
    //获取用户相关的小仓库  +++++++++
    import useUserStore from '@/store/modules/user';
    ​
    ​
    let layoutSettingStore = useLayOutSettingStore();
     // ++++++++++++++++
    let userStore = useUserStore();
    ​
    ...............
    

2、退出登入业务

退出登入分析:请求后端退出的接口,将user仓库中和localStorage中关于用户的信息清除,并且页面跳转到登入页面。

  1. 在utils > token.ts 中添加清除localStorage 中用户信息的方法
//封装本地存储存储数据与读取数据方法
//存储数据
export const SET_TOKEN = (token: string) => {
    localStorage.setItem("TOKEN", token);
}
//本地存储获取数据
export const GET_TOKEN = () => {
    return localStorage.getItem('TOKEN');
}
//本地存储删除数据方法 ++++++++++++++
export const REMOVE_TOKEN = () => {
    localStorage.removeItem('TOKEN');
}
  1. 在 store> modules > uset.ts 新增退出登入的方法
//  创建用户相关的小仓库
import { defineStore } from 'pinia';
// 引入登入接口 +++++++
import { reqLogin, reqUserInfo, reqLogout } from '@/api/user';
// 引入数据类型
import type { loginFormData, loginResponseData, userInfoReponseData } from '@/api/user/type';
import type { UserState } from '@/store/modules/types/type'
//引入操作本地存储的工具方法  +++++++++++++++
import { SET_TOKEN, GET_TOKEN ,REMOVE_TOKEN} from '@/utils/token';
​
//引入路由(常量路由)
import { constantRoute } from '@/router/routes';
​
// 创建用户小仓库 
const useUserStore = defineStore('User', {
    // 小仓库存储数据的地方
​
    state: (): UserState => {
        return {
            token: GET_TOKEN(), //用户的唯一标识
            menuRoutes: constantRoute,//仓库存储生成菜单需要数组(路由)
            username: '',
            avatar: ''
        }
    },
    // 异步|逻辑的地方
    actions: {
        // 用户登入的方法
        async  userLogin(data: loginFormData) {
            console.log('pinia 触发了登入请求 =======>', data)
            // 发送登入请求
            const result: loginResponseData = await reqLogin(data)
            // 登入成功 200 -> token
            // 登入失败 201 -> 登入失败的信息
            if (result.code == 200) {
                console.log("获取登入成功的数据====", result)
                // 由于 pinia | vuex 存储的数据其实是利用的 js 对象
                this.token = (result.data.token as string);
                // 本地持久化一份数据
                SET_TOKEN((result.data.token as string));
                //能保证当前async函数返回一个成功的promise
                return 'ok';
            } else {
                return Promise.reject(new Error(result.data.message));
            }
        },
​
​
        //获取用户信息方法
        async userInfo() {
            //获取用户信息进行存储仓库当中[用户头像、名字]
            let result: userInfoReponseData = await reqUserInfo();
​
            console.log("yonghu", result)
            //如果获取用户信息成功,存储一下用户信息
            if (result.code == 200) {
                this.username = result.data.checkUser.username;
                this.avatar = result.data.checkUser.avatar;
                return 'ok';
            } else {
                return Promise.reject(new Error(result.message));
            }
        },
​
        //退出登录 ++++++++++++++++++
        async userLogout() {
            //退出登录请求
            // let result: any = await reqLogout(); 退出接口不存在所以先写死
            if (true) {
                //目前没有mock接口:退出登录接口(通知服务器本地用户唯一标识失效)
                this.token = '';
                this.username = '';
                this.avatar = '';
                REMOVE_TOKEN();
                return 'ok';
            } else {
                return Promise.reject(new Error(result.message));
            }
​
        },
​
​
    },
​
​
    getters: {
​
​
    }
​
})
​
// 对外暴露获取小仓库
export default useUserStore;
​
  1. 在layout > tabbar >setting >index.vue 实现 logout 方法
<template>
  <el-button
    size="small"
    icon="Refresh"
    circle
    @click="updateRefsh"
  ></el-button>
  <el-button
    size="small"
    icon="FullScreen"
    circle
    @click="fullScreen"
  ></el-button>
  <el-button size="small" icon="Setting" circle></el-button>
​
  <img
    :src="userStore.avatar"
    style="width: 24px; height: 24px; margin: 0px 10px; border-radius: 50%"
  />
  <!-- 下拉菜单 -->
  <el-dropdown>
    <span class="el-dropdown-link">
      {{ userStore.username }}
      <el-icon class="el-icon--right">
        <arrow-down />
      </el-icon>
    </span>
    <template #dropdown>
      <el-dropdown-menu>
        <!-- ++++++++++++++++++++ -->
        <el-dropdown-item @click="logout">退出登录</el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template><script setup lang="ts">
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
​
// 获取配置的小仓库
import useLayOutSettingStore from '@/store/modules/setting';
//获取用户相关的小仓库
import useUserStore from '@/store/modules/user';
​
let layoutSettingStore = useLayOutSettingStore()
let userStore = useUserStore()
​
//获取路由器对象
let $router = useRouter()
//获取路由对向
let $route = useRoute()
​
const updateRefsh = () => {
  layoutSettingStore.refsh = !layoutSettingStore.refsh
}
​
//全屏按钮点击的回调
const fullScreen = () => {
  //DOM对象的一个属性:可以用来判断当前是不是全屏模式[全屏:true,不是全屏:false]
  let full = document.fullscreenElement
  //切换为全屏模式
  if (!full) {
    //文档根节点的方法requestFullscreen,实现全屏模式
    document.documentElement.requestFullscreen()
  } else {
    //变为不是全屏模式->退出全屏模式
    document.exitFullscreen()
  }
}
​
//退出登录点击回调 +++++++++++++++
const logout = async () => {
  //第一件事情:需要向服务器发请求[退出登录接口]******
  //第二件事情:仓库当中关于用于相关的数据清空[token|username|avatar]
  //第三件事情:跳转到登录页面
  await userStore.userLogout()
  //跳转到登录页面
  $router.push({ path: '/login', query: { redirect: $route.path } })
}
</script><script lang="ts">
export default {
  name: 'Setting',
}
</script>
<style scoped></style>

3、路由鉴权之进度业务

实现分析:要实现点击左侧菜单栏路由切换时,浏览器需要实现一个进度条显示,我们的实现思路是在路由的全局守卫前置拦截的地方开启nprogressc 的进度显示插件,路由请求完成时关闭此插件来实现此功能。

  1. 安装进度条插件

    pnpm install nprogress
    
  2. 在src下新建permisstion.ts 文件,引入进度条插件和插件的的样式(并且可以自定进度条颜色,在对应的css文件修改即可)

    //路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
    import router from '@/router';
    import setting from './setting';
    import nprogress from 'nprogress';
    //引入进度条样式
    import "nprogress/nprogress.css";
    ​
    ​
    //全局守卫:项目当中任意路由切换都会触发的钩子
    //全局前置守卫
    router.beforeEach(async (to: any, from: any, next: any) => {
        //to:你将要访问那个路由
        //from:你从来个路由而来
        //next:路由的放行函数
        nprogress.start();
        next();
    ​
    })
    //全局后置守卫
    router.afterEach((to: any, from: any) => {
        nprogress.done();
    });
    
  3. 在main.ts 中引入 permisstion.ts

import { createApp } from 'vue'
import App from './App.vue'
// 引入 element-plus 插件与样式
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css'
//@ts-ignore忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
// svg 插件需要引入的配置代码
import 'virtual:svg-icons-register'
// 引入自定义插件对象:注册整个项目的全局组件
import gloablComponent from '@/components/index';
// 引入模板的全局样式
import '@/styles/index.scss'// 引入路由
import router from '@/router'
// 引入仓库 pinia
import pinia from '@/store'
// 引入权限控制 ++++++++++++++++
import '@/permisstion'
​
​
// 获取应用实列对象
const app = createApp(App)
// 安装element-plus插件
app.use(ElementPlus, {
    locale: zhCn
});
// 安装自定义插件--> 会调用@/components/index.js中的方法
app.use(gloablComponent);
// 注册模板路由
app.use(router);
// 注册仓库
app.use(pinia);
​
// 将应用挂载到挂载点
app.mount('#app')
​

4、路由鉴权

问题引入:问题 一,目前我们的项目的即使登入成功了,但是在浏览器输入 /login 的url 的时候还是会跳转到登入的页面,问题二,即使我们没有登入在浏览器的地址栏输入其他页面的 url 仍然是可以访问的。问题三,在登入成功后会自动跳转到home页面,因为home 的 onMounted方法会去请求用户详情然后显示出用户的头像和name,但是如果点击左侧的菜单的导航栏到其他页面的时候,点击刷新浏览器时用户的头像,username就会丢失 等等问题。

实现分析: 我们的需求是,用户未登录:可以访问login,其余六个路由不能访问(指向login),用户登录成功:不可以访问login[指向首页],其余的路由可以访问,所以我们可以在permisstion.ts 中引入用户仓库,然后在全局守卫的前置拦截的地方,每次要路由跳转前去仓库中获取用户的token,如果有token说明用户登入了,则不让其跳转到登入页面,然后根据仓库中存储的用户的对用的权限来判断当前操作用户是否有全校跳转到对应的路由页面。其次不管每次路由跳转到哪个组件中,顶部组件的用户名和头像图片地址都是需要的,之前写在了home页面中(现在可以将home中获取用户的详情的逻辑去除掉),现在我们可以写在全局守卫这里,应为每个页面都需要。

permisstion.ts 编辑如下:

//路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from '@/router';
import setting from './setting';
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";
nprogress.configure({ showSpinner: false });
​
//获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user';
import pinia from './store';
let userStore = useUserStore(pinia);
​
//全局守卫:项目当中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
    // 动态替换每次路由跳转后浏览器title的值
    document.title = `${setting.title} - ${to.meta.title}`
​
    //to:你将要访问那个路由
    //from:你从来个路由而来
    //next:路由的放行函数
    nprogress.start();
   //获取token,去判断用户登录、还是未登录
    
    console.log("ttt",userStore.token)
   let token = userStore.token;
   //获取用户名字
   let username = userStore.username;
   //用户登录判断
   if (token) {
    //登录成功,访问login,不能访问,指向首页
    if (to.path == '/login') {
        next({ path: '/' })
    } else {
        //登录成功访问其余六个路由(登录排除)
        //有用户信息
        if (username) {
            //放行
            next();
        } else {
            //如果没有用户信息,在守卫这里发请求获取到了用户信息再放行
            console.log("如果没有用户信息,在守卫这里发请求获取到了用户信息再放行")
            try {
                //获取用户信息
                await userStore.userInfo();
                //放行
                //万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
                next({...to});
            } 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();
});

5、真实接口替换mock接口

功能分析:之前的接口都是假的接口,直接是在前端通过mock生成的,现在我们需要修改为真实的接口地址,需要修改的地方:

服务器域名:sph-api.atguigu.cn

swagger文档:

(商品相关) http://139.198.104.58:8209/swagger-ui.html (用户相关) http://139.198.104.58:8212/swagger-ui.html#/

  1. 修改三个环境变量文件(.env.development,.env.production ,.env.test)为真实的环境变量

    1. .env.development
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/api'
VITE_SERVE="http://sph-api.atguigu.cn"
  1. .env.production
NODE_ENV = 'production'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/prod-api'
VITE_SERVE="http://sph-api.atguigu.cn"
  1. .env.test
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/test-api'
VITE_SERVE="http://sph-api.atguigu.cn" 
  1. 修改 vite.config.ts 中的使用的当前环境的配置文集和代理服务器

    import { defineConfig, loadEnv } 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, mode }) => {  // +++++++++
    ​
      //获取各种环境下的对应的变量 默认就是开发环境,打包的时候可以指定 mode 为哪一个配置文件,是.env.development还是.env.production +++++++++++++
      let env = loadEnv(mode, process.cwd());
    ​
      return {
        plugins: [vue(),
        createSvgIconsPlugin({
          // Specify the icon folder to be cached
          iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
          // Specify symbolId format
          symbolId: 'icon-[dir]-[name]',
        }),
        viteMockServe({
          localEnabled: command === 'serve', //保证开发阶段可以使用mock的数据
        })
        ],
        resolve: {
          alias: {
            "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
          }
        },
        css: {
          preprocessorOptions: {
            scss: {
              javascriptEnabled: true,
              additionalData: '@import "./src/styles/variable.scss";',
            },
          },
        },
    ​
        //代理跨域 +++++++++++++++++++
        server: {
          proxy: {
            [env.VITE_APP_BASE_API]: {
              //获取数据的服务器地址设置
              target: env.VITE_SERVE,
              //需要代理跨域
              changeOrigin: true,
              //路径重写
              rewrite: (path) => path.replace(/^/api/, ''),
            }
          }
        }
      }
    })
    ​
    
  2. 修改 api > user > index.ts 中的api 地址

//统一管理咱们项目用户相关的接口
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<any, loginResponseData>(API.LOGIN_URL, data);
//获取用户信息
export const reqUserInfo = () => request.get<any, userInfoReponseData>(API.USERINFO_URL);
//退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

注意:涉及到的 type 对象(loginFormData,loginResponseData,userInfoReponseData)等都要检查修改,因为真实的接口和之前测模拟接口存在差异。

  1. 检查api> user >index.ts 中所有的类型数据

    //定义用户相关数据的ts类型
    //用户登录接口携带参数的ts类型
    export interface loginFormData {
        username: string,
        password: string
    }
    ​
    //定义全部接口返回数据都拥有ts类型
    export interface ResponseData{
        code:number,
        message:string,
        ok:boolean
    }
    ​
    //定义登录接口返回数据类型
    export interface loginResponseData extends ResponseData{
           data:string
    }
    ​
    //定义获取用户信息返回数据类型
    export interface userInfoReponseData extends ResponseData{
        data:{
            routes:string[],
            buttons:string[],
            roles:string[],
            name:string,
            avatar:string
        }
    }
    
  2. store > modules > user.ts 中的登入,用户详情,退出接口,以及请求和接收参数的 type对象(真实的接口和之前假接口类型存在差异)

    //  创建用户相关的小仓库
    import { defineStore } from 'pinia';
    // 引入登入接口
    import { reqLogin, reqUserInfo, reqLogout } from '@/api/user';
    // 引入数据类型
    import type { loginFormData, loginResponseData, userInfoReponseData } from "@/api/user/type";
    import type { UserState } from '@/store/modules/types/type'
    //引入操作本地存储的工具方法
    import { SET_TOKEN, GET_TOKEN ,REMOVE_TOKEN} from '@/utils/token';
    ​
    //引入路由(常量路由)
    import { constantRoute } from '@/router/routes';
    ​
    // 创建用户小仓库 
    const useUserStore = defineStore('User', {
        // 小仓库存储数据的地方
    ​
        state: (): UserState => {
            return {
                token: GET_TOKEN(), //用户的唯一标识
                menuRoutes: constantRoute,//仓库存储生成菜单需要数组(路由)
                username: '',
                avatar: '',
                //存储当前用户是否包含某一个按钮 (此属性是这次新加的,需要早在对应的UserState也加入)
                buttons:[]
            }
        },
        // 异步|逻辑的地方
        actions: {
            // 用户登入的方法
            async userLogin(data: loginFormData) {
                //登录请求
                let result: loginResponseData = await reqLogin(data);
                //登录请求:成功200->token 
                //登录请求:失败201->登录失败错误的信息
                console.log("请求登入成功后的数据:",result)
                if (result.code == 200) {
                    //pinia仓库存储一下token
                    //由于pinia|vuex存储数据其实利用js对象
                    this.token = (result.data as string);
                    //本地存储持久化存储一份
                    SET_TOKEN((result.data as string));
                    //能保证当前async函数返回一个成功的promise
                    return 'ok';
                } else {
                    return Promise.reject(new Error(result.data));
                }
            },
    ​
    ​
            //获取用户信息方法
            async userInfo() {
                //获取用户信息进行存储仓库当中[用户头像、名字] 
                let result: userInfoReponseData = await reqUserInfo();
                console.log("获取用户的详情为:", result)
                //如果获取用户信息成功,存储一下用户信息
                if (result.code == 200) {
                    this.username = result.data.name;
                    this.avatar = result.data.avatar;
                    this.buttons = result.data.buttons;
                    return 'ok';
                } else {
                    return Promise.reject(new Error(result.message));
                }
            },
    ​
            //退出登录
            async userLogout() {
                //退出登录请求
                let result: any = await reqLogout();
                console.log("退出登入响应:",result)
                if (result.code == 200) {
                    //目前没有mock接口:退出登录接口(通知服务器本地用户唯一标识失效)
                    this.token = '';
                    this.username = '';
                    this.avatar = '';
                    REMOVE_TOKEN();
                    return 'ok';
                } else {
                    return Promise.reject(new Error(result.message));
                }
    ​
            }
    ​
    ​
        },
    ​
        getters: {
    ​
        }
    ​
    })
    ​
    // 对外暴露获取小仓库
    export default useUserStore;
    ​
    

6、接口类型定义

此功能在上一步的第四小步已经完成了。