Antd+Vue最详细的搭建流程(下篇之封装Axios、Mock模拟数据、Router基础路由配置和Vuex全局存储使用)

五、Vue中Axios过滤封装请求

Vue的基础环境的搭建安装请看上篇

1. 安装Axios

npm install axios

2. 使用Axios

(1)创建开发环境、生产环境、测试环境的配置文件

在根目录(项目文件夹)下新建三个文件
第一个文件(.env.development)开发环境配置文文件
# 开发环境配置
ENV = 'development'

# 开发环境
VUE_APP_BASE_API = '/development-api'

# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true
第二个文件(.env.production)开发环境配置文文件
# 生产环境配置
ENV = 'production'

# 生产环境
VUE_APP_BASE_API = '/production-api'
第三个文件(.env.staging)开发环境配置文文件
NODE_ENV = production

# 测试环境配置
ENV = 'staging'

# 若依管理系统/测试环境
VUE_APP_BASE_API = '/staging-api'

(2)创建request.js 过滤封装请求

在src文件夹下新建utils文件夹,在utils下新建啊request.js文件
目录如下:

在这里插入图片描述
request.js

import axios from "axios";
import { Message } from "ant-design-vue";

axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8";
// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: process.env.VUE_APP_BASE_API,
  // 超时
  timeout: 10000,
});
// request拦截器
service.interceptors.request.use(
  (config) => {
    // 是否需要设置 token
    return config;
  },
  (error) => {
    console.log(error);
    Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (success) => {
    if (success.status && success.status == 200 && success.data.status == 500) {
      Message.error({ message: success.data.msg });
      return;
    }
    if (success.data.msg) {
      Message.success({ message: success.data.msg });
    }
    return success.data;
  },
  (error) => {
    if (error.response.status == 504 || error.response.status == 404) {
      Message.error({ message: "服务器被吃了( ╯□╰ )" });
    } else if (error.response.status == 403) {
      Message.error({ message: "权限不足,请联系管理员" });
    } else if (error.response.status == 401) {
      Message.error({
        message: error.response.data.msg
          ? error.response.data.msg
          : "尚未登录,请登录",
      });
      location.href = "/";
    } else {
      if (error.response.data.msg) {
        Message.error({ message: error.response.data.msg });
      } else {
        Message.error({ message: "未知错误!" });
      }
    }
   return Promise.reject(error);
  }
);

export default service;

(3)测试一下,是否拦截成功

在src文件夹下新建api文件夹,在api文件夹下新建login.js
login.js

import request from "@/utils/request";

// 登录方法
export function login(username, password, code, uuid) {
  const data = {
    username,
    password,
    code,
    uuid,
  };
  return request({
    url: "/login",
    method: "post",
    data: data,
  });
}

修改之前的login.vue文件

<template>
  <a-form :form="form" class="login-form" @submit="handleSubmit">
    <a-form-item>
      <a-input
        v-decorator="[
          'username',
          {
            rules: [{ required: true, message: '请输入用户名!' }],
          },
        ]"
        placeholder="用户名"
      >
        <a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)" />
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-input
        v-decorator="[
          'password',
          {
            rules: [{ required: true, message: '请输入密码!' }],
          },
        ]"
        type="password"
        placeholder="密码"
      >
        <a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)" />
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-checkbox
        v-decorator="[
          'remember',
          {
            valuePropName: 'checked',
            initialValue: true,
          },
        ]"
      >记住我</a-checkbox>
      <a class="login-form-forgot" href>忘记密码</a>
      <a-button type="primary" html-type="submit" class="login-form-button">登录</a-button>
      <a href="/register">注册</a>
    </a-form-item>
  </a-form>
</template>

<script>
import { login } from "@/api/login";
export default {
  beforeCreate() {
    this.form = this.$form.createForm(this, { name: "normal_login" });
  },
  methods: {
    handleSubmit(e) {
      e.preventDefault();
      this.form.validateFields((err, values) => {
        if (!err) {
          this.loading = true;
          console.log("表单里输入的值: ", values);
          var { username, password } = values;
          login(username, password)
            .then((res) => {
              this.loading = false;
              this.$message.info("登陆成功!");
            })
            .catch((error) => {
              this.loading = false;
              this.$message.error("登陆失败!");
            });
        }
      });
    },
  },
};
</script>
<style>
.login-form {
  max-width: 300px;
  margin: auto !important;
}
.login-form-forgot {
  float: right;
}
.login-form-button {
  width: 100%;
}
</style>

测试成功
在这里插入图片描述

六、Vue中mockjs模拟数据

1.安装mockjs

npm install mockjs --save-dev

2.预备知识

(1)Mock主要方法

