我寄愁心与爪哇

如果能一步登天,
那别人的坚持又算什么呢?

0%

谷粒学院-day09

个人笔记,仅供参考

一、总览

1.课程管理目标:

  • 发布课程
    • 编辑课程基本信息
    • 创建课程大纲
      • 添加章节
      • 添加课时
    • 发布课程预览
  • 课程列表
    • 搜索栏
    • 列表
    • 分页条

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
// 课程管理
{
path: '/course',
component: Layout,
redirect: '/course/list',
name: 'Course',
meta: { title: '课程管理' },
children: [
{
path: 'list',
name: 'CourseList',
component: () => import('@/views/course/list'),
meta: { title: '课程列表' }
},
{
path: 'info',
name: 'CourseInfo',
component: () => import('@/views/course/form'),
meta: { title: '发布课程' }
},
{
path: 'info/:id',
name: 'CourseInfoEdit',
component: () => import('@/views/course/form'),
meta: { title: '编辑课程' },
hidden: true
},
{
path: 'chapter/:id',
name: 'CourseChapterEdit',
component: () => import('@/views/course/form'),
meta: { title: '编辑大纲' },
hidden: true
}
]
},

3.发布课程结构

发布课程1

  • el-steps
  • 三个步骤的主体页面
    • 创建子组件,然后根据v-if控制是否显示。
    • 这里主体页面跟随步骤导航条的变化而变化,所以v-if判断active的值决定渲染哪个页面。

4.新建Vue文件

image-20220506212152907

3.1.1form.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">

<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="active" finish-status="success" simple style="margin-bottom: 40px">
<el-step title="填写课程基本信息" />
<el-step title="创建课程大纲" />
<el-step title="发布课程" />
</el-steps>

<!-- 填写课程基本信息 -->
<info v-if="active === 0" />

<!-- 创建课程大纲 -->
<chapter v-if="active === 1" />

<!-- 发布课程 -->
<Publish v-if="active === 2 || active === 3" />

</div>
</template>

<script>
// 引入子组件
import Info from '@/views/course/components/Info'
import Chapter from '@/views/course/components/Chapter'
import Publish from '@/views/course/components/Publish'

export default {
components: { Info, Chapter, Publish }, // 注册子组件
data() {
return {
active: 0,
courseId: null
}
}
}
</script>
  • 对于import Chapter from ‘@/views/course/components/Chapter’
    • 先找…/components/Chapter.vue
    • 再找…/components/Chapter/Index.vue 或 Chapter.vue(与文件夹同名)
  • courseId是否有值决定了是新增还是修改

3.1.2components/Info.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
<template>
<div class="app-container">

<!-- 课程信息表单 TODO -->

<div style="text-align:center">
<el-button :disabled="saveBtnDisabled" type="primary" @click="saveAndNext()">保存并下一步</el-button>
</div>
</div>
</template>

<script>
export default {
data() {
return {
saveBtnDisabled: false // 按钮是否禁用
}
},

methods: {
// 保存并下一步
saveAndNext() {
this.saveBtnDisabled = true
this.$parent.active = 1
}
}
}
</script>

3.1.3components/Chapter/Index.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
<template>
<div class="app-container">

<!-- 章节列表 TODO -->

<div style="text-align:center">
<el-button type="primary" @click="prev()">上一步</el-button>
<el-button type="primary" @click="next()">下一步</el-button>
</div>
</div>
</template>

<script>
export default {

methods: {

// 上一步
prev() {
this.$parent.active = 0
},

// 下一步
next() {
this.$parent.active = 2
}
}
}
</script>

3.1.4components/Publish.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
<template>
<div class="app-container">

<!--课程预览 TODO-->

<div style="text-align:center">
<el-button type="primary" @click="prev()">上一步</el-button>
<el-button :disabled="publishBtnDisabled" type="primary" @click="publish()">发布课程</el-button>
</div>
</div>
</template>

<script>
export default {
data() {
return {
publishBtnDisabled: false // 按钮是否禁用
}
},

methods: {

// 上一步
prev() {
this.$parent.active = 1
},

// 下一步
publish() {
this.publishBtnDisabled = true
this.$parent.active = 3
}
}
}
</script>

3.1.5list.vue

  • 创建文件,内容暂时留空
  • 用于课程列表展示

二、发布课程-课程基本信息

0.说明

先写发布课程中第一个步骤,编辑课程基本信息的页面。

1.后端

