vue TS项目

使用到的资源
vue官网: https://cn.vuejs.org/
element-ui : https://element.eleme.cn/#/zh-CN/component/icon
ESLint
TS
SCSS

源码地址: https://github.com/qifutian/learngit/tree/main/TSvue-DEMO/edu-boss-fed

一 、 搭建项目架构

使用vue-cli创建项目

  1. vue create edu-boss-fed
  2. 使用自定义选项,选择babel,ts,route,vuex,css预处理,linter
  3. 使用class风格描述组件
  4. 选择ts和babel编译
  5. 不选择history路由模式
  6. 选择预处理器Sass,建议选择 dart-sass
  7. 选择ESlint
  8. 单独存放个自的配置文件
  9. 进入项目中,npm run serve 启动,默认打开8080地址

加入git 版本管理

  1. 创建远程仓库
  2. 本地代码提交远程仓库
  3. 生成git本地仓库 git init
  4. git status 查看当前状态
  5. git add . 将所有文件放到暂存区
  6. git status 查看暂存区内容,是否选中
  7. git commit -m "init " 记录提交日志
  8. git log 查看历史记录
  9. git remote add origin 远程地址 关联远程地址
  10. git remote -v 查看远程地址
  11. git push -u origin master 第一次记录当前origin中的master分支

目录结构

main.ts — 整个系统的入口
public — 纯静态资源文件,放index.html,不被webpack打包的
App.vue — 项目中的根组件,router-view是根路由出口
shims.tsx.d.ts 和 shims-vue.d.ts — 都是整个项目中ts的配置
Home.vue — home组件,组件中是ts规范
store — vuex目录,存放vuex的配置
route — 放置路由表,存放对应的ts路由,严格描述
components — 放置公共的组件文件夹
assets — 放置静态资源
node_modules — 第三方包
.gitignore — 放置不会被git 提交的文件
babel.config.js — babel 的配置
tsconfig.json – ts的配置

调整初始目录结构

App.vue 删除样式,删除多余模板
route/index.ts 清空路由表
删除Home.vue 和 about.vue
删除Hello.vue,删除log.png
创建utils目录,存放一些功能模块,请求等
创建styles目录,全局样式
创建services,放接口部分

环境说明

在Vue中如何启动 typescript 支持
两种方式: 1. 在使用vue-cli创建项目中选择typescript
2. 已有项目,添加vue官方配置的ts适配插件

vue  add  @vue/typescript

使用TS开发,编辑器推荐VS Code
如果是vue项目,推荐安装 vetur插件

相关配置说明
ts相关的配置
vue TS项目_第1张图片
vue TS项目_第2张图片
vue TS项目_第3张图片
vue TS项目_第4张图片

使用Options APIs
定义组件的方式

要让 TypeScript 正确推断 Vue 组件选项中的类型,您需要使用 Vue.component 或 Vue.extend 定义组件:

import Vue from 'vue'
const Component = Vue.extend({
     
  // 类型推断已启用
})

const Component = {
     
  // 这里不会有类型推断,
  // 因为 TypeScript 不能确认这是 Vue 组件的选项
}

基于类的 Vue 组件

import Vue from 'vue'
import Component from 'vue-class-component'

// @Component 修饰符注明了此类为一个 Vue 组件
@Component({
     
  // 所有的组件选项都可以放在这里
  template: ''
})
export default class MyComponent extends Vue {
     
  // 初始数据可以直接声明为实例的 property
  message: string = 'Hello!'

  // 组件方法也可以直接声明为实例的方法
  onClick (): void {
     
    window.alert(this.message)
  }
}

装饰器语法

vue TS项目_第5张图片

代码格式规范

良好的代码格式有利于维护,多人协作,阅读
约束代码规范
默认阅读不可靠,需要工具强制执行

  • JSLint
  • JSHint
  • ESLint 等等

项目中的代码规范

module.exports = {
     
  root: true,
  env: {
     
    node: true
  },
  // 使用插件的编码校验规则
  extends: [
    'plugin:vue/essential',
    '@vue/standard',
    '@vue/typescript/recommended'
  ],
  parserOptions: {
     
    ecmaVersion: 2020
  },

  // 自定义编码校验规则
  rules: {
     
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    // 'semi': ['error', 'always']
    '@typescript-eslint/member-delimiter-style': ['error', {
     
      "multiline": {
     
        "delimiter": "none",
        "requireLast": true
      }
    }]
  }
}

自定义的校验规则

// 自定义编码校验规则
  rules: {
     
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    // 'semi': ['error', 'always']
    '@typescript-eslint/member-delimiter-style': ['error', {
     
      "multiline": {
     
        "delimiter": "none",
        "requireLast": true
      }
    }]
  }

导入element

安装: npm i element-ui -S
引入element-ui
两种方式: 1.全部引入 2. 按需引入

在 main.js 中写入以下内容:

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

Vue.use(ElementUI);

new Vue({
     
  el: '#app',
  render: h => h(App)
});

样式处理

在使用element,进行组件定制
在src/styles中新增

index.scss 全局样式,在入口模块被加载
mixin.scss 公共的mixin混入
reset.scss 重置默认样式
variables.scss 公共样式变量

index.scss

@import './variables.scss';

// globals
html {
     
  font-family: $font-family;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  // better Font Rendering
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
     
  margin: 0;
  background-color: $body-bg;
}

// custom element theme
$--color-primary: $primary-color;
$--color-success: $success-color;
$--color-warning: $warning-color;
$--color-danger: $danger-color;
$--color-info: $info-color;
// change font path, required
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// import element default theme
@import '~element-ui/packages/theme-chalk/src/index';
// node_modules/element-ui/packages/theme-chalk/src/common/var.scss

// overrides

// .el-menu-item, .el-submenu__title {
     
//   height: 50px;
//   line-height: 50px;
// }

.el-pagination {
     
  color: #868e96;
}

// components