Mock.mock( rurl?, rtype?, template|function( options ) )
根据数据模板生成模拟数据
(1)属性 rurl 可选
表示需要拦截的 URL,可以是 URL 字符串或 URL 正则。例如 //domain/list.json/、’/domian/list.json’。
(2)属性 rtype 可选
表示需要拦截的 Ajax 请求类型。例如 GET、POST、PUT、DELETE 等。
(3)template 可选
表示数据模板,可以是对象或字符串。例如 { ‘data|1-10’:[{}] }、’@EMAIL’。
(4)function(options) 可选
表示用于生成响应数据的函数。
options 指向本次请求的 Ajax 选项集,含有 url、type 和 body 三个属性,参见 XMLHttpRequest 规范

Mock.setup( settings )
配置拦截 Ajax 请求时的行为。支持的配置项有:timeout
(1)属性settings 必选 配置项集合
(2)timeout 可选

//指定被拦截的 Ajax 请求的响应时间,单位是毫秒。值可以是正整数,例如 400,表示 400 毫秒 后才会返回响应内容
//也可以是横杠 '-' 风格的字符串,例如 '200-600',表示响应时间介于 200 和 600 毫秒之间。默认值是'10-100'
Mock.setup({
    timeout: 400
})
Mock.setup({
    timeout: '200-600'
})

(2)语法规范

(1)数据模板定义规范(Data Template Definition,DTD)
数据模板中的每个属性由 3 部分构成:属性名、生成规则、属性值

// 属性名   name
// 生成规则 rule
// 属性值   value
'name|rule': value

(2)数据占位符定义规范(Data Placeholder Definition,DPD)
占位符 只是在属性值字符串中占个位置,并不出现在最终的属性值中。
占位符 的格式为:

@占位符
@占位符(参数 [, 参数])

3.使用mockjs

(1)创建mock.js

在项目的根目录(与src同级)下,新建mock文件夹,并在mock文件夹下新建mock.js文件

// 引入mockjs
const Mock = require("mockjs");
// 获取 mock.Random 对象
const Random = Mock.Random;
//利用Random随机个一个用户名称和年龄
const userInfo={
    "name":Random.cname(),//随机生成一个常见的中文姓名
    "age":Random.integer(0,100),//随机生成0~100之间的数
    "nickName":"@CNAME",//随机生成一个常见的中文姓名(采用数据占位符定义规范写)
    "ageIternet|0-100":1//随机生成0~100之间的数(采用数据模板定义规范写)
}
// Mock.mock( url, post/get , 返回的数据);
Mock.mock("/development-api/login", "post", userInfo);

具体的mock规则可以参考:mock语法规范

(2)引入mock.js

//在mian.js 位置添加引入即可
// 引入mockjs
require("../mock/mock.js");

(3)测试一下,是否拦截并模拟成功

// 文件login.vue文件 中 方法修改如下:
	handleSubmit(e) {
      e.preventDefault();
      this.form.validateFields((err, values) => {
        if (!err) {
          this.loading = true;
          console.log("表单里输入的值: ", values);
          var { username, password } = values;
          login(username, password)
            .then((res) => {
              this.loading = false;
              let { name, age, nickName, ageIternet } = res;
              this.$message.info(
                `欢迎--${name}--登录!恭喜${age}岁,您的昵称为${nickName},网龄已经${ageIternet}个月`
              );
            })
            .catch((error) => {
              this.loading = false;
              this.$message.error("登陆失败!");
            });
        }
      });
    },

成功页面:
在这里插入图片描述

七、Vue中Router路由

1.安装Router

npm install vue-router

2.预备知识

(1)路由组件

官方内置的两个:和

<router-view/>组件是一个 functional 组件,渲染路径匹配到的视图组件
(1)属性名 name 类型: string 默认值: "default"
如果 <router-view>设置了名称,则会渲染对应的路由配置中 components 下的相应组件

<router-link/>组件支持用户在具有路由功能的应用中(点击) 导航
(1)属性 to 类型: string | Location required
表示目标路由的链接。当被点击后,内部会立刻把 to 的值传到 router.push(),所以这个值可以是一个字符串或者是描述目标位置的对象
(2)属性 replace 类型: boolean 默认值: false
设置 replace 属性的话,当点击时,会调用 router.replace() 而不是 router.push(),于是导航后不会留下 history 记录。

(2)动态路由匹配规则

如何动态路径参数?
一个“路径参数”使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params,可以在每个组件内使用
例如:
const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
  routes: [
    // 动态路径参数 以冒号开头
    { path: '/user/:id', component: User }
  ]
})

(3)路由传参

有三种模式:布尔模式、对象模式和函数模式

//(1)布尔模式
//如果 props 被设置为 true,route.params 将会被设置为组件属性
//也就是说动态路径参数在props设置为true时,被路由的页面可以接受这个值作为组件属性
//例如:
const User = {
  props: ['id'],
  template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User, props: true },
  ]
 })
