【VUE】demo02-VUE后台管理系统-axios接口测试+router动态路由

目录

1.5加入axios

1.7修改vue-router为动态路由并修改sidebar为动态的导航栏


上篇文章:【VUE】demo01-VUE做后台管理系统页面实例-创建基本环境+页面布局

工具:Visual Studio Code + Vue + Vue cil2 + Vuex + Vue Router + ElementUI + axios 

项目代码地址:macrozheng_mall学习: 学习macrozheng的mall项目,进行学习记录与代码拆解 - Gitee.com

1.5加入axios

我们先尝试加入接口调用的工具,通过登录接口来测试 axios 是否成功。(mall 项目的登录接口,是通过调用 vuex 操作的,但我们还没有加入vuex ,因为暂时还用不到,vuex 比较复杂。其实是一样哒,后续会加入的!)

注意:mall 的后端项目是使用的 token 来验证用户信息。也就是调用接口时,如果不是公开的接口需要提供 token 值的。

由于 axios 是一个工具,一般来说就像我们使用 Ajax 一样,调用一个方法就行,axios 也给我们提供了封装接口得到方法,一般就需要一个创建实例的文件,然后就是封装接口的js文件。

首先创建在src下一个创建 axios 实例文件 util/request.js

import axios from 'axios'
import { Message, MessageBox } from 'element-ui'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 15000 // 请求超时时间
})

// request请求拦截器,每次请求接口前都会执行这个
service.interceptors.request.use(config => {
  //可以在这里设置请求头等内容
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

// respone相应拦截器
service.interceptors.response.use(
  response => {
  //这里是获取返回值,例如mall系统的后端返回内容是 code,data,message
  //所以我们拿到code,判断是否等于200,不是就走接口错误的处理方式
    const res = response.data
    if (res.code !== 200) { //处理错误的方式
      return Promise.reject('error')
    } else {//处理正确的方式
      return response.data
    }
  },
  error => {
    console.log('err' + error)// for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 3 * 1000
    })
    return Promise.reject(error)
  }
)

//输出对象
export default service

这里需要注意 axios 实例中的 baseURL ,我们可以在这里写死,也可以在项目的配置文件中设置,然后获取。

baseURL: process.env.BASE_API

“process.env.BASE_API”获取的就是 config/dev.env.js 文件中的 key= BASE_API 的 value 数据.

所以我们打开 config/dev.env.js 文件,添加一个 k:v 。

// --------------------config/dev.env.js---------------------
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
  //记住这里必须加上 // 或者加上 http://,否则会被认为是没有IP端口协议的,会自动带上当前前端路径,
  //就像这样 :http://localhost:8080/139.196.220.24:80/***/***
  BASE_API: '"http://admin-api.macrozheng.com"' //就是这里
})

注意,我们这里 BASE_API 是 mall 系统提供的测试域名和接口,这样我们就不用在本地部署一遍后端程序了(后端项目学习也会有笔记记录),感谢 macrozheng !

axios 实例准备完成!!接下来我们需要进行一下测试,保证 axios 添加成功。因为很多接口都有访问权限,也为了直观,我们从登录接口添加 axios 调用接口的封装 js 文件。

首先,我们在 src 文件夹中创建一个文件 api/login.js ,之后的接口调用文件都在 src/api 文件夹中。

//------api/login.js--------------------------------
//引入我们写的 axios 
import request from '@/utils/request'

export function login(username, password) {
  return request({
    url: '/admin/login',
    method: 'post',
    //data是添加到请求体(body)中的, 用于post请求。
    data: {
      username,
      password
    }
  })
}

export function getInfo() {
  return request({
    url: '/admin/info',
    method: 'get',
  })
}

export function logout() {
  return request({
    url: '/admin/logout',
    method: 'post'
  })
}

export function fetchList(params) {
  return request({
    url: '/admin/list',
    method: 'get',
    //params是添加到url的请求字符串中的,用于get请求
    params: params
  })
}

 之后我们调用 login 相关的接口时,直接引入这个 login.js 中暴露出去的方法就可以啦!