.status {
     
  display: inline-block;
  cursor: pointer;
  width: .875rem;
  height: .875rem;
  vertical-align: middle;
  border-radius: 50%;

  &-primary {
     
    background: $--color-primary;
  }

  &-success {
     
    background: $--color-success;
  }

  &-warning {
     
    background: $--color-warning;
  }

  &-danger {
     
    background: $--color-danger;
  }

  &-info {
     
    background: $--color-info;
  }
}

variables.scss

$primary-color: #40586F;
$success-color: #51cf66;
$warning-color: #fcc419;
$danger-color: #ff6b6b;
$info-color: #868e96; // #22b8cf;

$body-bg: #E9EEF3; // #f5f5f9;

$sidebar-bg: #F8F9FB;
$navbar-bg: #F8F9FB;

$font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

样式处理 – 共享全局样式变量

在全局配置,节省组件加载,注入到全局

增加vue.config.js

// vue.config.js
module.exports = {
     
  css: {
     
    loaderOptions: {
     
      // 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效
      // 因为 `scss` 语法在内部也是由 sass-loader 处理的
      // 但是在配置 `prependData` 选项的时候
      // `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号
      // 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置
      scss: {
     
        prependData: `@import "~@/styles/variables.scss";`
      }
    }
  },

  
}

接口处理

使用proxy设置代理接口地址
在vue.config.js 在添加

devServer: {
     
    proxy: {
     
      '/boss': {
     
        target: 'http://eduboss.lagou.com',
        changeOrigin: true // 把请求头中的 host 配置为 target
      },
      '/front': {
     
        target: 'http://edufront.lagou.com',
        changeOrigin: true
      }
    }
  }

封装请求模块

安装 axios
npm i axios
创建请求模块,src/utils/request.js

import axios from 'axios'

const request = axios.create({
     
  // 配置选项
  // baseURL,
  // timeout
})

// 请求拦截器

// 响应拦截器

export default request

初始化路由组件

view下新创建对应的文件
在route/index.ts 下配置 路由懒加载,进行lauout布局,路由嵌套
layout 布局,src下新建layout文件夹,将除了登录页和404页面外全部是layout的子路由

import Vue from 'vue'
import VueRouter, {
      RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'

Vue.use(VueRouter)

// 路由配置规则
const routes: Array<RouteConfig> = [
  {
     
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
  },
  {
     
    path: '/',
    component: Layout,
    children: [
      {
     
        path: '', // 默认子路由
        name: 'home',
        component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
      },
      {
     
        path: '/role',
        name: 'role',
        component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue')
      },
      {
     
        path: '/menu',
        name: 'menu',
        component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
      },
      {
     
        path: '/resource',
        name: 'resource',
        component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
      },
      {
     
        path: '/course',
        name: 'course',
        component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue')
      },
      {
     
        path: '/user',
        name: 'user',
        component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
      },
      {
     
        path: '/advert',
        name: 'advert',
        component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
      },
      {
     
        path: '/advert-space',
        name: 'advert-space',
        component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
      }
    ]
  },
  {
     
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
  }
]

const router = new VueRouter({
     
  routes
})

export default router

侧边栏组件

使用container布局容器
在app-aside.vue中使用el-menu标签,加入route进入导航资源

<template>
  <div class="aside">
    <el-menu
      default-active="4"
      @open="handleOpen"
      @close="handleClose"
      background-color="#545c64"
      text-color="#fff"
      active-text-color="#ffd04b"
      router
    >
      <el-submenu index="1">
        <template slot="title">
          <i class="el-icon-location"></i>
          <span>权限管理</span>
        </template>
        <el-menu-item index="/role">
          <i class="el-icon-setting"></i>
          <span slot="title">角色管理</span>
        </el-menu-item>
        <el-menu-item index="/menu">
          <i class="el-icon-setting"></i>
          <span slot="title">菜单管理</span>
        </el-menu-item>
        <el-menu-item index="/resource">
          <i class="el-icon-setting"></i>
          <span slot="title">资源管理</span>
        </el-menu-item>
      </el-submenu>
      <el-menu-item index="/course">
        <i class="el-icon-menu"></i>
        <span slot="title">课程管理</span>
      </el-menu-item>
      <el-menu-item index="/user">
        <i class="el-icon-document"></i>
        <span slot="title">用户管理</span>
      </el-menu-item>
      <el-submenu index="4">
        <template slot="title">
          <i class="el-icon-location"></i>
          <span>广告管理</span>
        </template>
        <el-menu-item index="/advert">
          <i class="el-icon-setting"></i>
          <span slot="title">广告列表</span>
        </el-menu-item>
        <el-menu-item index="/advert-space">
          <i class="el-icon-setting"></i>
          <span slot="title">广告位列表</span>
        </el-menu-item>
      </el-submenu>
    </el-menu>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
     
  name: 'AppAside',
  methods: {
     
    handleOpen (key: string, keyPath: string): void {
     
      console.log(key, keyPath)
    },

    handleClose (key: string, keyPath: string): void {
     
      console.log(key, keyPath)
    }
  }
})
</script>

<style lang="scss" scoped>
.aside {
     
  .el-menu {
     
    min-height: 100vh;
  }
}
</style>

头部导航

采用面包屑导航,新建app-header.vue组件
采用下拉菜单作为用户信息

<template>
  <div class="header">
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>活动管理</el-breadcrumb-item>
      <el-breadcrumb-item>活动列表</el-breadcrumb-item>
      <el-breadcrumb-item>活动详情</el-breadcrumb-item>
    </el-breadcrumb>
    <el-dropdown>
      <span class="el-dropdown-link">
        <el-avatar
          shape="square"
          :size="40"
          src="https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png"
        ></el-avatar>
        <i class="el-icon-arrow-down el-icon--right"></i>
      </span>
      <el-dropdown-menu slot="dropdown">
        <el-dropdown-item>用户ID</el-dropdown-item>
        <el-dropdown-item divided>退出</el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
     
  name: 'AppHeader'
})
</script>

