抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

Hello world!

前端技术点简单介绍

一下技术点详见其官方文档或者课件,我就瞄了几眼,之前多多少少也学过了

  1. ES6
  2. Vue
  3. axios
  4. node.js
  5. babel
  6. 模块化
  7. webpack

搭建前端环境与框架开发过程

vscode创建工作区

  1. 新建一个空文件夹
  2. 用VSCode打开该文件夹
  3. 选择菜单栏 文件 -> 将文件夹另存为工作区 即可

测试运行npm

  1. 复制template文件夹至工作区
  2. 安装依赖npm install
  3. npm run dev测试
  4. config -> index.js -> useEslint = false关闭代码检查

vue项目实现

  1. src -> router -> index.js 添加路由
  2. views -> xxx -> xxx.vue 创建vue页面
  3. api -> xxx.js 定义接口地址和参数
  4. 在vue页面引入js文件,调用方法实现功能

用户登录和跨域(后端+前端)

修改BASE_API(请求路径前缀)

config -> dev.env.js文件

1
2
3
4
5
6
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
// BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
// 注意这里要使用http
BASE_API: '"http://localhost:8001"',
})

编写loginController

先跳过数据库验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.atguigu.eduservice.controller;

import com.atguigu.commonutils.R;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api("后台登录")
@RestController
@RequestMapping("/eduservice/user")
@CrossOrigin // 解决跨域问题
public class EduLoginController {

// 登录
@PostMapping("login")
public R login() {
// 先跳过验证
// 此处token需要与前端对应
return R.ok().data("token", "admin");
}

// 获得用户数据
@GetMapping("info")
public R info() {

return R.ok().data("roles", "[admin]").data("name", "admin").data("avatar", "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
}

}

修改前端访问路径

修改api文件夹下的login.js里的接口路径即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function login(username, password) {
return request({
url: '/eduservice/user/login',
method: 'post',
data: {
username,
password
}
})
}

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

跨域问题

访问地址的时候,以下任何一个地方不同,会产生跨域问题:

  1. 访问协议
  2. IP地址
  3. 端口号
  1. 在后端controller接口上添加注解@CrossOrigin即可(常用)
  2. 使用网关解决

讲师管理模块(前端)

讲师列表

添加路由

src\router\index.js -> constantRouterMap -> children -> component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export const constantRouterMap = [
// ……
{
path: '/teacher',
component: Layout,
redirect: '/teacher/table',
name: '讲师管理',
meta: { title: '讲师管理', icon: 'example' },
children: [
{
path: 'list',
name: '讲师列表',
component: () => import('@/views/edu/teacher/list'),
meta: { title: '讲师列表', icon: 'table' }
},
{
path: 'save',
name: '添加讲师',
component: () => import('@/views/edu/teacher/save'),
meta: { title: '添加讲师', icon: 'tree' }
}
]
},
// ……

}

创建并编写js文件(调用接口)

src\api\edu\teacher.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import request from '@/utils/request'

export default {

// 查询讲师列表(带条件分页)
getTeacherListPage(current, limit, teacherQuery) {
return request({
// url可使用字符串拼接,但是最好用反引号
url: `/eduservice/teacher/pageTeacherCondition/${current}/${limit}`,
method: 'post',
// 后端使用requestBody获取teacherQuery,此时需要用data(将teacherQuery转换为JSON格式传输)
data: teacherQuery
})
}

}

创建vue页面,通过接口获取数据

注意routerMap中的对应对象(即创建的router)中component对应的地址,就是vue组件的地址,在该地址下创建vue文件

src\views\edu\teacher\list.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div class="app-container">讲师列表</div>
</template>

<script>
// 引入调用接口的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() {
teacher
.getTeacherListPage(this.page, this.limit, this.teacherQuery)
.then((response) => {
// console.log(response);
this.list = response.data.records;
this.total = response.data.total;
})
.catch((error) => {
console.log(error);
});
},
},
};
</script>

编写列表格式代码

使用element-ui表格即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<div class="app-container">
<!-- 表格 -->
<el-table :data="list" border fit highlight-current-row>
<el-table-column label="序号" width="70" header-align="center">
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>

<el-table-column prop="name" label="名称" width="80" header-align="center"/>

<el-table-column label="头衔" width="80" header-align="center">
<!-- 使用template slot-scope 进行判断显示 -->
<template slot-scope="scope">
{{ scope.row.level === 1 ? "高级讲师" : "首席讲师" }}
</template>
</el-table-column>

