..
This commit is contained in:
-70
@@ -1,70 +0,0 @@
|
||||
package com.budwk.app.zhgh.dayofficework.edu.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.base.annotation.SLog;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduChapters;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduVideos;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@IocBean
|
||||
@At("/platform/edu/chapters")
|
||||
@Ok("json:full")
|
||||
@Api(tags = "理论学习课程章节")
|
||||
public class EduChaptersController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@At
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("数据列表")
|
||||
public Result list(@Param("courseId") String courseId) {
|
||||
List<EduChapters> list = dao.query(EduChapters.class, Cnd.where(EduChapters::getCourseId, "=", courseId).asc(EduChapters::getSortCode));
|
||||
for (EduChapters chapters : list) {
|
||||
dao.fetchLinks(chapters, "videos", Cnd.NEW().asc(EduVideos::getSortCode));
|
||||
}
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
|
||||
@At
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("添加")
|
||||
@SLog(tag = "理论学习课程", msg = "添加课程章节")
|
||||
public Result insert(EduChapters eduChapters) {
|
||||
dao.insert(eduChapters);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
@At
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("修改")
|
||||
@SLog(tag = "理论学习课程", msg = "修改课程章节")
|
||||
public Result update(EduChapters eduChapters) {
|
||||
dao.update(eduChapters);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("删除")
|
||||
@SLog(tag = "理论学习课程", msg = "删除课程章节,id:${args[0]}")
|
||||
public Result delete(@Param("id") String id) {
|
||||
dao.delete(EduChapters.class, id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+88
-7
@@ -5,7 +5,9 @@ import com.budwk.app.base.annotation.SLog;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduChapters;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduCourses;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduVideos;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.service.EduCoursesService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
@@ -17,8 +19,10 @@ import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@IocBean
|
||||
@At("/platform/edu/courses")
|
||||
@At("/platform/edu")
|
||||
@Ok("json:full")
|
||||
@Api(tags = "理论学习课程")
|
||||
public class EduCoursesController {
|
||||
@@ -28,14 +32,14 @@ public class EduCoursesController {
|
||||
@Inject
|
||||
private EduCoursesService eduCoursesService;
|
||||
|
||||
@At("")
|
||||
@At("/courses")
|
||||
@Ok("beetl:/platform/zhgh/dayofficework/edu/courses/index.html")
|
||||
@SaCheckPermission("edu.courses")
|
||||
public void index() {
|
||||
|
||||
}
|
||||
|
||||
@At
|
||||
@At("/courses/pageData")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("数据列表")
|
||||
public Result pageData(PageForm pageForm) {
|
||||
@@ -45,7 +49,7 @@ public class EduCoursesController {
|
||||
}
|
||||
|
||||
|
||||
@At
|
||||
@At("/courses/insert")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("添加")
|
||||
@SLog(tag = "理论学习课程", msg = "添加课程")
|
||||
@@ -55,7 +59,7 @@ public class EduCoursesController {
|
||||
}
|
||||
|
||||
|
||||
@At
|
||||
@At("/courses/update")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("修改")
|
||||
@SLog(tag = "理论学习课程", msg = "修改课程")
|
||||
@@ -64,14 +68,91 @@ public class EduCoursesController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@At("/courses/delete")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("删除")
|
||||
@SLog(tag = "理论学习课程", msg = "删除课程,id:${args[0]}")
|
||||
public Result delete(@Param("id") String id) {
|
||||
public Result deleteCourses(@Param("id") String id) {
|
||||
dao.delete(EduCourses.class, id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
@At("/chapters/list")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("章节列表")
|
||||
public Result list(@Param("courseId") String courseId) {
|
||||
List<EduChapters> list = dao.query(EduChapters.class, Cnd.where(EduChapters::getCourseId, "=", courseId).asc(EduChapters::getSortCode));
|
||||
for (EduChapters chapters : list) {
|
||||
dao.fetchLinks(chapters, "videos", Cnd.NEW().asc(EduVideos::getSortCode));
|
||||
}
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
@At("/chapters/insert")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("添加")
|
||||
@SLog(tag = "理论学习课程", msg = "添加课程章节")
|
||||
public Result insert(EduChapters eduChapters) {
|
||||
dao.insert(eduChapters);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
@At("/chapters/update")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("修改")
|
||||
@SLog(tag = "理论学习课程", msg = "修改课程章节")
|
||||
public Result update(EduChapters eduChapters) {
|
||||
dao.update(eduChapters);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At("/chapters/delete")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("删除")
|
||||
@SLog(tag = "理论学习课程", msg = "删除课程章节,id:${args[0]}")
|
||||
public Result deleteChapters(@Param("id") String id) {
|
||||
dao.delete(EduChapters.class, id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At("/video/list")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("数据列表")
|
||||
public Result listVideo(@Param("chapterId") String chapterId) {
|
||||
List<EduVideos> list = dao.query(EduVideos.class, Cnd.where(EduVideos::getChapterId, "=", chapterId).asc(EduVideos::getSortCode));
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
|
||||
@At("/video/insert")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("添加")
|
||||
@SLog(tag = "理论学习课程", msg = "添加课程视频")
|
||||
public Result insert(EduVideos eduVideos) {
|
||||
dao.insert(eduVideos);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
@At("/video/update")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("修改")
|
||||
@SLog(tag = "理论学习课程", msg = "修改课程视频")
|
||||
public Result update(EduVideos eduVideos) {
|
||||
dao.update(eduVideos);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At("/video/delete")
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("删除")
|
||||
@SLog(tag = "理论学习课程", msg = "删除课程视频,id:${args[0]}")
|
||||
public Result deleteVideo(@Param("id") String id) {
|
||||
dao.delete(EduVideos.class, id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.budwk.app.zhgh.dayofficework.edu.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.base.annotation.SLog;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduChapters;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduVideos;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@IocBean
|
||||
@At("/platform/edu/video")
|
||||
@Ok("json:full")
|
||||
@Api(tags = "理论学习课程视频")
|
||||
public class EduVideoController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@At
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("数据列表")
|
||||
public Result list(@Param("chapterId") String chapterId) {
|
||||
List<EduVideos> list = dao.query(EduVideos.class, Cnd.where(EduVideos::getChapterId, "=", chapterId).asc(EduVideos::getSortCode));
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
|
||||
@At
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("添加")
|
||||
@SLog(tag = "理论学习课程", msg = "添加课程视频")
|
||||
public Result insert(EduVideos eduVideos) {
|
||||
dao.insert(eduVideos);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
@At
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("修改")
|
||||
@SLog(tag = "理论学习课程", msg = "修改课程视频")
|
||||
public Result update(EduVideos eduVideos) {
|
||||
dao.update(eduVideos);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("edu.courses")
|
||||
@ApiOperation("删除")
|
||||
@SLog(tag = "理论学习课程", msg = "删除课程视频,id:${args[0]}")
|
||||
public Result delete(@Param("id") String id) {
|
||||
dao.delete(EduVideos.class, id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package com.budwk.app.zhgh.dayofficework.edu.h5controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduChapters;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduCourses;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduStudyRecords;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduVideos;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.service.EduCoursesService;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.service.EduStudyRecordsService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @ClassName H5EduController
|
||||
* @Description 移动端教育培训控制器
|
||||
* @Author AI Assistant
|
||||
* @Date 2024/01/01
|
||||
*/
|
||||
@Slf4j
|
||||
@IocBean
|
||||
@At("/platform/h5/edu")
|
||||
@Ok("json:full")
|
||||
@Api(tags = "移动端教育培训")
|
||||
public class H5EduController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private EduCoursesService eduCoursesService;
|
||||
@Inject
|
||||
private EduStudyRecordsService eduStudyRecordsService;
|
||||
|
||||
@At("/courses")
|
||||
@Ok("beetl:/platform/zhghh5/dayofficework/edu/courses/index.html")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("课程列表页面")
|
||||
public void coursesIndex() {
|
||||
}
|
||||
|
||||
@At("/courses/list")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("获取课程列表")
|
||||
public Result getCoursesList(PageForm pageForm, @Param("category") String category) {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and("disabled", "=", false);
|
||||
if (StrUtil.isNotBlank(category)) {
|
||||
cnd.and("category", "=", category);
|
||||
}
|
||||
cnd.orderBy("createdAt", "desc");
|
||||
Pagination pagination = eduCoursesService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
||||
return Result.success(pagination);
|
||||
}
|
||||
|
||||
@At("/course/detail")
|
||||
@Ok("beetl:/platform/zhghh5/dayofficework/edu/course/detail.html")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("课程详情页面")
|
||||
public void courseDetail() {
|
||||
}
|
||||
|
||||
@At("/course/detail/data")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("获取课程详情数据")
|
||||
public Result getCourseDetail(@Param("courseId") String courseId) {
|
||||
try {
|
||||
if (courseId == null || courseId.trim().isEmpty()) {
|
||||
return Result.error("课程ID不能为空");
|
||||
}
|
||||
|
||||
EduCourses course = dao.fetch(EduCourses.class, courseId);
|
||||
if (course == null) {
|
||||
return Result.error("课程不存在");
|
||||
}
|
||||
|
||||
// 获取课程章节
|
||||
List<EduChapters> chapters = dao.query(EduChapters.class,
|
||||
Cnd.where("courseId", "=", courseId).orderBy("sortCode", "asc"));
|
||||
|
||||
// 为每个章节获取视频列表
|
||||
for (EduChapters chapter : chapters) {
|
||||
List<EduVideos> videos = dao.query(EduVideos.class,
|
||||
Cnd.where("chapterId", "=", chapter.getId()).orderBy("sortCode", "asc"));
|
||||
chapter.setVideos(videos);
|
||||
}
|
||||
|
||||
course.setChapters(chapters);
|
||||
return Result.success(course);
|
||||
} catch (Exception e) {
|
||||
log.error("获取课程详情失败", e);
|
||||
return Result.error("获取课程详情失败");
|
||||
}
|
||||
}
|
||||
|
||||
@At("/course/progress")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("获取课程学习进度")
|
||||
public Result getCourseProgress(@Param("courseId") String courseId) {
|
||||
try {
|
||||
if (courseId == null || courseId.trim().isEmpty()) {
|
||||
return Result.error("课程ID不能为空");
|
||||
}
|
||||
|
||||
String userId = SecurityUtil.getUserId();
|
||||
if (userId == null) {
|
||||
return Result.error("用户未登录");
|
||||
}
|
||||
|
||||
// 获取课程总视频数
|
||||
// int totalVideos = dao.count("edu_videos v",
|
||||
// "LEFT JOIN edu_chapters c ON v.chapterId = c.id",
|
||||
// Cnd.where("c.courseId", "=", courseId));
|
||||
int totalVideos = 10;
|
||||
|
||||
// 获取已完成的视频ID列表
|
||||
List<EduStudyRecords> completedRecords = dao.query(EduStudyRecords.class,
|
||||
Cnd.where("userId", "=", userId)
|
||||
.and("courseId", "=", courseId)
|
||||
.and("completed", "=", true));
|
||||
|
||||
List<String> completedVideoIds = completedRecords.stream()
|
||||
.map(EduStudyRecords::getVideoId)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
int completedVideos = completedVideoIds.size();
|
||||
|
||||
java.util.Map<String, Object> progressData = new java.util.HashMap<>();
|
||||
progressData.put("totalVideos", totalVideos);
|
||||
progressData.put("completedVideos", completedVideos);
|
||||
progressData.put("completedVideoIds", completedVideoIds);
|
||||
|
||||
return Result.success(progressData);
|
||||
} catch (Exception e) {
|
||||
log.error("获取课程进度失败", e);
|
||||
return Result.error("获取课程进度失败");
|
||||
}
|
||||
}
|
||||
|
||||
@At("/video/detail")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("获取视频详情")
|
||||
public Result getVideoDetail(@Param("videoId") String videoId) {
|
||||
try {
|
||||
if (videoId == null || videoId.trim().isEmpty()) {
|
||||
return Result.error("视频ID不能为空");
|
||||
}
|
||||
|
||||
EduVideos video = dao.fetch(EduVideos.class, videoId);
|
||||
if (video == null) {
|
||||
return Result.error("视频不存在");
|
||||
}
|
||||
|
||||
return Result.success(video);
|
||||
} catch (Exception e) {
|
||||
log.error("获取视频详情失败", e);
|
||||
return Result.error("获取视频详情失败");
|
||||
}
|
||||
}
|
||||
|
||||
@At("/video/play")
|
||||
@Ok("beetl:/platform/zhghh5/dayofficework/edu/video/play.html")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("视频播放页面")
|
||||
public void videoPlay() {
|
||||
}
|
||||
|
||||
@At("/study/history")
|
||||
@Ok("beetl:/platform/zhghh5/dayofficework/edu/study/history.html")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("学习历史页面")
|
||||
public void studyHistory() {
|
||||
}
|
||||
|
||||
@At("/study/progress/save")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("保存观看进度")
|
||||
public Result saveWatchProgress(@Param("videoId") String videoId,
|
||||
@Param("courseId") String courseId,
|
||||
@Param("watchedDuration") Integer watchedDuration) {
|
||||
try {
|
||||
if (videoId == null || videoId.trim().isEmpty()) {
|
||||
return Result.error("视频ID不能为空");
|
||||
}
|
||||
if (courseId == null || courseId.trim().isEmpty()) {
|
||||
return Result.error("课程ID不能为空");
|
||||
}
|
||||
if (watchedDuration == null || watchedDuration < 0) {
|
||||
return Result.error("观看时长不能为空或小于0");
|
||||
}
|
||||
|
||||
String userId = SecurityUtil.getUserId();
|
||||
if (userId == null) {
|
||||
return Result.error("用户未登录");
|
||||
}
|
||||
|
||||
EduStudyRecords record = eduStudyRecordsService.saveOrUpdateWatchProgress(
|
||||
userId, videoId, courseId, watchedDuration);
|
||||
|
||||
return Result.success(record);
|
||||
} catch (Exception e) {
|
||||
log.error("保存观看进度失败", e);
|
||||
return Result.error("保存观看进度失败");
|
||||
}
|
||||
}
|
||||
|
||||
@At("/study/complete")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("标记视频完成")
|
||||
public Result markVideoCompleted(@Param("videoId") String videoId,
|
||||
@Param("courseId") String courseId) {
|
||||
try {
|
||||
if (videoId == null || videoId.trim().isEmpty()) {
|
||||
return Result.error("视频ID不能为空");
|
||||
}
|
||||
if (courseId == null || courseId.trim().isEmpty()) {
|
||||
return Result.error("课程ID不能为空");
|
||||
}
|
||||
|
||||
String userId = SecurityUtil.getUserId();
|
||||
if (userId == null) {
|
||||
return Result.error("用户未登录");
|
||||
}
|
||||
|
||||
EduStudyRecords record = eduStudyRecordsService.markVideoCompleted(
|
||||
userId, videoId, courseId);
|
||||
|
||||
return Result.success(record);
|
||||
} catch (Exception e) {
|
||||
log.error("标记视频完成失败", e);
|
||||
return Result.error("标记视频完成失败");
|
||||
}
|
||||
}
|
||||
|
||||
@At("/study/history/list")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("获取学习历史列表")
|
||||
public Result getStudyHistoryList(@Param("courseId") String courseId) {
|
||||
try {
|
||||
String userId = SecurityUtil.getUserId();
|
||||
if (userId == null) {
|
||||
return Result.error("用户未登录");
|
||||
}
|
||||
|
||||
List<EduStudyRecords> historyList = eduStudyRecordsService.getUserStudyHistory(userId, courseId);
|
||||
return Result.success(historyList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取学习历史失败", e);
|
||||
return Result.error("获取学习历史失败");
|
||||
}
|
||||
}
|
||||
|
||||
@At("/study/progress/stats")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("获取课程学习进度统计")
|
||||
public Result getCourseProgressStats(@Param("courseId") String courseId) {
|
||||
try {
|
||||
if (courseId == null || courseId.trim().isEmpty()) {
|
||||
return Result.error("课程ID不能为空");
|
||||
}
|
||||
|
||||
String userId = SecurityUtil.getUserId();
|
||||
if (userId == null) {
|
||||
return Result.error("用户未登录");
|
||||
}
|
||||
|
||||
EduStudyRecordsService.StudyProgressStats stats =
|
||||
eduStudyRecordsService.getCourseProgressStats(userId, courseId);
|
||||
|
||||
return Result.success(stats);
|
||||
} catch (Exception e) {
|
||||
log.error("获取课程进度统计失败", e);
|
||||
return Result.error("获取课程进度统计失败");
|
||||
}
|
||||
}
|
||||
|
||||
@At("/study/record")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("获取用户视频学习记录")
|
||||
public Result getUserStudyRecord(@Param("videoId") String videoId) {
|
||||
try {
|
||||
if (videoId == null || videoId.trim().isEmpty()) {
|
||||
return Result.error("视频ID不能为空");
|
||||
}
|
||||
|
||||
String userId = SecurityUtil.getUserId();
|
||||
if (userId == null) {
|
||||
return Result.error("用户未登录");
|
||||
}
|
||||
|
||||
EduStudyRecords record = eduStudyRecordsService.getUserStudyRecord(userId, videoId);
|
||||
return Result.success(record);
|
||||
} catch (Exception e) {
|
||||
log.error("获取学习记录失败", e);
|
||||
return Result.error("获取学习记录失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Table("edu_courses")
|
||||
@@ -44,5 +46,10 @@ public class EduCourses extends BaseModel {
|
||||
@Default(value = "0")
|
||||
private Boolean disabled;
|
||||
|
||||
/**
|
||||
* 章节列表
|
||||
*/
|
||||
@Many(field = "courseId")
|
||||
private List<EduChapters> chapters;
|
||||
|
||||
}
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package com.budwk.app.zhgh.dayofficework.edu.service;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduStudyRecords;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @ClassName EduStudyRecordsService
|
||||
* @Description 学习记录服务接口
|
||||
* @Author AI Assistant
|
||||
* @Date 2024/01/01
|
||||
*/
|
||||
public interface EduStudyRecordsService extends BaseService<EduStudyRecords> {
|
||||
|
||||
/**
|
||||
* 保存或更新观看进度
|
||||
* @param userId 用户ID
|
||||
* @param videoId 视频ID
|
||||
* @param courseId 课程ID
|
||||
* @param watchedDuration 观看时长(秒)
|
||||
* @return 学习记录
|
||||
*/
|
||||
EduStudyRecords saveOrUpdateWatchProgress(String userId, String videoId, String courseId, Integer watchedDuration);
|
||||
|
||||
/**
|
||||
* 标记视频为已完成
|
||||
* @param userId 用户ID
|
||||
* @param videoId 视频ID
|
||||
* @param courseId 课程ID
|
||||
* @return 学习记录
|
||||
*/
|
||||
EduStudyRecords markVideoCompleted(String userId, String videoId, String courseId);
|
||||
|
||||
/**
|
||||
* 获取用户的学习记录
|
||||
* @param userId 用户ID
|
||||
* @param videoId 视频ID(可选)
|
||||
* @return 学习记录
|
||||
*/
|
||||
EduStudyRecords getUserStudyRecord(String userId, String videoId);
|
||||
|
||||
/**
|
||||
* 获取用户的学习历史列表
|
||||
* @param userId 用户ID
|
||||
* @param courseId 课程ID(可选)
|
||||
* @return 学习记录列表
|
||||
*/
|
||||
List<EduStudyRecords> getUserStudyHistory(String userId, String courseId);
|
||||
|
||||
/**
|
||||
* 获取用户课程的学习进度统计
|
||||
* @param userId 用户ID
|
||||
* @param courseId 课程ID
|
||||
* @return 进度统计信息
|
||||
*/
|
||||
StudyProgressStats getCourseProgressStats(String userId, String courseId);
|
||||
|
||||
/**
|
||||
* 学习进度统计信息
|
||||
*/
|
||||
class StudyProgressStats {
|
||||
private int totalVideos; // 总视频数
|
||||
private int completedVideos; // 已完成视频数
|
||||
private int totalDuration; // 总时长(秒)
|
||||
private int watchedDuration; // 已观看时长(秒)
|
||||
private double completionRate; // 完成率
|
||||
|
||||
public StudyProgressStats() {}
|
||||
|
||||
public StudyProgressStats(int totalVideos, int completedVideos, int totalDuration, int watchedDuration) {
|
||||
this.totalVideos = totalVideos;
|
||||
this.completedVideos = completedVideos;
|
||||
this.totalDuration = totalDuration;
|
||||
this.watchedDuration = watchedDuration;
|
||||
this.completionRate = totalVideos > 0 ? (double) completedVideos / totalVideos * 100 : 0;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public int getTotalVideos() { return totalVideos; }
|
||||
public void setTotalVideos(int totalVideos) { this.totalVideos = totalVideos; }
|
||||
|
||||
public int getCompletedVideos() { return completedVideos; }
|
||||
public void setCompletedVideos(int completedVideos) { this.completedVideos = completedVideos; }
|
||||
|
||||
public int getTotalDuration() { return totalDuration; }
|
||||
public void setTotalDuration(int totalDuration) { this.totalDuration = totalDuration; }
|
||||
|
||||
public int getWatchedDuration() { return watchedDuration; }
|
||||
public void setWatchedDuration(int watchedDuration) { this.watchedDuration = watchedDuration; }
|
||||
|
||||
public double getCompletionRate() { return completionRate; }
|
||||
public void setCompletionRate(double completionRate) { this.completionRate = completionRate; }
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package com.budwk.app.zhgh.dayofficework.edu.service.impl;
|
||||
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduStudyRecords;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.models.EduVideos;
|
||||
import com.budwk.app.zhgh.dayofficework.edu.service.EduStudyRecordsService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @ClassName EduStudyRecordsServiceImpl
|
||||
* @Description 学习记录服务实现类
|
||||
* @Author AI Assistant
|
||||
* @Date 2024/01/01
|
||||
*/
|
||||
@Slf4j
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class EduStudyRecordsServiceImpl extends BaseServiceImpl<EduStudyRecords> implements EduStudyRecordsService {
|
||||
|
||||
public EduStudyRecordsServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EduStudyRecords saveOrUpdateWatchProgress(String userId, String videoId, String courseId, Integer watchedDuration) {
|
||||
try {
|
||||
// 查找现有记录
|
||||
EduStudyRecords existingRecord = dao().fetch(EduStudyRecords.class,
|
||||
Cnd.where("userId", "=", userId)
|
||||
.and("videoId", "=", videoId)
|
||||
.and("courseId", "=", courseId));
|
||||
|
||||
Date now = new Date();
|
||||
|
||||
if (existingRecord != null) {
|
||||
// 更新现有记录
|
||||
existingRecord.setWatchedDuration(watchedDuration);
|
||||
existingRecord.setLastWatchedTime(now);
|
||||
|
||||
// 获取视频总时长,判断是否完成
|
||||
EduVideos video = dao().fetch(EduVideos.class, videoId);
|
||||
if (video != null && video.getDuration() != null && watchedDuration >= video.getDuration() * 0.9) {
|
||||
existingRecord.setIsCompleted(true);
|
||||
}
|
||||
|
||||
dao().update(existingRecord);
|
||||
return existingRecord;
|
||||
} else {
|
||||
// 创建新记录
|
||||
EduStudyRecords newRecord = new EduStudyRecords();
|
||||
newRecord.setUserId(userId);
|
||||
newRecord.setVideoId(videoId);
|
||||
newRecord.setCourseId(courseId);
|
||||
newRecord.setWatchedDuration(watchedDuration);
|
||||
newRecord.setLastWatchedTime(now);
|
||||
|
||||
// 获取视频总时长,判断是否完成
|
||||
EduVideos video = dao().fetch(EduVideos.class, videoId);
|
||||
if (video != null && video.getDuration() != null && watchedDuration >= video.getDuration() * 0.9) {
|
||||
newRecord.setIsCompleted(true);
|
||||
} else {
|
||||
newRecord.setIsCompleted(false);
|
||||
}
|
||||
|
||||
dao().insert(newRecord);
|
||||
return newRecord;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("保存观看进度失败", e);
|
||||
throw new RuntimeException("保存观看进度失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EduStudyRecords markVideoCompleted(String userId, String videoId, String courseId) {
|
||||
try {
|
||||
EduStudyRecords record = dao().fetch(EduStudyRecords.class,
|
||||
Cnd.where("userId", "=", userId)
|
||||
.and("videoId", "=", videoId)
|
||||
.and("courseId", "=", courseId));
|
||||
|
||||
Date now = new Date();
|
||||
|
||||
if (record != null) {
|
||||
record.setIsCompleted(true);
|
||||
record.setLastWatchedTime(now);
|
||||
|
||||
// 如果观看时长为0,设置为视频总时长
|
||||
if (record.getWatchedDuration() == null || record.getWatchedDuration() == 0) {
|
||||
EduVideos video = dao().fetch(EduVideos.class, videoId);
|
||||
if (video != null && video.getDuration() != null) {
|
||||
record.setWatchedDuration(video.getDuration());
|
||||
}
|
||||
}
|
||||
|
||||
dao().update(record);
|
||||
return record;
|
||||
} else {
|
||||
// 创建新的完成记录
|
||||
EduStudyRecords newRecord = new EduStudyRecords();
|
||||
newRecord.setUserId(userId);
|
||||
newRecord.setVideoId(videoId);
|
||||
newRecord.setCourseId(courseId);
|
||||
newRecord.setIsCompleted(true);
|
||||
newRecord.setLastWatchedTime(now);
|
||||
|
||||
// 设置观看时长为视频总时长
|
||||
EduVideos video = dao().fetch(EduVideos.class, videoId);
|
||||
if (video != null && video.getDuration() != null) {
|
||||
newRecord.setWatchedDuration(video.getDuration());
|
||||
} else {
|
||||
newRecord.setWatchedDuration(0);
|
||||
}
|
||||
|
||||
dao().insert(newRecord);
|
||||
return newRecord;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("标记视频完成失败", e);
|
||||
throw new RuntimeException("标记视频完成失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EduStudyRecords getUserStudyRecord(String userId, String videoId) {
|
||||
try {
|
||||
return dao().fetch(EduStudyRecords.class,
|
||||
Cnd.where("userId", "=", userId)
|
||||
.and("videoId", "=", videoId));
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户学习记录失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EduStudyRecords> getUserStudyHistory(String userId, String courseId) {
|
||||
try {
|
||||
Cnd cnd = Cnd.where("userId", "=", userId);
|
||||
if (courseId != null && !courseId.trim().isEmpty()) {
|
||||
cnd.and("courseId", "=", courseId);
|
||||
}
|
||||
cnd.orderBy("lastWatchedTime", "desc");
|
||||
|
||||
return dao().query(EduStudyRecords.class, cnd);
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户学习历史失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StudyProgressStats getCourseProgressStats(String userId, String courseId) {
|
||||
try {
|
||||
// 获取课程下所有视频的总数和总时长
|
||||
Sql videoStatsSql = Sqls.create("SELECT COUNT(*) as total_videos, COALESCE(SUM(duration), 0) as total_duration " +
|
||||
"FROM edu_videos v " +
|
||||
"INNER JOIN edu_chapters c ON v.chapterId = c.id " +
|
||||
"WHERE c.courseId = @courseId");
|
||||
videoStatsSql.params().set("courseId", courseId);
|
||||
videoStatsSql.setCallback(Sqls.callback.maps());
|
||||
dao().execute(videoStatsSql);
|
||||
|
||||
int totalVideos = 0;
|
||||
int totalDuration = 0;
|
||||
// if (!videoStatsSql.getList().isEmpty()) {
|
||||
// totalVideos = ((Number) videoStatsSql.getList().get(0).get("total_videos")).intValue();
|
||||
// totalDuration = ((Number) videoStatsSql.getList().get(0).get("total_duration")).intValue();
|
||||
// }
|
||||
|
||||
// 获取用户已完成的视频数和观看时长
|
||||
Sql userStatsSql = Sqls.create("SELECT COUNT(*) as completed_videos, COALESCE(SUM(watchedDuration), 0) as watched_duration " +
|
||||
"FROM edu_study_records " +
|
||||
"WHERE userId = @userId AND courseId = @courseId AND isCompleted = 1");
|
||||
userStatsSql.params().set("userId", userId).set("courseId", courseId);
|
||||
userStatsSql.setCallback(Sqls.callback.maps());
|
||||
dao().execute(userStatsSql);
|
||||
|
||||
int completedVideos = 0;
|
||||
int watchedDuration = 0;
|
||||
// if (!userStatsSql.getList().isEmpty()) {
|
||||
// completedVideos = ((Number) userStatsSql.getList().get(0).get("completed_videos")).intValue();
|
||||
// watchedDuration = ((Number) userStatsSql.getList().get(0).get("watched_duration")).intValue();
|
||||
// }
|
||||
|
||||
return new StudyProgressStats(totalVideos, completedVideos, totalDuration, watchedDuration);
|
||||
} catch (Exception e) {
|
||||
log.error("获取课程进度统计失败", e);
|
||||
return new StudyProgressStats(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -59,7 +59,7 @@ public class ProposalDelegationController {
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At("/platform/h5/proposal/delegation")
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/delegation/index.html")
|
||||
@SaCheckPermission("proposal.delegation")
|
||||
public void h5Index() {
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ public class ProposalFeedbackEvaluationController {
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5/proposal/feedbackEvaluation",top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/feedbackEvaluation/index.html")
|
||||
@SaCheckPermission("proposal.feedbackEvaluation")
|
||||
public void h5Index() {
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ public class ProposalMineController {
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5/proposal/mine",top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/mine/index.html")
|
||||
@SaCheckPermission("proposal.mine")
|
||||
public void h5Index() {
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ public class ProposalSchoolLeaderApprovalController {
|
||||
}
|
||||
|
||||
|
||||
@At(value = "/platform/h5/proposal/schoolLeaderApproval",top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/schoolLeaderApproval/index.html")
|
||||
@SaCheckPermission("proposal.schoolLeaderApproval")
|
||||
public void h5Index() {
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public class ProposalSecondedController {
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5/proposal/seconded",top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/seconded/index.html")
|
||||
@SaCheckPermission("proposal.seconded")
|
||||
public void h5Index() {
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ public class ProposalUnderTakeReplyController {
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5/proposal/unitReply",top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/unitReply/index.html")
|
||||
@SaCheckPermission("proposal.unitReply")
|
||||
public void h5Index() {
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ public class ProposalWriteController {
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5/proposal/write",top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/write/index.html")
|
||||
@SaCheckPermission("proposal.write")
|
||||
public void h5Index(){
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ public class SuggestionBoxMineController {
|
||||
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5/suggestionBox/mine", top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/suggestionBox/mine/index.html")
|
||||
@SaCheckLogin
|
||||
public void h5Index() {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public class SuggestionBoxQueryController {
|
||||
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5/suggestionBox/query", top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/suggestionBox/query/index.html")
|
||||
@SaCheckPermission("suggestionBox.query")
|
||||
public void h5Index() {
|
||||
|
||||
+1
-2
@@ -46,9 +46,8 @@ public class SuggestionBoxWriteController {
|
||||
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5/suggestionBox/write",top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/suggestionBox/write/index.html")
|
||||
// @SaCheckPermission("h5.suggestionBox.write")
|
||||
@SaCheckLogin
|
||||
public void h5Index(){
|
||||
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
package com.budwk.app.zhgh.democratic.suggestionBox.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.democratic.suggestionBox.models.SuggestionBox;
|
||||
import io.swagger.annotations.Api;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
@IocBean
|
||||
@At("/platform/suggestionBox/view")
|
||||
@Ok("json:full")
|
||||
@Api("意见箱")
|
||||
public class SuggestionViewController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@At("/index")
|
||||
@Ok("beetl:/platform/zhgh/dayofficework/suggestionBox/view/index.html")
|
||||
@SaCheckLogin
|
||||
public void index() {
|
||||
|
||||
}
|
||||
|
||||
@At("/info")
|
||||
@SaCheckLogin
|
||||
public Result info(String id) {
|
||||
SuggestionBox box = dao.fetch(SuggestionBox.class, id);
|
||||
return Result.success(box);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -41,7 +41,7 @@ public class SuggestionXghController {
|
||||
|
||||
}
|
||||
|
||||
@At(value = "/platform/h5/suggestionBox/xgh",top = true)
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/democratic/suggestionBox/xgh/index.html")
|
||||
@SaCheckPermission("suggestionBox.xgh")
|
||||
public void h5Index() {
|
||||
|
||||
@@ -19,14 +19,7 @@ const basicForm = {
|
||||
></file-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="课程分类" prop="category">
|
||||
<el-select v-model="formData.category" placeholder="请选择分类" style="width: 100%;">
|
||||
<el-option label="前端开发" value="前端开发"></el-option>
|
||||
<el-option label="后端开发" value="后端开发"></el-option>
|
||||
<el-option label="移动开发" value="移动开发"></el-option>
|
||||
<el-option label="数据库" value="数据库"></el-option>
|
||||
<el-option label="运维部署" value="运维部署"></el-option>
|
||||
<el-option label="人工智能" value="人工智能"></el-option>
|
||||
</el-select>
|
||||
<dict-select v-model="formData.category" code="TRAIN_EDU_COURSE_TYPE" style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否启用">
|
||||
<el-switch v-model="formData.disabled" :active-value="false" :inactive-value="true"></el-switch>
|
||||
|
||||
@@ -161,8 +161,6 @@ const contentForm = {
|
||||
</video>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
|
||||
</el-dialog>
|
||||
`,
|
||||
data() {
|
||||
@@ -352,12 +350,12 @@ const contentForm = {
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
|
||||
.edu_content_dialog .el-dialog__body {
|
||||
::v-deep .edu_content_dialog .el-dialog__body {
|
||||
height: calc(100vh - 155px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
::v-deep .section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -365,7 +363,7 @@ const contentForm = {
|
||||
padding: 20px 20px 0;
|
||||
}
|
||||
|
||||
.video-item {
|
||||
::v-deep .video-item {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
@@ -374,19 +372,19 @@ const contentForm = {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.video-item:hover {
|
||||
::v-deep .video-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.chapter-card {
|
||||
::v-deep .chapter-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chapter-header {
|
||||
::v-deep .chapter-header {
|
||||
background: #f5f7fa;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
@@ -395,11 +393,11 @@ const contentForm = {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chapter-content {
|
||||
::v-deep .chapter-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
::v-deep .stats-card {
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
@@ -407,14 +405,14 @@ const contentForm = {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
::v-deep .course-cover {
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.duration-display {
|
||||
::v-deep .duration-display {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.course-cover{
|
||||
width: 100px;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<table-tool>
|
||||
@@ -9,19 +16,12 @@ layout("/layouts/platform.html"){
|
||||
</table-tool>
|
||||
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="title" label="课程标题" min-width="150">
|
||||
<el-table-column prop="cover" label="课程封面" width="150">
|
||||
<template slot-scope="scope">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<img v-if="scope.row.cover" :src="scope.row.cover" class="course-cover"
|
||||
style="margin-right: 10px;">
|
||||
<div v-else
|
||||
style="width: 100px; height: 60px; background: #f0f0f0; border-radius: 4px; margin-right: 10px; display: flex; align-items: center; justify-content: center; color: #999;">
|
||||
无封面
|
||||
</div>
|
||||
<strong>{{ scope.row.title }}</strong>
|
||||
</div>
|
||||
<img v-if="scope.row.cover" :src="scope.row.cover" class="course-cover">
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="课程标题" min-width="150"></el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="category" label="分类" width="120">
|
||||
<template slot-scope="scope">
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
<div id="suggestion-box-apply-form" v-cloak>
|
||||
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":" class="flow-task-form">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="姓名">{{formData.userName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工号">{{formData.loginName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="单位">{{formData.unitName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工会">{{formData.unionName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">
|
||||
<el-form-item prop="title" label="标题">
|
||||
<el-input v-model="formData.title" maxlength="100" show-word-limit placeholder="请输入标题"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="填写意见建议内容" :span="2">
|
||||
<el-form-item prop="content" label="填写意见建议内容">
|
||||
<el-input v-model="formData.content" type="textarea" maxlength="500" show-word-limit placeholder="请输入内容"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-form>
|
||||
<snaker-flow-task-form-action @task-action="handleTaskAction" @save-draft="handleSaveDraft"></snaker-flow-task-form-action>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#suggestion-box-apply-form",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
businessId: GetQueryString("businessId"),
|
||||
instanceId: GetQueryString("instanceId"),
|
||||
formData: {},
|
||||
formRules: {
|
||||
title: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
content: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTaskAction(val) {
|
||||
console.log(val)
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (valid) {
|
||||
if (!this.instanceId) {
|
||||
this.handleApply(val)
|
||||
} else {
|
||||
this.handleReApply(val)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 提交申请
|
||||
handleApply(val) {
|
||||
this.$axios
|
||||
.post("/flow/common/startInstanceAndExecute", {
|
||||
...val,
|
||||
bizData: JSON.stringify(this.formData)
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
window.GlobalBroadcastChannel.postMessage({
|
||||
type: "task-complete"
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 重新提交申请
|
||||
handleReApply(val) {
|
||||
this.$axios
|
||||
.post("/flow/common/executeTask", {
|
||||
data: JSON.stringify({
|
||||
processTaskId: val.taskId,
|
||||
processInstanceId: val.instanceId,
|
||||
submitType: val.submitType,
|
||||
f_data: JSON.stringify(this.formData)
|
||||
})
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
window.GlobalBroadcastChannel.postMessage({
|
||||
type: "task-complete"
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 保存申请
|
||||
handleSaveDraft(val) {
|
||||
this.$refs.formRef.validateField(["title"], (errMsg) => {
|
||||
if (errMsg) {
|
||||
this.$message.warning("请填写标题")
|
||||
return
|
||||
}
|
||||
this.$axios
|
||||
.post("/flow/common/startInstance", {
|
||||
...val,
|
||||
bizData: JSON.stringify(this.formData)
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化
|
||||
init() {
|
||||
if (this.businessId) {
|
||||
this.$axios.post("/platform/suggestionBox/view/info", { id: this.businessId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.formData = res.data
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const { username, loginname, id, unit, union, mobile } = this.$store.state.user
|
||||
this.formData = {
|
||||
userName: username,
|
||||
loginName: loginname,
|
||||
submitterId: id,
|
||||
unitId: unit?.id,
|
||||
unitName: unit?.name,
|
||||
unionId: union?.id,
|
||||
unionName: union?.name,
|
||||
concat: mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,128 +0,0 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<guava ref="guava">
|
||||
<div id="app">
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度">
|
||||
<el-date-picker v-model="pageForm.year" type="year" value-format="yyyy" placeholder="年度" style="width: 100%"></el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="标题">
|
||||
<el-input v-model="pageForm.title" placeholder="标题" clearable></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<table-tool></table-tool>
|
||||
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="title" label="投稿标题"></el-table-column>
|
||||
<el-table-column prop="loginName" label="投稿人工号"></el-table-column>
|
||||
<el-table-column prop="userName" label="投稿人姓名"></el-table-column>
|
||||
<el-table-column prop="unitName" label="投稿人单位"></el-table-column>
|
||||
<el-table-column prop="unionName" label="投稿人工会"></el-table-column>
|
||||
<el-table-column prop="submitTime" label="投稿时间"></el-table-column>
|
||||
<el-table-column prop="taskName" label="当前节点"></el-table-column>
|
||||
<el-table-column prop="instanceState" label="流程状态">
|
||||
<template slot-scope="{row}">
|
||||
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="300px">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
|
||||
<el-button v-if="row.taskKey === 'startTask'" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||
<el-button v-if="row.canRevoke" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
|
||||
<!-- <el-button v-if="row.instanceState === 10" size="mini" type="danger" @click="onWithDraw(row)">撤销</el-button>-->
|
||||
<!-- v-if="row.instanceState === 30"-->
|
||||
<el-button @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</div>
|
||||
</guava>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
pageDataUrl: "/platform/suggestionBox2/apply/pageData"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openView(row) {
|
||||
window.open("/flow/common/approval/form?" + "instanceId=" + (row.instanceId || "") + "&businessId=" + row.id + "&defineKey=XWTG")
|
||||
},
|
||||
onEdit(row) {
|
||||
window.open(
|
||||
"/flow/common/approval/form?taskId=" +
|
||||
(row.taskId || "") +
|
||||
"&instanceId=" +
|
||||
(row.instanceId || "") +
|
||||
"&businessId=" +
|
||||
row.id +
|
||||
"&defineKey=XWTG"
|
||||
)
|
||||
},
|
||||
|
||||
onRevoke(row) {
|
||||
this.$confirm("您确定要撤回吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post("/flow/common/revokeTask", { taskId: row.startTaskId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
onWithDraw({ instanceId }) {
|
||||
this.$confirm("您确定要撤销申请吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post("/flow/common/withdrawInstance", { instanceId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
onDelete(id) {
|
||||
this.$confirm("您确定要删除吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post("/platform/suggestionBox2/apply/delete", { id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -1,88 +0,0 @@
|
||||
<div id="suggestion-box-view">
|
||||
<!-- <el-form ref="formRef" label-width="0" label-suffix=":" class="flow-task-form">-->
|
||||
<el-descriptions :column="2" border class="flow-task-form">
|
||||
<el-descriptions-item label="姓名">{{viewData.userName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工号">{{viewData.loginName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="单位">{{viewData.unitName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工会">{{viewData.unionName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">{{viewData.title}}</el-descriptions-item>
|
||||
<el-descriptions-item label="填写意见建议内容" :span="2">{{viewData.content}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template v-for="task in doneTasks">
|
||||
<div class="task-panel mt10">
|
||||
<div class="task-panel-header">{{ task.displayName }}</div>
|
||||
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
|
||||
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
|
||||
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})</el-descriptions-item>
|
||||
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理意见" v-if="!task.ext.isFirstTaskNode">{{ task.taskFormData.opinion }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
<!-- </el-form>-->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#suggestion-box-view",
|
||||
store,
|
||||
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
businessId: GetQueryString("businessId"),
|
||||
instanceId: GetQueryString("instanceId"),
|
||||
viewData: {},
|
||||
doneTasks: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
info() {
|
||||
this.$axios.post("/platform/suggestionBox/view/info", { id: this.businessId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.viewData = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
getDoneTasks() {
|
||||
this.$axios.post("/flow/common/doneTasks", { instanceId: this.instanceId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.doneTasks = res.data
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.info()
|
||||
this.getDoneTasks()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.task-panel {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-panel-header {
|
||||
background: rgb(250, 250, 250);
|
||||
border: 1px solid rgb(228, 231, 237);
|
||||
border-bottom: none;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
<div id="suggestion-box-apply-form" v-cloak>
|
||||
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":" class="flow-task-form">
|
||||
<el-form-item label="审批意见" prop="tf_opinion" :rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<snaker-flow-task-form-action @task-action="handleTaskAction" @cancel="handleCancel"></snaker-flow-task-form-action>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#suggestion-box-apply-form",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
businessId: GetQueryString("businessId"),
|
||||
formData: {},
|
||||
formRules: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTaskAction(val) {
|
||||
console.log(val)
|
||||
|
||||
this.$axios
|
||||
.post("/flow/common/executeTask", {
|
||||
data: JSON.stringify({
|
||||
...val,
|
||||
...this.formData
|
||||
})
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("操作成功")
|
||||
// 发送完成消息
|
||||
window.GlobalBroadcastChannel.postMessage({
|
||||
type: "task-complete",
|
||||
payload: val
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
handleCancel(val) {
|
||||
console.log(val)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,81 +0,0 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<guava ref="guava">
|
||||
<div id="app">
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度">
|
||||
<el-date-picker v-model="pageForm.year" type="year" value-format="yyyy" placeholder="年度" style="width: 100%"></el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="标题">
|
||||
<el-input v-model="pageForm.title" placeholder="标题" clearable></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<table-tool>
|
||||
<el-radio-group v-model="pageForm.approval" size="small" @change="doSearch">
|
||||
<el-radio-button :label="true">已审核</el-radio-button>
|
||||
<el-radio-button :label="false">未审核</el-radio-button>
|
||||
</el-radio-group>
|
||||
</table-tool>
|
||||
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="title" label="投稿标题"></el-table-column>
|
||||
<el-table-column prop="loginName" label="投稿人工号"></el-table-column>
|
||||
<el-table-column prop="userName" label="投稿人姓名"></el-table-column>
|
||||
<el-table-column prop="unitName" label="投稿人单位"></el-table-column>
|
||||
<el-table-column prop="unionName" label="投稿人工会"></el-table-column>
|
||||
<el-table-column prop="submitTime" label="投稿时间"></el-table-column>
|
||||
<el-table-column prop="curTaskName" label="当前节点"></el-table-column>
|
||||
<el-table-column prop="instanceState" label="流程状态">
|
||||
<template slot-scope="{row}">
|
||||
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="200px">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
|
||||
<el-button v-if="row.canRevoke" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
|
||||
<!-- <el-button v-if="row.taskState === 10" @click="openView(row)" size="mini" type="primary">审核</el-button>-->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</div>
|
||||
</guava>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
pageDataUrl: "/platform/suggestionBox2/xgh/pageData",
|
||||
pageForm: {
|
||||
approval: false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openView(row) {
|
||||
window.open("/flow/common/approval/form?taskId=" + row.taskId + "&instanceId=" + row.instanceId + "&businessId=" + row.businessNo)
|
||||
},
|
||||
onRevoke(row) {
|
||||
console.log(row)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,271 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="课程详情" placeholder fixed></van-nav-bar>
|
||||
|
||||
<div v-if="loading" class="loading-container">
|
||||
<van-loading type="spinner" size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<div v-else-if="course">
|
||||
<!-- 课程封面 -->
|
||||
<div class="course-cover">
|
||||
<van-image v-if="course.cover" :src="course.cover" fit="cover" :alt="course.title">
|
||||
<template v-slot:error>
|
||||
<van-icon name="photo-fail" size="48" color="#ddd"></van-icon>
|
||||
</template>
|
||||
</van-image>
|
||||
<van-icon v-else name="graduation" size="48" color="#ddd" ></van-icon>
|
||||
</div>
|
||||
|
||||
<!-- 课程信息 -->
|
||||
<van-cell-group class="course-info">
|
||||
<div class="course-title">{{ course.title || '未命名课程' }}</div>
|
||||
<div class="course-desc">{{ course.description || '暂无描述' }}</div>
|
||||
<div class="course-meta">
|
||||
<van-tag type="primary" size="mini">{{ course.category || '未分类' }}</van-tag>
|
||||
<span class="course-date">{{ formatDate(course.createdAt) }}</span>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 学习进度 -->
|
||||
<van-cell-group title="学习进度" class="progress-section">
|
||||
<van-cell>
|
||||
<template #default>
|
||||
<div class="progress-container">
|
||||
<van-progress :percentage="progressPercentage" stroke-width="6" color="#1989fa" />
|
||||
<span class="progress-text">{{ progressPercentage }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 课程章节 -->
|
||||
<van-cell-group title="课程章节" class="chapters-section">
|
||||
<van-collapse v-model="activeChapters">
|
||||
<van-collapse-item
|
||||
v-for="(chapter, index) in course.chapters"
|
||||
:key="chapter.id"
|
||||
:name="index"
|
||||
:title="chapter.title || `第${index + 1}章`"
|
||||
>
|
||||
<van-cell
|
||||
v-for="video in chapter.videos"
|
||||
:key="video.id"
|
||||
:title="video.title || '未命名视频'"
|
||||
:label="formatDuration(video.duration)"
|
||||
is-link
|
||||
@click="playVideo(video.id, video.title)"
|
||||
>
|
||||
<template #icon>
|
||||
<van-icon
|
||||
:name="isVideoCompleted(video.id) ? 'success' : 'play-circle-o'"
|
||||
:color="isVideoCompleted(video.id) ? '#07c160' : '#1989fa'"
|
||||
/>
|
||||
</template>
|
||||
<template #right-icon v-if="isVideoCompleted(video.id)">
|
||||
<van-tag type="success" size="mini">已完成</van-tag>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-empty v-if="!chapter.videos || !chapter.videos.length" description="暂无视频" />
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
<van-empty v-if="!course.chapters || !course.chapters.length" description="暂无章节内容" />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<van-empty v-else description="课程不存在" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: linear-gradient(45deg, #f0f2f5, #e9ecef);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.course-cover .van-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.course-info .van-cell-group {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.course-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.course-date {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-container .van-progress {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.chapters-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
course: null,
|
||||
loading: true,
|
||||
courseId: null,
|
||||
activeChapters: [],
|
||||
progressData: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
progressPercentage() {
|
||||
if (!this.progressData || !this.progressData.totalVideos) return 0;
|
||||
return Math.round(this.progressData.completedVideos / this.progressData.totalVideos * 100);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
|
||||
// 加载课程详情
|
||||
async loadCourseDetail() {
|
||||
if (!this.courseId) {
|
||||
this.$toast.fail('缺少课程ID参数');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { code, data, msg } = await $.get('/platform/h5/edu/course/detail/data', {
|
||||
courseId: this.courseId
|
||||
});
|
||||
|
||||
if (code === 0 && data) {
|
||||
this.course = data;
|
||||
this.loadCourseProgress();
|
||||
} else {
|
||||
this.$toast.fail(msg || '加载课程详情失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast.fail('网络错误');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
// 加载课程进度
|
||||
async loadCourseProgress() {
|
||||
try {
|
||||
const { code, data } = await $.get('/platform/h5/edu/course/progress', {
|
||||
courseId: this.courseId
|
||||
});
|
||||
|
||||
if (code === 0 && data) {
|
||||
this.progressData = data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载进度失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 检查视频是否已完成
|
||||
isVideoCompleted(videoId) {
|
||||
return this.progressData &&
|
||||
this.progressData.completedVideoIds &&
|
||||
this.progressData.completedVideoIds.includes(videoId);
|
||||
},
|
||||
|
||||
// 播放视频
|
||||
playVideo(videoId, videoTitle) {
|
||||
pjaxReplace(`/platform/h5/edu/video/play?videoId=${videoId}&courseId=${this.courseId}&title=${encodeURIComponent(videoTitle || '')}`);
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.getFullYear() + '-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getDate()).padStart(2, '0');
|
||||
},
|
||||
|
||||
// 格式化时长
|
||||
formatDuration(seconds) {
|
||||
if (!seconds) return '00:00';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return String(minutes).padStart(2, '0') + ':' + String(remainingSeconds).padStart(2, '0');
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 获取URL参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.courseId = urlParams.get('courseId');
|
||||
|
||||
this.loadCourseDetail();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,234 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.course-item {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 6px;
|
||||
margin: 12px 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.course-item:active {
|
||||
transform: translateY(2px) scale(0.98);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
margin: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.course-cover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.course-cover .van-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 14px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.course-cover .van-icon {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
.course-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 16px 16px 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.course-desc {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.course-date {
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="教育培训课程" placeholder fixed>
|
||||
<template #right>
|
||||
<van-icon name="clock-o" @click="goToHistory"></van-icon>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
<van-sticky offset-top="46px">
|
||||
<van-tabs v-model="pageForm.category" @change="doSearch">
|
||||
<van-tab name="" title="全部"></van-tab>
|
||||
<van-tab name="理论学习" title="理论学习"></van-tab>
|
||||
<van-tab name="技能培训" title="技能培训"></van-tab>
|
||||
<van-tab name="安全教育" title="安全教育"></van-tab>
|
||||
<van-tab name="职业发展" title="职业发展"></van-tab>
|
||||
</van-tabs>
|
||||
</van-sticky>
|
||||
|
||||
<van-pull-refresh v-model="tableRefreshing" @refresh="onRefresh">
|
||||
<van-list v-model="tableLoading" :finished="tableFinished" finished-text="没有更多了" @load="onLoad">
|
||||
<div class="course-item" v-for="(course, index) in tableData" :key="course.id"
|
||||
@click="viewCourse(course.id)">
|
||||
<div class="course-cover">
|
||||
<van-image v-if="course.cover" :src="course.cover" fit="cover" :alt="course.title">
|
||||
<template v-slot:error>
|
||||
<van-icon name="photo-fail" size="32" color="#ddd"></van-icon>
|
||||
</template>
|
||||
</van-image>
|
||||
<van-icon v-else name="play-circle-o" size="48" color="#ddd"></van-icon>
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<div class="course-title">{{ course.title || '未命名课程' }}</div>
|
||||
<div class="course-desc">{{ course.description || '暂无描述' }}</div>
|
||||
<div class="course-meta">
|
||||
<van-tag type="primary" size="mini">{{ course.category || '未分类' }}</van-tag>
|
||||
<span class="course-date">{{ formatDate(course.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<van-empty v-if="!tableLoading && !tableData.length" description="暂无课程"></van-empty>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
dicts:['TRAIN_EDU_COURSE_TYPE'],
|
||||
data() {
|
||||
return {
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
category: ""
|
||||
},
|
||||
tableData: [],
|
||||
tableLoading: false,
|
||||
tableFinished: false,
|
||||
tableRefreshing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
|
||||
// 刷新
|
||||
onRefresh() {
|
||||
this.tableFinished = false;
|
||||
this.tableLoading = true
|
||||
this.pageForm.pageNumber = 1;
|
||||
this.onLoad();
|
||||
},
|
||||
|
||||
// 加载更多
|
||||
async onLoad() {
|
||||
if(this.tableRefreshing){
|
||||
this.tableData = []
|
||||
this.tableRefreshing = false
|
||||
}
|
||||
|
||||
this.$axios.post('/platform/h5/edu/courses/list', this.pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = this.tableData.concat(res.data.list)
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
if (this.tableData.length >= this.pageForm.totalCount) {
|
||||
this.tableFinished = true
|
||||
}
|
||||
this.pageForm.pageNumber++;
|
||||
}
|
||||
}).finally(() => {
|
||||
this.tableLoading = false
|
||||
this.tableRefreshing = false
|
||||
})
|
||||
},
|
||||
|
||||
// 搜索
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1;
|
||||
this.tableData = [];
|
||||
this.tableFinished = false;
|
||||
this.tableLoading = true;
|
||||
this.onLoad();
|
||||
},
|
||||
|
||||
// 查看课程详情
|
||||
viewCourse(courseId) {
|
||||
pjaxReplace(`/platform/h5/edu/course/detail?courseId=` + courseId);
|
||||
},
|
||||
|
||||
// 前往学习历史
|
||||
goToHistory() {
|
||||
pjaxReplace('/platform/h5/edu/history');
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.getFullYear() + '-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getDate()).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,385 @@
|
||||
<%
|
||||
layout('/platform/zhghh5/layout/layout.html',{
|
||||
title: '学习历史',
|
||||
keywords: '学习历史,视频学习,在线教育',
|
||||
description: '查看学习历史记录和进度统计'
|
||||
}){
|
||||
%>
|
||||
|
||||
<style>
|
||||
.history-container {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.stats-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 15px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.stats-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1989fa;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.history-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
margin: 15px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.history-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.course-info {
|
||||
flex: 1;
|
||||
}
|
||||
.course-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.course-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.course-category {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.progress-info {
|
||||
text-align: right;
|
||||
min-width: 80px;
|
||||
}
|
||||
.progress-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #28a745;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.progress-bar {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
background: #e9ecef;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-left: auto;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #28a745, #20c997);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.video-list {
|
||||
padding: 0 20px 15px;
|
||||
}
|
||||
.video-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
cursor: pointer;
|
||||
}
|
||||
.video-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.video-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, #1989fa, #1976d2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.video-icon.completed {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
}
|
||||
.video-info {
|
||||
flex: 1;
|
||||
}
|
||||
.video-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.video-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.video-duration {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.video-progress {
|
||||
color: #28a745;
|
||||
}
|
||||
.watch-time {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="history-container">
|
||||
<van-nav-bar
|
||||
title="学习历史"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
|
||||
<van-tabs v-model="activeTab" @change="onTabChange">
|
||||
<van-tab title="全部" name="all"></van-tab>
|
||||
<van-tab title="已完成" name="completed"></van-tab>
|
||||
<van-tab title="学习中" name="in_progress"></van-tab>
|
||||
<van-tab title="最近观看" name="recent"></van-tab>
|
||||
</van-tabs>
|
||||
|
||||
<!-- 学习统计 -->
|
||||
<div v-if="showStats && stats" class="stats-card">
|
||||
<div class="stats-title">
|
||||
<van-icon name="chart-trending-o" style="margin-right: 8px; color: #1989fa;"/>
|
||||
学习统计
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ stats.totalCourses || 0 }}</div>
|
||||
<div class="stat-label">学习课程</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ stats.completedVideos || 0 }}</div>
|
||||
<div class="stat-label">完成视频</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ Math.round((stats.totalWatchTime || 0) / 60) }}</div>
|
||||
<div class="stat-label">学习时长(分)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 学习历史列表 -->
|
||||
<van-loading v-if="loading" type="spinner" color="#1989fa" style="margin: 40px auto; display: block;">
|
||||
加载中...
|
||||
</van-loading>
|
||||
|
||||
<van-empty v-else-if="historyList.length === 0" description="暂无学习记录">
|
||||
<van-button round type="primary" @click="loadData">刷新</van-button>
|
||||
</van-empty>
|
||||
|
||||
<div v-else>
|
||||
<div v-for="course in historyList" :key="course.courseId" class="history-item">
|
||||
<div class="history-header" @click="viewCourse(course.courseId)">
|
||||
<div class="course-info">
|
||||
<div class="course-title">{{ course.courseTitle || '未命名课程' }}</div>
|
||||
<div class="course-meta">
|
||||
<span class="course-category">{{ course.courseCategory || '未分类' }}</span>
|
||||
<span>最后学习:{{ formatDate(course.lastWatchTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-info">
|
||||
<div class="progress-text">{{ getProgressPercent(course) }}%</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: getProgressPercent(course) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="course.videos && course.videos.length > 0" class="video-list">
|
||||
<div
|
||||
v-for="video in course.videos"
|
||||
:key="video.videoId"
|
||||
class="video-item"
|
||||
@click="playVideo(video.videoId, course.courseId, video.videoTitle)"
|
||||
>
|
||||
<div class="video-icon" :class="{ completed: video.isCompleted }">
|
||||
<van-icon :name="video.isCompleted ? 'success' : 'play-circle-o'" />
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<div class="video-title">{{ video.videoTitle || '未命名视频' }}</div>
|
||||
<div class="video-meta">
|
||||
<span class="video-duration">{{ formatDuration(video.duration) }}</span>
|
||||
<span class="video-progress">
|
||||
{{ getVideoProgress(video) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="watch-time">
|
||||
{{ formatDate(video.lastWatchTime) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'all',
|
||||
loading: false,
|
||||
showStats: false,
|
||||
stats: null,
|
||||
historyList: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadData();
|
||||
},
|
||||
methods: {
|
||||
onClickLeft() {
|
||||
window.location.href = '${base}/platform/h5/edu/courses';
|
||||
},
|
||||
|
||||
onTabChange(name) {
|
||||
this.activeTab = name;
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 如果是全部标签,先加载统计信息
|
||||
if (this.activeTab === 'all') {
|
||||
await this.loadStats();
|
||||
this.showStats = true;
|
||||
} else {
|
||||
this.showStats = false;
|
||||
}
|
||||
|
||||
// 加载学习历史
|
||||
await this.loadHistory();
|
||||
} catch (error) {
|
||||
this.$toast('加载失败,请重试');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await this.$http.get('${base}/platform/h5/edu/study/stats');
|
||||
if (response.data.code === 0) {
|
||||
this.stats = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadHistory() {
|
||||
try {
|
||||
const response = await this.$http.get('${base}/platform/h5/edu/study/history', {
|
||||
params: { filter: this.activeTab }
|
||||
});
|
||||
if (response.data.code === 0) {
|
||||
this.historyList = response.data.data || [];
|
||||
} else {
|
||||
this.$toast(response.data.msg || '加载失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast('网络错误,请检查网络连接');
|
||||
}
|
||||
},
|
||||
|
||||
viewCourse(courseId) {
|
||||
window.location.href = `${base}/platform/h5/edu/course/detail?courseId=${courseId}`;
|
||||
},
|
||||
|
||||
playVideo(videoId, courseId, videoTitle) {
|
||||
window.location.href = `${base}/platform/h5/edu/video/play?videoId=${videoId}&courseId=${courseId}&title=${encodeURIComponent(videoTitle)}`;
|
||||
},
|
||||
|
||||
getProgressPercent(course) {
|
||||
if (!course.videos || course.videos.length === 0) return 0;
|
||||
const completedCount = course.videos.filter(v => v.isCompleted).length;
|
||||
return Math.round((completedCount / course.videos.length) * 100);
|
||||
},
|
||||
|
||||
getVideoProgress(video) {
|
||||
if (video.isCompleted) return '已完成';
|
||||
if (video.duration > 0 && video.watchDuration > 0) {
|
||||
const percent = Math.round((video.watchDuration / video.duration) * 100);
|
||||
return `观看${percent}%`;
|
||||
}
|
||||
return '未开始';
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '未知';
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffTime = now - date;
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
|
||||
if (diffHours === 0) {
|
||||
const diffMinutes = Math.floor(diffTime / (1000 * 60));
|
||||
return diffMinutes <= 0 ? '刚刚' : `${diffMinutes}分钟前`;
|
||||
}
|
||||
return `${diffHours}小时前`;
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天';
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}天前`;
|
||||
} else {
|
||||
return date.getFullYear() + '-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getDate()).padStart(2, '0');
|
||||
}
|
||||
},
|
||||
|
||||
formatDuration(seconds) {
|
||||
if (!seconds) return '00:00';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return String(minutes).padStart(2, '0') + ':' + String(remainingSeconds).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<%}%>
|
||||
@@ -0,0 +1,262 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="videoTitle" placeholder fixed />
|
||||
|
||||
<div class="video-container">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<van-loading type="spinner" size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<div v-else-if="videoInfo" class="video-player">
|
||||
<video
|
||||
ref="videoElement"
|
||||
class="video-element"
|
||||
:src="videoInfo.url"
|
||||
controls
|
||||
preload="metadata"
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@play="onVideoPlay"
|
||||
@pause="onVideoPause"
|
||||
@timeupdate="onTimeUpdate"
|
||||
@ended="onVideoEnded"
|
||||
@error="onVideoError"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<van-empty v-else description="视频加载失败" />
|
||||
</div>
|
||||
|
||||
<!-- 进度保存提示 -->
|
||||
<van-toast v-model="showProgressToast" message="进度已保存" :duration="1000" />
|
||||
|
||||
<!-- 完成学习弹窗 -->
|
||||
<van-dialog
|
||||
v-model="showCompletionDialog"
|
||||
title="恭喜完成学习!"
|
||||
message="您已完成本视频的学习,学习记录已保存。"
|
||||
show-cancel-button
|
||||
cancel-button-text="继续观看"
|
||||
confirm-button-text="返回课程"
|
||||
@confirm="backToCourse"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.video-container {
|
||||
background: #000;
|
||||
min-height: calc(100vh - 46px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: calc(100vh - 46px);
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
videoInfo: null,
|
||||
loading: true,
|
||||
videoId: null,
|
||||
courseId: null,
|
||||
videoTitle: '视频播放',
|
||||
isPlaying: false,
|
||||
progressSaveTimer: null,
|
||||
showProgressToast: false,
|
||||
showCompletionDialog: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
|
||||
// 加载视频信息
|
||||
async loadVideoInfo() {
|
||||
if (!this.videoId) {
|
||||
this.$toast.fail('缺少视频ID参数');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { code, data, msg } = await $.get('${base}/platform/h5/edu/video/detail', {
|
||||
videoId: this.videoId
|
||||
});
|
||||
|
||||
if (code === 0 && data) {
|
||||
this.videoInfo = data;
|
||||
this.$nextTick(() => {
|
||||
this.loadWatchProgress();
|
||||
});
|
||||
} else {
|
||||
this.$toast.fail(msg || '加载视频信息失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast.fail('网络错误');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
// 视频加载完成
|
||||
onVideoLoaded() {
|
||||
console.log('视频加载完成');
|
||||
},
|
||||
|
||||
// 视频开始播放
|
||||
onVideoPlay() {
|
||||
this.isPlaying = true;
|
||||
this.startProgressSave();
|
||||
},
|
||||
|
||||
// 视频暂停
|
||||
onVideoPause() {
|
||||
this.isPlaying = false;
|
||||
this.saveProgress();
|
||||
},
|
||||
|
||||
// 时间更新
|
||||
onTimeUpdate() {
|
||||
// 可以在这里添加进度更新逻辑
|
||||
},
|
||||
|
||||
// 视频播放结束
|
||||
onVideoEnded() {
|
||||
this.isPlaying = false;
|
||||
this.markVideoCompleted();
|
||||
},
|
||||
|
||||
// 视频加载错误
|
||||
onVideoError() {
|
||||
this.$toast.fail('视频加载失败');
|
||||
},
|
||||
|
||||
// 加载观看进度
|
||||
async loadWatchProgress() {
|
||||
if (!this.videoId || !this.courseId) return;
|
||||
|
||||
try {
|
||||
const { code, data } = await $.get('${base}/platform/h5/edu/study/record', {
|
||||
videoId: this.videoId
|
||||
});
|
||||
|
||||
if (code === 0 && data && data.watchDuration > 0) {
|
||||
const video = this.$refs.videoElement;
|
||||
if (video) {
|
||||
video.currentTime = data.watchDuration;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载观看进度失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 保存观看进度
|
||||
async saveProgress() {
|
||||
const video = this.$refs.videoElement;
|
||||
if (!video || !this.videoId || !this.courseId) return;
|
||||
|
||||
try {
|
||||
await $.post('${base}/platform/h5/edu/study/progress/save', {
|
||||
videoId: this.videoId,
|
||||
courseId: this.courseId,
|
||||
watchDuration: Math.floor(video.currentTime)
|
||||
});
|
||||
|
||||
this.showProgressToast = true;
|
||||
} catch (error) {
|
||||
console.error('保存进度失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 开始定时保存进度
|
||||
startProgressSave() {
|
||||
this.clearProgressSave();
|
||||
this.progressSaveTimer = setInterval(() => {
|
||||
this.saveProgress();
|
||||
}, 10000); // 每10秒保存一次
|
||||
},
|
||||
|
||||
// 清除定时保存
|
||||
clearProgressSave() {
|
||||
if (this.progressSaveTimer) {
|
||||
clearInterval(this.progressSaveTimer);
|
||||
this.progressSaveTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 标记视频完成
|
||||
async markVideoCompleted() {
|
||||
if (!this.videoId || !this.courseId) return;
|
||||
|
||||
try {
|
||||
const { code } = await $.post('${base}/platform/h5/edu/study/complete', {
|
||||
videoId: this.videoId,
|
||||
courseId: this.courseId
|
||||
});
|
||||
|
||||
if (code === 0) {
|
||||
this.showCompletionDialog = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('标记完成失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 返回课程
|
||||
backToCourse() {
|
||||
if (this.courseId) {
|
||||
this.$router.push(`/course/detail?courseId=${this.courseId}`);
|
||||
} else {
|
||||
this.historyBack();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 获取URL参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.videoId = urlParams.get('videoId');
|
||||
this.courseId = urlParams.get('courseId');
|
||||
this.videoTitle = decodeURIComponent(urlParams.get('title') || '视频播放');
|
||||
|
||||
this.loadVideoInfo();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// 页面销毁前保存进度
|
||||
this.saveProgress();
|
||||
this.clearProgressSave();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user