1.1思路

  • 接收:收集表单数据,封装实体类CourseInfoForm

  • 返回:客户端只需要保存存入数据库课程对应的courseId,所以返回的R对象中保存courseId并返回

1.2实体类

根据表单数据创建实体类对象

…/edu/entity/vo/CourseInfoForm.java

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
package com.atguigu.guli.service.edu.entity.form;

@ApiModel("课程基本信息")
@Data
public class CourseInfoForm implements Serializable {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "课程ID")
private String id;

@ApiModelProperty(value = "课程讲师ID")
private String teacherId;

@ApiModelProperty(value = "课程专业ID")
private String subjectId;

@ApiModelProperty(value = "课程专业父级ID")
private String subjectParentId;

@ApiModelProperty(value = "课程标题")
private String title;

@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
private BigDecimal price;

@ApiModelProperty(value = "总课时")
private Integer lessonNum;

@ApiModelProperty(value = "课程封面图片路径")
private String cover;

@ApiModelProperty(value = "课程简介")
private String description;
}

1.3修改

编写接口之前需要修改两个地方,一个是修改CourseDescription主键策略,另一个是为课程状态添加两个常量。

  • 说明
    • 保存课程基本信息需要保存到课程表和课程简介表,对应实体类Course和CourseDescription。
    • 而两张表是一对一的关系,所以我们这里采取主键跟随的策略。
    • 也就是,保存完Course表返回courseId后,再保存CourseDescription,并设置与课程相同的主键(courseId)。
    • 所以这里要将CourseDescription主键策略设置为空。

1.3.1CourseDescription主键策略

修改CourseDescription.java

1
2
3
@ApiModelProperty(value = "ID")
@TableId(value = "id", type = IdType.NONE)
private String id;

1.3.2课程状态常量

Course.java

1
2
public static final String COURSE_DRAFT = "Draft";
public static final String COURSE_NORMAL = "Normal";

1.4接口开发

1.4.1Controller

CourseController.java

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
package com.atguigu.guli.service.edu.controller;


import com.atguigu.guli.common.base.result.R;
import com.atguigu.guli.service.edu.entity.vo.CourseInfoForm;
import com.atguigu.guli.service.edu.service.CourseService;
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.web.bind.annotation.*;

/**
* <p>
* 课程 前端控制器
* </p>
*
* @author Cysheng
* @since 2022-03-01
*/
@Api(description="课程管理")
@CrossOrigin //跨域
@RestController
@RequestMapping("/admin/edu/course")
public class CourseController {
@Autowired
private CourseService courseService;

@ApiOperation("新增课程")
@PostMapping("save-course-info")
public R saveCourseInfo(
@ApiParam(value = "课程基本信息", required = true)
@RequestBody CourseInfoForm courseInfoForm){
String courseId = courseService.saveCourseInfo(courseInfoForm);
return R.ok().data("courseId", courseId).message("保存成功");
}
}

1.4.2Service

CourseService.java

1
String saveCourseInfo(CourseInfoForm courseInfoForm);

CourseServiceImpl.java

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
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService {

@Autowired
private CourseDescriptionMapper courseDescriptionMapper;

@Transactional(rollbackFor = Exception.class)
@Override
public String saveCourseInfo(CourseInfoForm courseInfoForm) {

//保存课程基本信息
Course course = new Course();
course.setStatus(Course.COURSE_DRAFT);
BeanUtils.copyProperties(courseInfoForm, course);
baseMapper.insert(course);

//保存课程详情信息
CourseDescription courseDescription = new CourseDescription();
courseDescription.setDescription(courseInfoForm.getDescription());
courseDescription.setId(course.getId());
courseDescriptionMapper.insert(courseDescription);

return course.getId();
}
}

2.前端

2.1API

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

export default {
saveCourseInfo(courseInfo) {
return request({
url: '/admin/edu/course/save-course-info',
method: 'post',
data: courseInfo
})
}
}

2.2简单字段保存

  • 要保存的字段比较多、比较复杂。这时可以先保存少量简单字段,试着跑通前后端,然后填充复杂字段。

  • 保存少量简单字段,其他字段因为设置了非空字段可能会报错,这里表单中没有的数据需要在data属性中临时指定默认值。

Info.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 课程信息表单 -->
<el-form label-width="120px">

<el-form-item label="课程标题">
<el-input v-model="courseInfo.title" placeholder=" 示例:机器学习项目课:从基础到搭建项目视频课程。专业名称注意大小写"/>
</el-form-item>