<el-table-column prop="intro" label="资历" header-align="center"/>

<el-table-column prop="gmtCreate" label="添加时间" width="160" header-align="center"/>

<el-table-column prop="sort" label="排序" width="60" header-align="center"/>

<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>
</div>
</template>

添加分页条

使用element-ui分页即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
<div class="app-container">
<!-- 表格 -->

<!-- 分页条 -->
<!-- @current-change="getList" 只需要写方法名即可,自动传入需要跳转的页数 -->
<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>
// 引入调用接口的js文件
import teacher from "@/api/edu/teacher.js";

export default {
// ……
methods: {
// 获得讲师列表,page默认为1
getList(page = 1) {
this.page = page;
teacher
.getTeacherListPage(this.page, this.limit, this.teacherQuery)
.then((response) => {
// console.log(response);
this.list = response.data.records;
this.total = response.data.total;
})
.catch((error) => {
console.log(error);
});
},
},
};
</script>

条件查询分页

使用element-ui表单即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<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>
</template>

<script>
// 引入调用接口的js文件
import teacher from "@/api/edu/teacher.js";

export default {
methods: {
// 获得讲师列表,page默认为1
getList(page = 1) {
},
//清空表单输入项数据
resetData() {
this.teacherQuery = {};
//查询所有讲师数据
this.getList();
},
},
};
</script>

删除讲师

添加页面结构按钮

记得绑定对应方法名

slot插槽,Vue - slot-scope=”scope” 的意义 - 武卡卡 - 博客园 (cnblogs.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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>

编写接口调用方法

先去api文件夹下编写调用接口的方法 teacher.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import request from '@/utils/request'

export default {

// 根据id删除讲师
deleteTeacherById(id) {
return request({
url: `/eduservice/teacher/${id}`,
method: 'delete',
})
}

}

编写按钮对应方法

编写按钮对应方法,考虑到用户体验,添加element-ui弹框提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
removeDataById(id) {
this.$confirm('此操作将永久删除该讲师记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
// 调用删除接口
teacher
.deleteTeacherById(id)
.then(response => {
// 显示提示信息
this.$message({
type: 'success',
message: '删除成功!'
})
// 刷新列表信息
// 如果是最后一页最后一个记录(total-(page-1)*limit == 1)
if (this.total - (this.page - 1) * this.limit == 1) {
this.getList(this.page - 1)
} else {
this.getList(this.page)
}
})
})
.catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
}

当前项目结构

image-20210922181723945

添加讲师

添加页面结构

src\views\edu\teacher\save.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<template>
<div class="app-container">
<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-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="请选择"
>
<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>
<el-button
:disabled="saveBtnDisabled"
type="primary"
@click="saveOrUpdate"
>保存</el-button
>
</el-form-item>
</el-form>
</div>
</div>
</template>

<script>
// 引入调用接口的js文件
import teacherApi from '@/api/edu/teacher.js'

export default {
data() {
return {
teacher: {
name: '',
sort: 0,
level: 1,
career: '',
intro: '',
avatar: ''
},
saveBtnDisabled: false
}
},
created() {},
methods: {}
}
</script>

编写接口调用方法

1
2
3
4
5
6
7
8
// 添加讲师
addTeacher(teacher) {
return request({
url: `/eduservice/teacher/addTeacher`,
method: 'post',
data: teacher
})
}

编写按钮对应方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script>
// 引入调用接口的js文件
import teacherApi from '@/api/edu/teacher.js'

export default {
data() {
return {
teacher: {
name: '',
sort: 0,
level: 1,
career: '',
intro: '',
avatar: ''
},
saveBtnDisabled: false
}
},
created() {},
methods: {
// 添加和修改使用同一个页面
saveOrUpdate() {
this.saveTeacher()
},
saveTeacher() {
teacherApi.addTeacher(this.teacher).then(response => {
// 显示提示信息
this.$message({
type: 'success',
message: '添加成功!'
})
// 路由跳转到列表页面
this.$router.push({ path: '/teacher/list' })
})
}
}
}
</script>

优化讲师列表排序

根据添加时间降序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ApiOperation("多条件组合分页")
@PostMapping("pageTeacherCondition/{current}/{limit}")
public R pageListTeacherCondition(@PathVariable long current,
@PathVariable long limit,
@RequestBody(required = false) TeacherQuery teacherQuery) {
// ……

// 添加排序条件
wrapper.orderByDesc("gmt_create");

// 调用service方法将page对象根据条件分页
teacherService.page(pageTeacher, wrapper);

// ……
}

