3 Star 11 Fork 46

老汤 / vue-cms

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
Apache-2.0

创建项目

使用命令创建一个项目。

vue create 项目名
比如 
vue create vue-cms

如果前一天,已经存了预设,直接使用那个预设。

没有存预设,按照下面的步骤安装:

  1. Manually select features
    • (*) Choose Vue version
    • (*) Babel
    • ( ) TypeScript
    • ( ) Progressive Web App (PWA) Support
    • (*) Router
    • (*) Vuex
    • (*) CSS Pre-processors
    • (*) Linter / Formatter
    • ( ) Unit Testing
    • ( ) E2E Testing
  2. Choose a version of Vue.js that you want to start the project with (Use arrow keys)
    • 2.x
    • 3.x
  3. Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n)
    • n
  4. Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)
    • Sass/SCSS (with dart-sass)
    • Sass/SCSS (with node-sass)
    • Less
    • Stylus
  5. Pick a linter / formatter config: (Use arrow keys)
    • ESLint + Airbnb config
    • ESLint + Standard config
    • ESLint + Prettier
  6. Pick additional lint features: (Press to select, to toggle all, to invert selection)
    • (*) Lint on save
    • ( ) Lint and fix on commit
  7. Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
    • In dedicated config files
    • In package.json
  8. Save this as a preset for future projects? (y/N)
    • N

推送到远程仓库

  • 创建远程仓库

  • (本地已经有一次提交了)

  • 添加远程仓库地址

    git remote add origin 仓库地址
  • 首次推送

    git push -u origin master

ESLint配置

找到项目中的 .eslintrc.js ,在 rules 中加入如下的配置项

// 修正函数名和小括号之间的空格问题
// 参考文档:https://eslint.bootcss.com/docs/rules/space-before-function-paren
'space-before-function-paren': [
    'error',
    {
    anonymous: 'always', // 匿名函数小括号前,需要空格。比如 setTimeout(function () {})
    named: 'never', // 有名字的方法,不需要空格。比如 abc() {}
    asyncArrow: 'always' // 箭头函数,需要空格
    }
]

清空初始的项目

App.vue,只留下基础的模板即可

<template>
  <div id="app">大事件项目</div>
</template>

删除views里面的两个组件(Home.vue 和 About.vue )

删除 components 里面的 HelloWorld.vue 组件

重置路由(router/index.js)

import Vue from 'vue'
import VueRouter from 'vue-router'
// 去掉加载 Home 这行

Vue.use(VueRouter)

const routes = []  // 清空这个数组

const router = new VueRouter({
  routes
})

export default router

项目的目录结构:

node_modules       --- 我们下载的第三方包(不能动)
public             --- 存放项目的首页(不能动)
src                --- 我们编码的位置
  - assets         --- 存储项目的资源(图片等等)
  - components     --- 存储组件的(不受路由控制,一般放一下通用的、封装好的组件)
  - views          --- 存储组件的(受路由控制的组件,放到这里)
  - router         --- 配置文件的文件
  - store          --- vuex的配置
  - App.vue        --- 项目的根组件(其他所有组件都是它的孩子)
  - main.js        --- 它是打包的入口文件(main.js是最先执行的文件)
.eslintrc.js       --- ESLint的配置文件
.gitignore         --- git的忽略文件(已经写好,不需要处理了)
babel.config.js    --- babel是对JS进行降价处理的(目前的项目不需要处理的)
package-lock.json  --- 锁定第三方包的版本的文件(不需要动)
package.json       --- 项目的配置文件

将assets资料包放到项目中

将 发的资料里面的 asset 文件夹里面的全部内容,复制到项目的 assets 文件夹中。

image-20220123095507838

准备登录注册组件并配置好路由

  • 组件应该放到哪里?

    • components文件夹,不受路由控制的
    • views文件夹,存放的组件是受路由控制的
  • 在views里面创建 Login.vue (登录组件) 和 Reg.vue (注册组件)

  • 配置路由(router/index.js)

    import Login from '@/views/Login.vue'
    
    Vue.use(VueRouter)  // 这行是原有的
    
    const routes = [
      // 直接写到这个最大的数组中的路由,对应的组件,应该显示在App.vue的占位符位置
      // 比如下面的 登录组件,应该显示在App.vue的占位符位置
      // { path: '路由地址', component: '组件' }
      { path: '/login', component: Login }, // 使用组件应该先导入
      { path: '/reg', component: () => import('@/views/Reg.vue') }
    ]
  • App.vue 设置 <router-view></router-view> 占位符

    <template>
      <router-view></router-view>
    </template>
    

    一定要去掉原有的 <div id="app"></div>

  • 测试

安装element-ui

参考:https://element.eleme.cn/#/zh-CN/component/quickstart

安装:npm i element-ui

在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)
});

测试:

复制一个按钮到 Login.vue 中

<template>
  <div>
    <el-button type="primary">主要按钮</el-button>
  </div>
</template>

打开页面(http://localhost:8080/#/login),看一下是否能够正常显示按钮。

开发Login页面

设置背景图片

  • public/index.html ,加入样式,设置html,body高100%

    <style>
      html,
      body {
        margin: 0;
        padding: 0;
        height: 100%;
      }
    </style>
  • Login.vue 里面,设置最大的盒子的样式

    <template>
      <div class="login-container"></div>
    </template>
    
    <style lang="less">
    .login-container {
      height: 100%;
      background: url(../assets/images/login_bg.jpg) no-repeat;
      background-size: cover;
    }
    </style>
    

登录盒子和登录标题

  • 将element-ui里面的卡片,复制一个,放到 <div class="login-container"></div> 里面

    <template>
      <div class="login-container">
        <!-- 下面使用element的卡片,做中间的那个盒子 -->
        <el-card shadow="hover">
          这是一个卡片
        </el-card>
      </div>
    </template>
  • 设置卡片的样式(居中、高、宽)

    .el-card {
      height: 355px; // 高度随便加一个,最后完成后,去掉,让高度自动撑开
      width: 400px;
      position: absolute;
      left: 50%; // 针对父级元素的50%
      top: 50%;
      transform: translate(-50%, -50%); // 向回平移自身的50%
    }
  • <el-card> 里面加入登录标题 <div class="login-title"></div>

    <template>
      <div class="login-container">
        <!-- 下面使用element的卡片,做中间的那个盒子 -->
        <el-card shadow="hover">
          <!-- 登录标题 -->
          <div class="login-title"></div>
        </el-card>
      </div>
    </template>
  • 设置标题样式

    .login-title {
      height: 60px;
      background: url(../assets/images/login_title.png) 50% no-repeat;
    }

做表单

  • element-ui的表单参考:https://element.eleme.cn/#/zh-CN/component/form

  • 复制 <el-form></el-form> 到 标题div后面

  • 复制 三个表单项 (<el-form-item>......</el-form-item>) 到 <el-form></el-form>

    <!-- 登录标题 -->
    <div class="login-title"></div>
    
    <!-- 表单盒子 -->
    <el-form ref="form">
      <!-- 用户名 -->
      <el-form-item>
        <el-input></el-input>
      </el-form-item>
      <!-- 密码 -->
      <el-form-item>
        <el-input></el-input>
      </el-form-item>
      <!-- 按钮 -->
      <el-form-item>
        <el-input></el-input>
      </el-form-item>
    </el-form>

复制之后,去掉和数据相关的部分,否则报错

  • 去掉 <el-form>:model 属性
  • 去掉 <el-input>v-model 属性

去掉 label 和 label-width(输入框前面的文字去掉)

处理按钮和超链接

  • 将最后一个输入框换成按钮,设置宽度 100%

    <!-- 按钮 -->
    <el-form-item>
      <el-button type="primary">登录</el-button>
    </el-form-item>
  • 表单后面加入超链接,参考:https://element.eleme.cn/#/zh-CN/component/link

    <!-- 表单后面,放一个超链接 -->
    <el-link :underline="false">还没有账号,去注册</el-link>

将 .el-card 的高度去掉,让内容自动撑开

使用图标美化输入框

  • 参考文档:https://element.eleme.cn/#/zh-CN/component/input

  • 可以通过 prefix-iconsuffix-icon 属性在 input 组件首部和尾部增加显示图标

    <!-- 用户名 -->
    <el-form-item>
      <el-input placeholder="请输入用户名" prefix-icon="el-icon-user"></el-input>
    </el-form-item>
    <!-- 密码 -->
    <el-form-item>
      <el-input placeholder="请输入密码" prefix-icon="el-icon-lock"></el-input>
    </el-form-item>
  • 图标的文档:https://element.eleme.cn/#/zh-CN/component/icon

密码框切换显示隐藏

参考文档:https://element.eleme.cn/#/zh-CN/component/input

  • 给密码框加 type="password" show-password
<!-- 密码 -->
<el-form-item>
  <el-input type="password" show-password placeholder="请输入密码" prefix-icon="el-icon-lock"></el-input>
</el-form-item>

开发Reg页面

  • 复制 登录页 (Login.vue)里面的全部代码,到 Reg.vue 里面
  • 多加一个密码框
  • 把按钮 和 超链接里面的文字修改一下即可
    • 把“登录”改为“注册”
    • 把“还没有账号,去注册”改为“已有账号,去登录”

自愿修改类名等等,把login改为reg

测试

切换登录和注册页面

现在,是手动修改地址栏,显示登录注册。

下面通过 编程式导航(@click="$router.push('hash地址')"),实现两个页面之间的跳转。

Reg.vue

<!-- 表单后面,放一个超链接 -->
<el-link :underline="false" @click="$router.push('/login')">已有账号,去登录</el-link>

<!-- <el-link :underline="false" href="/#/login">已有账号,去登录</el-link> -->
<!-- <router-link to="/login">已有账号,去登录</router-link> -->

Login.vue

<!-- 表单后面,放一个超链接 -->
<el-link :underline="false" @click="$router.push('/reg')">还没有账号,去注册</el-link>

<!-- <el-link :underline="false" href="/#/reg">还没有账号,去注册</el-link> -->
<!-- <router-link to="/reg">还没有账号,去注册</router-link> -->

注册页-做双向数据绑定

Reg.vue 的 JS中,设置一份数据

export default {
  data() {
    return {
      // 1. 做双向数据绑定(data中放数据,v-model和input建立双向绑定关系)
      userInfo: {
        username: '',
        password: '',
        repassword: ''
      }
    }
  }

  // 2. 表单验证(减少一下不必要的请求)
  // 3. 表单提交,完成注册功能
}

通过v-model 和三个输入进行双向绑定

<!-- 用户名 -->
<el-form-item>
  <el-input 
    v-model="userInfo.username"    <!-- 这里加入v-model -->
    placeholder="请输入用户名" 
    prefix-icon="el-icon-user"
  ></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
  <el-input
    v-model="userInfo.password"   <!-- 这里加入v-model -->
    type="password"
    show-password
    placeholder="请输入密码"
    prefix-icon="el-icon-lock"
  ></el-input>
</el-form-item>
<!-- 确认密码 -->
<el-form-item>
  <el-input
    v-model="userInfo.repassword"   <!-- 这里加入v-model -->
    type="password"
    show-password
    placeholder="请再次输入密码"
    prefix-icon="el-icon-lock"
  ></el-input>
</el-form-item>

注册页-做表单验证

参考文档:https://element.eleme.cn/#/zh-CN/component/form

三个对应关系。

  • <el-form> 中的 :model 和数据项对应; :rules 和验证规则对应

    <!-- :model和数据项对应 -->
    <!-- :rules和验证规则对应 -->
    <el-form 
      ref="form" 
      :model="userInfo" 
      :rules="rules"
    >
  • <el-form-item> 中的 prop 和验证规则中的数据项对应

    • <el-form-item prop="username">
    • <el-form-item prop="password">
    • <el-form-item prop="repassword">

JS中,定义数据项(已经定义了userInfo)

JS中,定义验证规则(rules)

data() {
  return {
    // 1. 做双向数据绑定(data中放数据,v-model和input建立双向绑定关系)
    userInfo: {
      username: '',
      password: '',
      repassword: ''
    },
    // 2. 表单验证(减少一下不必要的请求)
    rules: {
      username: [
        // required表示必填,message验证不通过时的提示,trigger,表示什么时候触发验证
        { required: true, message: '请输入用户名', trigger: 'blur' },
        // { min: 1, max: 10, message: '用户名长度1~10位', trigger: 'blur' }
        {
          pattern: /^[a-zA-Z0-9]{1,10}$/,
          message: '用户名必须是长度1-10位的字母数字',
          trigger: 'blur'
        }
      ],
      password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 6, max: 15, message: '密码长度6~15位', trigger: 'blur' }
      ],
      repassword: [
        { required: true, message: '请输入确认密码', trigger: 'blur' },
        { min: 6, max: 15, message: '确认密码长度6~15位', trigger: 'blur' }
      ]
    }
  }
}