<!-- 课程讲师 TODO -->

<!-- 所属分类 TODO -->

<el-form-item label="总课时">
<el-input-number :min="0" v-model="courseInfo.lessonNum" controls-position="right" placeholder="请填写课程的总课时数"/>
</el-form-item>

<!-- 课程简介 TODO -->

<!-- 课程封面 TODO -->

<el-form-item label="课程价格">
<el-input-number :min="0" v-model="courseInfo.price" controls-position="right" placeholder="免费课程请设置为0元"/>
</el-form-item>
</el-form>

Info.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
<script>
import courseApi from '@/api/course'

export default {
data() {
return {
saveBtnDisabled: false, // 按钮是否禁用
courseInfo: {// 表单数据
price: 0,
lessonNum: 0,
// 以下解决表单数据不全时insert语句非空校验
teacherId: '',
subjectId: '',
subjectParentId: '',
cover: '',
description: ''
}
}
},

methods: {
// 保存并下一步
saveAndNext() {
this.saveBtnDisabled = true
this.saveData()
},

// 保存
saveData() {
courseApi.saveCourseInfo(this.courseInfo).then(response => {
this.$message.success(response.message)
this.$parent.courseId = response.data.courseId // 获取courseId
this.$parent.active = 1 // 下一步
})
}
}
}
</script>

注:调用父组件:this.$parent

2.3其他字段保存

2.3.1讲师下拉框

查询讲师列表并渲染到下拉框组件

Info.vue

  • 定义data,保存下拉框选项
1
teacherList: [] // 讲师列表
  • 引入teacher API
1
import teacherApi from '@/api/teacher'
  • 调用teacherApi
1
2
3
4
5
initTeacherList() {
teacherApi.list().then(response => {
this.teacherList = response.data.items
})
},
1
2
3
4
created() {
// 获取讲师列表
this.initTeacherList()
},
  • 组件模板
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 课程讲师 -->
<el-form-item label="课程讲师">
<el-select
v-model="courseInfo.teacherId"
placeholder="请选择">
<el-option
v-for="teacher in teacherList"
:key="teacher.id"
:label="teacher.name"
:value="teacher.id"/>
</el-select>
</el-form-item>

2.3.2课程分类

课程分类涉及到二级连调,而对于二级连调,有两种解决方案。

方案一

  • 一次性从后端取出嵌套的数据。当切换一级分类时,在select的change事件中渲染对应二级分类。

  • 适用:并发多,数据量少。

方案二

  • 延迟加载。先查询一级分类,根据一级分类id再查询二级分类。切换一级分类后,根据切换后一级分类id查询二级分类。

  • 适用:数据量大,考虑移动端流量消耗

这里我们采用方案一来实现二级连调。

<1>获取嵌套数据
  • 引入API

Info.vue

1
import subjectApi from '@/api/subject'
  • 调用API的方法

提前定义好data接收数据

1
2
subjectList: [],//一级分类列表
subjectLevelTwoList: []//二级分类列表

Info.vue的methods中

1
2
3
4
5
initSubjectList() {
subjectApi.getNestedTreeList().then(response => {
this.subjectList = response.data.items
})
},
  • 页面渲染前调用方法

Info.vue的created中

1
2
3
4
5
6
created() {
// 获取讲师列表
this.initTeacherList()
// 初始化分类列表
this.initSubjectList()
},
<2>展示一级分类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 所属分类:级联下拉列表 -->
<el-form-item label="课程类别">
<!-- 一级分类 -->
<el-select
v-model="courseInfo.subjectParentId"
placeholder="请选择">
<el-option
v-for="subject in subjectList"
:key="subject.id"
:label="subject.title"
:value="subject.id"/>
</el-select>

<!-- 二级分类 TODO -->
</el-form-item>
<3>展示二级分类

一级分类修改时,将对应数据绑定到subjectLevelTwoList,然后在组件模板中渲染。

  • 为el-select添加change事件
1
<el-select @change="subjectChanged" ...
  • 编写change事件对应的方法
1
2
3
4
5
6
7
8
subjectChanged(value) {
this.subjectList.forEach(subject => {
if (subject.id === value) {
this.courseInfo.subjectId = ''
this.subjectLevelTwoList = subject.children
}
})
},
  • 组件模板渲染
