This commit is contained in:
那些花儿
2025-09-10 08:46:52 +08:00
parent 5a55444209
commit ab6323a7de
31 changed files with 1884 additions and 704 deletions
@@ -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();
}
}
@@ -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;
}
@@ -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; }
}
}
@@ -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);
}
}
}
@@ -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() {
@@ -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() {
@@ -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() {
@@ -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() {
@@ -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() {
@@ -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() {
@@ -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(){
@@ -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() {
@@ -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() {
@@ -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(){
@@ -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);
}
}
@@ -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>
<!--#
}
#-->