配置都完成啦,接下来进行测试,首先在 view 文件夹添加一个登陆页面 login/index.vue  

//------login/index.vue 有删减----------------
<template>
  <div>
    <el-card class="login-form-layout">
      <el-form autoComplete="on"
               :model="loginForm"
               :rules="loginRules"
               ref="loginForm"
               label-position="left">
        <div style="text-align: center">
          <svg-icon icon-class="login-mall" style="width: 56px;height: 56px;color: #409EFF"></svg-icon>
        </div>
        <h2 class="login-title color-main">mall-admin-web</h2>
        <el-form-item prop="username">
          <el-input name="username"
                    type="text"
                    v-model="loginForm.username"
                    autoComplete="on"
                    placeholder="请输入用户名">
          <span slot="prefix">
            <svg-icon icon-class="user" class="color-main"></svg-icon>
          </span>
          </el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input name="password"
                    :type="pwdType"
                    @keyup.enter.native="handleLogin"
                    v-model="loginForm.password"
                    autoComplete="on"
                    placeholder="请输入密码">
          <span slot="prefix">
            <svg-icon icon-class="password" class="color-main"></svg-icon>
          </span>
            <span slot="suffix" @click="showPwd">
            <svg-icon icon-class="eye" class="color-main"></svg-icon>
          </span>
          </el-input>
        </el-form-item>
        <el-form-item style="margin-bottom: 60px;text-align: center">
          <el-button style="width: 45%" type="primary" :loading="loading" @click.native.prevent="handleLogin">
            登录
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script>
import { isvalidUsername } from "@/utils/validate";
import { login, logout, getInfo } from "@/api/login";
import login_center_bg from "@/assets/images/login_center_bg.png";


export default {
  name: "login",
  data() {
    const validateUsername = (rule, value, callback) => {
      if (!isvalidUsername(value)) {
        callback(new Error("请输入正确的用户名"));
      } else {
        callback();
      }
    };
    const validatePass = (rule, value, callback) => {
      if (value.length < 3) {
        callback(new Error("密码不能小于3位"));
      } else {
        callback();
      }
    };
    return {
      loginForm: {
        username: "",
        password: "",
      },
      loginRules: {
        username: [
          { required: true, trigger: "blur", validator: validateUsername },
        ],
        password: [
          { required: true, trigger: "blur", validator: validatePass },
        ],
      },
      loading: false,
      pwdType: "password",
      login_center_bg,
      dialogVisible: false,
      supportDialogVisible: false,
    };
  },
  created() {
     console.log("beforeEach:"+this.$router.options.routes);
  },
  methods: {
    showPwd() {
      if (this.pwdType === "password") {
        this.pwdType = "";
      } else {
        this.pwdType = "password";
      }
    },
    //登陆核心
    handleLogin() {
      this.$refs.loginForm.validate((valid) => {
        if (valid) {
          this.loading = true;
          const username = this.loginForm.username.trim()
          login(username, this.loginForm.password)
            .then((response) => {
              this.loading = false
              const data = response.data;
              const tokenStr = data.tokenHead + data.token;
              console.log("tokenStr="+tokenStr)
            })
            .catch((error) => {
              this.loading = false
              //抛出错误
            });
        } else {
          console.log("参数验证不合法!");
          return false;
        }
      });
    },
  },
};
</script>

<style scoped>
.login-form-layout {
  position: absolute;
  left: 0;
  right: 0;
  width: 360px;
  margin: 140px auto;
  border-top: 10px solid #409eff;
}

.login-title {
  text-align: center;
}

.login-center-layout {
  background: #409eff;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  margin-top: 200px;
}
</style>

我们可以直接复制mall项目里面的,注意,有些东西我们之前没有添加,如果觉得不需要也可以去掉,反正以后也要用,添加部分如下:

1.SvgIcon组件和icons文件;

复制src/components/SvgIcon所有文件到我们项目中同样位置,之后复制 src/icons 所有文件到我们的项目中同样的位置。