修改讲师

编写页面结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<!-- router跳转 -->
<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>

隐藏路由

此处需要额外添加router,且不显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export const constantRouterMap = [
{
path: '/teacher',
component: Layout,
redirect: '/teacher/table',
name: '讲师管理',
meta: { title: '讲师管理', icon: 'example' },
children: [
{
path: 'list',
name: '讲师列表',
component: () => import('@/views/edu/teacher/list'),
meta: { title: '讲师列表', icon: 'table' }
},
{
path: 'save',
name: '添加讲师',
component: () => import('@/views/edu/teacher/save'),
meta: { title: '添加讲师', icon: 'tree' }
},
{
// 带有id参数,:为占位符
path: 'edit/:id',
name: 'EduTeacherEdit',
// 此处进入的vue组件为save.vue
component: () => import('@/views/edu/teacher/save'),
meta: { title: '编辑讲师', icon: 'tree' },
// 不显示该路由
hidden: true
}
]
},
]

数据回显

编写后端接口调用方法

1
2
3
4
5
6
7
// 根据id获取讲师信息
getTeacherInfo(id) {
return request({
url: `/eduservice/teacher/getTeacher/${id}`,
method: 'get',
})
}

编写调用该方法,注意要在需要显示该信息的组件中调用该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
created() {
// 判断当前路径是否含有id,有则为修改,否则为添加
if (this.$route.params && this.$route.params.id) {
// 从路径中获取参数值
const id = this.$route.params.id
this.getTeacherInfo(id)
}
},
methods: {
// 根据id获得讲师信息
getTeacherInfo(id) {
teacherApi.getTeacherInfo(id).then(response => {
this.teacher = response.data.teacher
})
}
}
}
</script>

保存修改

编写后端接口调用方法

1
2
3
4
5
6
7
8
// 修改讲师信息
updateTeacher(teacher) {
return request({
url: `/eduservice/teacher/updateTeacher`,
method: 'post',
data: teacher
})
}

页面调用该方法,需要判断是修改还是添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script>
export default {
methods: {
// 添加和修改使用同一个页面
saveOrUpdate() {
// 判断是否有id
if (this.teacher.id) {
this.updateTeacher()
} else {
this.saveTeacher()
}
},
// 修改讲师信息
updateTeacher() {
teacherApi.updateTeacher(this.teacher).then(response => {
// 显示提示信息
this.$message({
type: 'success',
message: '修改成功!'
})
// 路由跳转到列表页面
this.$router.push({ path: '/teacher/list' })
})
}
}
}
</script>

路由切换存留信息问题

感谢弹幕提醒,使用监听路由的方法,可以防止当点击修改后在edit页面再次点击添加讲师,显示后的页面仍然留存讲师信息的小问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
export default {
// 监听
watch: {
// 监听路由是否发生了变化,如果是则清除当前页面的数据
$route: {
handler: function(val, oldVal) {
this.resetData()
}
}
},
methods: {
// 清空数据
resetData() {
this.teacher = {
name: '',
sort: 0,
level: 1,
career: '',
intro: '',
avatar: ''
}
}
}
}
</script>

好吧原来下一集就把这个问题解决了,原因是多次跳转至同一个页面created钩子函数只会执行一次

以下是老师的方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script>
export default {
// 监听
watch: {
// 监听路由是否发生了变化,如果是则清除当前页面的数据
$route(to, from) {
this.init()
}
},
created() {
// 这里可以不用调用初始化方法,已经设置了监听
this.init()
},
methods: {
// 初始化
init() {
// 判断当前路径是否含有id,有则为修改,否则为添加
if (this.$route.params && this.$route.params.id) {
// 从路径中获取参数值
const id = this.$route.params.id
this.getTeacherInfo(id)
} else {
this.teacher = {
name: '',
sort: 0,
level: 1,
career: '',
intro: '',
avatar: ''
}
}
}
}
}
</script>

nginx

反向代理服务器,功能如下:

  1. 请求转发
  2. 负载均衡
  3. 动静分离

基础命令

  1. 启动:nginx.exe
  2. 关闭:nginx.exe -s stop
  3. 重启:nginx.exe -s reload

配置

nginx.conf文件中配置

  1. 修改第一个server下的listen端口号 -> 81 (最好改一下)
  2. 添加自己的server,其中listen监听的端口号要和前端config中的端口号(BASE_PATH)对应

