在线教育项目 04

在线教育项目 04

本文为b站尚硅谷-全栈在线教育项目-谷粒学院【Spring Boot + Spring Cloud Alibaba + Vue.js】的个人学习笔记

前端基础知识

开发平台:vs code

开发工具:Node.js、vue.js、axios、element-ui

​ npm、babel转码器、webpack、ESLint语法检查

存在的问题:双向数据绑定(vue.js)、跨域问题(axios)、运行前端项目(npm、vue-element-admin)

Vue.js - 页面局部刷新

  • 通过路由(锚点),可以实现对于页面的局部刷新【单页应用,SPA】
  • 例:百度百科中的目录跳转

步骤:

  1. 在项目文件夹中复制js资源,并引入

    <script src="vue.min.js"></script>
    <script src="vue-router.min.js"></script>
    
  2. 编写html和js

    <!DOCTYPE html>
    <html lang="en">
    
    <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">
        <title>Document</title>
    </head>
    
    <body>
    
        <div id="app">
            <h1>Hello App!</h1>
            <p>
                <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
                <!-- 通过传入 `to` 属性指定链接. -->
                <router-link to="/">首页</router-link>
                <router-link to="/student">会员管理</router-link>
                <router-link to="/teacher">讲师管理</router-link>
            </p>
            <!-- 路由出口 -->
            <!-- 路由匹配到的组件将渲染在这里 -->
            <router-view></router-view>
        </div>
    
        <script src="vue.min.js"></script>
        <script src="vue-router.min.js"></script>
        <script>
            // 1. 定义(视图)组件。
            // 复杂的组件也可以从独立的vue文件中引入
            const Welcome = { template: '<div>欢迎</div>' }
            const Student = { template: '<div>student list</div>' }
            const Teacher = { template: '<div>teacher list</div>' }
        
            // 2. 定义路由
            // 每个路由应该映射一个组件。
            const routes = [
                { path: '/', redirect: '/welcome' }, //设置默认指向的路径
                { path: '/welcome', component: Welcome },
                { path: '/student', component: Student },
                { path: '/teacher', component: Teacher }
            ]
        
            // 3. 创建 router 实例,然后传 `routes` 配置
            const router = new VueRouter({
                routes // (缩写)相当于 routes: routes
            })
        
            // 4. 创建和挂载根实例。//挂载路由实例
            // 从而让整个应用都有路由功能
            new Vue({
                el: '#app',
                router
            })
        
            // 现在,应用已经启动了!
        </script>
    </body>
    
    </html>
    
  3. 效果预览:【目录中会以#作为锚点进行变化】

    image-20210505135344977

image-20210505135359576

image-20210505135414963

讲师管理前端开发

前端框架设置

vue-element-template的目录结构

├── build                      // 构建相关  
├── config                     // 配置相关
├── src                        // 源代码
│   ├── api                    // 所有请求
│   ├── assets                 // 主题 字体等静态资源
│   ├── components             // 全局公用组件
│   ├── directive              // 全局指令
│   ├── filtres                // 全局 filter
│   ├── icons                  // 项目所有 svg icons
│   ├── lang                   // 国际化 language
│   ├── mock                   // 项目mock 模拟数据
│   ├── router                 // 路由
│   ├── store                  // 全局 store管理
│   ├── styles                 // 全局样式
│   ├── utils                  // 全局公用方法
│   ├── vendor                 // 公用vendor
│   ├── views                   // view
│   ├── App.vue                // 入口页面
│   ├── main.js                // 入口 加载组件 初始化等
│   └── permission.js          // 权限管理
├── static                     // 第三方不打包资源
│   └── Tinymce                // 富文本
├── .babelrc                   // babel-loader 配置
├── .eslintrc.js                // eslint 配置项
├── .gitignore                 // git 忽略项
├── favicon.ico                // favicon图标
├── index.html                 // html模板
└── package.json               // package.json

config/dev.env.js中定义全局常量BASE_API

  BASE_API: '"http://127.0.0.1:8110"'
  //后端项目的主机地址及端口号

src/api/login.js中引用request模块,调用远程api

export function login(username, password) {
  return request({
    url: '/user/login',
    method: 'post',
    data: {
      username,
      password
    }
  })
}

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

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