//(2)对象模式
//如果 props 是一个对象,它会被按原样设置为组件属性,当 props 是静态的时候有用
//例如:
const router = new VueRouter({
  routes: [
    { path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } }
  ]
})
//(3)函数模式
//你可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等
//例如:
const router = new VueRouter({
  routes: [
    { path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
  ]
})

(4)HTML5 History 模式

vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id,也好看!
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面

3.使用Router

(1)创建首页Home.vue、Welcome.vue、NotFoundComponent.vue

//创建之前先把Home.vue和Welcome.vue中用到的Antd组件按需引入一下,顺带着把router.js引入一下,
//NotFoundComponent中图片可以下载我的源码,或者直接删掉
//修改后的main.js
import Vue from "vue";
import App from "./App.vue";
//按需引入Antd组件库
import {
  Button,
  Form,
  Input,
  Icon,
  Checkbox,
  message,
  Layout,
  Menu,
  Skeleton,
} from "ant-design-vue";
//引入Antd组件库CSS文件
import "ant-design-vue/dist/antd.css";
Vue.use(Button);
Vue.use(Form);
Vue.use(Input);
Vue.use(Icon);
Vue.use(Checkbox);
Vue.use(Layout);
Vue.use(Menu);
Vue.use(Skeleton);
//message 是全局提示它的触发是方法调用的,所以直接绑定在Vue的原型上
//这样无论何时用Vue.message都可以弹出全局提示
Vue.prototype.$message = message;
Vue.config.productionTip = false;

// 引入mockjs
require("../mock/mock.js");

//引入router
import router from "./router";

new Vue({
  router,
  render: (h) => h(App),
  mounted: function() {
    this.$message.info("你好");
  },
}).$mount("#app");
//Welcome.vue 欢迎页面
<template>
  <div>
    欢迎--{{ name }}
    <a-skeleton avatar :paragraph="{ rows: 4 }" />
    <a-skeleton avatar :paragraph="{ rows: 4 }" />
    <a-skeleton avatar :paragraph="{ rows: 4 }" />
    <a-skeleton avatar :paragraph="{ rows: 4 }" />
  </div>
</template>
<script>
export default {
  name: "Welcome",
  props: {
    name: String,
  },
};
</script>
//Home.vue 主页页面
<template>
  <a-layout id="components-layout-demo-custom-trigger">
    <a-layout-sider v-model="collapsed" :trigger="null" collapsible>
      <div class="logo" />
      <a-menu theme="dark" mode="inline" :default-selected-keys="['0']">
        <a-menu-item key="0">
          <router-link to="/home">
            <a-icon type="user" />
            <span>不传参</span>
          </router-link>
        </a-menu-item>
        <a-menu-item key="1">
          <router-link to="/home/1">
            <a-icon type="user" />
            <span>静态传参</span>
          </router-link>
        </a-menu-item>
        <a-menu-item key="2">
          <router-link to="/home/2/我是动态参数">
            <a-icon type="video-camera" />
            <span>动态传参</span>
          </router-link>
        </a-menu-item>
        <a-menu-item key="3">
          <router-link to="/home/3/我是动态参数">
            <a-icon type="upload" />
            <span>混合传参</span>
          </router-link>
        </a-menu-item>
        <a-menu-item key="4">
          <router-link to="/home/随便填">
            <a-icon type="upload" />
            <span>无页面</span>
          </router-link>
        </a-menu-item>
      </a-menu>
    </a-layout-sider>
    <a-layout>
      <a-layout-header style="background: #fff; padding: 0">
        <a-icon
          class="trigger"
          :type="collapsed ? 'menu-unfold' : 'menu-fold'"
          @click="() => (collapsed = !collapsed)"
        />
      </a-layout-header>
      <a-layout-content
        :style="{
          margin: '24px 16px',
          padding: '24px',
          background: '#fff',
          minHeight: '280px',
        }"
      >
        <router-view></router-view>
      </a-layout-content>
    </a-layout>
  </a-layout>
</template>
<script>
export default {
  data() {
    return {
      collapsed: false,
    };
  },
};
</script>
<style>
#components-layout-demo-custom-trigger {
  height: 100%;
}
#components-layout-demo-custom-trigger .trigger {
  font-size: 18px;
  line-height: 64px;
  padding: 0 24px;
  cursor: pointer;
  transition: color 0.3s;
}

#components-layout-demo-custom-trigger .trigger:hover {
  color: #1890ff;
}

#components-layout-demo-custom-trigger .logo {
  height: 32px;
  background: rgba(255, 255, 255, 0.2);
  margin: 16px;
}
</style>

//NotFoundComponent.vue 404页面

<template>
  <div class="wscn-http404-container">
    <div class="wscn-http404">
      <div class="pic-404">
        <img
          class="pic-404__parent"
          src="@/assets/404_images/404.png"
          alt="404"
        />
        <img
          class="pic-404__child left"
          src="@/assets/404_images/404_cloud.png"
          alt="404"
        />
        <img
          class="pic-404__child mid"
          src="@/assets/404_images/404_cloud.png"
          alt="404"
        />
        <img
          class="pic-404__child right"
          src="@/assets/404_images/404_cloud.png"
          alt="404"
        />
      </div>
      <div class="bullshit">
        <div class="bullshit__oops">404错误!</div>
        <div class="bullshit__headline">
          {{ message }}
        </div>
        <div class="bullshit__info">
          对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
        </div>
        <router-link to="/home" class="bullshit__return-home">
          返回首页
        </router-link>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Page404",
  computed: {
    message() {
      return "找不到网页!";
    },
  },
};
</script>
<style scoped>
.wscn-http404-container {
  transform: translate(-50%, -50%);
  position: absolute;
  top: 40%;
  left: 50%;
}