例如相当于访问9001下的xxx,就会转发到8001下的xxx或者其他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 9001;
server_name localhost;

location ~ /eduservice/ {
proxy_pass http://localhost:8001;
}
location ~ /eduoss/ {
proxy_pass http://localhost:8002;
}
location ~ /eduvod/ {
proxy_pass http://localhost:8003;
}
}

记得修改前端中的BASE_PATH和监听端口一致

测试

application和npm都打开,前端测试一下,查看network中的header是否为nginx中的监听端口号

上传头像功能(后端+前端)

阿里云OSS

简单来说:解决海量数据存储与弹性扩容

简单入门

  1. 开通就能进控制台了
  2. 创建bucket
  3. 文件管理中可以进行上传文件等操作
  4. 创建access key获得id和秘钥

小小的吐槽:当天给我的账户里充了1rmb(巨额,确信),配置了一些OSS的东西,第二天就来了个专属客服打电话给我,向我介绍各种产品(上次刚注册的时候也给我打了电话,里面的小姐姐说话都好好听)服务太热情了!

后端阿里云OSS搭建

去官方文档查看详细介绍

快速入门 (aliyun.com)

创建对应子模块

service模块 -> service_oss子模块即可

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<!-- 阿里云oss依赖 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>

<!-- 日期工具栏依赖 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
</dependencies>

配置application.properties

aliyun.oss.file后配置阿里云OSS和AccessKey相关信息

1
2
3
4
5
6
7
server.port=8002
spring.application.name=service-oss
spring.profiles.active=dev
aliyun.oss.file.endpoint=oss-cn-beijing.aliyuncs.com
aliyun.oss.file.keyid=
aliyun.oss.file.keysecret=
aliyun.oss.file.bucketname=demo-edu-guli

创建启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.oss;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = {"com.atguigu"})
public class OssApplication {
public static void main(String[] args) {
SpringApplication.run(OssApplication.class, args);
}
}

由于只使用上传的功能,所以不需要操作数据库,需要配置默认不加载数据库(否则会报错)

1
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // exclude排除这个数据源配置类

编写常量类

用于获取配置文件中的常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.atguigu.oss.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ConstantPropertiesUtils implements InitializingBean {
// 读取配置文件内容
@Value("${aliyun.oss.file.endpoint}") // 自动属性注入(注意此处属性名要与properties文件中的名字对应)
private String endpoint;
@Value("${aliyun.oss.file.keyid}")
private String keyid;
@Value("${aliyun.oss.file.keysecret}")
private String keysecret;
@Value("${aliyun.oss.file.bucketname}")
private String bucketname;

// 定义公开静态常量
public static String END_POINT;
public static String ACCESS_KEY_ID;
public static String ACCESS_KEY_SECRET;
public static String BUCKET_NAME;

// 当初始化properties完成后该方法会执行
@Override
public void afterPropertiesSet() throws Exception {
END_POINT = endpoint;
ACCESS_KEY_ID = keyid;
ACCESS_KEY_SECRET = keysecret;
BUCKET_NAME = bucketname;
}
}

编写Controller

实现文件获取和回传url和页面跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.atguigu.oss.controller;

import com.atguigu.commonutils.R;
import com.atguigu.oss.service.OssService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/eduoss/fileoss")
@CrossOrigin // 解决跨域
public class OssController {

@Autowired
private OssService ossService;

// 上传头像
@PostMapping()
public R uploadOssFile(MultipartFile file) {
// 获取到上传的文件(MultipartFile)
String url = ossService.uploadFileAvatar(file);
return R.ok().data("url", url);
}

}

主要业务逻辑编写

编写Service实现类

先创建接口和实现类,实现类中实现上传业务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.atguigu.oss.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.atguigu.oss.service.OssService;
import com.atguigu.oss.utils.ConstantPropertiesUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Service
public class OssServiceImpl implements OssService {
@Override
public String uploadFileAvatar(MultipartFile file) {
// 简单上传文件格式(oss文档修改即可)
String endpoint = ConstantPropertiesUtils.END_POINT;
String accessKeyId = ConstantPropertiesUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantPropertiesUtils.ACCESS_KEY_SECRET;
String bucketName = ConstantPropertiesUtils.BUCKET_NAME;
String fileName = file.getOriginalFilename();

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

// 上传文件
try {
//依次填写Bucket名称、Object完整路径(文件路径+文件名称)、文件流。Object完整路径中不能包含Bucket名称。
ossClient.putObject(bucketName, fileName, file.getInputStream());
// 拼接oss中文件路径
String url = "https://" + bucketName + "." + endpoint + "/" + fileName;
return url;
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
// 关闭OSSClient。
ossClient.shutdown();
}
}
}