图示对应关系:

image-20220123165157172

注册页-验证两次密码

参考文档:https://element.eleme.cn/#/zh-CN/component/form

  • data 函数中,return 之前,自定义函数验证确认密码。

    var validatePass2 = (rule, value, callback) => {
      // rule 没什么用。
      // value 表示使用这个规则的输入框。案例中,确认密码使用这个验证规则,所以value表示我们输入的确认密码值
      // callback 表示是否通过验证
    
      // 和密码进行比较
      if (value !== this.userInfo.password) { // 这里的this.userInfo.password表示我们的密码
        callback(new Error('两次输入密码不一致!')) // 验证不通过,传入错误对象
      } else {
        callback()  // 验证通过,直接callback()
      }
    }
  • 在确认密码验证时,加入 validator,值就是上面的自定义函数

    repassword: [
      { required: true, message: '请输入确认密码', trigger: 'blur' },
      { min: 6, max: 15, message: '确认密码长度6~15位', trigger: 'blur' },
      { validator: validatePass2, trigger: 'blur' }
    ]

注册页-表单提交前的完整验证

  • 找到提交按钮,注册单击事件

    <el-button type="primary" @click="reg">注册</el-button>
  • 补充 methods 方法(methods和data平级),处理单击事件

    • 按照element的文档,对表单进行完整的验证
    • 验证不通过,return
    • 验证通过,提交数据
    // 3. 表单提交,先进行完整的表单验证,验证通过再完成注册功能
    methods: {
      reg() {
        // this.$refs.form 找到表单
        // 调用 validate方法,进行表单完整的验证
        this.$refs.form.validate(valid => {
          // console.log(valid) // 布尔值,验证通过是true;验证不通过是false
          if (!valid) return
          console.log('验证通过,可以提交数据了')
        })
      }
    }

使用axios的步骤

image-20220123173908281

  • 为什么要封装一个 request.js
    • 通过创建 axios 实例,可以全局配置 axios
    • 可以通过创建多个axios的实例,来向不同的服务器发送请求(适合大项目)
  • 为什么需要 reg.js
    • 为了提高复用性
    • 比如项目中,很多组件都需要 商品列表的请求,统一封装起来,其他组件可以随意调用

完成注册

  • 下载安装 axios

    • npm i axios
  • 封装并导出 axios 的实例

    • 在src中,创建 utils 的文件夹,里面创建 request.js

      import axios from 'axios'
      
      // 创建axios的实例。 这个实例中,可以对axios进行全局的配置
      // 得到的 request 也是axios。只不过名字不能再叫axios,因为重名了
      const request = axios.create({
        baseURL: 'http://www.liulongbin.top:3008'
      })
      
      // const request2 = axios.create({
      //   baseURL: 'http://www.itcbc.com'
      // })
      
      export default request
  • 封装注册请求的方法

    • 在src中,创建 api 的文件夹,里面创建 reg.js

      // 这里导入 axios的实例对象
      import request from '@/utils/request'
      
      // 按需导出封装后的方法
      export const regAPI = data => {
        return request.post('/api/reg', data)
      }
      
  • Reg.vue 组件中,使用

    • 按需导入 regAPI 方法

      // 按需导入 API 方法
      import { regAPI } from '@/api/reg.js'
    • 当注册验证通过后,调用 regAPI 方法,完成注册功能

      • regAPI 方法返回的是 Promise 对象
      • 调用的时候,前面加上 await 关键字
      • await 最近的函数,加上 async
      // 3. 表单提交,先进行完整的表单验证,验证通过再完成注册功能
      methods: {
        reg() {
          // this.$refs.form 找到表单
          // 调用 validate方法,进行表单完整的验证
          // 下面的 form 是 <el-form> 的 ref 属性。作用是找到表单
          this.$refs.form.validate(async valid => {
            // console.log(valid) // 布尔值,验证通过是true;验证不通过是false
            if (!valid) return
            // console.log('验证通过,可以提交数据了')
            const { data: res } = await regAPI(this.userInfo)
            // console.log(res) // { code: 0, message: '提示消息' }
            if (res.code === 0) {
              alert(res.message)
            } else {
              alert(res.message)
            }
          })
        }
      }

注册成功要做的事情

  • 使用 element-ui 的提示
  • 清空输入框(重置表单)
  • 跳转到登录页
if (res.code === 0) {
  // alert(res.message)
  // this.$message({ type: 'success', message: res.message })
  this.$message.success(res.message)
  // 清空输入框(重置表单) // form 是表单的 ref="form" 值
  this.$refs.form.resetFields()
  // 跳转到登录页
  this.$router.push('/login')
} else {
  this.$message({ type: 'error', message: res.message })
}

快速完成登录

因为注册和登录绝大多数代码相同。

所以可以复制注册页的全部代码,到登录页。

修改不同的地方即可:

  • 登录两个输入框,所以删除确认密码框
  • 无需进行确认密码验证(删除确认密码验证)
  • 注册按钮改为登录
  • 修改 “还没有账号,去注册”超链接
  • 注册的接口和登录的接口不一样,所以在 reg.js 中,封装一个登录方法
  • 登录成功后,要做的事情不一样

登录功能

双向数据绑定

  • 设置data中的 userInfo 数据

    export default {
      data() {
        return {
          // 1. 设置数据,和表单input进行双向绑定
          userInfo: {
            username: '',
            password: ''
          }
        }
      }
    
      // 2. 表单验证
      // 3. 通过验证,表单提交,完成登录
    }
  • 使用 v-model="xxxx" 进行双向数据绑定

    <!-- 用户名 -->
    <el-form-item>
      <el-input
        v-model="userInfo.username"
        placeholder="请输入用户名"
        prefix-icon="el-icon-user"
      ></el-input>
    </el-form-item>
    <!-- 密码 -->
    <el-form-item>
      <el-input
        type="password"
        v-model="userInfo.password"
        show-password
        placeholder="请输入密码"
        prefix-icon="el-icon-lock"
      ></el-input>
    </el-form-item>

表单验证

  • 建议参考注册时的图示,找好三个对应关系即可
    • 数据项 userInfo<el-form>:model="userInfo" 对应
    • 验证规则 rules<el-form>:rules="rules" 对应
    • 每一个验证字段 和 <el-form-item>prop="xxx" 对应

具体代码,见 Login.vue

表单提交进行完整的验证

参考文档:https://element.eleme.cn/#/zh-CN/component/form 里面的表单验证

  • 给 按钮,添加单击事件

    <el-button type="primary" @click="login">登录</el-button>
  • methods中 login方法,进行完整的验证

    // 3. 通过验证,表单提交,完成登录
    methods: {
      login() {
        this.$refs.form.validate(valid => {
          if (!valid) return // 表单验证未通过,直接 return
          // 表单验证通过,发送请求,完成登录
          console.log('通过验证')
        })
      }
    }

完成登录功能

  • reg.js 中封装并导出 loginAPI

    // 按需导出 登录方法
    // export const loginAPI = data => {
    //   return request.post('/api/login', data)
    // }
    
    export const loginAPI = data => request.post('/api/login', data)
    
  • Login.vue 组件中,导入 loginAPI,调用它,发送ajax请求

    methods: {
      login() {
        this.$refs.form.validate(async valid => {
          if (!valid) return // 表单验证未通过,直接 return
          // 表单验证通过,发送请求,完成登录
          // console.log('通过验证')
          const { data: res } = await loginAPI(this.userInfo)
          if (res.code === 0) {
            this.$message.success(res.message)
          } else {
            this.$message.error(res.message)
          }
        })
      }
    }

登录成功的处理

  • 提示
  • 存储token(因为要在其他很多组件中使用token,所以需要把token存到vuex当中)
  • 跳转 (this.$router.push('/')

存储token

  • 在store文件夹,新建 user.js

    export default {
      namespaced: true,
      state() {
        return {
          token: ''
        }
      },
      mutations: {
        // 修改token,只能通过下面的方法修改
        updateToken(state, token) {
          state.token = token
        }
      },
      actions: {},
      getters: {}
    }
    
  • 在store文件夹的 index.js 中,导入 user.js 并注册成模块

    import Vue from 'vue'
    import Vuex from 'vuex'
    +import userModule from './user.js'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
    +  strict: true,
      modules: {
    +    // 模块名:导入进来的值
    +    user: userModule
      }
    })
    
  • Login.vue 组件中,登录成功后,调用 mutations中的方法,对token进行更新

    if (res.code === 0) {
      this.$message.success(res.message)
      // 存储token(调用mutations中的updateToken方法)
      // this.$store.commit('模块名/方法名', '其他参数')
      this.$store.commit('user/updateToken', res.token)
      // 跳转到后台首页
      this.$router.push('/')
    } else {
      this.$message.error(res.message)
    }
  • 测试

    • 打开登录页,正常登录
    • 登录后,使用 dev-tools 工具,查看vuex 中是否把token更新了

token的持久化存储

目前,token存储到vuex当中,即内存中。

页面只要一刷新,就会重置token为空。

所以需要 vuex-persistedstate 插件,该插件可以

  • 将vuex中的数据存到本地存储中
  • 也可以将本地存储中的数据恢复到vuex当中

插件的参考网站:https://www.npmjs.com/package/vuex-persistedstate

具体使用:

  • npm i vuex-persistedstate@3.2.1

  • store/index.js中,导入并注册为插件

    import Vue from 'vue'
    import Vuex from 'vuex'
    +import createPersistedState from 'vuex-persistedstate'
    import userModule from './user.js'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      strict: true,
    +  plugins: [createPersistedState()],
      modules: {
        // 模块名:导入进来的值
        user: userModule
      }
    })

后台首页布局

配置后台首页路由

router/index.js 中,增加 / 路由规则,指定显示 Main.vue组件

const routes = [
  // 直接写到这个最大的数组中的路由,对应的组件,应该显示在App.vue的占位符位置
  // 比如下面的 登录组件,应该显示在App.vue的占位符位置
  // { path: '路由地址', component: '组件' }
  { path: '/login', component: Login }, // 使用组件应该先导入
  { path: '/reg', component: () => import('@/views/Reg.vue') },
  // 使用 / 当前后台首页
+  { path: '/', component: () => import('@/views/Main.vue') }
]

在views文件夹,创建对应的组件 Main.vue

分四个区域

参考文档:https://element.eleme.cn/#/zh-CN/component/container

  • 找到倒数第三个示例,复制到Main.vue中
  • 设置最外层的盒子,高度100%
  • 其他几个区域,自己设置背景颜色即可
<template>
  <el-container class="main-container">
    <el-header>Header</el-header>
    <el-container>
      <el-aside width="200px">Aside</el-aside>
      <el-container>
        <el-main>Main</el-main>
        <el-footer>Footer</el-footer>
      </el-container>
    </el-container>
  </el-container>
</template>

<style lang="less">
.main-container {
  height: 100%;
}
.el-header,
.el-aside {
  background-color: #23262e;
}
.el-main {
  background-color: #f2f2f2;
}
.el-footer {
  background-color: #eee;
}
</style>