.wscn-http404 {
  position: relative;
  width: 1200px;
  padding: 0 50px;
  overflow: hidden;
}
.wscn-http404 .pic-404 {
  position: relative;
  float: left;
  width: 600px;
  overflow: hidden;
}
.wscn-http404 .pic-404__parent {
  width: 100%;
}
.wscn-http404 .pic-404__child {
  position: absolute;
}
.wscn-http404 .pic-404__child.left {
  width: 80px;
  top: 17px;
  left: 220px;
  opacity: 0;
  animation-name: cloudLeft;
  animation-duration: 2s;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  animation-delay: 1s;
}
.wscn-http404 .pic-404__child.mid {
  width: 46px;
  top: 10px;
  left: 420px;
  opacity: 0;
  animation-name: cloudMid;
  animation-duration: 2s;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  animation-delay: 1.2s;
}
.wscn-http404 .pic-404__child.right {
  width: 62px;
  top: 100px;
  left: 500px;
  opacity: 0;
  animation-name: cloudRight;
  animation-duration: 2s;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  animation-delay: 1s;
}
@keyframes cloudLeft {
  0% {
    top: 17px;
    left: 220px;
    opacity: 0;
  }
  20% {
    top: 33px;
    left: 188px;
    opacity: 1;
  }
  80% {
    top: 81px;
    left: 92px;
    opacity: 1;
  }
  100% {
    top: 97px;
    left: 60px;
    opacity: 0;
  }
}
@keyframes cloudMid {
  0% {
    top: 10px;
    left: 420px;
    opacity: 0;
  }
  20% {
    top: 40px;
    left: 360px;
    opacity: 1;
  }
  70% {
    top: 130px;
    left: 180px;
    opacity: 1;
  }
  100% {
    top: 160px;
    left: 120px;
    opacity: 0;
  }
}
@keyframes cloudRight {
  0% {
    top: 100px;
    left: 500px;
    opacity: 0;
  }
  20% {
    top: 120px;
    left: 460px;
    opacity: 1;
  }
  80% {
    top: 180px;
    left: 340px;
    opacity: 1;
  }
  100% {
    top: 200px;
    left: 300px;
    opacity: 0;
  }
}
.wscn-http404 .bullshit {
  position: relative;
  float: left;
  width: 300px;
  padding: 30px 0;
  overflow: hidden;
}
.wscn-http404 .bullshit__oops {
  font-size: 32px;
  font-weight: bold;
  line-height: 40px;
  color: #1482f0;
  opacity: 0;
  margin-bottom: 20px;
  animation-name: slideUp;
  animation-duration: 0.5s;
  animation-fill-mode: forwards;
}
.wscn-http404 .bullshit__headline {
  font-size: 20px;
  line-height: 24px;
  color: #222;
  font-weight: bold;
  opacity: 0;
  margin-bottom: 10px;
  animation-name: slideUp;
  animation-duration: 0.5s;
  animation-delay: 0.1s;
  animation-fill-mode: forwards;
}
.wscn-http404 .bullshit__info {
  font-size: 13px;
  line-height: 21px;
  color: grey;
  opacity: 0;
  margin-bottom: 30px;
  animation-name: slideUp;
  animation-duration: 0.5s;
  animation-delay: 0.2s;
  animation-fill-mode: forwards;
}
.wscn-http404 .bullshit__return-home {
  display: block;
  float: left;
  width: 110px;
  height: 36px;
  background: #1482f0;
  border-radius: 100px;
  text-align: center;
  color: #ffffff;
  opacity: 0;
  font-size: 14px;
  line-height: 36px;
  cursor: pointer;
  animation-name: slideUp;
  animation-duration: 0.5s;
  animation-delay: 0.3s;
  animation-fill-mode: forwards;
}
@keyframes slideUp {
  0% {
    transform: translateY(60px);
    opacity: 0;
  }
  100% {
    transform: translateY(0);
    opacity: 1;
  }
}
</style>

(2)修改App.vue和Login.vue

//修改App.vue后
<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script>
import Login from "./pages/Login";

export default {
  name: "App",
};
</script>