2.util包中的validate文件;

复制src/views/validate.js 文件到我们项目中同样的位置。

3.images文件

复制src/assets/images 文件到我们项目中同样的位置。

如果不添加,记得将index文件中对应的标签或者引入组件删除,不然会编译报错的。

添加成功后我们还不可以访问,因为我们还没有添加路由呢!

打开 router/index.js 文件,添加 login 页面路由:

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

/* Layout */
import Layout from '../views/layout/Layout'


export const constantRouterMap = [
  //这里添加的路由
  {path: '/login', component: () => import('@/views/login/index'), hidden: true},
  {
    path: '',
    component: Layout,
    redirect: '/home',
    children: [{
      path: 'home',
      name: 'home',
      component: () => import('@/components/HelloWorld'),
      meta: {title: '首页', icon: 'home'}
    }]

  }
]

export default new Router({
  // mode: 'history', //后端支持可开
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRouterMap
})

运行项目后,打开  http://localhost:8080/#/login 页面试一下,mall 后端接口测试提供的账号密码是: admin  macro123(希望可以看看这位作者的公众号哈),

访问成功!说明我们的 axios 引入成功!

当我们能够写基本的页面,能够调用接口时,基本的使用就可以,我们可以直接通过现有的工具编写系统页面。

注意,我们之前有说后端项目是使用的 token 来验证用户信息,管理系统除了登录页面,其余的页面都是需要权限的。所以思考一下,如果仅用现有的工具是非常麻烦的,我们登录之后需要保存 token 值,每次调用接口时都需要在请求头中加入 token 值,那么在好多路由中使用时就需要不停的传递 token 参数,非常麻烦!!!!

所以我们就需要有一个总的工具帮我们保存这个 token 值,只要我们需要就从他中获取,就不需要路由跳转传递啦!

因为保存的数据很小,所以直接用cookie保存,js-cookie就是关于cookie存储的一个js的API。可以看看这篇文章vue 项目中使用 js-cookie细则

我们登陆成功之后,就需要调用 cookie 保存,(mall 项目是通过 vuex 调用 cookie 保存的,其实是一样的,只不过我们暂时没有加入 vuex 模块),我们直接在调用登录接口成功后,就调用 cookie 进行保存。

之后就需要在每次调用接口时,都加上token,我们也不可能每个接口都逐个添加,所以 axios 就提供了拦截器,我们在 1.5 中 util/request.js 文件中就添加了请求拦截器了,每次调用接口时都会拦截请求,然后我们在拦截中加上 token 值,这样就保证每个请求都携带 token ,当然没有 token 就不加了。

首先我们需要配置  js-cookie 工具,mall 项目有提供这个工具文件,就在 util 文件夹中的 auth.js,这个就是专门保存登录 token 的,注意不是专门操作 cookie 的,而是专门操作保存登录的cookie 的。我们拷贝过来,可以修改这个 TokenKey 为自己想要的。

//----auth.js-------------
import Cookies from 'js-cookie'

const TokenKey = 'loginToken'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

然后再 login/index.vue 文件中引入 auth.js 文件,并在调用接口成功后使用 setToken,添加到cookie 中。

//
//...
<script>
import { isvalidUsername } from "@/utils/validate";
import { login, logout, getInfo } from "@/api/login";
import login_center_bg from "@/assets/images/login_center_bg.png";
import { setToken } from '@/utils/auth' //引入cookie的setToken 方法


export default {
  name: "login",
  data() {
    ...
    };
  },
  ...
  methods: {
    ...
    //登陆核心
    handleLogin() {
      this.$refs.loginForm.validate((valid) => {
        if (valid) {
          this.loading = true;
          const username = this.loginForm.username.trim()
          login(username, this.loginForm.password)
            .then((response) => {
              this.loading = false
              const data = response.data;
              const tokenStr = data.tokenHead + data.token;
              console.log("tokenStr="+tokenStr)
              setToken(tokenStr) //添加到 cookie
              this.$router.push({path: '/home'}) //成功后跳转到首页
            })
            .catch((error) => {
              this.loading = false
              //抛出错误
            });
        } else {
          console.log("参数验证不合法!");
          return false;
        }
      });
    },
  },
};
...