头部区和侧边栏布局

element菜单的使用

  • 一个菜单就是一个 <el-menu>.....</el-menu>

  • 如果只是一级菜单,没有下拉菜单,则使用 <el-menu-item index="1">一个菜单</el-menu-item>

  • 如果有下拉菜单,则使用

    <el-submenu index="2">
      <template slot="title">顶级菜单</template>
      <el-menu-item index="2-1">子菜单1</el-menu-item>
      <el-menu-item index="2-2">子菜单2</el-menu-item>
      <el-menu-item index="2-3">子菜单3</el-menu-item>
    </el-submenu>
  • 菜单属性(<el-menu> 标签的属性)

    • mode 表示菜单的模式(水平菜单还是垂直菜单,默认垂直菜单)
      • horizontal 水平菜单
      • vertical 垂直菜单(默认值)
    • background-color="#23262e" 菜单的背景色
    • text-color="#fff" 菜单的字体颜色
    • active-text-color="#409eff" 激活时的字体颜色
    • default-active="菜单的index值" 表示默认让这项激活
    • unique-opened 表示手风琴效果(一个菜单展开,其他全部折叠)
    • router 布尔值,如果是true。则表示使用 index 属性当前路由跳转地址
  • 子菜单的属性(<el-submenu><el-menu-item> 的属性)

    • index 表示菜单的唯一标识(很有用)
  • 完整的HTML

    <template>
      <el-container class="main-container">
        <el-header>
          <img src="../assets/images/logo.png" alt="" />
          <!-- 头部 右侧的菜单 -->
          <el-menu
            mode="horizontal"
            background-color="#23262e"
            text-color="#fff"
            active-text-color="#409eff"
          >
            <!-- 下面的菜单没有下拉菜单,则使用 el-menu-item 标签 -->
            <!-- <el-menu-item>处理中心</el-menu-item> -->
            <!-- 下面的菜单有下拉菜单,则使用 el-submenu 标签 -->
            <el-submenu index="1">
              <template slot="title">
                <img class="avatar" src="../assets/logo.png" alt="" />
                个人中心
              </template>
              <el-menu-item index="1-1"><i class="el-icon-edit"></i>用户资料</el-menu-item>
              <el-menu-item index="1-2"><i class="el-icon-edit"></i>重置密码</el-menu-item>
              <el-menu-item index="1-3"><i class="el-icon-edit"></i>更换头像</el-menu-item>
            </el-submenu>
            <el-menu-item index="2"><i class="el-icon-switch-button"></i>退出</el-menu-item>
          </el-menu>
        </el-header>
        <el-container>
          <el-aside width="200px">
            <div class="user-info">
              <img class="avatar" src="../assets/logo.png" alt="" />
              <span>欢迎你 xxx</span>
            </div>
            <!-- 侧边栏的菜单 -->
            <el-menu
              background-color="#23262e"
              text-color="#fff"
              active-text-color="#409eff"
              default-active="aaa"
              unique-opened
              class="aside-menu"
            >
              <el-menu-item index="aaa"><i class="el-icon-edit"></i>首页</el-menu-item>
              <el-submenu index="abc">
                <template slot="title"><i class="el-icon-edit"></i>文章管理</template>
                <el-menu-item index="bbb"><i class="el-icon-edit"></i>文章分类</el-menu-item>
                <el-menu-item index="ccc"><i class="el-icon-edit"></i>文章列表</el-menu-item>
              </el-submenu>
              <el-submenu index="bcd">
                <template slot="title"><i class="el-icon-edit"></i>个人中心</template>
                <el-menu-item index="ddd"><i class="el-icon-edit"></i>基本资料</el-menu-item>
                <el-menu-item index="eee"><i class="el-icon-edit"></i>重置密码</el-menu-item>
                <el-menu-item index="fff"><i class="el-icon-edit"></i>更换头像</el-menu-item>
              </el-submenu>
            </el-menu>
          </el-aside>
          <el-container>
            <el-main>Main</el-main>
            <el-footer>Footer</el-footer>
          </el-container>
        </el-container>
      </el-container>
    </template>
  • 首页四个区域的样式

    .main-container {
      height: 100%;
    }
    .el-header,
    .el-aside {
      background-color: #23262e;
    }
    .el-main {
      background-color: #f2f2f2;
    }
    .el-footer {
      background-color: #eee;
    }
  • 调整头部区,使用flex布局,让logo图片和菜单左右分散对齐

    .el-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
  • 侧边栏的样式

    .aside-menu {
      width: 200px;
    }
    .avatar {
      width: 35px;
      height: 35px;
      background-color: #fff;
      border-radius: 50%;
      margin-right: 15px;
      // object-fit: cover 和 background-size基本一个意思
      // 表示让图片按照原始的比例完整的显示出来
      object-fit: cover;
    }
    .user-info {
      height: 70px;
      color: #fff;
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 12px;
    }

    自愿 在 main.js 中导入 assets 里面的 global.less 。

    如果导入,需要将 global.less 中 “重置卡片的基础样式” 中的 font-size: 13px 去掉

获取用户信息

  • 获取用户信息,就需要向 /my/userinfo 发送请求
  • /my/userinfo 发送请求,就需要设置请求头 Authorization: token字符串
  • 假设能够得到数据,放哪里存储
    • 因为用户数据会在很多组件中使用,所以需要把数据放到 vuex 中
    • 所以就得在 vuex 中的 actions 中封装方法,获取用户数据
    • 所以,还需要设置 mutations 中的方法,来更新 state 中的数据
  • 上述方法都写完,也不会自动调用
    • 所以在Main.vue组件中,使用 created 生命周期函数,来调用 actions 中的方法
    • 这样,页面刷新,就会自动获取用户数据了
  • 数据存到state之后,组件中就可以把数据取出,用到页面当中了(处理头像和欢迎语)

设置请求头

image-20220124181611054

具体的代码

utils/request.js 代码如下:(注意,拦截器要加给 request 对象

import axios from 'axios'

// 导入 store/index.js ,因为要使用这里的state中的token
import store from '@/store'

// 创建axios的实例。 这个实例中,可以对axios进行全局的配置
// 得到的 request 也是axios。只不过名字不能再叫axios,因为重名了
const request = axios.create({
  baseURL: 'http://www.liulongbin.top:3008'
})

// 通过请求拦截器来设置请求头
// 添加请求拦截器
request.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    // config就是请求的配置对象
    /**
     * config = {
     *    url: '/user',
     *    method: 'get',
     *    baseURL: 'https://some-domain.com/api/',
     *    headers: {'X-Requested-With': 'XMLHttpRequest'},
     *    ......
     * }
     */
    // 添加请求头,判断一下,如果请求的url地址,是以 /my 开头的,则添加请求头
    if (config.url.startsWith('/my/')) {
      // config.headers.Authorization = 'tokenxxxxxx'
      // 组件中的this.$store ==== 上面导入的store
      config.headers.Authorization = store.state.user.token
    }
    return config
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)
// const request2 = axios.create({
//   baseURL: 'http://www.itcbc.com'
// })

export default request

注意,拦截器要加给 request 对象

api中封装发送请求的方法

由于前面已经给 request 加好了拦截器。所以下面直接发送请求即可

api/reg.js中代码

// .....其他代码略

// 导出获取用户信息的方法
export const getUserAPI = () => request.get('/my/userinfo')

调用 getUserAPI 获取用户数据

image-20220124182058294

具体代码

store/user.js

// 导入获取用户信息的方法
import { getUserAPI } from '@/api/reg'
export default {
  namespaced: true,
  state() {
    return {
      token: '',
      user: {} // 准备存储用户数据
    }
  },
  mutations: {
    // 修改token,只能通过下面的方法修改
    updateToken(state, token) {
      state.token = token
    },
    // 更新user数据
    updateUser(state, user) {
      state.user = user
    }
  },
  actions: {
    async getUser(ctx) {
      const { data: res } = await getUserAPI()
      // console.log(res)
      if (res.code === 0) {
        ctx.commit('updateUser', res.data)
      }
    }
  },
  getters: {}
}

上述方法并不会自动执行,所以在组件的生命周期函数中,调用 actions中的 getUser 方法。

Main.vue中,调用

export default {
  created() {
    // 调用vuex中的 actions 中的 getUser 方法
    // this.$store.dispatch('模块名/方法名')
    this.$store.dispatch('user/getUser')
  }
}

经过上述步骤,数据在登录后,即可存储到 vuex 当中。

组件中使用数据

Main.vue中,调用

import { mapState } from 'vuex'
export default {
  created() {
    // 调用vuex中的 actions 中的 getUser 方法
    // this.$store.dispatch('模块名/方法名')
    this.$store.dispatch('user/getUser')
  },
  computed: {
    // ...mapState('模块名', ['xxx', 'xxxx'])
    ...mapState('user', ['user'])
  }
}

页面中:

<div class="user-info">
  <img class="avatar" :src="user.user_pic" alt="" v-if="user.user_pic" />
  <img class="avatar" src="../assets/logo.png" alt="" v-else />
  <!-- 有昵称nickname,则使用昵称,没有昵称,只能使用账号username -->
  <span>欢迎你 {{ user.nickname || user.username }}</span>
</div>

身份认证

导航守卫判断token有无

前提,先手动清除本地存储的vuex,刷新页面,检测vuex中的token是否为空

如果vuex中的token为空,则不允许访问后台首页 (/)。此时可以使用全局前置导航守卫进行控制

  • 判断访问的是否是登录或注册,如果是,则放行
  • 如果访问的不是登录注册,则继续判断vuex中的token是否为空
// 必须加载store,因为下面的判断使用了vuex中的数据
import store from '@/store'

// 全局前置导航守卫(判断token是否为空,如果token为空,则跳转到登录页,不允许访问其他页面(路由))
router.beforeEach((to, from, next) => {
  // 判断访问的是否是登录或注册,如果是,则不需要判断token,直接放行
  // console.log(to) // to.path 表示即将访问的那个路由
  if (to.path === '/login' || to.path === '/reg') {
    next()
  } else {
    if (store.state.user.token) {
      next() // 表示允许访问(放行)
    } else {
      next('/login') // 表示跳转到指定的路由
    }
  }
})

响应拦截器判断token的真假

前面的导航守卫只能判断token有没有,但是浏览器端无法判断token的真假,无法判断token是否过期 只能使用服务端响应结果来判断token的真假

具体做法

  • 在 utils/request.js 中,加入响应拦截器 (把 axios 改为 request)
  • 在响应拦截器的第2个函数中(响应状态码大于200),进行判断
  • 如果响应结果 code===1 并且 message==='身份认证失败!' ,则清空token和user,跳转到登录页

代码中,使用了 store 和 router,所以一定要检测代码中是否加载的 '@/store' 和 '@/router'

// 添加响应拦截器,判断token的真假
request.interceptors.response.use(
  function (response) {
    // 对响应数据做点什么(响应状态码如果是2xx,则执行这个函数)
    return response
  },
  function (error) {
    // 对响应错误做点什么(响应状态码超过 2xx的范围,则执行这个函数)
    // console.dir(error) // 使用 dir 的目的是能够展开错误对象,能够看到对象内部的结构
    if (error.response) {
      if (error.response.data.code === 1 && error.response.data.message === '身份认证失败!') {
        // 说明浏览器端的token是假的或者是过期的
        // 加个提示
        // 清除假token
        store.commit('user/updateToken', '')
        // 清除用户数据
        store.commit('user/updateUser', {})
        // 跳转到登录页
        router.push('/login')
      }
    }
    return Promise.reject(error)
  }
)

退出功能