<style>
#app {
  height: 100vh;
}
</style>
<template>
  <a-form :form="form" class="login-form" @submit="handleSubmit">
    <a-form-item>
      <img alt="Vue logo" src="@/assets/logo.png" />
    </a-form-item>
    <a-form-item>
      <a-input
        v-decorator="[
          'username',
          {
            rules: [{ required: true, message: '请输入用户名!' }],
          },
        ]"
        placeholder="用户名"
      >
        <a-icon slot="prefix" type="user" style="color: rgba(0, 0, 0, 0.25)" />
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-input
        v-decorator="[
          'password',
          {
            rules: [{ required: true, message: '请输入密码!' }],
          },
        ]"
        type="password"
        placeholder="密码"
      >
        <a-icon slot="prefix" type="lock" style="color: rgba(0, 0, 0, 0.25)" />
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-checkbox
        v-decorator="[
          'remember',
          {
            valuePropName: 'checked',
            initialValue: true,
          },
        ]"
        >记住我</a-checkbox
      >
      <a class="login-form-forgot" href>忘记密码</a>
      <a-button type="primary" html-type="submit" class="login-form-button"
        >登录</a-button
      >
      <a href="/register">注册</a>
    </a-form-item>
  </a-form>
</template>

<script>
import { login } from "@/api/login";
export default {
  beforeCreate() {
    this.form = this.$form.createForm(this, { name: "normal_login" });
  },
  methods: {
    handleSubmit(e) {
      e.preventDefault();
      this.form.validateFields((err, values) => {
        if (!err) {
          this.loading = true;
          console.log("表单里输入的值: ", values);
          var { username, password } = values;
          login(username, password)
            .then((res) => {
              this.loading = false;
              let { name, age, nickName, ageIternet } = res;
              this.$message.info(
                `欢迎--${name}--登录!恭喜${age}岁,您的昵称为${nickName},网龄已经${ageIternet}个月`
              );
              this.$router.push({ path: "home" });
            })
            .catch((error) => {
              this.loading = false;
              this.$message.error("登陆失败!");
            });
        }
      });
    },
  },
};
</script>
<style>
.login-form {
  max-width: 300px;
  margin: auto !important;
}
.login-form-forgot {
  float: right;
}
.login-form-button {
  width: 100%;
}
</style>

(3)创建router.js

//在src文件夹下面,新建router.js文件
import Vue from "vue";
import VueRouter from "vue-router";
import Login from "./pages/Login";
import Home from "./pages/Home";
import NotFoundComponent from "./pages/NotFoundComponent";
import Welcome from "./pages/Welcome";

Vue.use(VueRouter);
export default new VueRouter({
  mode: "history", //默认hash模式(url比较丑),采用history模式
  routes: [
    { path: "/", component: Login },
    {
      path: "/home",
      component: Home,
      children: [
        {
          path: "",
          component: Welcome, //不使用路由传参数
        },
        {
          path: "1",
          component: Welcome,
          props: { name: "使用静态参数" }, //使用路由静态参数
        },
        {
          path: "2/:name",
          component: Welcome,
          props: true, //使用路由动态传递参数
        },
        {
          path: "3/:name",
          component: Welcome,
          props: (route) => {
            //使用路由动态态值与基于路由的值结合传递参数
            const now = new Date();
            return {
              name: `动态态值与基于路由的值结合,--${
                route.params.name
              }--${now.getSeconds()}秒`,
            };
          },
        },
      ],
    },
    { path: "*", component: NotFoundComponent }, //页面404
  ],
});

(4)运行测试路由

先登录后跳转到首页
在这里插入图片描述
无参数路由跳转
在这里插入图片描述
静态参数页面(对象模式)
在这里插入图片描述
动态参数页面(布尔模式)
在这里插入图片描述
混合参数(函数模式)
在这里插入图片描述
当页面没有时,路由统一向NotFoundComponent.vue页面
在这里插入图片描述
在这里插入图片描述

八、Vue中Vuex全局存储

1.安装Vuex

npm install vuex --save

2.预备知识

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化

//举个栗子 
//store.js文件
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})
//main.js文件
import store from "@/store";
new Vue({
  el: '#app',
  store: store,
})
//test.js 使用方法
import store from "@/store";
console.log(store.state.count); // -> 0
store.commit('increment');
console.log(store.state.count); // -> 1

(1)State(单一状态树)

存储在 Vuex 中的数据和 Vue 实例中的 data 遵循相同的规则,例如状态对象必须是纯粹 (plain) 的

  1. Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态
import store from "@/store";//直接将store实例引入使用,在此之前要把store配置好
// 创建一个 Counter 组件
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return store.state.count
    }
  }
}
  1. Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex))
import store from "@/store";
const app = new Vue({
  el: '#app',
  // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})
  1. 可以从组件的方法提交一个变更
methods: {
  increment() {
    this.$store.commit('increment')
    console.log(this.$store.state.count)
  }
}

(2)Getters(获取器)

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

//有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:
computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}
//如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想
  1. Getter 接受 state 作为其第一个参数
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
  1. 通过属性访问
import store from "@/store";
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
//或者在vue内部
computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}
//也可以接受第二个参数
getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1
  1. 通过方法访问
getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
  1. mapGetters 辅助函数
    mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}
