最近在學習Spring Boot + Vue的整合,通過尚硅谷的一個穀粒學院項目進行練習。
一、課程介紹
系統後端接口部分,使用目前流行的SpringBoot+SpringCloud進行微服務架構,使用Feign、Gateway、Hystrix,以及阿里巴巴的Nacos等組件搭建了項目的基礎環境。項目中還使用MyBatisPlus進行持久層的操作,使用了OAuth2+JWT實現了分佈式的訪問,項目中整合了SpringSecurity進行了權限控制。除此之外,項目中使用了阿里巴巴的EasyExcel實現對Excel的讀寫操作,使用了Redis進行首頁數據的緩存,使用Git進行代碼的版本控制,還整合了Swagger生成接口文檔 。
系統前端部分,使用主流的前端框架Vue,使用Es6的開發規範,採用模塊化的開發模式,搭建頁面環境使用了Nuxt框架和vue-admin-template模板,使用Element-ui進行頁面佈局。前端環境中使用Npm進行依賴管理,使用Babel進行代碼轉換,使用Webpack進行靜態資源的打包,採用axios進行Ajax請求調用,使用了ECharts進行數據的圖表展示。
二、前置知識梳理
1、Swagger
Swagger 是一個規範和完整的框架,用於生成、描述、調用和可視化 RESTful 風格的 Web 服務。
作用:
(1)接口的文檔在線自動生成。
(2)功能測試。
方法測試
2、輕量級後臺管理系統基礎模板,vue-admin-template
3、MySQL數據庫
建表語句:
CREATE TABLE `edu_teacher` (
`id` char(19) NOT NULL COMMENT '講師ID',
`name` varchar(20) NOT NULL COMMENT '講師姓名',
`intro` varchar(500) NOT NULL DEFAULT '' COMMENT '講師簡介',
`career` varchar(500) DEFAULT NULL COMMENT '講師資歷,一句話說明講師',
`level` int(10) unsigned NOT NULL COMMENT '頭銜 1高級講師 2首席講師',
`avatar` varchar(255) DEFAULT NULL COMMENT '講師頭像',
`sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '邏輯刪除 1(true)已刪除, 0(false)未刪除',
`gmt_create` datetime NOT NULL COMMENT '創建時間',
`gmt_modified` datetime NOT NULL COMMENT '更新時間',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='講師';
4、nginx
(1)簡介
nginx (engine x) 是一個高性能的HTTP和反向代理web服務器。
(2)Nginx的主要作用
- 請求轉發
- 負載均衡
- 動態分離
(3)啓動
cmd路徑,執行nginx.exe命令即可。
(4)配置
目前暫時只有兩個子端口,9001爲nginx端口號,8001爲講師模塊,8002爲阿里雲oss上傳頭像模塊。
三、Spring Boot + Vue 實現CRUD
1、EduTeacherController
package com.atguigu.eduservice.controller;
import com.atguigu.commonutils.R;
import com.atguigu.eduservice.entity.EduTeacher;
import com.atguigu.eduservice.entity.vo.TeacherQuery;
import com.atguigu.eduservice.service.EduTeacherService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(description="講師管理")
@RestController
@RequestMapping("/eduservice/teacher")
@CrossOrigin
public class EduTeacherController {
@Autowired
private EduTeacherService teacherService;
@ApiOperation(value = "所有講師列表")
@GetMapping("findAll")
public R findAllTeacher(){
List<EduTeacher> list = teacherService.list(null);
return R.Ok().data("items",list);
}
@ApiOperation(value = "根據ID查詢講師")
@GetMapping("findById{id}")
public R findById(@ApiParam(name="id",value="講師ID",required=true) @PathVariable String id){
EduTeacher teacher = teacherService.getById(id);
return R.Ok().data("items",teacher.getJson());
}
@ApiOperation(value = "根據ID刪除講師")
//邏輯刪除講師的方法
@DeleteMapping("{id}")
public R removeTeacher(@ApiParam(name="id",value="講師ID",required=true) @PathVariable String id){
Boolean flag = teacherService.removeById(id);
if(flag){
return R.Ok();
}else{
return R.error();
}
}
//分頁
@GetMapping("pageTeacher/{current}/{limit}")
public R pageListTeacher(@PathVariable long current,@PathVariable long limit){
//創建page對象
Page<EduTeacher> pageTeacher = new Page<>(current,limit);
//調用方法的時候,底層封裝,把分頁所有數據封裝到pageTeacher中
teacherService.page(pageTeacher,null);
long total = pageTeacher.getTotal();
List<EduTeacher> records = pageTeacher.getRecords();
//Map map = new HashMap();
//map.put("total",total);
//map.put("rows",records);
return R.Ok().data("total",total).data("rows",records);
}
@PostMapping("pageTeacherCondition/{current}/{limit}")
public R pageTeacherCondition(@PathVariable long current,@PathVariable long limit,
@RequestBody(required = false) TeacherQuery teacherQuery){
//創建一個page對象
Page<EduTeacher> pageTeacher = new Page<>(current,limit);
QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
//多條件組合查詢
//動態SQL
//判斷條件值是否爲空,不爲空就拼接
String name = teacherQuery.getName();
Integer level = teacherQuery.getLevel();
String begin = teacherQuery.getBegin();
String end = teacherQuery.getEnd();
if(!StringUtils.isEmpty(name)){
wrapper.like("name",name);
}
if(!StringUtils.isEmpty(level)){
wrapper.eq("level",level);
}
if(!StringUtils.isEmpty(begin)){
wrapper.ge("gmt_create",begin);
}
if(!StringUtils.isEmpty(end)){
wrapper.le("gmt_modified",end);
}
wrapper.orderByDesc("gmt_create");
//wrapper.
teacherService.page(pageTeacher,wrapper);
long total = pageTeacher.getTotal();
List<EduTeacher> records = pageTeacher.getRecords();
return R.Ok().data("total",total).data("rows",records);
}
//添加講師接口的方法
@PostMapping("addTeacher")
public R addTeacher(@RequestBody EduTeacher eduTeacher){
boolean save = teacherService.save(eduTeacher);
if(save){
return R.Ok();
}else {
return R.error();
}
}
@GetMapping("getTeacher/{id}")
public R getTeacher(@PathVariable String id){
EduTeacher byId = teacherService.getById(id);
return R.Ok().data("teacher",byId);
}
@PostMapping("updateTeacher")
public R updateTeacher(@RequestBody EduTeacher eduTeacher){
boolean flag = teacherService.updateById(eduTeacher);
if(flag){
return R.Ok();
}else {
return R.error();
}
}
}
2、前端講師列表
<template>
<div>
<el-form label-width="120px">
<el-form-item label="講師名稱">
<el-input v-model="teacher.name"/>
</el-form-item>
<el-form-item label="講師排序">
<el-input-number v-model="teacher.sort" controls-position="right" min="0"/>
</el-form-item>
<el-form-item label="講師頭銜">
<el-select v-model="teacher.level" clearable placeholder="請選擇">
<!--
數據類型一定要和取出的json中的一致,否則沒法回填
因此,這裏value使用動態綁定的值,保證其數據類型是number
-->
<el-option :value="1" label="高級講師"/>
<el-option :value="2" label="首席講師"/>
</el-select>
</el-form-item>
<el-form-item label="講師資歷">
<el-input v-model="teacher.career"/>
</el-form-item>
<el-form-item label="講師簡介">
<el-input v-model="teacher.intro" :rows="10" type="textarea"/>
</el-form-item>
<!-- 講師頭像:TODO -->
<!-- 講師頭像 -->
<el-form-item label="講師頭像">
<!-- 頭銜縮略圖 -->
<pan-thumb :image="teacher.avatar"/>
<!-- 文件上傳按鈕 -->
<el-button type="primary" icon="el-icon-upload" @click="imagecropperShow=true">更換頭像
</el-button>
<!--
v-show:是否顯示上傳組件
:key:類似於id,如果一個頁面多個圖片上傳控件,可以做區分
:url:後臺上傳的url地址
@close:關閉上傳組件
@crop-upload-success:上傳成功後的回調
<input type="file" name="file"/>
-->
<image-cropper
v-show="imagecropperShow"
:width="300"
:height="300"
:key="imagecropperKey"
:url="BASE_API+'/eduoss/fileoss'"
field="file"
@close="close"
@crop-upload-success="cropSuccess"/>
</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/edu/teacher.js'
import ImageCropper from '@/components/ImageCropper'
import PanThumb from '@/components/PanThumb'
export default {
data() {
return {
teacher: {
name: '',
sort: 0,
level: 1,
career: '',
intro: '',
avatar: ''
},
//上傳彈框組件是否顯示
imagecropperShow:false,
imagecropperKey:0,//上傳組件key值
BASE_API:process.env.BASE_API, //獲取dev.env.js裏面地址
saveBtnDisabled:false // 保存按鈕是否禁用,
}
},
created(){
this.init()
},
watch: { //監聽
$route(to, from) { //路由變化方式,路由發生變化,方法就會執行
this.init()
}
},
methods: {
close() { //關閉上傳彈框的方法
this.imagecropperShow=false
//上傳組件初始化
this.imagecropperKey = this.imagecropperKey+1
},
//上傳成功方法
cropSuccess(data) {
this.imagecropperShow=false
//上傳之後接口返回圖片地址
this.teacher.avatar = data.url
this.imagecropperKey = this.imagecropperKey+1
},
init() {
//判斷路徑有id值,做修改
if(this.$route.params && this.$route.params.id) {
//從路徑獲取id值
const id = this.$route.params.id
//調用根據id查詢的方法
this.getInfo(id)
} else { //路徑沒有id值,做添加
//清空表單
this.teacher = {}
}
},
//根據講師id查詢的方法
getInfo(id) {
teacherApi.getTeacherInfo(id)
.then(response => {
this.teacher = response.data.teacher
})
},
saveOrUpdate() {
//根據teacher中是否有ID
if(!this.teacher.id){
this.saveTeacher();
}else{
this.updateTeacher();
}
},
updateTeacher() {
teacherApi.updateTeacherInfo(this.teacher)
.then(response => {
//提示信息
this.$message({
type: 'success',
message: '修改成功!'
});
//回到列表頁面,路由跳轉
this.$router.push({ path: '/teacher/table' })
})
},
// 保存
saveTeacher() {
teacherApi.addTeacher(this.teacher)
.then(response => {
//提示信息
this.$message({
type: 'success',
message: '添加成功!'
});
//回到列表頁面,路由跳轉
this.$router.push({ path: '/teacher/table' })
})
}
}
}
</script>
3、添加講師頁面
<template>
<div class="app-container">
<!--查詢表單-->
<el-form :inline="true" class="demo-form-inline">
<el-form-item>
<el-input v-model="teacherQuery.name" placeholder="講師名"/>
</el-form-item>
<el-form-item>
<el-select v-model="teacherQuery.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="teacherQuery.begin"
type="datetime"
placeholder="選擇開始時間"
value-format="yyyy-MM-dd HH:mm:ss"
default-time="00:00:00"
/>
</el-form-item>
<el-form-item>
<el-date-picker
v-model="teacherQuery.end"
type="datetime"
placeholder="選擇截止時間"
value-format="yyyy-MM-dd HH:mm:ss"
default-time="00:00:00"
/>
</el-form-item>
<el-button type="primary" icon="el-icon-search" @click="getList()">查詢</el-button>
<el-button type="default" @click="resetData()">清空</el-button>
</el-form>
<!-- 表格 -->
<el-table
:data="list"
border
fit
highlight-current-row>
<el-table-column
label="序號"
width="70"
align="center">
<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="80">
<template slot-scope="scope">
{{ scope.row.level===1?'高級講師':'首席講師' }}
</template>
</el-table-column>
<el-table-column prop="intro" label="資歷" />
<el-table-column prop="gmtCreate" label="添加時間" width="160"/>
<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="removeDataById(scope.row.id)">刪除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分頁 -->
<el-pagination
:current-page="page"
:page-size="limit"
:total="total"
style="padding: 30px 0; text-align: center;"
layout="total, prev, pager, next, jumper"
@current-change="getList"
/>
</div>
</template>
<script>
//引入teacher.js
import teacher from '@/api/edu/teacher.js'
export default {
data() { //定義變量
return {
list:null,
page:1,
limit:10,
total:0,
teacherQuery:{}
}
},
created() {//調用方法
this.getList()
},
methods: {//定義方法
//講師列表的方法
getList(page =1){
this.page = page
teacher.getTeacherListPage(this.page,this.limit,this.teacherQuery)
.then(response => {//請求成功
//response接收返回的數據
this.list = response.data.rows
this.total = response.data.total
console.log(this.list)
console.log(this.total)
})
.catch(error => {//請求失敗
console.log(error)
})
},
resetData() {
this.teacherQuery = {}
this.getList()
},
removeDataById(id) {
// debugger
// console.log(memberId)
this.$confirm('此操作將永久刪除講師記錄, 是否繼續?', '提示', {
confirmButtonText: '確定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
teacher.deleteTeacherId(id)
.then(response =>{
this.$message({
type: 'success',
message: '刪除成功!'
});
this.getList()
})
})
}
}
}
</script>
4、teacher.js
import request from '@/utils/request'
export default{
//1 講師列表(條件查詢分頁)
getTeacherListPage(current,limit,teacherQuery) {
return request({
url: `/eduservice/teacher/pageTeacherCondition/${current}/${limit}`,
method: 'post',
//後端用RequestBody,JSON傳遞
//data表示把對象轉換成json進行傳遞到接口裏面
data: teacherQuery
})
},
deleteTeacherId(id){
return request({
url: `/eduservice/teacher/${id}`,
method: 'delete'
})
},
addTeacher(teacher){
return request({
url: `/eduservice/teacher/addTeacher`,
method: 'post',
data: teacher
})
},
getTeacherInfo(id){
return request({
url: `/eduservice/teacher/getTeacher/${id}`,
method: 'get'
})
},
updateTeacherInfo(teacher){
return request({
url: '/eduservice/teacher/updateTeacher',
method: 'post',
data: teacher
})
}
}
以上爲SpringBoot + Vue實現CRUD的主要代碼!
5、頁面效果展示