methods: {
  logout() {
    // 询问是否要退出
    this.$confirm('确定要退出吗?', '提示')
      .then(() => {
        // 点击了确定
        // console.log(111)
        // 确定退出,清除token、清除user
        this.$store.commit('user/updateToken', '')
        this.$store.commit('user/updateUser', {})
        // 跳转到登录页
        this.$router.push('/login')
      })
      .catch(() => {
        // 点击了取消
        // console.log(22)
      })
  }
}

左侧菜单数据处理

获取数据存到data中

  • 创建 api/menu.js。里面导入 request.js,并封装获取菜单的方法 getMenuAPI
  • 组件中,created中,调用 getMenuAPI 方法,将得到的结果存储到data中

menu.js 代码

// 菜单相关的请求
import request from '@/utils/request'

export const getMenuAPI = () => request.get('/my/menus')

Main.vue 代码

export default {
  data() {
    return {
      menus: []
    }
  },
  async created() {
    // 调用vuex中的 actions 中的 getUser 方法
    // this.$store.dispatch('模块名/方法名')
    this.$store.dispatch('user/getUser')
    // 调用 getMenuAPI 方法,马上获取菜单数据
    const { data: res } = await getMenuAPI()
    // console.log(res)
    if (res.code === 0) {
      // 获取菜单成功,把数据存储到哪里?
      this.menus = res.data
    }
  },
  ...............
}

循环一级菜单

  • 我们需要循环的是菜单项(菜单项有 el-menu-item 标签 和 el-submenu 标签),循环谁呢?
  • 所以外层包裹一个 <template> 标签,v-for 加个它
  • 循环里面,通过 v-if="item.children === null" 来控制,到达使用el-menu-item 标签 还是 el-submenu 标签
  • :key="item.indexPath" 不能加给虚拟的 template 标签
  • 注意,index、class改为动态属性后,别忘记前面加 :
<template v-for="item in menus">
  <el-menu-item
    :index="item.indexPath"
    v-if="item.children === null"
    :key="item.indexPath"
  >
    <i :class="item.icon"></i>{{ item.title }}
  </el-menu-item>
  <el-submenu :index="item.indexPath" v-else :key="item.indexPath">
    <template slot="title"><i :class="item.icon"></i>{{ item.title }}</template>
    <el-menu-item index="bbb"><i class="el-icon-edit"></i>文章分类</el-menu-item>
    <el-menu-item index="ccc"><i class="el-icon-edit"></i>文章列表</el-menu-item>
  </el-submenu>
</template>

循环二级菜单

  • 对于有children的菜单,就是有二级菜单,还需要循环它
  • 循环的时候,item这个变量就不要用了,因为item已经表示一级菜单了
<el-submenu :index="item.indexPath" v-else :key="item.indexPath">
  <template slot="title"><i :class="item.icon"></i>{{ item.title }}</template>
  <el-menu-item
    :index="subItem.indexPath"
    v-for="subItem in item.children"
    :key="subItem.indexPath"
  >
    <i :class="subItem.icon"></i>{{ subItem.title }}
  </el-menu-item>
</el-submenu>

后台首页的echarts图表页

创建组件和路由配置

  • 创建Home.vue组件
    <template>
      <div>这是首页 --- 这里放echarts图表</div>
    </template>
  • router/index.js 进行路由的配置
    {
      path: '/',
      redirect: '/home',
      component: () => import('@/views/Main.vue'),
      children: [
        { path: 'art-cate', component: () => import('@/views/ArtCate.vue') },
        { path: 'home', component: () => import('@/views/Home.vue') }
      ]
    }
  • Main.vue 内容主体区,放好占位符 <router-view></router-view>

绘制Home组件中的echarts

  • npm i echarts 下载安装echarts
  • 复制文档中的代码即可
  • 如果希望回顾一下echarts图表是如何使用的,查看vue基础第4天的资料。
<template>
  <div>
    <div class="container-fluid">
      <el-row class="spannel_list" :gutter="10">
        <el-col :sm="6" :xs="12">
          <div class="spannel">
            <em>10015</em><span></span>
            <b>总文章数</b>
          </div>
        </el-col>
        <el-col :sm="6" :xs="12">
          <div class="spannel scolor01">
            <em>123</em><span></span>
            <b>日新增文章数</b>
          </div>
        </el-col>
        <el-col :sm="6" :xs="12">
          <div class="spannel scolor02">
            <em>35</em><span></span>
            <b>评论总数</b>
          </div>
        </el-col>
        <el-col :sm="6" :xs="12">
          <div class="spannel scolor03">
            <em>123</em><span></span>
            <b>日新增评论数</b>
          </div>
        </el-col>
      </el-row>
    </div>

    <div class="container-fluid">
      <el-row class="curve-pie" :gutter="10">
        <el-col :sm="16" :xs="16">
          <div class="gragh_pannel" id="curve_show"></div>
        </el-col>
        <el-col :sm="8" :xs="8">
          <div class="gragh_pannel" id="pie_show"></div>
        </el-col>
      </el-row>
    </div>

    <div class="container-fluid">
      <div class="column_pannel" id="column_show"></div>
    </div>
  </div>
</template>

<script>
import * as echarts from 'echarts'

export default {
  name: 'Home',
  mounted() {
    var oChart = echarts.init(document.getElementById('curve_show'))
    var aListAll = [
      { count: 36, date: '2019-04-13' },
      { count: 52, date: '2019-04-14' },
      { count: 78, date: '2019-04-15' },
      { count: 85, date: '2019-04-16' },
      { count: 65, date: '2019-04-17' },
      { count: 72, date: '2019-04-18' },
      { count: 88, date: '2019-04-19' },
      { count: 64, date: '2019-04-20' },
      { count: 72, date: '2019-04-21' },
      { count: 90, date: '2019-04-22' },
      { count: 96, date: '2019-04-23' },
      { count: 100, date: '2019-04-24' },
      { count: 102, date: '2019-04-25' },
      { count: 110, date: '2019-04-26' },
      { count: 123, date: '2019-04-27' },
      { count: 100, date: '2019-04-28' },
      { count: 132, date: '2019-04-29' },
      { count: 146, date: '2019-04-30' },
      { count: 200, date: '2019-05-01' },
      { count: 180, date: '2019-05-02' },
      { count: 163, date: '2019-05-03' },
      { count: 110, date: '2019-05-04' },
      { count: 80, date: '2019-05-05' },
      { count: 82, date: '2019-05-06' },
      { count: 70, date: '2019-05-07' },
      { count: 65, date: '2019-05-08' },
      { count: 54, date: '2019-05-09' },
      { count: 40, date: '2019-05-10' },
      { count: 45, date: '2019-05-11' },
      { count: 38, date: '2019-05-12' }
    ]

    const aCount = []
    const aDate = []

    for (var i = 0; i < aListAll.length; i++) {
      aCount.push(aListAll[i].count)
      aDate.push(aListAll[i].date)
    }

    var chartopt = {
      title: {
        text: '月新增文章数',
        left: 'center',
        top: '10'
      },
      tooltip: {
        trigger: 'axis'
      },
      legend: {
        data: ['新增文章'],
        top: '40'
      },
      toolbox: {
        show: true,
        feature: {
          mark: { show: true },
          dataView: { show: true, readOnly: false },
          magicType: { show: true, type: ['line', 'bar'] },
          restore: { show: true },
          saveAsImage: { show: true }
        }
      },
      calculable: true,
      xAxis: [
        {
          name: '',
          type: 'category',
          boundaryGap: false,
          data: aDate
        }
      ],
      yAxis: [
        {
          name: '月新增文章数',
          type: 'value'
        }
      ],
      series: [
        {
          name: '新增文章',
          type: 'line',
          smooth: true,
          areaStyle: { type: 'default' },
          itemStyle: { color: '#f80', lineStyle: { color: '#f80' } },
          data: aCount
        }
      ],
      areaStyle: {
        normal: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            {
              offset: 0,
              color: 'rgba(255,136,0,0.39)'
            },
            {
              offset: 0.34,
              color: 'rgba(255,180,0,0.25)'
            },
            {
              offset: 1,
              color: 'rgba(255,222,0,0.00)'
            }
          ])
        }
      },
      grid: {
        show: true,
        x: 50,
        x2: 50,
        y: 80,
        height: 220
      }
    }

    oChart.setOption(chartopt)

    var oPie = echarts.init(document.getElementById('pie_show'))
    var oPieopt = {
      title: {
        top: 10,
        text: '分类文章数量比',
        x: 'center'
      },
      tooltip: {
        trigger: 'item',
        formatter: '{a} <br/>{b} : {c} ({d}%)'
      },
      color: ['#5885e8', '#13cfd5', '#00ce68', '#ff9565'],
      legend: {
        x: 'center',
        top: 65,
        data: ['奇趣事', '会生活', '爱旅行', '趣美味']
      },
      toolbox: {
        show: true,
        x: 'center',
        top: 35,
        feature: {
          mark: { show: true },
          dataView: { show: true, readOnly: false },
          magicType: {
            show: true,
            type: ['pie', 'funnel'],
            option: {
              funnel: {
                x: '25%',
                width: '50%',
                funnelAlign: 'left',
                max: 1548
              }
            }
          },
          restore: { show: true },
          saveAsImage: { show: true }
        }
      },
      calculable: true,
      series: [
        {
          name: '访问来源',
          type: 'pie',
          radius: ['45%', '60%'],
          center: ['50%', '65%'],
          data: [
            { value: 300, name: '奇趣事' },
            { value: 100, name: '会生活' },
            { value: 260, name: '爱旅行' },
            { value: 180, name: '趣美味' }
          ]
        }
      ]
    }
    oPie.setOption(oPieopt)

    var oColumn = echarts.init(document.getElementById('column_show'))
    var oColumnopt = {
      title: {
        text: '文章访问量',
        left: 'center',
        top: '10'
      },
      tooltip: {
        trigger: 'axis'
      },
      legend: {
        data: ['奇趣事', '会生活', '爱旅行', '趣美味'],
        top: '40'
      },
      toolbox: {
        show: true,
        feature: {
          mark: { show: true },
          dataView: { show: true, readOnly: false },
          magicType: { show: true, type: ['line', 'bar'] },
          restore: { show: true },
          saveAsImage: { show: true }
        }
      },
      calculable: true,
      xAxis: [
        {
          type: 'category',
          data: ['1月', '2月', '3月', '4月', '5月']
        }
      ],
      yAxis: [
        {
          name: '访问量',
          type: 'value'
        }
      ],
      series: [
        {
          name: '奇趣事',
          type: 'bar',
          barWidth: 20,
          areaStyle: { type: 'default' },
          itemStyle: {
            color: '#fd956a'
          },
          data: [800, 708, 920, 1090, 1200]
        },
        {
          name: '会生活',
          type: 'bar',
          barWidth: 20,
          areaStyle: { type: 'default' },
          itemStyle: {
            color: '#2bb6db'
          },
          data: [400, 468, 520, 690, 800]
        },
        {
          name: '爱旅行',
          type: 'bar',
          barWidth: 20,
          areaStyle: { type: 'default' },
          itemStyle: {
            color: '#13cfd5'
          },
          data: [500, 668, 520, 790, 900]
        },
        {
          name: '趣美味',
          type: 'bar',
          barWidth: 20,
          areaStyle: { type: 'default' },
          itemStyle: {
            color: '#00ce68'
          },
          data: [600, 508, 720, 890, 1000]
        }
      ],
      grid: {
        show: true,
        x: 50,
        x2: 30,
        y: 80,
        height: 260
      },
      dataZoom: [
        // 给x轴设置滚动条
        {
          start: 0, // 默认为0
          end: 100 - 1000 / 31, // 默认为100
          type: 'slider',
          show: true,
          xAxisIndex: [0],
          handleSize: 0, // 滑动条的 左右2个滑动条的大小
          height: 8, // 组件高度
          left: 45, // 左边的距离
          right: 50, // 右边的距离
          bottom: 26, // 右边的距离
          handleColor: '#ddd', // h滑动图标的颜色
          handleStyle: {
            borderColor: '#cacaca',
            borderWidth: '1',
            shadowBlur: 2,
            background: '#ddd',
            shadowColor: '#ddd'
          }
        }
      ]
    }
    oColumn.setOption(oColumnopt)
  }
}
</script>