//如果你想将一个 getter 属性另取一个名字,使用对象形式:
...mapGetters({
  // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})

(3)Mutations(同步函数)

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation

  1. Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)
const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {//这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
      // 变更状态
      state.count++
    }
  }
})
//使用方法
store.commit('increment')
  1. 提交载荷(Payload)
    可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload)
mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})
  1. 对象风格的提交方式
    提交 mutation 的另一种方式是直接使用包含 type 属性的对象
store.commit({
  type: 'increment',
  amount: 10
})
  1. Mutation 需遵守 Vue 的响应规则
    最好提前在你的 store 中初始化好所有所需属性
    当需要在对象上添加新属性时,你应该使用 Vue.set(obj, ‘newProp’, 123), 或者以新对象替换老对象
    例如,利用对象展开运算符我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }

(4)Actions(可以是异步函数)

Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作
1.举个栗子

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
    //Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,
    //因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters
      context.commit('increment')
    }
  }
})
//实战中推荐使用结构赋值来简化代码
actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

2.分发Action

//Action 通过 store.dispatch 方法触发
//为什么要出现Action??直接用Mutations不就行了
//实际是Mutations必须是同步的,无法异步,但在业务中异步是不可避免的
store.dispatch('increment')

Actions 支持同样的载荷方式和对象方式进行分发

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})
  1. 在组件中分发 Action
import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}
  1. 组合Action
    store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise
    并且 store.dispatch 仍旧返回 Promise
actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

现在你可以

store.dispatch('actionA').then(() => {
  // ...
})
//在另外一个 action 中也可以
actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

利用 async / await

// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

(5)Modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

  1. 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
  1. 模块的局部状态
    对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState:

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

3 使用Vuex

这个小例子:
一共两个存储模块:用户信息存储和系统设置存储
(1)在用户登陆后,将用户信息存入store中,在主页把用户信息显示出来,并且提供修改功能
(2)项目store初始化将setting.js系统配置文件读出来,两个:系统标题和是否显示logo,存入store中,在首页中根据store中存储信息,显示布局,并提供修改系统配置文件功能

(1)创建目录结构

第一步:在src文件夹下新建store文件夹
第二步:在store文件夹下新建modules文件夹
第三部:在src文件夹下新建types文件夹
在这里插入图片描述

(2)创建文件

第一步:在src文件夹下新建文件setting.js

export default {
  /**
   * 应用名称
   */
  title: "XXX系统",
  /**
   * 是否显示logo
   */
  logo: true,
};

第二步:在types下新建mutaion-types.js

export const SET_TITLE = "SET_TITLE";
export const SET_LOGO = "SET_LOGO";
export const SET_USERINFO = "SET_USERINFO";

第三步:在store文件夹modules文件下新建setting.js文件、user.js文件

//setting.js
import settings from "@/settings";
import { SET_TITLE, SET_LOGO } from "@/types/mutation-types";
const { title, logo } = settings;
const state = {
  title,
  logo,
};
const mutations = {
  [SET_TITLE]: function(state, payload) {
    state.title = payload.title;
  },
  [SET_LOGO]: function(state, payload) {
    const { logo } = payload;
    //使用结构赋值简化代码
    state.logo = logo;
  },
};
const actions = {
  [SET_TITLE]: function({ commit }, payload) {
    commit(SET_TITLE, {
      //以载荷形式提交
      title: payload.title,
    });
  },
  [SET_LOGO]: function({ commit }, payload) {
    //使用结构赋值简化代码
    commit({ type: SET_LOGO, logo: payload.logo });
  },
};
export default {
  namespaced: true,
  state,
  mutations,
  actions,
};
//user.js
import { SET_USERINFO } from "@/types/mutation-types";
window.console.log(SET_USERINFO);
const state = {
  name: "",
  age: "",
  nickname: "",
  ageInternet: "",
};
const mutations = {
  [SET_USERINFO]: function(state, payload) {
    const { name, age, nickname, ageInternet } = payload;
    //使用结构赋值简化代码
    state.name = name;
    state.age = age;
    state.nickname = nickname;
    state.ageInternet = ageInternet;
  },
};

const actions = {
  [SET_USERINFO]: function({ commit }, payload) {
    commit(SET_USERINFO, {
      //以载荷形式提交
      name: payload.name,
      age: payload.age,
      nickname: payload.nickname,
      ageInternet: payload.ageInternet,
    });
  },
};
export default {
  namespaced: true,
  state,
  mutations,
  actions,
};

第四步:在store文件夹下新建getter.js文件

const getters = {
  title: (state) => state.settings.title,
  logo: (state) => state.settings.logo,
  name: (state) => state.user.name,
  nickname: (state) => state.user.nickname,
  age: (state) => state.user.age,
  ageInternet: (state) => state.user.ageInternet,
};
export default getters;

第五步:在store文件夹下新建index.js文件