<style lang="scss" scoped>
.header {
     
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  .el-dropdown-link {
     
    display: flex;
    align-items: center;
  }
}
</style>

页面布局

进行登录页面的布局,采用form表单组件
可以只使用样式部分,js逻辑会进行删除

<template>
  <div class="login-container">
    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">

      <div class="title-container">
        <h3 class="title">综合管理平台</h3>
      </div>

      <el-form-item prop="username">
        <span class="svg-container">
          <i class = "el-icon-user"></i>
        </span>
        <el-input
          ref="username"
          v-model="loginForm.username"
          placeholder="请输入用户名"
          name="username"
          type="text"
          tabindex="1"
          auto-complete="on"
        />
      </el-form-item>

      <el-form-item prop="password">
        <span class="svg-container">
           <i class="el-icon-view"></i>
        </span>
        <el-input
          :key="passwordType"
          ref="password"
          v-model="loginForm.password"
          :type="passwordType"
          placeholder="请输入密码"
          name="password"
          tabindex="2"
          auto-complete="on"
          @keyup.enter.native="handleLogin"
        />
        <span class="show-pwd" @click="showPwd">
          <!-- <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /> -->
        </span>
      </el-form-item>

      <div class="box clearfix">
        <div class="rf">
            <el-checkbox v-model="checked" style="color:#a0a0a0; padding-bottom: 10px;">记住密码</el-checkbox>
        </div>
      </div>

      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>

    </el-form>
  </div>
</template>

<script lang="ts">

import Vue from 'vue'
export default Vue.extend({
     
  name: 'LoginIndex',
  data () {
     
    const validateUsername = (rule, value, callback) => {
     
      callback()
    }
    const validatePassword = (rule, value, callback) => {
     
      if (value.length < 6) {
     
        callback(new Error('密码不低于六位数!'))
      } else {
     
        callback()
      }
    }
    return {
     
      loginForm: {
     
        username: '',
        password: ''
      },
      loginRules: {
     
        username: [{
      required: true, trigger: 'blur', validator: validateUsername }],
        password: [{
      required: true, trigger: 'blur', validator: validatePassword }]
      },
      loading: false,
      passwordType: 'password',
      redirect: undefined,
      checked: false
    }
  },
  mounted () {
     
    this.getCookie()
  },
  watch: {
     
    $route: {
     
      handler: function (route) {
     
        this.redirect = route.query && route.query.redirect
      },
      immediate: true
    }
  },
  methods: {
     
    showPwd () {
     
      if (this.passwordType === 'password') {
     
        this.passwordType = ''
      } else {
     
        this.passwordType = 'password'
      }
      this.$nextTick(() => {
     
        this.$refs.password.focus()
      })
    },
    handleLogin () {
     
      console.log('登录')
      // this.$refs.loginForm.validate(valid => {
     
      //   if (valid) {
     
      //     if (this.checked) {
     
      //       this.setCookie(this.loginForm.username, this.loginForm.password, 7)
      //     } else {
     
      //       this.clearCookie()
      //     }
      //     this.loading = true
      //     this.$store.dispatch('user/login', this.loginForm).then(() => {
     
      //       this.$router.push({ path: this.redirect || '/' })
      //       this.loading = false
      //     }).catch(() => {
     
      //       this.loading = false
      //     })
      //   } else {
     
      //     console.log('error submit!!')
      //     return false
      //   }
      // })
    },
    setCookie (name, pwd, exdays) {
     
      const exdate = new Date()
      exdate.setTime(exdate.getTime() + 24 * 60 * 60 * 1000 * exdays)
      window.document.cookie = 'userName' + '=' + name + ';path=/;expires=' + exdate.toGMTString()
      window.document.cookie = 'password' + '=' + pwd + ';path=/;expires=' + exdate.toGMTString()
    },
    getCookie () {
     
      if (document.cookie.length > 0) {
     
        const arr = document.cookie.split('; ')
        for (let i = 0; i < arr.length; i++) {
     
          const arr2 = arr[i].split('=')
          if (arr2[0] === 'userName') {
     
            this.loginForm.username = arr2[1]
          } else if (arr2[0] === 'password') {
     
            this.loginForm.password = arr2[1]
          }
        }
        this.checked = true
      }
    },
    clearCookie () {
     
      this.setCookie('', '', -1)
    }
  }
})
</script>

<style lang="scss">

$bg:#283443;
$light_gray:#fff;
$cursor: #fff;

@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
     
  .login-container .el-input input {
     
    color: $cursor;
  }
}

/* reset element-ui css */
.login-container {
     
  .el-input {
     
    display: inline-block;
    height: 47px;
    width: 85%;

    input {
     
      background: transparent;
      border: 0px;
      -webkit-appearance: none;
      border-radius: 0px;
      padding: 12px 5px 12px 15px;
      color: $light_gray;
      height: 47px;
      caret-color: $cursor;

      &:-webkit-autofill {
     
        box-shadow: 0 0 0px 1000px $bg inset !important;
        -webkit-text-fill-color: $cursor !important;
      }
    }
    .rf{
     
      float: right;
    }
    .box{
     
      min-width: 350px;
      margin-left:50px;
      width: 30%;
    }
    .clearfix:after {
     
      content:".";
      display:block;
      height:0;
      visibility:hidden;
      clear:both;
    }
    .clearfix {
     
      *zoom:1;
    }
  }

  .el-form-item {
     
    border: 1px solid rgba(255, 255, 255, 0.1);
    background: rgba(0, 0, 0, 0.1);
    border-radius: 5px;
    color: #454545;
  }
}
</style>

<style lang="scss" scoped>
$bg:#2d3a4b;
$dark_gray:#889aa4;
$light_gray:#eee;