<style lang="less" scoped>
.spannel_list {
  margin-top: 20px;
}

.spannel {
  height: 100px;
  overflow: hidden;
  text-align: center;
  position: relative;
  background-color: #fff;
  border: 1px solid #e7e7e9;
  margin-bottom: 20px;
}

.spannel em {
  font-style: normal;
  font-size: 50px;
  line-height: 50px;
  display: inline-block;
  margin: 10px 0 0 20px;
  font-family: 'Arial';
  color: #83a2ed;
}

.spannel span {
  font-size: 14px;
  display: inline-block;
  color: #83a2ed;
  margin-left: 10px;
}

.spannel b {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  line-height: 24px;
  background: #e5e5e5;
  color: #333;
  font-size: 14px;
  font-weight: normal;
}

.scolor01 em,
.scolor01 span {
  color: #6ac6e2;
}

.scolor02 em,
.scolor02 span {
  color: #5fd9de;
}

.scolor03 em,
.scolor03 span {
  color: #58d88e;
}

.gragh_pannel {
  height: 350px;
  border: 1px solid #e7e7e9;
  background-color: #fff !important;
  margin-bottom: 20px;
}

.column_pannel {
  margin-bottom: 20px;
  height: 400px;
  border: 1px solid #e7e7e9;
  background-color: #fff !important;
}
</style>

用户资料

组件的存储思路

随时项目的开发,组件会越来越多。如果把组件全部放到 views 中,会非常乱,不好找 正确的做法是,将组件分门别类的存储在子文件夹中。 比如我们的项目,菜单:

菜单(Menu)
  - 首页(Home)
    - Home.vue
  - 文章管理(Article)
    - 文章分类 XXX.vue
    - 文章列表 XXX.vue
  - 个人中心(User)
    - 用户资料 UserInfo.vue
    - 重置密码 Xxxxx.vue
    - 更换头像 Xxxx.vue

目前,我们开发到用户资料这个阶段,所以在 /views/Menu/User 中 创建 UserInfo.vue 并且把Home.vue的位置修改一下。 最后,路由的配置:

{
  path: '/',
  redirect: '/home',
  component: () => import('@/views/Main.vue'),
  children: [
    { path: 'home', component: () => import('@/views/Menu/Home/Home.vue') },
    { path: 'user-info', component: () => import('@/views/Menu/User/UserInfo.vue') }
  ]
}

绘制用户资料页面

elementUI的卡片和表单

<template>
  <el-card class="box-card">
    <div slot="header" class="clearfix">
      <span>基本资料</span>
    </div>
    <div class="text item">
      <el-form ref="form" label-width="80px">
        <!-- 第一项:登录账号 -->
        <el-form-item label="登录账号">
          <el-input disabled></el-input>
        </el-form-item>
        <!-- 第二项:用户昵称 -->
        <el-form-item label="用户昵称">
          <el-input></el-input>
        </el-form-item>
        <!-- 第三项:用户邮箱 -->
        <el-form-item label="用户邮箱">
          <el-input></el-input>
        </el-form-item>
        <!-- 第四项:按钮 -->
        <el-form-item>
          <el-button type="primary">立即创建</el-button>
          <el-button>取消</el-button>
        </el-form-item>
      </el-form>
    </div>
  </el-card>
</template>

<style lang="less" scoped>
.el-card {
  font-size: 13px;
}

.el-form {
  width: 500px;
}
</style>

将vuex中的数据拿到组件并双向绑定

做修改之前,必须进行数据回填(把当前用户的数据,回填到输入框中)

怎样数据回填?

  • 用户数据在 vuex 中
  • 把数据转存到组件中一份
  • 使用 v-model 进行双向数据绑定
export default {
  data() {
    return {
      userInfo: {
        username: this.$store.state.user.user.username,
        nickname: this.$store.state.user.user.nickname,
        email: this.$store.state.user.user.email
      }
    }
  }
}

每个 input 框,加好 v-model

如果数据回填后,发现昵称和邮箱是空的,不一定是错的。新注册的账号,就是没有昵称和邮箱的