import Vue from "vue";
import Vuex from "vuex";
import settings from "./modules/settings";
import user from "./modules/user";
import getters from "./getters";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    settings,
    user,
  },
  getters,
});

export default store;

(3)修改文件

第一步:在main.js中引入store,并添加注册Avatar组件(首页会用到)

import Vue from "vue";
import App from "./App.vue";
//按需引入Antd组件库
import {
  Button,
  Form,
  Input,
  Icon,
  Checkbox,
  message,
  Layout,
  Menu,
  Skeleton,
  Avatar,
} from "ant-design-vue";
//引入Antd组件库CSS文件
import "ant-design-vue/dist/antd.css";
Vue.use(Button);
Vue.use(Form);
Vue.use(Input);
Vue.use(Icon);
Vue.use(Checkbox);
Vue.use(Layout);
Vue.use(Menu);
Vue.use(Skeleton);
Vue.use(Avatar);
//message 是全局提示它的触发是方法调用的,所以直接绑定在Vue的原型上
//这样无论何时用Vue.message都可以弹出全局提示
Vue.prototype.$message = message;
Vue.config.productionTip = false;

// 引入mockjs
require("../mock/mock.js");

//引入router
import router from "./router";

//引入store
import store from "./store";
new Vue({
  router,
  store,
  render: (h) => h(App),
  mounted: function() {
    this.$message.info("你好");
  },
}).$mount("#app");

第二步:修改login.vue文件(登陆成功后,把用户信息存入store中)

<template>
  <a-form :form="form" class="login-form" @submit="handleSubmit">
    <a-form-item>
      <img alt="Vue logo" src="@/assets/logo.png" />
    </a-form-item>
    <a-form-item>
      <a-input
        v-decorator="[
          'username',
          {
            rules: [{ required: true, message: '请输入用户名!' }],
          },
        ]"
        placeholder="用户名"
      >
        <a-icon slot="prefix" type="user" style="color: rgba(0, 0, 0, 0.25)" />
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-input
        v-decorator="[
          'password',
          {
            rules: [{ required: true, message: '请输入密码!' }],
          },
        ]"
        type="password"
        placeholder="密码"
      >
        <a-icon slot="prefix" type="lock" style="color: rgba(0, 0, 0, 0.25)" />
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-checkbox
        v-decorator="[
          'remember',
          {
            valuePropName: 'checked',
            initialValue: true,
          },
        ]"
        >记住我</a-checkbox
      >
      <a class="login-form-forgot" href>忘记密码</a>
      <a-button type="primary" html-type="submit" class="login-form-button"
        >登录</a-button
      >
      <a href="/register">注册</a>
    </a-form-item>
  </a-form>
</template>

<script>
import { login } from "@/api/login";
import { SET_USERINFO } from "@/types/mutation-types";
export default {
  beforeCreate() {
    this.form = this.$form.createForm(this, { name: "normal_login" });
  },
  methods: {
    handleSubmit(e) {
      e.preventDefault();
      this.form.validateFields((err, values) => {
        if (!err) {
          this.loading = true;
          console.log("表单里输入的值: ", values);
          var { username, password } = values;
          login(username, password)
            .then((res) => {
              this.loading = false;
              const { name, age, nickname, ageInternet } = res;
              this.$message.info(
                `欢迎--${name}--登录!恭喜${age}岁,您的昵称为${nickname},网龄已经${ageInternet}个月`
              );
              window.console.log(SET_USERINFO);
              this.$store.dispatch("user/"+SET_USERINFO, {
                name,
                age,
                nickname,
                ageInternet,
              });
              this.$router.push({ path: "home" });
            })
            .catch((error) => {
              this.loading = false;
              this.$message.error("登陆失败!");
            });
        }
      });
    },
  },
};
</script>
<style>
.login-form {
  max-width: 300px;
  margin: auto !important;
}
.login-form-forgot {
  float: right;
}
.login-form-button {
  width: 100%;
}
</style>

第三步:修改home.vue文件(将存在store中用户信息显示出来,添加修改用户信息按钮,系统配置显示出来,修改系统配置按钮)