.login-container {
     
  min-height: 100%;
  width: 100%;
  background-color: $bg;
  overflow: hidden;

  .login-form {
     
    position: relative;
    width: 520px;
    max-width: 100%;
    padding: 160px 35px 0;
    margin: 0 auto;
    overflow: hidden;
  }

  .tips {
     
    font-size: 14px;
    color: #fff;
    margin-bottom: 10px;

    span {
     
      &:first-of-type {
     
        margin-right: 16px;
      }
    }
  }

  .svg-container {
     
    padding: 6px 5px 6px 15px;
    color: $dark_gray;
    vertical-align: middle;
    width: 30px;
    display: inline-block;
  }

  .title-container {
     
    position: relative;

    .title {
     
      font-size: 26px;
      color: $light_gray;
      margin: 0px auto 40px auto;
      text-align: center;
      font-weight: bold;
    }
  }

  .show-pwd {
     
    position: absolute;
    right: 10px;
    top: 7px;
    font-size: 16px;
    color: $dark_gray;
    cursor: pointer;
    user-select: none;
  }
}
</style>

接口测试

可以使用postman,专门用来测试接口的
通过表单处理传入的数据

封装请求方法
services下新增user.ts

/**
 * 用户相关请求模块
 */

import request from '@/utils/request'
import qs from 'qs'

interface User {
     
  phone: string
  password: string
}

export const login = (data: User) => {
     
  return request({
     
    method: 'POST',
    url: '/front/user/login',
    // headers: { 'content-type': 'application/x-www-form-urlencoded' },

    // 如果 data 是普通对象,则 Content-Type 是 application/json
    // 如果 data 是 qs.stringify(data) 转换之后的数据:key=value&key=value,则 Content-Type 会被设置为 application/x-www-form-urlencoded
    // 如果 data 是 FormData 对象,则 Content-Type 是 multipart/form-data
    data: qs.stringify(data) // axios 默认发送的是 application/json 格式的数据
  })
}

export const getUserInfo = () => {
     
  return request({
     
    method: 'GET',
    url: '/front/user/getInfo'
  })
}

将用户状态存到vuex中

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
     
  // 容器的状态实现了数据共享,在组件里面访问方便,但是没有持久化的功能
  state: {
     
    user: JSON.parse(window.localStorage.getItem('user') || 'null')
    // user: null // 当前登录用户状态
  },
  mutations: {
     
    // 修改容器数据必须使用 mutation 函数
    setUser (state, payload) {
     
      state.user = JSON.parse(payload)

      // 为了防止页面刷新数据丢失,我们需要把 user 数据持久化
      // 注意:本地存储只能存字符串
      window.localStorage.setItem('user', payload)
    }
  },
  actions: {
     
  },
  modules: {
     
  }
})

使用路由拦截器控制页面的访问权限,修改route.ts
或者·可以搭配使用路由元信息,定义meta字段