最后要在 axios 请求拦截器中 getToken 添加 token 值到请求头中

//----util/request.js--------
...
import { getToken } from '@/utils/auth'  //引入 gettoken 方法

// 创建axios实例
const service = axios.create({
  ...
})

// request请求拦截器,每次请求接口前都会执行这个
service.interceptors.request.use(config => {
  //可以在这里设置请求头等内容
  if (getToken()) { //如果cookie有 token 就加到请求头中,这里注意需要与后端代码结合
    config.headers['Authorization'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  return config
}, error => {
  ...
})

...

添加成功后我们打开登陆页面试一下,填写正确的用户信息,会登陆成功到 /home页面,因为我们现在只有 login 这一个请求后端的接口,所以我们返回登陆页面,再次输入一个错误的用户信息调用一下接口,发现这个请求接口的请求头中是有我们添加的 token 值的!

只有 axios 调用的接口会被拦截,添加 token 值,其余的非axios调用接口,是没有添加这个的!

 成功!!!

1.7修改vue-router为动态路由并修改sidebar为动态的导航栏

终于到这里了,理一下思路,我们现在也登陆成功了,也保存了token了,那么按照这个方式也可以继续编写代码了。

我们接下来需要添加权限模块的代码了,每一个登陆账号都有自己的权限,一方面是后端控制,一方面是前端控制,如果前端不控制那用户体验感是非常不好的。

那么当用户登录后,就需要获取用户的权限,这里的权限指:导航栏、按钮是否显示。我们先实现左侧导航栏的显示。

router路由给我们提供了一个动态路由的工具,可以帮助我们在运行时添加路由页面地址。那就是路由守卫,也就是路由过滤器,如果我们已经登陆账号并且此时访问非白名单路径(例如添加用户),那么就获取用户的权限,然后生成对应的动态导航栏。

思考一下,感觉路由守卫的router.beforeEach逻辑步骤应该是这样的:

1.判断是否登录,如果未登录,跳转到步骤5;

2.如果已登录,判断是否已获取添加动态路由,如果有获取动态路由,跳转到步骤4;

3.如果已登录,当没有获取动态路由,则调用接口获取到动态路由,并加入我们的路由里面,然后进行放行,结束。

4.直接放行,结束。

5.直接定位到登陆页面,不允许未登录时访问业务页面,结束。

 上面的步骤是没问题的,当然也得根据实际的场景进行修改,具体就看业务啦~~

我们将上面的步骤转化为代码:

首先需要新建一个路由配置文件,在 src 文件夹中直接创建 permission.js :

//路由守卫,也相当于路由前的拦截器
router.beforeEach((to, from, next) => {
  NProgress.start()  //进度条开始
  // console.log("未登录访问 /");
  if (getToken()) {  //若登录则获取权限等
    if (to.path === '/login') { //已登陆,若访问登陆页面直接到首页
      next({ path: '/' })
      NProgress.done() // if current page is dashboard will not trigger	afterEach hook, so manually handle it
    } else {  //否则获取用户权限信息,并设置动态路由
      //这里需要加个判断哈,如果添加了当前用户的路由就不需要再次添加了,否则警告重复。是通过 cookies 值判断的,true就是一天假,false是未添加
     if(!getHaveAuth()){
      getInfo().then(response => { //不用传值,已登陆的话后台会根据 token 拿到用户信息的。
        const data = response.data
        let menus = data.menus;
        //获取当前用户的权限整理出来,若有则显示,若没有则隐藏,也就是最终 accessedRouters = asyncRouterMap 的修改版的,asyncRouterMap 就是前端写的所有动态路由数组。 
        const accessedRouters = asyncRouterMap.filter(v => {
              ... //太长了,这里是获取动态路由
        });
        //对菜单进行排序
        sortRouters(accessedRouters);
        router.addRoutes(accessedRouters); //通过方法将动态路由添加到可访问路由表
        setHaveAuth(true);  //已添加,所以设置值为true
        next({ ...to, replace: true }) //中断当前路由,并再次进入路由守卫,那么此时的路由请求还是当前请求,比如 /
        // next() // 一般动态添加路由后不能直接用这个,可能会找不到新添加的路由,这里是为了测试演示使用的(因为我们没有是否获取权限的判断,所以只能从这里放行)
      }).catch(error => {
        reject(error)
      })
     }
     next()
    }
  } else { //若没有登陆
    if (whiteList.indexOf(to.path) !== -1) {  //判断是否进入白名单页面,是则放行,并跳出守卫进入页面
      next() //放行
      NProgress.done() //进度条结束
    } else {  //不是则进入登录页面
      next('/login') //中断当前路由,并再次进入路由守卫,那么此时的路由请求就是 /login
    }
  }
})
//----------auth.js 文件中添加如下--------记得在 permission.js 中引入
const isHaveAuth = 'isHaveAuth'

export function getHaveAuth() {
  return Cookies.get(isHaveAuth)
}

export function setHaveAuth(auth) {
  return Cookies.set(isHaveAuth, auth)
}

写好后,记得将这个文件引入项目中,在 main.js 文件中引入:

import '@/permission' // permission control router路由的拦截器配置

第二步:添加路由页面,这里我们直接在login文件夹同级创建一些页面,就像这样:

第三步,我们修改路由,前端这里也需要加上动态路由的配置,这里要记得我们是使用的mall系统的后台,所以权限的名称匹配,要跟 mall 系统对接,不能随便写的!我们使用的 admin 账号是全部权限的账号,你也可以自己跑个后台然后进行对应的修改!打开 router/index.js :

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

/* Layout */
import Layout from '../views/layout/Layout'


export const constantRouterMap = [
...
]
//------------------------------------------------------
//这个就是动态的全部权限!里面的数据,根据你的业务酌情修改
export const asyncRouterMap = [
  {
    path:'/ums',
    component: Layout,
    redirect: '/ums/admin',
    //最终在 permission.js 的 getMenu() 方法中通过 name 进行匹配,也就是后端返回的权限中也有一个 name,这两个 name 匹配中了,就代表用户有这个权限
    name: 'ums', 
    //这里的可以修改,反正已后端返回的为准
    meta: {title: '权限', icon: 'ums'},
    children: [
      {
        path: 'admin',
        //同理
        name: 'admin',
        component: () => import('@/views/ums/admin/index'),
        //同理
        meta: {title: '用户列表', icon: 'ums-admin'}
      },
      {
        path: 'role',
        name: 'role',
        component: () => import('@/views/ums/role/index'),
        meta: {title: '角色列表', icon: 'ums-role'}
      },
    ]
  }
]


export default new Router({
  ...
})

 最后我们需要将 router 传给 layout 组件中的左侧导航栏组件,只有传给 sidebar ,sidebar 才能够使用,之前我们用的是纯静态的。

打开 src\views\layout\components\Sidebar\index.vue 

//-----sidebar/index.vue------
<template>
  <el-menu
    mode="vertical"
    background-color="#304156"
    text-color="#bfcbd9"
    active-text-color="#409EFF"
  >
    <sidebar-item :routes="routes"></sidebar-item> //添加父向子传递的数据
  </el-menu>
</template>

<script>
import SidebarItem from "./SidebarItem";

export default {
  components: { SidebarItem },
  data() {
    return {
      routes: this.$router.options.routes, //在这里加入数据
    };
  },
};
</script>


//-----sidebar/sidebaeItme.vue------

<template>
  <div class="menu-wrapper">
      <!-- 当前这个父权限没有隐藏并且有孩子才会显示 -->
    <template v-for="item in routes" v-if="!item.hidden && item.children">
      <!-- 这个是没有子路由的 -->
      <router-link v-if="hasOneShowingChildren(item.children) && !item.children[0].children&&!item.alwaysShow" :to="item.path+'/'+item.children[0].path"
        :key="item.children[0].name">
        <el-menu-item :index="item.path+'/'+item.children[0].path" :class="{'submenu-title-noDropdown':!isNest}">
          <!-- <svg-icon v-if="item.children[0].meta&&item.children[0].meta.icon" :icon-class="item.children[0].meta.icon"></svg-icon> -->
          <span v-if="item.children[0].meta&&item.children[0].meta.title" slot="title">{{item.children[0].meta.title}}</span>
        </el-menu-item>
      </router-link>
      <!-- 这个是有子路由的 -->
      <el-submenu v-else :index="item.name||item.path" :key="item.name">
        <template slot="title">
          <!-- <svg-icon v-if="item.meta&&item.meta.icon" :icon-class="item.meta.icon"></svg-icon> -->
          <span v-if="item.meta&&item.meta.title" slot="title">{{item.meta.title}}</span>
        </template>

        <template v-for="child in item.children" v-if="!child.hidden">
          <sidebar-item :is-nest="true" class="nest-menu" v-if="child.children&&child.children.length>0" :routes="[child]" :key="child.path"></sidebar-item>

          <router-link v-else :to="item.path+'/'+child.path" :key="child.name">
            <el-menu-item :index="item.path+'/'+child.path">
              <!-- <svg-icon v-if="child.meta&&child.meta.icon" :icon-class="child.meta.icon"></svg-icon> -->
              <span v-if="child.meta&&child.meta.title" slot="title">{{child.meta.title}}</span>
            </el-menu-item>
          </router-link>
        </template>
      </el-submenu>
    </template>
  </div>
</template>

<script>
export default {
  name: "SidebarItem",
  //获取父组件传的数据
  props: {
    routes: {
      type: Array,
    },
    isNest: {
      type: Boolean,
      default: false
    }
  },
  methods:{
    hasOneShowingChildren(children) {
      const showingChildren = children.filter(item => {
        return !item.hidden
      })
      if (showingChildren.length === 1) {
        return true
      }
      return false
    }
  }
};
</script>

这样,我们就可以获取并拿到动态路由啦,访问 http://localhost:8080/#/login 链接,登陆成功后,发现左侧并没有变化??????添加断点,发现确实在路由前置守卫中添加动态路由了,但是,并没有显示在左侧导航栏。

 说明权限是有获取到的,是前端有问题!!

首先我想到了:我们 sidebar 组件中的 data 数据是跟着项目运行而初始化的,也就是说,data 已经赋值为静态路由了,并且我们也没有在这个页面中主动修改过 data 数据,就导致,这个组件的 routes 并没有改变!!!!!(这个描述不确定是不是对的,确实不清楚这样能不能拿到修改后的 routes 。但是不成功的原因不是这个!(待学习)

but,在之后的测试中我又发现了一件事:

我们登陆之后,左侧导航并没有改变,sidebar 组件中的 data 数据也没有改变,但是确实会跳转到,我们在网址上输入 http://localhost:8080/#/pms/product 就能够打开我们添加的路由页面!!!

也就是说路由是添加进去了,但是并没有传给sidebar 组件!于是我网上搜索找到了这样一个描述:this.$router.options.routes 可以拿到初始化时配置的路由规则。所以,我们动态路由 add 后他是拿不到的,<( ̄ ﹌  ̄)@m  生气!不过还好找到原因了。 

那么我们单纯的使用路由 router 来解决动态路由是不行的,所以我们需要一个缓存器,帮助我们保存动态的路由,然后在 sidebarItem 组件中拿到渲染到 router-view 中,就可以啦!mall 里面就是用 vuex 这样写的,奈何我一开始没这样试,不过也了解了这个问题,以后就不会抓头发啦!

说到缓存器,mall里面用的 vuex ,我也按照这个使用,因为我们需要进行父子组件之间传递。不清楚缓存的可以看看这个,只是说个大概:vuex和缓存的区别

 下一篇就开始加入 vuex 啦!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值