1
2
3
4
5
6
7
8
<!-- 二级分类 -->
<el-select v-model="courseInfo.subjectId" placeholder="请选择">
<el-option
v-for="subject in subjectLevelTwoList"
:key="subject.id"
:label="subject.title"
:value="subject.id"/>
</el-select>

2.3.3课程简介

<1>引入Tinymce

课程简介需要先引入富文本编辑器Tinymce。Tinymce是一个传统javascript插件,默认不能用于Vue.js因此需要做一些特殊的整合步骤。

  • 复制脚本库
    • vue-element-admin-master/static/tinymce4.7.5
    • –复制–>
    • guli-admin/static/tinymce4.7.5

  • 引入JS文件

guli-admin/index.html

1
2
<script src="/static/tinymce4.7.5/tinymce.min.js"></script>
<script src="/static/tinymce4.7.5/langs/zh_CN.js"></script>
  • 复制组件

    • vue-element-admin-master/src/components/Tinymce
    • –复制–>
    • guli-admin/src/components/Tinymce
  • 引入组件并注册

Info.vue

1
2
3
4
5
6
import Tinymce from '@/components/Tinymce'

export default {
components: { Tinymce },
......
}

<2>使用

1
2
3
4
<!-- 课程简介-->
<el-form-item label="课程简介">
<tinymce :height="300" v-model="courseInfo.description"/>
</el-form-item>

<3>样式微调

样式有一点瑕疵,需要略微调整。

Info.vue

1
2
3
4
5
6
<style>
.tinymce-container {
position: relative;
line-height: normal;
}
</style>

2.3.4课程封面

课程封面与讲师头像部分流程一样。

后端采取相同接口,不过module参数为cover。

前端:

Info.vue

  • 组件模板
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 课程封面 -->
<el-form-item label="课程封面">
<el-upload
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload"
:on-error="handleCoverError"
class="cover-uploader"
action="http://localhost:8120/admin/oss/file/upload?module=cover">
<img v-if="courseInfo.cover" :src="courseInfo.cover">
<i v-else class="el-icon-plus avatar-uploader-icon"/>
</el-upload>
</el-form-item>
  • 样式
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
<style>
.cover-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.cover-uploader .el-upload:hover {
border-color: #409EFF;
}
.cover-uploader .avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 640px;
height: 357px;
line-height: 357px;
text-align: center;
}
.cover-uploader img {
width: 640px;
height: 357px;
display: block;
}
</style>
  • 脚本
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
// 上传成功回调
handleCoverSuccess(res, file) {
if (res.success) {
// console.log(res)
this.courseInfo.cover = res.data.url
// 强制重新渲染
this.$forceUpdate()
} else {
this.$message.error('上传失败1')
}
},

// 上传校验
beforeCoverUpload(file) {
const isJPG = file.type === 'image/jpeg'
const isLt2M = file.size / 1024 / 1024 < 2

if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 格式!')
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!')
}
return isJPG && isLt2M
},

// 错误处理
handleCoverError() {
console.log('error')
this.$message.error('上传失败2')
},

三、课程信息回显

编辑讲师、创建课程大纲页上一步按钮都需要进行回显。这里先对点击上一步按钮跳转讲师信息页进行回显表单。

1.后端

通过保存在父组件form.vue中的courseId查询出CourseInfoForm对象,然后封装到R对象返回。

1.1Controller

CourseController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
@ApiOperation("根据ID查询课程")
@GetMapping("course-info/{id}")
public R getById(
@ApiParam(value = "课程ID", required = true)
@PathVariable String id){

CourseInfoForm courseInfoForm = courseService.getCourseInfoById(id);
if (courseInfoForm != null) {
return R.ok().data("item", courseInfoForm);
} else {
return R.error().message("数据不存在");
}
}

1.2Service

CourseService.java

1
CourseInfoForm getCourseInfoById(String id);

CourseServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public CourseInfoForm getCourseInfoById(String id) {

//从course表中取数据
Course course = baseMapper.selectById(id);
if(course == null){
return null;
}

//从course_description表中取数据
CourseDescription courseDescription = courseDescriptionMapper.selectById(id);

//创建courseInfoForm对象
CourseInfoForm courseInfoForm = new CourseInfoForm();
BeanUtils.copyProperties(course, courseInfoForm);
courseInfoForm.setDescription(courseDescription.getDescription());

return courseInfoForm;
}

2.前端

前端将查询的数据绑定到courseInfo,页面表单与数据双向绑定,所以会直接回显出来。