完善Service

防止文件名重复

1
2
3
4
5
6
// 为文件名添加唯一的值
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
fileName = uuid + fileName;
// 将文件进行分类管理,按照日期分类,使用工具类
String datePath = new DateTime().toString("yyyy/MM/dd");
fileName = datePath + "/" + fileName;

上传头像功能(前端)

添加页面结构

element-UI里有相关组件(ImageCropper、PanThumb),cv即可

注意要把组件复制到conponents里

注意修改image-cropper中绑定的url、filed中属性值要与后端接口方法中参数名对应

在添加讲师的页面添加如下标签及代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<template>
<div class="app-container">
<div class="app-container">
<el-form label-width="120px">
<!-- 讲师头像 -->
<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>
</div>
</div>
</template>

<script>
// 上传头像所需组件
import ImageCropper from '@/components/ImageCropper'
import PanThumb from '@/components/PanThumb'
export default {
// 声明组件
components: { ImageCropper, PanThumb },
data() {
return {
// 上传弹框组件是否显示
imagecropperShow: false,
// 上传组件key值
imagecropperKey: 0,
// 获取接口API,固定写法
BASE_API: process.env.BASE_API
}
},
methods: {
// 关闭上传弹框
close() {},
// 上传成功
cropSuccess() {}
}
}
</script>

使用组件

import以及声明即可

1
2
3
4
5
6
7
8
9
<script>
// 上传头像所需组件
import ImageCropper from '@/components/ImageCropper'
import PanThumb from '@/components/PanThumb'
export default {
// 声明组件
components: { ImageCropper, PanThumb },
}
</script>

编写按钮方法

1
2
3
4
5
6
7
8
9
10
11
// 关闭上传弹框
close() {
this.imagecropperShow = false
// 上传组件初始化
this.imagecropperKey = this.imagecropperKey + 1
},
// 上传成功(获得头像url并显示)
cropSuccess(data) {
this.teacher.avatar = data.url
this.close()
}

测试,报错与解决✅

报了一个错,显示上传失败,去看network里的请求URL里的地址不对,中间多了一个undefined,想到可能变量名写错了,稍微对比了一下,BASE_API写成BASE_PATH了,改了就可以了

课程分类管理模块(后端+前端)

记得先导入数据库

添加使用上传excel文件的形式

EasyExcel入门

EasyExcel · 语雀 (yuque.com)

记得引入依赖坐标,同时需要poi的,不过父模块已经引入了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- EasyExcel依赖 -->
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>

<!--以下是父模块中的相关依赖-->
<!--xls-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>

<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>

<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>

写Excel

先创建实体类(一级分类、二级分类)

属性需要全小写

1
2
3
4
5
6
7
8
@Data
public class DemoData {
// 实体类,其中属性和Excel表格对应
@ExcelProperty("学生编号") // 该注释用于定义表头
private Integer sno;
@ExcelProperty("学生姓名")
private String sname;
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestEasyExcel {

public static void main(String[] args) {
// 写Excel
// 1、设置文件地址和名称
String filename = "D:\\MyProject\\excel\\test.xlsx";
// 调用easyExcel方法写入, 可以设置sheet, 执行doWrite传入数据
EasyExcel.write(filename, DemoData.class).sheet("学生列表").doWrite(getData());

}

private static List<DemoData> getData() {
List<DemoData> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
DemoData data = new DemoData();
data.setSname("aaa" + i);
data.setSno(i);
list.add(data);
}
return list;
}
}

读Excel

稍微修改一下实体类

1
2
3
4
5
6
7
8
@Data
public class DemoData {
// 实体类,其中属性和Excel表格对应
@ExcelProperty(value = "学生编号", index = 0) // 该注释用于定义表头
private Integer sno;
@ExcelProperty(value = "学生姓名", index = 1) // index标记第几列,读操作需要
private String sname;
}

创建监听器对excel文件的读取(一行一行的读取需要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 监听器
public class ExcelListener extends AnalysisEventListener<DemoData> {
// 读取行数据到实体类对象中
@Override
public void invoke(DemoData demoData, AnalysisContext analysisContext) {
System.out.println("*****" + demoData + "*****");
}

// 读取完成之后执行
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {

}

// 读取表头内容
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头:" + headMap);
}
}