表单验证

  • 表单加入两个属性
    • :model="数据项"
    • :rules="验证规则对象"
  • 表单项(<el-form-item prop="字段">
  • 编写验证规则
    • data里面定义验证规则 formRules: { 字段: [{required: true}, {...}], 字段: [] }
    • 验证邮箱格式,可以 { type: 'email', message: '邮箱格式不正确', trigger: ['change', 'blur'] }
  • 表单提交的时候,进行完整的验证
    • this.$refs.表单的ref值.validate(valid => { /* 根据valid进行判断 */ })

对象的浅拷贝

let obj1 = { name: 'zs', age: 20 };
// let obj2 = obj1;

// 浅拷贝和深拷贝
// 浅拷贝,把一个对象的属性和值,拷贝一份,加入到另一个空对象中
// 深拷贝,把一个对象的属性和值,拷贝一份,加入到另一个对象。(如果对象的某个属性值是引用类型的,则需要递归拷贝),拷贝后,两个对象是完全独立的两个对象

// 浅拷贝的方案(for循环、Object.assign、转成JSON再转成JS对象、...展开运算符)
let obj2 = {};

// for (let k in obj1) {
//   obj2[k] = obj1[k]
// }

// obj2 = Object.assign({}, obj1)

// obj2 = JSON.parse(JSON.stringify(obj1))

obj2 = { ...obj1 }

项目中,可以通过浅拷贝来简化代码:

UserInfo.vue 中:

data() {
  return {
    userInfo: { ...this.$store.state.user.user }
  }
}

表单提交,完成资料修改

  • 找到提交按钮,注册单击事件
    <el-button type="primary" @click="updateUser">确认修改</el-button>
  • 组件中,添加 updateUser 方法
    methods: {
      async updateUser() {
        // console.log(this.userInfo)
        const { data: res } = await updateUserAPI(this.userInfo)
        // console.log(res)
        if (res.code === 0) {
          this.$message.success(res.message)
          // 调用 vuex 中的 actions 中的 getUser 方法,获取并更新最新的用户数据
          // this.$store.dispatch('模块名/方法名')
          this.$store.dispatch('user/getUser')
        } else {
          this.$message.error(res.message)
        }
      }
    }
  • api/reg.js 中,封装发送请求的 updateUserAPI 方法
    // 更新用户信息
    export const updateUserAPI = data => request.put('/my/userinfo', data)

重置表单

  • 修改 按钮 为 重置两个字

  • 加入单击事件 <el-button @click="reset">重置</el-button>

  • 补充 reset 方法

    reset() {
      // 下面的代码作用是重置表单(恢复输入框的值为打开页面时的初始值;清除掉表单验证红色的提示)
      this.$refs.userInfoRef.resetFields()
      // 从vuex中更新一份数据到当前组件的 userInfo中
      this.userInfo = Object.assign({}, this.$store.state.user.user)
    }

重置密码

页面布局

  • 因为页面和用户资料页面一样。所以复制“用户资料”页面的结构

    • 修改文字
    • 去掉第一个输入框的 disabled
  • 配置路由 (router/index.js)

    {
      path: '/',
      redirect: '/home',
      component: () => import('@/views/Main.vue'),
      children: [
        { path: 'home', component: () => import('@/views/Menu/Home/Home.vue') },
        { path: 'user-info', component: () => import('@/views/Menu/User/UserInfo.vue') },
    +    { path: 'user-pwd', component: () => import('@/views/Menu/User/RePwd.vue') }
      ]
    }

表单验证

  • <el-form :model="数据项" :rules="验证规则">

    • 对应数据项定义一下;
    • 对应的规则定义一下
  • 每个 <el-form-item> 加入 prop 属性

  • 每个 <el-input> 加入 v-model 双向数据绑定

  • 定义具体的规则

    • 必填
    • 6~15位的非空字符
  • 自定义验证规则(验证新密码不能和原密码一致;验证两次新密码必须一致)

    • 在 data 中,return之前,声明函数

      const xxx = (rule, value, callback) => {
        // rule 表示验证规则对象,没有什么用
        // value 表示当前输入框的值 (比如新密码使用了这个规则,则value表示输入的新密码)
        // callback() --- 表示通过验证
        // callbakc(new Error('不通过的提示')) --- 表示验证不通过
      }
    • 验证规则中,加入这个自定义的规则

      new_pwd: [
         { required: true, message: '原密码必填', trigger: 'blur' },
         { pattern: /^\S{6,15}$/, message: '6~15位的非空字符', trigger: 'blur' },
      +  { validator: xxx, trigger: 'blur' }
      ]

提交表单,完成更新密码

  • 提交密码,需要发送请求,所以需要一个API方法 (api/reg.js)

    // 重置密码
    export const updatePwdAPI = data => request.patch('/my/updatepwd', data)
  • 表单提交,注册按钮的单击事件

    <el-button type="primary" @click="updatePwd">确认修改</el-button>
  • 事件处理方法中,找到表单进行一次完整的验证

  • 验证通过,则调用 API 方法,发送请求

  • 更新成功,提示,重置表单

    import { updatePwdAPI } from '@/api/reg.js'
    
    methods: {
      updatePwd() {
        this.$refs.passRef.validate(async valid => {
          if (valid) {
            // 表示验证通过
            const { data: res } = await updatePwdAPI(this.pass)
            if (res.code === 0) {
              this.$message.success(res.message) // 提示
              this.$refs.passRef.resetFields() // 重置表单
            } else {
              this.$message.error(res.message)
            }
          }
        })
      }
    }

重置表单

<el-button @click="$refs.passRef.resetFields()">重置</el-button>

更换头像

页面布局

  • 创建组件,并复制页面结构

    <template>
      <el-card class="box-card">
        <div slot="header" class="clearfix">
          <span>更换头像</span>
        </div>
        <div>
          <!-- 图片,用来展示用户选择的头像 -->
          <img src="../../../assets/images/avatar.jpg" alt="" class="preview" />
    
          <!-- 按钮区域 -->
          <div class="btn-box">
            <el-button type="primary" icon="el-icon-plus">选择图片</el-button>
            <el-button type="success" icon="el-icon-upload" disabled>上传头像</el-button>
          </div>
        </div>
      </el-card>
    </template>
    
    <script>
    export default {
      name: 'UserAvatar'
    }
    </script>
    
    <style lang="less" scoped>
    .btn-box {
      margin-top: 10px;
    }
    .preview {
      object-fit: cover;
      width: 350px;
      height: 350px;
    }
    </style>
  • 配置路由

    {
      path: '/',
      redirect: '/home',
      component: () => import('@/views/Main.vue'),
      children: [
        { path: 'home', component: () => import('@/views/Menu/Home/Home.vue') },
        { path: 'user-info', component: () => import('@/views/Menu/User/UserInfo.vue') },
        { path: 'user-pwd', component: () => import('@/views/Menu/User/RePwd.vue') },
    +    { path: 'user-avatar', component: () => import('@/views/Menu/User/Avatar.vue') }
      ]
    }

点击选择图片按钮选择图片

按钮前面,添加一个文件域 <input type="file" ref="fileRef" /> ,因为在所有的HTML标签中,只有文件域能够选择图片。

点击按钮,触发文件域的单击事件即可。

<el-button type="primary" icon="el-icon-plus" @click="$refs.fileRef.click()">
  选择图片
</el-button>

选择图片后实现图片预览

无论使用 FileReader 对象,还是使用 URL对象做预览效果,都需要先得到文件对象。

如何得到文件对象?

  • 找到文件域,注册 change 事件 (当文件域的文件改变的时候,触发change事件)

    <input type="file" ref="fileRef" @change="preview" />
  • 事件源.files[0] 就是文件对象

    methods: {
      preview(e) {
        // e.target // 表示事件源
        // console.dir(e.target)
        if (e.target.files.length > 0) {
          // 说明我们选择了图片
          // 获取文件对象
          const fileObj = e.target.files[0]
          console.log(fileObj)
        } else {
          // 说明没有选择图片
        }
      }
    }
  • 使用URL对象,实现图片预览

    • https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL

    • 调用浏览器内置方法 URL.createObjectURL ,参数是 文件对象。返回值就是一个url。这个url可以访问到我们选择的那个图片

    • const url = URL.createObjectURL(fileObj)

    • 把url转存到data数据项中

      export default {
        data() {
          return {
            url: ''
          }
        },
        methods: {
          preview(e) {
            // e.target // 表示事件源
            // console.dir(e.target)
            if (e.target.files.length > 0) {
              // 说明我们选择了图片
              // 获取文件对象
              const fileObj = e.target.files[0]
              // console.log(fileObj)
              // 得到访问图片的url地址
              const url = URL.createObjectURL(fileObj)
              // console.log(url)
              this.url = url
            } else {
              // 说明没有选择图片
            }
          }
        }
      }
    • 页面中,展示

      <!-- 图片,用来展示用户选择的头像 -->
      <img src="../../../assets/images/avatar.jpg" alt="" class="preview" v-if="url === ''" />
      <img :src="url" alt="" class="preview" v-else />
  • 使用FileReader实现图片预览

    • https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsDataURL

    • 实例化 FileReader对象

    • 调用 readAsDataURL(文件对象) 方法,读取并转换成base64

    • 通过 onload 事件,监听转换是否完成

    • 转换完成后,通过 result 属性获取结果

      methods: {
        preview(e) {
          // e.target // 表示事件源
          // console.dir(e.target)
          if (e.target.files.length > 0) {
            // 说明我们选择了图片
            // 获取文件对象
            const fileObj = e.target.files[0]
            // console.log(fileObj)
            // 通过 FileReader 得到base64格式
            const fr = new FileReader()
            fr.readAsDataURL(fileObj)
            // 下面的事件处理函数,必须使用箭头函数。否则this就表示事件源fr了
            fr.onload = () => {
              // 把转成后的base64结果,赋值给数据项中的url
              this.url = fr.result
            }
          } else {
            // 说明没有选择图片
          }
        }
      }
    • 页面中的 img 标签

      <!-- 图片,用来展示用户选择的头像 -->
      <img src="../../../assets/images/avatar.jpg" alt="" class="preview" v-if="url === ''" />
      <img :src="url" alt="" class="preview" v-else />

点击上传头像按钮更换头像

  • 封装 更换头像的 API 方法

    // 更换头像
    export const updateAvatarAPI = str => {
      return request.patch('/my/update/avatar', { avatar: str })
    }
  • 根据数据是否为空,控制 按钮是否 为禁用

  • 注册单击事件

    <el-button
               type="success"
               icon="el-icon-upload"
               :disabled="url === ''"
               @click="updateAvatar"
               >
      上传头像
    </el-button>
  • 事件处理方法,调用API方法,发送请求,提交 base64 字符串

  • 更换成功后,调用 vuex 中的 actions 中的 getUser 方法,更新数据

    async updateAvatar() {
      const { data: res } = await updateAvatarAPI(this.url)
      if (res.code === 0) {
        this.$message.success(res.message)
        // 调用 vuex 中的 actions中的 getUser 方法,更新用户数据
        this.$store.dispatch('user/getUser')
      } else {
        this.$message.error(res.message)
      }
    }

文章分类

页面布局

  • 创建 在 /views/Menus/Article 中,创建 ArtCate.vue,代码如下

    <template>
      <el-card class="box-card">
        <div slot="header" class="clearfix card-header">
          <span>文章分类</span>
          <el-button type="primary" size="medium">添加分类</el-button>
        </div>
        <!-- 卡片的内容区,放表格 -->
        <el-table>
          <!-- 第1列 prop表示数据项的名字; label是表头 -->
          <el-table-column prop="date" label="序号" width="100"> </el-table-column>
          <el-table-column prop="date" label="分类名称"> </el-table-column>
          <el-table-column prop="date" label="分类别名"> </el-table-column>
          <el-table-column prop="date" label="操作"> </el-table-column>
        </el-table>
      </el-card>
    </template>
    
    <script>
    export default {}
    </script>
    
    <style lang="less" scoped>
    .el-card {
      font-size: 13px;
    }
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    </style>
    
  • 配置路由 (router/index.js)

    { path: 'art-cate', component: () => import('@/views/Menu/Article/ArtCate.vue') }

获取分类展示

  • api/article.js 封装API方法,发送请求获取文章分类数据

    import request from '@/utils/request'
    
    // 封装获取文章类别的API方法
    export const getCategoryAPI = () => request.get('/my/cate/list')
  • 组件中,调用API方法,取得文章分类数据。

  • 把数据转存到data数据项中

    // 按需加载 API 方法
    import { getCategoryAPI } from '@/api/article.js'
    
    export default {
      data() {
        return {
          cateList: []
        }
      },
      created() {
        // 页面刷新,马上调用获取文章分类的方法
        this.initCategory()
      },
      methods: {
        async initCategory() {
          const { data: res } = await getCategoryAPI()
          if (res.code === 0) {
            this.cateList = res.data
          }
        }
      }
    }
  • 配合element-ui的语法,将数据渲染到表格中

    • <el-table> 加入 :data="数据项"
    • <el-table-column> 加入 prop="字段"
    • 如果 第一列 用连续的序号,使用element-ui提供的固定的 type=index
    <el-table :data="cateList">
      <!-- 第1列 prop表示数据项的名字; label是表头 -->
      <!-- 第一列放序号,使用固定的 type="index" 即可得到连续的序号 -->
      <el-table-column type="index" label="序号" width="100"> </el-table-column>
      <el-table-column prop="cate_name" label="分类名称"> </el-table-column>
      <el-table-column prop="cate_alias" label="分类别名"> </el-table-column>
      <el-table-column label="操作"> </el-table-column>
    </el-table>

表格如果需要垂直方向的边框,给 <el-table> 加入 border 属性

表格如果需要隔行换色,给 <el-table> 加入 stripe 属性

添加分类

  • 点击“添加分类”,显示对话框

    • https://element.eleme.cn/#/zh-CN/component/dialog
    • 数据项中,设置一个控制添加分类的对话框是否显示的 变量 addVisible: false
    • “添加分类”按钮,加入单击事件,单击的时候,@click="addVisible=true"
    • 修改“取消”和“确定”按钮的变量为 addVisible
    <el-dialog title="提示" :visible.sync="addVisible" width="30%">
      <span>这是一段信息</span>
      <span slot="footer" class="dialog-footer">
        <el-button @click="addVisible = false">取 消</el-button>
        <el-button type="primary" @click="addVisible = false">确 定</el-button>
      </span>
    </el-dialog>
  • 绘制弹框中的表单(把原来的 <span>这是一段信息</span> 换成表单)

    <el-form label-width="80px">
      <!-- 第一项:分类名称 -->
      <el-form-item label="分类名称">
        <el-input></el-input>
      </el-form-item>
      <!-- 第二项:分类别名 -->
      <el-form-item label="分类别名">
        <el-input></el-input>
      </el-form-item>
    </el-form>
  • 表单验证

    • <el-form> 加入 :model="数据项":rules="验证规则"

    • <el-form-item> 加入 prop="字段"

    • <el-input> 加入 v-model="字段"

    • JS中,设置数据项,设置验证规则

      data() {
        return {
          cateList: [],
          addVisible: false, // 控制添加的弹框是否显示,默认false。表示不显示
          // 添加分类的数据项
          addData: {
            cate_name: '', // 分类名称
            cate_alias: '' // 分类别名
          },
          // 添加分类的验证规则
          addFormRules: {
            cate_name: [
              { required: true, message: '分类名称必填', trigger: 'blur' },
              { pattern: /^\S{1,10}$/, message: '分类名称必须是1~10位非空字符', trigger: 'blur' }
            ],
            cate_alias: [
              { required: true, message: '分类别名必填', trigger: 'blur' },
              {
                pattern: /^[a-zA-Z0-9]{1,15}$/,
                message: '分类名称必须是1~15位的字母数字组合',
                trigger: 'blur'
              }
            ]
          }
        }
      },
  • 提交表单数据,完成添加

    • 封装 API 方法

      // 封装添加类别的方法
      export const addCategoryAPI = data => request.post('/my/cate/add', data)
    • 组件中,修改确定按钮的单击事件,单击的时候,调用方法

      <el-button type="primary" @click="addCategory">确 定</el-button>
    • 补充添加分类的方法。

      // 添加类别的方法
      addCategory() {
        this.$refs.addRef.validate(async valid => {
          if (valid) {
            const { data: res } = await addCategoryAPI(this.addData)
            if (res.code === 0) {
              this.$message.success(res.message) // 提示
              this.$refs.addRef.resetFields() // 重置表单
              this.addVisible = false // 关闭弹框
              this.initCategory() // 更新数据
            } else {
              this.$message.error(res.message)
            }
          }
        })
      }
  • 当弹框关闭的时候,应该重置一下表单

    • <el-dialog> 加入 :before-close="resetAddForm" ,表示关闭弹框之前,做什么

    • JS中,补充 resetAddForm 方法

      // 关闭添加弹框的时候,重置表单
      resetAddForm(done) {
        this.$refs.addRef.resetFields() // 重置表单
        this.addVisible = false // 关闭弹框
        // done() // 调用它,表示关闭对话框(必须调用它,如果不调用,会暂停 Dialog 的关闭)
      },
    • 取消的时候,也调用这个方法

      <el-button @click="resetAddForm">取 消</el-button>

删除分类

  • 封装删除分类的API方法

    // 删除分类
    export const deleteCategoryAPI = id => request.delete('/my/cate/del', { params: { id } })
  • 参考elementUI的表格 https://element.eleme.cn/#/zh-CN/component/table

    • 找到带有“编辑” 或 “删除” 的表格,分析

    • 发现 操作列使用了vue的 “作用域插槽” (通过 slot-scope="scope" 接收到数据)

      • 其中,slot-scope 是vue2版本的插槽写法,vue3不支持。可以更换为 v-slot
      • 后一个scope,也只是一个变量名,可以随便换,比如换成 obj
      • obj.row 表示当前行的数据信息
      <el-table-column label="操作">
        <template v-slot="obj">
          <el-button type="primary" size="mini">
            修改
          </el-button>
          <el-button type="danger" size="mini" @click="deleteRow(obj.row.id)">
            删除
          </el-button>
        </template>
      </el-table-column>
  • JS中,定义 deleteRow 方法,接收id参数,调用API方法,完成删除

    import { getCategoryAPI, addCategoryAPI, deleteCategoryAPI } from '@/api/article.js'
    
    
    其他代码略
    
    
    // 删除
    deleteRow(id) {
      if (id === 1 || id === 2) return this.$message.error('管理员不允许删除id=1和id=2的数据')
      this.$confirm('确定要删除吗?', '提示', {
        callback: async r => {
          // console.log(r) // cancel confirm
          if (r === 'confirm') {
            const { data: res } = await deleteCategoryAPI(id)
            if (res.code === 0) {
              this.$message.success(res.message)
              this.initCategory()
            } else {
              this.$message.error(res.message)
            }
          }
        }
      })
    },

修改分类

凡是修改,必须进行数据回填。回填的数据也是来自于前面的 作用域插槽

  • ArtCate.vue 中声明修改分类的对话框:

    • 复制 添加的对话框
    • 修改 addVisibleeditVisible
    • 修改addDataeditData
    • 修改 表单的ref 为 editRef
    • 去掉 对话框的 @closed="resetAddForm"
    • 去掉 确定按钮的 单击事件
    • 表单验证 继续使用 添加的表单验证即可
    <!-- 修改分类的对话框 -->
    <el-dialog title="修改类别" :visible.sync="editVisible" width="35%">
      <!-- 这里放添加的表单 -->
      <el-form :model="editData" :rules="addFormRules" label-width="80px" ref="editRef">
        <!-- 第一项:分类名称 -->
        <el-form-item label="分类名称" prop="cate_name">
          <el-input v-model="editData.cate_name"></el-input>
        </el-form-item>
        <!-- 第二项:分类别名 -->
        <el-form-item label="分类别名" prop="cate_alias">
          <el-input v-model="editData.cate_alias"></el-input>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="editVisible = false">取 消</el-button>
        <el-button type="primary">确 定</el-button>
      </span>
    </el-dialog>
  • 在 data 中定义布尔值 editVisible 和数据对象 editData

    data() {
      return {
        // 控制修改对话框的显示与隐藏
        editVisible: false,
        // 修改表单的数据对象
        editData: {}
      }
    }
  • 为修改按钮绑定点击事件处理函数:

    <template v-slot="obj">
      <el-button type="primary" size="mini" @click="editCategory(obj.row)">
        修改
      </el-button>
      <el-button type="danger" size="mini" @click="deleteRow(obj.row.id)">
        删除
      </el-button>
    </template>
  • 定义 editCategory处理函数:

    // 显示修改的弹框
    editCategory(row) {
      if (row.id === 1 || row.id === 2) return this.$message.error('不允许修改前两条数据')
      this.editVisible = true
      // console.log(row)
      this.editData = { ...row }
    },

至此,数据回填完成。

  • 封装修改分类的API方法

    // 修改分类
    export const updateCategoryAPI = data => request.put('/my/cate/info', data)
  • 确定按钮添加单击事件

    <el-button type="primary" @click="updateCategory">确 定</el-button>
  • 组件中,完成修改方法

    // 按需加载 API 方法
    import {
      getCategoryAPI,
      addCategoryAPI,
      deleteCategoryAPI,
      updateCategoryAPI
    } from '@/api/article.js'
    
    
    其他代码略
    
    
    // 修改分类
    updateCategory() {
      this.$refs.editRef.validate(async valid => {
        if (valid) {
          const { data: res } = await updateCategoryAPI(this.editData)
          if (res.code === 0) {
            this.$message.success(res.message)
            this.initCategory()
            this.editVisible = false
          } else {
            this.$message.error(res.message)
          }
        }
      })
    }

添加文章

新建组件定义路由

  • src/views/Menus/Article/ 目录下,新建 ArtList.vue 组件

  • 补充路由规则

    { path: 'art-list', component: () => import('@/views/Menu/Article/ArtList.vue') }

页面布局

<template>
  <div>
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <span>文章列表</span>
      </div>
      <!-- 搜索区域 -->
      <div class="search-box">
        <el-form :inline="true" :model="q">
          <el-form-item label="文章分类">
            <el-select v-model="q.cate_id" placeholder="请选择分类" size="small">
              <el-option label="区域一" value="shanghai"></el-option>
              <el-option label="区域二" value="beijing"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="发布状态" style="margin-left: 15px;">
            <el-select v-model="q.state" placeholder="请选择状态" size="small">
              <el-option label="已发布" value="已发布"></el-option>
              <el-option label="草稿" value="草稿"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" size="small">筛选</el-button>
            <el-button type="info" size="small">重置</el-button>
          </el-form-item>
        </el-form>
        <!-- 发表文章的按钮 -->
        <el-button type="primary" size="small" class="btn-pub">发表文章</el-button>
      </div>

      <!-- 文章表格区域 -->

      <!-- 分页区域 -->
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'ArtList',
  data() {
    return {
      // 查询参数对象
      q: {
        pagenum: 1,
        pagesize: 2,
        cate_id: '',
        state: ''
      }
    }
  }
}
</script>

