(1)CourseController 添加方法。
package com.myxh.smart.planet.vod.controller;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vod.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author MYXH
* @date 2023/10/8
*
*
* 课程 前端控制器
*
*/
@Tag(name = "课程接口", description = "课程管理接口")
@RestController
@RequestMapping("/admin/vod/course")
public class CourseController
{
@Autowired
private CourseService courseService;
/**
* 查询所有课程
*
* @return Result 全局统一返回结果
*/
@Operation(summary = "查询所有课程", description = "查询所有课程")
@GetMapping("find/all")
public Result<List<Course>> findAll()
{
List<Course> courseList = courseService.findlist();
return Result.ok(courseList);
}
}
(2)CourseService 实现方法。
package com.myxh.smart.planet.vod.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vod.mapper.CourseMapper;
import com.myxh.smart.planet.vod.service.CourseService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author MYXH
* @date 2023/10/8
*
*
* 课程 服务实现类
*
*/
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService
{
/**
* 查询所有课程
*
* @return courseList 所有课程列表
*/
@Override
public List<Course> findlist()
{
List<Course> courseList = baseMapper.selectList(null);
courseList.stream().forEach(this::getTeacherAndSubjectName);
return courseList;
}
}
// 直播管理
{
path: "/live",
component: Layout,
redirect: "/live/liveCourse/list",
name: "Live",
meta: {
title: "直播管理",
icon: "el-icon-bangzhu",
},
alwaysShow: true,
children: [
{
path: "liveCourse/list",
name: "liveCourseList",
component: () => import("@/views/live/liveCourse/list"),
meta: { title: "直播列表" },
},
{
path: "liveCourse/config/:id",
name: "liveCourseConfig",
component: () => import("@/views/live/liveCourse/config"),
meta: { title: "直播配置" },
hidden: true,
},
{
path: "liveVisitor/list/:id",
name: "liveVisitor",
component: () => import("@/views/live/liveVisitor/list"),
meta: { title: "观看记录" },
hidden: true,
},
],
},
import request from "@/utils/request";
const LIVE_COURSE_API = "/admin/live/live/course";
export default {
/**
* 直播课程分页列表
*
* @param {number} current 当前页码
* @param {number} limit 每页记录数
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
getPageList(page, limit) {
return request({
url: `${LIVE_COURSE_API}/find/query/page/${page}/${limit}`,
method: "get",
});
},
/**
* 添加直播课程
*
* @param {Object} liveCourse 直播课程
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
save(liveCourse) {
return request({
url: `${LIVE_COURSE_API}/save`,
method: "post",
data: liveCourse,
});
},
/**
* 删除直播课程
*
* @param {number} id id
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
removeById(id) {
return request({
url: `${LIVE_COURSE_API}/remove/${id}`,
method: "delete",
});
},
/**
* 根据 id 查询直播课程基本信息和描述信息
*
* @param {number} id id
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
getById(id) {
return request({
url: `${LIVE_COURSE_API}/get/info/${id}`,
method: "get",
});
},
/**
* 根据 id 修改直播课程
*
* @param {Object} liveCourse 直播课程
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
updateById(liveCourse) {
return request({
url: `${LIVE_COURSE_API}/update`,
method: "put",
data: liveCourse,
});
},
/**
* 获取直播账号信息
*
* @param {number} id id
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
getLiveCourseAccount(id) {
return request({
url: `${LIVE_COURSE_API}/get/live/course/account/${id}`,
method: "get",
});
},
/**
* 获取直播配置信息
*
* @param {number} id id
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
getCourseConfig(id) {
return request({
url: `${LIVE_COURSE_API}/get/course/config/${id}`,
method: "get",
});
},
/**
* 修改直播配置信息
*
* @param {Object} liveCourseConfigVo 直播配置信息
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
updateConfig(liveCourseConfigVo) {
return request({
url: `${LIVE_COURSE_API}/update/config`,
method: "put",
data: liveCourseConfigVo,
});
},
/**
* 获取最近的直播
*
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
findLatelyList() {
return request({
url: `${LIVE_COURSE_API}/find/lately/list`,
method: "get",
});
},
/**
* 批量删除直播课程
*
* @param {Array}idList id 数组,Json 数组 [1,2,3,...]
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
removeRows(idList) {
return request({
url: `${LIVE_COURSE_API}/remove/batch`,
method: "delete",
data: idList,
});
},
};
import request from "@/utils/request";
const COURSE_API = "/admin/vod/course";
export default {
/**
* 查询所有课程
*
* @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
*/
findAll() {
return request({
url: `${COURSE_API}/find/all`,
method: "get",
});
},
};
<template>
<div class="app-container">
<head>
<meta name="referrer" content="no-referrer" />
head>
<el-card class="operate-container" shadow="never">
<i class="el-icon-tickets" style="margin-top: 5px">i>
<span style="margin-top: 5px">数据列表span>
<el-button class="btn-add" size="mini" @click="add">添 加el-button>
el-card>
<el-table
v-loading="listLoading"
:data="list"
stripe
border
style="width: 100%; margin-top: 10px"
>
<el-table-column label="序号" width="50" align="center">
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
template>
el-table-column>
<el-table-column label="封面" width="200" align="center">
<template slot-scope="scope">
<img :src="scope.row.cover" width="100%" />
template>
el-table-column>
<el-table-column prop="courseName" label="直播名称" />
<el-table-column prop="startTime" label="直播时间">
<template slot-scope="scope">
{{ scope.row.startTime }} 至 {{ scope.row.endTime }}
template>
el-table-column>
<el-table-column prop="endTime" label="直播结束时间" />
<el-table-column prop="param.teacherName" label="直播教师" />
<el-table-column label="头衔" width="90">
<template slot-scope="scope">
<el-tag
v-if="scope.row.param.teacherLevel === 1"
type="success"
size="mini"
>高级教师el-tag
>
<el-tag v-if="scope.row.param.teacherLevel === 0" size="mini"
>首席教师el-tag
>
template>
el-table-column>
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="edit(scope.row.id)"
>修改el-button
>
<el-button
type="text"
size="mini"
@click="removeDataById(scope.row.id)"
>删除el-button
>
<el-button type="text" size="mini" @click="showAccount(scope.row)"
>查看账号el-button
>
<router-link :to="'/live/liveCourse/config/' + scope.row.id">
<el-button type="text" size="mini" style="margin-left: 10px"
>配置el-button
>
router-link>
<router-link :to="'/live/liveVisitor/list/' + scope.row.id">
<el-button type="text" size="mini">观看记录el-button>
router-link>
template>
el-table-column>
el-table>
<el-pagination
:current-page="page"
:total="total"
:page-size="limit"
:page-sizes="[5, 10, 20, 30, 40, 50, 100]"
style="padding: 30px 0; text-align: center"
layout="sizes, prev, pager, next, jumper, ->, total, slot"
@current-change="fetchData"
@size-change="changeSize"
/>
<el-dialog title="添加/修改" :visible.sync="dialogVisible" width="60%">
<el-form
ref="flashPromotionForm"
label-width="150px"
size="small"
style="padding-right: 40px"
>
<el-form-item label="直播教师">
<el-select v-model="liveCourse.teacherId" placeholder="请选择">
<el-option
v-for="teacher in teacherList"
:key="teacher.id"
:label="teacher.name"
:value="teacher.id"
/>
el-select>
el-form-item>
<el-form-item label="直播教师登录密码" v-if="liveCourse.id === ''">
<el-input v-model="liveCourse.password" />
el-form-item>
<el-form-item label="直播名称">
<el-input v-model="liveCourse.courseName" />
el-form-item>
<el-form-item label="直播开始时间">
<el-date-picker
v-model="liveCourse.startTime"
type="datetime"
placeholder="选择开始日期"
value-format="yyyy-MM-dd HH:mm:ss"
/>
el-form-item>
<el-form-item label="直播结束时间">
<el-date-picker
v-model="liveCourse.endTime"
type="datetime"
placeholder="选择结束日期"
value-format="yyyy-MM-dd HH:mm:ss"
/>
el-form-item>
<el-form-item label="直播封面">
<el-upload
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload"
:on-error="handleCoverError"
:action="BASE_API + '/admin/vod/file/upload?module=cover'"
class="cover-uploader"
>
<img v-if="liveCourse.cover" :src="liveCourse.cover" width="60%" />
<i v-else class="el-icon-plus avatar-uploader-icon" />
el-upload>
el-form-item>
<el-form-item label="直播详情">
<el-input v-model="liveCourse.description" type="textarea" rows="5" />
el-form-item>
el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false" size="small">取 消el-button>
<el-button type="primary" @click="saveOrUpdate()" size="small"
>确 定el-button
>
span>
el-dialog>
<el-dialog
title="查看账号"
:visible.sync="accountDialogVisible"
width="60%"
>
<el-form
ref="accountForm"
label-width="150px"
size="small"
style="padding-right: 40px"
>
<div style="margin-left: 40px">
<h4>主播帮助信息h4>
<el-row style="height: 35px">
<el-co>
<span class="spd-info">主播登录链接:span>
<span class="spd-info"
>https://live.zhibodun.com/live/courseLogin.php?course_id={{
liveCourseAccount.courseId }}&role=adminspan
>
el-co>
el-row>
<el-row style="height: 35px">
<el-col>
<span class="spd-info"
>主播登录密码:{{ liveCourseAccount.anchorKey }}span
>
el-col>
el-row>
div>
<div style="margin-left: 40px">
<h4>主播客户端账号信息h4>
<el-row style="height: 35px">
<el-col>
<span class="spd-info"
>主播登录账户:{{ liveCourseAccount.anchorAccount }}span
>
el-col>
el-row>
<el-row style="height: 35px">
<el-col>
<span class="spd-info"
>主播登录密码:{{ liveCourseAccount.anchorPassword }}span
>
el-col>
el-row>
div>
<div style="margin-left: 40px">
<h4>助教信息h4>
<el-row style="height: 35px">
<el-co>
<span class="spd-info">助教登录连接:span>
<span class="spd-info"
>https://live.zhibodun.com/live/courseLogin.php?course_id={{
liveCourseAccount.courseId }}&role=adminspan
>
el-co>
el-row>
<el-row style="height: 35px">
<el-col>
<span class="spd-info"
>主播登录密码:{{ liveCourseAccount.adminKey }}span
>
el-col>
el-row>
div>
<div style="margin-left: 40px">
<h4>学生观看信息h4>
<el-row style="height: 35px">
<el-co>
<span class="spd-info">观看连接:span>
<span class="spd-info"
>http://smartplanetmobile.free.idcfengye.com/#/live/info/{{
liveCourseAccount.courseId }}span
>
el-co>
el-row>
<el-row style="height: 35px">
<el-col>
<span class="spd-info"
>观看二维码:<img src="@/assets/SmartPlanet.png" width="80px"
/>span>
el-col>
el-row>
div>
el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="accountDialogVisible = false" size="small"
>关 闭el-button
>
span>
el-dialog>
div>
template>
<script>
import liveCourseAPI from "@/api/live/liveCourse";
import teacherAPI from "@/api/vod/teacher";
const defaultForm = {
id: "",
courseName: "",
startTime: "",
endTime: "",
teacherId: "",
password: "",
description: "",
cover: "https://img-blog.csdnimg.cn/b9f45932f6cf471aa53ee24e5555d9c2.png",
};
export default {
data() {
return {
BASE_API: "http://localhost:8333",
// BASE_API: "http://smartplanet.free.idcfengye.com",
// 数据是否正在加载
listLoading: true,
// banner 列表
list: null,
// 数据库中的总记录数
total: 0,
// 默认页码
page: 1,
// 每页记录数
limit: 10,
// 查询表单对象
searchObj: {},
// 教师列表
teacherList: [],
dialogVisible: false,
liveCourse: defaultForm,
saveBtnDisabled: false,
accountDialogVisible: false,
liveCourseAccount: {
courseId: "",
},
};
},
// 生命周期函数:内存准备完毕,页面尚未渲染
created() {
console.log("list created...");
this.fetchData();
// 获取教师列表
this.initTeacherList();
},
// 生命周期函数:内存准备完毕,页面渲染成功
mounted() {
console.log("list mounted...");
},
methods: {
// 当页码发生改变的时候
changeSize(size) {
console.log(size);
this.limit = size;
this.fetchData(1);
},
// 加载 banner 列表数据
fetchData(page = 1) {
console.log("翻页..." + page);
// 异步获取远程数据(ajax)
this.page = page;
liveCourseAPI.getPageList(this.page, this.limit).then((response) => {
this.list = response.data.records;
this.total = response.data.total;
// 数据加载并绑定成功
this.listLoading = false;
});
},
// 获取教师列表
initTeacherList() {
teacherAPI.list().then((response) => {
this.teacherList = response.data;
});
},
// 重置查询表单
resetData() {
console.log("重置查询表单");
this.searchObj = {};
this.fetchData();
},
// 根据 id 删除数据
removeDataById(id) {
this.$confirm("此操作将永久删除该记录, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
// 点击确定,远程调用ajax
return liveCourseAPI.removeById(id);
})
.then((response) => {
this.fetchData(this.page);
if (response.code) {
this.$message({
type: "success",
message: "删除成功!",
});
}
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
add() {
this.dialogVisible = true;
this.liveCourse = Object.assign({}, defaultForm);
},
edit(id) {
this.dialogVisible = true;
this.fetchDataById(id);
},
fetchDataById(id) {
liveCourseAPI.getById(id).then((response) => {
this.liveCourse = response.data;
});
},
saveOrUpdate() {
// 防止表单重复提交
this.saveBtnDisabled = true;
if (!this.liveCourse.teacherId) {
this.$message.error("请选择直播教师");
this.saveBtnDisabled = false;
return;
}
if (!this.liveCourse.id && !this.liveCourse.password) {
this.$message.error("请输入直播教师登录密码");
this.saveBtnDisabled = false;
return;
}
if (!this.liveCourse.courseName) {
this.$message.error("请输入直播名称");
this.saveBtnDisabled = false;
return;
}
if (!this.liveCourse.courseName) {
this.$message.error("请输入直播名称");
this.saveBtnDisabled = false;
return;
}
if (!this.liveCourse.startTime) {
this.$message.error("请选择直播开始时间");
this.saveBtnDisabled = false;
return;
}
if (!this.liveCourse.endTime) {
this.$message.error("请选择直播结束时间");
this.saveBtnDisabled = false;
return;
}
// 验证开始时间和结束时间的合法性
if (!this.validateDateRange()) {
return;
}
if (!this.liveCourse.description) {
this.$message.error("请输入直播详情");
this.saveBtnDisabled = false;
return;
}
if (!this.liveCourse.id) {
this.saveData();
} else {
this.updateData();
}
},
// 验证开始时间和结束时间的合法性
validateDateRange() {
if (
this.liveCourse.startTime &&
this.liveCourse.endTime &&
this.liveCourse.startTime > this.liveCourse.endTime
) {
this.$message.error("开始时间不能晚于结束时间");
return false;
}
const startDateTime = new Date(this.liveCourse.startTime);
const endDateTime = new Date(this.liveCourse.endTime);
const durationInMilliseconds = endDateTime - startDateTime;
const durationInHours = durationInMilliseconds / (1000 * 60 * 60);
if (durationInHours > 24) {
this.$message.error("直播时间不能超过24小时");
return false;
}
return true;
},
// 新增
saveData() {
liveCourseAPI.save(this.liveCourse).then((response) => {
if (response.code) {
this.$message({
type: "success",
message: response.message,
});
this.dialogVisible = false;
this.fetchData(this.page);
}
});
},
// 根据 id 更新记录
updateData() {
liveCourseAPI.updateById(this.liveCourse).then((response) => {
if (response.code) {
this.$message({
type: "success",
message: response.message,
});
this.dialogVisible = false;
this.fetchData(this.page);
}
});
},
// 根据 id 查询记录
fetchDataById(id) {
liveCourseAPI.getById(id).then((response) => {
this.liveCourse = response.data;
});
},
showAccount(row) {
this.accountDialogVisible = true;
liveCourseAPI.getLiveCourseAccount(row.id).then((response) => {
this.liveCourseAccount = response.data;
this.liveCourseAccount.courseId = row.courseId;
});
},
// 上传成功回调
handleCoverSuccess(res, file) {
this.liveCourse.cover = res.data;
},
// 上传校验
beforeCoverUpload(file) {
const isJPG = file.type === "image/jpeg";
const isPNG = file.type === "image/png";
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG && !isPNG) {
this.$message.error("上传直播封面只能是 JPG 或 PNG 格式!");
}
if (!isLt2M) {
this.$message.error("上传直播封面大小不能超过 2MB!");
}
return (isJPG || isPNG) && isLt2M;
},
// 错误处理
handleCoverError() {
console.log("error");
this.$message.error("上传失败");
},
},
};
script>
<template>
<div class="app-container">
<el-form label-width="120px" size="small">
<div
style="
background-color: #e0e0e0;
width: 100%;
padding: 1px 10px;
margin: 10px 0;
"
>
<h3>功能设置 h3>
div>
<el-form-item label="界面模式">
<el-radio-group v-model="liveCourseConfigVo.pageViewMode">
<el-radio :label="1">全屏模式el-radio>
<el-radio :label="0">二分屏el-radio>
<el-radio :label="2">课件模式el-radio>
el-radio-group>
el-form-item>
<el-form-item label="观看人数开关">
<el-radio-group v-model="liveCourseConfigVo.numberEnable">
<el-radio :label="1">是el-radio>
<el-radio :label="0">否el-radio>
el-radio-group>
el-form-item>
<el-form-item label="商城开关:">
<el-radio-group v-model="liveCourseConfigVo.storeEnable">
<el-radio :label="1">是el-radio>
<el-radio :label="0">否el-radio>
el-radio-group>
el-form-item>
<div
style="
background-color: #e0e0e0;
width: 100%;
padding: 1px 10px;
margin: 10px 0;
"
>
<h3>
商品列表
<el-button type="" size="mini" @click="addCourse()"
>添加 / 更新el-button
>
h3>
div>
<el-table
v-loading="listLoading"
:data="liveCourseConfigVo.liveCourseGoodsList"
stripe
border
style="width: 100%; margin-top: 10px"
>
<el-table-column label="序号" width="70" align="center">
<template slot-scope="scope"> {{ scope.$index + 1 }} template>
el-table-column>
<el-table-column label="商品图片" width="120" align="center">
<template slot-scope="scope">
<img :src="scope.row.img" width="80px" />
template>
el-table-column>
<el-table-column prop="name" label="名称" width="100" />
<el-table-column prop="price" label="价格" width="100" />
<el-table-column prop="originalPrice" label="原价" />
el-table>
<el-dialog
title="添加 / 更新课程"
:visible.sync="dialogVisible"
width="50%"
>
<el-form
:inline="true"
label-width="150px"
size="small"
style="padding-right: 40px"
>
<el-table
v-loading="listLoading"
:data="courseList"
stripe
border
style="width: 100%; margin-top: 10px"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="序号" width="70" align="center">
<template slot-scope="scope"> {{ scope.$index + 1 }} template>
el-table-column>
<el-table-column label="分类">
<template slot-scope="scope">
{{ scope.row.param.subjectParentTitle }} > {{
scope.row.param.subjectTitle }}
template>
el-table-column>
<el-table-column prop="title" label="课程名称" width="150" />
<el-table-column prop="lessonNum" label="课时" width="100" />
<el-table-column prop="param.teacherName" label="教师" />
el-table>
<el-form-item style="margin-top: 10px">
<el-button type="" @click="dialogVisible = false">取消el-button>
<el-button type="" @click="selectCourse()">保存el-button>
el-form-item>
el-form>
el-dialog>
<br /><br />
<el-form-item>
<el-button type="primary" @click="saveOrUpdate">保存el-button>
<el-button @click="back">返回el-button>
el-form-item>
el-form>
div>
template>
<script>
import liveCourseAPI from "@/api/live/liveCourse";
import courseAPI from "@/api/vod/course";
const defaultForm = {
id: "",
liveCourseId: "",
pageViewMode: 1,
numberEnable: 1,
storeEnable: 1,
storeType: 1,
liveCourseGoodsList: [],
};
export default {
data() {
return {
// 数据是否正在加载
listLoading: true,
liveCourseConfigVo: defaultForm,
saveBtnDisabled: false,
dialogVisible: false,
courseList: [],
// 批量选择中选择的记录列表
multipleSelection: [],
};
},
// 监听器
watch: {
$route(to, from) {
console.log("路由变化...");
console.log(to);
console.log(from);
this.init();
},
},
// 生命周期方法(在路由切换,组件不变的情况下不会被调用)
created() {
console.log("form created...");
this.init();
},
methods: {
// 表单初始化
init() {
this.liveCourseConfigVo.liveCourseId = this.$route.params.id;
this.fetchDataById(this.liveCourseConfigVo.liveCourseId);
this.fetchCourseList();
},
back() {
this.$router.push({ path: "/live/liveCourse/list" });
},
// 根据 id 查询记录
fetchDataById(id) {
liveCourseAPI.getCourseConfig(id).then((response) => {
if (null !== response.data.id) {
this.liveCourseConfigVo = response.data;
}
this.listLoading = false;
});
},
fetchCourseList() {
courseAPI.findAll().then((response) => {
this.courseList = response.data;
});
},
handleSelectionChange(selection) {
console.log(selection);
this.multipleSelection = selection;
},
addCourse() {
this.dialogVisible = true;
},
selectCourse() {
if (this.multipleSelection.length === 0) {
this.$message({
type: "warning",
message: "请选择对应课程!",
});
return;
}
let list = [];
this.multipleSelection.forEach((item) => {
var obj = {
liveCourseId: this.liveCourseConfigVo.liveCourseId,
goodsId: item.id,
name: item.title,
img: item.cover,
price: item.price,
originalPrice: item.price,
tab: "1",
url:
"http://smartplanetmobile.free.idcfengye.com/#/course/info/" +
item.id,
putaway: "1",
pay: "1",
qrcode: "",
};
list.push(obj);
});
this.liveCourseConfigVo.liveCourseGoodsList = list;
this.dialogVisible = false;
},
saveOrUpdate() {
liveCourseAPI.updateConfig(this.liveCourseConfigVo).then((response) => {
this.$message({
type: "success",
message: response.message,
});
this.$router.push({ path: "/live/liveCourse/list" });
});
},
},
};
script>
接口文档:https://open.talk-fun.com/docs/js/index.html
用户要观看直播,必须获取对应的用户 access_token,通过 access_token 获取观看的直播课程;
接口参数:直播 id,用户 id。
(1)创建 LiveCourseApiController。
package com.myxh.smart.planet.live.api;
import com.alibaba.fastjson2.JSONObject;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.order.AuthContextHolder;
import com.myxh.smart.planet.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author MYXH
* @date 2023/10/28
*/
@Tag(name = "直播课程 API", description = "直播课程 API 接口")
@RestController
@RequestMapping("api/live/live/course")
public class LiveCourseApiController
{
@Resource
private LiveCourseService liveCourseService;
/**
* 获取用户 access_token
*
* @param id 直播课程 id
* @return Result 全局统一返回结果
*/
@Operation(summary = "获取用户 access_token", description = "获取用户 access_token")
@GetMapping("get/play/auth/{id}")
public Result<JSONObject> getPlayAuth(@PathVariable("id") Long id)
{
JSONObject accessToken = liveCourseService.getAccessToken(id, AuthContextHolder.getUserId());
return Result.ok(accessToken);
}
}
(2)LiveCourseService 添加方法。
package com.myxh.smart.planet.live.service;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourse;
/**
* @author MYXH
* @date 2023/10/26
*
*
* 直播课程 服务类
*
*/
public interface LiveCourseService extends IService<LiveCourse>
{
/**
* 获取用户 access_token
*
* @param id id 直播课程 id
* @param userId 用户 id
* @return accessToken 访问令牌
*/
JSONObject getAccessToken(Long id, Long userId);
}
(3)LiveCourseServiceImpl 实现方法。
package com.myxh.smart.planet.live.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.client.user.UserInfoFeignClient;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.live.mapper.LiveCourseMapper;
import com.myxh.smart.planet.live.mtcloud.CommonResult;
import com.myxh.smart.planet.live.mtcloud.MTCloud;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.model.user.UserInfo;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
/**
* @author MYXH
* @date 2023/10/26
*
*
* 直播课程 服务实现类
*
*/
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService
{
@Autowired
private UserInfoFeignClient userInfoFeignClient;
@Autowired
private MTCloud mtCloudClient;
/**
* 获取用户 access_token
*
* @param id id 直播课程 id
* @param userId 用户 id
* @return accessToken 访问令牌
*/
@SneakyThrows
@Override
public JSONObject getAccessToken(Long id, Long userId)
{
// 根据课程 id 获取直播课程信息
LiveCourse liveCourse = baseMapper.selectById(id);
// 根据用户 id 获取用户信息
UserInfo userInfo = userInfoFeignClient.getUserInfoById(userId);
// 封装需要的参数
HashMap<Object, Object> options = new HashMap<>();
String res = mtCloudClient.courseAccess(liveCourse.getCourseId().toString(),
userId.toString(),
userInfo.getNickName(),
MTCloud.ROLE_USER,
80 * 80 * 80,
options);
System.out.println("res = " + res);
CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);
if (Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS)
{
JSONObject accessToken = commonResult.getData();
System.out.println("access_token = " + accessToken.getString("access_token"));
return accessToken;
}
else
{
throw new SmartPlanetException(20001, "获取失败");
}
}
}
下载地址:https://open.talk-fun.com/docs/js/download.html
下载模板,修改 token 获取方式。
var url = window.location.search;
var token = url.split("=")[1];
(1)创建直播播放页面 live.html。
DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
<title>TalkFun Vod QuickStart - v2title>
<style type="text/css">
* {
margin: 0;
padding: 0;
list-style-type: none;
font-family: "Microsoft YaHei", "STHeiti";
}
.flash-wran {
display: none;
position: absolute;
top: 0;
width: 100%;
padding: 5px 0;
text-align: center;
background: #fff6a2;
border: 1px solid #ffd913;
}
.flash-wran a {
color: red;
}
.wrapper {
display: flex;
padding: 10px;
}
#cameraPlayer,
#pptPlayer {
height: auto;
flex: 1;
text-align: center;
font-size: 12px;
overflow: hidden;
}
#pptPlayer {
height: 300px;
width: 100%;
}
#modPptPlayer,
#modCameraPlayer {
margin-top: 10px;
border: 1px solid #c7c7c7;
}
.chat-wrap {
padding: 5px;
margin: 10px;
border: 2px solid #cccccc;
background: #f1f1f1;
}
.mod-chat-list {
margin: 20px 0;
border: 1px solid #cccccc;
min-height: 100px;
font-size: 12px;
background: #dedede;
padding: 5px;
max-height: 200px;
overflow-y: scroll;
}
.mod-chat-list li {
padding: 2px 0;
margin-bottom: 5px;
border-bottom: 1px dotted #cccccc;
}
input {
display: inline-block;
width: 200px;
padding: 5px;
}
button {
display: inline-block;
padding: 5px;
margin-left: 5px;
}
#toast {
padding: 20px;
position: fixed;
z-index: 100;
display: none;
background: rgba(212, 28, 28, 0.8);
left: 50%;
top: 30%;
border-radius: 50em;
font-size: 14px;
color: #ffffff;
box-shadow: 0 0 6px 0px #bb2d2d;
}
#talkfun-video-wrap,
#talkfun-camera-wrap {
position: relative;
background: #000;
}
style>
<script
type="text/javascript"
src="https://static-1.talk-fun.com/open/TalkFun_SDK_Pack/v7.0/TalkFunWebSDK-7.7.min.js"
>script>
head>
<body>
<div class="wrapper">
<div id="videoPlayer">
<p id="loadplayer">p>
div>
div>
<script>
// [第一步] 如何获取 access_token => http://open.talk-fun.com/docs/getstartV2/access_token.html
// [第二步] 根据Api文档方法 监听 / 调用方法 JS Api => http://open.talk-fun.com/docs/js/sdk.js.getstart.html
var url = window.location.search;
var token = url.split("=")[1];
// 更多配置项 => https://open.talk-fun.com/docs/js/sdk.js.getstart.html?h=%E9%85%8D%E7%BD%AE%E9%A1%B9
var HT = new MT.SDK.main(
token,
{
config: {
// Safari 浏览器建议设置为 HLS
techOrder: "FLV",
},
},
function (data) {
console.warn("sdk 加载完成", data);
}
);
// 连接状态
HT.on("connect", function () {
console.log("TalkFun 通信 => 连接成功...");
});
// 课件播放器
HT.whiteboardPlayer("pptPlayer", "docPlayer", function (player) {
console.log("课件播放器 => 初始化成功");
document.querySelector("#loadplayer").innerHTML = "画板模块加载完成";
});
// 视频插播 | 桌面分享
HT.videoPlayer("videoPlayer", "modVideoplayer", function (player) {
console.log("视频播放器 => 初始化成功");
document.querySelector("#loadplayer").innerHTML = "视频插播加载完成";
});
// 摄像头播放器
HT.camera("cameraPlayer", "modCameraPlayer", function () {
console.log("摄像头播放器 => 初始化成功");
document.querySelector("#loadcam").innerHTML = "摄像头模块加载完成";
});
// 接收聊天
var receivedChat = function (chat) {
var tpl = chat.nickname + ": " + chat.msg;
var chatItem = document.createElement("li");
chatItem.innerHTML = tpl;
chatItem.className = "chat-" + chat.xid;
document.querySelector("#chat-list").appendChild(chatItem);
};
// 接收聊天信息
HT.on("chat:send", function (chat) {
receivedChat(chat);
});
// 发送聊天信息
document.querySelector("#chatSubmit").addEventListener(
"click",
function () {
var chatIpt = document.querySelector("#chatVal");
var chatValue = chatIpt.value;
HT.emit("chat:send", { msg: chatValue }, function (res) {
// 发送成功
if (Number(res.code) === 0) {
receivedChat(res.data);
chatIpt.value = "";
}
// 发送失败
else {
console.warn(res.msg);
}
});
},
false
);
// Flash 插件异常
HT.on("flash:load:error", function (obj) {
if (!obj.flash) {
document.querySelector("#flashTip").style.display = "block";
}
});
// 课程错误信息
HT.on("live:course:access:error", function (res) {
console.error("错误信息 ==>", res);
});
// 课程错误信息
HT.on("system:room:error", function (res) {
var toast = document.querySelector("#toast");
if (typeof res === "string") {
toast.innerHTML = res.msg;
} else if (res.msg) {
toast.innerHTML = res.msg;
}
toast.style.display = "block";
var _left = toast.clientWidth / 2;
toast.style.marginLeft = -_left + "px";
});
script>
body>
html>
观众在直播详情页面点击观看,获取通过接口获取 access_token,然后带上 access_token 参数跳转到直播观看页面即可,关键代码:
liveInfo.vue
<template>
<div>
<head>
<meta name="referrer" content="no-referrer" />
head>
<van-image
width="100%"
height="200"
src="https://img-blog.csdnimg.cn/b9f45932f6cf471aa53ee24e5555d9c2.png"
/>
<h1 class="van-ellipsis course_title">{{ liveCourse.courseName }}h1>
<div class="course_teacher_price_box">
<div class="course_teacher_price">
<div class="course_price">直播时间:div>
<div class="course_price_number">
{{ liveCourse.param.startTimeString }}至{{
liveCourse.param.endTimeString }}
div>
div>
div>
<div class="course_teacher_price_box">
<div class="course_teacher_box">
<div class="course_teacher">教师: {{ teacher.name }}div>
<van-image :src="teacher.avatar" round width="50px" height="50px" />
div>
div>
<div class="course_contents">
<div class="course_title_font">课程详情div>
<van-divider :style="{ margin: '5px 0 ' }" />
<div class="course_content" v-html="description">div>
div>
<van-goods-action>
<van-goods-action-button type="danger" text="直播中" @click="play" />
van-goods-action>
<van-loading vertical="true" v-show="loading">加载中...van-loading>
div>
template>
<script>
import liveAPI from "@/api/live";
import shareAPI from "@/api/share";
import wxShare from "@/utils/wxShare";
export default {
data() {
return {
loading: false,
liveCourseId: null,
liveCourse: { param: {} },
description: "",
teacher: {},
liveStatus: 0,
activeNames: ["1"],
};
},
created() {
this.liveCourseId = this.$route.params.liveCourseId;
this.fetchData();
},
methods: {
fetchData() {
this.loading = true;
liveAPI.getInfo(this.liveCourseId).then((response) => {
console.log(response.data);
this.liveCourse = response.data.liveCourse;
this.description = response.data.description;
this.teacher = response.data.teacher;
this.liveStatus = response.data.liveStatus;
this.loading = false;
// 分享注册
this.wxRegister();
});
},
play() {
liveAPI.getPlayAuth(this.liveCourseId).then((response) => {
console.log(response.data);
// this.$router.push({ path: '/live/online?token='+response.data.access_token })
window.location = "./live.html?token=" + response.data.access_token;
this.finished = true;
});
},
wxRegister() {
// 说明:后台加密 url 必须与当前页面 url 一致
let url = window.location.href.replace("#", "smartplanet");
shareAPI.getSignature(url).then((response) => {
console.log(response.data);
// 记录分享用户
let link = "";
if (window.location.href.indexOf("?") != -1) {
link =
window.location.href + "&recommend=" + response.data.userEedId;
} else {
link =
window.location.href + "?recommend=" + response.data.userEedId;
}
let option = {
title: this.liveCourse.courseName,
desc: this.description,
link: link,
imgUrl: this.liveCourse.cover,
};
wxShare.wxRegister(response.data, option);
});
},
},
};
script>
<style lang="scss" scoped>
.gap {
height: 10px;
}
::v-deep.van-image {
display: block;
}
.course_count {
background-color: #82848a;
color: white;
padding: 5px;
text-align: center;
border-right: 1px solid #939393;
h1 {
font-size: 14px;
margin: 0;
}
p {
margin: 0;
font-size: 16px;
}
}
.course_title {
font-size: 20px;
margin: 10px;
}
.course_teacher_price_box {
margin: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.course_teacher_price {
display: flex;
font-size: 14px;
align-items: center;
.course_price_number {
color: red;
font-size: 18px;
font-weight: bold;
}
.course_teacher {
margin-left: 20px;
}
}
.course_teacher_box {
display: flex;
justify-content: center;
align-items: center;
.course_teacher {
margin-right: 20px;
}
}
}
.course_contents {
margin: 10px;
.course_title_font {
color: #68cb9b;
font-weight: bold;
}
.course_content {
margin-bottom: 20px;
}
}
.course_chapter_list {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
font-size: 14px;
}
p {
margin: 0;
}
}
style>
http://localhost:8080/live.html 为直播观看访问方式。
(1)LiveCourseApiController 类。
package com.myxh.smart.planet.live.api;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* @author MYXH
* @date 2023/10/28
*/
@Tag(name = "直播课程 API", description = "直播课程 API 接口")
@RestController
@RequestMapping("api/live/live/course")
public class LiveCourseApiController
{
@Resource
private LiveCourseService liveCourseService;
/**
* 根据直播课程 id 查询直播课程信息
*
* @param liveCourseId 直播课程 id
* @return Result 全局统一返回结果
*/
@Operation(summary = "根据直播课程 id 查询直播课程信息", description = "根据直播课程 id 查询直播课程信息")
@GetMapping("get/info/{liveCourseId}")
public Result<Map<String, Object>> getInfo(@Parameter(name = "liveCourseId", description = "直播课程 ID", required = true)
@PathVariable("liveCourseId") Long liveCourseId)
{
Map<String, Object> liveCourseInfo = liveCourseService.getInfoById(liveCourseId);
return Result.ok(liveCourseInfo);
}
}
(2)LiveCourseServiceImpl 实现。
package com.myxh.smart.planet.live.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.client.course.CourseFeignClient;
import com.myxh.smart.planet.live.mapper.LiveCourseMapper;
import com.myxh.smart.planet.live.service.LiveCourseDescriptionService;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.model.live.LiveCourseDescription;
import com.myxh.smart.planet.model.vod.Teacher;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @author MYXH
* @date 2023/10/26
*
*
* 直播课程 服务实现类
*
*/
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService
{
@Autowired
private CourseFeignClient teacherFeignClient;
@Autowired
private LiveCourseDescriptionService liveCourseDescriptionService;
/**
* 根据直播课程 id 查询直播课程信息
*
* @param liveCourseId 直播课程 id
* @return liveCourseInfo 直播课程信息
*/
@Override
public Map<String, Object> getInfoById(Long liveCourseId)
{
LiveCourse liveCourse = this.getById(liveCourseId);
liveCourse.getParam().put("startTimeString", new DateTime(liveCourse.getStartTime()).toString("yyyy年MM月dd HH:mm"));
liveCourse.getParam().put("endTimeString", new DateTime(liveCourse.getEndTime()).toString("yyyy年MM月dd HH:mm"));
Teacher teacher = teacherFeignClient.getTeacherLive(liveCourse.getTeacherId());
LiveCourseDescription liveCourseDescription = liveCourseDescriptionService.getLiveCourseDescriptionByLiveCourseId(liveCourseId);
Map<String, Object> liveCourseInfo = new HashMap<>();
liveCourseInfo.put("liveCourse", liveCourse);
liveCourseInfo.put("liveStatus", this.getLiveStatus(liveCourse));
liveCourseInfo.put("teacher", teacher);
if (liveCourseDescription != null)
{
liveCourseInfo.put("description", liveCourseDescription.getDescription());
}
else
{
liveCourseInfo.put("description", "");
}
return liveCourseInfo;
}
}
1、点播课程详情页面分享。
参考文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html
先登录微信公众平台进入“设置与开发”,“公众号设置”的“功能设置”里填写“JS 接口安全域名”。
说明:本地测试设置内网穿透地址。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,,chrome=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no"
/>
<title>智慧星球title>
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<script
src="http://res2.wx.qq.com/open/js/jweixin-1.6.0.js"
type="text/javascript"
>script>
<link
href="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/tcplayer.min.css"
rel="stylesheet"
/>
<script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/libs/hls.min.1.1.6.js">script>
<script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/libs/flv.min.1.6.3.js">script>
<script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/libs/dash.all.min.4.5.2.js">script>
<script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/tcplayer.v4.7.2.min.js">script>
head>
<body>
<script
type="text/javascript"
src="https://static-1.talk-fun.com/open/TalkFun_SDK_Pack/v7.0/TalkFunWebSDK-7.7.min.js"
>script>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
properly without JavaScript enabled. Please enable it to
continue.strong
>
noscript>
<div id="app">div>
body>
html>
引入前端项目/public/index.html 文件。
参考官方文档封装接口。
需要分享的页面有直播详情页、点播课程详情页等,因此把分享代码封装后,在对应的页面直接引入与调用即可。
新建 src/util/wxShare.js 文件。
/**
* 微信 js-sdk
* 参考文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
*/
const wxShare = {
/**
* [wxRegister 微信 Api 初始化]
* @param {Function} callback [ready 回调函数]
*/
wxRegister(data, option) {
// data 是微信配置信息,option 是分享的配置内容
wx.config({
// 开启调试模式
debug: false,
// 必填,公众号的唯一标识
appId: data.appId,
// 必填,生成签名的时间戳
timestamp: data.timestamp,
// 必填,生成签名的随机串
nonceStr: data.nonceStr,
// 必填,签名,见附录 1
signature: data.signature,
// 必填,需要使用的 JS 接口列表,所有 JS 接口列表见附录 2
jsApiList: ["onMenuShareAppMessage"],
});
wx.ready(function () {
wx.onMenuShareAppMessage({
// 分享标题
title: option.title,
// 分享描述
desc: option.desc,
// 分享链接
link: option.link,
// 分享图标
imgUrl: option.imgUrl,
success() {
// 用户成功分享后执行的回调函数
// option.success()
console.log("ok");
},
cancel() {
// 用户取消分享后执行的回调函数
// option.error()
console.log("cancel");
},
});
});
wx.error(function (res) {
// config 信息验证失败会执行 error 函数,如签名过期导致验证失败,具体错误信息可以打开 config 的 debug 模式查看,也可以在返回的 res 参数中查看,对于 SPA 可以在这里更新签名。
alert("error:" + JSON.stringify(res));
});
},
};
export default wxShare;
新增 ShareController 类。
说明:微信分享要对当前 url 加密处理,由于 url 路由都是带“#”符号,服务器端接收不到,因此通过“smartplanet”单词代替了“#”。
package com.myxh.smart.planet.wechat.controller;
import com.myxh.smart.planet.order.AuthContextHolder;
import com.myxh.smart.planet.order.Base64Util;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.wechat.WeChatJsapiSignatureVo;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author MYXH
* @date 2023/10/28
*/
@RestController
@RequestMapping("/api/wechat/share")
@Slf4j
public class ShareController
{
@Autowired
private WxMpService wxMpService;
/**
* 获取签名
*
* @param url url
* @return Result 全局统一返回结果
* @throws WxErrorException 微信错误异常
*/
@GetMapping("/get/signature")
public Result<WeChatJsapiSignatureVo> getSignature(@RequestParam("url") String url) throws WxErrorException
{
String currentUrl = url.replace("smartplanet", "#");
WxJsapiSignature jsapiSignature = wxMpService.createJsapiSignature(currentUrl);
WeChatJsapiSignatureVo wxJsapiSignatureVo = new WeChatJsapiSignatureVo();
BeanUtils.copyProperties(jsapiSignature, wxJsapiSignatureVo);
wxJsapiSignatureVo.setUserEedId(Base64Util.base64Encode(AuthContextHolder.getUserId() + ""));
return Result.ok(wxJsapiSignatureVo);
}
}
页面:courseInfo.vue。
(1)引入分享。
import shareAPI from "@/api/share";
import wxShare from "@/utils/wxShare";
(2)代码实现。
关键代码。
<template>
<div>
<van-image width="100%" height="200" :src="courseVo.cover" />
<van-row>
<van-col span="8">
<div class="course_count">
<h1>购买数h1>
<p>{{ courseVo.buyCount }}p>
div>
van-col>
<van-col span="8">
<div class="course_count">
<h1>课时数h1>
<p>{{ courseVo.lessonNum }}p>
div>
van-col>
<van-col span="8">
<div class="course_count">
<h1>浏览数h1>
<p>{{ courseVo.viewCount }}p>
div>
van-col>
van-row>
<h1 class="van-ellipsis course_title">{{ courseVo.title }}h1>
<div class="course_teacher_price_box">
<div class="course_teacher_price">
<div class="course_price">价格:div>
<div class="course_price_number">¥{{ courseVo.price }}div>
div>
<div>
<van-button
@click="see()"
v-if="isBuy || courseVo.price == '0.00'"
plain
type="warning"
size="mini"
>立即观看van-button
>
<van-button @click="buy" v-else plain type="warning" size="mini"
>立即购买van-button
>
div>
div>
<div class="course_teacher_price_box">
<div class="course_teacher_box">
<div class="course_teacher">教师: {{ teacher.name }}div>
<van-image :src="teacher.avatar" round width="50px" height="50px" />
div>
div>
<div class="course_contents">
<div class="course_title_font">课程详情div>
<van-divider :style="{ margin: '5px 0 ' }" />
<div class="course_content" v-html="description">div>
<div class="course_title_font">课程大纲div>
<div class="gap">div>
<van-collapse v-model="activeNames">
<van-collapse-item
:title="item.title"
:name="item.id"
v-for="item in chapterVoList"
:key="item.id"
>
<ul
class="course_chapter_list"
v-for="child in item.children"
:key="child.id"
>
<h2>{{ child.title }}h2>
<p v-if="child.isFree == 1">
<van-button @click="play(child)" type="warning" size="mini" plain
>免费观看van-button
>
p>
<p v-else>
<van-button @click="play(child)" type="warning" size="mini" plain
>观看van-button
>
p>
ul>
van-collapse-item>
van-collapse>
div>
<van-loading vertical="true" v-show="loading">加载中...van-loading>
div>
template>
<script>
import courseAPI from "@/api/course";
import shareAPI from "@/api/share";
import wxShare from "@/utils/wxShare";
export default {
data() {
return {
loading: false,
courseId: null,
courseVo: {},
description: "",
teacher: {},
chapterVoList: [],
isBuy: false,
activeNames: ["1"],
};
},
created() {
this.courseId = this.$route.params.courseId;
this.fetchData();
},
methods: {
fetchData() {
this.loading = true;
courseAPI.getInfo(this.courseId).then((response) => {
console.log(response.data);
this.courseVo = response.data.courseVo;
this.description = response.data.description;
this.isBuy = response.data.isBuy;
this.chapterVoList = response.data.chapterVoList;
this.teacher = response.data.teacher;
this.loading = false;
// 分享注册
this.wxRegister();
});
},
buy() {
this.$router.push({ path: "/trade/" + this.courseId });
},
play(video) {
let videoId = video.id;
let isFree = video.isFree;
if (isFree === 1 || this.isBuy || this.courseVo.price == "0.00") {
this.$router.push({ path: "/play/" + this.courseId + "/" + videoId });
} else {
// this.$router.push({ path: "/play/" + this.courseId + "/" + videoId });
if (window.confirm("购买了才可以观看, 是否继续?")) {
this.buy();
}
}
},
see() {
this.$router.push({ path: "/play/" + this.courseId + "/0" });
},
wxRegister() {
// 说明:后台加密 url 必须与当前页面 url 一致
let url = window.location.href.replace("#", "smartplanet");
shareAPI.getSignature(url).then((response) => {
console.log(response.data);
// 记录分享用户
let link = "";
if (window.location.href.indexOf("?") != -1) {
link =
window.location.href + "&recommend=" + response.data.userEedId;
} else {
link =
window.location.href + "?recommend=" + response.data.userEedId;
}
let option = {
title: this.courseVo.title,
desc: this.description,
link: link,
imgUrl: this.courseVo.cover,
};
wxShare.wxRegister(response.data, option);
});
},
},
};
script>
<style lang="scss" scoped>
.gap {
height: 10px;
}
::v-deep.van-image {
display: block;
}
.course_count {
background-color: #82848a;
color: white;
padding: 5px;
text-align: center;
border-right: 1px solid #939393;
h1 {
font-size: 14px;
margin: 0;
}
p {
margin: 0;
font-size: 16px;
}
}
.course_title {
font-size: 20px;
margin: 10px;
}
.course_teacher_price_box {
margin: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.course_teacher_price {
display: flex;
font-size: 14px;
align-items: center;
.course_price_number {
color: red;
font-size: 18px;
font-weight: bold;
}
.course_teacher {
margin-left: 20px;
}
}
.course_teacher_box {
display: flex;
justify-content: center;
align-items: center;
.course_teacher {
margin-right: 20px;
}
}
}
.course_contents {
margin: 10px;
.course_title_font {
color: #68cb9b;
font-weight: bold;
}
.course_content {
margin-bottom: 20px;
}
}
.course_chapter_list {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
font-size: 14px;
}
p {
margin: 0;
}
}
style>
(1)使用手机测试,其他端测试可能会出现错误问题。
完整 DevOps 示例如下:
整合 CODING 实现 DevOps:
https://console.cloud.tencent.com/coding/container-devops
腾讯云使用文档:
https://help.coding.net/docs/start/new.html
DevOps 是 Development 和 Operations 的组合词,代表着重视「软件开发人员(Dev)」和「IT 运维技术人员(Ops)」之间沟通合作的文化;旨在透过自动化「软件交付」和「架构变更」的流程,使得构建、 测试、发布软件的过程能够更加地快捷、频繁和可靠。Gartner 咨询公司认为 DevOps 代表了 IT 文化的变化趋势。
CODING DevOps 是面向软件研发团队的一站式研发协作管理平台,提供从需求到设计、开发、构建、测试、发布到部署的全流程协同及研发工具支撑。CODING 解决方案可助力企业实现代码的统一安全管控,并快速实践敏捷开发与 DevOps,提升软件交付质量与速度,降低企业研发成本,实现研发效能升级。
一站式协作平台及研发工具链,提升研发效能。
CODING 与云端优势相结合,依托业界敏捷项目管理与 DevOps 体系方法融入到产品中,打通研发过程中的工具链孤岛及协作壁垒,覆盖敏捷开发全生命周期,帮助团队实现需求、迭代、开发、测试、持续集成、持续部署全方位研发管理,提升软件研发效能。
支持双态研发体系建设,满足多样化业务需求。
CODING 适用于不同规模的开发团队以及不同类型的软件开发模式(如瀑布模型、敏捷模型),满足多业务场景的协作需求。
项目工作流和度量数据可视化,项目管理更轻松。
CODING 提供可视化看板,支持对代码、项目进度、人员工作量等不同维度输出详尽的数据报告,为团队管理者提供决策依据,调整项目计划和合理安排研发人力。
丰富的扩展能力,无缝集成第三方平台。
CODING 支持无缝集成 GitHub、GitLab 等第三方代码库及各类常见的运维系统和云原生环境,让用户实现跨平台的无缝迁移。
CODING DevOps 平台主要提供以下功能特性:
团队级功能:
项目级功能:
项目协同:软件开发团队可自由选择适合的研发管理模式,支持多项目管理、敏捷迭代管理、需求管理、缺陷跟踪、多维度报表数据等功能。
代码仓库:提供企业级的 Git/SVN 代码管理服务,支持精细化权限管控、多分支并行开发、多版本管理等功能。
代码扫描:提供针对不同编程语言的代码扫描方案,支持对扫描规则、度量规则等进行自定义配置。根据代码扫描测试结果,开发人员可及时发现代码缺陷并作出修正,有效管控代码质量。
持续集成:提供基于云端的自动化代码构建、测试、分析和部署工作流服务,支持通过模板快速创建构建任务并进行可视化编排,极大提高软件开发团队的构建效率。
持续部署:提供全自动化软件部署,可持续、可控地把软件制品在线发布到服务集群中,支持蓝绿分发布、灰度发布(金丝雀发布)等多种发布策略。
制品管理:提供云端构建产物管理服务,支持云端构建和本地构建推送,可快速索引存档构建物、进行版本控制。
测试管理:提供面向敏捷团队的测试一站式云端测试平台,支持可视化的测试规划和多维度的测试报告,满足敏捷团队对测试过程的多样化需求。
文档管理:提供灵活易用的文档管理服务,可用于记录整个项目的来龙去脉,展示当前项目状态,也可让项目成员更好地进行文档书写及协作。
以下流程图展示了 CODING DevOps 软件开发平台的基本操作流程,您可以按照实际需求有选择性阅读。
如要开始使用 CODING DevOps,您需要先注册创建或接受邀请后加入一个团队。
加入团队之后,您可以在团队内创建项目或受他人邀请加入别的项目。“项目”是核心单元,几乎大部分工作都需要在项目中展开。
项目创建之后,项目经理、开发、测试等不同的项目角色可通过项目协同实现简单高效的项目协作,包含迭代管理、需求管理、任务管理等。
完成项目规划之后,可利用代码仓库管理项目代码。该功能提供企业级的基于 Git 的云端代码管理服务,支持精细化权限管控、多分支并行开发、多版本管理等功能。
对于使用 CODING 代码仓库管理的代码,开发者可使用代码扫描功能进行代码检查,以便及时发现代码缺陷并作出修正,有效管控代码质量。
项目代码开发完成之后,可通过持续集成功能快速创建构建任务,将项目代码编译打包成软件包。
在您将项目代码构建好之后,可以使用制品管理功能管理构建产物。CODING 支持多种制品库类型,包括 Docker、Maven、Helm 和 npm。
当您的项目代码已经完成构建,可使用持续部署把控构建之后的项目发布与部署到生产环境中去。
当您在 CODING 平台创建项目之后,您可以使用面向敏捷团队的测试管理功能来管理项目内的测试活动,确保产品的高质量交付。
在项目进行中,必然会产生大量的信息,并且需要对这些信息进行记录、传递、分享。文档管理功能提供灵活易用的文档管理服务,可用于记录整个项目的来龙去脉。
(1)腾讯云搜索 CODING-DevOps。
(2)进入界面。
(1)第一次进入没有账号,去开通。
(2)产品授权。
(3)输入信息,邮箱验证。
(1)输入团队名称提交。
(2)开通成功。
(3)点击立即使用,进入。
(4)进入工作台。
在 CODING DevOps 平台建立团队之后,团队内成员可按需创建项目。只有项目创建之后,项目成员才能按需使用项目协同、代码仓库、持续集成、持续部署等功能。
(1)前往初始化。
(2)配置并开启项目协同。
选择经典项目管理。
(1)点击右上角创建迭代。
(2)填写迭代信息。
(3)点击创建并规划,创建需求。
(4)点击查看详情。
(5)设置迭代详情信息。
(6)效果。
(1)填写仓库信息。
(2)克隆仓库。
点击克隆。
(1)在 Git 客户端中输入克隆命令。
git clone <您克隆的代码仓库地址>
首次拉取后会提示填写凭据,此处填写在注册 CODING 时所使用的邮箱与密码即可。
命令操作提示成功之后,你可以在本地代码仓库中进行代码修改。
(1)在 Git 客户端,运行以下三条命令将代码推送到 CODING 平台上的代码仓库。
git add .
git commit -m "<您对本次提交备注的信息>"
git push git仓库地址
(2)创建提交文件。
(3)进入 cmd 窗口执行。
(4)查看所有的提交记录。
(1)复制两个文件到项目目录。
(2)项目路径 cmd 窗口执行。
(3)查看效果。
根据具体需要,自定义执行流程。
(1)创建集群。
在容器服务中创建集群。
(2)开放外网。
(1)复制集群凭证。
(2)复制到云账户。
1、选择示例镜像用于测试使用。
2、可以选择 CODING Docker 仓库里面自己创建的镜像。
(1)在部署控制台创建应用,选择腾讯云弹性伸缩。
(1)点击创建流程。
(2)选择流程。
(3)修改流程内容。
(1)执行成功后,集群中查看。