import Vue from 'vue'
import VueRouter, {
      RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'
import store from '@/store'

Vue.use(VueRouter)

// 路由配置规则
const routes: Array<RouteConfig> = [
  {
     
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
  },
  {
     
    path: '/',
    component: Layout,
    meta: {
     
      requiresAuth: true
    },
    children: [
      {
     
        path: '', // 默认子路由
        name: 'home',
        component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
      },
      {
     
        path: '/role',
        name: 'role',
        component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue')
      },
      {
     
        path: '/menu',
        name: 'menu',
        component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
      },
      {
     
        path: '/resource',
        name: 'resource',
        component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
      },
      {
     
        path: '/course',
        name: 'course',
        component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue')
      },
      {
     
        path: '/user',
        name: 'user',
        component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
      },
      {
     
        path: '/advert',
        name: 'advert',
        component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
      },
      {
     
        path: '/advert-space',
        name: 'advert-space',
        component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
      }
    ]
  },
  {
     
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
  }
]

const router = new VueRouter({
     
  routes
})

// 全局前置守卫:任何页面的访问都要经过这里
// to:要去哪里的路由信息
// from:从哪里来的路由信息
// next:通行的标志
router.beforeEach((to, from, next) => {
     
  // to.matched 是一个数组(匹配到是路由记录)
  if (to.matched.some(record => record.meta.requiresAuth)) {
     
    if (!store.state.user) {
     
      // 跳转到登录页面
      next({
     
        name: 'login',
        query: {
      // 通过 url 传递查询字符串参数
          redirect: to.fullPath // 把登录成功需要返回的页面告诉登录页面
        }
      })
    } else {
     
      next() // 允许通过
    }
  } else {
     
    next() // 允许通过
  }

  // // 路由守卫中一定要调用 next,否则页码无法展示
  // next()
  // if (to.path !== '/login') {
     
  //   // 校验登录状态
  // }
})

export default router

用户登录和身份认证

处理token过期,token的有效时间是接口负责,一般是根据项目需要,如果设置时间比较短,体验并不好,可以根据设置refresh_token获取新的token,主要是处理安全性的问题

解决方法

  1. 在请求前拦截每一个请求,判断是否过期,若过期,先将请求挂起,获取新的token在继续请求
  2. 不在请求前拦截,拦截返回的数据,接口返回过期后,刷新token,在进行一次重试

使用请求拦截器设置token

import axios from 'axios'
import store from '@/store'
import {
      Message } from 'element-ui'
import router from '@/router'
import qs from 'qs'

const request = axios.create({
     
  // 配置选项
  // baseURL,
  // timeout
})

function redirectLogin () {
     
  router.push({
     
    name: 'login',
    query: {
     
      redirect: router.currentRoute.fullPath
    }
  })
}

function refreshToken () {
     
  return axios.create()({
     
    method: 'POST',
    url: '/front/user/refresh_token',
    data: qs.stringify({
     
      // refresh_token 只能使用1次
      refreshtoken: store.state.user.refresh_token
    })
  })
}

// 请求拦截器
request.interceptors.request.use(function (config) {
     
  // 我们就在这里通过改写 config 配置信息来实现业务功能的统一处理
  const {
      user } = store.state
  if (user && user.access_token) {
     
    config.headers.Authorization = user.access_token
  }

  // 注意:这里一定要返回 config,否则请求就发不出去了
  return config
}, function (error) {
     
  // Do something with request error
  return Promise.reject(error)
})

// 响应拦截器
let isRfreshing = false // 控制刷新 token 的状态
let requests: any[] = [] // 存储刷新 token 期间过来的 401 请求
request.interceptors.response.use(function (response) {
      // 状态码为 2xx 都会进入这里
  // console.log('请求响应成功了 => ', response)
  // 如果是自定义错误状态码,错误处理就写到这里
  return response
}, async function (error) {
      // 超出 2xx 状态码都都执行这里
  // console.log('请求响应失败了 => ', error)
  // 如果是使用的 HTTP 状态码,错误处理就写到这里
  // console.dir(error)
  if (error.response) {
      // 请求发出去收到响应了,但是状态码超出了 2xx 范围
    const {
      status } = error.response
    if (status === 400) {
     
      Message.error('请求参数错误')
    } else if (status === 401) {
     
      // token 无效(没有提供 token、token 是无效的、token 过期了)
      // 如果有 refresh_token 则尝试使用 refresh_token 获取新的 access_token
      if (!store.state.user) {
     
        redirectLogin()
        return Promise.reject(error)
      }

      // 刷新 token
      if (!isRfreshing) {
     
        isRfreshing = true // 开启刷新状态
        // 尝试刷新获取新的 token
        return refreshToken().then(res => {
     
          if (!res.data.success) {
     
            throw new Error('刷新 Token 失败')
          }

          // 刷新 token 成功了
          store.commit('setUser', res.data.content)
          // 把 requests 队列中的请求重新发出去
          requests.forEach(cb => cb())
          // 重置 requests 数组
          requests = []
          return request(error.config)
        }).catch(err => {
     
          console.log(err)
          store.commit('setUser', null)
          redirectLogin()
          return Promise.reject(error)
        }).finally(() => {
     
          isRfreshing = false // 重置刷新状态
        })
      }

      // 刷新状态下,把请求挂起放到 requests 数组中
      return new Promise(resolve => {
     
        requests.push(() => {
     
          resolve(request(error.config))
        })
      })
    } else if (status === 403) {
     
      Message.error('没有权限,请联系管理员')
    } else if (status === 404) {
     
      Message.error('请求资源不存在')
    } else if (status >= 500) {
     
      Message.error('服务端错误,请联系管理员')
    }
  } else if (error.request) {
      // 请求发出去没有收到响应
    Message.error('请求超时,请刷新重试')
  } else {
      // 在设置请求时发生了一些事情,触发了一个错误
    Message.error(`请求失败:${
       error.message}`)
  }

  // 把请求失败的错误对象继续抛出,扔给上一个调用者
  return Promise.reject(error)
})

export default request

显示用户头像和退出,修改app-header.vue组件

<template>
  <div class="header">
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>活动管理</el-breadcrumb-item>
      <el-breadcrumb-item>活动列表</el-breadcrumb-item>
      <el-breadcrumb-item>活动详情</el-breadcrumb-item>
    </el-breadcrumb>
    <el-dropdown>
      <span class="el-dropdown-link">
        <el-avatar
          shape="square"
          :size="40"
          :src="userInfo.portrait || require('@/assets/default-avatar.png')"
        ></el-avatar>
        <i class="el-icon-arrow-down el-icon--right"></i>
      </span>
      <el-dropdown-menu slot="dropdown">
        <el-dropdown-item>{
     {
      userInfo.userName }}</el-dropdown-item>
        <el-dropdown-item
          divided
          @click.native="handleLogout"
        >退出</el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import {
      getUserInfo } from '@/services/user'

export default Vue.extend({
     
  name: 'AppHeader',
  data () {
     
    return {
     
      userInfo: {
     } // 当前登录用户信息
    }
  },
  created () {
     
    this.loadUserInfo()
  },
  methods: {
     
    async loadUserInfo () {
     
      const {
      data } = await getUserInfo()
      this.userInfo = data.content
      console.log('loadUserInfo')
    },

    handleLogout () {
     
      this.$confirm('确认退出吗?', '退出提示', {
     
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
      // 确认执行这里
        // 清除登录状态
        this.$store.commit('setUser', null)

        // 跳转到登录页面
        this.$router.push({
     
          name: 'login'
        })

        this.$message({
     
          type: 'success',
          message: '退出成功!'
        })
      }).catch(() => {
      // 取消执行这里
        this.$message({
     
          type: 'info',
          message: '已取消退出'
        })
      })
    }
  }
})
</script>

<style lang="scss" scoped>
.header {
     
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  .el-dropdown-link {
     
    display: flex;
    align-items: center;
  }
}
</style>

修改后的login/index.vue

<template>
  <div class="login">
    <!--
      1. :model="ruleForm"
      2. :rules="rules"
      3. ref="ruleForm"
      4. el-form-item 绑定 prop 属性
     -->
    <el-form
      class="login-form"
      label-position="top"
      ref="form"
      :model="form"
      :rules="rules"
      label-width="80px"
    >
      <el-form-item label="手机号" prop="phone">
        <el-input v-model="form.phone"></el-input>
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input type="password" v-model="form.password"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button
          class="login-btn"
          type="primary"
          :loading="isLoginLoading"
          @click="onSubmit"
        >登录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import {
      Form } from 'element-ui'
import {
      login } from '@/services/user'

export default Vue.extend({
     
  name: 'LoginIndex',
  data () {
     
    return {
     
      form: {
     
        phone: '18201288771',
        password: '111111'
      },
      rules: {
     
        phone: [
          {
      required: true, message: '请输入手机号', trigger: 'blur' },
          {
      pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' }
        ],
        password: [
          {
      required: true, message: '请输入密码', trigger: 'blur' },
          {
      min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' }
        ]
      },
      isLoginLoading: false
    }
  },

  methods: {
     
    async onSubmit () {
     
      try {
     
        // 1. 表单验证
        await (this.$refs.form as Form).validate()

        // 登录按钮 loading
        this.isLoginLoading = true

        // 2. 验证通过 -> 提交表单
        const {
      data } = await login(this.form)
        // const { data } = await request({
     
        //   method: 'POST',
        //   url: '/front/user/login',
        //   headers: { 'content-type': 'application/x-www-form-urlencoded' },
        //   data: qs.stringify(this.form) // axios 默认发送的是 application/json 格式的数据
        // })

        // 3. 处理请求结果
        //    失败:给出提示
        if (data.state !== 1) {
     
          this.$message.error(data.message)
        } else {
     
          // 1. 登录成功,记录登录状态,状态需要能够全局访问(放到 Vuex 容器中)
          this.$store.commit('setUser', data.content)
          // 2. 然后在访问需要登录的页面的时候判断有没有登录状态(路由拦截器)
          //    成功:跳转回原来页面或首页
          this.$router.push(this.$route.query.redirect as string || '/')
          // this.$router.push({
     
          //   name: 'home'
          // })
          this.$message.success('登录成功')
        }
      } catch (err) {
     
        console.log('登录失败', err)
      }

      // 结束登录按钮的 loading
      this.isLoginLoading = false
    }
  }
})
</script>

<style lang="scss" scoped>
.login {
     
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  .login-form {
     
    width: 300px;
    background: #fff;
    padding: 20px;
    border-radius: 5px;
  }
  .login-btn {
     
    width: 100%;
  }
}
</style>

用户权限

不同的角色有不同的权限,可以在从角色入手,角色的菜单和权限
在views下的menu进行组件创建及修改

<template>
  <div class="menu">
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <el-button @click="$router.push({ name: 'menu-create' })">添加菜单</el-button>
      </div>
      <el-table
        :data="menus"
        style="width: 100%">
        <el-table-column
          label="编号"
          min-width="150"
          type="index">
        </el-table-column>
        <el-table-column
          prop="name"
          label="菜单名称"
          min-width="150">
        </el-table-column>
        <el-table-column
          prop="level"
          label="菜单级数"
          min-width="150">
        </el-table-column>
        <el-table-column
          prop="icon"
          label="前端图标"
          min-width="150">
        </el-table-column>
        <el-table-column
          prop="orderNum"
          label="排序"
          min-width="150">
        </el-table-column>
        <el-table-column
          label="操作"
          min-width="150">
          <template slot-scope="scope">
            <el-button
              size="mini"
              @click="handleEdit(scope.row)">编辑</el-button>
            <el-button
              size="mini"
              type="danger"
              @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import {
      getAllMenus, deleteMenu } from '@/services/menu'

export default Vue.extend({
     
  name: 'MenuIndex',
  data () {
     
    return {
     
      menus: [] // 菜单列表
    }
  },

  created () {
     
    this.loadAllMenus()
  },

  methods: {
     
    async loadAllMenus () {
     
      const {
      data } = await getAllMenus()
      if (data.code === '000000') {
     
        this.menus = data.data
      }
    },

    handleEdit (item: any) {
     
      this.$router.push({
     
        name: 'menu-edit',
        params: {
     
          id: item.id
        }
      })
    },

    handleDelete (item: any) {
     
      this.$confirm('确认删除吗?', '删除提示', {
     })
        .then(async () => {
      // 确认执行这里
          // 请求删除操作
          const {
      data } = await deleteMenu(item.id)
          if (data.code === '000000') {
     
            this.$message.success('删除成功')
            this.loadAllMenus() // 更新数据列表
          }
        })
        .catch(err => {
      // 取消执行这里
          console.log(err)
          this.$message.info('已取消删除')
        })
    }
  }
})
</script>

<style lang="scss" scoped></style>

servicers下新增menu.ts

/**
 * 菜单相关请求模块
 */

import request from '@/utils/request'

export const createOrUpdateMenu = (data: any) => {
     
  return request({
     
    method: 'POST',
    url: '/boss/menu/saveOrUpdate',
    data
  })
}

export const getEditMenuInfo = (id: string | number = -1) => {
     
  return request({
     
    method: 'GET',
    url: '/boss/menu/getEditMenuInfo',
    params: {
     
      id
    }
  })
}

export const getAllMenus = () => {
     
  return request({
     
    method: 'GET',
    url: '/boss/menu/getAll'
  })
}

export const deleteMenu = (id: number) => {
     
  return request({
     
    method: 'DELETE',
    url: `/boss/menu/${
       id}`
  })
}

export const getMenuNodeList = () => {
     
  return request({
     
    method: 'GET',
    url: '/boss/menu/getMenuNodeList'
  })
}

export const allocateRoleMenus = (data: any) => {
     
  return request({
     
    method: 'POST',
    url: '/boss/menu/allocateRoleMenus',
    data
  })
}

export const getRoleMenus = (roleId: string | number) => {
     
  return request({
     
    method: 'GET',
    url: '/boss/menu/getRoleMenus',
    params: {
      // axios 会把 params 转换为 key=value&key=value 的数据格式放到 url 后面(以?分割)
      roleId
    }
  })
}

菜单管理页面
create.vue

<template>
  <div class="menu-create">
    <create-or-edit />
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import CreateOrEdit from './components/CreateOrEdit.vue'

export default Vue.extend({
     
  name: 'MenuCreate',
  components: {
     
    CreateOrEdit
  },
  data () {
     
    return {
     }
  }
})
</script>

<style lang="scss" scoped></style>

封装公共组件菜单,menu下新增components/CreateOrEdit.vue

<template>
  <div class="menu-create">
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <span>{
     {
      isEdit ? '编辑菜单' : '添加菜单' }}</span>
      </div>
      <el-form ref="form" :model="form" label-width="80px">
        <el-form-item label="菜单名称">
          <el-input v-model="form.name"></el-input>
        </el-form-item>
        <el-form-item label="菜单路径">
          <el-input v-model="form.href"></el-input>
        </el-form-item>
        <el-form-item label="上级菜单">
          <el-select v-model="form.parentId" placeholder="请选择上级菜单">
            <el-option :value="-1" label="无上级菜单"></el-option>
            <el-option
              :label="item.name"
              :value="item.id"
              v-for="item in parentMenuList"
              :key="item.id"
            ></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="描述">
          <el-input v-model="form.description"></el-input>
        </el-form-item>
        <el-form-item label="前端图标">
          <el-input v-model="form.icon"></el-input>
        </el-form-item>
        <el-form-item label="是否显示">
          <el-radio-group v-model="form.shown">
            <el-radio :label="true"></el-radio>
            <el-radio :label="false"></el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="排序">
          <el-input-number v-model="form.orderNum" :min="1" label="描述文字"></el-input-number>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit">提交</el-button>
          <el-button
            v-if="!isEdit"
          >重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import {
      createOrUpdateMenu, getEditMenuInfo } from '@/services/menu'

export default Vue.extend({
     
  name: 'MenuCreateOrEdit',
  props: {
     
    isEdit: {
     
      type: Boolean,
      default: false
    }
  },
  data () {
     
    return {
     
      form: {
     
        parentId: -1, // -1 表示没有上级菜单
        name: '123',
        href: '123',
        icon: '123',
        orderNum: 0,
        description: '123',
        shown: false
      },
      parentMenuList: [] // 父级菜单列表
    }
  },

  created () {
     
    this.loadMenuInfo()
  },

  methods: {
     
    async loadMenuInfo () {
     
      const {
      data } = await getEditMenuInfo(this.$route.params.id || -1)
      if (data.data.menuInfo) {
     
        this.form = data.data.menuInfo
      }
      if (data.code === '000000') {
     
        this.parentMenuList = data.data.parentMenuList
      }
    },

    async onSubmit () {
     
      // 1. 表单验证
      // 2. 验证通过,提交表单
      const {
      data } = await createOrUpdateMenu(this.form)
      if (data.code === '000000') {
     
        this.$message.success('提交成功')
        this.$router.back()
      }
    }
  }
})
</script>

<style lang="scss" scoped></style>

资源管理页面

views下resource的index.vue

<template>
  <div class="resource">
    <resource-list />
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import ResourceList from './components/List.vue'

export default Vue.extend({
     
  name: 'ResourceIndex',
  components: {
     
    ResourceList
  }
})
</script>

<style lang="scss" scoped></style>

ResourceList.vue

<template>
  <div class="resource-list">
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <el-form ref="form" :model="form" label-width="80px">
          <el-form-item prop="name" label="资源名称">
            <el-input v-model="form.name"></el-input>
          </el-form-item>
          <el-form-item prop="url" label="资源路径">
            <el-input v-model="form.url"></el-input>
          </el-form-item>
          <el-form-item prop="categoryId" label="资源分类">
            <el-select
              v-model="form.categoryId"
              placeholder="请选择资源分类"
              clearable
            >
              <el-option
                :label="item.name"
                :value="item.id"
                v-for="item in resourceCategories"
                :key="item.id"
              ></el-option>
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button
              type="primary"
              @click="onSubmit"
              :disabled="isLoading"
            >查询搜索</el-button>
            <el-button
              @click="onReset"
              :disabled="isLoading"
            >重置</el-button>
          </el-form-item>
        </el-form>
      </div>
      <el-table
        :data="resources"
        style="width: 100%; margin-bottom: 20px"
        v-loading="isLoading"
      >
        <el-table-column
          type="index"
          label="编号"
          width="100">
        </el-table-column>
        <el-table-column
          prop="name"
          label="资源名称"
          width="180">
        </el-table-column>
        <el-table-column
          prop="url"
          width="180"
          label="资源路径">
        </el-table-column>
        <el-table-column
          prop="description"
          width="180"
          label="描述">
        </el-table-column>
        <el-table-column
          width="180"
          prop="createdTime"
          label="添加时间">
        </el-table-column>
        <el-table-column
          width="180"
          label="操作">
          <template slot-scope="scope">
            <el-button
              size="mini"
              @click="handleEdit(scope.row)">编辑</el-button>
            <el-button
              size="mini"
              type="danger"
              @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>

      <!--
        total 总记录数
        page-size 每页大小
        分页组件会自动根据 total 和 page-size 计算出一共分多少页
       -->
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :disabled="isLoading"
        :current-page.sync="form.current"
        :page-sizes="[5, 10, 20]"
        :page-size="form.size"
        layout="total, sizes, prev, pager, next, jumper"
        :total="totalCount">
      </el-pagination>
    </el-card>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import {
      getResourcePages } from '@/services/resource'
import {
      getResourceCategories } from '@/services/resource-category'
import {
      Form } from 'element-ui'

export default Vue.extend({
     
  name: 'ResourceList',
  data () {
     
    return {
     
      resources: [], // 资源列表
      form: {
     
        name: '',
        url: '',
        current: 1, // 默认查询第1页数据
        size: 5, // 每页大小
        categoryId: null // 资源分类
      },
      totalCount: 0,
      resourceCategories: [], // 资源分类列表
      isLoading: true // 加载状态
    }
  },

  created () {
     
    this.loadResources()
    this.loadResourceCategories()
  },

  methods: {
     
    async loadResourceCategories () {
     
      const {
      data } = await getResourceCategories()
      this.resourceCategories = data.data
    },

    async loadResources () {
     
      this.isLoading = true // 展示加载中状态
      const {
      data } = await getResourcePages(this.form)
      this.resources = data.data.records
      this.totalCount = data.data.total
      this.isLoading = false // 关闭加载中状态
    },

    onSubmit () {
     
      this.form.current = 1 // 筛选查询从第 1 页开始
      this.loadResources()
    },

    handleEdit (item: any) {
     
      console.log('handleEdit', item)
    },

    handleDelete (item: any) {
     
      console.log('handleDelete', item)
    },

    handleSizeChange (val: number) {
     
      this.form.size = val
      this.form.current = 1 // 每页大小改变重新查询第1页数据
      this.loadResources()
    },

    handleCurrentChange (val: number) {
     
      // 请求获取对应页码的数据
      this.form.current = val // 修改要查询的页码
      this.loadResources()
    },

    onReset () {
     
      (this.$refs.form as Form).resetFields()
      this.form.current = 1 // 重置回到第1页
      this.loadResources()
    }
  }
})
</script>

<style lang="scss" scoped></style>

角色权限管理

Role部分

index.vue

<template>
  <div class="role">
    <role-list />
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import RoleList from './components/List.vue'

export default Vue.extend({
     
  name: 'RoleIndex',
  components: {
     
    RoleList
  }
})
</script>

<style lang="scss" scoped></style>

分配组件

<template>
  <div class="alloc-menu">
    <el-card>
      <div slot="header">
        <span>分配菜单</span>
      </div>
      <el-tree
        ref="menu-tree"
        :data="menus"
        node-key="id"
        :props="defaultProps"
        :default-checked-keys="checkedKeys"
        show-checkbox
        default-expand-all
      ></el-tree>
      <div style="text-align: center">
        <el-button @click="resetChecked">清空</el-button>
        <el-button type="primary" @click="onSave">保存</el-button>
      </div>
    </el-card>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import {
     
  getMenuNodeList,
  allocateRoleMenus,
  getRoleMenus
} from '@/services/menu'
import {
      Tree } from 'element-ui'
import {
      getRoleById } from '@/services/role'

export default Vue.extend({
     
  name: 'AllocMenu',
  props: {
     
    roleId: {
     
      type: [String, Number],
      required: true
    }
  },
  data () {
     
    return {
     
      menus: [],
      defaultProps: {
     
        children: 'subMenuList',
        label: 'name'
      },
      checkedKeys: []
    }
  },

  async created () {
     
    await this.loadMenus()
    this.loadRoleMenus()
  },

  methods: {
     
    async loadRoleMenus () {
     
      const {
      data } = await getRoleMenus(this.roleId)
      this.getCheckedKeys(data.data)
    },

    getCheckedKeys (menus: any) {
     
      menus.forEach((menu: any) => {
     
        if (menu.selected) {
     
          // this.checkedKeys.push(menu.id as never)
          this.checkedKeys = [...this.checkedKeys, menu.id] as any
        }
        if (menu.subMenuList) {
     
          this.getCheckedKeys(menu.subMenuList)
        }
      })
    },

    async loadMenus () {
     
      const {
      data } = await getMenuNodeList()
      this.menus = data.data
    },

    async onSave () {
     
      const menuIdList = (this.$refs['menu-tree'] as Tree).getCheckedKeys()
      // 拿到选中节点的数据 id 列表
      // 请求提交保存
      await allocateRoleMenus({
     
        roleId: this.roleId,
        menuIdList
      })
      this.$message.success('操作成功')
      this.$router.back()
    },

    resetChecked () {
     
      (this.$refs['menu-tree'] as Tree).setCheckedKeys([])
    }
  }
})
</script>

<style lang="scss" scoped></style>

分配资源

<template>
  <div class="alloc-resource">
    <el-card>
      <div slot="header">
        <span>分配资源</span>
      </div>
      <el-tree
        ref="tree"
        :data="resources"
        node-key="id"
        :props="defaultProps"
        show-checkbox
        :default-checked-keys="defaultCheckedKeys"
        :default-expanded-keys="defaultCheckedKeys"
      ></el-tree>
      <div style="text-align: center">
        <el-button @click="resetChecked">清空</el-button>
        <el-button type="primary" @click="onSave">保存</el-button>
      </div>
    </el-card>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import {
     
  getAllResources,
  allocateRoleResources,
  getRoleResources
} from '@/services/resource'
import {
      getResourceCategories } from '@/services/resource-category'
import {
      Tree } from 'element-ui'

export default Vue.extend({
     
  name: 'AllocResource',
  props: {
     
    roleId: {
     
      type: [String, Number],
      required: true
    }
  },
  data () {
     
    return {
     
      resources: [],
      defaultProps: {
     
        children: 'children',
        label: 'name'
      },
      defaultCheckedKeys: []
    }
  },
  created () {
     
    this.loadResources()
    this.loadRoleResources()
  },
  methods: {
     
    async loadResources () {
     
      const ret = await Promise.all([getAllResources(), getResourceCategories()])
      const resources = ret[0].data.data
      const resourceCategories = ret[1].data.data

      resources.forEach((r: any) => {
     
        const category = resourceCategories.find((c: any) => c.id === r.categoryId)
        if (category) {
     
          category.children = category.children || []
          category.children.push(r)
        }
      })
      // 修改顶层分类 ID:因为分类 ID 和资源 ID 冲突
      resourceCategories.forEach((item: any) => {
     
        item.id = Math.random()
      })

      this.resources = resourceCategories
    },

    async loadRoleResources () {
     
      const {
      data } = await getRoleResources(this.roleId)
      this.getCheckedResources(data.data)
    },

    getCheckedResources (resources: any) {
     
      resources.forEach((r: any) => {
     
        r.resourceList && r.resourceList.forEach((c: any) => {
     
          if (c.selected) {
     
            this.defaultCheckedKeys = [...this.defaultCheckedKeys, c.id] as any
          }
        })
      })
    },

    async onSave () {
     
      const checkedNodes = (this.$refs.tree as Tree).getCheckedNodes()
      const resourceIdList: number[] = []
      checkedNodes.forEach(item => {
     
        if (!item.children) {
     
          resourceIdList.push(item.id)
        }
      })
      await allocateRoleResources({
     
        roleId: this.roleId,
        resourceIdList
      })
      this.$message.success('保存成功')
      this.$router.back()
    },

    resetChecked () {
     
      (this.$refs.tree as Tree).setCheckedKeys([])
    }
  }
})
</script>

<style lang="scss" scoped></style>

你可能感兴趣的:(vue,代码管理,javascript,vue.js)