测试代码

1
2
String filename = "D:\\MyProject\\excel\\test.xlsx";
EasyExcel.read(filename, DemoData.class, new ExcelListener()).sheet().doRead();

代码生成器

先生成entity、service、mapper、controller再说

操作还是一样,详细代码见1,只需要修改表名运行即可

1
2
// 一半不同模块需要修改的地方就是表名
strategy.setInclude("edu_subject"); //加载表

记得为自动生成的实体类添加自动填充(@TableField(fill = FieldFill.INSERT)等)的注解

注意controller添加注释@CrossOrigin解决跨域

添加课程分类(后端)

创建表格实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* subject表格实体类
*/
@Data
public class SubjectData {

@ExcelProperty(index = 0)
private String oneSubjectName;

@ExcelProperty(index = 1)
private String twoSubjectName;

}

编写service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class EduSubjectServiceImpl extends ServiceImpl<EduSubjectMapper, EduSubject> implements EduSubjectService {

@Override
public void saveSubject(MultipartFile file) {
try {
// 获得文件输入流
InputStream inputStream = file.getInputStream();
// 调用方法读取
EasyExcel.read(inputStream, SubjectData.class, new SubjectExcelListener()).sheet().doRead();
} catch (Exception e) {
e.printStackTrace();
}
}
}

编写监听器

注意DataListener 不能被spring管理(官方说的),如果使用spring需要使用特定构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.atguigu.eduservice.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.atguigu.eduservice.entity.EduSubject;
import com.atguigu.eduservice.entity.excel.SubjectData;
import com.atguigu.eduservice.service.EduSubjectService;
import com.atguigu.servicebase.ExceptionHandler.GuliException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

public class SubjectExcelListener extends AnalysisEventListener<SubjectData> {

public EduSubjectService subjectService;

public SubjectExcelListener() {
}

// 当new当前对象的时候同时注入service
public SubjectExcelListener(EduSubjectService subjectService) {
this.subjectService = subjectService;
}

// 读取excel内容,写入数据库
@Override
public void invoke(SubjectData subjectData, AnalysisContext analysisContext) {
if (subjectData == null) {
throw new GuliException(20001, "文件数据为空");
}
// 添加一级分类,先判断一级分类是否已经存在
EduSubject existOneSubject = this.existSubject(subjectData.getOneSubjectName(), "0");
if (existOneSubject == null) {
// 此时没有相同的一级分类,需要添加
EduSubject eduSubject = new EduSubject();
eduSubject.setParentId("0");
eduSubject.setTitle(subjectData.getOneSubjectName());
subjectService.save(eduSubject);
} else {
//添加二级分类,先判断
EduSubject eduTwoSubject = this.existSubject(subjectData.getTwoSubjectName(), existOneSubject.getId());
if (eduTwoSubject == null) {
// 此时没有相同的一级分类,需要添加
EduSubject eduSubject = new EduSubject();
eduSubject.setParentId(existOneSubject.getId());
eduSubject.setTitle(subjectData.getTwoSubjectName());
subjectService.save(eduSubject);
}
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {

}

// 用于判断分类是否存在
public EduSubject existSubject(String title, String pid) {
// 添加查询条件
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("title", title);
// pid=0为一级分类
wrapper.eq("parent_id", pid);
return subjectService.getOne(wrapper);
}
}

同时修改一下service中方法的参数

其实这里有点奇怪,listener做了主要业务逻辑,等做完了再试试别的方法

编写controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/eduservice/subject")
@CrossOrigin
public class EduSubjectController {

@Autowired
private EduSubjectService subjectService;

// 添加课程分类
// 获取上传过来的文件,把文件内容读取出来即可
@PostMapping("addSubject")
public R addSubject(MultipartFile file) {
// 获得上传过来的excel文件
subjectService.saveSubject(file, subjectService);
return R.ok();
}
}

测试,报错与解决✅

swagger成功,后台报错,看了下说gtm_time没有默认值,然后想到自动注入可能没加,果然。

然后是能够写入数据库了但是一级写到二级去了,看了下是添加二级分类的时候的get方法成getOneSubjectName而不是getTwoSubjectName,改一下就解决了

添加课程分类(前端)

配置router

src -> router -> index.js 当中添加即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 课程管理模块路由
{
path: '/subject',
component: Layout,
redirect: '/subject/list',
name: '课程分类管理',
meta: { title: '课程分类管理', icon: 'example' },
children: [
{
path: 'list',
name: '课程分类列表',
component: () => import('@/views/edu/subject/list'),
meta: { title: '课程分类列表', icon: 'tree' }
},
{
path: 'save',
name: '添加课程分类',
component: () => import('@/views/edu/subject/add'),
meta: { title: '添加课程分类', icon: 'table' }
},
]
},

编写添加课程分类视图

element-ui 中 cv一下(文件上传相关)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div class="app-container">
<el-form label-width="120px">
<el-form-item label="信息描述">
<el-tag type="info">excel模版说明</el-tag>
<el-tag>
<i class="el-icon-download" />
<!-- 这里模板放在了本地static文件夹里,也可以放在OSS当中 -->
<a :href="'/static/demo.xlsx'">点击下载模版</a>
</el-tag>
</el-form-item>

<el-form-item label="选择Excel">
<el-upload
ref="upload"
:auto-upload="false"
:on-success="fileUploadSuccess"
:on-error="fileUploadError"
:disabled="importBtnDisabled"
:limit="1"
:action="BASE_API + '/eduservice/subject/addSubject'"
name="file"
accept="application/vnd.ms-excel"
>
<el-button slot="trigger" size="small" type="primary"
>选取文件</el-button
>
<el-button
:loading="loading"
style="margin-left: 10px"
size="small"
type="success"
@click="submitUpload"
>上传到服务器</el-button
>
</el-upload>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
BASE_API: process.env.BASE_API, // 接口API地址
OSS_PATH: process.env.OSS_PATH, // 阿里云OSS地址
importBtnDisabled: false, // 按钮是否禁用,
loading: false
}
},
created() {},
methods: {}
}
</script>

