在线教育项目 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】
- 例:百度百科中的目录跳转
步骤:
-
在项目文件夹中复制js资源,并引入
<script src="vue.min.js"></script> <script src="vue-router.min.js"></script>
-
编写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>
-
效果预览:【目录中会以#作为锚点进行变化】
讲师管理前端开发
前端框架设置
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)
-
前端登录接口文件:
src/api/login.js
,可以在这个文件中分析后端模拟数据的接口的url地址:- 登录:/user/login
- 登出:/user/logout
- 获取用户信息:/user/info
-
后端接口文件
暂时在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)
-
自定义异常(文件上传异常)
-
批量删除(复选框+批量删除按钮)
-
自动完成(根据搜索框内已输入的内容,显示历史搜索记录或给出搜索建议)
过程中的问题
-
在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> ---此处空一行---
-
vue文件组成:分为两个部分,模板和脚本
//模板 <template> <div> ... </div> </template> //脚本: <script> ... </script>
-
运行前端项目时,需要同时开启后端服务器,才能进行页面和数据的渲染
渲染过程中,通过对浏览器控制台的network和vue developper进行检查,知道渲染是否成功
同时,上传头像时需要在后端启动OSS项目,否则会显示
上传失败(http错误)
的错误提示(该提示为自己创建的,默认无) -
在翻页处,对于事件的注册不能在方法后添加圆括号。此处为方法引用,否则为方法调用。
@current-change="changeCurrentPage" ---- // 改变页码,page:回调参数,表示当前选中的“页码” changeCurrentPage(page) { this.page = page this.fetchData() },
-
自己在测试阶段制造的异常不要忘记在测试结束后修改过来。
未解决问题
-
通过前端页面添加讲师时,添加后的讲师信息会乱码。
检查:数据库、后端IDEA、前端VS Code均已设置UTF-8编码格式
暂时的结论:前端代码中的姓名(name)部分未设置UTF-8
前端代码
-
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' }) } }
-
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"' })
-
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>
-
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>
-
babel转码器配置文件
.babelrc
{ "presets": [ ["env", { "modules": false, "targets": { "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] } }], "stage-2" ], "plugins":["transform-vue-jsx", "transform-runtime"] }
end.