<style lang="less" scoped>
.search-box {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  .btn-pub {
    margin-top: 5px;
  }
}
</style>

全屏的对话框

  1. ArtList.vue 组件中,声明发表文章的对话框:

    <!-- 发表文章的 Dialog 对话框 -->
    <el-dialog title="发表文章" :visible.sync="pubDialogVisible" fullscreen :before-close="handleClose">
      <span>这是一段信息</span>
    </el-dialog>
  2. 在 data 中定义布尔值 pubDialogVisible,用来控制对话框的显示与隐藏:

    data() {
      return {
        // 控制发表文章对话框的显示与隐藏
        pubDialogVisible: false
      }
    }
  3. 点击发布按钮,展示对话框:

    <!-- 发表文章的按钮 -->
    <el-button 
      type="primary" 
      size="small" 
      class="btn-pub" 
      @click="pubDialogVisible = true"
    >
      发表文章
    </el-button>
  4. 在对话框将要关闭时,询问用户是否确认关闭对话框:

    // 关闭事件的处理函数
    handleClose(done) {
      this.$confirm('关闭将会丢失已填写的数据,确定要关闭吗?', {
        callback: r => {
          if (r === 'confirm') {
            // this.pubDialogVisible = false
            done()
          }
        }
      })
    }

渲染发表文章的表单

  1. 初步定义表单的 UI 结构:

    <!-- 全屏对话框的表单 -->
    <el-form :model="pubForm" :rules="pubFormRules" ref="pubFormRef" label-width="80px">
      <!-- 第一项:文章标题 -->
      <el-form-item label="文章标题" prop="title">
        <el-input v-model="pubForm.title" placeholder="请输入标题"></el-input>
      </el-form-item>
      <!-- 第二项:选择分类 -->
      <el-form-item label="选择分类" prop="cate_id">
        <el-select v-model="pubForm.cate_id" placeholder="请选择分类" style="width: 100%">
          <el-option label="北京" value="2"></el-option>
          <el-option label="上海" value="5"></el-option>
        </el-select>
      </el-form-item>
    </el-form>
  2. 在 data 中定义数据对象和验证规则对象:

    data() {
      return {
        // 表单的数据对象
        pubForm: {
          title: '',
          cate_id: ''
        },
        // 表单的验证规则对象
        pubFormRules: {
          title: [
            { required: true, message: '请输入文章标题', trigger: 'blur' },
            { min: 1, max: 30, message: '文章标题的长度为1-30个字符', trigger: 'blur' }
          ],
          cate_id: [{ required: true, message: '请选择分类', trigger: 'blur' }]
        }
      }
    }

动态请求并渲染文章分类的数据

  1. 封装获取分类的API (前面已经封装过,这里别忘记导入)

    // 加载获取分类的API方法,获取分类
    import { getCategoryAPI } from '@/api/article.js'
  2. 在 methods 中,声明初始化文章分类列表数据的方法:

    // 初始化文章分类的列表数据
    async initCateList() {
      const { data: res } = await getCategoryAPI()
      if (res.code === 0) {
        this.cateList = res.data
      } else {
        this.$message.error(res.message)
      }
    },
  3. 在 data 中声明 cateList 数组:

    data() {
      return {
        // 文章分类
        cateList: []
      }
    }
  4. 在 created 生命周期函数中,调用步骤2声明的方法:

    created() {
      this.initCateList()
    },
  5. 循环渲染文章分类的可选项:

    <el-form-item label="文章分类" prop="cate_id">
      <el-select v-model="pubForm.cate_id" placeholder="请选择分类" style="width: 100%;">
        <!-- 循环渲染分类的可选项 -->
        <el-option :label="item.cate_name" :value="item.id" v-for="item in cateList" :key="item.id">
        </el-option>
      </el-select>
    </el-form-item>

渲染富文本编辑器

基于 vue-quill-editor 实现富文本编辑器:https://www.npmjs.com/package/vue-quill-editor

  1. 运行如下的命令,在项目中安装富文本编辑器:

    npm i vue-quill-editor@3.0.6 -S
  2. 在项目入口文件 main.js 中导入并全局注册富文本编辑器:

    // 导入富文本编辑器
    import VueQuillEditor from 'vue-quill-editor'
    // 导入富文本编辑器的样式
    import 'quill/dist/quill.core.css'
    import 'quill/dist/quill.snow.css'
    import 'quill/dist/quill.bubble.css'
    
    // 全局注册富文本编辑器
    Vue.use(VueQuillEditor)
  3. ArtList.vue 组件的 data 中,定义富文本编辑器对应的数据项:

    data() {
      return {
        // 表单的数据对象
        pubForm: {
          title: '',
          cate_id: '',
          // 文章的内容
    +     content: ''
        },
      }
    }
  4. ArtList.vue 组件的模板结构中,添加富文本编辑器的 DOM 结构:

    <el-form-item label="文章内容">
      <!-- 使用 v-model 进行双向的数据绑定 -->
      <quill-editor v-model="pubForm.content"></quill-editor>
    </el-form-item>
  5. 美化富文本编辑器的样式:

    // 设置富文本编辑器的默认最小高度
    /deep/ .ql-editor {
      min-height: 300px;
    }

处理封面图片

  • 页面布局(点击按钮,触发文件域的单击事件)

    <!-- 第四项:封面图片 -->
    <el-form-item label="封面图片">
      <div>
        <img src="../../../assets/images/cover.jpg" alt="" ref="imgRef" />
      </div>
      <input type="file" style="display: none" ref="fileRef" @change="chooseImg" />
      <el-button type="text" @click="$refs.fileRef.click()">+ 选择图片</el-button>
    </el-form-item>
  • 图片预览

    // 文件域图片改变,做预览
    chooseImg(e) {
      if (e.target.files.length > 0) {
        // 说明选择了图片
        // 得到文件对象
        const fileObj = e.target.files[0]
        this.imgUrl = URL.createObjectURL(fileObj)
        // 做好预览之后,将文件对象赋值到 添加的数据项中
        this.pubForm.cover_img = fileObj
      } else {
        // 说明没选图片
        this.pubForm.cover_img = ''
        this.imgUrl = ''
      }
    }
  • 把图片存储到数据项中(Ajax提交数据用)

    data() {
      return {
        // 下面的pubForm准备一会提交到服务器的
        pubForm: {
          title: '',
          cate_id: '',
          content: '',
          cover_img: ''
        },
        // 下面的imgUrl是做预览用的
        imgUrl: ''
      }
    }
  • 页面图片展示

    <img :src="imgUrl" alt="" ref="imgRef" v-if="imgUrl" class="cover_img" />
    <img src="../../../assets/images/cover.jpg" alt="" ref="imgRef" v-else />
  • 样式

    // 图片的样式
    .cover_img {
      width: 400px;
      height: 280px;
      // 下面的object-fit的意思是让图片不缩放,保持原比例
      object-fit: cover;
    }

设置文章状态

  • 准备数据项(添加文章的接口,需要state项)

    data() {
      return {
        // 下面的pubForm准备一会提交到服务器的
        pubForm: {
          title: '',
          cate_id: '',
          content: '',
          cover_img: '',
    +      state: '', // 这个表示文章状态
        }
      }
    }
  • 设置两个按钮(发布和存为草稿)

    <!-- 第五项:按钮 -->
    <el-form-item>
      <el-button type="primary">发布</el-button>
      <el-button type="info">存为草稿</el-button>
    </el-form-item>
  • 点击发布和存为草稿时,修改数据项

    <el-button type="primary" @click="publishArt('已发布')">发布</el-button>
    <el-button type="info" @click="publishArt('草稿')">存为草稿</el-button>
    // 发布文章的方法
    publishArt(s) {
      this.pubForm.state = s
    }

发布文章