编写调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
methods: {
// 上传文件
submitUpload() {
this.importBtnDisabled = true
this.loading = true
// 使用vue的提交,而不是Ajax请求
this.$refs.upload.submit()
},
// 上传成功
fileUploadSuccess() {
this.loading = false
this.$message({
type: 'success',
message: '添加课程分类成功!'
})
},
// 上传失败
fileUploadError() {
this.loading = false
this.$message({
type: 'success',
message: '添加课程分类失败!'
})
}
}

完善

  1. el-upload添加一个 :before-upload=”beforeUpload”,用于上传前判断文件后缀格式
  2. 上传文件时判断文件是否为空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<template>
<div class="app-container">
<el-form label-width="120px">
<el-form-item label="选择Excel">
<el-upload
ref="upload"
:auto-upload="true"
:before-upload="beforeUpload"
:on-success="fileUploadSuccess"
:on-error="fileUploadError"
:disabled="importBtnDisabled"
:limit="1"
:action="BASE_API + '/eduservice/subject/addSubject'"
name="file"
accept="application.xlsx"
>
<el-button slot="trigger" size="small" type="primary"
>选取文件</el-button
>
<el-button
:loading="loading"
style="margin-left: 10px"
size="small"
type="success"
@click="submitUpload"
>上传到服务器</el-button
>
</el-upload>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
methods: {
// 上传前判断格式
beforeUpload() {
const fileName = this.$refs.upload.uploadFiles[0].name
const suffix = fileName.substring(fileName.lastIndexOf('.') + 1)
if (suffix != 'xlsx') {
this.fileSuffixError()
return false
}
},
// 上传文件
submitUpload() {
if (this.$refs.upload.uploadFiles.length == 0) {
this.fileUploadError()
return
}
this.importBtnDisabled = true
this.loading = true
// 使用vue的提交,而不是Ajax请求
this.$refs.upload.submit()
},
// 文件格式错误失败
fileSuffixError() {
this.loading = false
this.$message({
type: 'error',
message: '文件格式错误!'
})
}
}
}
</script>

课程分类列表(后端)

针对返回的数据创建对应实体类

一级分类、二级分类都要创建对应实体类

1
2
3
4
5
6
7
// 一级分类
@Data
public class OneSubject {
private String id;
private String title;
private List<TwoSubject> children = new ArrayList<>();
}
1
2
3
4
5
6
// 二级分类
@Data
public class TwoSubject {
private String id;
private String title;
}

编写controller

1
2
3
4
5
6
// 课程分类列表(树形)
@GetMapping("getAllSubjects")
public R getAllSubjects() {
List<OneSubject> list = subjectService.getAllSubjects();
return R.ok().data("list", "list");
}

编写service

service继承的ServiceImpl已经自动注入了baseMapper