<template>
  <a-layout id="components-layout-demo-custom-trigger">
    <a-layout-sider v-model="collapsed" :trigger="null" collapsible>
      <div v-if="logo" class="logo">
        <a-avatar src="@/asserts/logo.png" />{{ title }}
      </div>
      <a-menu theme="dark" mode="inline" :default-selected-keys="['0']">
        <a-menu-item key="0">
          <router-link to="/home">
            <a-icon type="user" />
            <span>不传参</span>
          </router-link>
        </a-menu-item>
        <a-menu-item key="1">
          <router-link to="/home/1">
            <a-icon type="user" />
            <span>静态传参</span>
          </router-link>
        </a-menu-item>
        <a-menu-item key="2">
          <router-link to="/home/2/我是动态参数">
            <a-icon type="video-camera" />
            <span>动态传参</span>
          </router-link>
        </a-menu-item>
        <a-menu-item key="3">
          <router-link to="/home/3/我是动态参数">
            <a-icon type="upload" />
            <span>混合传参</span>
          </router-link>
        </a-menu-item>
        <a-menu-item key="4">
          <router-link to="/home/随便填">
            <a-icon type="upload" />
            <span>无页面</span>
          </router-link>
        </a-menu-item>
      </a-menu>
    </a-layout-sider>
    <a-layout>
      <a-layout-header style="background: #fff; padding: 0">
        <a-icon
          class="trigger"
          :type="collapsed ? 'menu-unfold' : 'menu-fold'"
          @click="() => (collapsed = !collapsed)"
        />
        欢迎--{{ name }}--登录!恭喜{{ age }}岁,您的昵称为{{
          nickname
        }},网龄已经{{ ageInternet }}个月
        <a-button
          type="primary"
          @click="
            () => {
              setUserInfo();
            }
          "
          >修改用户信息</a-button
        >
        <a-button
          @click="
            () => {
              setTitle();
            }
          "
          >修改系统标题</a-button
        >
        <a-button
          @click="
            () => {
              setLogo();
            }
          "
          >影藏图标</a-button
        >
      </a-layout-header>
      <a-layout-content
        :style="{
          margin: '24px 16px',
          padding: '24px',
          background: '#fff',
          minHeight: '280px',
        }"
      >
        <router-view></router-view>
      </a-layout-content>
    </a-layout>
  </a-layout>
</template>
<script>
import { mapGetters, mapState, mapActions, mapMutations } from "vuex";
import { SET_TITLE, SET_LOGO, SET_USERINFO } from "@/types/mutation-types";
export default {
  data() {
    return {
      collapsed: false,
    };
  },
  // computed: {
  //   ...mapGetters(["logo", "title", "name", "nickname", "age", "ageInternet"]),
  // },
  computed: {
    //...mapState(["settings"]),//这样可以直接把模块里的setting所有state属性变成对象形式绑定在实例上
    ...mapGetters(["logo"]), //采用Getter获取状态
    ...mapState("settings", ["title"]), //采用mapState获取状态
    ...mapState("user", ["name", "nickname", "age", "ageInternet"]),
  },
  methods: {
    ...mapActions("user", [SET_USERINFO]), //采用mapActions获取Action方法
    ...mapMutations("settings", [SET_LOGO]), //采用mapMutations获取Mutations方法
    setTitle: function () {
      this.$store.dispatch("settings/" + SET_TITLE, { title: "设置系统" }); //直接store实例调用
    },
    setLogo: function () {
      this[SET_LOGO]({ logo: !this.logo });
    },
    setUserInfo: function () {
      this[SET_USERINFO]({
        name: "设置名称",
        age: "55",
        nickname: "设置名称",
        ageInternet: "44",
      });
    },
  },
};
</script>
<style>
#components-layout-demo-custom-trigger {
  height: 100%;
}
#components-layout-demo-custom-trigger .trigger {
  font-size: 18px;
  line-height: 64px;
  padding: 0 24px;
  cursor: pointer;
  transition: color 0.3s;
}

#components-layout-demo-custom-trigger .trigger:hover {
  color: #1890ff;
}

#components-layout-demo-custom-trigger .logo {
  height: 32px;
  background: rgba(255, 255, 255, 0.2);
  margin: 16px;
}
</style>

(4)运行测试

登陆
在这里插入图片描述
登录用户信息
在这里插入图片描述
修改用户信息
在这里插入图片描述
修改系统标题
在这里插入图片描述
显示影藏logo
在这里插入图片描述
源码下载

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
搭建一个基于Vite、Ant Design和Vue 3的项目,你可以按照以下步骤进行操作: 1. 首先,你需要安装Vite。你可以通过运行以下命令来安装最新版本的Vite: ``` npm init vite@latest my-project cd my-project npm install ``` 这将创建一个新的Vite项目,并安装所有必要的依赖项。 2. 接下来,你需要安装Ant Design Vue。运行以下命令来安装Ant Design Vue库: ``` npm install ant-design-vue@next ``` 这将安装最新版本的Ant Design Vue,并将其添加到你的项目中。 3. 配置路由。你可以通过以下步骤来配置Vue Router: - 首先,运行以下命令来安装Vue Router: ``` npm install vue-router@next ``` - 在你的项目的src目录下创建一个名为"router"的文件夹。 - 在"router"文件夹中创建一个名为"index.js"的文件,并配置你的路由信息。 - 最后,在项目的"main.js"文件中引入并使用Vue Router。 以上是使用Vite、Ant Design VueVue 3搭建项目的基本步骤。你可以根据需要进一步添加其他功能和组件。祝你搭建项目成功!<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [vue3+vite+antd 后台管理系统基础模板](https://download.csdn.net/download/yehaocheng520/87420798)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [vite + vue3 + Antd 搭建后台管理系统](https://blog.csdn.net/m0_58094704/article/details/127850749)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值