发布之前,一定要用 Vue dev-tools 工具,检查一下数据项是否齐全,是否正确(是否符合接口要求)

  • 封装API方法

    // 添加文章
    export const addArticleAPI = data => request.post('/my/article/add', data)
  • “发布” 和 “存为草稿” 的单击事件处理函数中 publishArt

    • elementUI 验证通过(标题、选择分类)
    • 手动验证是否填写了内容
    • 手动验证是否选择了图片
    • 将数据项 pubForm 转为 FormData 格式(因为接口需要FormData格式)
    • 调用API接口,完成发布
    // 发布文章的方法
    publishArt(s) {
      // 设置文章的状态
      this.pubForm.state = s
      // 通过表单验证
      this.$refs.pubFormRef.validate(async valid => {
        if (valid) {
          // 验证通过,只能说明文章标题写了,选择了分类
          // 对于其他项,手动验证
          if (this.pubForm.content === '') return this.$message.error('请填写内容')
          if (this.pubForm.cover_img === '') return this.$message.error('请选择图片')
          if (this.pubForm.state === '') return this.$message.error('请选择状态')
          // console.log('验证通过')
          // 接口要的是FormData格式的数据,所以需要将 pubForm对象转成FormData格式
          const fd = new FormData()
          for (const key in this.pubForm) {
            fd.append(key, this.pubForm[key])
          }
          const { data: res } = await addArticleAPI(fd)
          if (res.code === 0) {
            this.$message.success(res.message) // 提示
            this.pubDialogVisible = false // 关闭弹出层
            // 更新页面数据(等做好文章列表后,在补充)
          } else {
            this.$message.error(res.message)
          }
        }
      })
    }

文章列表

如果没有完成添加文章。可以先去演示页面去添加几条。

渲染文章的列表数据

  1. 封装获取文章列表的API方法

    // 获取文章列表数据
    // 调用方法的时候,会传递请求参数过来 { pagenum: 1, pagesize: 2, cate_id: 1, state: 'xxx' }
    export const getArticleAPI = obj => request.get('/my/article/list', { params: obj })
  2. 在 data 中定义如下的数据:

    data() {
      return {
        // 文章的列表数据
        artList: [],
        // 总数据条数
        total: 0
        // ........其他略
      }
    }
  3. 在 methods 中声明 initArtList 函数,请求文章的列表数据:

    // 获取文章列表数据
    async initArtList() {
      const { data: res } = await getArticleAPI(this.q)
      // console.log(res)
      if (res.code === 0) {
        this.artList = res.data
        this.total = res.total
      }
    }
  4. 在 created 中调用步骤 3 封装的函数:

    created() {
      this.initCateList()
      // 请求文章的列表数据
      this.initArtList()
    },
  5. 在模板结构中,通过 el-table 组件渲染文章的表格数据:

    <!-- 文章表格区域 -->
    <el-table :data="artList" style="width: 100%;" border stripe>
      <el-table-column label="文章标题" prop="title"></el-table-column>
      <el-table-column label="分类" prop="cate_name"></el-table-column>
      <el-table-column label="发表时间" prop="pub_date"></el-table-column>
      <el-table-column label="状态" prop="state"></el-table-column>
      <el-table-column label="操作"></el-table-column>
    </el-table>
  6. (回顾发布文章)在发表文章成功后,调用步骤 3 封装的 initArtList 函数

格式化时间

  1. 安装格式化时间的第三方包 dayjs

    npm i dayjs -S
  2. 在项目入口文件 main.js 中导入并使用 dayjs,定义全局过滤器:

    // 导入dayjs
    import dayjs from 'dayjs'
    // 定义全局的过滤器函数
    // Vue.filter('过滤器名', fn)
    Vue.filter('dtformat', dt => {
      // dt 表示原本要输出的那个值。即 Wed Jan 19 2022 18:39:40 GMT+0800 (China Standard Time)
      return dayjs(dt).format('YYYY-MM-DD HH:mm:ss')
    })
  3. ArtList.vue 组件中,调用全局过滤器,对时间进行格式化:

    <el-table-column label="发表时间">
      <template v-slot="{ row }">
        {{ row.pub_date | dtformat }}
      </template>
    </el-table-column>

实现分页功能

  1. 直接从elementUI复制过来的完整的分页

    <el-pagination
      @size-change="handleSizeChange"     <!-- 当page-size改变的时候触发的函数-->
      @current-change="handleCurrentChange" <!-- 当前页切换的时候,比如从第3页到第4页,触发-->
      :current-page="2" <!-- 当前页,比如2。页面中的第2页是蓝色的,表示正在浏览第2页数据 -->
      :page-sizes="[10, 200, 300, 400]" <!-- 下拉框中,每页的条数 -->
      :page-size="10" <!-- 每一页多少条数据 -->
      layout="total, sizes, prev, pager, next, jumper" <!-- 自定义布局 -->
      :total="400" <!-- 数据总数 -->
    >
    </el-pagination>
  2. ArtList.vue 组件中使用 el-pagination 组件:

    <!-- 分页区域 -->
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="q.pagenum"
      :page-sizes="[2, 3, 5, 10]"
      :page-size="q.pagesize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
    >
    </el-pagination>
  3. 美化样式:

    .el-pagination {
      margin-top: 15px;
    }
  4. 声明 handleSizeChange 函数,监听 pageSize 的变化:

    // pageSize 发生了变化
    handleSizeChange(newSize) {
      // 为 pagesize 赋值
      this.q.pagesize = newSize
      // 默认展示第一页数据
      this.pagenum = 1
      // 重新发起请求
      this.initArtList()
    },
  5. 声明 handleCurrentChange 函数,监听页码值的变化:

    // 页码值发生了变化
    handleCurrentChange(newPage) {
      // 为页码值赋值
      this.q.pagenum = newPage
      // 重新发起请求
      this.initArtList()
    }

实现筛选功能

  1. 动态为筛选表单中的文章分类下拉菜单,渲染可选项列表:

    <el-form-item label="文章分类">
      <el-select v-model="q.cate_id" placeholder="请选择分类" size="small">
        <!-- 循环渲染可选项 -->
        <el-option v-for="item in cateList" :key="item.id" :label="item.cate_name" :value="item.id">
        </el-option>
      </el-select>
    </el-form-item>
  2. 当用户点击筛选按钮时,调用 initArtList 函数重新发起数据请求:

    <el-button type="primary" size="small" @click="initArtList">筛选</el-button>
  3. 当用户点击重置按钮时,调用 resetList 函数:

    <el-button type="info" size="small" @click="resetList">重置</el-button>
  4. 声明 resetList 函数如下:

    // 重置文章的列表数据
    resetList() {
      // 1. 重置查询参数对象
      this.q = {
        pagenum: 1,
        pagesize: 2,
        cate_id: '',
        state: ''
      }
      // 2. 重新发起请求
      this.initArtList()
    }

打包发布

直接打包发布

运行 npm run build 即可生成 dist 文件夹。这就是打包之后的结果。

移除 map 文件

项目根目录创建 Vue 的配置文件 vue.config.js

module.exports = {
  // 移除掉打包后生成的 xxx.map 文件
  productionSourceMap: false,
}

打包结果支持File协议

module.exports = {
  // 配置publicPath 为 '' 或者 './' 则打包后的结果,可以通过 File 协议直接预览。
  publicPath: '',
  // 移除掉打包后生成的 xxx.map 文件
  productionSourceMap: false,
}

生成打包报告

  1. 打开 package.json 配置文件,为 scripts 节点下的 build 命令添加 --report 参数:

    {
      "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build --report",
        "lint": "vue-cli-service lint"
      }
    }
  2. 重新运行打包的命令:

    npm run build
  3. 打包完成后,发现在 dist 目录下多了一个名为 report.html 的文件。在浏览器中打开此文件,会看到详细的打包报告。

基于 externals 配置 CDN 加速

  1. 未配置 externals 之前:
    1. 凡是 import 导入的第三方模块,在最终打包完成后,会被合并到 chunk-vendors.js
    2. 缺点:导致单个文件的体积过大
  2. 配置了 externals 之后:
    1. webpack 在进行打包时,会把 externals 节点下声明的第三方包排除在外
    2. 因此最终打包生成的 js 文件中,不会包含 externals 节点下的包
    3. 好处:优化了打包后项目的体积。

我们使用的CDN加速,只是减少了打包的体积,并不一定会加快网站的打开速度。

初步配置 externals 节点

  1. 在项目根目录下创建 vue.config.js 配置文件,在里面新增 configureWebpack 节点如下:

    module.exports = {
      // 省略其它代码...
    
      // 增强 vue-cli 的 webpack 配置项
      configureWebpack: {
        // 打包优化
        externals: {
          // import导包的包名: window全局的成员名称
          echarts: 'echarts',
        }
      }
    }
  2. /public/index.html 文件的 <body> 结束标签之前,新增如下的资源引用:

    <!DOCTYPE html>
    <html lang="">
    
    <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width,initial-scale=1.0">
      <link rel="icon" href="<%= BASE_URL %>favicon.ico">
      <title>
        <%= htmlWebpackPlugin.options.title %>
      </title>
    </head>
    
    <body>
      <noscript>
        <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
            Please enable it to continue.</strong>
      </noscript>
      <div id="app"></div>
      <!-- built files will be auto injected -->
      <script src="https://cdn.jsdelivr.net/npm/echarts@5.2.1/dist/echarts.min.js"></script>
    </body>
    
    </html>
  3. 重新运行 npm run build 命令,对比配置 externals 前后文件的体积变化。

完整的 externals 配置项

  1. vue.config.js 配置文件中,找到 configureWebpack 下的 externals,添加如下的配置项:

    module.exports = {
      // 省略其它代码...
      publicPath: './',
    
      // 增强 vue-cli 的 webpack 配置项
      configureWebpack: {
        // 打包优化
        externals: {
          // import导包的包名: window全局的成员名称
          echarts: 'echarts',
          vue: 'Vue',
          'vue-router': 'VueRouter',
          vuex: 'Vuex',
          axios: 'axios',
          dayjs: 'dayjs',
          'element-ui': 'ELEMENT',
          'vue-quill-editor': 'VueQuillEditor',
          'vuex-persistedstate': 'createPersistedState'
        }
      }
    }
    
  2. /public/index.html 文件的 <body> 结束标签之前,添加如下的 js 引用:

    <!-- built files will be auto injected -->
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.2.1/dist/echarts.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.2/dist/vue-router.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.21.3/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/dayjs@1.10.6/dayjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/index.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue-quill-editor@3.0.6/dist/vue-quill-editor.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuex-persistedstate@3.2.1/dist/vuex-persistedstate.umd.js"></script>
  3. main.js 中注释掉 element-ui 的样式和 quill 的样式:

    // 1. 导入 element-ui 组件库的样式
    // import 'element-ui/lib/theme-chalk/index.css'
    
    // 2. 导入 quill 的样式
    // import 'quill/dist/quill.core.css'
    // import 'quill/dist/quill.snow.css'
    // import 'quill/dist/quill.bubble.css'
  4. /public/index.html 文件的 <title></title> 标签之后,引入需要的 css 样式:

    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/theme-chalk/index.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.core.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.bubble.css">

配置路由懒加载(旧版的vue-cli才需要)

参考 vue-router 的官方文档,进行路由懒加载的配置

  1. 什么是路由懒加载:
    • 仅在需要的时候,加载路由对应的组件页面
  2. 好处:
    • 可以提高 index.html 首页的打开速度

配置路由懒加载的核心步骤:

  1. 运行如下的命令,安装 babel 插件:

    npm install --save-dev @babel/plugin-syntax-dynamic-import
  2. 修改项目根目录下的 babel.config.js 配置文件,新增 plugins 节点:

    module.exports = {
      presets: ['@vue/cli-plugin-babel/preset'],
      // 实现路由组件按需导入的 babel 插件
    + plugins: ['@babel/plugin-syntax-dynamic-import']
    }
  3. /src/router/index.js 模块中,基于 const 组件 = () => import('组件的存放路径') 语法,这个我们一开始就这样做的,无需修改

Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

简介

vue项目-大事件-bigevent 展开 收起
Vue 等 4 种语言
Apache-2.0
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
1
https://gitee.com/laotang1234/vue-cms.git
git@gitee.com:laotang1234/vue-cms.git
laotang1234
vue-cms
vue-cms
master

搜索帮助