这里的代码不够优雅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Override
public List<OneSubject> getAllSubjects() {
// 查询所有一级分类
QueryWrapper<EduSubject> wrapperOne = new QueryWrapper<>();
wrapperOne.eq("parent_id", "0");
List<EduSubject> oneSubjectList = baseMapper.selectList(wrapperOne);
// 查询所有二级分类
QueryWrapper<EduSubject> wrapperTwo = new QueryWrapper<>();
wrapperTwo.ne("parent_id", "0");
List<EduSubject> twoSubjectList = baseMapper.selectList(wrapperTwo);

List<OneSubject> finalList = new ArrayList<>();


for (int i = 0; i < oneSubjectList.size(); i++) {
// 封装一级
EduSubject eduSubjectOne = oneSubjectList.get(i);
OneSubject oneSubject = new OneSubject();
BeanUtils.copyProperties(eduSubjectOne, oneSubject);
finalList.add(oneSubject);

//封装二级分类
List<TwoSubject> childrenList = new ArrayList<>();
//遍历二级分类list集合
for (int m = 0; m < twoSubjectList.size(); m++) {
//获取每个二级分类
EduSubject eduSubjectTwo = twoSubjectList.get(m);
//判断二级分类parentid和一级分类id是否一样
if (eduSubjectTwo.getParentId().equals(eduSubjectOne.getId())) {
//把tSubject值复制到TwoSubject里面,放到twoFinalSubjectList里面
TwoSubject twoSubject = new TwoSubject();
BeanUtils.copyProperties(eduSubjectTwo, twoSubject);
childrenList.add(twoSubject);
}
}
//把一级下面所有二级分类放到一级分类里面
oneSubject.setChildren(childrenList);
}

return finalList;
}

课程分类列表(前端)

树形结构模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<template>
<div class="app-container">
<!-- 检索 -->
<el-input
v-model="filterText"
placeholder="Filter keyword"
style="margin-bottom: 30px"
/>
<!-- 树形结构 -->
<el-tree
ref="tree2"
:data="data2"
:props="defaultProps"
:filter-node-method="filterNode"
class="filter-tree"
default-expand-all
/>
</div>
</template>

<script>
export default {
data() {
return {
filterText: '',
data2: [
{
id: 1,
label: 'Level one 1',
children: [
{
id: 4,
label: 'Level two 1-1',
children: [
{
id: 9,
label: 'Level three 1-1-1'
},
{
id: 10,
label: 'Level three 1-1-2'
}
]
}
]
},
{
id: 2,
label: 'Level one 2',
children: [
{
id: 5,
label: 'Level two 2-1'
},
{
id: 6,
label: 'Level two 2-2'
}
]
},
{
id: 3,
label: 'Level one 3',
children: [
{
id: 7,
label: 'Level two 3-1'
},
{
id: 8,
label: 'Level two 3-2'
}
]
}
],
defaultProps: {
children: 'children',
label: 'label'
}
}
},
watch: {
filterText(val) {
this.$refs.tree2.filter(val)
}
},

methods: {
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
}
}
}
</script>

需要稍微修改

后端接口调用

api -> edu -> subject.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import request from '@/utils/request'

export default {

// 课程分类列表
getSubjectList() {
return request({
url: `/eduservice/subject/getAllSubjects`,
method: 'get'
})
}

}

记得在vue视图中引入

vue视图和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<template>
<div class="app-container">
<!-- 检索 -->
<el-input
v-model="filterText"
placeholder="Filter keyword"
style="margin-bottom: 30px"
/>
<!-- 树形结构 -->
<el-tree
ref="tree2"
:data="list"
:props="defaultProps"
:filter-node-method="filterNode"
class="filter-tree"
default-expand-all
/>
</div>
</template>

<script>
// 引入调用接口的js文件
import subjectApi from '@/api/edu/subject.js'
export default {
data() {
return {
// 索引文本
filterText: '',
list: [],
defaultProps: {
children: 'children',
label: 'title'
}
}
},
watch: {
filterText(val) {
this.$refs.tree2.filter(val)
}
},
created() {
this.getAllSubjectList()
},
methods: {
// 获得所有subject
getAllSubjectList() {
subjectApi.getSubjectList().then(result => {
this.list = result.data.list
})
},
// 过滤文本(不区分大小写)
filterNode(value, data) {
if (!value) return true
return data.title.toLowerCase().indexOf(value.toLowerCase()) !== -1
}
}
}
</script>

测试

记得开nginx

待续

进度1/3

评论




🧡💛💚💙💜🖤🤍