2.1API

api/course.js

1
2
3
4
5
6
getCourseInfoById(id) {
return request({
url: `/admin/edu/course/course-info/${id}`,
method: 'get'
})
}

2.2组件js

Info.vue

  • 定义获取courseInfo并绑定的方法
1
2
3
4
5
fetchCourseInfoById(id) {
courseApi.getCourseInfoById(id).then(response => {
this.courseInfo = response.data.item
})
},
  • 页面渲染前判断父组件courseId,如果存在courseId,则回显
1
2
3
4
5
6
7
8
9
created() {
if (this.$parent.courseId) { // 回显
this.fetchCourseInfoById(this.$parent.courseId)
}
// 获取讲师列表
this.initTeacherList()
// 初始化分类列表
this.initSubjectList()
},

2.3二级分类渲染问题

  • 问题说明

    • 因为initSubjectList()方法只将一级分类列表(subjectList)进行绑定,二级分类列表(subjectLevelTwoList)没有绑定,二级列表没有数据。
    • 所以二级分类列表回填时,根据id找不到对应title值,然后值能显示出id值。
  • 解决:填充二级分类列表

    • 遍历一级分类列表,取出与当前一级分类对应的嵌套数据。(subject>children)
    • 将children赋值给subjectLevelTwoList

Info.vue

填充二级分类列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fetchCourseInfoById(id) {
courseApi.getCourseInfoById(id).then(response => {
this.courseInfo = response.data.item

// 初始化分类列表
// this.initSubjectList()
subjectApi.getNestedTreeList().then(response => {
this.subjectList = response.data.items

// 填充二级菜单:遍历一级菜单列表,
this.subjectList.forEach(subject => {
// 找到和courseInfo.subjectParentId一致的父类别记录
if (subject.id === this.courseInfo.subjectParentId) {
// 拿到当前类别下的子类别列表,将子类别列表填入二级下拉菜单列表
this.subjectLevelTwoList = subject.children
}
})
})
})
},

回显状态填充,新增状态不填充

1
2
3
4
5
6
7
8
9
10
created() {
if (this.$parent.courseId) { // 回显
this.fetchCourseInfoById(this.$parent.courseId)
} else { // 新增
// 初始化分类列表
this.initSubjectList()
}
// 获取讲师列表
this.initTeacherList()
},

四、更新

  • 返回上一步之后,再点下一步实现对原有数据表的更新。

  • 判断 更新 or 新增 => courseId

1.后端

CourseController.java

1
2
3
4
5
6
7
8
9
@ApiOperation("更新课程")
@PutMapping("update-course-info")
public R updateCourseInfoById(
@ApiParam(value = "课程基本信息", required = true)
@RequestBody CourseInfoForm courseInfoForm){

courseService.updateCourseInfoById(courseInfoForm);
return R.ok().message("修改成功");
}

CourseService.java

1
void updateCourseInfoById(CourseInfoForm courseInfoForm);

CourseServiceImpl.java

多表修改操作,涉及事务,添加注解@Transactional(rollbackFor = Exception.class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCourseInfoById(CourseInfoForm courseInfoForm) {
//保存课程基本信息
Course course = new Course();
BeanUtils.copyProperties(courseInfoForm, course);
baseMapper.updateById(course);

//保存课程详情信息
CourseDescription courseDescription = new CourseDescription();
courseDescription.setDescription(courseInfoForm.getDescription());
courseDescription.setId(course.getId());
courseDescriptionMapper.updateById(courseDescription);
}

2.前端

2.1API

src/course.js

1
2
3
4
5
6
7
updateCourseInfoById(courseInfo) {
return request({
url: '/admin/edu/course/update-course-info',
method: 'put',
data: courseInfo
})
}

Info.vue

  • 更新
1
2
3
4
5
6
7
// 修改
updateData() {
courseApi.updateCourseInfoById(this.courseInfo).then(response => {
this.$message.success(response.message)
this.$parent.active = 1 // 下一步
})
}
  • 根据courseId判断 新增 or 更新
1
2
3
4
5
6
7
8
9
// 保存并下一步
saveAndNext() {
this.saveBtnDisabled = true
if (!this.$parent.courseId) {
this.saveData()
} else {
this.updateData()
}
}
  • 本文主题: 谷粒学院-day09
  • 本文作者: 我寄愁心与爪哇
  • 本文链接: https://cysheng.gitee.io/77448589.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道