模拟登录接口,暂时解决前端登录问题

将后台登陆管理改为本地(临时使用)(后面把登录添加权限框架 spring security)

  1. 前端登录接口文件:src/api/login.js,可以在这个文件中分析后端模拟数据的接口的url地址:

    • 登录:/user/login
    • 登出:/user/logout
    • 获取用户信息:/user/info
  2. 后端接口文件

    暂时在service_edu微服务中创建LoginController,模拟上面三个接口

    @CrossOrigin //跨域
    @RestController // 支持restful风格
    @RequestMapping("/user")
    public class LoginController {
    
        //登录
        @PostMapping("login")
        public R login() {
            return R.ok().data("token","admin");
        }
    
        //获取用户信息
        @GetMapping("info")
        public R info() {
            return R.ok()
                    .data("roles","[admin]")
    				.data("name","admin")
    .data("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
        }
    
        //退出
        @PostMapping("logout")
        public R logout(){
            return R.ok();
        }
    }
    

前端项目配置

前端部分内容按照尚硅谷笔记中直接复制所得,不再重复粘贴。

将遇到问题的地方进行提示,并附上项目最终代码。

前端实现的功能
  • 分页查询与显示

  • 删除讲师

  • 新增讲师

  • 修改讲师信息(显示与更新)

    【vue中存在组件重用,需要通过设置解决。组件重用导致的问题:切换了路由,但是页面并未重新渲染。解决方案的原理:[计算属性] 计算出一个不断变化的属性值】

  • 讲师头像上传(阿里云oss)

  • 自定义异常(文件上传异常)

  • 批量删除(复选框+批量删除按钮)

  • 自动完成(根据搜索框内已输入的内容,显示历史搜索记录或给出搜索建议

过程中的问题
  1. 在src/views文件夹下创建vue组件时,代码的末端需要换行符

    form.vue和list.vue组件:

    //form.vue
    <template>
      <div class="app-container">
        讲师表单
      </div>
    </template>
    ---此处空一行---
    
    //list.vue
    <template>
      <div class="app-container">
        讲师列表
      </div>
    </template>
    ---此处空一行---
    
  2. vue文件组成:分为两个部分,模板和脚本

    //模板
    <template>
      <div>
      ...
      </div>
    </template>
    
    //脚本:
    <script>
    ...
    </script>
    
  3. 运行前端项目时,需要同时开启后端服务器,才能进行页面和数据的渲染

    渲染过程中,通过对浏览器控制台的network和vue developper进行检查,知道渲染是否成功

    同时,上传头像时需要在后端启动OSS项目,否则会显示上传失败(http错误)的错误提示(该提示为自己创建的,默认无)

    img

    img

  4. 在翻页处,对于事件的注册不能在方法后添加圆括号。此处为方法引用,否则为方法调用。

    @current-change="changeCurrentPage"
    
    ----
    
    // 改变页码,page:回调参数,表示当前选中的“页码”
    changeCurrentPage(page) {
        this.page = page
        this.fetchData()
    },  
    
  5. 自己在测试阶段制造的异常不要忘记在测试结束后修改过来。

未解决问题
  1. 通过前端页面添加讲师时,添加后的讲师信息会乱码。

    检查:数据库、后端IDEA、前端VS Code均已设置UTF-8编码格式

    暂时的结论:前端代码中的姓名(name)部分未设置UTF-8

前端代码
  1. src/api/teacher.js

    // @ 符号在build/webpack.base.conf.js 中配置 [表示 'src' 路径]
    import request from '@/utils/request'
    // 会报错,未引用,不需要管
    
    export default {
      // 根据后端接口定义Api模块
      list() {
        return request({ // request当作是一个方法,拼接baseUrl
          url: '/admin/edu/teacher/list',
          method: 'get'
        })
      },
      pageList(page, limit, searchObj) {
        return request({ // request当作是一个方法,拼接baseUrl
          url: `/admin/edu/teacher/list/${page}/${limit}`,
          method: 'get',
          params: searchObj // 是表单或者url字符串的传参方式
        })
      },
      removeById(id) {
        return request({
          url: `/admin/edu/teacher/remove/${id}`,
          method: 'delete'
        })
      },
      batchRemove(idList) {
        return request({
          url: '/admin/edu/teacher/batch-remove/',
          method: 'delete',
          data: idList
        })
      },
      save(teacher) {
        return request({
          url: '/admin/edu/teacher/save',
          method: 'post',
          data: teacher // json的传参方式
        })
      },
      getById(id) {
        return request({
          url: `/admin/edu/teacher/get/${id}`,
          method: 'get'
        })
      },
      updateById(teacher) {
        return request({
          url: '/admin/edu/teacher/update',
          method: 'put',
          data: teacher
        })
      },
      selectNameListByKey(key) {
        return request({
          url: `/admin/edu/teacher/list/name/${key}`,
          method: 'get'
        })
      }
    }
    
    
  2. src/config/dev.env.js

    'use strict'
    const merge = require('webpack-merge')
    const prodEnv = require('./prod.env')
    
    module.exports = merge(prodEnv, {
      NODE_ENV: '"development"',
      BASE_API: '"http://127.0.0.1:8110"'
    })
    
  3. src/views/teacher/form.vue

    <template>
      <div class="app-container">
        <!-- 输入表单 -->
        <el-form label-width="120px">
          <el-form-item label="讲师名称">
            <el-input v-model="teacher.name" />
          </el-form-item>
          <el-form-item label="入驻时间">
            <el-date-picker v-model="teacher.joinDate" value-format="yyyy-MM-dd" />
          </el-form-item>
          <el-form-item label="讲师排序">
            <el-input-number v-model="teacher.sort" :min="0"/>
          </el-form-item>
          <el-form-item label="讲师头衔">
            <el-select v-model="teacher.level">
              <!--
                  数据类型一定要和取出的json中的一致,否则没法回填
                  因此,这里value使用动态绑定的值,保证其数据类型是number
                  -->
              <el-option :value="1" label="高级讲师"/>
              <el-option :value="2" label="首席讲师"/>
              <!-- 此处的1没有加单引号,因此是一个数值。如果在双引号内加上了单引号,则表示是一个字符。 -->
              <!-- 同时,value前的冒号也表示这是一个表达式,其中是数值 -->
            </el-select>
          </el-form-item>
          <el-form-item label="讲师简介">
            <el-input v-model="teacher.intro"/>
          </el-form-item>
          <el-form-item label="讲师资历">
            <el-input v-model="teacher.career" :rows="10" type="textarea"/>
          </el-form-item>
          <!-- 讲师头像:TODO -->
          <!-- 讲师头像 -->
          <el-form-item label="讲师头像">
            <el-upload
              :show-file-list="false"
              :on-success="handleAvatarSuccess"
              :on-error="handleAvatarError"
              :before-upload="beforeAvatarUpload"
              class="avatar-uploader"
              action="http://localhost:8120/admin/oss/file/upload?module=avatar">
              <img v-if="teacher.avatar" :src="teacher.avatar">
              <!-- 此处需要修改,访问的是teacher.avatar -->
              <i v-else class="el-icon-plus avatar-uploader-icon"/>
            </el-upload>
          </el-form-item>
    
          <el-form-item>
            <el-button :disabled="saveBtnDisabled" type="primary" @click="saveOrUpdate()">保存</el-button>
          </el-form-item>
        </el-form>
      </div>
    </template>
    
    <script>
    import teacherApi from '@/api/teacher'
    export default {
      data() {
        return {
          // 初始化讲师默认数据
          teacher: {
            sort: 0,
            level: 1
            // 需要在前方做匹配,数值/字符 1
          },
          saveBtnDisabled: false // 保存按钮是否禁用,防止表单重复提交(true防止自动提交)
        }
      },
    
      // 页面渲染成功
      // 判断路由中是否有id,是否需要启用edit
      created() {
        if (this.$route.params.id) {
          this.fetchDataById(this.$route.params.id)
        }
      },
      methods: {
    
        saveOrUpdate() {
          // 禁用保存按钮
          this.saveBtnDisabled = true
          // 判断是save还是update
          if (!this.teacher.id) {
            this.saveData()
          } else {
            this.updateData()
          }
        },
    
        // 新增讲师
        saveData() {
          // debugger
          teacherApi.save(this.teacher).then(response => {
            this.$message({
              type: 'success',
              message: response.message
            })
            this.$router.push({ path: '/teacher' })
          })
        },
        // 根据id 查询记录
        fetchDataById(id) {
          teacherApi.getById(id).then(response => {
            this.teacher = response.data.items
          })
        },
    
        // 文件上传成功
        handleAvatarSuccess(res, file) {
          console.log(res)
          if (res.success) {
            // console.log(res)
            this.teacher.avatar = res.data.url
            // 强制重新渲染
            this.$forceUpdate()
          } else {
            this.$message.error('上传失败 (非20000)')
          }
        },
    
        // 文件上传失败
        // 错误处理
        handleAvatarError() {
          console.log('error')
          this.$message.error('上传失败(http失败)')
        },
    
        // 上传校验
        beforeAvatarUpload(file) {
          const isJPG = file.type === 'image/jpeg' // MIME类型
          const isLt2M = file.size / 1024 / 1024 < 2
    
          if (!isJPG) {
            this.$message.error('上传头像图片只能是 JPG 格式!')
          }
          if (!isLt2M) {
            this.$message.error('上传头像图片大小不能超过 2MB!')
          }
          return isJPG && isLt2M
        },
        // 根据id 更新记录
        updateData() {
          // teacher数据的获取
          teacherApi.updateById(this.teacher).then(response => {
            // 弹出成功提示
            this.$message({
              type: 'success',
              message: response.message
            })
            this.$router.push({ path: '/teacher' })
          })
        }
      }
    }
    
    </script>
    
    <style>
      .avatar-uploader .el-upload {
        border: 1px dashed #d9d9d9;
        border-radius: 6px;
        cursor: pointer;
        position: relative;
        overflow: hidden;
      }
      .avatar-uploader .el-upload:hover {
        border-color: #409EFF;
      }
      .avatar-uploader .avatar-uploader-icon {
        font-size: 28px;
        color: #8c939d;
        width: 178px;
        height: 178px;
        line-height: 178px;
        text-align: center;
      }
      .avatar-uploader img {
        width: 178px;
        height: 178px;
        display: block;
      }
    </style>
    
    
  4. src/views/teacher/list.vue

    <template>
      <div class="app-container">
        <!--查询表单-->
        <el-form :inline="true">
          <el-form-item>
            <!-- <el-input v-model="searchObj.name" placeholder="讲师名" /> -->
            <el-autocomplete
              v-model="searchObj.name"
              :fetch-suggestions="querySearch"
              :trigger-on-focus="false"
              class="inline-input"
              placeholder="讲师名称"
              value-key="name" />
          </el-form-item>
    
          <el-form-item>
            <el-select v-model="searchObj.level" clearable placeholder="头衔">
              <el-option value="1" label="高级讲师"/>
              <el-option value="2" label="首席讲师"/>
            </el-select>
          </el-form-item>
    
          <el-form-item label="入驻时间">
            <el-date-picker
              v-model="searchObj.joinDateBegin"
              placeholder="开始时间"
              value-format="yyyy-MM-dd" />
          </el-form-item>
          <el-form-item label="-">
            <el-date-picker
              v-model="searchObj.joinDateEnd"
              placeholder="结束时间"
              value-format="yyyy-MM-dd" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" icon="el-icon-search" @click="fetchData()">查询</el-button>
            <el-button type="default" @click="resetData()">清空</el-button>
          </el-form-item>
        </el-form>
    
        <!-- 批量删除按钮 -->
        <div style="margin-bottom: 10px">
          <el-button type="danger" size="mini" @click="batchRemove()">批量删除</el-button>
        </div>
    
        <!-- 表格 -->
        <el-table
          :data="list"
          border
          stripe
          @selection-change="handleSelectionChange">
    
          <!-- 复选框 -->
          <el-table-column type="selection"/>
    
          <!-- 索引序号 -->
          <el-table-column
            label="#"
            width="50">
            <template slot-scope="scope">
              {{ (page - 1) * limit + scope.$index + 1 }}
            </template>
          </el-table-column>
          <el-table-column prop="name" label="姓名" width="80" />
          <el-table-column label="头衔" width="90">
            <template slot-scope="scope">
              <el-tag v-if="scope.row.level === 1" type="success" size="mini">高级讲师</el-tag>
              <el-tag v-if="scope.row.level === 2" size="mini">首席讲师</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="intro" label="简介" />
          <el-table-column prop="sort" label="排序" width="60" />
          <el-table-column label="操作" width="200" align="center">
            <!-- 删除 -->
            <template slot-scope="scope">
              <router-link :to="'/teacher/edit/'+scope.row.id">
                <el-button type="primary" size="mini" icon="el-icon-edit">修改</el-button>
              </router-link>
              <el-button
                type="danger"
                size="mini"
                icon="el-icon-delete"
                @click="removeById(scope.row.id)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
    
        <!-- 分页组件 -->
        <el-pagination
          :current-page="page"
          :total="total"
          :page-size="limit"
          :page-sizes="[5, 10, 20]"
          style="padding: 30px 0; text-align: center;"
          layout="total, sizes, prev, pager, next, jumper"
          @current-change="changeCurrentPage"
          @size-change="changePageSize"
        />
      </div>
    </template>
    
    <script>
    import teacherApi from '@/api/teacher'
    export default {
      // 定义数据模型
      data() { // 定义数据
        return {
          list: null, // 数据列表
          total: 0, // 总记录数
          page: 1, // 页码
          limit: 10, // 每页记录数
          searchObj: {},
          multipleSelection: []// 批量删除选中的记录列表// 查询条件
        }
      },
    
      // 页面一加载就显示讲师列表
      // 页面渲染成功后获取数据
      created() {
        this.fetchData()
      },
    
      // 定义方法
      methods: {
        fetchData() {
        // 调用api
          teacherApi.pageList(this.page, this.limit, this.searchObj).then(response => {
            this.list = response.data.rows
            this.total = response.data.total
          })
        },
        // 改变页码,page:回调参数,表示当前选中的“页码”
        changeCurrentPage(page) {
          this.page = page
          this.fetchData()
        },
        // 每页记录数改变,size:回调参数,表示当前选中的“每页条数”
        changePageSize(size) {
          this.limit = size
          this.fetchData()
        },
        // 重置表单
        resetData() {
          this.searchObj = {}
          this.fetchData()
        },
        // 根据id删除数据
        removeById(id) {
          this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            return teacherApi.removeById(id)
          }).then((response) => {
            this.fetchData()
            this.$message.success(response.message)
            // 删除成功,消息提示
          }).catch(error => {
            console.log('error', error)
            // 当取消时会进入catch语句:error = 'cancel'
            // 当后端服务抛出异常时:error = 'error'
            if (error === 'cancel') {
              this.$message.info('已取消删除')
            }
          })
        },
    
        // 当多选选项发生变化的时候调用
        handleSelectionChange(selection) {
          console.log(selection)
          this.multipleSelection = selection
        },
    
        // 批量删除
        batchRemove() {
          console.log('removeRows......')
    
          if (this.multipleSelection.length === 0) {
            this.$message.warning('请选择要删除的记录!')
            return
          }
    
          this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            // 点击确定,远程调用ajax
            // 遍历selection,将id取出放入id列表
            var idList = []
            this.multipleSelection.forEach(item => {
              idList.push(item.id)
            })
            // 调用api
            return teacherApi.batchRemove(idList)
          }).then((response) => {
            this.fetchData()
            this.$message.success(response.message)
          }).catch(error => {
            if (error === 'cancel') {
              this.$message.info('取消删除')
            }
          })
        },
        // 输入建议
        querySearch(queryString, cb) {
          teacherApi.selectNameListByKey(queryString).then(response => {
            cb(response.data.nameList)
          })
        }
      }
    }
    </script>
    
    
  5. babel转码器配置文件.babelrc

    {
      "presets": [
        ["env", {
          "modules": false,
          "targets": {
            "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
          }
        }],
        "stage-2"
      ],
      "plugins":["transform-vue-jsx", "transform-runtime"]
    }
    
    

    end.


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值