1
This commit is contained in:
@@ -6,7 +6,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.sys.models.Sys_home_activity;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
|
||||
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
|
||||
import io.swagger.annotations.Api;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
@@ -32,6 +32,8 @@ public class SysH5IndexController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private ActivityBasicScopeService activityBasicScopeService;
|
||||
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/sys/home/index.html")
|
||||
@@ -40,6 +42,26 @@ public class SysH5IndexController {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the mobile featured activity aggregation page.
|
||||
*/
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/sys/home/featuredActivity.html")
|
||||
@SaCheckLogin
|
||||
public void featuredActivity() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the mobile festival benefit aggregation page.
|
||||
*/
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/sys/home/festivalBenefit.html")
|
||||
@SaCheckLogin
|
||||
public void festivalBenefit() {
|
||||
|
||||
}
|
||||
|
||||
@At
|
||||
@Ok("beetl:/platform/zhghh5/index/mine/index.html")
|
||||
@SaCheckLogin
|
||||
@@ -88,8 +110,8 @@ public class SysH5IndexController {
|
||||
}
|
||||
|
||||
if (allowUserGroupId != null) {
|
||||
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", allowUserGroupId).and("userId", "=", userId));
|
||||
if (count > 0) {
|
||||
// Delegate group membership checks to the service so SQL based groups and stored groups stay consistent.
|
||||
if (activityBasicScopeService.isUserInGroup(allowUserGroupId, userId)) {
|
||||
allowActivityList.add(activity);
|
||||
continue;
|
||||
}
|
||||
@@ -97,7 +119,10 @@ public class SysH5IndexController {
|
||||
|
||||
if (StrUtil.isNotBlank(allowUserSql)){
|
||||
Sql sql = Sqls.create(allowUserSql).setParam("userId", userId);
|
||||
NutMap nutMap = (NutMap) Daos.query(dao, sql.toString(), Sqls.callback.map());
|
||||
// Execute the configured visibility SQL with Nutz parameters to avoid rebuilding raw SQL text in controller logic.
|
||||
sql.setCallback(Sqls.callback.map());
|
||||
dao.execute(sql);
|
||||
NutMap nutMap = (NutMap) sql.getResult();
|
||||
if (Lang.isNotEmpty(nutMap) && StrUtil.isNotBlank(nutMap.getString("userId"))) {
|
||||
allowActivityList.add(activity);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.sys.models.Sys_home_activity;
|
||||
import com.budwk.app.sys.models.Sys_home_template;
|
||||
import com.budwk.app.sys.models.Sys_menu;
|
||||
import com.budwk.app.sys.services.SysMenuService;
|
||||
import com.budwk.app.sys.services.SysUserService;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
@@ -39,6 +42,7 @@ import org.nutz.mvc.annotation.Param;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -255,6 +259,26 @@ public class SysHomeController {
|
||||
// return Result.success(hasPermisiionList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 PC 首页工作模板。
|
||||
* 仅系统管理员和校工会管理员可见,返回前隐藏模板 SQL 和后端类路径等敏感字段。
|
||||
*
|
||||
* @return 首页工作模板列表
|
||||
*/
|
||||
@At
|
||||
@SaCheckLogin
|
||||
@ApiOperation("首页工作模板")
|
||||
@Ok("json")
|
||||
public Result listHomeTemplate() {
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
||||
return Result.success(Collections.emptyList());
|
||||
}
|
||||
FieldFilter fieldFilter = FieldFilter.locked(Sys_home_template.class, "allowUserSql|classPath");
|
||||
List<Sys_home_template> list = Daos.ext(dao, fieldFilter).query(Sys_home_template.class,
|
||||
Cnd.where("enable", "=", 1).asc("sortNo"));
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
|
||||
@At
|
||||
@SaCheckLogin
|
||||
|
||||
@@ -338,8 +338,12 @@ public class SysUserController {
|
||||
@At
|
||||
@Ok("json")
|
||||
@SaCheckLogin
|
||||
public Result subAppMenus(@Param("appId") String appId) {
|
||||
public Result subAppMenus(@Param("appId") String appId, @Param("platform") String platform) {
|
||||
List<Sys_menu> menus = sysUserService.getMenus(SecurityUtil.getUserId());
|
||||
if (StrUtil.isNotBlank(platform)) {
|
||||
// Keep sub-application menus aligned with the requested client platform.
|
||||
menus = menus.stream().filter(menu -> platform.equals(menu.getPlatform())).toList();
|
||||
}
|
||||
List<Sys_menu> list = SysMenuUtil.createTreeMenus(menus, appId);
|
||||
if (ObjectUtil.isEmpty(list)) {
|
||||
List<Sys_menu> self = menus.stream().filter(menu -> menu.getId().equals(appId)).toList();
|
||||
|
||||
@@ -75,6 +75,7 @@ public class SysV4AppsController {
|
||||
m.href,
|
||||
m.icon,
|
||||
m.picIcon,
|
||||
m.location,
|
||||
sm.`name` AS moduleName,
|
||||
CASE
|
||||
WHEN f.userId IS NOT NULL THEN
|
||||
@@ -97,6 +98,7 @@ public class SysV4AppsController {
|
||||
cnd.and("m.id","in", menus.stream().map(Sys_menu::getId).toArray());
|
||||
cnd.andEX("m.moduleId", "=", categoryId);
|
||||
cnd.asc("m.location");
|
||||
cnd.asc("m.id");
|
||||
if (StrUtil.isNotBlank(keyword)) {
|
||||
cnd.where().andLike("m.name", keyword);
|
||||
}
|
||||
@@ -143,7 +145,7 @@ public class SysV4AppsController {
|
||||
@Ok("json:full")
|
||||
@SaCheckLogin
|
||||
@ApiOperation("获取收藏的应用")
|
||||
public Result favorite(HttpServletRequest req) {
|
||||
public Result favorite(@Param(value = "platform", df = "PC") String platform, HttpServletRequest req) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
m.id,
|
||||
@@ -151,6 +153,7 @@ public class SysV4AppsController {
|
||||
m.href,
|
||||
m.icon,
|
||||
m.picIcon,
|
||||
m.location,
|
||||
sm.`name` AS moduleName,
|
||||
1 AS isFavorite
|
||||
FROM
|
||||
@@ -162,12 +165,15 @@ public class SysV4AppsController {
|
||||
LEFT JOIN sys_module sm ON sm.id = m.moduleId
|
||||
WHERE
|
||||
f.userId = @userId
|
||||
AND m.platform = 'PC'
|
||||
AND m.platform = @platform
|
||||
AND m.disabled = 0
|
||||
ORDER BY
|
||||
m.location ASC;
|
||||
m.location ASC,
|
||||
m.id ASC;
|
||||
""");
|
||||
sql.setParam("userId", SecurityUtil.getUserId());
|
||||
// The H5 home sends platform=H5; defaulting to PC keeps the existing desktop caller compatible.
|
||||
sql.setParam("platform", platform);
|
||||
List<NutMap> list = sysMenuService.listMap(sql);
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
+43
@@ -55,6 +55,49 @@ public class HonorManageController {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the mobile honor display page used by the H5 home feature card.
|
||||
*/
|
||||
@At("/h5")
|
||||
@SaCheckLogin
|
||||
@Ok("beetl:/platform/zhghh5/dayofficework/honor/manage/index.html")
|
||||
public void h5() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Query honor records for the mobile honor display page.
|
||||
*/
|
||||
@At
|
||||
@SaCheckLogin
|
||||
public Result h5Data(HonorPageForm pageForm) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
h.id,
|
||||
h.userName,
|
||||
h.applyUnionName,
|
||||
h.unionName,
|
||||
h.grantDate,
|
||||
h.files AS photoFiles,
|
||||
prize.`name` AS prizeName,
|
||||
type.`name` AS typeName,
|
||||
type.queryTypeCode AS typeQueryTypeCode
|
||||
FROM
|
||||
`honor` h
|
||||
LEFT JOIN honor_basic_settings prize ON prize.id = h.prize
|
||||
LEFT JOIN honor_basic_settings type ON type.id = h.honorType
|
||||
$condition
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
Integer year = pageForm.getYear() == null ? Calendar.getInstance().get(Calendar.YEAR) : pageForm.getYear();
|
||||
cnd.andEX("YEAR(h.grantDate)", "=", year);
|
||||
cnd.andEX("h.honorType", "=", pageForm.getHonorType());
|
||||
cnd.desc("h.grantDate");
|
||||
sql.setCondition(cnd);
|
||||
Pagination pagination = honorViService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||
return Result.success(pagination);
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("honor.manage")
|
||||
public Result pageData(HonorPageForm pageForm) {
|
||||
|
||||
+24
-1
@@ -642,8 +642,10 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl<TourUserAssig
|
||||
throw new IllegalArgumentException("该人员已取消退出");
|
||||
}
|
||||
checkAssignmentCancelDeadline(assignment);
|
||||
// 取消退出时删除对应事项台账,再标记人员分配表状态,防止后续移动端再次报名。
|
||||
// 取消退出时先删除对应事项台账,并仅清空当前人员分配记录的线路快照,避免误处理同配置下其它分配记录。
|
||||
deleteAssignmentLedgers(assignment);
|
||||
clearAssignmentMatterSnapshot(assignment.getId());
|
||||
// 标记人员分配表取消状态,防止后续移动端再次报名。
|
||||
update(Chain.make("cancelled", true),
|
||||
Cnd.where(TourUserAssignment::getId, "=", assignment.getId()));
|
||||
}
|
||||
@@ -829,6 +831,27 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl<TourUserAssig
|
||||
dao().clear(TourLedger.class, Cnd.where(TourLedger::getId, "in", ledgerIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前人员分配记录中的线路相关快照,不删除人员分配表数据。
|
||||
* 出行时间由 matterId 关联事项表展示,清空 matterId 后列表不再展示原出行时间。
|
||||
*
|
||||
* @param assignmentId 人员分配记录ID
|
||||
*/
|
||||
private void clearAssignmentMatterSnapshot(String assignmentId) {
|
||||
if (StrUtil.isBlank(assignmentId)) {
|
||||
return;
|
||||
}
|
||||
update(Chain.make("matterId", null)
|
||||
.add("matterName", null)
|
||||
.add("lineId", null)
|
||||
.add("lineName", null)
|
||||
.add("travelAgencyId", null)
|
||||
.add("travelAgencyName", null)
|
||||
.add("boardingPlace", null),
|
||||
Cnd.where(TourUserAssignment::getId, "=", assignmentId)
|
||||
.and(TourUserAssignment::getDelFlag, "=", false));
|
||||
}
|
||||
|
||||
private void restoreCancelledAssignment(TourUserAssignment assignment) {
|
||||
if (!Boolean.TRUE.equals(assignment.getCancelled())) {
|
||||
throw new IllegalArgumentException("该人员未取消退出,无需恢复");
|
||||
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
package com.budwk.app.zhgh.learning.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.learning.models.LearningCourse;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourseOutline;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourseType;
|
||||
import com.budwk.app.zhgh.learning.models.LearningOutlineResource;
|
||||
import com.budwk.app.zhgh.learning.models.LearningStudyRule;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.pager.Pager;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("学习教育平台-章节内容管理")
|
||||
@At("/platform/learning/chapter/content")
|
||||
public class LearningChapterContentController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/Learning/chapterContent/index.html")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("课程列表")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
public Result courses(String courseId, String courseName, String courseTypeId, String lecturerName, String status) {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("id", "=", courseId);
|
||||
cnd.and(Cnd.likeEX("courseName", courseName));
|
||||
cnd.andEX("courseTypeId", "=", courseTypeId);
|
||||
cnd.and(Cnd.likeEX("lecturerName", lecturerName));
|
||||
cnd.andEX("status", "=", status);
|
||||
cnd.desc("createdAt");
|
||||
return Result.success(dao.query(LearningCourse.class, cnd, dao.createPager(1, 50)));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("课程类型")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
public Result courseTypes() {
|
||||
return Result.success(dao.query(LearningCourseType.class, Cnd.where("enabled", "=", true).asc("sortNum").asc("createdAt")));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("课程大纲树")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
public Result tree(String courseId) {
|
||||
if (StrUtil.isBlank(courseId)) {
|
||||
return Result.success(List.of());
|
||||
}
|
||||
List<LearningCourseOutline> outlines = dao.query(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).asc("sortOrder").asc("createdAt"));
|
||||
Map<String, List<NutMap>> childrenMap = new HashMap<>();
|
||||
List<NutMap> roots = new ArrayList<>();
|
||||
for (LearningCourseOutline outline : outlines) {
|
||||
NutMap map = NutMap.NEW()
|
||||
.addv("id", outline.getId())
|
||||
.addv("courseId", outline.getCourseId())
|
||||
.addv("courseName", outline.getCourseName())
|
||||
.addv("parentId", outline.getParentId())
|
||||
.addv("nodeType", outline.getNodeType())
|
||||
.addv("title", outline.getTitle())
|
||||
.addv("subtitle", outline.getSubtitle())
|
||||
.addv("description", outline.getDescription())
|
||||
.addv("sortOrder", outline.getSortOrder())
|
||||
.addv("required", outline.getRequired())
|
||||
.addv("status", outline.getStatus())
|
||||
.addv("children", new ArrayList<>());
|
||||
String parentId = StrUtil.blankToDefault(outline.getParentId(), "");
|
||||
childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(map);
|
||||
}
|
||||
roots.addAll(childrenMap.getOrDefault("", new ArrayList<>()));
|
||||
appendChildren(roots, childrenMap);
|
||||
return Result.success(roots);
|
||||
}
|
||||
|
||||
private void appendChildren(List<NutMap> nodes, Map<String, List<NutMap>> childrenMap) {
|
||||
for (NutMap node : nodes) {
|
||||
List<NutMap> children = childrenMap.getOrDefault(node.getString("id"), new ArrayList<>());
|
||||
node.put("children", children);
|
||||
appendChildren(children, childrenMap);
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("新增/编辑大纲节点")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
@SLog(tag = "保存大纲节点", msg = "节点标题:${args[0].title}")
|
||||
public Result saveNode(LearningCourseOutline outline) {
|
||||
Result checkResult = checkNode(outline);
|
||||
if (checkResult != null) {
|
||||
return checkResult;
|
||||
}
|
||||
LearningCourse course = dao.fetch(LearningCourse.class, outline.getCourseId());
|
||||
outline.setCourseName(course == null ? outline.getCourseName() : course.getCourseName());
|
||||
if ("chapter".equals(outline.getNodeType())) {
|
||||
outline.setParentId("");
|
||||
}
|
||||
if (outline.getRequired() == null) {
|
||||
outline.setRequired(false);
|
||||
}
|
||||
if (StrUtil.isBlank(outline.getStatus())) {
|
||||
outline.setStatus("enabled");
|
||||
}
|
||||
if (outline.getSortOrder() == null) {
|
||||
outline.setSortOrder(nextNodeSort(outline.getCourseId(), outline.getParentId()));
|
||||
}
|
||||
if (StrUtil.isBlank(outline.getId())) {
|
||||
dao.insert(outline);
|
||||
} else {
|
||||
dao.updateIgnoreNull(outline);
|
||||
}
|
||||
ensureRule(outline.getCourseId(), "outline", outline.getId(), outline.getRequired(), "enabled");
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("删除大纲节点")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
@SLog(tag = "删除大纲节点", msg = "节点ID:${args[0]}")
|
||||
public Result deleteNode(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
return Result.error("请选择要删除的数据");
|
||||
}
|
||||
int childCount = dao.count(LearningCourseOutline.class, Cnd.where("parentId", "=", id));
|
||||
int resourceCount = dao.count(LearningOutlineResource.class, Cnd.where("outlineId", "=", id));
|
||||
if (childCount > 0 || resourceCount > 0) {
|
||||
return Result.error("该节点下存在子节点或学习资料,请先删除后再操作");
|
||||
}
|
||||
dao.clear(LearningStudyRule.class, Cnd.where("targetId", "=", id).and("targetType", "=", "outline"));
|
||||
dao.clear(LearningCourseOutline.class, Cnd.where("id", "=", id));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("启用/禁用大纲节点")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
public Result toggleNode(String id, String status) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
return Result.error("请选择要操作的数据");
|
||||
}
|
||||
dao.update(LearningCourseOutline.class, Chain.make("status", status), Cnd.where("id", "=", id));
|
||||
dao.update(LearningStudyRule.class, Chain.make("status", status), Cnd.where("targetId", "=", id).and("targetType", "=", "outline"));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("移动大纲节点")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
public Result moveNode(String id, String direction) {
|
||||
LearningCourseOutline current = dao.fetch(LearningCourseOutline.class, id);
|
||||
if (current == null) {
|
||||
return Result.error("节点不存在");
|
||||
}
|
||||
Cnd cnd = Cnd.where("courseId", "=", current.getCourseId()).and("parentId", "=", StrUtil.blankToDefault(current.getParentId(), ""));
|
||||
if ("up".equals(direction)) {
|
||||
cnd.and("sortOrder", "<", current.getSortOrder()).desc("sortOrder");
|
||||
} else {
|
||||
cnd.and("sortOrder", ">", current.getSortOrder()).asc("sortOrder");
|
||||
}
|
||||
LearningCourseOutline target = dao.fetch(LearningCourseOutline.class, cnd);
|
||||
if (target == null) {
|
||||
return Result.success();
|
||||
}
|
||||
Integer currentSort = current.getSortOrder();
|
||||
dao.update(LearningCourseOutline.class, Chain.make("sortOrder", target.getSortOrder()), Cnd.where("id", "=", current.getId()));
|
||||
dao.update(LearningCourseOutline.class, Chain.make("sortOrder", currentSort), Cnd.where("id", "=", target.getId()));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("学习资料分页")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
public Result resourcePage(PageForm pageForm, String outlineId) {
|
||||
if (StrUtil.isBlank(outlineId)) {
|
||||
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, List.of()));
|
||||
}
|
||||
Cnd cnd = Cnd.where("outlineId", "=", outlineId);
|
||||
String orderColumn = getResourceOrderColumn(pageForm.getPageOrderName());
|
||||
if (StrUtil.isBlank(orderColumn)) {
|
||||
cnd.asc("sortOrder").asc("createdAt");
|
||||
} else if ("descending".equals(pageForm.getPageOrderBy())) {
|
||||
cnd.desc(orderColumn);
|
||||
} else {
|
||||
cnd.asc(orderColumn);
|
||||
}
|
||||
int count = dao.count(LearningOutlineResource.class, Cnd.where("outlineId", "=", outlineId));
|
||||
Pager pager = dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize());
|
||||
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, dao.query(LearningOutlineResource.class, cnd, pager)));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("新增/编辑学习资料")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
@SLog(tag = "保存学习资料", msg = "资料标题:${args[0].resourceTitle}")
|
||||
public Result saveResource(LearningOutlineResource resource) {
|
||||
Result checkResult = checkResource(resource);
|
||||
if (checkResult != null) {
|
||||
return checkResult;
|
||||
}
|
||||
if (resource.getRequired() == null) {
|
||||
resource.setRequired(false);
|
||||
}
|
||||
if (resource.getAllowPreview() == null) {
|
||||
resource.setAllowPreview(true);
|
||||
}
|
||||
if (resource.getAllowDownload() == null) {
|
||||
resource.setAllowDownload(false);
|
||||
}
|
||||
if (StrUtil.isBlank(resource.getStatus())) {
|
||||
resource.setStatus("enabled");
|
||||
}
|
||||
if (resource.getSortOrder() == null) {
|
||||
resource.setSortOrder(nextResourceSort(resource.getOutlineId()));
|
||||
}
|
||||
if (StrUtil.isBlank(resource.getId())) {
|
||||
dao.insert(resource);
|
||||
} else {
|
||||
dao.updateIgnoreNull(resource);
|
||||
}
|
||||
ensureRule(resource.getCourseId(), "resource", resource.getId(), resource.getRequired(), resource.getStatus());
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("删除学习资料")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
@SLog(tag = "删除学习资料", msg = "资料ID:${args[0]}")
|
||||
public Result deleteResource(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
return Result.error("请选择要删除的数据");
|
||||
}
|
||||
dao.clear(LearningStudyRule.class, Cnd.where("targetId", "=", id).and("targetType", "=", "resource"));
|
||||
dao.clear(LearningOutlineResource.class, Cnd.where("id", "=", id));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("查询学习规则")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
public Result getRule(String courseId, String targetType, String targetId) {
|
||||
LearningStudyRule rule = dao.fetch(LearningStudyRule.class, Cnd.where("courseId", "=", courseId).and("targetType", "=", targetType).and("targetId", "=", targetId));
|
||||
if (rule == null) {
|
||||
rule = defaultRule(courseId, targetType, targetId, false, "enabled");
|
||||
}
|
||||
return Result.success(rule);
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("保存学习规则")
|
||||
@SaCheckPermission("learning.chapter.content")
|
||||
public Result saveRule(LearningStudyRule rule) {
|
||||
if (StrUtil.isBlank(rule.getCourseId()) || StrUtil.isBlank(rule.getTargetType()) || StrUtil.isBlank(rule.getTargetId())) {
|
||||
return Result.error("规则对象不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(rule.getStatus())) {
|
||||
rule.setStatus("enabled");
|
||||
}
|
||||
LearningStudyRule old = dao.fetch(LearningStudyRule.class, Cnd.where("courseId", "=", rule.getCourseId()).and("targetType", "=", rule.getTargetType()).and("targetId", "=", rule.getTargetId()));
|
||||
if (old == null) {
|
||||
dao.insert(rule);
|
||||
} else {
|
||||
rule.setId(old.getId());
|
||||
dao.updateIgnoreNull(rule);
|
||||
}
|
||||
if ("outline".equals(rule.getTargetType())) {
|
||||
dao.update(LearningCourseOutline.class, Chain.make("required", rule.getRequired()).add("status", rule.getStatus()), Cnd.where("id", "=", rule.getTargetId()));
|
||||
}
|
||||
if ("resource".equals(rule.getTargetType())) {
|
||||
dao.update(LearningOutlineResource.class, Chain.make("required", rule.getRequired()).add("status", rule.getStatus()), Cnd.where("id", "=", rule.getTargetId()));
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private Result checkNode(LearningCourseOutline outline) {
|
||||
if (outline == null || StrUtil.isBlank(outline.getCourseId())) {
|
||||
return Result.error("请选择课程");
|
||||
}
|
||||
if (StrUtil.isBlank(outline.getNodeType())) {
|
||||
return Result.error("请选择节点类型");
|
||||
}
|
||||
if (StrUtil.isBlank(outline.getTitle())) {
|
||||
return Result.error("标题不能为空");
|
||||
}
|
||||
if ("section".equals(outline.getNodeType()) && StrUtil.isBlank(outline.getParentId())) {
|
||||
return Result.error("新增节需要选择所属章");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Result checkResource(LearningOutlineResource resource) {
|
||||
if (resource == null || StrUtil.isBlank(resource.getCourseId()) || StrUtil.isBlank(resource.getOutlineId())) {
|
||||
return Result.error("请选择章/节节点");
|
||||
}
|
||||
if (StrUtil.isBlank(resource.getResourceTitle())) {
|
||||
return Result.error("资料标题不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(resource.getResourceType())) {
|
||||
return Result.error("请选择资料类型");
|
||||
}
|
||||
if (StrUtil.isBlank(resource.getFileData())) {
|
||||
return Result.error("请上传附件");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Integer nextNodeSort(String courseId, String parentId) {
|
||||
LearningCourseOutline outline = dao.fetch(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).and("parentId", "=", StrUtil.blankToDefault(parentId, "")).desc("sortOrder"));
|
||||
return outline == null || outline.getSortOrder() == null ? 1 : outline.getSortOrder() + 1;
|
||||
}
|
||||
|
||||
private Integer nextResourceSort(String outlineId) {
|
||||
LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, Cnd.where("outlineId", "=", outlineId).desc("sortOrder"));
|
||||
return resource == null || resource.getSortOrder() == null ? 1 : resource.getSortOrder() + 1;
|
||||
}
|
||||
|
||||
private void ensureRule(String courseId, String targetType, String targetId, Boolean required, String status) {
|
||||
LearningStudyRule rule = dao.fetch(LearningStudyRule.class, Cnd.where("courseId", "=", courseId).and("targetType", "=", targetType).and("targetId", "=", targetId));
|
||||
if (rule == null) {
|
||||
dao.insert(defaultRule(courseId, targetType, targetId, required, status));
|
||||
} else {
|
||||
dao.update(LearningStudyRule.class,
|
||||
Chain.make("required", required != null && required).add("status", StrUtil.blankToDefault(status, "enabled")),
|
||||
Cnd.where("id", "=", rule.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
private LearningStudyRule defaultRule(String courseId, String targetType, String targetId, Boolean required, String status) {
|
||||
LearningStudyRule rule = new LearningStudyRule();
|
||||
rule.setCourseId(courseId);
|
||||
rule.setTargetType(targetType);
|
||||
rule.setTargetId(targetId);
|
||||
rule.setRequired(required != null && required);
|
||||
rule.setStudyMode("mixed");
|
||||
rule.setCompletionRule("all_required_resource");
|
||||
rule.setCompletePercent(90);
|
||||
rule.setMinStudySeconds(0);
|
||||
rule.setUnlockRule("free");
|
||||
rule.setAllowSkip(false);
|
||||
rule.setAllowDrag(true);
|
||||
rule.setPauseCountTime(false);
|
||||
rule.setHiddenCountTime(false);
|
||||
rule.setInactiveCountTime(false);
|
||||
rule.setStatus(StrUtil.blankToDefault(status, "enabled"));
|
||||
return rule;
|
||||
}
|
||||
|
||||
private String getResourceOrderColumn(String prop) {
|
||||
Map<String, String> columns = new HashMap<>();
|
||||
columns.put("resourceTitle", "resourceTitle");
|
||||
columns.put("resourceType", "resourceType");
|
||||
columns.put("fileExt", "fileExt");
|
||||
columns.put("durationSeconds", "durationSeconds");
|
||||
columns.put("sortOrder", "sortOrder");
|
||||
columns.put("required", "required");
|
||||
columns.put("allowPreview", "allowPreview");
|
||||
columns.put("allowDownload", "allowDownload");
|
||||
columns.put("status", "status");
|
||||
return columns.get(prop);
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
package com.budwk.app.zhgh.learning.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
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.base.utils.OfficePlusUtil;
|
||||
import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
|
||||
import com.budwk.app.sys.models.Sys_file;
|
||||
import com.budwk.app.sys.models.Sys_dict;
|
||||
import com.budwk.app.sys.services.SysDictService;
|
||||
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourseOutline;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourseType;
|
||||
import com.budwk.app.zhgh.learning.models.LearningOutlineResource;
|
||||
import com.budwk.app.zhgh.learning.models.LearningStudyRecord;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
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.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("学习教育平台-课程展示")
|
||||
@At("/platform/learning/course/display")
|
||||
public class LearningCourseDisplayController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@Inject
|
||||
private SysDictService sysDictService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/index.html")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At("/detail")
|
||||
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/detail.html")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public void detail() {
|
||||
}
|
||||
|
||||
@At("/study")
|
||||
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/study.html")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public void study() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("课程展示列表")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public Result pageData(PageForm pageForm, String keyword, String courseTypeId, String recommendFlag) {
|
||||
Cnd cnd = Cnd.where("c.status", "=", "published");
|
||||
if (StrUtil.isNotBlank(keyword)) {
|
||||
cnd.and(Cnd.exps("c.courseName", "like", "%" + keyword + "%")
|
||||
.or("c.courseIntro", "like", "%" + keyword + "%"));
|
||||
}
|
||||
cnd.andEX("c.courseTypeId", "=", courseTypeId);
|
||||
if (StrUtil.isNotBlank(recommendFlag)) {
|
||||
cnd.and("c.recommendFlags", "like", "%" + recommendFlag + "%");
|
||||
}
|
||||
|
||||
Sql countSql = Sqls.create("""
|
||||
SELECT COUNT(1)
|
||||
FROM learning_course c
|
||||
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
|
||||
$condition
|
||||
""");
|
||||
countSql.setCondition(cnd);
|
||||
countSql.setCallback(Sqls.callback.integer());
|
||||
dao.execute(countSql);
|
||||
int count = countSql.getInt();
|
||||
|
||||
Sql listSql = Sqls.create("""
|
||||
SELECT c.id, c.courseName, c.courseIntro, c.startTime, c.endTime, c.openType, c.cover,
|
||||
c.recommendFlags, c.courseTypeId, c.lecturerName, c.sortNum, t.typeName AS courseTypeName
|
||||
FROM learning_course c
|
||||
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
|
||||
$condition
|
||||
ORDER BY c.sortNum ASC, c.createdAt DESC
|
||||
""");
|
||||
listSql.setCondition(cnd);
|
||||
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||
listSql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(listSql);
|
||||
List<NutMap> rows = listSql.getList(NutMap.class);
|
||||
rows.forEach(row -> {
|
||||
Object startTime = row.get("startTime");
|
||||
Object endTime = row.get("endTime");
|
||||
row.put("startTimeText", startTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) startTime)));
|
||||
row.put("endTimeText", endTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) endTime)));
|
||||
});
|
||||
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, rows));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("课程学习基础信息")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public Result courseInfo(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
return Result.error("请选择课程");
|
||||
}
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT c.id, c.courseName, c.courseIntro, c.startTime, c.endTime, c.openType, c.cover,
|
||||
c.recommendFlags, c.lecturerName, t.typeName AS courseTypeName
|
||||
FROM learning_course c
|
||||
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
|
||||
WHERE c.id = @id
|
||||
""");
|
||||
sql.params().set("id", id);
|
||||
sql.setCallback(Sqls.callback.map());
|
||||
dao.execute(sql);
|
||||
NutMap course = sql.getObject(NutMap.class);
|
||||
if (course == null) {
|
||||
return Result.error("课程不存在");
|
||||
}
|
||||
Object startTime = course.get("startTime");
|
||||
Object endTime = course.get("endTime");
|
||||
course.put("startTimeText", startTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) startTime)));
|
||||
course.put("endTimeText", endTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) endTime)));
|
||||
return Result.success(course);
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("课程学习安排")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public Result studyTree(String courseId) {
|
||||
if (StrUtil.isBlank(courseId)) {
|
||||
return Result.success(List.of());
|
||||
}
|
||||
List<LearningCourseOutline> outlines = dao.query(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).and("status", "=", "enabled").asc("sortOrder").asc("createdAt"));
|
||||
List<LearningOutlineResource> resources = dao.query(LearningOutlineResource.class, Cnd.where("courseId", "=", courseId).and("status", "=", "enabled").asc("sortOrder").asc("createdAt"));
|
||||
List<LearningStudyRecord> records = dao.query(LearningStudyRecord.class, Cnd.where("courseId", "=", courseId).and("userId", "=", SecurityUtil.getUserId()));
|
||||
Map<String, List<NutMap>> childrenMap = new HashMap<>();
|
||||
List<NutMap> roots = new ArrayList<>();
|
||||
Map<String, LearningStudyRecord> recordMap = new HashMap<>();
|
||||
for (LearningStudyRecord record : records) {
|
||||
recordMap.put(record.getOutlineId(), record);
|
||||
}
|
||||
|
||||
Map<String, NutMap> outlineNodeMap = new HashMap<>();
|
||||
for (LearningCourseOutline outline : outlines) {
|
||||
List<NutMap> children = new ArrayList<>();
|
||||
NutMap node = NutMap.NEW()
|
||||
.addv("id", outline.getId())
|
||||
.addv("type", "outline")
|
||||
.addv("nodeType", outline.getNodeType())
|
||||
.addv("title", outline.getTitle())
|
||||
.addv("required", outline.getRequired())
|
||||
.addv("progressPercent", recordMap.containsKey(outline.getId()) ? recordMap.get(outline.getId()).getProgressPercent() : 0)
|
||||
.addv("studySeconds", recordMap.containsKey(outline.getId()) ? recordMap.get(outline.getId()).getStudySeconds() : 0)
|
||||
.addv("lastPositionSeconds", recordMap.containsKey(outline.getId()) ? value(recordMap.get(outline.getId()).getLastPositionSeconds()) : 0)
|
||||
.addv("completeStatus", recordMap.containsKey(outline.getId()) ? recordMap.get(outline.getId()).getCompleteStatus() : "not_started")
|
||||
.addv("children", children);
|
||||
outlineNodeMap.put(outline.getId(), node);
|
||||
childrenMap.put(outline.getId(), children);
|
||||
}
|
||||
for (LearningCourseOutline outline : outlines) {
|
||||
NutMap node = outlineNodeMap.get(outline.getId());
|
||||
if (StrUtil.isBlank(outline.getParentId())) {
|
||||
roots.add(node);
|
||||
} else {
|
||||
childrenMap.computeIfAbsent(outline.getParentId(), key -> new ArrayList<>()).add(node);
|
||||
}
|
||||
}
|
||||
for (LearningOutlineResource resource : resources) {
|
||||
NutMap node = NutMap.NEW()
|
||||
.addv("id", resource.getId())
|
||||
.addv("type", "resource")
|
||||
.addv("outlineId", resource.getOutlineId())
|
||||
.addv("title", resource.getResourceTitle())
|
||||
.addv("resourceType", resource.getResourceType())
|
||||
.addv("fileExt", resource.getFileExt())
|
||||
.addv("fileData", resource.getFileData())
|
||||
.addv("durationSeconds", resource.getDurationSeconds())
|
||||
.addv("required", resource.getRequired())
|
||||
.addv("progressPercent", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getProgressPercent() : 0)
|
||||
.addv("studySeconds", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getStudySeconds() : 0)
|
||||
.addv("lastPositionSeconds", recordMap.containsKey(resource.getOutlineId()) ? value(recordMap.get(resource.getOutlineId()).getLastPositionSeconds()) : 0)
|
||||
.addv("completeStatus", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getCompleteStatus() : "not_started");
|
||||
childrenMap.computeIfAbsent(resource.getOutlineId(), key -> new ArrayList<>()).add(node);
|
||||
}
|
||||
return Result.success(roots);
|
||||
}
|
||||
|
||||
@At
|
||||
@Ok("void")
|
||||
@ApiOperation("PDF内联预览")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public void pdfPreview(String id, HttpServletResponse response) throws IOException {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "文件ID不能为空");
|
||||
return;
|
||||
}
|
||||
Sys_file file = dao.fetch(Sys_file.class, id);
|
||||
if (file == null) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
|
||||
return;
|
||||
}
|
||||
byte[] bytes;
|
||||
if (SysFileEngineTypeEnum.MINIO.getValue().equals(file.getEngine())) {
|
||||
bytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
|
||||
} else {
|
||||
File localFile = FileUtil.file(file.getStoragePath());
|
||||
if (!FileUtil.exist(localFile)) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
|
||||
return;
|
||||
}
|
||||
bytes = IoUtil.readBytes(FileUtil.getInputStream(localFile));
|
||||
}
|
||||
if (!"pdf".equalsIgnoreCase(file.getSuffix())) {
|
||||
File sourceFile = File.createTempFile("learning_preview_origin", "." + file.getSuffix());
|
||||
File pdfFile = File.createTempFile("learning_preview", ".pdf");
|
||||
try {
|
||||
Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE);
|
||||
OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath());
|
||||
if (!FileUtil.exist(pdfFile) || pdfFile.length() == 0) {
|
||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "文件转换PDF失败");
|
||||
return;
|
||||
}
|
||||
bytes = IoUtil.readBytes(FileUtil.getInputStream(pdfFile));
|
||||
} finally {
|
||||
FileUtil.del(sourceFile);
|
||||
FileUtil.del(pdfFile);
|
||||
}
|
||||
}
|
||||
String fileName = StrUtil.blankToDefault(FileUtil.mainName(file.getName()), "preview") + ".pdf";
|
||||
String encodedName = URLEncoder.encode(fileName, CharsetUtil.UTF_8).replace("+", "%20");
|
||||
response.setContentType("application/pdf");
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"preview.pdf\"; filename*=UTF-8''" + encodedName);
|
||||
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(bytes.length));
|
||||
try (OutputStream out = response.getOutputStream()) {
|
||||
out.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("启用课程类型")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public Result courseTypes() {
|
||||
return Result.success(dao.query(LearningCourseType.class, Cnd.where("enabled", "=", true).asc("sortNum").asc("createdAt")));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("推荐标识")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public Result recommendOptions() {
|
||||
Sys_dict root = sysDictService.fetch(Cnd.where("code", "=", "学习教育").or("name", "=", "学习教育"));
|
||||
if (root == null) {
|
||||
return Result.success(List.of());
|
||||
}
|
||||
List<Sys_dict> firstLevel = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(root.getId())).and("disabled", "=", false).asc("location"));
|
||||
Sys_dict group = firstLevel.stream()
|
||||
.filter(dict -> "推荐标识".equals(dict.getName()) || "推荐标识".equals(dict.getCode()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (group == null) {
|
||||
return Result.success(firstLevel);
|
||||
}
|
||||
List<Sys_dict> children = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(group.getId())).and("disabled", "=", false).asc("location"));
|
||||
return Result.success(children.isEmpty() ? firstLevel : children);
|
||||
}
|
||||
|
||||
private int value(Integer value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
package com.budwk.app.zhgh.learning.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.sys.models.Sys_dict;
|
||||
import com.budwk.app.sys.services.SysDictService;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourse;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourseOutline;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourseType;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
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.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("学习教育平台-课程管理")
|
||||
@At("/platform/learning/course/manage")
|
||||
public class LearningCourseManageController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@Inject
|
||||
private SysDictService sysDictService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/Learning/courseManage/index.html")
|
||||
@SaCheckPermission("learning.course.manage")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("课程列表")
|
||||
@SaCheckPermission("learning.course.manage")
|
||||
public Result pageData(PageForm pageForm,
|
||||
String courseName,
|
||||
String courseTypeId,
|
||||
String startTime,
|
||||
String endTime,
|
||||
String lecturerName,
|
||||
String status,
|
||||
String recommendFlag) {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and(Cnd.likeEX("c.courseName", courseName));
|
||||
cnd.andEX("c.courseTypeId", "=", courseTypeId);
|
||||
cnd.and(Cnd.likeEX("c.lecturerName", lecturerName));
|
||||
cnd.andEX("c.status", "=", status);
|
||||
if (StrUtil.isNotBlank(recommendFlag)) {
|
||||
cnd.and("c.recommendFlags", "like", "%" + recommendFlag + "%");
|
||||
}
|
||||
if (StrUtil.isNotBlank(startTime)) {
|
||||
cnd.and("c.startTime", ">=", DateUtil.parse(startTime));
|
||||
}
|
||||
if (StrUtil.isNotBlank(endTime)) {
|
||||
cnd.and("c.endTime", "<=", DateUtil.parse(endTime));
|
||||
}
|
||||
|
||||
String orderColumn = getOrderColumn(pageForm.getPageOrderName());
|
||||
String orderBy = "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc";
|
||||
if (StrUtil.isBlank(orderColumn)) {
|
||||
orderColumn = "c.sortNum";
|
||||
orderBy = "asc";
|
||||
}
|
||||
|
||||
Sql countSql = Sqls.create("""
|
||||
SELECT COUNT(1)
|
||||
FROM learning_course c
|
||||
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
|
||||
$condition
|
||||
""");
|
||||
countSql.setCondition(cnd);
|
||||
countSql.setCallback(Sqls.callback.integer());
|
||||
dao.execute(countSql);
|
||||
int count = countSql.getInt();
|
||||
|
||||
Sql listSql = Sqls.create("""
|
||||
SELECT c.*, t.typeName AS courseTypeName
|
||||
FROM learning_course c
|
||||
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
|
||||
$condition
|
||||
ORDER BY $orderColumn $orderBy
|
||||
""");
|
||||
listSql.setCondition(cnd);
|
||||
listSql.setVar("orderColumn", orderColumn);
|
||||
listSql.setVar("orderBy", orderBy);
|
||||
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||
listSql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(listSql);
|
||||
|
||||
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class));
|
||||
return Result.success(pagination);
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("新增课程")
|
||||
@SaCheckPermission("learning.course.manage")
|
||||
@SLog(tag = "新增课程", msg = "课程名称:${args[0].courseName}")
|
||||
public Result doAdd(LearningCourse course) {
|
||||
Result checkResult = checkCourse(course, null);
|
||||
if (checkResult != null) {
|
||||
return checkResult;
|
||||
}
|
||||
if (StrUtil.isBlank(course.getStatus())) {
|
||||
course.setStatus("draft");
|
||||
}
|
||||
dao.insert(course);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("编辑课程")
|
||||
@SaCheckPermission("learning.course.manage")
|
||||
@SLog(tag = "编辑课程", msg = "课程ID:${args[0].id}")
|
||||
public Result doEdit(LearningCourse course) {
|
||||
if (StrUtil.isBlank(course.getId())) {
|
||||
return Result.error("请选择要编辑的数据");
|
||||
}
|
||||
Result checkResult = checkCourse(course, course.getId());
|
||||
if (checkResult != null) {
|
||||
return checkResult;
|
||||
}
|
||||
dao.updateIgnoreNull(course);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("删除课程")
|
||||
@SaCheckPermission("learning.course.manage")
|
||||
@SLog(tag = "删除课程", msg = "课程ID:${args[0]}")
|
||||
public Result doDelete(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
return Result.error("请选择要删除的数据");
|
||||
}
|
||||
int outlineCount = dao.count(LearningCourseOutline.class, Cnd.where("courseId", "=", id));
|
||||
if (outlineCount > 0) {
|
||||
return Result.error("该课程存在章节内容,请先删除章节内容后再删除课程");
|
||||
}
|
||||
dao.clear(LearningCourse.class, Cnd.where("id", "=", id));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("启用课程类型")
|
||||
@SaCheckPermission("learning.course.manage")
|
||||
public Result courseTypes() {
|
||||
return Result.success(dao.query(LearningCourseType.class, Cnd.where("enabled", "=", true).asc("sortNum").asc("createdAt")));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("学习教育数据字典")
|
||||
@SaCheckPermission("learning.course.manage")
|
||||
public Result learningDictOptions(String name) {
|
||||
Sys_dict root = sysDictService.fetch(Cnd.where("code", "=", "学习教育").or("name", "=", "学习教育"));
|
||||
if (root == null) {
|
||||
return Result.success(List.of());
|
||||
}
|
||||
List<Sys_dict> firstLevel = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(root.getId())).and("disabled", "=", false).asc("location"));
|
||||
if (StrUtil.isBlank(name)) {
|
||||
return Result.success(firstLevel);
|
||||
}
|
||||
Sys_dict group = firstLevel.stream()
|
||||
.filter(dict -> name.equals(dict.getName()) || name.equals(dict.getCode()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (group == null) {
|
||||
return Result.success(firstLevel);
|
||||
}
|
||||
List<Sys_dict> children = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(group.getId())).and("disabled", "=", false).asc("location"));
|
||||
return Result.success(children.isEmpty() ? firstLevel : children);
|
||||
}
|
||||
|
||||
private Result checkCourse(LearningCourse course, String excludeId) {
|
||||
if (course == null || StrUtil.isBlank(course.getCourseName())) {
|
||||
return Result.error("课程名称不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(course.getCourseTypeId())) {
|
||||
return Result.error("请选择课程类型");
|
||||
}
|
||||
if (StrUtil.isBlank(course.getLecturerName())) {
|
||||
return Result.error("授课讲师不能为空");
|
||||
}
|
||||
if (course.getSortNum() == null) {
|
||||
return Result.error("排序编码不能为空");
|
||||
}
|
||||
if (!"long_term".equals(course.getOpenType()) && (course.getStartTime() == null || course.getEndTime() == null)) {
|
||||
return Result.error("请选择开课时间");
|
||||
}
|
||||
Cnd cnd = Cnd.where("courseName", "=", course.getCourseName().trim());
|
||||
if (StrUtil.isNotBlank(excludeId)) {
|
||||
cnd.and("id", "!=", excludeId);
|
||||
}
|
||||
if (dao.count(LearningCourse.class, cnd) > 0) {
|
||||
return Result.error("课程名称已存在");
|
||||
}
|
||||
course.setCourseName(course.getCourseName().trim());
|
||||
if ("long_term".equals(course.getOpenType())) {
|
||||
course.setStartTime(null);
|
||||
course.setEndTime(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getOrderColumn(String prop) {
|
||||
Map<String, String> columns = new HashMap<>();
|
||||
columns.put("courseName", "c.courseName");
|
||||
columns.put("courseTypeName", "t.typeName");
|
||||
columns.put("lecturerName", "c.lecturerName");
|
||||
columns.put("startTime", "c.startTime");
|
||||
columns.put("status", "c.status");
|
||||
columns.put("recommendFlags", "c.recommendFlags");
|
||||
columns.put("sortNum", "c.sortNum");
|
||||
return columns.get(prop);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.budwk.app.zhgh.learning.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.learning.models.LearningCourseType;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.pager.Pager;
|
||||
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;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("学习教育平台-课程类型设置")
|
||||
@At("/platform/learning/course/type")
|
||||
public class LearningCourseTypeController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/Learning/courseType/index.html")
|
||||
@SaCheckPermission("learning.course.type")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("课程类型列表")
|
||||
@SaCheckPermission("learning.course.type")
|
||||
public Result pageData(PageForm pageForm, @Param("typeName") String typeName) {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and(Cnd.likeEX("typeName", typeName));
|
||||
cnd.asc("sortNum").asc("createdAt");
|
||||
int count = dao.count(LearningCourseType.class, cnd);
|
||||
Pager pager = dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize());
|
||||
Pagination<LearningCourseType> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, dao.query(LearningCourseType.class, cnd, pager));
|
||||
return Result.success(pagination);
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("新增课程类型")
|
||||
@SaCheckPermission("learning.course.type")
|
||||
@SLog(tag = "新增课程类型", msg = "课程类型名称:${args[0].typeName}")
|
||||
public Result doAdd(LearningCourseType courseType) {
|
||||
Result checkResult = checkCourseType(courseType, null);
|
||||
if (checkResult != null) {
|
||||
return checkResult;
|
||||
}
|
||||
if (courseType.getEnabled() == null) {
|
||||
courseType.setEnabled(true);
|
||||
}
|
||||
dao.insert(courseType);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("编辑课程类型")
|
||||
@SaCheckPermission("learning.course.type")
|
||||
@SLog(tag = "编辑课程类型", msg = "课程类型ID:${args[0].id}")
|
||||
public Result doEdit(LearningCourseType courseType) {
|
||||
if (StrUtil.isBlank(courseType.getId())) {
|
||||
return Result.error("请选择要编辑的数据");
|
||||
}
|
||||
Result checkResult = checkCourseType(courseType, courseType.getId());
|
||||
if (checkResult != null) {
|
||||
return checkResult;
|
||||
}
|
||||
dao.updateIgnoreNull(courseType);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("删除课程类型")
|
||||
@SaCheckPermission("learning.course.type")
|
||||
@SLog(tag = "删除课程类型", msg = "课程类型ID:${args[0]}")
|
||||
public Result doDelete(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
return Result.error("请选择要删除的数据");
|
||||
}
|
||||
dao.clear(LearningCourseType.class, Cnd.where("id", "=", id));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private Result checkCourseType(LearningCourseType courseType, String excludeId) {
|
||||
if (courseType == null || StrUtil.isBlank(courseType.getTypeName())) {
|
||||
return Result.error("课程类型名称不能为空");
|
||||
}
|
||||
if (courseType.getSortNum() == null) {
|
||||
return Result.error("排序编号不能为空");
|
||||
}
|
||||
Cnd cnd = Cnd.where("typeName", "=", courseType.getTypeName().trim());
|
||||
if (StrUtil.isNotBlank(excludeId)) {
|
||||
cnd.and("id", "!=", excludeId);
|
||||
}
|
||||
if (dao.count(LearningCourseType.class, cnd) > 0) {
|
||||
return Result.error("课程类型名称已存在");
|
||||
}
|
||||
courseType.setTypeName(courseType.getTypeName().trim());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.budwk.app.zhgh.learning.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
@IocBean
|
||||
@At("/platform/learning")
|
||||
public class LearningPlatformController {
|
||||
|
||||
@At("/activity/manage")
|
||||
@Ok("beetl:/platform/zhgh/Learning/activityManage/index.html")
|
||||
@SaCheckPermission("learning.activity.manage")
|
||||
public void activityManage() {
|
||||
}
|
||||
|
||||
@At("/my/record")
|
||||
@Ok("beetl:/platform/zhgh/Learning/myRecord/index.html")
|
||||
@SaCheckPermission("learning.my.record")
|
||||
public void myRecord() {
|
||||
}
|
||||
|
||||
@At("/my/record/h5")
|
||||
@Ok("beetl:/platform/zhghh5/learning/myRecord/index.html")
|
||||
@SaCheckPermission("h5.learning.my.record")
|
||||
public void myRecordH5() {
|
||||
}
|
||||
|
||||
@At("/statistics")
|
||||
@Ok("beetl:/platform/zhgh/Learning/statistics/index.html")
|
||||
@SaCheckPermission("learning.statistics")
|
||||
public void statistics() {
|
||||
}
|
||||
|
||||
@At("/course/h5")
|
||||
@Ok("beetl:/platform/zhghh5/learning/course/index.html")
|
||||
@SaCheckPermission("h5.learning.course.display")
|
||||
public void courseH5() {
|
||||
}
|
||||
|
||||
@At("/course/h5/study")
|
||||
@Ok("beetl:/platform/zhghh5/learning/course/study.html")
|
||||
@SaCheckPermission("h5.learning.course.display")
|
||||
public void courseH5Study() {
|
||||
}
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
package com.budwk.app.zhgh.learning.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.utils.PageUtil;
|
||||
import com.budwk.app.sys.models.Sys_union;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourse;
|
||||
import com.budwk.app.zhgh.learning.param.LearningStatisticsPageForm;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.dao.util.cri.Static;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("学习教育平台-学习统计")
|
||||
@At("/platform/learning/statistics")
|
||||
public class LearningStatisticsController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/Learning/statistics/index.html")
|
||||
@SaCheckPermission("learning.statistics")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("课程统计")
|
||||
@SaCheckPermission("learning.statistics")
|
||||
public Result coursePageData(LearningStatisticsPageForm pageForm) {
|
||||
Cnd cnd = courseCondition(pageForm);
|
||||
Sql countSql = Sqls.create("""
|
||||
SELECT COUNT(1)
|
||||
FROM learning_course c
|
||||
$condition
|
||||
""");
|
||||
countSql.setCondition(cnd);
|
||||
countSql.setCallback(Sqls.callback.integer());
|
||||
dao.execute(countSql);
|
||||
|
||||
Sql listSql = Sqls.create("""
|
||||
SELECT
|
||||
c.id AS courseId,
|
||||
c.courseName,
|
||||
IFNULL(COUNT(uc.userId), 0) AS learnerCount,
|
||||
IFNULL(SUM(CASE WHEN target.targetCount > 0 AND uc.completedOutlineCount >= target.targetCount THEN 1 ELSE 0 END), 0) AS completedCount,
|
||||
IFNULL(ROUND(AVG(uc.studySeconds)), 0) AS avgStudySeconds
|
||||
FROM learning_course c
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
courseId,
|
||||
CASE
|
||||
WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0
|
||||
THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END)
|
||||
ELSE COUNT(1)
|
||||
END AS targetCount,
|
||||
SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount
|
||||
FROM learning_course_outline
|
||||
WHERE status = 'enabled'
|
||||
GROUP BY courseId
|
||||
) target ON target.courseId = c.id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
r.courseId,
|
||||
r.userId,
|
||||
SUM(IFNULL(r.studySeconds, 0)) AS studySeconds,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN r.completeStatus = 'completed'
|
||||
AND ((target.requiredCount > 0 AND o.required = 1) OR target.requiredCount = 0)
|
||||
THEN r.outlineId
|
||||
ELSE NULL
|
||||
END) AS completedOutlineCount
|
||||
FROM learning_study_record r
|
||||
JOIN learning_course_outline o ON o.id = r.outlineId AND o.courseId = r.courseId AND o.status = 'enabled'
|
||||
JOIN (
|
||||
SELECT
|
||||
courseId,
|
||||
CASE
|
||||
WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0
|
||||
THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END)
|
||||
ELSE COUNT(1)
|
||||
END AS targetCount,
|
||||
SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount
|
||||
FROM learning_course_outline
|
||||
WHERE status = 'enabled'
|
||||
GROUP BY courseId
|
||||
) target ON target.courseId = r.courseId
|
||||
$recordCondition
|
||||
GROUP BY r.courseId, r.userId
|
||||
) uc ON uc.courseId = c.id
|
||||
$condition
|
||||
GROUP BY c.id, c.courseName
|
||||
ORDER BY $orderColumn $orderBy
|
||||
""");
|
||||
listSql.setCondition(cnd);
|
||||
listSql.setVar("recordCondition", new Static(recordWhere(pageForm)));
|
||||
setOrder(listSql, pageForm, "course");
|
||||
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||
listSql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(listSql);
|
||||
List<NutMap> rows = listSql.getList(NutMap.class);
|
||||
rows.forEach(row -> row.put("avgStudyTimeText", formatStudySeconds(row.getInt("avgStudySeconds", 0))));
|
||||
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), countSql.getInt(), rows));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("分工会统计")
|
||||
@SaCheckPermission("learning.statistics")
|
||||
public Result unionPageData(LearningStatisticsPageForm pageForm) {
|
||||
Cnd cnd = unionCondition(pageForm);
|
||||
Sql countSql = Sqls.create("""
|
||||
SELECT COUNT(1)
|
||||
FROM learning_course c
|
||||
JOIN (
|
||||
SELECT r.courseId, r.unionId
|
||||
FROM learning_study_record r
|
||||
$recordCondition
|
||||
GROUP BY r.courseId, r.unionId
|
||||
) uc ON uc.courseId = c.id
|
||||
LEFT JOIN sys_union un ON un.id = uc.unionId
|
||||
$condition
|
||||
""");
|
||||
countSql.setCondition(cnd);
|
||||
countSql.setVar("recordCondition", new Static(recordWhere(pageForm)));
|
||||
countSql.setCallback(Sqls.callback.integer());
|
||||
dao.execute(countSql);
|
||||
|
||||
Sql listSql = Sqls.create("""
|
||||
SELECT
|
||||
c.id AS courseId,
|
||||
c.courseName,
|
||||
un.id AS unionId,
|
||||
IFNULL(un.name, uc.unionName) AS unionName,
|
||||
COUNT(uc.userId) AS learnerCount,
|
||||
IFNULL(SUM(uc.studySeconds), 0) AS studySeconds,
|
||||
IFNULL(SUM(CASE WHEN target.targetCount > 0 AND uc.completedOutlineCount >= target.targetCount THEN 1 ELSE 0 END), 0) AS completedCount
|
||||
FROM learning_course c
|
||||
JOIN (
|
||||
SELECT
|
||||
r.courseId,
|
||||
r.userId,
|
||||
r.unionId,
|
||||
MAX(r.unionName) AS unionName,
|
||||
SUM(IFNULL(r.studySeconds, 0)) AS studySeconds,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN r.completeStatus = 'completed'
|
||||
AND ((target.requiredCount > 0 AND o.required = 1) OR target.requiredCount = 0)
|
||||
THEN r.outlineId
|
||||
ELSE NULL
|
||||
END) AS completedOutlineCount
|
||||
FROM learning_study_record r
|
||||
JOIN learning_course_outline o ON o.id = r.outlineId AND o.courseId = r.courseId AND o.status = 'enabled'
|
||||
JOIN (
|
||||
SELECT
|
||||
courseId,
|
||||
CASE
|
||||
WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0
|
||||
THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END)
|
||||
ELSE COUNT(1)
|
||||
END AS targetCount,
|
||||
SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount
|
||||
FROM learning_course_outline
|
||||
WHERE status = 'enabled'
|
||||
GROUP BY courseId
|
||||
) target ON target.courseId = r.courseId
|
||||
$recordCondition
|
||||
GROUP BY r.courseId, r.userId, r.unionId
|
||||
) uc ON uc.courseId = c.id
|
||||
LEFT JOIN sys_union un ON un.id = uc.unionId
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
courseId,
|
||||
CASE
|
||||
WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0
|
||||
THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END)
|
||||
ELSE COUNT(1)
|
||||
END AS targetCount,
|
||||
SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount
|
||||
FROM learning_course_outline
|
||||
WHERE status = 'enabled'
|
||||
GROUP BY courseId
|
||||
) target ON target.courseId = c.id
|
||||
$condition
|
||||
GROUP BY c.id, c.courseName, uc.unionId, un.id, un.name
|
||||
ORDER BY $orderColumn $orderBy
|
||||
""");
|
||||
listSql.setCondition(cnd);
|
||||
listSql.setVar("recordCondition", new Static(recordWhere(pageForm)));
|
||||
setOrder(listSql, pageForm, "union");
|
||||
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||
listSql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(listSql);
|
||||
List<NutMap> rows = listSql.getList(NutMap.class);
|
||||
rows.forEach(row -> {
|
||||
int learnerCount = row.getInt("learnerCount", 0);
|
||||
int completedCount = row.getInt("completedCount", 0);
|
||||
row.put("studyTimeText", formatStudySeconds(row.getInt("studySeconds", 0)));
|
||||
row.put("completeRate", learnerCount == 0 ? "0%" : BigDecimal.valueOf(completedCount * 100.0 / learnerCount).setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() + "%");
|
||||
});
|
||||
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), countSql.getInt(), rows));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("可选课程")
|
||||
@SaCheckPermission("learning.statistics")
|
||||
public Result courseOptions(String courseName) {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and(Cnd.likeEX("courseName", courseName));
|
||||
cnd.desc("createdAt").asc("sortNum");
|
||||
return Result.success(dao.query(LearningCourse.class, cnd, dao.createPager(1, 50)));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("可选分工会")
|
||||
@SaCheckPermission("learning.statistics")
|
||||
public Result unionOptions() {
|
||||
if (hasSchoolScope()) {
|
||||
return Result.success(dao.query(Sys_union.class, Cnd.NEW().asc("unionCode").asc("name")));
|
||||
}
|
||||
if (hasBranchScope()) {
|
||||
return Result.success(dao.query(Sys_union.class, Cnd.where("id", "=", SecurityUtil.getUnionId())));
|
||||
}
|
||||
return Result.success(List.of());
|
||||
}
|
||||
|
||||
private Cnd courseCondition(LearningStatisticsPageForm pageForm) {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("c.id", "=", pageForm.getCourseId());
|
||||
cnd.and(Cnd.likeEX("c.courseName", pageForm.getCourseName()));
|
||||
return cnd;
|
||||
}
|
||||
|
||||
private Cnd unionCondition(LearningStatisticsPageForm pageForm) {
|
||||
Cnd cnd = courseCondition(pageForm);
|
||||
cnd.andEX("uc.unionId", "=", pageForm.getUnionId());
|
||||
return cnd;
|
||||
}
|
||||
|
||||
private String recordWhere(LearningStatisticsPageForm pageForm) {
|
||||
StringBuilder where = new StringBuilder("WHERE 1 = 1");
|
||||
if (hasBranchScope()) {
|
||||
where.append(" AND r.unionId = '").append(escapeSql(SecurityUtil.getUnionId())).append("'");
|
||||
} else if (!hasSchoolScope()) {
|
||||
where.append(" AND r.userId = '").append(escapeSql(SecurityUtil.getUserId())).append("'");
|
||||
}
|
||||
if (StrUtil.isNotBlank(pageForm.getUnionId())) {
|
||||
where.append(" AND r.unionId = '").append(escapeSql(pageForm.getUnionId())).append("'");
|
||||
}
|
||||
return where.toString();
|
||||
}
|
||||
|
||||
private void setOrder(Sql sql, LearningStatisticsPageForm pageForm, String type) {
|
||||
String orderColumn = "union".equals(type) ? getUnionOrderColumn(pageForm.getPageOrderName()) : getCourseOrderColumn(pageForm.getPageOrderName());
|
||||
String orderBy = PageUtil.getOrder(pageForm.getPageOrderBy());
|
||||
if (StrUtil.isBlank(orderColumn)) {
|
||||
orderColumn = "union".equals(type) ? "c.courseName ASC, un.unionCode" : "c.sortNum ASC, c.createdAt";
|
||||
orderBy = "DESC";
|
||||
} else if (StrUtil.isBlank(orderBy)) {
|
||||
orderBy = "ASC";
|
||||
}
|
||||
sql.setVar("orderColumn", new Static(orderColumn));
|
||||
sql.setVar("orderBy", new Static(orderBy));
|
||||
}
|
||||
|
||||
private String getCourseOrderColumn(String prop) {
|
||||
Map<String, String> columns = new HashMap<>();
|
||||
columns.put("courseName", "c.courseName");
|
||||
columns.put("learnerCount", "learnerCount");
|
||||
columns.put("completedCount", "completedCount");
|
||||
columns.put("avgStudySeconds", "avgStudySeconds");
|
||||
return columns.get(prop);
|
||||
}
|
||||
|
||||
private String getUnionOrderColumn(String prop) {
|
||||
Map<String, String> columns = new HashMap<>();
|
||||
columns.put("courseName", "c.courseName");
|
||||
columns.put("unionName", "unionName");
|
||||
columns.put("learnerCount", "learnerCount");
|
||||
columns.put("studySeconds", "studySeconds");
|
||||
columns.put("completeRate", "completedCount");
|
||||
return columns.get(prop);
|
||||
}
|
||||
|
||||
private boolean hasSchoolScope() {
|
||||
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name());
|
||||
}
|
||||
|
||||
private boolean hasBranchScope() {
|
||||
return AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name());
|
||||
}
|
||||
|
||||
private String escapeSql(String value) {
|
||||
return value == null ? "" : value.replace("'", "''");
|
||||
}
|
||||
|
||||
private String formatStudySeconds(Integer seconds) {
|
||||
int value = seconds == null ? 0 : seconds;
|
||||
int hour = value / 3600;
|
||||
int minute = value % 3600 / 60;
|
||||
int second = value % 60;
|
||||
if (hour > 0) {
|
||||
return hour + "小时" + minute + "分" + second + "秒";
|
||||
}
|
||||
if (minute > 0) {
|
||||
return minute + "分" + second + "秒";
|
||||
}
|
||||
return second + "秒";
|
||||
}
|
||||
}
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
package com.budwk.app.zhgh.learning.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
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.base.utils.PageUtil;
|
||||
import com.budwk.app.sys.models.Sys_union;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourse;
|
||||
import com.budwk.app.zhgh.learning.models.LearningCourseOutline;
|
||||
import com.budwk.app.zhgh.learning.models.LearningOutlineResource;
|
||||
import com.budwk.app.zhgh.learning.models.LearningStudyRecord;
|
||||
import com.budwk.app.zhgh.learning.models.LearningStudyRule;
|
||||
import com.budwk.app.zhgh.learning.models.LearningStudySegment;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@Api("学习教育平台-学习记录")
|
||||
@At("/platform/learning/study/record")
|
||||
public class LearningStudyRecordController {
|
||||
|
||||
private static final int MAX_HEARTBEAT_SECONDS = 30;
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@At
|
||||
@ApiOperation("学习记录分页")
|
||||
@SaCheckPermission(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR)
|
||||
public Result pageData(PageForm pageForm,
|
||||
@Param("keyword") String keyword,
|
||||
@Param("unionId") String unionId,
|
||||
@Param("courseId") String courseId,
|
||||
@Param("completeStatus") String completeStatus) {
|
||||
Cnd cnd = Cnd.NEW();
|
||||
if (StrUtil.isNotBlank(keyword)) {
|
||||
SqlExpressionGroup seg = new SqlExpressionGroup();
|
||||
seg.or("r.loginName", "like", "%" + keyword + "%");
|
||||
seg.or("r.userName", "like", "%" + keyword + "%");
|
||||
cnd.and(seg);
|
||||
}
|
||||
cnd.andEX("r.unionId", "=", unionId);
|
||||
cnd.andEX("r.courseId", "=", courseId);
|
||||
cnd.andEX("r.completeStatus", "=", completeStatus);
|
||||
appendScope(cnd);
|
||||
|
||||
String orderColumn = getOrderColumn(pageForm.getPageOrderName());
|
||||
if (StrUtil.isNotBlank(orderColumn) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) {
|
||||
cnd.orderBy(orderColumn, PageUtil.getOrder(pageForm.getPageOrderBy()));
|
||||
} else {
|
||||
cnd.desc("r.latestStudyTime");
|
||||
}
|
||||
|
||||
Sql countSql = Sqls.create("SELECT COUNT(1) FROM learning_study_record r $condition");
|
||||
countSql.setCondition(cnd);
|
||||
countSql.setCallback(Sqls.callback.integer());
|
||||
dao.execute(countSql);
|
||||
|
||||
Sql listSql = Sqls.create("""
|
||||
SELECT r.*
|
||||
FROM learning_study_record r
|
||||
$condition
|
||||
""");
|
||||
listSql.setCondition(cnd);
|
||||
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||
listSql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(listSql);
|
||||
List<NutMap> rows = listSql.getList(NutMap.class);
|
||||
rows.forEach(row -> row.put("studyTimeText", formatStudySeconds(row.getInt("studySeconds", 0))));
|
||||
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), countSql.getInt(), rows));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("移动端我的学习汇总")
|
||||
@SaCheckPermission(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR)
|
||||
public Result h5Summary() {
|
||||
String userId = SecurityUtil.getUserId();
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT IFNULL(SUM(IFNULL(studySeconds, 0)), 0) AS studySeconds,
|
||||
COUNT(DISTINCT courseId) AS courseCount,
|
||||
COUNT(DISTINCT CASE WHEN completeStatus = 'completed' THEN courseId END) AS completedCourseCount,
|
||||
COUNT(DISTINCT outlineId) AS outlineCount,
|
||||
COUNT(DISTINCT CASE WHEN completeStatus = 'completed' THEN outlineId END) AS completedOutlineCount
|
||||
FROM learning_study_record
|
||||
WHERE userId = @userId
|
||||
""");
|
||||
sql.params().set("userId", userId);
|
||||
sql.setCallback(Sqls.callback.map());
|
||||
dao.execute(sql);
|
||||
NutMap data = sql.getObject(NutMap.class);
|
||||
if (data == null) {
|
||||
data = NutMap.NEW();
|
||||
}
|
||||
int studySeconds = data.getInt("studySeconds", 0);
|
||||
data.put("studyHour", studySeconds / 3600);
|
||||
data.put("studyMinute", studySeconds % 3600 / 60);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("移动端我的学习课程")
|
||||
@SaCheckPermission(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR)
|
||||
public Result h5Courses() {
|
||||
String userId = SecurityUtil.getUserId();
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT r.courseId,
|
||||
MAX(r.courseName) AS courseName,
|
||||
MAX(c.cover) AS cover,
|
||||
COUNT(1) AS outlineCount,
|
||||
SUM(CASE WHEN r.completeStatus = 'completed' THEN 1 ELSE 0 END) AS completedOutlineCount,
|
||||
IFNULL(SUM(IFNULL(r.studySeconds, 0)), 0) AS studySeconds,
|
||||
IFNULL(ROUND(AVG(IFNULL(r.progressPercent, 0))), 0) AS progressPercent,
|
||||
MAX(r.latestStudyTime) AS latestStudyTime
|
||||
FROM learning_study_record r
|
||||
LEFT JOIN learning_course c ON c.id = r.courseId
|
||||
WHERE r.userId = @userId
|
||||
GROUP BY r.courseId
|
||||
ORDER BY latestStudyTime DESC
|
||||
""");
|
||||
sql.params().set("userId", userId);
|
||||
sql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(sql);
|
||||
List<NutMap> rows = sql.getList(NutMap.class);
|
||||
rows.forEach(row -> row.put("studyTimeText", formatStudySeconds(row.getInt("studySeconds", 0))));
|
||||
return Result.success(rows);
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("开始学习")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public Result start(@Param("courseId") String courseId,
|
||||
@Param("resourceId") String resourceId,
|
||||
@Param("positionSeconds") Integer positionSeconds) {
|
||||
if (StrUtil.hasBlank(courseId, resourceId)) {
|
||||
return Result.error("请选择课程资料后开始学习");
|
||||
}
|
||||
LearningCourse course = dao.fetch(LearningCourse.class, courseId);
|
||||
LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, resourceId);
|
||||
if (course == null || resource == null || !courseId.equals(resource.getCourseId())) {
|
||||
return Result.error("课程资料不存在");
|
||||
}
|
||||
LearningCourseOutline outline = dao.fetch(LearningCourseOutline.class, resource.getOutlineId());
|
||||
if (outline == null) {
|
||||
return Result.error("章节不存在");
|
||||
}
|
||||
|
||||
String userId = SecurityUtil.getUserId();
|
||||
Date now = new Date();
|
||||
closeUnfinishedSegments(userId, "interrupted");
|
||||
|
||||
LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, Cnd.where("userId", "=", userId)
|
||||
.and("courseId", "=", courseId)
|
||||
.and("outlineId", "=", outline.getId()));
|
||||
if (record == null) {
|
||||
record = buildRecord(userId, course, outline, now);
|
||||
record.setLastPositionSeconds(sanitizePosition(positionSeconds, 0));
|
||||
dao.insert(record);
|
||||
} else {
|
||||
int requiredSeconds = requiredSeconds(outline.getId());
|
||||
dao.update(LearningStudyRecord.class, Chain.make("latestStudyTime", now)
|
||||
.add("courseName", course.getCourseName())
|
||||
.add("outlineName", outline.getTitle())
|
||||
.add("lastPositionSeconds", sanitizePosition(positionSeconds, record.getLastPositionSeconds()))
|
||||
.add("requiredSeconds", requiredSeconds)
|
||||
.add("progressPercent", progress(record.getStudySeconds(), requiredSeconds))
|
||||
.add("completeStatus", "completed".equals(record.getCompleteStatus()) ? "completed" : "studying"),
|
||||
Cnd.where("id", "=", record.getId()));
|
||||
record = dao.fetch(LearningStudyRecord.class, record.getId());
|
||||
}
|
||||
|
||||
LearningStudySegment segment = new LearningStudySegment();
|
||||
segment.setRecordId(record.getId());
|
||||
segment.setUserId(userId);
|
||||
segment.setCourseId(courseId);
|
||||
segment.setOutlineId(outline.getId());
|
||||
segment.setResourceId(resourceId);
|
||||
segment.setResourceName(resource.getResourceTitle());
|
||||
segment.setStartTime(now);
|
||||
segment.setLastHeartbeatTime(now);
|
||||
segment.setActiveSeconds(0);
|
||||
segment.setState("studying");
|
||||
dao.insert(segment);
|
||||
|
||||
return Result.success(NutMap.NEW()
|
||||
.addv("recordId", record.getId())
|
||||
.addv("segmentId", segment.getId())
|
||||
.addv("studySeconds", record.getStudySeconds())
|
||||
.addv("lastPositionSeconds", record.getLastPositionSeconds())
|
||||
.addv("requiredSeconds", record.getRequiredSeconds())
|
||||
.addv("progressPercent", record.getProgressPercent())
|
||||
.addv("completeStatus", record.getCompleteStatus()));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("学习心跳")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public Result heartbeat(@Param("segmentId") String segmentId,
|
||||
@Param("activeSeconds") Integer activeSeconds,
|
||||
@Param("positionSeconds") Integer positionSeconds) {
|
||||
LearningStudySegment segment = fetchOwnStudyingSegment(segmentId);
|
||||
if (segment == null) {
|
||||
return Result.error("学习时段已结束,请重新开始学习");
|
||||
}
|
||||
int seconds = Math.max(0, Math.min(activeSeconds == null ? 0 : activeSeconds, MAX_HEARTBEAT_SECONDS));
|
||||
return Result.success(addActiveSeconds(segment, seconds, false, positionSeconds));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("结束学习")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public Result finish(@Param("segmentId") String segmentId,
|
||||
@Param("activeSeconds") Integer activeSeconds,
|
||||
@Param("positionSeconds") Integer positionSeconds) {
|
||||
LearningStudySegment segment = fetchOwnStudyingSegment(segmentId);
|
||||
if (segment == null) {
|
||||
return Result.success();
|
||||
}
|
||||
int seconds = Math.max(0, Math.min(activeSeconds == null ? 0 : activeSeconds, MAX_HEARTBEAT_SECONDS));
|
||||
NutMap result = addActiveSeconds(segment, seconds, true, positionSeconds);
|
||||
dao.update(LearningStudySegment.class, Chain.make("endTime", new Date()).add("state", "finished"), Cnd.where("id", "=", segmentId));
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("保存播放位置")
|
||||
@SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR)
|
||||
public Result position(@Param("courseId") String courseId,
|
||||
@Param("resourceId") String resourceId,
|
||||
@Param("positionSeconds") Integer positionSeconds) {
|
||||
if (StrUtil.hasBlank(courseId, resourceId) || positionSeconds == null || positionSeconds < 0) {
|
||||
return Result.success();
|
||||
}
|
||||
LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, resourceId);
|
||||
if (resource == null || !courseId.equals(resource.getCourseId())) {
|
||||
return Result.success();
|
||||
}
|
||||
dao.update(LearningStudyRecord.class,
|
||||
Chain.make("lastPositionSeconds", positionSeconds).add("latestStudyTime", new Date()),
|
||||
Cnd.where("userId", "=", SecurityUtil.getUserId())
|
||||
.and("courseId", "=", courseId)
|
||||
.and("outlineId", "=", resource.getOutlineId()));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("可选课程")
|
||||
@SaCheckPermission(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR)
|
||||
public Result courseOptions() {
|
||||
return Result.success(dao.query(LearningCourse.class, Cnd.NEW().asc("sortNum").desc("createdAt")));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("可选分工会")
|
||||
@SaCheckPermission(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR)
|
||||
public Result unionOptions() {
|
||||
if (hasSchoolScope()) {
|
||||
return Result.success(dao.query(Sys_union.class, Cnd.NEW().asc("unionCode").asc("name")));
|
||||
}
|
||||
if (hasBranchScope()) {
|
||||
return Result.success(dao.query(Sys_union.class, Cnd.where("id", "=", SecurityUtil.getUnionId())));
|
||||
}
|
||||
return Result.success(List.of());
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("删除学习记录")
|
||||
@SaCheckPermission(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR)
|
||||
public Result delete(@Param("id") String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
return Result.error("请选择要删除的学习记录");
|
||||
}
|
||||
LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, id);
|
||||
if (record == null) {
|
||||
return Result.error("学习记录不存在");
|
||||
}
|
||||
if (!canOperate(record)) {
|
||||
return Result.error("无权删除该学习记录");
|
||||
}
|
||||
dao.clear(LearningStudySegment.class, Cnd.where("recordId", "=", id));
|
||||
dao.clear(LearningStudyRecord.class, Cnd.where("id", "=", id));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private LearningStudyRecord buildRecord(String userId, LearningCourse course, LearningCourseOutline outline, Date now) {
|
||||
NutMap user = currentUserInfo(userId);
|
||||
int requiredSeconds = requiredSeconds(outline.getId());
|
||||
LearningStudyRecord record = new LearningStudyRecord();
|
||||
record.setUserId(userId);
|
||||
record.setLoginName(user.getString("loginname", SecurityUtil.getUserLoginname()));
|
||||
record.setUserName(user.getString("username", SecurityUtil.getUserUsername()));
|
||||
record.setUnionId(user.getString("unionId", SecurityUtil.getUnionId()));
|
||||
record.setUnionName(user.getString("unionName", ""));
|
||||
record.setCourseId(course.getId());
|
||||
record.setCourseName(course.getCourseName());
|
||||
record.setOutlineId(outline.getId());
|
||||
record.setOutlineName(outline.getTitle());
|
||||
record.setFirstStudyTime(now);
|
||||
record.setLatestStudyTime(now);
|
||||
record.setStudySeconds(0);
|
||||
record.setLastPositionSeconds(0);
|
||||
record.setRequiredSeconds(requiredSeconds);
|
||||
record.setProgressPercent(0);
|
||||
record.setCompleteStatus("studying");
|
||||
return record;
|
||||
}
|
||||
|
||||
private NutMap addActiveSeconds(LearningStudySegment segment, int activeSeconds, boolean finish, Integer positionSeconds) {
|
||||
Date now = new Date();
|
||||
int segmentSeconds = value(segment.getActiveSeconds()) + activeSeconds;
|
||||
dao.update(LearningStudySegment.class, Chain.make("activeSeconds", segmentSeconds).add("lastHeartbeatTime", now), Cnd.where("id", "=", segment.getId()));
|
||||
|
||||
LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, segment.getRecordId());
|
||||
int studySeconds = value(record.getStudySeconds()) + activeSeconds;
|
||||
int requiredSeconds = requiredSeconds(record.getOutlineId());
|
||||
int progressPercent = progress(studySeconds, requiredSeconds);
|
||||
String completeStatus = completeStatus(studySeconds, requiredSeconds, finish);
|
||||
if ("completed".equals(completeStatus)) {
|
||||
progressPercent = 100;
|
||||
}
|
||||
Chain chain = Chain.make("latestStudyTime", now)
|
||||
.add("studySeconds", studySeconds)
|
||||
.add("requiredSeconds", requiredSeconds)
|
||||
.add("progressPercent", progressPercent)
|
||||
.add("completeStatus", completeStatus);
|
||||
int lastPositionSeconds = value(record.getLastPositionSeconds());
|
||||
if (positionSeconds != null && positionSeconds >= 0) {
|
||||
lastPositionSeconds = positionSeconds;
|
||||
chain.add("lastPositionSeconds", positionSeconds);
|
||||
}
|
||||
if ("completed".equals(completeStatus) && record.getCompletedAt() == null) {
|
||||
chain.add("completedAt", now);
|
||||
}
|
||||
dao.update(LearningStudyRecord.class, chain, Cnd.where("id", "=", record.getId()));
|
||||
return NutMap.NEW()
|
||||
.addv("studySeconds", studySeconds)
|
||||
.addv("studyTimeText", formatStudySeconds(studySeconds))
|
||||
.addv("lastPositionSeconds", lastPositionSeconds)
|
||||
.addv("requiredSeconds", requiredSeconds)
|
||||
.addv("progressPercent", progressPercent)
|
||||
.addv("completeStatus", completeStatus);
|
||||
}
|
||||
|
||||
private LearningStudySegment fetchOwnStudyingSegment(String segmentId) {
|
||||
if (StrUtil.isBlank(segmentId)) {
|
||||
return null;
|
||||
}
|
||||
return dao.fetch(LearningStudySegment.class, Cnd.where("id", "=", segmentId)
|
||||
.and("userId", "=", SecurityUtil.getUserId())
|
||||
.and("state", "=", "studying"));
|
||||
}
|
||||
|
||||
private void closeUnfinishedSegments(String userId, String state) {
|
||||
dao.update(LearningStudySegment.class,
|
||||
Chain.make("endTime", new Date()).add("state", state),
|
||||
Cnd.where("userId", "=", userId).and("state", "=", "studying"));
|
||||
}
|
||||
|
||||
private int requiredSeconds(String outlineId) {
|
||||
LearningStudyRule rule = dao.fetch(LearningStudyRule.class, Cnd.where("targetType", "=", "outline")
|
||||
.and("targetId", "=", outlineId)
|
||||
.and("status", "=", "enabled"));
|
||||
if (rule != null && value(rule.getMinStudySeconds()) > 0) {
|
||||
return rule.getMinStudySeconds();
|
||||
}
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT IFNULL(SUM(IFNULL(durationSeconds, 0)), 0)
|
||||
FROM learning_outline_resource
|
||||
WHERE outlineId = @outlineId AND status = 'enabled'
|
||||
""");
|
||||
sql.params().set("outlineId", outlineId);
|
||||
sql.setCallback(Sqls.callback.integer());
|
||||
dao.execute(sql);
|
||||
return Math.max(0, sql.getInt());
|
||||
}
|
||||
|
||||
private int progress(Integer studySeconds, int requiredSeconds) {
|
||||
if (requiredSeconds <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(100, (int) Math.floor(value(studySeconds) * 100.0 / requiredSeconds));
|
||||
}
|
||||
|
||||
private String completeStatus(Integer studySeconds, int requiredSeconds, boolean finish) {
|
||||
if (requiredSeconds <= 0) {
|
||||
return finish ? "completed" : "studying";
|
||||
}
|
||||
if (value(studySeconds) >= requiredSeconds) {
|
||||
return "completed";
|
||||
}
|
||||
return value(studySeconds) > 0 || finish ? "studying" : "not_started";
|
||||
}
|
||||
|
||||
private NutMap currentUserInfo(String userId) {
|
||||
Sql sql = Sqls.create("SELECT id, loginname, username, unionId AS unionId, unionName AS unionName FROM vw_user WHERE id = @id");
|
||||
sql.params().set("id", userId);
|
||||
sql.setCallback(Sqls.callback.map());
|
||||
dao.execute(sql);
|
||||
NutMap user = sql.getObject(NutMap.class);
|
||||
return user == null ? NutMap.NEW() : user;
|
||||
}
|
||||
|
||||
private void appendScope(Cnd cnd) {
|
||||
if (hasSchoolScope()) {
|
||||
return;
|
||||
}
|
||||
if (hasBranchScope()) {
|
||||
cnd.and("r.unionId", "=", SecurityUtil.getUnionId());
|
||||
return;
|
||||
}
|
||||
cnd.and("r.userId", "=", SecurityUtil.getUserId());
|
||||
}
|
||||
|
||||
private boolean hasSchoolScope() {
|
||||
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name());
|
||||
}
|
||||
|
||||
private boolean hasBranchScope() {
|
||||
return AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name());
|
||||
}
|
||||
|
||||
private boolean canOperate(LearningStudyRecord record) {
|
||||
if (hasSchoolScope()) {
|
||||
return true;
|
||||
}
|
||||
if (hasBranchScope()) {
|
||||
return StrUtil.equals(record.getUnionId(), SecurityUtil.getUnionId());
|
||||
}
|
||||
return StrUtil.equals(record.getUserId(), SecurityUtil.getUserId());
|
||||
}
|
||||
|
||||
private String getOrderColumn(String prop) {
|
||||
Map<String, String> columns = new HashMap<>();
|
||||
columns.put("loginName", "r.loginName");
|
||||
columns.put("userName", "r.userName");
|
||||
columns.put("unionName", "r.unionName");
|
||||
columns.put("courseName", "r.courseName");
|
||||
columns.put("outlineName", "r.outlineName");
|
||||
columns.put("firstStudyTime", "r.firstStudyTime");
|
||||
columns.put("latestStudyTime", "r.latestStudyTime");
|
||||
columns.put("studySeconds", "r.studySeconds");
|
||||
columns.put("progressPercent", "r.progressPercent");
|
||||
columns.put("completeStatus", "r.completeStatus");
|
||||
return columns.get(prop);
|
||||
}
|
||||
|
||||
private String formatStudySeconds(Integer seconds) {
|
||||
int value = value(seconds);
|
||||
int hour = value / 3600;
|
||||
int minute = value % 3600 / 60;
|
||||
int second = value % 60;
|
||||
if (hour > 0) {
|
||||
return hour + "小时" + minute + "分" + second + "秒";
|
||||
}
|
||||
if (minute > 0) {
|
||||
return minute + "分" + second + "秒";
|
||||
}
|
||||
return second + "秒";
|
||||
}
|
||||
|
||||
private int value(Integer value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
private int sanitizePosition(Integer positionSeconds, Integer defaultValue) {
|
||||
if (positionSeconds == null || positionSeconds < 0) {
|
||||
return value(defaultValue);
|
||||
}
|
||||
return positionSeconds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.budwk.app.zhgh.learning.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Table
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LearningCourse extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("课程名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String courseName;
|
||||
|
||||
@Column
|
||||
@Comment("课程类型")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String courseTypeId;
|
||||
|
||||
@Column
|
||||
@Comment("授课讲师")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String lecturerName;
|
||||
|
||||
@Column
|
||||
@Comment("讲师基本信息")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String lecturerInfo;
|
||||
|
||||
@Column
|
||||
@Comment("课程简介")
|
||||
@ColDefine(customType = "longtext")
|
||||
private String courseIntro;
|
||||
|
||||
@Column
|
||||
@Comment("适合人群")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String suitablePeople;
|
||||
|
||||
@Column
|
||||
@Comment("学习目标")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String learningGoal;
|
||||
|
||||
@Column
|
||||
@Comment("课程周期")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||
private String coursePeriod;
|
||||
|
||||
@Column
|
||||
@Comment("开课类型")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String openType;
|
||||
|
||||
@Column
|
||||
@Comment("开始时间")
|
||||
@ColDefine(type = ColType.DATETIME)
|
||||
private Date startTime;
|
||||
|
||||
@Column
|
||||
@Comment("结束时间")
|
||||
@ColDefine(type = ColType.DATETIME)
|
||||
private Date endTime;
|
||||
|
||||
@Column
|
||||
@Comment("课程状态")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String status;
|
||||
|
||||
@Column
|
||||
@Comment("推荐标识")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String recommendFlags;
|
||||
|
||||
@Column
|
||||
@Comment("课程标签")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String courseTags;
|
||||
|
||||
@Column
|
||||
@Comment("学习对象")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String targetType;
|
||||
|
||||
@Column
|
||||
@Comment("指定组织")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String targetOrgText;
|
||||
|
||||
@Column
|
||||
@Comment("排序编码")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer sortNum;
|
||||
|
||||
@Column
|
||||
@Comment("课程封面")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String cover;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.budwk.app.zhgh.learning.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
@Data
|
||||
@Table
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LearningCourseOutline extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("所属课程ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String courseId;
|
||||
|
||||
@Column
|
||||
@Comment("所属课程名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String courseName;
|
||||
|
||||
@Column
|
||||
@Comment("父节点ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String parentId;
|
||||
|
||||
@Column
|
||||
@Comment("节点类型")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String nodeType;
|
||||
|
||||
@Column
|
||||
@Comment("章/节标题")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String title;
|
||||
|
||||
@Column
|
||||
@Comment("副标题")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 200)
|
||||
private String subtitle;
|
||||
|
||||
@Column
|
||||
@Comment("简介")
|
||||
@ColDefine(customType = "longtext")
|
||||
private String description;
|
||||
|
||||
@Column
|
||||
@Comment("排序值")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer sortOrder;
|
||||
|
||||
@Column
|
||||
@Comment("是否必学")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean required;
|
||||
|
||||
@Column
|
||||
@Comment("状态")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.budwk.app.zhgh.learning.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
@Data
|
||||
@Table
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LearningCourseType extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("课程类型名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String typeName;
|
||||
|
||||
@Column
|
||||
@Comment("排序编号")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer sortNum;
|
||||
|
||||
@Column
|
||||
@Comment("是否启用")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean enabled;
|
||||
|
||||
@Column
|
||||
@Comment("备注")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.budwk.app.zhgh.learning.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
@Data
|
||||
@Table
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LearningOutlineResource extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("所属课程ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String courseId;
|
||||
|
||||
@Column
|
||||
@Comment("所属章/节ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String outlineId;
|
||||
|
||||
@Column
|
||||
@Comment("资料标题")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String resourceTitle;
|
||||
|
||||
@Column
|
||||
@Comment("资料类型")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String resourceType;
|
||||
|
||||
@Column
|
||||
@Comment("文件扩展名")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String fileExt;
|
||||
|
||||
@Column
|
||||
@Comment("附件")
|
||||
@ColDefine(customType = "longtext")
|
||||
private String fileData;
|
||||
|
||||
@Column
|
||||
@Comment("视频/音频时长")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer durationSeconds;
|
||||
|
||||
@Column
|
||||
@Comment("排序值")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer sortOrder;
|
||||
|
||||
@Column
|
||||
@Comment("是否必学")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean required;
|
||||
|
||||
@Column
|
||||
@Comment("是否允许预览")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean allowPreview;
|
||||
|
||||
@Column
|
||||
@Comment("是否允许下载")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean allowDownload;
|
||||
|
||||
@Column
|
||||
@Comment("状态")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.budwk.app.zhgh.learning.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.Index;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.entity.annotation.TableIndexes;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Table("learning_study_record")
|
||||
@TableIndexes({
|
||||
@Index(name = "idx_learning_record_user_course_outline", fields = {"userId", "courseId", "outlineId"}, unique = true),
|
||||
@Index(name = "idx_learning_record_union", fields = {"unionId"}, unique = false),
|
||||
@Index(name = "idx_learning_record_course", fields = {"courseId"}, unique = false)
|
||||
})
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LearningStudyRecord extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("学习人ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String userId;
|
||||
|
||||
@Column
|
||||
@Comment("工号")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 120)
|
||||
private String loginName;
|
||||
|
||||
@Column
|
||||
@Comment("姓名")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String userName;
|
||||
|
||||
@Column
|
||||
@Comment("所属分工会ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String unionId;
|
||||
|
||||
@Column
|
||||
@Comment("所属分工会")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String unionName;
|
||||
|
||||
@Column
|
||||
@Comment("课程ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String courseId;
|
||||
|
||||
@Column
|
||||
@Comment("课程名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String courseName;
|
||||
|
||||
@Column
|
||||
@Comment("章节ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String outlineId;
|
||||
|
||||
@Column
|
||||
@Comment("章节名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String outlineName;
|
||||
|
||||
@Column
|
||||
@Comment("第一次进入时间")
|
||||
@ColDefine(type = ColType.DATETIME)
|
||||
private Date firstStudyTime;
|
||||
|
||||
@Column
|
||||
@Comment("最近学习时间")
|
||||
@ColDefine(type = ColType.DATETIME)
|
||||
private Date latestStudyTime;
|
||||
|
||||
@Column
|
||||
@Comment("累计有效学习时长(秒)")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer studySeconds;
|
||||
|
||||
@Column
|
||||
@Comment("最近播放位置(秒)")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer lastPositionSeconds;
|
||||
|
||||
@Column
|
||||
@Comment("要求学习时长(秒)")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer requiredSeconds;
|
||||
|
||||
@Column
|
||||
@Comment("学习进度")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer progressPercent;
|
||||
|
||||
@Column
|
||||
@Comment("完成状态")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String completeStatus;
|
||||
|
||||
@Column
|
||||
@Comment("完成时间")
|
||||
@ColDefine(type = ColType.DATETIME)
|
||||
private Date completedAt;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.budwk.app.zhgh.learning.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
@Data
|
||||
@Table
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LearningStudyRule extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("所属课程ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String courseId;
|
||||
|
||||
@Column
|
||||
@Comment("规则对象类型")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String targetType;
|
||||
|
||||
@Column
|
||||
@Comment("规则对象ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String targetId;
|
||||
|
||||
@Column
|
||||
@Comment("是否必学")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean required;
|
||||
|
||||
@Column
|
||||
@Comment("学习方式")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String studyMode;
|
||||
|
||||
@Column
|
||||
@Comment("完成规则")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||
private String completionRule;
|
||||
|
||||
@Column
|
||||
@Comment("完成比例")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer completePercent;
|
||||
|
||||
@Column
|
||||
@Comment("最少学习时长")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer minStudySeconds;
|
||||
|
||||
@Column
|
||||
@Comment("解锁规则")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String unlockRule;
|
||||
|
||||
@Column
|
||||
@Comment("是否允许跳过")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean allowSkip;
|
||||
|
||||
@Column
|
||||
@Comment("是否允许拖动")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean allowDrag;
|
||||
|
||||
@Column
|
||||
@Comment("暂停是否计时")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean pauseCountTime;
|
||||
|
||||
@Column
|
||||
@Comment("页面隐藏是否计时")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean hiddenCountTime;
|
||||
|
||||
@Column
|
||||
@Comment("长时间无操作是否计时")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean inactiveCountTime;
|
||||
|
||||
@Column
|
||||
@Comment("状态")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.budwk.app.zhgh.learning.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.Index;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.entity.annotation.TableIndexes;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Table("learning_study_segment")
|
||||
@TableIndexes({
|
||||
@Index(name = "idx_learning_segment_record", fields = {"recordId"}, unique = false),
|
||||
@Index(name = "idx_learning_segment_user_state", fields = {"userId", "state"}, unique = false)
|
||||
})
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LearningStudySegment extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Name
|
||||
@Comment("ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("学习记录ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String recordId;
|
||||
|
||||
@Column
|
||||
@Comment("学习人ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String userId;
|
||||
|
||||
@Column
|
||||
@Comment("课程ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String courseId;
|
||||
|
||||
@Column
|
||||
@Comment("章节ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String outlineId;
|
||||
|
||||
@Column
|
||||
@Comment("资源ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String resourceId;
|
||||
|
||||
@Column
|
||||
@Comment("资源名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String resourceName;
|
||||
|
||||
@Column
|
||||
@Comment("开始时间")
|
||||
@ColDefine(type = ColType.DATETIME)
|
||||
private Date startTime;
|
||||
|
||||
@Column
|
||||
@Comment("结束时间")
|
||||
@ColDefine(type = ColType.DATETIME)
|
||||
private Date endTime;
|
||||
|
||||
@Column
|
||||
@Comment("最近心跳时间")
|
||||
@ColDefine(type = ColType.DATETIME)
|
||||
private Date lastHeartbeatTime;
|
||||
|
||||
@Column
|
||||
@Comment("本时段有效学习时长(秒)")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer activeSeconds;
|
||||
|
||||
@Column
|
||||
@Comment("状态")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String state;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.budwk.app.zhgh.learning.param;
|
||||
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class LearningStatisticsPageForm extends PageForm {
|
||||
|
||||
private String courseId;
|
||||
|
||||
private String courseName;
|
||||
|
||||
private String unionId;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
CREATE TABLE IF NOT EXISTS learning_course_outline (
|
||||
id VARCHAR(32) NOT NULL COMMENT 'ID',
|
||||
courseId VARCHAR(32) NULL COMMENT '所属课程ID',
|
||||
courseName VARCHAR(100) NULL COMMENT '所属课程名称',
|
||||
parentId VARCHAR(32) NULL COMMENT '父节点ID',
|
||||
nodeType VARCHAR(20) NULL COMMENT '节点类型',
|
||||
title VARCHAR(100) NULL COMMENT '章/节标题',
|
||||
subtitle VARCHAR(200) NULL COMMENT '副标题',
|
||||
description LONGTEXT NULL COMMENT '简介',
|
||||
sortOrder INT NULL COMMENT '排序值',
|
||||
required TINYINT(1) NULL COMMENT '是否必学',
|
||||
status VARCHAR(20) NULL COMMENT '状态',
|
||||
createdBy VARCHAR(32) NULL COMMENT '创建人',
|
||||
createdAt BIGINT NULL COMMENT '创建时间',
|
||||
updatedBy VARCHAR(32) NULL COMMENT '修改人',
|
||||
updatedAt BIGINT NULL COMMENT '修改时间',
|
||||
delFlag TINYINT(1) NULL COMMENT '删除标记',
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_learning_outline_course (courseId),
|
||||
INDEX idx_learning_outline_parent (parentId)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育课程大纲';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS learning_outline_resource (
|
||||
id VARCHAR(32) NOT NULL COMMENT 'ID',
|
||||
courseId VARCHAR(32) NULL COMMENT '所属课程ID',
|
||||
outlineId VARCHAR(32) NULL COMMENT '所属章/节ID',
|
||||
resourceTitle VARCHAR(100) NULL COMMENT '资料标题',
|
||||
resourceType VARCHAR(20) NULL COMMENT '资料类型',
|
||||
fileExt VARCHAR(20) NULL COMMENT '文件扩展名',
|
||||
fileData LONGTEXT NULL COMMENT '附件',
|
||||
durationSeconds INT NULL COMMENT '视频/音频时长',
|
||||
sortOrder INT NULL COMMENT '排序值',
|
||||
required TINYINT(1) NULL COMMENT '是否必学',
|
||||
allowPreview TINYINT(1) NULL COMMENT '是否允许预览',
|
||||
allowDownload TINYINT(1) NULL COMMENT '是否允许下载',
|
||||
status VARCHAR(20) NULL COMMENT '状态',
|
||||
createdBy VARCHAR(32) NULL COMMENT '创建人',
|
||||
createdAt BIGINT NULL COMMENT '创建时间',
|
||||
updatedBy VARCHAR(32) NULL COMMENT '修改人',
|
||||
updatedAt BIGINT NULL COMMENT '修改时间',
|
||||
delFlag TINYINT(1) NULL COMMENT '删除标记',
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_learning_resource_course (courseId),
|
||||
INDEX idx_learning_resource_outline (outlineId)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育大纲资料';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS learning_study_rule (
|
||||
id VARCHAR(32) NOT NULL COMMENT 'ID',
|
||||
courseId VARCHAR(32) NULL COMMENT '所属课程ID',
|
||||
targetType VARCHAR(20) NULL COMMENT '规则对象类型',
|
||||
targetId VARCHAR(32) NULL COMMENT '规则对象ID',
|
||||
required TINYINT(1) NULL COMMENT '是否必学',
|
||||
studyMode VARCHAR(20) NULL COMMENT '学习方式',
|
||||
completionRule VARCHAR(50) NULL COMMENT '完成规则',
|
||||
completePercent INT NULL COMMENT '完成比例',
|
||||
minStudySeconds INT NULL COMMENT '最少学习时长',
|
||||
unlockRule VARCHAR(20) NULL COMMENT '解锁规则',
|
||||
allowSkip TINYINT(1) NULL COMMENT '是否允许跳过',
|
||||
allowDrag TINYINT(1) NULL COMMENT '是否允许拖动',
|
||||
pauseCountTime TINYINT(1) NULL COMMENT '暂停是否计时',
|
||||
hiddenCountTime TINYINT(1) NULL COMMENT '页面隐藏是否计时',
|
||||
inactiveCountTime TINYINT(1) NULL COMMENT '长时间无操作是否计时',
|
||||
status VARCHAR(20) NULL COMMENT '状态',
|
||||
createdBy VARCHAR(32) NULL COMMENT '创建人',
|
||||
createdAt BIGINT NULL COMMENT '创建时间',
|
||||
updatedBy VARCHAR(32) NULL COMMENT '修改人',
|
||||
updatedAt BIGINT NULL COMMENT '修改时间',
|
||||
delFlag TINYINT(1) NULL COMMENT '删除标记',
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_learning_rule_target (targetType, targetId),
|
||||
INDEX idx_learning_rule_course (courseId)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育学习规则';
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE TABLE IF NOT EXISTS learning_course (
|
||||
id VARCHAR(32) NOT NULL COMMENT 'ID',
|
||||
courseName VARCHAR(100) NULL COMMENT '课程名称',
|
||||
courseTypeId VARCHAR(32) NULL COMMENT '课程类型',
|
||||
lecturerName VARCHAR(100) NULL COMMENT '授课讲师',
|
||||
lecturerInfo VARCHAR(500) NULL COMMENT '讲师基本信息',
|
||||
courseIntro LONGTEXT NULL COMMENT '课程简介',
|
||||
suitablePeople VARCHAR(500) NULL COMMENT '适合人群',
|
||||
learningGoal VARCHAR(500) NULL COMMENT '学习目标',
|
||||
coursePeriod VARCHAR(50) NULL COMMENT '课程周期',
|
||||
openType VARCHAR(20) NULL COMMENT '开课类型',
|
||||
startTime DATETIME NULL COMMENT '开始时间',
|
||||
endTime DATETIME NULL COMMENT '结束时间',
|
||||
status VARCHAR(20) NULL COMMENT '课程状态',
|
||||
recommendFlags VARCHAR(500) NULL COMMENT '推荐标识',
|
||||
courseTags VARCHAR(500) NULL COMMENT '课程标签',
|
||||
targetType VARCHAR(20) NULL COMMENT '学习对象',
|
||||
targetOrgText VARCHAR(500) NULL COMMENT '指定组织',
|
||||
sortNum INT NULL COMMENT '排序编码',
|
||||
cover VARCHAR(500) NULL COMMENT '课程封面',
|
||||
createdBy VARCHAR(32) NULL COMMENT '创建人',
|
||||
createdAt BIGINT NULL COMMENT '创建时间',
|
||||
updatedBy VARCHAR(32) NULL COMMENT '修改人',
|
||||
updatedAt BIGINT NULL COMMENT '修改时间',
|
||||
delFlag TINYINT(1) NULL COMMENT '删除标记',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育课程';
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE learning_course_type
|
||||
ADD COLUMN remark VARCHAR(500) NULL COMMENT '备注';
|
||||
@@ -0,0 +1,132 @@
|
||||
-- 学习教育移动端字典初始化。
|
||||
-- 移动端课程列表通过 /platform/learning/course/display/recommendOptions 读取“学习教育 -> 推荐标识”字典。
|
||||
|
||||
INSERT INTO `sys_dict` (
|
||||
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
|
||||
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
|
||||
)
|
||||
SELECT
|
||||
'a0f47f6a6d734c8b9c6ce5c0dfd81001',
|
||||
'',
|
||||
LPAD(IFNULL(MAX(CAST(`path` AS UNSIGNED)), 0) + 1, 4, '0'),
|
||||
'学习教育',
|
||||
'学习教育平台字典',
|
||||
'学习教育',
|
||||
0,
|
||||
990,
|
||||
1,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
0
|
||||
FROM `sys_dict`
|
||||
WHERE (`parentId` = '' OR `parentId` IS NULL)
|
||||
AND CHAR_LENGTH(`path`) = 4
|
||||
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = '学习教育' OR `name` = '学习教育') t);
|
||||
|
||||
INSERT INTO `sys_dict` (
|
||||
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
|
||||
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
|
||||
)
|
||||
SELECT
|
||||
'a0f47f6a6d734c8b9c6ce5c0dfd81002',
|
||||
p.id,
|
||||
CONCAT(p.`path`, '0001'),
|
||||
'推荐标识',
|
||||
'学习教育课程推荐标识',
|
||||
'推荐标识',
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
0
|
||||
FROM `sys_dict` p
|
||||
WHERE (p.`code` = '学习教育' OR p.`name` = '学习教育')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM (SELECT id FROM `sys_dict` WHERE `parentId` = p.id AND (`code` = '推荐标识' OR `name` = '推荐标识')) t
|
||||
);
|
||||
|
||||
INSERT INTO `sys_dict` (
|
||||
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
|
||||
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
|
||||
)
|
||||
SELECT
|
||||
'a0f47f6a6d734c8b9c6ce5c0dfd81003',
|
||||
p.id,
|
||||
CONCAT(p.`path`, '0001'),
|
||||
'推荐课程',
|
||||
'移动端课程列表推荐筛选项',
|
||||
'recommend',
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
0
|
||||
FROM `sys_dict` p
|
||||
JOIN `sys_dict` r ON r.id = p.`parentId`
|
||||
WHERE (r.`code` = '学习教育' OR r.`name` = '学习教育')
|
||||
AND (p.`code` = '推荐标识' OR p.`name` = '推荐标识')
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `parentId` = p.id AND `code` = 'recommend') t);
|
||||
|
||||
INSERT INTO `sys_dict` (
|
||||
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
|
||||
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
|
||||
)
|
||||
SELECT
|
||||
'a0f47f6a6d734c8b9c6ce5c0dfd81004',
|
||||
p.id,
|
||||
CONCAT(p.`path`, '0002'),
|
||||
'热门课程',
|
||||
'移动端课程列表热门筛选项',
|
||||
'hot',
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
0
|
||||
FROM `sys_dict` p
|
||||
JOIN `sys_dict` r ON r.id = p.`parentId`
|
||||
WHERE (r.`code` = '学习教育' OR r.`name` = '学习教育')
|
||||
AND (p.`code` = '推荐标识' OR p.`name` = '推荐标识')
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `parentId` = p.id AND `code` = 'hot') t);
|
||||
|
||||
INSERT INTO `sys_dict` (
|
||||
`id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`,
|
||||
`createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag`
|
||||
)
|
||||
SELECT
|
||||
'a0f47f6a6d734c8b9c6ce5c0dfd81005',
|
||||
p.id,
|
||||
CONCAT(p.`path`, '0003'),
|
||||
'最新课程',
|
||||
'移动端课程列表最新筛选项',
|
||||
'new',
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
0
|
||||
FROM `sys_dict` p
|
||||
JOIN `sys_dict` r ON r.id = p.`parentId`
|
||||
WHERE (r.`code` = '学习教育' OR r.`name` = '学习教育')
|
||||
AND (p.`code` = '推荐标识' OR p.`name` = '推荐标识')
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `parentId` = p.id AND `code` = 'new') t);
|
||||
|
||||
UPDATE `sys_dict`
|
||||
SET `hasChildren` = 1
|
||||
WHERE `code` IN ('学习教育', '推荐标识')
|
||||
OR `name` IN ('学习教育', '推荐标识');
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE learning_study_record
|
||||
ADD COLUMN lastPositionSeconds INT NULL COMMENT '最近播放位置(秒)' AFTER studySeconds;
|
||||
@@ -0,0 +1,52 @@
|
||||
CREATE TABLE IF NOT EXISTS learning_study_record (
|
||||
id VARCHAR(32) NOT NULL COMMENT 'ID',
|
||||
userId VARCHAR(32) NULL COMMENT '学习人ID',
|
||||
loginName VARCHAR(120) NULL COMMENT '工号',
|
||||
userName VARCHAR(100) NULL COMMENT '姓名',
|
||||
unionId VARCHAR(32) NULL COMMENT '所属分工会ID',
|
||||
unionName VARCHAR(100) NULL COMMENT '所属分工会',
|
||||
courseId VARCHAR(32) NULL COMMENT '课程ID',
|
||||
courseName VARCHAR(100) NULL COMMENT '课程名称',
|
||||
outlineId VARCHAR(32) NULL COMMENT '章节ID',
|
||||
outlineName VARCHAR(100) NULL COMMENT '章节名称',
|
||||
firstStudyTime DATETIME NULL COMMENT '第一次进入时间',
|
||||
latestStudyTime DATETIME NULL COMMENT '最近学习时间',
|
||||
studySeconds INT NULL COMMENT '累计有效学习时长(秒)',
|
||||
lastPositionSeconds INT NULL COMMENT '最近播放位置(秒)',
|
||||
requiredSeconds INT NULL COMMENT '要求学习时长(秒)',
|
||||
progressPercent INT NULL COMMENT '学习进度',
|
||||
completeStatus VARCHAR(20) NULL COMMENT '完成状态',
|
||||
completedAt DATETIME NULL COMMENT '完成时间',
|
||||
createdBy VARCHAR(32) NULL COMMENT '创建人',
|
||||
createdAt BIGINT NULL COMMENT '创建时间',
|
||||
updatedBy VARCHAR(32) NULL COMMENT '修改人',
|
||||
updatedAt BIGINT NULL COMMENT '修改时间',
|
||||
delFlag TINYINT(1) NULL COMMENT '删除标记',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY idx_learning_record_user_course_outline (userId, courseId, outlineId),
|
||||
INDEX idx_learning_record_union (unionId),
|
||||
INDEX idx_learning_record_course (courseId)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育学习记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS learning_study_segment (
|
||||
id VARCHAR(32) NOT NULL COMMENT 'ID',
|
||||
recordId VARCHAR(32) NULL COMMENT '学习记录ID',
|
||||
userId VARCHAR(32) NULL COMMENT '学习人ID',
|
||||
courseId VARCHAR(32) NULL COMMENT '课程ID',
|
||||
outlineId VARCHAR(32) NULL COMMENT '章节ID',
|
||||
resourceId VARCHAR(32) NULL COMMENT '资源ID',
|
||||
resourceName VARCHAR(100) NULL COMMENT '资源名称',
|
||||
startTime DATETIME NULL COMMENT '开始时间',
|
||||
endTime DATETIME NULL COMMENT '结束时间',
|
||||
lastHeartbeatTime DATETIME NULL COMMENT '最近心跳时间',
|
||||
activeSeconds INT NULL COMMENT '本时段有效学习时长(秒)',
|
||||
state VARCHAR(20) NULL COMMENT '状态',
|
||||
createdBy VARCHAR(32) NULL COMMENT '创建人',
|
||||
createdAt BIGINT NULL COMMENT '创建时间',
|
||||
updatedBy VARCHAR(32) NULL COMMENT '修改人',
|
||||
updatedAt BIGINT NULL COMMENT '修改时间',
|
||||
delFlag TINYINT(1) NULL COMMENT '删除标记',
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_learning_segment_record (recordId),
|
||||
INDEX idx_learning_segment_user_state (userId, state)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育学习时段';
|
||||
@@ -0,0 +1,105 @@
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT
|
||||
'a0f47f6a6d734c8b9c6ce5c0dfd80001',
|
||||
'',
|
||||
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
|
||||
'学习教育平台',
|
||||
'Learning',
|
||||
'menu',
|
||||
'',
|
||||
'',
|
||||
'ti-book',
|
||||
1,
|
||||
0,
|
||||
'learning',
|
||||
NULL,
|
||||
990,
|
||||
1,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
'',
|
||||
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||
0,
|
||||
'PC',
|
||||
NULL,
|
||||
NULL,
|
||||
'x',
|
||||
0,
|
||||
0
|
||||
FROM sys_menu
|
||||
WHERE (parentId = '' OR parentId IS NULL)
|
||||
AND CHAR_LENGTH(path) = 4
|
||||
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80002', p.id, CONCAT(p.path, '0001'), '课程类型设置', 'Course Type', 'menu', '/platform/learning/course/type', 'data-pjax', '', 1, 0, 'learning.course.type', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'k', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'learning'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.course.type') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80003', p.id, CONCAT(p.path, '0002'), '课程管理', 'Course Manage', 'menu', '/platform/learning/course/manage', 'data-pjax', '', 1, 0, 'learning.course.manage', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'k', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'learning'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.course.manage') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80004', p.id, CONCAT(p.path, '0003'), '章节内容管理', 'Chapter Content', 'menu', '/platform/learning/chapter/content', 'data-pjax', '', 1, 0, 'learning.chapter.content', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'z', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'learning'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.chapter.content') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80005', p.id, CONCAT(p.path, '0004'), '课程展示', 'Course Display', 'menu', '/platform/learning/course/display', 'data-pjax', '', 1, 0, 'learning.course.display', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'k', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'learning'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.course.display') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80006', p.id, CONCAT(p.path, '0005'), '学习活动管理', 'Activity Manage', 'menu', '/platform/learning/activity/manage', 'data-pjax', '', 1, 0, 'learning.activity.manage', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'learning'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.activity.manage') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80007', p.id, CONCAT(p.path, '0006'), '学习统计', 'Learning Statistics', 'menu', '/platform/learning/statistics', 'data-pjax', '', 1, 0, 'learning.statistics', NULL, 6, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'learning'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.statistics') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80008', p.id, CONCAT(p.path, '0007'), '我的学习记录', 'My Learning Record', 'menu', '/platform/learning/my/record', 'data-pjax', '', 1, 0, 'learning.my.record', NULL, 7, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'w', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'learning'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.my.record') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80009', p.id, CONCAT(p.path, '0008'), '我的课堂', 'My Course', 'menu', '/platform/learning/course/h5', 'data-pjax', '', 1, 0, 'h5.learning.course.display', NULL, 8, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'H5', NULL, NULL, 'w', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'learning'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'h5.learning.course.display') t);
|
||||
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||
SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80010', p.id, CONCAT(p.path, '0009'), '我的学习记录', 'My Learning Record H5', 'menu', '/platform/learning/my/record/h5', 'data-pjax', '', 1, 0, 'h5.learning.my.record', NULL, 9, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'H5', NULL, NULL, 'w', 0, 0
|
||||
FROM sys_menu p
|
||||
WHERE p.permission = 'learning'
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'h5.learning.my.record') t);
|
||||
|
||||
INSERT INTO sys_role_menu (roleId, menuId)
|
||||
SELECT r.id, m.id
|
||||
FROM sys_role r
|
||||
JOIN sys_menu m ON m.permission IN (
|
||||
'learning',
|
||||
'learning.course.type',
|
||||
'learning.course.manage',
|
||||
'learning.chapter.content',
|
||||
'learning.course.display',
|
||||
'learning.activity.manage',
|
||||
'learning.statistics',
|
||||
'learning.my.record',
|
||||
'h5.learning.course.display',
|
||||
'h5.learning.my.record'
|
||||
)
|
||||
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
||||
WHERE r.code = 'SYSADMIN'
|
||||
AND rm.roleId IS NULL;
|
||||
@@ -71,7 +71,7 @@
|
||||
<script src="https://vxeui.com/umd/xe-utils@3.5.30/dist/xe-utils.umd.min.js"></script>
|
||||
<script src="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/index.umd.min.js"></script>
|
||||
<script src="https://vxeui.com/umd/vxe-table@3.9.0/lib/index.umd.min.js"></script>
|
||||
l
|
||||
|
||||
<!-- 引入 form-create 和 designer -->
|
||||
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/form-create/index.umd.js"></script>
|
||||
@@ -80,11 +80,12 @@
|
||||
<script src="${base!}/assets/platform/plugins/swiper/swiper-bundle.js"></script>
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/swiper/swiper-bundle.min.css"></link>
|
||||
|
||||
<!-- <script src="${base!}/assets/platform/plugins/snaker/SnakerflowDesigner.umd.min.js"></script>-->
|
||||
<!--<script src="${base!}/assets/platform/plugins/snaker/SnakerflowDesigner.umd.js"></script>-->
|
||||
|
||||
<script src="${base!}/components/plugins/sysDict/DictData.js"></script>
|
||||
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
|
||||
<script src="${base!}/assets/platform/js/util/coordinateUtil.js"></script>
|
||||
<script src="${base!}/assets/platform/js/util/voiceMenuNavigator.js"></script>
|
||||
<script src="${base!}/assets/platform/js/main.js"></script>
|
||||
<script src="${base!}/assets/platform/js/mixin/initTableMixins.js"></script>
|
||||
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
|
||||
@@ -100,13 +101,32 @@
|
||||
|
||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: "4fa1e1aeabba7eb9518129cf57ab17c1"
|
||||
<script nonce="${cspNonce!}">
|
||||
// 在加载 lodash 后、使用前插入
|
||||
const originalDefaultsDeep = window._.defaultsDeep;
|
||||
window._.defaultsDeep = function(...args) {
|
||||
// 先对所有参数做原型污染清洗
|
||||
const cleanArgs = args.map(arg => sanitizeForPrototypePollution(arg));
|
||||
return originalDefaultsDeep.apply(window._, cleanArgs);
|
||||
};
|
||||
|
||||
function sanitizeForPrototypePollution(obj) {
|
||||
if (obj === null || typeof obj !== 'object') return obj;
|
||||
if (Array.isArray(obj)) return obj.map(sanitizeForPrototypePollution);
|
||||
|
||||
const clean = {};
|
||||
for (const key in obj) {
|
||||
if (!Object.hasOwn(obj, key)) continue;
|
||||
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
||||
continue;
|
||||
}
|
||||
clean[key] = sanitizeForPrototypePollution(obj[key]);
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
<script nonce="${cspNonce!}" type="text/javascript">
|
||||
Vue.config.devtools = false
|
||||
ELEMENT.locale(ELEMENT.lang["${lang,escape}"])
|
||||
ELEMENT.Dialog.props.closeOnClickModal.default = false
|
||||
@@ -123,12 +143,12 @@
|
||||
</script>
|
||||
|
||||
<!--广播频道-->
|
||||
<script type="text/javascript">
|
||||
<script nonce="${cspNonce!}" type="text/javascript">
|
||||
window.GlobalBroadcastChannel = new BroadcastChannel("zhgh-global-channel")
|
||||
</script>
|
||||
|
||||
<!--ws-->
|
||||
<script type="text/javascript">
|
||||
<script nonce="${cspNonce!}" type="text/javascript">
|
||||
class WebSocketPubSub {
|
||||
constructor() {
|
||||
this.ws = null
|
||||
@@ -150,6 +170,7 @@
|
||||
|
||||
connect() {
|
||||
const WS_URL = window.location.host + "${base}/websocket"
|
||||
// const WS_URL = 'zhgh.jshvc.edu.cn' + "${base}/websocket"
|
||||
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
|
||||
this.ws = new WebSocket(protocol + WS_URL)
|
||||
}
|
||||
@@ -162,13 +183,12 @@
|
||||
this.subscribers.get(type).add(callback)
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(this.ws.readyState)
|
||||
// 所有订阅完成后,发送 join 消息
|
||||
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.isSubscribed = true
|
||||
this.sendJoinMessage()
|
||||
}
|
||||
}, 2000)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
@@ -202,6 +222,7 @@
|
||||
// )
|
||||
// 连接建立时不立即发送 join,等待订阅完成
|
||||
this.ping()
|
||||
console.info('websocket open success')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +264,9 @@
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.sendJoinMessage()
|
||||
}
|
||||
}, 1000)
|
||||
}, 500)
|
||||
}
|
||||
}, 3000) // 延迟3秒重连
|
||||
}, 1000) // 延迟2秒重连
|
||||
}
|
||||
|
||||
// 窗口事件处理
|
||||
@@ -277,7 +298,7 @@
|
||||
</script>
|
||||
|
||||
<!--vuex-->
|
||||
<script type="text/javascript">
|
||||
<script nonce="${cspNonce!}" type="text/javascript">
|
||||
window.store = new Vuex.Store({
|
||||
plugins: [
|
||||
createPersistedState({
|
||||
@@ -303,11 +324,11 @@
|
||||
//设置用户信息
|
||||
setUser(state, payload) {
|
||||
state.user = payload
|
||||
state.room = payload.loginname + ":${@auth.getSessionId()}"
|
||||
}
|
||||
},
|
||||
actions: {}
|
||||
})
|
||||
|
||||
$.get("/platform/sys/user/getLogonUser").then((res) => {
|
||||
if (res.code === 0) {
|
||||
window.sessionStorage.setItem("user", JSON.stringify(res.data))
|
||||
@@ -317,13 +338,13 @@
|
||||
</script>
|
||||
|
||||
<!--beetl全局变量方法-->
|
||||
<script type="text/javascript">
|
||||
<script nonce="${cspNonce!}" type="text/javascript">
|
||||
const base = "${base!}"
|
||||
const APP_DOMAIN = "${AppDomain!}"
|
||||
</script>
|
||||
|
||||
<!--vue挂载-->
|
||||
<script type="text/javascript">
|
||||
<script nonce="${cspNonce!}" type="text/javascript">
|
||||
Vue.prototype.$moment = moment
|
||||
Vue.prototype.$businessTool = businessTool
|
||||
Vue.prototype.$commonUtil = commonUtil
|
||||
@@ -331,10 +352,20 @@
|
||||
Vue.prototype.$auth = commonUtil.authService()
|
||||
Vue.prototype.$axios = commonUtil.axiosService()
|
||||
Vue.prototype.$downLoad = commonUtil.downLoadService
|
||||
|
||||
Vue.prototype.$processStatusMap = {
|
||||
10: {text: "进行中", class: "doing"},
|
||||
20: {text: "已完成", class: "finished"},
|
||||
30: {text: "已撤回", class: "withdraw"},
|
||||
40: {text: "强行终止", class: "interrupt"},
|
||||
45: {text: "已拒绝", class: "reject"},
|
||||
50: {text: "挂起", class: "pending"},
|
||||
99: {text: "已废弃", class: "abandon"}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--vue全局组件-->
|
||||
<script>
|
||||
<script nonce="${cspNonce!}">
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
Vue.component("guava", httpVueLoader("/components/plugins/Guava.vue?v=" + new Date().getTime()))
|
||||
Vue.component("enum-tag", httpVueLoader("/components/plugins/sysEnum/EnumTag.vue?v=" + new Date().getTime()))
|
||||
@@ -511,6 +542,33 @@
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.v4-voice-menu {
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
padding: 0 14px;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
font: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.v4-voice-menu:hover,
|
||||
.v4-voice-menu.is-listening {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.v4-voice-menu.is-listening i {
|
||||
color: #ffdf6b;
|
||||
}
|
||||
|
||||
.v4-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -576,6 +634,10 @@
|
||||
.v4-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.voice-menu-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 页脚样式 */
|
||||
@@ -647,6 +709,10 @@
|
||||
</div>
|
||||
|
||||
<div class="v4-user-section">
|
||||
<button type="button" class="v4-voice-menu" id="voice-menu-btn" data-platform="PC" title="语音打开菜单">
|
||||
<i class="fa fa-microphone"></i>
|
||||
<span class="voice-menu-text">语音</span>
|
||||
</button>
|
||||
<div class="v4-user-info">
|
||||
<span class="v4-user-name">${@auth.getPrincipalProperty('username')} (${@auth.getPrincipalProperty('loginname')})</span>
|
||||
<!-- <i class="fa fa-angle-down"></i> -->
|
||||
@@ -657,7 +723,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<div style="height: 45px"></div>
|
||||
<div style="height: 64px"></div>
|
||||
|
||||
<main class="v4-content">
|
||||
<!-- 页面内容区域 -->
|
||||
@@ -679,12 +745,12 @@
|
||||
</footer>
|
||||
|
||||
|
||||
<script>
|
||||
<script nonce="${cspNonce!}">
|
||||
// 处理导航项的active状态
|
||||
$(document).ready(function () {
|
||||
// 控制页脚显示的函数
|
||||
function toggleFooter() {
|
||||
var currentPath = window.location.pathname
|
||||
var currentPath = getBaseSubAppPath()
|
||||
var footer = $("#v4-footer")
|
||||
|
||||
if (currentPath === "/platform/v4/home") {
|
||||
@@ -696,7 +762,7 @@
|
||||
|
||||
// 初始化:根据当前URL设置active状态
|
||||
function setActiveNavItem() {
|
||||
var currentPath = window.location.pathname
|
||||
var currentPath = getBaseSubAppPath()
|
||||
$(".v4-nav-item").removeClass("active")
|
||||
$(".v4-nav-item").each(function () {
|
||||
if ($(this).attr("href") === currentPath) {
|
||||
|
||||
@@ -1139,7 +1139,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
|
||||
.oa-grassroots-wrapper {
|
||||
width: 100%;
|
||||
background-image: url('https://cdgh.ncu.edu.cn/images/index_bg2.png');
|
||||
background-image: url(/assets/platform/img/v4/home-bg.png);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
@@ -1513,7 +1513,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
<div class="oa-hero-section">
|
||||
<!-- 背景图 -->
|
||||
<div class="oa-background-image">
|
||||
<img src="${config.AppHomeImg!}" alt=""/>
|
||||
<img :src="homeBannerList[0] || ''" alt=""/>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和统计区域 -->
|
||||
@@ -1999,7 +1999,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script nonce="${cspNonce!}">
|
||||
const vue = new Vue({
|
||||
el: "#v4-home-app",
|
||||
data() {
|
||||
@@ -2020,6 +2020,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
recommendServices: [],
|
||||
favoriteItems: [],
|
||||
recommendApps: [],
|
||||
homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item),
|
||||
|
||||
// 工会网站新闻
|
||||
websiteNews: {},
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
const act = {
|
||||
template: /*language=HTML*/ `
|
||||
<!-- 活动 -->
|
||||
<div class="section-wrapper">
|
||||
<div class="activity-section-wrapper">
|
||||
<div class="section-act">
|
||||
<!-- Swiper容器 -->
|
||||
|
||||
<div v-if="actList && actList.length > 0" class="activity-swiper-container">
|
||||
<div class="activity-swiper-container">
|
||||
<!-- 活动数据 - Swiper -->
|
||||
<div class="swiper activity-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
|
||||
<div class="item" @click="go(activity)">
|
||||
<div class="img-box">
|
||||
<img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
|
||||
alt="">
|
||||
<span class="status">
|
||||
|
||||
<div v-if="actList && actList.length > 0" class="activity-swiper-container">
|
||||
<div class="activity-swiper-container">
|
||||
<!-- 活动数据 - Swiper -->
|
||||
<div class="swiper activity-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
|
||||
<div class="item" @click="go(activity)">
|
||||
<div class="img-box">
|
||||
<img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
|
||||
alt="">
|
||||
<span class="status">
|
||||
{{getStatusText(activity)}}
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="title">
|
||||
{{ activity.name}}
|
||||
</h4>
|
||||
<!--报名时间-->
|
||||
<div class="time">
|
||||
<p>
|
||||
<i class="fa fa-clock-o"></i>
|
||||
时间:{{ activity.startDate }}至{{ activity.endDate }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航按钮 -->
|
||||
<div class="swiper-button-next"></div>
|
||||
<div class="swiper-button-prev"></div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="title">
|
||||
{{ activity.name}}
|
||||
</h4>
|
||||
<!--报名时间-->
|
||||
<div class="time">
|
||||
<p>
|
||||
<i class="fa fa-clock-o"></i>
|
||||
时间:{{ activity.startDate }}至{{ activity.endDate }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航按钮 -->
|
||||
<div class="swiper-button-next"></div>
|
||||
<div class="swiper-button-prev"></div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-activity-placeholder">
|
||||
<div class="icon">📅</div>
|
||||
<p>暂无近期活动</p>
|
||||
<p class="subtext">敬请关注后续通知</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-else class="no-activity-placeholder">
|
||||
<div class="icon">📅</div>
|
||||
<p>暂无近期活动</p>
|
||||
<p class="subtext">敬请关注后续通知</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
@@ -77,8 +77,8 @@ const act = {
|
||||
/*初始化Swiper*/
|
||||
initSwiper() {
|
||||
this.swiper = new Swiper('.activity-swiper', {
|
||||
slidesPerView: 'auto',
|
||||
spaceBetween: 30,
|
||||
slidesPerView: 1,
|
||||
spaceBetween: 18,
|
||||
centeredSlides: false,
|
||||
loop: false,
|
||||
navigation: {
|
||||
@@ -91,13 +91,13 @@ const act = {
|
||||
},
|
||||
breakpoints: {
|
||||
768: {
|
||||
slidesPerView: 1,
|
||||
},
|
||||
1024: {
|
||||
slidesPerView: 2,
|
||||
},
|
||||
1024: {
|
||||
slidesPerView: 4,
|
||||
},
|
||||
1200: {
|
||||
slidesPerView: 3,
|
||||
slidesPerView: 5,
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -119,6 +119,11 @@ const act = {
|
||||
this.$message.warning("管理员未配置活动链接")
|
||||
return
|
||||
}
|
||||
if (act.url === "/platform/activity/apply"
|
||||
&& !this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN", "BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_WENTI_SPORTS"])) {
|
||||
return this.$message.warning("PC端暂未开放报名,请前往移动端报名!")
|
||||
|
||||
}
|
||||
window.open(act.url)
|
||||
}
|
||||
},
|
||||
@@ -126,7 +131,7 @@ const act = {
|
||||
this.listAct()
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
.section-wrapper {
|
||||
.activity-section-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -171,7 +176,7 @@ const act = {
|
||||
.activity-swiper-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
padding: 9px 14px;
|
||||
/*margin-top: 30px;*/
|
||||
}
|
||||
|
||||
@@ -212,7 +217,7 @@ const act = {
|
||||
/* 进度条样式 */
|
||||
.activity-swiper-container .swiper-pagination {
|
||||
position: relative;
|
||||
margin-top: 30px;
|
||||
margin-top: 12px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
@@ -234,8 +239,8 @@ const act = {
|
||||
|
||||
.activity-swiper .swiper-slide .item .img-box {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-radius: 12px;
|
||||
height: 128px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
@@ -245,10 +250,12 @@ const act = {
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: #c11623;
|
||||
padding: 6px 2px;
|
||||
min-width: 50px;
|
||||
padding: 4px 2px;
|
||||
min-width: 42px;
|
||||
color: #ffffff;
|
||||
border-radius: 0 0 50% 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
||||
@@ -264,16 +271,16 @@ const act = {
|
||||
}
|
||||
|
||||
.activity-swiper .swiper-slide .item h4 {
|
||||
font-size: 20px;
|
||||
margin-top: 10px;
|
||||
height: 60px;
|
||||
font-size: 15px;
|
||||
margin: 8px 0 0;
|
||||
height: 42px;
|
||||
color: #333;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.5;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
@@ -287,14 +294,14 @@ const act = {
|
||||
}
|
||||
|
||||
.activity-swiper .swiper-slide .item .time p {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
/*line-height: 1.5;*/
|
||||
line-height: 1.4;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.activity-swiper .swiper-slide .item .time i {
|
||||
margin-right: 5px;
|
||||
margin-right: 4px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
const entry = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="entry-wrapper">
|
||||
<!-- 标签页导航 -->
|
||||
<div class="tabs">
|
||||
<div class="tab-item" :class="{ active: activeCategory === 'serv' }"
|
||||
@click="setActiveCategory('serv')">
|
||||
<i class="fa fa-star"></i> 推荐服务
|
||||
</div>
|
||||
<div class="tab-item" :class="{ active: activeCategory === 'app' }"
|
||||
@click="setActiveCategory('app')">
|
||||
<i class="fa fa-fire"></i> 推荐应用
|
||||
</div>
|
||||
<div class="tab-item" :class="{ active: activeCategory === 'fav' }"
|
||||
@click="setActiveCategory('fav')">
|
||||
<i class="fa fa-heart"></i> 我的收藏
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用块区域 -->
|
||||
<div class="apps-container">
|
||||
<div class="app-grid">
|
||||
<div class="app-item" v-for="(item, index) in currentServices" :key="index"
|
||||
@click="openService(item)">
|
||||
<div class="app-icon">
|
||||
<img :src="item.picIcon" alt="" />
|
||||
</div>
|
||||
<div class="app-name">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry-wrapper">
|
||||
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key">
|
||||
<div class="entry-section-header">
|
||||
<div class="entry-section-title">
|
||||
<i :class="section.icon"></i>
|
||||
<span>{{ section.title }}</span>
|
||||
<em>/Applications</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="apps-container">
|
||||
<div class="app-grid">
|
||||
<div class="app-item"
|
||||
v-for="(item, index) in section.list"
|
||||
:key="item.id || index"
|
||||
@click="openService(item, section.key)">
|
||||
<div class="app-icon">
|
||||
<img :src="item.picIcon" alt="" />
|
||||
</div>
|
||||
<div class="app-name">{{ item.name }}</div>
|
||||
</div>
|
||||
<el-empty class="app-empty"
|
||||
v-if="!section.list || section.list.length === 0"
|
||||
description="暂无数据"
|
||||
:image-size="72">
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
activeCategory: 'serv',
|
||||
hasMore: true,
|
||||
entries: {
|
||||
serv: [],
|
||||
@@ -43,8 +42,12 @@ const entry = {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentServices() {
|
||||
return this.entries[this.activeCategory] || [];
|
||||
entrySections() {
|
||||
return [
|
||||
{ key: 'serv', title: '推荐服务', icon: 'fa fa-star', list: this.entries.serv },
|
||||
{ key: 'app', title: '推荐应用', icon: 'fa fa-fire', list: this.entries.app },
|
||||
{ key: 'fav', title: '我的收藏', icon: 'fa fa-heart', list: this.entries.fav }
|
||||
];
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -53,10 +56,6 @@ const entry = {
|
||||
this.getFavorite()
|
||||
},
|
||||
methods: {
|
||||
setActiveCategory(category) {
|
||||
this.activeCategory = category;
|
||||
},
|
||||
|
||||
// 查询推荐服务
|
||||
async getRecommendService() {
|
||||
this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => {
|
||||
@@ -84,35 +83,37 @@ const entry = {
|
||||
})
|
||||
},
|
||||
|
||||
// 点击服务
|
||||
openService(service) {
|
||||
if(this.activeCategory === 'serv'){
|
||||
if(!service.href) this.$message.error('无效的链接地址')
|
||||
// 点击服务或应用
|
||||
openService(service, category) {
|
||||
if (category === 'serv') {
|
||||
if (!service.href) {
|
||||
this.$message.error('无效的链接地址')
|
||||
return
|
||||
}
|
||||
window.open(service.href)
|
||||
return
|
||||
}
|
||||
|
||||
if(this.activeCategory === 'app'){
|
||||
// 储存到缓存
|
||||
if (category === 'app') {
|
||||
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(service))
|
||||
// 新标签页打开
|
||||
window.open("/platform/v4/subApp?appId=" + service.id, "_blank")
|
||||
return
|
||||
}
|
||||
|
||||
if(this.activeCategory === 'fav'){
|
||||
if(!service.href) this.$message.error('无效的链接地址')
|
||||
window.open(service.href)
|
||||
return
|
||||
|
||||
if(!service.parentId){
|
||||
// 储存到缓存
|
||||
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(app))
|
||||
// 新标签页打开
|
||||
window.open("/platform/v4/subApp?appId=" + app.id, "_blank")
|
||||
if (category === 'fav') {
|
||||
if (service.href) {
|
||||
window.open(service.href)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!service.parentId) {
|
||||
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(service))
|
||||
window.open("/platform/v4/subApp?appId=" + service.id, "_blank")
|
||||
return
|
||||
}
|
||||
|
||||
this.$message.error('无效的链接地址')
|
||||
}
|
||||
}
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
@@ -121,37 +122,41 @@ const entry = {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
padding-bottom: 10px;
|
||||
.entry-section + .entry-section {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
/* 临时屏蔽“我的收藏”分组,保留原节点和数据逻辑便于恢复 */
|
||||
.entry-section-fav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.entry-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.entry-section-title {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
color: #19324d;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.entry-section-title i {
|
||||
color: #409eff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.entry-section-title em {
|
||||
color: #b5c0d6;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding-bottom: 8px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #1890ff;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #1890ff;
|
||||
}
|
||||
|
||||
.tab-item i {
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.apps-container {
|
||||
@@ -162,19 +167,17 @@ const entry = {
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
/* 改为自适应列数:每格最小 80px,自动换行 */
|
||||
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||
|
||||
justify-items: center; /* 子项内容居中(图标和文字) */
|
||||
justify-content: start; /* 整体网格靠左对齐,避免居中 */
|
||||
|
||||
gap: 4px;
|
||||
padding: 12px 4px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||
justify-items: center;
|
||||
justify-content: start;
|
||||
gap: 12px 18px;
|
||||
min-height: 116px;
|
||||
padding: 18px 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
overflow-x: hidden;
|
||||
|
||||
max-height: calc(350px - 74px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.app-item {
|
||||
@@ -187,6 +190,7 @@ const entry = {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -208,6 +212,7 @@ const entry = {
|
||||
}
|
||||
|
||||
.app-name {
|
||||
min-height: 34px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
@@ -215,31 +220,26 @@ const entry = {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
.app-empty {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tabs {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 8px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
grid-template-columns: repeat(3, minmax(72px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 14px 10px;
|
||||
}
|
||||
|
||||
.app-item {
|
||||
padding: 12px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.app-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, minmax(72px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
|
||||
.section-banner .banner-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.section-banner .banner-img img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.section-wrapper {
|
||||
@@ -28,7 +28,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
width: 80%;
|
||||
margin: 20px auto;
|
||||
display: grid;
|
||||
grid-template-columns: 2.2fr 0.8fr;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
row-gap: 24px;
|
||||
@@ -38,16 +38,16 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
display: flex;
|
||||
/*align-items: center;*/
|
||||
min-width: 320px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
padding: 16px;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.entry-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.act-card {
|
||||
@@ -55,43 +55,89 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
align-items: center;
|
||||
min-width: 320px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
padding: 11px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.home-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.home-section-title {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
color: #19324d;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.home-section-title i {
|
||||
color: #409eff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.home-section-title em {
|
||||
color: #b5c0d6;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="v4-container" id="v4-home-app">
|
||||
<!-- 首页banner -->
|
||||
<div class="section-banner">
|
||||
<div class="banner-img">
|
||||
<img src="${config.AppHomeImg!}" alt=""/>
|
||||
<img :src="homeBannerList[0] || ''" alt=""/>
|
||||
<stats></stats>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<div class="" style="padding-bottom: 20px">
|
||||
<user></user>
|
||||
|
||||
<div class="home-grid">
|
||||
<div class="act-card">
|
||||
<act></act>
|
||||
</div>
|
||||
|
||||
<div class="entry-card">
|
||||
<entry></entry>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<jcdt :list="websiteNews.grassroots"></jcdt>
|
||||
<div class="home-grid">
|
||||
<div class="home-section">
|
||||
<div class="home-section-header">
|
||||
<div class="home-section-title">
|
||||
<i class="fa fa-calendar"></i>
|
||||
<span>最新活动</span>
|
||||
<em>/Activities</em>
|
||||
</div>
|
||||
</div>
|
||||
<div class="act-card">
|
||||
<act></act>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
|
||||
|
||||
<jcdt :list="websiteNews"></jcdt>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script nonce="${cspNonce!}">
|
||||
<!--#include("act.js"){}#-->
|
||||
<!--#include("template.js"){}#-->
|
||||
<!--#include("jcdt.js"){}#-->
|
||||
<!--#include("entry.js"){}#-->
|
||||
<!--#include("user.js"){}#-->
|
||||
@@ -102,13 +148,13 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
el: '#v4-home-app',
|
||||
data(){
|
||||
return{
|
||||
websiteNews:{
|
||||
grassroots:[],
|
||||
}
|
||||
websiteNews: [],
|
||||
homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'act': act,
|
||||
'work-template': workTemplate,
|
||||
'jcdt': jcdt,
|
||||
'entry': entry,
|
||||
'user': user,
|
||||
|
||||
@@ -2,22 +2,29 @@ const jcdt = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="jcdt-wrapper">
|
||||
<div class="jcdt-container">
|
||||
<!-- <div class="jcdt-title">-->
|
||||
<!-- <span>基层动态</span>-->
|
||||
<!-- </div> -->
|
||||
<div class="jcdt-content">
|
||||
<div class="jcdt-list">
|
||||
<div class="jcdt-item" v-for="item in list" :key="item.title" @click="onLink(item)">
|
||||
<div class="date" v-if="item.date">{{item.date}}</div>
|
||||
<div class="title">{{item.title}}</div>
|
||||
<div class="image">
|
||||
<img v-if="item.image" :src="item.image" alt="">
|
||||
<img v-else src="/assets/platform/img/v4/not-image.png" alt="">
|
||||
</div>
|
||||
<div class="summary">{{item.summary}}</div>
|
||||
<el-carousel height="360px" :interval="10000" indicator-position="none">
|
||||
<el-carousel-item v-for="item,index in list" :key="index">
|
||||
<div class="jcdt-title">
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jcdt-content">
|
||||
<div class="jcdt-list">
|
||||
<div class="jcdt-item" v-for="o,i in item.value" :key="i" @click="onLink(o)">
|
||||
<div class="date" v-if="o.time">
|
||||
<i class="el-icon-alarm-clock"></i>
|
||||
<span>{{o.time}}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div class="title">{{o.text}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
@@ -32,9 +39,12 @@ const jcdt = {
|
||||
},
|
||||
methods: {
|
||||
onLink(item) {
|
||||
if(!item.url) return
|
||||
window.open(item.url)
|
||||
const url = this.isHttpOrHttps(item.href) ? item.href : "https://xgh.cug.edu.cn/" + item.href
|
||||
window.open(url)
|
||||
},
|
||||
isHttpOrHttps(url) {
|
||||
return /^(http:\/\/|https:\/\/)/i.test(url)
|
||||
}
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
.jcdt-wrapper {
|
||||
@@ -47,16 +57,14 @@ const jcdt = {
|
||||
.jcdt-container {
|
||||
width: 80%;
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
|
||||
margin: 0 auto 10px;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.jcdt-title{
|
||||
/deep/ .jcdt-title{
|
||||
font-size: 30px;
|
||||
color: #000000;
|
||||
display: block;
|
||||
@@ -87,14 +95,14 @@ const jcdt = {
|
||||
margin-left: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.jcdt-list {
|
||||
|
||||
/deep/ .jcdt-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.jcdt-item {
|
||||
/deep/ .jcdt-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
@@ -103,18 +111,17 @@ const jcdt = {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.jcdt-item:hover{
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
border-bottom-color: var(--color-primary);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.jcdt-item .date {
|
||||
/deep/ .jcdt-item:hover{
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/deep/ .jcdt-item .date {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
@@ -123,19 +130,19 @@ const jcdt = {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.jcdt-item .title {
|
||||
/deep/ .jcdt-item .title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
/*margin-bottom: 12px;*/
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
height: 45px;
|
||||
/*height: 45px;*/
|
||||
}
|
||||
|
||||
.jcdt-item .image {
|
||||
/deep/ .jcdt-item .image {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
margin: 0 0 16px 0;
|
||||
@@ -149,14 +156,14 @@ const jcdt = {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.jcdt-item .image img {
|
||||
/deep/ .jcdt-item .image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.jcdt-item .summary {
|
||||
/deep/ .jcdt-item .summary {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
@@ -168,5 +175,8 @@ const jcdt = {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/deep/ .jcdt-item i {
|
||||
color: #007aff;
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
@@ -7,66 +7,58 @@ const stats = {
|
||||
<div class="stats-grid">
|
||||
<!-- 待办 -->
|
||||
<div
|
||||
class="stat-item pending"
|
||||
@click="handleStatClick('todo')"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
class="stat-item pending"
|
||||
@click="handleStatClick('todo')"
|
||||
>
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">待办</div>
|
||||
<div class="stat-number">{{ stats.todoCount }}</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已办 -->
|
||||
<div
|
||||
class="stat-item completed"
|
||||
@click="handleStatClick('done')"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
class="stat-item completed"
|
||||
@click="handleStatClick('done')"
|
||||
>
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已办</div>
|
||||
<div class="stat-number">{{ stats.doneCount }}</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息 -->
|
||||
<div
|
||||
class="stat-item messages"
|
||||
@click="handleStatClick('notification')"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
class="stat-item messages"
|
||||
@click="handleStatClick('notification')"
|
||||
>
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-bell"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">消息</div>
|
||||
<div class="stat-number">{{ stats.notifications }}</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-bell"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发起 -->
|
||||
<div
|
||||
class="stat-item reminders"
|
||||
@click="handleStatClick('started')"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
class="stat-item reminders"
|
||||
@click="handleStatClick('started')"
|
||||
>
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-file-text"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">发起</div>
|
||||
<div class="stat-number">{{ stats.startedCount }}</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-file-text"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,15 +82,6 @@ const stats = {
|
||||
}
|
||||
},
|
||||
|
||||
onMouseEnter(event) {
|
||||
event.target.style.transform = 'translateY(-2px)';
|
||||
event.target.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
||||
},
|
||||
onMouseLeave(event) {
|
||||
event.target.style.transform = 'none';
|
||||
event.target.style.boxShadow = '0 2px 6px rgba(0,0,0,0.08)';
|
||||
},
|
||||
|
||||
loadStats() {
|
||||
this.$axios.post("/flow/todoCenter/statistics").then(res => {
|
||||
if (res.code === 0) {
|
||||
@@ -122,90 +105,128 @@ const stats = {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: -130px auto 0;
|
||||
margin: 20px auto 0;
|
||||
/*border: 1px solid #e8e8e8;*/
|
||||
}
|
||||
|
||||
/* 子内容临时屏蔽时,同步收起统计容器,避免首页 banner 下方出现空白 */
|
||||
.stats-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
width: 32%;
|
||||
min-width: 360px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 临时屏蔽首页右上角“待办、已办、消息、发起”统计卡片,保留原模板便于恢复 */
|
||||
.stats-grid {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: #f8f9fa;
|
||||
color: #495057;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #1f2937;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
width: 200px;
|
||||
transition: background 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12);
|
||||
width: 100%;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
border-color: rgba(255, 255, 255, 0.55);
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 30px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
border-radius: 50%;
|
||||
font-size: 26px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
color: rgba(31, 41, 55, 0.72);
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-item.pending .stat-icon,
|
||||
.stat-item.completed .stat-icon,
|
||||
.stat-item.messages .stat-icon,
|
||||
.stat-item.reminders .stat-icon {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
}
|
||||
|
||||
.stat-item.pending .stat-icon {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.stat-item.completed .stat-icon {
|
||||
color: #52c41a;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.stat-item.messages .stat-icon {
|
||||
color: #faad14;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.stat-item.reminders .stat-icon {
|
||||
color: #13c2c2;
|
||||
color: #0891b2;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 10px;
|
||||
margin-left: 0;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 15px;
|
||||
opacity: 0.7;
|
||||
font-size: 14px;
|
||||
color: rgba(31, 41, 55, 0.78);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-top: 2px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-top: 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stats-family {
|
||||
width: 68%;
|
||||
}
|
||||
|
||||
|
||||
.stats-family img {
|
||||
height: 190px !important;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
/* 临时屏蔽“欢迎来到职工之家”图片,保留原节点便于恢复 */
|
||||
.stats-family {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
@@ -68,10 +68,10 @@ const user = {
|
||||
<div class="header">
|
||||
<h3>待办中心</h3>
|
||||
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="待办事宜" name="todo"></el-tab-pane>
|
||||
<el-tab-pane label="已办事宜" name="done"></el-tab-pane>
|
||||
<el-tab-pane label="我的发起" name="started"></el-tab-pane>
|
||||
<el-tab-pane label="办结事务" name="completed"></el-tab-pane>
|
||||
<el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane>
|
||||
<el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane>
|
||||
<el-tab-pane :label="tabLabel('我的发起', taskStats.startedCount)" name="started"></el-tab-pane>
|
||||
<el-tab-pane :label="tabLabel('办结事务', taskStats.completedCount)" name="completed"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
@@ -97,6 +97,7 @@ const user = {
|
||||
<el-table-column prop="sender" label="申请人" width="120">
|
||||
<template slot-scope="{row}">{{row.variable.initiatorName}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="taskName" label="当前流程节点" width="140" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="date" label="发起日期" width="120">
|
||||
<template slot-scope="{row}">
|
||||
{{$moment(row.createdAt).format('YYYY-MM-DD')}}
|
||||
@@ -108,6 +109,7 @@ const user = {
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,6 +125,12 @@ const user = {
|
||||
pageSize: 5,
|
||||
totalCount: 0
|
||||
},
|
||||
taskStats: {
|
||||
todoCount: 0,
|
||||
doneCount: 0,
|
||||
startedCount: 0,
|
||||
completedCount: 0
|
||||
},
|
||||
// 🔆 天气相关数据
|
||||
weatherData: null,
|
||||
weatherError: false,
|
||||
@@ -181,9 +189,10 @@ const user = {
|
||||
|
||||
return info;
|
||||
},
|
||||
async getConfigKey(key) {
|
||||
const resp = await this.$axios.post("/open/common/getConfigKey", { key })
|
||||
return resp.data
|
||||
getConfigKey(key) {
|
||||
return this.$axios.post("/open/common/getConfigKey", { key }).then((resp) => {
|
||||
return resp.data
|
||||
})
|
||||
},
|
||||
// 🔆 获取天气
|
||||
async fetchWeather() {
|
||||
@@ -191,6 +200,9 @@ const user = {
|
||||
// 存localStorage 112.13,32.01 广州 113.27,23.14
|
||||
const AppMapCenterPointX = await this.getConfigKey("AppMapCenterPointX")
|
||||
const AppMapCenterPointY = await this.getConfigKey("AppMapCenterPointY")
|
||||
if (!AppMapCenterPointX || !AppMapCenterPointY){
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 先通过城市名获取 locationId
|
||||
this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/geo/v2/city/lookup?location=" + AppMapCenterPointX + "," + AppMapCenterPointY + "&key=" + this.QWEATHER_KEY).then((geoRes) => {
|
||||
@@ -224,6 +236,19 @@ const user = {
|
||||
}
|
||||
},
|
||||
// 获取任务列表
|
||||
tabLabel(title, count) {
|
||||
return title + "(" + (count || 0) + ")"
|
||||
},
|
||||
async getStatistics() {
|
||||
try {
|
||||
const res = await $.post("/flow/todoCenter/statistics")
|
||||
if (res.code === 0) {
|
||||
this.taskStats = Object.assign({}, this.taskStats, res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取待办统计失败:", error)
|
||||
}
|
||||
},
|
||||
async getTasks() {
|
||||
this.loading = true
|
||||
try {
|
||||
@@ -241,6 +266,7 @@ const user = {
|
||||
},
|
||||
pageData() {
|
||||
this.getTasks()
|
||||
this.getStatistics()
|
||||
},
|
||||
handleTabClick(tab) {
|
||||
this.pageData()
|
||||
@@ -254,7 +280,7 @@ const user = {
|
||||
this.$message.warning("当前流程没有配置地址")
|
||||
return
|
||||
}
|
||||
window.open(formKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey)
|
||||
window.open(formKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
|
||||
},
|
||||
getRoleNames() {
|
||||
$.post("/platform/sys/role/getRoleNames").then(res => {
|
||||
@@ -280,7 +306,7 @@ const user = {
|
||||
|
||||
.user-container {
|
||||
width: 80%;
|
||||
margin: 20px auto;
|
||||
margin: 12px auto 20px;
|
||||
display: grid;
|
||||
grid-template-columns: 0.7fr 2.3fr;
|
||||
gap: 24px;
|
||||
|
||||
@@ -333,7 +333,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
methods: {
|
||||
async loadStats() {
|
||||
try {
|
||||
const resp = await this.$axios.get('/platform/v4/msg/stats')
|
||||
const resp = await Promise.resolve(this.$axios.get('/platform/v4/msg/stats'))
|
||||
if (resp.code === 0) {
|
||||
this.stats = resp.data
|
||||
}
|
||||
@@ -345,7 +345,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
async loadMessages() {
|
||||
this.loading = true
|
||||
try {
|
||||
const resp = await this.$axios.post('/platform/v4/msg/pageData', this.pageForm)
|
||||
const resp = await Promise.resolve(this.$axios.post('/platform/v4/msg/pageData', this.pageForm))
|
||||
if (resp.code === 0) {
|
||||
this.messages = resp.data.list
|
||||
this.pageForm.totalCount = resp.data.totalCount
|
||||
@@ -360,7 +360,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
},
|
||||
|
||||
async viewMessage(message) {
|
||||
this.$axios.post(`/platform/v4/msg/detail/` + message.id).then(async res => {
|
||||
this.$axios.post('/platform/v4/msg/detail/' + message.id).then(async res => {
|
||||
if (res.code === 0) {
|
||||
this.currentMessage = res.data
|
||||
this.detailVisible = true
|
||||
@@ -376,7 +376,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
|
||||
async markAsRead(messageId) {
|
||||
try {
|
||||
await this.$axios.post(`/platform/v4/msg/read/` + messageId)
|
||||
await Promise.resolve(this.$axios.post('/platform/v4/msg/read/' + messageId))
|
||||
} catch (error) {
|
||||
console.error('标记已读失败:', error)
|
||||
}
|
||||
@@ -390,7 +390,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
}).then(async () => {
|
||||
this.loading = true
|
||||
try {
|
||||
const resp = await this.$axios.post('/platform/v4/msg/read/all')
|
||||
const resp = await Promise.resolve(this.$axios.post('/platform/v4/msg/read/all'))
|
||||
if (resp.code === 0) {
|
||||
this.$message.success('操作成功')
|
||||
this.loadMessages()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">学习活动管理模块待完善</el-card>
|
||||
</div>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,883 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
.learning-outline-layout {
|
||||
height: calc(100vh - 235px);
|
||||
}
|
||||
.learning-outline-tree {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.outline-context-menu {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
padding: 5px 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
.outline-context-menu li {
|
||||
min-width: 120px;
|
||||
padding: 7px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.outline-context-menu li:hover {
|
||||
background: #f2f6fc;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="课程名称">
|
||||
<el-select
|
||||
v-model="currentCourseId"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="courseNameRemoteSearch"
|
||||
placeholder="请选择或搜索课程名称"
|
||||
style="width: 100%"
|
||||
@change="courseChange"
|
||||
@clear="courseClear">
|
||||
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="课程类型">
|
||||
<el-select v-model="courseQuery.courseTypeId" clearable filterable placeholder="请选择课程类型" style="width: 100%">
|
||||
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="授课讲师">
|
||||
<el-input v-model="courseQuery.lecturerName" clearable placeholder="请输入授课讲师" style="width: 100%" @keyup.enter.native="loadCourses"></el-input>
|
||||
</search-item>
|
||||
<search-item label="课程状态">
|
||||
<el-select v-model="courseQuery.status" clearable placeholder="请选择课程状态" style="width: 100%">
|
||||
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="loadCourses">搜索</el-button>
|
||||
<el-button size="medium" @click="resetCourseQuery">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="10" class="mt10 learning-outline-layout">
|
||||
<el-col :span="6" style="height: 100%">
|
||||
<el-card shadow="never" style="height: 100%">
|
||||
<table-tool label="课程大纲">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openNodeForm('chapter')">新增章</el-button>
|
||||
</table-tool>
|
||||
<div class="learning-outline-tree">
|
||||
<el-tree
|
||||
ref="outlineTree"
|
||||
:data="outlineTreeData"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:expand-on-click-node="false"
|
||||
:props="{children:'children', label:'title'}"
|
||||
@node-click="nodeClick"
|
||||
@node-contextmenu="openContextMenu">
|
||||
<span slot-scope="{ node, data }">
|
||||
<i :class="data.nodeType === 'course' ? 'el-icon-collection' : (data.nodeType === 'chapter' ? 'el-icon-folder' : 'el-icon-document')"></i>
|
||||
<span>{{data.title}}</span>
|
||||
<el-tag v-if="data.status === 'disabled'" size="mini" type="info" style="margin-left: 6px">禁用</el-tag>
|
||||
<el-tag v-if="data.required" size="mini" type="warning" style="margin-left: 6px">必学</el-tag>
|
||||
</span>
|
||||
</el-tree>
|
||||
<el-empty v-if="treeData.length === 0" description="暂无课程大纲"></el-empty>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="18" style="height: 100%">
|
||||
<el-card shadow="never" style="height: 100%; overflow: auto">
|
||||
<el-empty v-if="!selectedNode.id" description="请选择左侧章/节节点"></el-empty>
|
||||
<el-tabs v-else v-model="activeTab">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form :model="nodeForm" ref="nodeForm" label-width="110px" :rules="nodeRules">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="节点类型">
|
||||
<el-tag :type="nodeForm.nodeType === 'chapter' ? 'primary' : 'success'">{{nodeForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-input-number v-model="nodeForm.sortOrder" :controls="false" :min="1" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="nodeForm.title" maxlength="100" placeholder="请输入章/节标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题" prop="subtitle">
|
||||
<el-input v-model="nodeForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="简介" prop="description">
|
||||
<el-input v-model="nodeForm.description" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入章/节简介"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveNodeBasic">保存基础信息</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="学习资料" name="resource">
|
||||
<table-tool label="学习资料">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openResourceForm()">新增资料</el-button>
|
||||
</table-tool>
|
||||
<el-table :data="resourceData" border @sort-change="resourcePageOrder">
|
||||
<el-table-column label="序号" type="index" width="70" :index="resourceIndex"></el-table-column>
|
||||
<el-table-column label="资料标题" prop="resourceTitle" sortable="custom" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="资料类型" prop="resourceType" sortable="custom" width="110">
|
||||
<template slot-scope="{row}">{{getResourceTypeName(row.resourceType)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件格式" prop="fileExt" sortable="custom" width="110"></el-table-column>
|
||||
<el-table-column label="学习时长(分钟)" prop="durationSeconds" sortable="custom" width="140">
|
||||
<template slot-scope="{row}">{{formatDurationMinutes(row.durationSeconds)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sortOrder" sortable="custom" width="90"></el-table-column>
|
||||
<el-table-column label="必学" prop="required" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="row.required ? 'warning' : 'info'" size="mini">{{row.required ? '是' : '否'}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预览" prop="allowPreview" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">{{row.allowPreview ? '允许' : '不允许'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下载" prop="allowDownload" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">{{row.allowDownload ? '允许' : '不允许'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="row.status === 'enabled' ? 'success' : 'info'" size="mini">{{row.status === 'enabled' ? '启用' : '禁用'}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openResourceForm(row)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="deleteResource(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="mt20"
|
||||
@size-change="resourceSizeChange"
|
||||
@current-change="resourceNumberChange"
|
||||
:current-page="resourcePageForm.pageNumber"
|
||||
:page-sizes="[5,10,20,30,50]"
|
||||
:page-size="resourcePageForm.pageSize"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="resourcePageForm.totalCount">
|
||||
</el-pagination>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="完成规则" name="rule">
|
||||
<el-form :model="ruleForm" label-width="150px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否必学">
|
||||
<el-switch v-model="ruleForm.required"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学习方式">
|
||||
<el-select v-model="ruleForm.studyMode" style="width: 100%">
|
||||
<el-option v-for="item in studyModeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="完成规则">
|
||||
<el-select v-model="ruleForm.completionRule" style="width: 100%">
|
||||
<el-option v-for="item in completionRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="完成比例">
|
||||
<el-input-number v-model="ruleForm.completePercent" :min="0" :max="100" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最少学习时长(分钟)">
|
||||
<el-input-number v-model="ruleForm.minStudyMinutes" :min="0" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="解锁规则">
|
||||
<el-select v-model="ruleForm.unlockRule" style="width: 100%">
|
||||
<el-option v-for="item in unlockRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="允许跳过"><el-switch v-model="ruleForm.allowSkip"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="允许拖动"><el-switch v-model="ruleForm.allowDrag"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="暂停计时"><el-switch v-model="ruleForm.pauseCountTime"></el-switch></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="隐藏计时"><el-switch v-model="ruleForm.hiddenCountTime"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="无操作计时"><el-switch v-model="ruleForm.inactiveCountTime"></el-switch></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveRule">保存完成规则</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="状态设置" name="status">
|
||||
<el-form :model="nodeForm" label-width="110px">
|
||||
<el-form-item label="节点状态">
|
||||
<el-radio-group v-model="nodeForm.status">
|
||||
<el-radio label="enabled">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必学">
|
||||
<el-switch v-model="nodeForm.required"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveNodeBasic">保存状态</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<ul v-show="contextVisible" class="outline-context-menu" :style="{left: contextLeft + 'px', top: contextTop + 'px'}">
|
||||
<li @click="openNodeForm('chapter')">新增章</li>
|
||||
<li v-if="contextNode.nodeType === 'chapter'" @click="openNodeForm('section', contextNode)">新增节</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="openNodeForm(contextNode.nodeType, contextNode)">编辑</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="deleteNode(contextNode)">删除</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="toggleNode(contextNode)">{{contextNode.status === 'enabled' ? '禁用' : '启用'}}</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'up')">上移</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'down')">下移</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="openResourceForm(null, contextNode)">上传资料</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="setRequired(contextNode)">设置必学</li>
|
||||
</ul>
|
||||
|
||||
<el-dialog :title="nodeDialogTitle" :visible.sync="nodeDialogVisible" :close-on-click-modal="false" width="45%">
|
||||
<el-form :model="nodeDialogForm" ref="nodeDialogForm" label-width="100px" :rules="nodeRules">
|
||||
<el-form-item label="节点类型">
|
||||
<el-tag>{{nodeDialogForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="上级节点">
|
||||
<el-input v-model="nodeDialogForm.parentName" disabled placeholder="上级节点"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="nodeDialogForm.title" maxlength="100" placeholder="请输入标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题">
|
||||
<el-input v-model="nodeDialogForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="简介">
|
||||
<el-input v-model="nodeDialogForm.description" type="textarea" :rows="4" maxlength="2000" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值">
|
||||
<el-input-number v-model="nodeDialogForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必学">
|
||||
<el-switch v-model="nodeDialogForm.required"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="nodeDialogForm.status">
|
||||
<el-radio label="enabled">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="nodeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitNodeDialog">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="resourceForm.id ? '编辑学习资料' : '新增学习资料'" :visible.sync="resourceDialogVisible" :close-on-click-modal="false" width="55%">
|
||||
<el-form :model="resourceForm" ref="resourceForm" label-width="120px" :rules="resourceRules">
|
||||
<el-form-item label="所属章节">
|
||||
<el-input v-model="resourceForm.outlineName" disabled placeholder="所属章节"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="资料标题" prop="resourceTitle">
|
||||
<el-input v-model="resourceForm.resourceTitle" maxlength="100" placeholder="请输入资料标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资料类型" prop="resourceType">
|
||||
<el-select v-model="resourceForm.resourceType" placeholder="请选择资料类型" style="width: 100%" @change="resourceTypeChange">
|
||||
<el-option v-for="item in resourceTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-input-number v-model="resourceForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学习时长(分钟)">
|
||||
<el-input-number v-model="resourceForm.durationMinutes" :min="0" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="resourceForm.status">
|
||||
<el-radio label="enabled">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="是否必学"><el-switch v-model="resourceForm.required"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="允许预览"><el-switch v-model="resourceForm.allowPreview"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="允许下载"><el-switch v-model="resourceForm.allowDownload"></el-switch></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item label="附件" prop="fileData">
|
||||
<file-upload
|
||||
:value.sync="resourceForm.fileData"
|
||||
:upload_number="1"
|
||||
upload_mode="drag"
|
||||
upload_result_category="array"
|
||||
complete_result
|
||||
:accept="fileAccept">
|
||||
</file-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="resourceDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitResource">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
courseQuery: {},
|
||||
courseOptions: [],
|
||||
courseTypeOptions: [],
|
||||
currentCourseId: "",
|
||||
treeData: [],
|
||||
selectedNode: {},
|
||||
activeTab: "basic",
|
||||
nodeForm: {},
|
||||
nodeDialogVisible: false,
|
||||
nodeDialogTitle: "",
|
||||
nodeDialogForm: {},
|
||||
nodeRules: {
|
||||
title: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
|
||||
},
|
||||
contextVisible: false,
|
||||
contextLeft: 0,
|
||||
contextTop: 0,
|
||||
contextNode: {},
|
||||
resourceData: [],
|
||||
resourcePageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "",
|
||||
pageOrderBy: ""
|
||||
},
|
||||
resourceDialogVisible: false,
|
||||
resourceForm: {},
|
||||
resourceRules: {
|
||||
resourceTitle: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
resourceType: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
fileData: [{ required: true, message: "请上传附件", trigger: ["blur", "change"] }]
|
||||
},
|
||||
fileAccept: "",
|
||||
ruleForm: {},
|
||||
statusOptions: [
|
||||
{ name: "草稿", code: "draft" },
|
||||
{ name: "待发布", code: "pending" },
|
||||
{ name: "已发布", code: "published" },
|
||||
{ name: "已下架", code: "offline" }
|
||||
],
|
||||
resourceTypeOptions: [
|
||||
{ name: "视频", code: "video", accept: ".mp4,.mov" },
|
||||
{ name: "音频", code: "audio", accept: ".mp3,.wav,.aac" },
|
||||
{ name: "PDF", code: "pdf", accept: ".pdf" },
|
||||
{ name: "PPT课件", code: "ppt", accept: ".ppt,.pptx" },
|
||||
{ name: "Word文档", code: "word", accept: ".doc,.docx" },
|
||||
{ name: "图片", code: "image", accept: ".jpg,.jpeg,.png" },
|
||||
{ name: "其他", code: "other", accept: "" }
|
||||
],
|
||||
studyModeOptions: [
|
||||
{ name: "视频", code: "video" },
|
||||
{ name: "音频", code: "audio" },
|
||||
{ name: "文档", code: "document" },
|
||||
{ name: "图片", code: "image" },
|
||||
{ name: "混合资料", code: "mixed" }
|
||||
],
|
||||
completionRuleOptions: [
|
||||
{ name: "完成所有必学资料", code: "all_required_resource" },
|
||||
{ name: "按资料学习进度", code: "resource_progress" },
|
||||
{ name: "达到最少学习时长", code: "min_study_time" },
|
||||
{ name: "管理员手动确认", code: "manual" }
|
||||
],
|
||||
unlockRuleOptions: [
|
||||
{ name: "自由学习", code: "free" },
|
||||
{ name: "顺序解锁", code: "sequential" }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentCourse() {
|
||||
return this.courseOptions.find(v => v.id === this.currentCourseId)
|
||||
},
|
||||
outlineTreeData() {
|
||||
if (!this.currentCourseId || !this.currentCourse) {
|
||||
return []
|
||||
}
|
||||
return [{
|
||||
id: "course-" + this.currentCourseId,
|
||||
courseId: this.currentCourseId,
|
||||
title: this.currentCourse.courseName,
|
||||
nodeType: "course",
|
||||
children: this.treeData
|
||||
}]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetCourseQuery() {
|
||||
this.courseQuery = {}
|
||||
this.currentCourseId = ""
|
||||
this.loadCourses()
|
||||
},
|
||||
getQueryParam(name) {
|
||||
return new URLSearchParams(window.location.search).get(name) || ""
|
||||
},
|
||||
async loadCourseTypes() {
|
||||
const resp = await axios.post(loc() + "/courseTypes")
|
||||
if (resp.code === 0) {
|
||||
this.courseTypeOptions = resp.data
|
||||
}
|
||||
},
|
||||
async loadCourses(keepCurrent = false) {
|
||||
const resp = await axios.post(loc() + "/courses", this.courseQuery)
|
||||
if (resp.code === 0) {
|
||||
this.courseOptions = resp.data
|
||||
if (!keepCurrent && (!this.currentCourseId || !this.courseOptions.some(v => v.id === this.currentCourseId))) {
|
||||
this.currentCourseId = this.courseOptions.length ? this.courseOptions[0].id : ""
|
||||
}
|
||||
await this.loadTree()
|
||||
}
|
||||
},
|
||||
courseNameRemoteSearch(query) {
|
||||
this.courseQuery.courseName = query
|
||||
this.loadCourses(true)
|
||||
},
|
||||
courseChange() {
|
||||
this.selectedNode = {}
|
||||
this.loadTree()
|
||||
},
|
||||
courseClear() {
|
||||
this.currentCourseId = ""
|
||||
this.selectedNode = {}
|
||||
this.treeData = []
|
||||
},
|
||||
async loadTree() {
|
||||
if (!this.currentCourseId) {
|
||||
this.treeData = []
|
||||
this.selectedNode = {}
|
||||
return
|
||||
}
|
||||
const resp = await axios.post(loc() + "/tree", { courseId: this.currentCourseId })
|
||||
if (resp.code === 0) {
|
||||
this.treeData = resp.data
|
||||
let nodeToSelect = null
|
||||
if (this.selectedNode.id) {
|
||||
nodeToSelect = this.findNode(this.treeData, this.selectedNode.id)
|
||||
}
|
||||
if (!nodeToSelect) {
|
||||
nodeToSelect = this.findFirstOutlineNode(this.treeData)
|
||||
}
|
||||
if (nodeToSelect) {
|
||||
this.nodeClick(nodeToSelect)
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.outlineTree) {
|
||||
this.$refs.outlineTree.setCurrentKey(nodeToSelect.id)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.selectedNode = {}
|
||||
this.nodeForm = {}
|
||||
this.resourceData = []
|
||||
}
|
||||
}
|
||||
},
|
||||
findFirstOutlineNode(nodes) {
|
||||
if (!nodes || !nodes.length) return null
|
||||
for (const node of nodes) {
|
||||
if (node.nodeType !== "course") return node
|
||||
const child = this.findFirstOutlineNode(node.children || [])
|
||||
if (child) return child
|
||||
}
|
||||
return null
|
||||
},
|
||||
findNode(nodes, id) {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node
|
||||
const child = this.findNode(node.children || [], id)
|
||||
if (child) return child
|
||||
}
|
||||
return null
|
||||
},
|
||||
getParentName(type, node, course, isEdit) {
|
||||
if (type === "chapter") {
|
||||
return course ? course.courseName : ""
|
||||
}
|
||||
if (!isEdit) {
|
||||
return node ? node.title : ""
|
||||
}
|
||||
const parent = node && node.parentId ? this.findNode(this.treeData, node.parentId) : null
|
||||
return parent ? parent.title : (course ? course.courseName : "")
|
||||
},
|
||||
nodeClick(data) {
|
||||
if (data.nodeType === "course") {
|
||||
this.selectedNode = {}
|
||||
this.nodeForm = {}
|
||||
this.resourceData = []
|
||||
return
|
||||
}
|
||||
this.selectedNode = data
|
||||
this.nodeForm = Object.assign({}, data)
|
||||
this.activeTab = "basic"
|
||||
this.loadResources()
|
||||
this.loadRule()
|
||||
},
|
||||
openContextMenu(event, data) {
|
||||
event.preventDefault()
|
||||
this.contextNode = data
|
||||
this.contextVisible = true
|
||||
this.contextLeft = event.clientX
|
||||
this.contextTop = event.clientY
|
||||
},
|
||||
openNodeForm(type, node = null) {
|
||||
if (!this.currentCourseId) {
|
||||
this.$message.warning("请先选择课程")
|
||||
return
|
||||
}
|
||||
const course = this.courseOptions.find(v => v.id === this.currentCourseId)
|
||||
if (node && node.id && node.nodeType === type) {
|
||||
this.nodeDialogTitle = "编辑" + (type === "chapter" ? "章" : "节")
|
||||
this.nodeDialogForm = Object.assign({}, node, {
|
||||
parentName: this.getParentName(type, node, course, true)
|
||||
})
|
||||
} else {
|
||||
this.nodeDialogTitle = "新增" + (type === "chapter" ? "章" : "节")
|
||||
this.nodeDialogForm = {
|
||||
courseId: this.currentCourseId,
|
||||
courseName: course ? course.courseName : "",
|
||||
parentId: type === "section" && node ? node.id : "",
|
||||
parentName: this.getParentName(type, node, course, false),
|
||||
nodeType: type,
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
sortOrder: 1,
|
||||
required: false,
|
||||
status: "enabled"
|
||||
}
|
||||
}
|
||||
this.nodeDialogVisible = true
|
||||
this.$nextTick(() => this.$refs.nodeDialogForm && this.$refs.nodeDialogForm.clearValidate())
|
||||
},
|
||||
submitNodeDialog() {
|
||||
this.$refs.nodeDialogForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const resp = await axios.post(loc() + "/saveNode", this.nodeDialogForm)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.nodeDialogVisible = false
|
||||
await this.loadTree()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
saveNodeBasic() {
|
||||
this.$refs.nodeForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const resp = await axios.post(loc() + "/saveNode", this.nodeForm)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.selectedNode = Object.assign({}, this.nodeForm)
|
||||
await this.loadTree()
|
||||
await this.loadRule()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteNode(node) {
|
||||
this.$confirm("确定要删除该节点吗?若下级有内容将不允许删除。", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await axios.post(loc() + "/deleteNode", { id: node.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.selectedNode = {}
|
||||
await this.loadTree()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
async toggleNode(node) {
|
||||
const status = node.status === "enabled" ? "disabled" : "enabled"
|
||||
const resp = await axios.post(loc() + "/toggleNode", { id: node.id, status })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
await this.loadTree()
|
||||
}
|
||||
},
|
||||
async moveNode(node, direction) {
|
||||
const resp = await axios.post(loc() + "/moveNode", { id: node.id, direction })
|
||||
if (resp.code === 0) {
|
||||
await this.loadTree()
|
||||
}
|
||||
},
|
||||
async setRequired(node) {
|
||||
const next = !node.required
|
||||
const data = Object.assign({}, node, { required: next })
|
||||
const resp = await axios.post(loc() + "/saveNode", data)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
await this.loadTree()
|
||||
if (this.selectedNode.id === node.id) {
|
||||
this.nodeClick(Object.assign({}, node, { required: next }))
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadResources() {
|
||||
if (!this.selectedNode.id) return
|
||||
const resp = await axios.post(loc() + "/resourcePage", Object.assign({}, this.resourcePageForm, { outlineId: this.selectedNode.id }))
|
||||
if (resp.code === 0) {
|
||||
this.resourceData = resp.data.list
|
||||
this.resourcePageForm.totalCount = resp.data.totalCount
|
||||
}
|
||||
},
|
||||
resourcePageOrder(column) {
|
||||
this.resourcePageForm.pageOrderName = column.prop
|
||||
this.resourcePageForm.pageOrderBy = column.order
|
||||
this.loadResources()
|
||||
},
|
||||
resourceSizeChange(val) {
|
||||
this.resourcePageForm.pageSize = val
|
||||
this.loadResources()
|
||||
},
|
||||
resourceNumberChange(val) {
|
||||
this.resourcePageForm.pageNumber = val
|
||||
this.loadResources()
|
||||
},
|
||||
resourceIndex(index) {
|
||||
return index + (this.resourcePageForm.pageNumber - 1) * this.resourcePageForm.pageSize + 1
|
||||
},
|
||||
openResourceForm(row = null, node = null) {
|
||||
const target = node || this.selectedNode
|
||||
if (!target.id) {
|
||||
this.$message.warning("请先选择章/节节点")
|
||||
return
|
||||
}
|
||||
if (row) {
|
||||
this.resourceForm = Object.assign({}, row, {
|
||||
outlineName: target.title,
|
||||
fileData: this.parseFiles(row.fileData),
|
||||
durationMinutes: this.formatDurationMinutes(row.durationSeconds)
|
||||
})
|
||||
} else {
|
||||
this.resourceForm = {
|
||||
courseId: target.courseId,
|
||||
outlineId: target.id,
|
||||
outlineName: target.title,
|
||||
resourceTitle: "",
|
||||
resourceType: "video",
|
||||
fileExt: "",
|
||||
fileData: [],
|
||||
durationSeconds: 0,
|
||||
durationMinutes: 0,
|
||||
sortOrder: 1,
|
||||
required: false,
|
||||
allowPreview: true,
|
||||
allowDownload: false,
|
||||
status: "enabled"
|
||||
}
|
||||
}
|
||||
this.resourceTypeChange(this.resourceForm.resourceType)
|
||||
this.resourceDialogVisible = true
|
||||
this.$nextTick(() => this.$refs.resourceForm && this.$refs.resourceForm.clearValidate())
|
||||
},
|
||||
resourceTypeChange(val) {
|
||||
const type = this.resourceTypeOptions.find(v => v.code === val)
|
||||
this.fileAccept = type ? type.accept : ""
|
||||
this.autoFillMediaDuration()
|
||||
},
|
||||
submitResource() {
|
||||
this.$refs.resourceForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const data = Object.assign({}, this.resourceForm)
|
||||
data.durationSeconds = Math.round((data.durationMinutes || 0) * 60)
|
||||
data.fileExt = this.getFileExt(data.fileData)
|
||||
if (!this.validResourceExt(data.resourceType, data.fileExt)) {
|
||||
this.$message.warning("当前资料类型不支持上传 ." + data.fileExt + " 格式文件")
|
||||
return
|
||||
}
|
||||
delete data.outlineName
|
||||
delete data.durationMinutes
|
||||
data.fileData = JSON.stringify(data.fileData || [])
|
||||
const resp = await axios.post(loc() + "/saveResource", data)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.resourceDialogVisible = false
|
||||
this.loadResources()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteResource(row) {
|
||||
this.$confirm("确定要删除该学习资料吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await axios.post(loc() + "/deleteResource", { id: row.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.loadResources()
|
||||
}
|
||||
})
|
||||
},
|
||||
async loadRule() {
|
||||
if (!this.selectedNode.id) return
|
||||
const resp = await axios.post(loc() + "/getRule", {
|
||||
courseId: this.selectedNode.courseId,
|
||||
targetType: "outline",
|
||||
targetId: this.selectedNode.id
|
||||
})
|
||||
if (resp.code === 0) {
|
||||
this.ruleForm = Object.assign({}, resp.data, {
|
||||
minStudyMinutes: this.formatDurationMinutes(resp.data.minStudySeconds)
|
||||
})
|
||||
}
|
||||
},
|
||||
async saveRule() {
|
||||
const data = Object.assign({}, this.ruleForm, {
|
||||
minStudySeconds: Math.round((this.ruleForm.minStudyMinutes || 0) * 60)
|
||||
})
|
||||
delete data.minStudyMinutes
|
||||
const resp = await axios.post(loc() + "/saveRule", data)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
await this.loadTree()
|
||||
await this.loadRule()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
},
|
||||
parseFiles(value) {
|
||||
if (!value) return []
|
||||
if (Array.isArray(value)) return value
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
getFileUrl(files) {
|
||||
const file = Array.isArray(files) && files.length ? files[0] : null
|
||||
if (!file) return ""
|
||||
return file.url || file.response?.data || file.data || ""
|
||||
},
|
||||
getFileExt(files) {
|
||||
const file = Array.isArray(files) && files.length ? files[0] : null
|
||||
if (!file) return ""
|
||||
const name = file.name || file.url || file.response?.data || ""
|
||||
const index = name.lastIndexOf(".")
|
||||
return index > -1 ? name.substring(index + 1).toLowerCase() : ""
|
||||
},
|
||||
formatDurationMinutes(durationSeconds) {
|
||||
const seconds = Number(durationSeconds || 0)
|
||||
return seconds > 0 ? Math.ceil(seconds / 60) : 0
|
||||
},
|
||||
autoFillMediaDuration() {
|
||||
if (!["video", "audio"].includes(this.resourceForm.resourceType)) return
|
||||
const url = this.getFileUrl(this.resourceForm.fileData)
|
||||
if (!url) return
|
||||
const media = document.createElement(this.resourceForm.resourceType === "video" ? "video" : "audio")
|
||||
media.preload = "metadata"
|
||||
media.onloadedmetadata = () => {
|
||||
window.URL.revokeObjectURL(media.src)
|
||||
if (isFinite(media.duration) && media.duration > 0) {
|
||||
this.$set(this.resourceForm, "durationMinutes", Math.ceil(media.duration / 60))
|
||||
}
|
||||
}
|
||||
media.onerror = () => {
|
||||
window.URL.revokeObjectURL(media.src)
|
||||
}
|
||||
media.src = url
|
||||
},
|
||||
validResourceExt(resourceType, fileExt) {
|
||||
const item = this.resourceTypeOptions.find(v => v.code === resourceType)
|
||||
if (!item || !item.accept) return true
|
||||
return item.accept.split(",").map(v => v.replace(".", "").toLowerCase()).includes(fileExt)
|
||||
},
|
||||
getResourceTypeName(code) {
|
||||
const item = this.resourceTypeOptions.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"resourceForm.fileData": {
|
||||
handler() {
|
||||
this.autoFillMediaDuration()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("click", () => this.contextVisible = false)
|
||||
},
|
||||
async created() {
|
||||
await this.loadCourseTypes()
|
||||
const courseId = this.getQueryParam("courseId")
|
||||
if (courseId) {
|
||||
this.currentCourseId = courseId
|
||||
this.courseQuery.courseId = courseId
|
||||
await this.loadCourses(true)
|
||||
this.$delete(this.courseQuery, "courseId")
|
||||
} else {
|
||||
await this.loadCourses()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,12 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<div slot="header">课程介绍</div>
|
||||
<el-empty description="课程介绍页面后续设计"></el-empty>
|
||||
</el-card>
|
||||
</div>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,350 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
.course-display {
|
||||
padding: 8px 10px 22px;
|
||||
background: #f4f5f7;
|
||||
min-height: calc(100vh - 88px);
|
||||
}
|
||||
.course-filter {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e8ef;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
min-height: 70px;
|
||||
border-bottom: 1px solid #e8ebf0;
|
||||
}
|
||||
.filter-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.filter-label {
|
||||
width: 132px;
|
||||
padding: 20px 16px;
|
||||
background: #eef5ff;
|
||||
color: #1f2d3d;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.filter-options {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 18px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
.filter-chip {
|
||||
min-width: 76px;
|
||||
height: 32px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: #526070;
|
||||
cursor: pointer;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.filter-chip.active {
|
||||
background: #1e63e9;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.course-result-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.course-count {
|
||||
color: #1f2d3d;
|
||||
}
|
||||
.course-count span {
|
||||
color: #2468f2;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.course-search {
|
||||
width: 310px;
|
||||
}
|
||||
.course-list {
|
||||
background: #fff;
|
||||
border-top: 1px solid #dfe4ec;
|
||||
}
|
||||
.course-item {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid #dfe4ec;
|
||||
}
|
||||
.course-cover {
|
||||
position: relative;
|
||||
width: 270px;
|
||||
height: 150px;
|
||||
flex: 0 0 270px;
|
||||
background: #e9eef5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.course-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.course-cover-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97a8;
|
||||
background: linear-gradient(135deg, #eaf0f8, #d8e3f2);
|
||||
font-weight: 600;
|
||||
}
|
||||
.course-cover-tag {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 12px;
|
||||
min-width: 70px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
background: #f5a400;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
.course-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.course-title {
|
||||
display: inline-block;
|
||||
margin: 4px 0 8px;
|
||||
color: #111827;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.course-title:hover {
|
||||
color: #006fc9;
|
||||
}
|
||||
.course-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 18px;
|
||||
color: #8b96a6;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.course-intro {
|
||||
color: #4f5f73;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.course-time {
|
||||
color: #8b96a6;
|
||||
font-size: 13px;
|
||||
}
|
||||
.recommend-tags {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
margin-left: 8px;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
.course-empty {
|
||||
background: #fff;
|
||||
padding: 80px 0;
|
||||
text-align: center;
|
||||
color: #8b96a6;
|
||||
border-top: 1px solid #dfe4ec;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="course-display" v-cloak>
|
||||
<div class="course-filter">
|
||||
<div class="filter-row">
|
||||
<div class="filter-label">推荐标识</div>
|
||||
<div class="filter-options">
|
||||
<button class="filter-chip" :class="{active: !query.recommendFlag}" @click="selectRecommend('')">全部</button>
|
||||
<button
|
||||
v-for="item in recommendOptions"
|
||||
:key="item.code"
|
||||
class="filter-chip"
|
||||
:class="{active: query.recommendFlag === item.code}"
|
||||
@click="selectRecommend(item.code)">
|
||||
{{item.name}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<div class="filter-label">课程类型</div>
|
||||
<div class="filter-options">
|
||||
<button class="filter-chip" :class="{active: !query.courseTypeId}" @click="selectCourseType('')">全部</button>
|
||||
<button
|
||||
v-for="item in courseTypeOptions"
|
||||
:key="item.id"
|
||||
class="filter-chip"
|
||||
:class="{active: query.courseTypeId === item.id}"
|
||||
@click="selectCourseType(item.id)">
|
||||
{{item.typeName}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="course-result-bar">
|
||||
<div class="course-count">为您找到相关课程<span>{{pageForm.totalCount || 0}}</span>门</div>
|
||||
<el-input
|
||||
class="course-search"
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
placeholder="搜索关键字"
|
||||
prefix-icon="el-icon-search"
|
||||
@keyup.enter.native="doSearch"
|
||||
@clear="doSearch">
|
||||
<el-button slot="append" icon="el-icon-search" @click="doSearch"></el-button>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div v-if="pageForm.list && pageForm.list.length" class="course-list">
|
||||
<div v-for="course in pageForm.list" :key="course.id" class="course-item">
|
||||
<div class="course-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="course-cover-empty">{{course.courseTypeName || '课程'}}</div>
|
||||
<div v-if="firstRecommendName(course.recommendFlags)" class="course-cover-tag">{{firstRecommendName(course.recommendFlags)}}</div>
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<div>
|
||||
<span class="course-title" @click="openDetail(course)">{{course.courseName}}</span>
|
||||
<span class="recommend-tags">
|
||||
<el-tag v-for="code in splitFlags(course.recommendFlags)" :key="code" size="mini" type="warning">{{getRecommendName(code)}}</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="course-meta">
|
||||
<span><i class="el-icon-office-building"></i> {{course.courseTypeName || '未设置课程类型'}}</span>
|
||||
<span><i class="el-icon-user"></i> {{course.lecturerName || '未设置讲师'}}</span>
|
||||
</div>
|
||||
<div class="course-intro">{{course.courseIntro || '暂无课程介绍'}}</div>
|
||||
<div class="course-time">
|
||||
<i class="el-icon-time"></i>
|
||||
<span v-if="course.openType === 'long_term'">长期开放</span>
|
||||
<span v-else>{{course.startTimeText || '-'}} 至 {{course.endTimeText || '-'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="course-empty">暂无相关课程</div>
|
||||
|
||||
<el-pagination
|
||||
class="mt20"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:current-page.sync="pageForm.pageNumber"
|
||||
:page-size.sync="pageForm.pageSize"
|
||||
:total="pageForm.totalCount"
|
||||
@size-change="pageData"
|
||||
@current-change="pageData">
|
||||
</el-pagination>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
query: {
|
||||
keyword: "",
|
||||
courseTypeId: "",
|
||||
recommendFlag: ""
|
||||
},
|
||||
courseTypeOptions: [],
|
||||
recommendOptions: [],
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
list: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async pageData() {
|
||||
const resp = await axios.post(loc() + "/pageData", Object.assign({}, this.query, {
|
||||
pageNumber: this.pageForm.pageNumber,
|
||||
pageSize: this.pageForm.pageSize
|
||||
}))
|
||||
if (resp.code === 0) {
|
||||
this.pageForm = resp.data
|
||||
}
|
||||
},
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageData()
|
||||
},
|
||||
selectRecommend(code) {
|
||||
this.query.recommendFlag = code
|
||||
this.doSearch()
|
||||
},
|
||||
selectCourseType(id) {
|
||||
this.query.courseTypeId = id
|
||||
this.doSearch()
|
||||
},
|
||||
async loadOptions() {
|
||||
const typeResp = await axios.post(loc() + "/courseTypes")
|
||||
if (typeResp.code === 0) {
|
||||
this.courseTypeOptions = typeResp.data
|
||||
}
|
||||
const recommendResp = await axios.post(loc() + "/recommendOptions")
|
||||
if (recommendResp.code === 0) {
|
||||
this.recommendOptions = recommendResp.data
|
||||
}
|
||||
},
|
||||
getCoverUrl(cover) {
|
||||
if (!cover) return ""
|
||||
if (Array.isArray(cover)) {
|
||||
return cover.length ? (cover[0].url || cover[0].response?.data || cover[0].data || "") : ""
|
||||
}
|
||||
if (typeof cover === "string" && cover.trim().startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(cover)
|
||||
return files.length ? (files[0].url || files[0].response?.data || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return cover
|
||||
},
|
||||
splitFlags(flags) {
|
||||
return flags ? flags.split(",").filter(Boolean) : []
|
||||
},
|
||||
getRecommendName(code) {
|
||||
const item = this.recommendOptions.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
},
|
||||
firstRecommendName(flags) {
|
||||
const codes = this.splitFlags(flags)
|
||||
return codes.length ? this.getRecommendName(codes[0]) : ""
|
||||
},
|
||||
openDetail(course) {
|
||||
window.location.href = loc() + "/study?id=" + course.id
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.loadOptions()
|
||||
await this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,755 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
#sidebar-menu,
|
||||
#menu-toggle-btn,
|
||||
#menu-overlay {
|
||||
display: none !important;
|
||||
}
|
||||
#sub-app-container-main-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
#sub-app-container-main-content-body {
|
||||
height: 100%;
|
||||
}
|
||||
.study-page {
|
||||
min-height: calc(100vh - 50px);
|
||||
background: #eef3f8;
|
||||
}
|
||||
.study-header {
|
||||
height: 150px;
|
||||
margin: 12px 14px 0;
|
||||
padding: 20px 24px;
|
||||
color: #1f2937;
|
||||
background: linear-gradient(120deg, #ffffff 0%, #f4f8ff 58%, #eaf2ff 100%);
|
||||
border: 1px solid #dbe7f5;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 6px 18px rgba(31, 64, 116, .08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.study-back {
|
||||
align-self: flex-start;
|
||||
margin-left: auto;
|
||||
order: 5;
|
||||
color: #1e63c8;
|
||||
border-color: #b8d0f2;
|
||||
background: #fff;
|
||||
}
|
||||
.study-cover {
|
||||
width: 190px;
|
||||
height: 102px;
|
||||
object-fit: cover;
|
||||
background: #eef4fb;
|
||||
border: 1px solid #d7e3f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(31, 64, 116, .12);
|
||||
}
|
||||
.study-cover-empty {
|
||||
width: 190px;
|
||||
height: 102px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #eef4fb;
|
||||
color: #6b7d90;
|
||||
border: 1px solid #d7e3f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(31, 64, 116, .12);
|
||||
font-weight: 600;
|
||||
}
|
||||
.study-course-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.study-title {
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.study-title .el-tag {
|
||||
margin-left: 8px;
|
||||
vertical-align: 5px;
|
||||
}
|
||||
.study-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 26px;
|
||||
color: #526376;
|
||||
font-size: 14px;
|
||||
}
|
||||
.study-body {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
height: calc(100vh - 212px);
|
||||
padding: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.study-tree-panel {
|
||||
width: 340px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.study-tree-title {
|
||||
height: 54px;
|
||||
padding: 0 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #1768e5;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
}
|
||||
.study-tree {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
.study-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
gap: 6px;
|
||||
}
|
||||
.study-node-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.study-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.resource-head {
|
||||
min-height: 54px;
|
||||
padding: 0 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
}
|
||||
.resource-title {
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.resource-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.study-progress {
|
||||
width: 180px;
|
||||
}
|
||||
.resource-view {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.media-player {
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 300px);
|
||||
background: #000;
|
||||
}
|
||||
.audio-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 320px;
|
||||
background: linear-gradient(135deg, #132544, #0a1224);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.audio-wrap audio {
|
||||
width: 80%;
|
||||
}
|
||||
.image-preview {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
}
|
||||
.doc-frame {
|
||||
width: 100%;
|
||||
height: calc(100vh - 275px);
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.file-fallback {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: #667085;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="study-page" v-cloak>
|
||||
<div class="study-header">
|
||||
<el-button class="study-back" icon="el-icon-back" size="small" @click="goBack">返回</el-button>
|
||||
<img v-if="coverUrl" class="study-cover" :src="coverUrl" :alt="course.courseName">
|
||||
<div v-else class="study-cover-empty">课程图片</div>
|
||||
<div class="study-course-main">
|
||||
<div class="study-title">
|
||||
{{course.courseName || '课程学习'}}
|
||||
<el-tag v-for="code in splitFlags(course.recommendFlags)" :key="code" size="small" type="warning">{{getRecommendName(code)}}</el-tag>
|
||||
</div>
|
||||
<div class="study-meta">
|
||||
<span><i class="el-icon-user"></i> 授课讲师:{{course.lecturerName || '未设置'}}</span>
|
||||
<span><i class="el-icon-date"></i> {{course.openType === 'long_term' ? '长期开放' : ((course.startTimeText || '-') + ' 至 ' + (course.endTimeText || '-'))}}</span>
|
||||
<span><i class="el-icon-collection-tag"></i> 课程类型:{{course.courseTypeName || '未设置'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="study-body">
|
||||
<div class="study-tree-panel">
|
||||
<div class="study-tree-title">
|
||||
<span>课程安排</span>
|
||||
<el-button type="text" size="mini" @click="collapseAll">收起</el-button>
|
||||
</div>
|
||||
<div class="study-tree">
|
||||
<el-tree
|
||||
ref="studyTree"
|
||||
:data="treeData"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:expand-on-click-node="false"
|
||||
:props="{children:'children', label:'title'}"
|
||||
@node-click="nodeClick">
|
||||
<span slot-scope="{ data }" class="study-node">
|
||||
<i :class="getNodeIcon(data)"></i>
|
||||
<span class="study-node-title">{{data.title}}</span>
|
||||
<el-tag v-if="data.required" size="mini" type="warning">必学</el-tag>
|
||||
</span>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="study-content">
|
||||
<div class="resource-head">
|
||||
<div class="resource-title">{{selectedResource.title || '请选择课程资料'}}</div>
|
||||
<div class="resource-actions">
|
||||
<el-progress
|
||||
v-if="selectedResource.id"
|
||||
class="study-progress"
|
||||
:percentage="recordProgress"
|
||||
:stroke-width="8">
|
||||
</el-progress>
|
||||
<el-tag v-if="studying" size="mini" type="success">学习中 {{recordStudyTime}}</el-tag>
|
||||
<el-button v-if="selectedResource.id && !studying" size="mini" type="primary" icon="el-icon-video-play" @click="startStudy">开始学习</el-button>
|
||||
<el-button v-if="studying" size="mini" type="danger" icon="el-icon-video-pause" @click="finishStudy(false)">结束学习</el-button>
|
||||
<el-button v-if="fileUrl" size="mini" type="text" icon="el-icon-download" @click="openFile">打开原文件</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-view">
|
||||
<template v-if="selectedResource.id">
|
||||
<video
|
||||
v-if="selectedResource.resourceType === 'video'"
|
||||
ref="mediaPlayer"
|
||||
class="media-player"
|
||||
:src="fileUrl"
|
||||
controls
|
||||
controlslist="nodownload"
|
||||
@play="mediaPlay"
|
||||
@ended="mediaEnded"
|
||||
@timeupdate="saveProgress"
|
||||
@loadedmetadata="mediaReady">
|
||||
</video>
|
||||
<div v-else-if="selectedResource.resourceType === 'audio'" class="audio-wrap">
|
||||
<audio
|
||||
ref="mediaPlayer"
|
||||
:src="fileUrl"
|
||||
controls
|
||||
controlslist="nodownload"
|
||||
@play="mediaPlay"
|
||||
@ended="mediaEnded"
|
||||
@timeupdate="saveProgress"
|
||||
@loadedmetadata="mediaReady">
|
||||
</audio>
|
||||
</div>
|
||||
<img v-else-if="selectedResource.resourceType === 'image'" class="image-preview" :src="fileUrl" :alt="selectedResource.title">
|
||||
<iframe v-else-if="canInlinePreview(selectedResource)" class="doc-frame" :src="inlinePreviewUrl"></iframe>
|
||||
<div v-else class="file-fallback">
|
||||
<file-preview :files="selectedResource.fileData" complete_result></file-preview>
|
||||
<el-button class="mt20" type="primary" icon="el-icon-view" @click="openFile">打开资料</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else description="请选择左侧课程资料"></el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
courseId: "",
|
||||
apiBase: "/platform/learning/course/display",
|
||||
recordApi: "/platform/learning/study/record",
|
||||
course: {},
|
||||
treeData: [],
|
||||
selectedResource: {},
|
||||
recommendOptions: [],
|
||||
studying: false,
|
||||
currentSegmentId: "",
|
||||
recordProgress: 0,
|
||||
recordStudyTime: "0秒",
|
||||
pendingSeconds: 0,
|
||||
heartbeatTimer: null,
|
||||
startingStudy: false,
|
||||
autoResumeKey: "",
|
||||
lastPositionSyncAt: 0,
|
||||
lastActiveAt: Date.now(),
|
||||
inactiveLimit: 60000
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coverUrl() {
|
||||
return this.getFileUrl(this.course.cover)
|
||||
},
|
||||
fileUrl() {
|
||||
return this.getFileUrl(this.selectedResource.fileData)
|
||||
},
|
||||
inlinePreviewUrl() {
|
||||
const id = this.getFileId(this.selectedResource.fileData)
|
||||
return id ? this.apiBase + "/pdfPreview?id=" + encodeURIComponent(id) : this.fileUrl
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getQuery(name) {
|
||||
return new URLSearchParams(window.location.search).get(name) || ""
|
||||
},
|
||||
async loadCourse() {
|
||||
const resp = await axios.post(this.apiBase + "/courseInfo", { id: this.courseId })
|
||||
if (resp.code === 0) {
|
||||
this.course = resp.data
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
},
|
||||
async loadTree() {
|
||||
const resp = await axios.post(this.apiBase + "/studyTree", { courseId: this.courseId })
|
||||
if (resp.code === 0) {
|
||||
this.treeData = resp.data || []
|
||||
const first = this.findFirstResource(this.treeData)
|
||||
if (first) {
|
||||
this.nodeClick(first)
|
||||
this.$nextTick(() => this.$refs.studyTree && this.$refs.studyTree.setCurrentKey(first.id))
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadRecommendOptions() {
|
||||
const resp = await axios.post(this.apiBase + "/recommendOptions")
|
||||
if (resp.code === 0) {
|
||||
this.recommendOptions = resp.data
|
||||
}
|
||||
},
|
||||
findFirstResource(nodes) {
|
||||
for (const node of nodes || []) {
|
||||
if (node.type === "resource") return node
|
||||
const child = this.findFirstResource(node.children || [])
|
||||
if (child) return child
|
||||
}
|
||||
return null
|
||||
},
|
||||
nodeClick(data) {
|
||||
if (data.type !== "resource") return
|
||||
if (this.studying && this.selectedResource.id !== data.id) {
|
||||
this.$message.warning("请先结束当前章节资料的学习")
|
||||
return
|
||||
}
|
||||
this.selectedResource = data
|
||||
this.recordProgress = data.progressPercent || 0
|
||||
this.recordStudyTime = this.formatSeconds(Number(data.studySeconds || 0))
|
||||
},
|
||||
getNodeIcon(data) {
|
||||
if (data.type === "resource") {
|
||||
const map = {
|
||||
video: "el-icon-video-camera",
|
||||
audio: "el-icon-headset",
|
||||
image: "el-icon-picture-outline",
|
||||
pdf: "el-icon-document",
|
||||
word: "el-icon-document",
|
||||
ppt: "el-icon-document"
|
||||
}
|
||||
return map[data.resourceType] || "el-icon-paperclip"
|
||||
}
|
||||
return data.nodeType === "chapter" ? "el-icon-folder" : "el-icon-notebook-2"
|
||||
},
|
||||
getFileUrl(value) {
|
||||
if (!value) return ""
|
||||
if (Array.isArray(value)) {
|
||||
return value.length ? (value[0].url || value[0].response?.data || value[0].data || "") : ""
|
||||
}
|
||||
if (typeof value === "string" && value.trim().startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(value)
|
||||
return files.length ? (files[0].url || files[0].response?.data || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return value
|
||||
},
|
||||
getFileId(value) {
|
||||
const url = this.getFileUrl(value)
|
||||
if (!url) return ""
|
||||
const matched = url.match(/[?&]id=([^&]+)/)
|
||||
if (matched) return decodeURIComponent(matched[1])
|
||||
if (!url.includes("/") && !url.includes(".")) return url
|
||||
return ""
|
||||
},
|
||||
canInlinePreview(resource) {
|
||||
const type = resource.resourceType
|
||||
const ext = (resource.fileExt || "").toLowerCase()
|
||||
return ["pdf", "ppt", "word"].includes(type) || ["pdf", "ppt", "pptx", "doc", "docx"].includes(ext)
|
||||
},
|
||||
progressKey() {
|
||||
return "learning-progress-" + this.courseId + "-" + this.selectedResource.id
|
||||
},
|
||||
mediaReady() {
|
||||
this.askResume()
|
||||
},
|
||||
askResume() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
if (this.studying || this.startingStudy) return
|
||||
const currentKey = this.progressKey()
|
||||
if (this.autoResumeKey === currentKey) return
|
||||
this.autoResumeKey = currentKey
|
||||
const player = this.$refs.mediaPlayer
|
||||
const localSaved = Number(localStorage.getItem(this.progressKey()) || 0)
|
||||
const serverSaved = Number(this.selectedResource.lastPositionSeconds || 0)
|
||||
const saved = Math.max(localSaved, serverSaved)
|
||||
if (!player || !saved || saved < 5 || saved >= player.duration - 5) return
|
||||
this.$confirm("检测到上次学习到 " + this.formatSeconds(saved) + ",请选择继续学习或从头开始。", "继续学习", {
|
||||
confirmButtonText: "继续学习",
|
||||
cancelButtonText: "从头开始",
|
||||
type: "info"
|
||||
}).then(() => {
|
||||
this.startStudy({ autoPlay: true, seekTo: saved })
|
||||
}).catch(() => {
|
||||
localStorage.removeItem(this.progressKey())
|
||||
this.saveServerPosition(0)
|
||||
this.startStudy({ autoPlay: true, seekTo: 0 })
|
||||
})
|
||||
},
|
||||
async mediaPlay() {
|
||||
if (this.studying || this.startingStudy) return
|
||||
await this.startStudy({ fromMedia: true })
|
||||
},
|
||||
mediaEnded() {
|
||||
this.finishStudy(false, { silent: true })
|
||||
localStorage.removeItem(this.progressKey())
|
||||
},
|
||||
async playSelectedMedia(seekTo) {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
await this.$nextTick()
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (!player) return
|
||||
try {
|
||||
if (typeof seekTo === "number" && !Number.isNaN(seekTo)) {
|
||||
await this.seekBeforePlay(player, seekTo)
|
||||
}
|
||||
const playPromise = player.play && player.play()
|
||||
if (playPromise && playPromise.catch) {
|
||||
await playPromise.catch(() => {
|
||||
this.$message.warning("浏览器阻止了自动播放,请点击播放器播放按钮继续")
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.warning("播放器定位失败,请手动点击播放后继续")
|
||||
}
|
||||
},
|
||||
async seekBeforePlay(player, seekTo) {
|
||||
await this.waitForMediaReady(player)
|
||||
const target = this.normalizeSeekTarget(player, seekTo)
|
||||
if (target === null) return
|
||||
if (Math.abs((player.currentTime || 0) - target) <= 1) return
|
||||
player.pause()
|
||||
player.currentTime = target
|
||||
await this.waitForSeek(player, target, 1800)
|
||||
if (Math.abs((player.currentTime || 0) - target) > 1) {
|
||||
player.currentTime = target
|
||||
await this.waitForSeek(player, target, 1200)
|
||||
}
|
||||
},
|
||||
waitForMediaReady(player) {
|
||||
if (player.readyState >= 1 && !Number.isNaN(player.duration)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const done = () => {
|
||||
player.removeEventListener("loadedmetadata", done)
|
||||
player.removeEventListener("durationchange", done)
|
||||
resolve()
|
||||
}
|
||||
player.addEventListener("loadedmetadata", done, { once: true })
|
||||
player.addEventListener("durationchange", done, { once: true })
|
||||
setTimeout(done, 2000)
|
||||
})
|
||||
},
|
||||
waitForSeek(player, target, timeout) {
|
||||
return new Promise(resolve => {
|
||||
const done = () => {
|
||||
player.removeEventListener("seeked", done)
|
||||
player.removeEventListener("timeupdate", done)
|
||||
resolve()
|
||||
}
|
||||
player.addEventListener("seeked", done, { once: true })
|
||||
player.addEventListener("timeupdate", done, { once: true })
|
||||
setTimeout(done, timeout)
|
||||
})
|
||||
},
|
||||
normalizeSeekTarget(player, seekTo) {
|
||||
const raw = Math.max(0, Number(seekTo || 0))
|
||||
if (Number.isNaN(raw)) return null
|
||||
if (!Number.isNaN(player.duration) && player.duration > 0) {
|
||||
return Math.min(raw, Math.max(0, player.duration - 1))
|
||||
}
|
||||
return raw
|
||||
},
|
||||
saveProgress() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (player && player.currentTime > 0) {
|
||||
const position = Math.floor(player.currentTime)
|
||||
localStorage.setItem(this.progressKey(), String(position))
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", position)
|
||||
if (Date.now() - this.lastPositionSyncAt > 5000) {
|
||||
this.saveServerPosition(position)
|
||||
}
|
||||
}
|
||||
},
|
||||
saveServerPosition(position, useBeacon) {
|
||||
if (!this.selectedResource.id || !["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
const seconds = Math.max(0, Math.floor(Number(position || 0)))
|
||||
this.lastPositionSyncAt = Date.now()
|
||||
if (useBeacon && navigator.sendBeacon) {
|
||||
const body = new URLSearchParams()
|
||||
body.append("courseId", this.courseId)
|
||||
body.append("resourceId", this.selectedResource.id)
|
||||
body.append("positionSeconds", String(seconds))
|
||||
const blob = new Blob([body.toString()], { type: "application/x-www-form-urlencoded;charset=UTF-8" })
|
||||
navigator.sendBeacon(this.recordApi + "/position", blob)
|
||||
return
|
||||
}
|
||||
this.$axios.post(this.recordApi + "/position", {
|
||||
courseId: this.courseId,
|
||||
resourceId: this.selectedResource.id,
|
||||
positionSeconds: seconds
|
||||
})
|
||||
},
|
||||
async startStudy(options) {
|
||||
const config = options && !options.target ? options : {}
|
||||
if (!this.selectedResource.id) {
|
||||
this.$message.warning("请选择左侧课程资料")
|
||||
return
|
||||
}
|
||||
if (this.studying) {
|
||||
if (config.autoPlay) {
|
||||
this.playSelectedMedia(config.seekTo)
|
||||
}
|
||||
return
|
||||
}
|
||||
this.startingStudy = true
|
||||
let resp
|
||||
try {
|
||||
resp = await axios.post(this.recordApi + "/start", {
|
||||
courseId: this.courseId,
|
||||
resourceId: this.selectedResource.id,
|
||||
positionSeconds: typeof config.seekTo === "number" ? Math.floor(config.seekTo) : this.getMediaPosition()
|
||||
})
|
||||
} finally {
|
||||
this.startingStudy = false
|
||||
}
|
||||
if (resp.code !== 0) {
|
||||
this.$message.warning(resp.msg)
|
||||
return
|
||||
}
|
||||
this.currentSegmentId = resp.data.segmentId
|
||||
this.studying = true
|
||||
this.pendingSeconds = 0
|
||||
this.lastActiveAt = Date.now()
|
||||
this.applyRecordState(resp.data)
|
||||
this.startHeartbeatTimer()
|
||||
if (config.autoPlay !== false && !config.fromMedia) {
|
||||
this.playSelectedMedia(config.seekTo)
|
||||
}
|
||||
this.$message.success("已开始学习")
|
||||
},
|
||||
async heartbeat() {
|
||||
if (!this.studying || !this.currentSegmentId || this.pendingSeconds <= 0) return
|
||||
const seconds = this.pendingSeconds
|
||||
this.pendingSeconds = 0
|
||||
const resp = await axios.post(this.recordApi + "/heartbeat", {
|
||||
segmentId: this.currentSegmentId,
|
||||
activeSeconds: seconds,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (resp.code === 0) {
|
||||
this.applyRecordState(resp.data)
|
||||
} else {
|
||||
this.stopHeartbeatTimer()
|
||||
this.studying = false
|
||||
this.currentSegmentId = ""
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
},
|
||||
async finishStudy(force, options) {
|
||||
const config = options || {}
|
||||
if (!this.currentSegmentId) return
|
||||
const segmentId = this.currentSegmentId
|
||||
const seconds = this.pendingSeconds
|
||||
this.pendingSeconds = 0
|
||||
this.stopHeartbeatTimer()
|
||||
this.studying = false
|
||||
this.currentSegmentId = ""
|
||||
if (!force) {
|
||||
this.pauseSelectedMedia()
|
||||
}
|
||||
if (force && navigator.sendBeacon) {
|
||||
this.saveServerPosition(this.getMediaPosition(), true)
|
||||
const formData = new FormData()
|
||||
formData.append("segmentId", segmentId)
|
||||
formData.append("activeSeconds", String(seconds))
|
||||
formData.append("positionSeconds", String(this.getMediaPosition()))
|
||||
navigator.sendBeacon(this.recordApi + "/finish", formData)
|
||||
return
|
||||
}
|
||||
const resp = await axios.post(this.recordApi + "/finish", {
|
||||
segmentId: segmentId,
|
||||
activeSeconds: seconds,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (resp.code === 0 && resp.data) {
|
||||
this.applyRecordState(resp.data)
|
||||
if (!config.silent) {
|
||||
this.$message.success("学习已结束")
|
||||
}
|
||||
}
|
||||
},
|
||||
pauseSelectedMedia() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (player && !player.paused) {
|
||||
player.pause()
|
||||
}
|
||||
},
|
||||
startHeartbeatTimer() {
|
||||
this.stopHeartbeatTimer()
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (!this.studying) return
|
||||
if (this.isEffectiveLearning()) {
|
||||
this.pendingSeconds += 1
|
||||
}
|
||||
if (this.pendingSeconds >= 15) {
|
||||
this.heartbeat()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
stopHeartbeatTimer() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
},
|
||||
isEffectiveLearning() {
|
||||
if (document.hidden) return false
|
||||
if (Date.now() - this.lastActiveAt > this.inactiveLimit) return false
|
||||
if (["video", "audio"].includes(this.selectedResource.resourceType)) {
|
||||
const player = this.$refs.mediaPlayer
|
||||
return !!player && !player.paused && !player.ended
|
||||
}
|
||||
return true
|
||||
},
|
||||
markActive() {
|
||||
this.lastActiveAt = Date.now()
|
||||
},
|
||||
applyRecordState(data) {
|
||||
this.recordProgress = Number(data.progressPercent || 0)
|
||||
this.recordStudyTime = data.studyTimeText || this.formatSeconds(Number(data.studySeconds || 0))
|
||||
if (this.selectedResource.id && data.lastPositionSeconds !== undefined) {
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", Number(data.lastPositionSeconds || 0))
|
||||
}
|
||||
},
|
||||
getMediaPosition() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return 0
|
||||
const player = this.$refs.mediaPlayer
|
||||
return player ? Math.floor(player.currentTime || 0) : Number(this.selectedResource.lastPositionSeconds || 0)
|
||||
},
|
||||
formatSeconds(seconds) {
|
||||
const hour = Math.floor(seconds / 3600)
|
||||
const minute = Math.floor(seconds % 3600 / 60)
|
||||
const second = Math.floor(seconds % 60)
|
||||
if (hour > 0) return hour + "小时" + minute + "分" + second + "秒"
|
||||
if (minute > 0) return minute + "分" + second + "秒"
|
||||
return second + "秒"
|
||||
},
|
||||
splitFlags(flags) {
|
||||
return flags ? flags.split(",").filter(Boolean) : []
|
||||
},
|
||||
getRecommendName(code) {
|
||||
const item = this.recommendOptions.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
},
|
||||
openFile() {
|
||||
if (this.fileUrl) window.open(this.fileUrl)
|
||||
},
|
||||
collapseAll() {
|
||||
const nodesMap = this.$refs.studyTree && this.$refs.studyTree.store.nodesMap
|
||||
Object.keys(nodesMap || {}).forEach(key => {
|
||||
nodesMap[key].expanded = false
|
||||
})
|
||||
},
|
||||
async goBack() {
|
||||
this.saveServerPosition(this.getMediaPosition())
|
||||
await this.finishStudy(false, { silent: true })
|
||||
window.location.href = this.apiBase
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.courseId = this.getQuery("id")
|
||||
window.addEventListener("mousemove", this.markActive)
|
||||
window.addEventListener("keydown", this.markActive)
|
||||
window.addEventListener("click", this.markActive)
|
||||
window.addEventListener("scroll", this.markActive, true)
|
||||
window.addEventListener("beforeunload", () => this.finishStudy(true))
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
this.saveServerPosition(this.getMediaPosition(), true)
|
||||
}
|
||||
})
|
||||
await this.loadRecommendOptions()
|
||||
await this.loadCourse()
|
||||
await this.loadTree()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.finishStudy(true)
|
||||
this.stopHeartbeatTimer()
|
||||
window.removeEventListener("mousemove", this.markActive)
|
||||
window.removeEventListener("keydown", this.markActive)
|
||||
window.removeEventListener("click", this.markActive)
|
||||
window.removeEventListener("scroll", this.markActive, true)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,396 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="课程名称">
|
||||
<el-input v-model="pageForm.courseName" clearable placeholder="请输入课程名称" style="width: 100%" @keyup.enter.native="doSearch"></el-input>
|
||||
</search-item>
|
||||
<search-item label="课程类型">
|
||||
<el-select v-model="pageForm.courseTypeId" clearable filterable placeholder="请选择课程类型" style="width: 100%">
|
||||
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="开课时间">
|
||||
<el-date-picker
|
||||
v-model="pageForm.courseTime"
|
||||
type="daterange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
range-separator="至"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="授课讲师">
|
||||
<el-input v-model="pageForm.lecturerName" clearable placeholder="请输入授课讲师" style="width: 100%" @keyup.enter.native="doSearch"></el-input>
|
||||
</search-item>
|
||||
<search-item label="课程状态">
|
||||
<el-select v-model="pageForm.status" clearable placeholder="请选择课程状态" style="width: 100%">
|
||||
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="推荐标识">
|
||||
<el-select v-model="pageForm.recommendFlag" clearable placeholder="请选择推荐标识" style="width: 100%">
|
||||
<el-option v-for="item in recommendOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="课程列表">
|
||||
<el-button @click="openAdd" size="medium" type="primary">
|
||||
<i class="el-icon-plus"></i>
|
||||
新增课程
|
||||
</el-button>
|
||||
</table-tool>
|
||||
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="80"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="180"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程类型" prop="courseTypeName" sortable="custom" show-overflow-tooltip width="140"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="授课讲师" prop="lecturerName" sortable="custom" show-overflow-tooltip width="140"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="开课时间" prop="startTime" sortable="custom" width="230">
|
||||
<template slot-scope="{row}">
|
||||
<span v-if="row.openType === 'long_term'">长期开放</span>
|
||||
<span v-else>{{formatTime(row.startTime)}} 至 {{formatTime(row.endTime)}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程状态" prop="status" sortable="custom" width="120">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="getStatusType(row.status)" size="mini">{{getStatusName(row.status)}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="推荐标识" prop="recommendFlags" sortable="custom" show-overflow-tooltip min-width="140">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag
|
||||
v-for="code in splitValue(row.recommendFlags)"
|
||||
:key="code"
|
||||
:type="getRecommendType(code)"
|
||||
size="mini"
|
||||
style="margin: 2px 3px">
|
||||
{{getDictName(code, recommendOptions)}}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="排序编码" prop="sortNum" sortable="custom" width="120"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="操作" width="260">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||
<el-button @click="goChapterContent(row)" size="mini" type="success">章节内容</el-button>
|
||||
<el-button @click="doDelete(row)" size="mini" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" width="72%">
|
||||
<el-form :model="formData" ref="courseForm" :rules="formRules" label-width="110px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="课程名称" prop="courseName">
|
||||
<el-input v-model="formData.courseName" maxlength="100" placeholder="请输入课程名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="课程类型" prop="courseTypeId">
|
||||
<el-select v-model="formData.courseTypeId" filterable placeholder="请选择课程类型" style="width: 100%">
|
||||
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="授课讲师" prop="lecturerName">
|
||||
<el-input v-model="formData.lecturerName" maxlength="100" placeholder="请输入教师姓名"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="课程状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择课程状态" style="width: 100%">
|
||||
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开课方式" prop="openType">
|
||||
<el-radio-group v-model="formData.openType">
|
||||
<el-radio label="fixed">固定开课时间</el-radio>
|
||||
<el-radio label="long_term">长期开放</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开课时间" prop="openTime" v-if="formData.openType !== 'long_term'">
|
||||
<el-date-picker
|
||||
v-model="formData.openTime"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
range-separator="至"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="推荐标识" prop="recommendFlagList">
|
||||
<el-select v-model="formData.recommendFlagList" multiple clearable placeholder="请选择推荐标识" style="width: 100%">
|
||||
<el-option v-for="item in recommendOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序编码" prop="sortNum">
|
||||
<el-input-number v-model="formData.sortNum" :controls="false" :min="0" :precision="0" style="width: 100%" placeholder="请输入排序编码"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="课程介绍" prop="courseIntro">
|
||||
<el-input v-model="formData.courseIntro" type="textarea" maxlength="2000" :rows="4" show-word-limit placeholder="请输入课程简介"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="适合人群" prop="suitablePeople">
|
||||
<el-input v-model="formData.suitablePeople" type="textarea" maxlength="500" :rows="3" show-word-limit placeholder="请输入适合人群"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学习目标" prop="learningGoal">
|
||||
<el-input v-model="formData.learningGoal" type="textarea" maxlength="500" :rows="3" show-word-limit placeholder="请输入学习目标"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="课程封面" prop="cover">
|
||||
<file-upload
|
||||
:upload_number="1"
|
||||
:upload_size="5242880"
|
||||
:value.sync="formData.cover"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
complete_result
|
||||
upload_mode="image"
|
||||
upload_result_category="interval">
|
||||
</file-upload>
|
||||
<div class="el-upload__tip">只能上传1个文件;只能上传.jpg,.jpeg,.png文件;单个文件大小不能超过5M。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="subDis" @click="operation">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
title: "",
|
||||
dialogVisible: false,
|
||||
subDis: false,
|
||||
courseTypeOptions: [],
|
||||
recommendOptions: [],
|
||||
statusOptions: [
|
||||
{ name: "草稿", code: "draft" },
|
||||
{ name: "待发布", code: "pending" },
|
||||
{ name: "已发布", code: "published" },
|
||||
{ name: "已下架", code: "offline" }
|
||||
],
|
||||
formData: {},
|
||||
formRules: {
|
||||
courseName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
courseTypeId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
lecturerName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
status: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortNum: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
cover: [{ required: true, message: "请上传课程封面", trigger: ["blur", "change"] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildPageParams() {
|
||||
const params = Object.assign({}, this.pageForm)
|
||||
params.startTime = params.courseTime && params.courseTime.length ? params.courseTime[0] : ""
|
||||
params.endTime = params.courseTime && params.courseTime.length ? params.courseTime[1] : ""
|
||||
return params
|
||||
},
|
||||
pageData(data = null) {
|
||||
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
|
||||
this.tableLoading = true
|
||||
this.$axios.post(address, data ? data : this.buildPageParams()).then((res) => {
|
||||
this.tableLoading = false
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
}
|
||||
})
|
||||
},
|
||||
resetSearch() {
|
||||
this.pageForm.courseName = ""
|
||||
this.pageForm.courseTypeId = ""
|
||||
this.pageForm.courseTime = []
|
||||
this.pageForm.lecturerName = ""
|
||||
this.pageForm.status = ""
|
||||
this.pageForm.recommendFlag = ""
|
||||
this.doSearch()
|
||||
},
|
||||
openAdd() {
|
||||
this.title = "新增课程"
|
||||
this.formData = {
|
||||
courseName: "",
|
||||
courseTypeId: "",
|
||||
lecturerName: "",
|
||||
courseIntro: "",
|
||||
suitablePeople: "",
|
||||
learningGoal: "",
|
||||
openType: "fixed",
|
||||
openTime: [],
|
||||
status: "draft",
|
||||
recommendFlagList: [],
|
||||
sortNum: 0,
|
||||
cover: ""
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.courseForm && this.$refs.courseForm.clearValidate()
|
||||
})
|
||||
},
|
||||
openEdit(row) {
|
||||
this.title = "编辑课程"
|
||||
this.formData = Object.assign({}, row, {
|
||||
openTime: row.startTime && row.endTime ? [this.formatTime(row.startTime), this.formatTime(row.endTime)] : [],
|
||||
recommendFlagList: this.splitValue(row.recommendFlags)
|
||||
})
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.courseForm && this.$refs.courseForm.clearValidate()
|
||||
})
|
||||
},
|
||||
goChapterContent(row) {
|
||||
const url = "/platform/learning/chapter/content?courseId=" + encodeURIComponent(row.id || "")
|
||||
if (window.commonUtil && commonUtil.pjaxPush) {
|
||||
commonUtil.pjaxPush(url)
|
||||
} else {
|
||||
window.location.href = url
|
||||
}
|
||||
},
|
||||
operation() {
|
||||
this.$refs.courseForm.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (this.formData.openType !== "long_term" && (!this.formData.openTime || this.formData.openTime.length !== 2)) {
|
||||
this.$message.warning("请选择开课时间")
|
||||
return
|
||||
}
|
||||
const data = Object.assign({}, this.formData)
|
||||
data.startTime = data.openType === "long_term" ? "" : data.openTime[0]
|
||||
data.endTime = data.openType === "long_term" ? "" : data.openTime[1]
|
||||
data.recommendFlags = (data.recommendFlagList || []).join(",")
|
||||
delete data.openTime
|
||||
delete data.recommendFlagList
|
||||
const method = data.id ? "/doEdit" : "/doAdd"
|
||||
this.subDis = true
|
||||
const resp = await axios.post(loc() + method, data)
|
||||
this.subDis = false
|
||||
if (resp.code === 0) {
|
||||
this.dialogVisible = false
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm("确定要删除该课程吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await axios.post(loc() + "/doDelete", { id: row.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
async getCourseTypes() {
|
||||
const resp = await axios.post(loc() + "/courseTypes")
|
||||
if (resp.code === 0) {
|
||||
this.courseTypeOptions = resp.data
|
||||
}
|
||||
},
|
||||
async getLearningDict(name) {
|
||||
const resp = await axios.post(loc() + "/learningDictOptions", { name })
|
||||
return resp.code === 0 ? resp.data : []
|
||||
},
|
||||
splitValue(value) {
|
||||
return value ? value.split(",").filter(Boolean) : []
|
||||
},
|
||||
getStatusName(code) {
|
||||
const item = this.statusOptions.find(v => v.code === code)
|
||||
return item ? item.name : ""
|
||||
},
|
||||
getStatusType(code) {
|
||||
const map = {
|
||||
draft: "info",
|
||||
pending: "warning",
|
||||
published: "success",
|
||||
offline: "danger"
|
||||
}
|
||||
return map[code] || ""
|
||||
},
|
||||
getDictName(code, options) {
|
||||
const item = options.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
},
|
||||
getDictNames(value, options) {
|
||||
const values = this.splitValue(value)
|
||||
return values.map(code => {
|
||||
const item = options.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
}).join("、")
|
||||
},
|
||||
getRecommendType(code) {
|
||||
const name = this.getDictName(code, this.recommendOptions)
|
||||
if (name.includes("热门") || name.includes("推荐")) return "warning"
|
||||
if (name.includes("新")) return "success"
|
||||
return ""
|
||||
},
|
||||
formatTime(time) {
|
||||
return time ? this.$moment(time).format("YYYY-MM-DD HH:mm:ss") : ""
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.pageForm.courseTime = []
|
||||
await this.getCourseTypes()
|
||||
this.recommendOptions = await this.getLearningDict("推荐标识")
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,189 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="课程类型名称">
|
||||
<el-input
|
||||
v-model="pageForm.typeName"
|
||||
clearable
|
||||
placeholder="请输入课程类型名称"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="课程类型列表">
|
||||
<el-button @click="openAdd" size="medium" type="primary">
|
||||
<i class="el-icon-plus"></i>
|
||||
新建
|
||||
</el-button>
|
||||
</table-tool>
|
||||
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border>
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="80"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程类型名称" prop="typeName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="备注" prop="remark" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="排序编号" prop="sortNum" width="140"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="是否启用" prop="enabled" width="140">
|
||||
<template slot-scope="{row}">
|
||||
<el-switch
|
||||
v-model="row.enabled"
|
||||
active-color="#13ce66"
|
||||
inactive-color="#ff4949"
|
||||
@change="switchChange(row)">
|
||||
</el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="操作" width="180">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||
<el-button @click="doDelete(row)" size="mini" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" width="40%">
|
||||
<el-form :model="formData" ref="courseTypeForm" :rules="formRules" label-width="120px">
|
||||
<el-form-item label="课程类型名称" prop="typeName">
|
||||
<el-input v-model="formData.typeName" maxlength="100" placeholder="请输入课程类型名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序编号" prop="sortNum">
|
||||
<el-input-number
|
||||
v-model="formData.sortNum"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
placeholder="请输入排序编号"
|
||||
style="width: 100%">
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否启用" prop="enabled">
|
||||
<el-switch v-model="formData.enabled" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:rows="4"
|
||||
placeholder="请输入备注">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="subDis" @click="operation">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
title: "",
|
||||
dialogVisible: false,
|
||||
subDis: false,
|
||||
formData: {},
|
||||
formRules: {
|
||||
typeName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortNum: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetSearch() {
|
||||
this.pageForm.typeName = ""
|
||||
this.doSearch()
|
||||
},
|
||||
openAdd() {
|
||||
this.title = "新建课程类型"
|
||||
this.formData = {
|
||||
typeName: "",
|
||||
sortNum: 0,
|
||||
enabled: true,
|
||||
remark: ""
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.courseTypeForm && this.$refs.courseTypeForm.clearValidate()
|
||||
})
|
||||
},
|
||||
openEdit(row) {
|
||||
this.title = "编辑课程类型"
|
||||
this.formData = Object.assign({}, row)
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.courseTypeForm && this.$refs.courseTypeForm.clearValidate()
|
||||
})
|
||||
},
|
||||
operation() {
|
||||
const method = this.formData.id ? "/doEdit" : "/doAdd"
|
||||
this.$refs.courseTypeForm.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
this.subDis = true
|
||||
const resp = await axios.post(loc() + method, this.formData)
|
||||
this.subDis = false
|
||||
if (resp.code === 0) {
|
||||
this.dialogVisible = false
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
async switchChange(row) {
|
||||
const resp = await axios.post(loc() + "/doEdit", row)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
row.enabled = !row.enabled
|
||||
}
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm("确定要删除该课程类型吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await axios.post(loc() + "/doDelete", { id: row.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,180 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="工号/姓名">
|
||||
<el-input
|
||||
v-model="pageForm.keyword"
|
||||
clearable
|
||||
placeholder="请输入工号或姓名"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="所属分工会" v-if="unionOptions.length">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属分工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="课程名称">
|
||||
<el-select v-model="pageForm.courseId" clearable filterable placeholder="请选择课程" style="width: 100%">
|
||||
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="是否完成">
|
||||
<el-select v-model="pageForm.completeStatus" clearable placeholder="请选择完成状态" style="width: 100%">
|
||||
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="学习列表"></table-tool>
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="工号" prop="loginName" sortable="custom" show-overflow-tooltip width="120"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="姓名" prop="userName" sortable="custom" show-overflow-tooltip width="110"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属分工会" prop="unionName" sortable="custom" show-overflow-tooltip min-width="150"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="180"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="章节名称" prop="outlineName" sortable="custom" show-overflow-tooltip min-width="160"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习开始时间" prop="firstStudyTime" sortable="custom" width="170">
|
||||
<template slot-scope="{row}">{{formatTime(row.firstStudyTime)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="最近学习时间" prop="latestStudyTime" sortable="custom" width="170">
|
||||
<template slot-scope="{row}">{{formatTime(row.latestStudyTime)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习时长" prop="studySeconds" sortable="custom" width="130">
|
||||
<template slot-scope="{row}">{{row.studyTimeText}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习进度" prop="progressPercent" sortable="custom" width="180">
|
||||
<template slot-scope="{row}">
|
||||
<el-progress :percentage="Number(row.progressPercent || 0)" :stroke-width="8"></el-progress>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="是否完成" prop="completeStatus" sortable="custom" width="110">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="statusType(row.completeStatus)" size="mini">{{statusName(row.completeStatus)}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="操作" width="100">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteRecord(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/learning/study/record",
|
||||
courseOptions: [],
|
||||
unionOptions: [],
|
||||
statusOptions: [
|
||||
{ name: "未开始", code: "not_started", type: "info" },
|
||||
{ name: "学习中", code: "studying", type: "warning" },
|
||||
{ name: "已完成", code: "completed", type: "success" }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pageData() {
|
||||
this.tableLoading = true
|
||||
this.$axios.post(this.apiBase + "/pageData", this.pageForm).then(resp => {
|
||||
this.tableLoading = false
|
||||
if (resp.code === 0) {
|
||||
this.tableData = resp.data.list || []
|
||||
this.pageForm.totalCount = resp.data.totalCount
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.tableLoading = false
|
||||
})
|
||||
},
|
||||
pageOrder(column) {
|
||||
this.pageForm.pageOrderName = column.prop
|
||||
this.pageForm.pageOrderBy = column.order
|
||||
this.pageData()
|
||||
},
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageData()
|
||||
},
|
||||
resetSearch() {
|
||||
this.pageForm.keyword = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.courseId = ""
|
||||
this.pageForm.completeStatus = ""
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageData()
|
||||
},
|
||||
async loadOptions() {
|
||||
const courseResp = await axios.post(this.apiBase + "/courseOptions")
|
||||
if (courseResp.code === 0) {
|
||||
this.courseOptions = courseResp.data || []
|
||||
}
|
||||
const unionResp = await axios.post(this.apiBase + "/unionOptions")
|
||||
if (unionResp.code === 0) {
|
||||
this.unionOptions = unionResp.data || []
|
||||
}
|
||||
},
|
||||
statusName(code) {
|
||||
const item = this.statusOptions.find(v => v.code === code)
|
||||
return item ? item.name : "未开始"
|
||||
},
|
||||
statusType(code) {
|
||||
const item = this.statusOptions.find(v => v.code === code)
|
||||
return item ? item.type : "info"
|
||||
},
|
||||
formatTime(value) {
|
||||
return value ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : ""
|
||||
},
|
||||
deleteRecord(row) {
|
||||
this.$confirm("确定要删除该学习记录吗?删除后该章节的学习时段也会一并清理。", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await axios.post(this.apiBase + "/delete", { id: row.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.$set(this.pageForm, "keyword", "")
|
||||
this.$set(this.pageForm, "unionId", "")
|
||||
this.$set(this.pageForm, "courseId", "")
|
||||
this.$set(this.pageForm, "completeStatus", "")
|
||||
await this.loadOptions()
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,167 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="课程名称">
|
||||
<el-select
|
||||
v-model="pageForm.courseId"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="queryCourseOptions"
|
||||
placeholder="请选择课程"
|
||||
style="width: 100%">
|
||||
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="所属分工会" v-if="unionOptions.length">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属分工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<el-tabs v-model="activeTab" @tab-click="tabChange">
|
||||
<el-tab-pane label="课程统计" name="course"></el-tab-pane>
|
||||
<el-tab-pane label="分工会统计" name="union"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<table-tool :app="this" :label="activeTab === 'course' ? '课程统计' : '分工会统计'"></table-tool>
|
||||
<el-table
|
||||
v-if="activeTab === 'course'"
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="220" width="360"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习人数" prop="learnerCount" sortable="custom" width="130"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="完成人数" prop="completedCount" sortable="custom" width="130"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="平均学习时长" prop="avgStudySeconds" sortable="custom" width="170">
|
||||
<template slot-scope="{row}">{{row.avgStudyTimeText}}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="220"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="分工会名称" prop="unionName" sortable="custom" show-overflow-tooltip min-width="170"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习人数" prop="learnerCount" sortable="custom" width="130"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习时长" prop="studySeconds" sortable="custom" width="160">
|
||||
<template slot-scope="{row}">{{row.studyTimeText}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="完成率" prop="completeRate" sortable="custom" width="130"></el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/learning/statistics",
|
||||
activeTab: "course",
|
||||
courseOptions: [],
|
||||
unionOptions: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pageData() {
|
||||
this.tableLoading = true
|
||||
const url = this.activeTab === "course" ? "/coursePageData" : "/unionPageData"
|
||||
this.$axios.post(this.apiBase + url, this.pageForm).then(resp => {
|
||||
this.tableLoading = false
|
||||
if (resp.code === 0) {
|
||||
this.tableData = resp.data.list || []
|
||||
this.pageForm.totalCount = resp.data.totalCount
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.tableLoading = false
|
||||
})
|
||||
},
|
||||
pageOrder(column) {
|
||||
this.pageForm.pageOrderName = column.prop
|
||||
this.pageForm.pageOrderBy = column.order
|
||||
this.pageData()
|
||||
},
|
||||
async doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
await this.ensureUnionDefaultCourse()
|
||||
this.pageData()
|
||||
},
|
||||
async resetSearch() {
|
||||
this.pageForm.courseId = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.pageOrderName = ""
|
||||
this.pageForm.pageOrderBy = ""
|
||||
this.pageForm.pageNumber = 1
|
||||
await this.queryCourseOptions("")
|
||||
await this.ensureUnionDefaultCourse()
|
||||
this.pageData()
|
||||
},
|
||||
async tabChange() {
|
||||
this.tableData = []
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.pageOrderName = ""
|
||||
this.pageForm.pageOrderBy = ""
|
||||
await this.ensureUnionDefaultCourse()
|
||||
this.pageData()
|
||||
},
|
||||
async queryCourseOptions(query) {
|
||||
const resp = await axios.post(this.apiBase + "/courseOptions", { courseName: query || "" })
|
||||
if (resp.code === 0) {
|
||||
this.courseOptions = resp.data || []
|
||||
}
|
||||
},
|
||||
async ensureUnionDefaultCourse() {
|
||||
if (this.activeTab !== "union" || this.pageForm.courseId) {
|
||||
return
|
||||
}
|
||||
if (!this.courseOptions.length) {
|
||||
await this.queryCourseOptions("")
|
||||
}
|
||||
if (this.courseOptions.length) {
|
||||
this.pageForm.courseId = this.courseOptions[0].id
|
||||
}
|
||||
},
|
||||
async loadUnionOptions() {
|
||||
const resp = await axios.post(this.apiBase + "/unionOptions")
|
||||
if (resp.code === 0) {
|
||||
this.unionOptions = resp.data || []
|
||||
}
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.$set(this.pageForm, "courseId", "")
|
||||
this.$set(this.pageForm, "unionId", "")
|
||||
await this.queryCourseOptions("")
|
||||
await this.loadUnionOptions()
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,494 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<div class="honor-showcase">
|
||||
<van-nav-bar
|
||||
title="劳模先进"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
fixed
|
||||
placeholder
|
||||
@click-left="goBack"
|
||||
></van-nav-bar>
|
||||
|
||||
<div class="honor-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="honor-tab"
|
||||
:class="{ active: activeTab === 'labor' }"
|
||||
@click="switchTab('labor')">
|
||||
劳模风采
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="honor-tab"
|
||||
:class="{ active: activeTab === 'studio' }"
|
||||
@click="switchTab('studio')">
|
||||
创新工作室
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="honor-intro">
|
||||
<div class="honor-intro__corner honor-intro__corner--left"></div>
|
||||
<div class="honor-intro__corner honor-intro__corner--right"></div>
|
||||
<p>一份坚守,淬炼出奋斗的生命价值;一份专注,迸发不断创新的发展动力。</p>
|
||||
<p>他们扎根岗位、追求卓越,以实干书写担当,用匠心点亮前行方向。</p>
|
||||
<p>致敬劳模先进,礼赞奋斗精神。</p>
|
||||
</section>
|
||||
|
||||
<van-loading v-if="loading" class="honor-loading" color="#fff">加载中...</van-loading>
|
||||
|
||||
<div v-else class="honor-list">
|
||||
<article v-for="item in honorList" :key="item.id" class="honor-card">
|
||||
<div class="honor-card__image-wrap">
|
||||
<img v-if="item.cover" class="honor-card__image" :src="item.cover" alt="">
|
||||
<div v-else class="honor-card__placeholder">
|
||||
<div class="honor-card__placeholder-title">{{ getDisplayName(item) }}</div>
|
||||
<div class="honor-card__placeholder-sub">劳模先进</div>
|
||||
</div>
|
||||
<div class="honor-card__name">{{ getDisplayName(item) }}</div>
|
||||
</div>
|
||||
<div class="honor-card__body">
|
||||
<div class="honor-card__gold-line"></div>
|
||||
<div class="honor-card__prize">《{{ item.prizeName || '荣誉奖项' }}》</div>
|
||||
<div class="honor-card__date" v-if="item.grantDate">授予时间:{{ item.grantDate }}</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<van-empty v-if="honorList.length === 0" image="search" description="暂无当年荣誉"></van-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data: function () {
|
||||
return {
|
||||
activeTab: "labor",
|
||||
currentYear: new Date().getFullYear(),
|
||||
honorTypeList: [],
|
||||
honorTypeMap: {
|
||||
labor: null,
|
||||
studio: null
|
||||
},
|
||||
honorList: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack: function () {
|
||||
this.returnHome()
|
||||
},
|
||||
returnHome: function () {
|
||||
if (window.store && typeof window.store.commit === "function") {
|
||||
window.store.commit("setActiveTarBar", "home")
|
||||
}
|
||||
if (!window.$ || !$.support || !$.support.pjax) {
|
||||
window.location.replace("/platform/h5/home")
|
||||
return
|
||||
}
|
||||
var fallbackTimer = window.setTimeout(function () {
|
||||
window.location.replace("/platform/h5/home")
|
||||
}, 1200)
|
||||
$(document).one("pjax:complete", function () {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
})
|
||||
$(document).one("pjax:error pjax:timeout", function () {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
window.location.replace("/platform/h5/home")
|
||||
})
|
||||
$.pjax({
|
||||
url: "/platform/h5/home",
|
||||
container: "#container",
|
||||
fragment: "#container",
|
||||
push: false,
|
||||
replace: true,
|
||||
timeout: 8000
|
||||
})
|
||||
},
|
||||
switchTab: function (tab) {
|
||||
if (this.activeTab === tab) {
|
||||
return
|
||||
}
|
||||
this.activeTab = tab
|
||||
this.listHonor()
|
||||
},
|
||||
getDisplayName: function (item) {
|
||||
if (this.activeTab === "studio") {
|
||||
return item.applyUnionName || item.unionName || item.userName || "创新工作室"
|
||||
}
|
||||
return item.userName || item.applyUnionName || item.unionName || "劳模先进"
|
||||
},
|
||||
getHonorTypes: function () {
|
||||
var self = this
|
||||
return this.$axios.post("/platform/honor/basic/settings/getHonorType").then(function (res) {
|
||||
if (res.code === 0) {
|
||||
self.honorTypeList = res.data || []
|
||||
self.honorTypeMap.labor = self.findHonorType("HONOR_SINGLE", "个人荣誉")
|
||||
self.honorTypeMap.studio = self.findHonorType("HONOR_LIST", "集体荣誉")
|
||||
}
|
||||
})
|
||||
},
|
||||
findHonorType: function (queryTypeCode, name) {
|
||||
var target = this.honorTypeList.find(function (item) {
|
||||
return item.queryTypeCode === queryTypeCode
|
||||
})
|
||||
if (!target) {
|
||||
target = this.honorTypeList.find(function (item) {
|
||||
return item.name === name
|
||||
})
|
||||
}
|
||||
return target || null
|
||||
},
|
||||
listHonor: function () {
|
||||
var self = this
|
||||
var type = this.activeTab === "studio" ? this.honorTypeMap.studio : this.honorTypeMap.labor
|
||||
if (!type) {
|
||||
this.honorList = []
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$axios.post("/platform/honor/manage/h5Data", {
|
||||
year: this.currentYear,
|
||||
honorType: type.id,
|
||||
pageNumber: 1,
|
||||
pageSize: 50
|
||||
}).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
var list = (res.data && res.data.list) || []
|
||||
self.honorList = list.map(function (item) {
|
||||
item.cover = ""
|
||||
item.photoId = self.getFirstPhotoId(item.photoFiles)
|
||||
return item
|
||||
})
|
||||
self.loadPhotoMap()
|
||||
} else {
|
||||
self.honorList = []
|
||||
}
|
||||
}).finally(function () {
|
||||
self.loading = false
|
||||
})
|
||||
},
|
||||
getFirstPhotoId: function (photoFiles) {
|
||||
var files = []
|
||||
if (Array.isArray(photoFiles)) {
|
||||
files = photoFiles
|
||||
} else if (photoFiles) {
|
||||
try {
|
||||
files = JSON.parse(photoFiles)
|
||||
} catch (e) {
|
||||
files = []
|
||||
}
|
||||
}
|
||||
if (!files.length) {
|
||||
return ""
|
||||
}
|
||||
var first = files[0]
|
||||
var data = first && first.response ? first.response.data : first
|
||||
if (!data) {
|
||||
return ""
|
||||
}
|
||||
data = String(data)
|
||||
return data.indexOf("=") > -1 ? data.substring(data.lastIndexOf("=") + 1) : data
|
||||
},
|
||||
loadPhotoMap: function () {
|
||||
var self = this
|
||||
var ids = this.honorList.map(function (item) {
|
||||
return item.photoId
|
||||
}).filter(function (id) {
|
||||
return !!id
|
||||
})
|
||||
if (!ids.length) {
|
||||
return
|
||||
}
|
||||
this.$axios.post("/platform/sys/file/previewFileData", { ids: JSON.stringify(ids) }).then(function (res) {
|
||||
if (res.code !== 0) {
|
||||
return
|
||||
}
|
||||
var fileMap = {}
|
||||
;(res.data || []).forEach(function (file) {
|
||||
fileMap[file.id] = file.thumbnail || file.downloadPath || ""
|
||||
})
|
||||
self.honorList = self.honorList.map(function (item) {
|
||||
item.cover = fileMap[item.photoId] || ""
|
||||
return item
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var self = this
|
||||
this.getHonorTypes().then(function () {
|
||||
self.listHonor()
|
||||
})
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
body {
|
||||
background: #c80019;
|
||||
}
|
||||
|
||||
.honor-showcase {
|
||||
min-height: 100vh;
|
||||
padding: 0 14px 28px;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
radial-gradient(circle at 50% 0, rgba(255, 113, 89, .42) 0, rgba(255, 113, 89, 0) 32%),
|
||||
linear-gradient(180deg, #d6001c 0%, #bd0018 56%, #a90016 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.honor-showcase /deep/ .van-nav-bar {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.honor-showcase /deep/ .van-nav-bar__title {
|
||||
color: #20242a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.honor-showcase /deep/ .van-nav-bar .van-icon,
|
||||
.honor-showcase /deep/ .van-nav-bar__text {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.honor-showcase /deep/ .van-hairline--bottom::after {
|
||||
border-color: #eef1f6;
|
||||
}
|
||||
|
||||
.honor-tabs {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
height: 76px;
|
||||
}
|
||||
|
||||
.honor-tab {
|
||||
position: relative;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: rgba(255, 247, 220, .76);
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
line-height: 28px;
|
||||
padding: 0 8px 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.honor-tab.active {
|
||||
color: #fff6d9;
|
||||
}
|
||||
|
||||
.honor-tab.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 4px;
|
||||
width: 38px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: #ffe6a3;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0 2px 8px rgba(255, 230, 163, .38);
|
||||
}
|
||||
|
||||
.honor-intro {
|
||||
position: relative;
|
||||
margin-bottom: 18px;
|
||||
padding: 20px 20px 46px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
color: #ffe9bd;
|
||||
background:
|
||||
linear-gradient(165deg, rgba(255, 255, 255, .12), rgba(255, 255, 255, 0) 46%),
|
||||
linear-gradient(180deg, #d73535 0%, #c9262f 100%);
|
||||
box-shadow: 0 10px 22px rgba(90, 0, 8, .22);
|
||||
}
|
||||
|
||||
.honor-intro::before,
|
||||
.honor-intro::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -30px;
|
||||
right: -30px;
|
||||
height: 58px;
|
||||
border: 5px solid rgba(255, 223, 151, .86);
|
||||
border-top: 0;
|
||||
border-radius: 0 0 50% 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.honor-intro::before {
|
||||
bottom: 14px;
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
|
||||
.honor-intro::after {
|
||||
bottom: -12px;
|
||||
opacity: .75;
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
|
||||
.honor-intro p {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0 0 9px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.honor-intro p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.honor-intro__corner {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-top: 3px solid rgba(255, 224, 161, .82);
|
||||
}
|
||||
|
||||
.honor-intro__corner--left {
|
||||
left: 14px;
|
||||
border-left: 3px solid rgba(255, 224, 161, .82);
|
||||
}
|
||||
|
||||
.honor-intro__corner--right {
|
||||
right: 14px;
|
||||
border-right: 3px solid rgba(255, 224, 161, .82);
|
||||
}
|
||||
|
||||
.honor-loading {
|
||||
margin-top: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.honor-list {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.honor-card {
|
||||
margin-bottom: 18px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #be0018;
|
||||
box-shadow: 0 10px 22px rgba(83, 0, 9, .28);
|
||||
}
|
||||
|
||||
.honor-card__image-wrap {
|
||||
position: relative;
|
||||
height: 206px;
|
||||
overflow: hidden;
|
||||
background: #e6b06d;
|
||||
}
|
||||
|
||||
.honor-card__image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.honor-card__placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff5d8;
|
||||
background:
|
||||
radial-gradient(circle at 78% 30%, rgba(255, 221, 148, .36), rgba(255, 221, 148, 0) 26%),
|
||||
linear-gradient(140deg, #d93a31 0%, #b60018 100%);
|
||||
}
|
||||
|
||||
.honor-card__placeholder-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.honor-card__placeholder-sub {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0;
|
||||
color: rgba(255, 245, 216, .82);
|
||||
}
|
||||
|
||||
.honor-card__image-wrap::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 58px;
|
||||
background: linear-gradient(165deg, rgba(177, 0, 24, .86) 0%, rgba(177, 0, 24, .74) 62%, rgba(177, 0, 24, .1) 63%);
|
||||
}
|
||||
|
||||
.honor-card__name {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 16px;
|
||||
z-index: 1;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
font-weight: 800;
|
||||
text-shadow: 0 2px 8px rgba(89, 0, 12, .32);
|
||||
}
|
||||
|
||||
.honor-card__body {
|
||||
position: relative;
|
||||
min-height: 82px;
|
||||
padding: 22px 20px 20px;
|
||||
box-sizing: border-box;
|
||||
color: #ffe5ad;
|
||||
background:
|
||||
radial-gradient(circle at 92% 14%, rgba(255, 230, 168, .4), rgba(255, 230, 168, 0) 16%),
|
||||
#be0018;
|
||||
}
|
||||
|
||||
.honor-card__body::after {
|
||||
content: "★";
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: -20px;
|
||||
color: #ffd886;
|
||||
font-size: 42px;
|
||||
text-shadow: 0 3px 8px rgba(85, 0, 9, .28);
|
||||
transform: rotate(-12deg);
|
||||
}
|
||||
|
||||
.honor-card__gold-line {
|
||||
width: 44px;
|
||||
height: 3px;
|
||||
border-radius: 999px;
|
||||
background: #e7b13c;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.honor-card__prize {
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 700;
|
||||
color: #ffe5ad;
|
||||
}
|
||||
|
||||
.honor-card__date {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: rgba(255, 229, 173, .72);
|
||||
}
|
||||
|
||||
.honor-showcase /deep/ .van-empty__description {
|
||||
color: rgba(255, 255, 255, .78);
|
||||
}
|
||||
`
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -129,6 +129,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<van-field label="姓名" readonly v-model="signupForm.userName"></van-field>
|
||||
<van-field label="工号" readonly v-model="signupForm.jobNo"></van-field>
|
||||
<van-field label="身份证号码" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号码"></van-field>
|
||||
<van-field label="性别" readonly v-model="signupForm.gender"></van-field>
|
||||
<van-field label="年龄" readonly :value="staffAge(signupForm)"></van-field>
|
||||
<van-field label="手机号" v-model="signupForm.mobile" type="tel" maxlength="30" placeholder="请填写手机号"></van-field>
|
||||
<van-field label="所属工会" readonly v-model="signupForm.unionName"></van-field>
|
||||
|
||||
@@ -156,7 +156,7 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
|
||||
.tour-line-shortcuts {
|
||||
display: grid;
|
||||
display: none;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
@@ -212,29 +212,54 @@ layout("/layouts/platform_h5.html"){
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.tour-line-period-entry {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
padding: 0 12px;
|
||||
background: #ffffff;
|
||||
color: #0f172a;
|
||||
.tour-line-period-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 2px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.tour-line-period-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tour-line-period-chip {
|
||||
min-width: 78px;
|
||||
max-width: 168px;
|
||||
min-height: 34px;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 17px;
|
||||
padding: 0 12px;
|
||||
background: #ffffff;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-line-period-entry--active {
|
||||
color: #0b75bd;
|
||||
.tour-line-period-chip--active {
|
||||
border-color: #0b75bd;
|
||||
background: #0b75bd;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tour-line-period-placeholder {
|
||||
color: #64748b;
|
||||
.tour-line-period-tip {
|
||||
margin-top: 6px;
|
||||
color: #d97706;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.tour-line-list {
|
||||
@@ -465,14 +490,18 @@ layout("/layouts/platform_h5.html"){
|
||||
</div>
|
||||
</div>
|
||||
<div class="tour-line-filter-bar">
|
||||
<button
|
||||
type="button"
|
||||
class="tour-line-period-entry"
|
||||
:class="{'tour-line-period-entry--active': !!pageForm.travelPeriod}"
|
||||
@click="openTravelPeriodPopup">
|
||||
<span :class="{'tour-line-period-placeholder': !pageForm.travelPeriod}">{{ selectedTravelPeriodText() }}</span>
|
||||
<van-icon name="arrow-down"></van-icon>
|
||||
</button>
|
||||
<div class="tour-line-period-list">
|
||||
<button
|
||||
v-for="item in visibleTravelPeriodList"
|
||||
:key="item.value || 'all'"
|
||||
type="button"
|
||||
class="tour-line-period-chip"
|
||||
:class="{'tour-line-period-chip--active': pageForm.travelPeriod === item.value}"
|
||||
@click="selectTravelPeriod(item)">
|
||||
{{ item.name }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tour-line-period-tip">可选择指定出行时间,快速查看对应线路。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -485,7 +514,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<div class="tour-line-list">
|
||||
<div class="tour-line-card" v-for="row in list" :key="row.matterId" @click="onLine(row)">
|
||||
<div class="tour-line-thumb">
|
||||
<van-image v-if="thumbUrl(row)" :src="thumbUrl(row)" fit="cover" lazy-load>
|
||||
<van-image v-if="row._thumbUrl" :src="row._thumbUrl" fit="cover">
|
||||
<template #loading>
|
||||
<div class="tour-line-thumb-empty">图片加载中</div>
|
||||
</template>
|
||||
@@ -556,25 +585,6 @@ layout("/layouts/platform_h5.html"){
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-popup v-model="showTravelPeriodPopup" position="bottom" round>
|
||||
<div class="tour-line-union-popup">
|
||||
<div class="tour-line-union-popup__title">选择出行时段</div>
|
||||
<div class="tour-line-union-popup__list">
|
||||
<van-cell
|
||||
v-for="item in travelPeriodList"
|
||||
:key="item.value || 'all'"
|
||||
clickable
|
||||
class="tour-line-union-popup__item"
|
||||
:class="{'tour-line-union-popup__item--active': pageForm.travelPeriod === item.value}"
|
||||
:title="item.name"
|
||||
@click="selectTravelPeriod(item)">
|
||||
<template #right-icon>
|
||||
<van-icon v-if="pageForm.travelPeriod === item.value" name="success" color="#0b75bd"></van-icon>
|
||||
</template>
|
||||
</van-cell>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
@@ -588,13 +598,10 @@ layout("/layouts/platform_h5.html"){
|
||||
finished: false,
|
||||
lineChecking: false,
|
||||
showUnionPopup: false,
|
||||
showTravelPeriodPopup: false,
|
||||
unionList: [
|
||||
{id: "", name: "全部分工会"}
|
||||
],
|
||||
travelPeriodList: [
|
||||
{value: "", name: "全部出行时段"}
|
||||
],
|
||||
travelPeriodList: [],
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
@@ -610,9 +617,56 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visibleTravelPeriodList() {
|
||||
return this.travelPeriodList.filter((item) => item && item.value)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
thumbUrl(row) {
|
||||
return row.lineMobileThumb || row.agencyMobileThumb || ""
|
||||
const thumb = this.resolveThumbPath(row && (row.lineMobileThumb || row.agencyMobileThumb))
|
||||
if (!thumb) {
|
||||
return ""
|
||||
}
|
||||
if (thumb.indexOf("http://") === 0 || thumb.indexOf("https://") === 0 || thumb.indexOf("//") === 0 || thumb.indexOf("data:") === 0) {
|
||||
return thumb
|
||||
}
|
||||
const domain = "${AppFileDomain!}" || "${AppDomain!}" || ""
|
||||
if (!domain) {
|
||||
return thumb
|
||||
}
|
||||
const prefix = domain.lastIndexOf("/") === domain.length - 1 ? domain.substring(0, domain.length - 1) : domain
|
||||
const path = thumb.indexOf("/") === 0 ? thumb : "/" + thumb
|
||||
return prefix + path
|
||||
},
|
||||
resolveThumbPath(value) {
|
||||
if (!value) {
|
||||
return ""
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.length ? this.resolveThumbPath(value[0]) : ""
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return value.thumbnail || value.downloadPath || value.url || value.data || value.path || ""
|
||||
}
|
||||
const text = String(value).trim()
|
||||
if (!text) {
|
||||
return ""
|
||||
}
|
||||
if (text.indexOf("[") === 0 || text.indexOf("{") === 0) {
|
||||
try {
|
||||
return this.resolveThumbPath(JSON.parse(text))
|
||||
} catch (e) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return text.split(",").map((item) => item.trim()).filter((item) => item)[0] || ""
|
||||
},
|
||||
normalizeLineRows(rows) {
|
||||
return (rows || []).map((row) => {
|
||||
row._thumbUrl = this.thumbUrl(row)
|
||||
return row
|
||||
})
|
||||
},
|
||||
lineTypeBadge(lineType) {
|
||||
if (lineType === "省内线路" || lineType === "省内") {
|
||||
@@ -623,13 +677,6 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
return ""
|
||||
},
|
||||
selectedTravelPeriodText() {
|
||||
if (!this.pageForm.travelPeriod) {
|
||||
return "选择出行时段"
|
||||
}
|
||||
const selected = this.travelPeriodList.find((item) => item.value === this.pageForm.travelPeriod)
|
||||
return selected && selected.name ? selected.name : this.pageForm.travelPeriod
|
||||
},
|
||||
isSigned(row) {
|
||||
return !!(row && (row.signed === true || row.signed === 1 || row.signed === "1" || row.ledgerId))
|
||||
},
|
||||
@@ -769,12 +816,6 @@ layout("/layouts/platform_h5.html"){
|
||||
this.loadUnionOptions()
|
||||
}
|
||||
},
|
||||
openTravelPeriodPopup() {
|
||||
this.showTravelPeriodPopup = true
|
||||
if (this.travelPeriodList.length <= 1) {
|
||||
this.loadTravelPeriodOptions()
|
||||
}
|
||||
},
|
||||
selectUnion(item) {
|
||||
const unionId = item && item.id ? item.id : ""
|
||||
this.showUnionPopup = false
|
||||
@@ -786,8 +827,9 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
selectTravelPeriod(item) {
|
||||
const travelPeriod = item && item.value ? item.value : ""
|
||||
this.showTravelPeriodPopup = false
|
||||
if (this.pageForm.travelPeriod === travelPeriod) {
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.doSearch()
|
||||
return
|
||||
}
|
||||
this.pageForm.travelPeriod = travelPeriod
|
||||
@@ -817,19 +859,14 @@ layout("/layouts/platform_h5.html"){
|
||||
return
|
||||
}
|
||||
const rows = res.data || []
|
||||
this.travelPeriodList = [{value: "", name: "全部出行时段"}].concat(rows.map((item) => {
|
||||
this.travelPeriodList = rows.map((item) => {
|
||||
return {
|
||||
value: item.travelPeriod || "",
|
||||
name: item.travelPeriod || "未设置出行时间"
|
||||
}
|
||||
}).filter((item) => item.value))
|
||||
})
|
||||
},
|
||||
showSignupNotice() {
|
||||
vant.Dialog.alert({
|
||||
title: "温馨提示",
|
||||
message: "请确认并选择对应的出行路线时间段进行报名",
|
||||
confirmButtonColor: "#1867b0"
|
||||
}).filter((item) => item.value).sort((a, b) => {
|
||||
return String(b.value).localeCompare(String(a.value))
|
||||
})
|
||||
})
|
||||
},
|
||||
loadData() {
|
||||
@@ -846,7 +883,7 @@ layout("/layouts/platform_h5.html"){
|
||||
return
|
||||
}
|
||||
const data = res.data || {}
|
||||
const rows = data.list || []
|
||||
const rows = this.normalizeLineRows(data.list || [])
|
||||
this.pageForm.totalCount = data.totalCount || 0
|
||||
this.list = this.pageForm.pageNumber === 1 ? rows : this.list.concat(rows)
|
||||
if (this.list.length >= this.pageForm.totalCount || rows.length === 0) {
|
||||
@@ -899,7 +936,6 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.showSignupNotice()
|
||||
this.loadUnionOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadData()
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.learning-course-page {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f7fb;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-course-page .van-nav-bar {
|
||||
background: #f4f7fb;
|
||||
}
|
||||
.learning-banner-wrap {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 14px 0;
|
||||
}
|
||||
.learning-banner {
|
||||
height: 128px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: #dce7f5;
|
||||
}
|
||||
.learning-banner-item {
|
||||
position: relative;
|
||||
height: 128px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0b75bd, #33b3a6);
|
||||
}
|
||||
.learning-banner-item img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.learning-banner-fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 22px;
|
||||
color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
radial-gradient(circle at 14% 20%, rgba(255,255,255,.26), transparent 22%),
|
||||
linear-gradient(135deg, #0b75bd, #35b6a8 55%, #f6b24d);
|
||||
}
|
||||
.learning-banner-title {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 28px 14px 12px;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
background: linear-gradient(to top, rgba(15, 23, 42, .64), transparent);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-banner-fallback .learning-banner-title {
|
||||
position: static;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.learning-filter-card {
|
||||
flex-shrink: 0;
|
||||
margin: 14px 14px 0;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.learning-list-scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-filter-group {
|
||||
display: grid;
|
||||
grid-template-columns: 112px minmax(0, 1fr);
|
||||
min-height: 72px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: #eef6ff;
|
||||
}
|
||||
.learning-filter-group + .learning-filter-group {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.learning-filter-category {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-filter-category-text {
|
||||
min-width: 0;
|
||||
}
|
||||
.learning-filter-category-title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.learning-filter-options {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
background: rgba(255, 255, 255, .55);
|
||||
}
|
||||
.learning-filter-options::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.learning-filter-item {
|
||||
flex: 0 0 72px;
|
||||
min-width: 72px;
|
||||
padding: 8px 2px 7px;
|
||||
border-left: 1px solid rgba(255, 255, 255, .72);
|
||||
text-align: center;
|
||||
color: #5f6b7a;
|
||||
font-size: 11px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-filter-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 auto 5px;
|
||||
border-radius: 50%;
|
||||
color: #4f87c8;
|
||||
font-size: 18px;
|
||||
background: rgba(255, 255, 255, .78);
|
||||
}
|
||||
.learning-filter-item.no-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 58px;
|
||||
color: #0b75bd;
|
||||
font-weight: 700;
|
||||
}
|
||||
.learning-filter-item.active {
|
||||
color: #0b75bd;
|
||||
font-weight: 700;
|
||||
background: rgba(255, 255, 255, .86);
|
||||
}
|
||||
.learning-filter-item.active .learning-filter-icon {
|
||||
color: #ffffff;
|
||||
background: #0b75bd;
|
||||
}
|
||||
.learning-filter-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.learning-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 16px 10px;
|
||||
}
|
||||
.learning-section-title {
|
||||
color: #202733;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.learning-section-total {
|
||||
color: #8b96a6;
|
||||
font-size: 13px;
|
||||
}
|
||||
.learning-course-list {
|
||||
padding: 0 12px 18px;
|
||||
}
|
||||
.learning-course-card {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
min-height: 104px;
|
||||
margin-bottom: 12px;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-course-cover {
|
||||
position: relative;
|
||||
flex: 0 0 142px;
|
||||
width: 142px;
|
||||
height: 82px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #e6edf6;
|
||||
}
|
||||
.learning-course-cover img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.learning-course-cover-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(135deg, #3a7bd5, #8b5cf6);
|
||||
}
|
||||
.learning-course-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.learning-course-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
}
|
||||
.learning-course-title-text {
|
||||
min-width: 0;
|
||||
color: #202733;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.learning-course-badge {
|
||||
flex-shrink: 0;
|
||||
max-width: 72px;
|
||||
padding: 1px 6px;
|
||||
border: 1px solid #ffd1a8;
|
||||
border-radius: 4px;
|
||||
color: #f28a35;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: #fff7ee;
|
||||
}
|
||||
.learning-course-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
.learning-course-intro {
|
||||
margin-top: 8px;
|
||||
color: #7a8798;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.learning-empty {
|
||||
padding: 22px 0 36px;
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.learning-filter-card {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
.learning-filter-group {
|
||||
grid-template-columns: 94px minmax(0, 1fr);
|
||||
}
|
||||
.learning-filter-category {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
.learning-filter-category-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
.learning-filter-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 17px;
|
||||
}
|
||||
.learning-filter-item {
|
||||
font-size: 11px;
|
||||
}
|
||||
.learning-course-cover {
|
||||
flex-basis: 120px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="learning-course-page" v-cloak>
|
||||
<van-nav-bar title="学习教育" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<div class="learning-banner-wrap">
|
||||
<van-swipe class="learning-banner" :autoplay="3500" indicator-color="#ffffff">
|
||||
<van-swipe-item v-for="course in bannerCourses" :key="course.id" @click="openCourse(course)">
|
||||
<div class="learning-banner-item">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="learning-banner-fallback">
|
||||
<div class="learning-banner-title">{{ course.courseName || '学习教育' }}</div>
|
||||
</div>
|
||||
<div v-if="getCoverUrl(course.cover)" class="learning-banner-title">{{ course.courseName || '学习教育' }}</div>
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
<van-swipe-item v-if="!bannerCourses.length">
|
||||
<div class="learning-banner-item learning-banner-fallback">
|
||||
<div class="learning-banner-title">学习教育</div>
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
|
||||
<div class="learning-filter-card">
|
||||
<div class="learning-filter-group" v-for="group in filterGroups" :key="group.key">
|
||||
<div class="learning-filter-category" :style="{background: group.color}">
|
||||
<div class="learning-filter-category-text">
|
||||
<div class="learning-filter-category-title">{{ group.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="learning-filter-options">
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
class="learning-filter-item"
|
||||
:class="{active: isFilterActive(item), 'no-icon': !item.icon}"
|
||||
@click="selectFilter(item)">
|
||||
<div v-if="item.icon" class="learning-filter-icon">
|
||||
<van-icon :name="item.icon"></van-icon>
|
||||
</div>
|
||||
<div class="learning-filter-name">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="learning-list-scroll">
|
||||
<div class="learning-section-head">
|
||||
<div class="learning-section-title">{{ listTitle }}</div>
|
||||
<div class="learning-section-total">共 {{ pageForm.totalCount || 0 }} 门</div>
|
||||
</div>
|
||||
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list
|
||||
v-model="loading"
|
||||
:finished="finished"
|
||||
:immediate-check="false"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad">
|
||||
<div class="learning-course-list">
|
||||
<div
|
||||
class="learning-course-card"
|
||||
v-for="course in courseList"
|
||||
:key="course.id"
|
||||
@click="openCourse(course)">
|
||||
<div class="learning-course-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="learning-course-cover-empty">{{ course.courseTypeName || '课程' }}</div>
|
||||
</div>
|
||||
<div class="learning-course-info">
|
||||
<div class="learning-course-title">
|
||||
<div class="learning-course-title-text">{{ course.courseName || '未命名课程' }}</div>
|
||||
<div v-if="firstRecommendName(course.recommendFlags)" class="learning-course-badge">{{ firstRecommendName(course.recommendFlags) }}</div>
|
||||
</div>
|
||||
<div class="learning-course-meta">
|
||||
<van-tag v-if="course.courseTypeName" plain type="primary">{{ course.courseTypeName }}</van-tag>
|
||||
<van-tag v-if="course.lecturerName" plain type="success">{{ course.lecturerName }}</van-tag>
|
||||
</div>
|
||||
<div class="learning-course-intro">{{ course.courseIntro || '暂无课程介绍' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<van-empty v-if="!loading && !courseList.length" class="learning-empty" description="暂无相关课程"></van-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/learning/course/display",
|
||||
query: {
|
||||
keyword: "",
|
||||
courseTypeId: "",
|
||||
recommendFlag: ""
|
||||
},
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0
|
||||
},
|
||||
courseList: [],
|
||||
bannerCourses: [],
|
||||
courseTypeOptions: [],
|
||||
recommendOptions: [],
|
||||
loading: false,
|
||||
finished: false,
|
||||
refreshing: false,
|
||||
requestSeq: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterGroups() {
|
||||
const recommendItems = this.recommendOptions.map((item, index) => ({
|
||||
key: "recommend_" + item.code,
|
||||
type: "recommend",
|
||||
value: item.code,
|
||||
name: item.name,
|
||||
icon: this.recommendIcon(index)
|
||||
}))
|
||||
const typeItems = this.courseTypeOptions.map((item, index) => ({
|
||||
key: "type_" + item.id,
|
||||
type: "type",
|
||||
value: item.id,
|
||||
name: item.typeName,
|
||||
icon: this.typeIcon(index)
|
||||
}))
|
||||
return [
|
||||
{
|
||||
key: "recommend",
|
||||
name: "推荐标识",
|
||||
color: "linear-gradient(135deg, #ff8a3d, #ffbf5e)",
|
||||
items: recommendItems
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
name: "课程类型",
|
||||
color: "linear-gradient(135deg, #2878d7, #64a8f4)",
|
||||
items: typeItems
|
||||
}
|
||||
]
|
||||
},
|
||||
listTitle() {
|
||||
const recommend = this.recommendOptions.find(item => item.code === this.query.recommendFlag)
|
||||
const type = this.courseTypeOptions.find(item => item.id === this.query.courseTypeId)
|
||||
if (recommend && type) {
|
||||
return recommend.name + " · " + type.typeName
|
||||
}
|
||||
if (recommend) {
|
||||
return recommend.name
|
||||
}
|
||||
if (type) {
|
||||
return type.typeName
|
||||
}
|
||||
return "我的课堂"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
async loadOptions() {
|
||||
const [typeResp, recommendResp] = await Promise.all([
|
||||
axios.post(this.apiBase + "/courseTypes"),
|
||||
axios.post(this.apiBase + "/recommendOptions")
|
||||
])
|
||||
if (typeResp.code === 0) {
|
||||
this.courseTypeOptions = typeResp.data || []
|
||||
}
|
||||
if (recommendResp.code === 0) {
|
||||
this.recommendOptions = recommendResp.data || []
|
||||
}
|
||||
},
|
||||
onRefresh() {
|
||||
this.finished = false
|
||||
this.pageForm.pageNumber = 1
|
||||
this.courseList = []
|
||||
this.onLoad()
|
||||
},
|
||||
onLoad() {
|
||||
this.fetchCourses()
|
||||
},
|
||||
fetchCourses() {
|
||||
this.loading = true
|
||||
const requestSeq = ++this.requestSeq
|
||||
this.$axios.post(this.apiBase + "/pageData", {
|
||||
keyword: this.query.keyword,
|
||||
courseTypeId: this.query.courseTypeId,
|
||||
recommendFlag: this.query.recommendFlag,
|
||||
pageNumber: this.pageForm.pageNumber,
|
||||
pageSize: this.pageForm.pageSize
|
||||
}).then((res) => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
const list = data.list || []
|
||||
if (this.refreshing || this.pageForm.pageNumber === 1) {
|
||||
this.courseList = []
|
||||
}
|
||||
this.courseList = this.courseList.concat(list)
|
||||
this.pageForm.totalCount = data.totalCount || 0
|
||||
this.pageForm.pageNumber += 1
|
||||
this.finished = this.courseList.length >= this.pageForm.totalCount
|
||||
this.syncBannerCourses()
|
||||
} else {
|
||||
this.finished = true
|
||||
this.$toast(res.msg || "查询失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
if (requestSeq === this.requestSeq) {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
})
|
||||
},
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.courseList = []
|
||||
this.finished = false
|
||||
this.loading = true
|
||||
this.onLoad()
|
||||
},
|
||||
selectFilter(item) {
|
||||
if (!item) return
|
||||
if (item.type === "recommend") {
|
||||
this.query.recommendFlag = item.value || ""
|
||||
} else if (item.type === "type") {
|
||||
this.query.courseTypeId = item.value || ""
|
||||
}
|
||||
this.doSearch()
|
||||
},
|
||||
isFilterActive(item) {
|
||||
if (!item) return false
|
||||
if (item.type === "recommend") {
|
||||
return (this.query.recommendFlag || "") === (item.value || "")
|
||||
}
|
||||
if (item.type === "type") {
|
||||
return (this.query.courseTypeId || "") === (item.value || "")
|
||||
}
|
||||
return false
|
||||
},
|
||||
syncBannerCourses() {
|
||||
const withCover = this.courseList.filter(item => this.getCoverUrl(item.cover)).slice(0, 5)
|
||||
this.bannerCourses = (withCover.length ? withCover : this.courseList.slice(0, 5))
|
||||
},
|
||||
getCoverUrl(cover) {
|
||||
if (!cover) return ""
|
||||
if (Array.isArray(cover)) {
|
||||
return cover.length ? (cover[0].url || (cover[0].response && cover[0].response.data) || cover[0].data || "") : ""
|
||||
}
|
||||
if (typeof cover === "string") {
|
||||
const text = cover.trim()
|
||||
if (!text) return ""
|
||||
if (text.startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(text)
|
||||
return files.length ? (files[0].url || (files[0].response && files[0].response.data) || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
},
|
||||
openCourse(course) {
|
||||
if (!course || !course.id) return
|
||||
pjaxReplace("/platform/learning/course/h5/study?id=" + course.id)
|
||||
},
|
||||
splitFlags(flags) {
|
||||
return flags ? flags.split(",").filter(Boolean) : []
|
||||
},
|
||||
getRecommendName(code) {
|
||||
const item = this.recommendOptions.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
},
|
||||
firstRecommendName(flags) {
|
||||
const codes = this.splitFlags(flags)
|
||||
return codes.length ? this.getRecommendName(codes[0]) : ""
|
||||
},
|
||||
recommendIcon(index) {
|
||||
const icons = ["fire-o", "star-o", "award-o", "gem-o", "flag-o", "good-job-o"]
|
||||
return icons[index % icons.length]
|
||||
},
|
||||
typeIcon(index) {
|
||||
const icons = ["video-o", "records-o", "description-o", "bookmark-o", "cluster-o", "desktop-o"]
|
||||
return icons[index % icons.length]
|
||||
},
|
||||
palette(index) {
|
||||
const colors = ["#28a6df", "#f2b84b", "#f46d5f", "#7b8fda", "#59bbb2", "#2d7db8", "#ee755b", "#f3c256", "#ef9461", "#8ba4dc"]
|
||||
return colors[index % colors.length]
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadOptions()
|
||||
this.onLoad()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,732 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.learning-study-page {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
.study-hero {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: 195px;
|
||||
background: linear-gradient(135deg, #7554e8, #48b7f0);
|
||||
overflow: hidden;
|
||||
}
|
||||
.study-hero img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.study-hero-empty {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28px;
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.study-back {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 8px;
|
||||
z-index: 2;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
background: rgba(15, 23, 42, .22);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.study-title-block {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 18px 10px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.study-title {
|
||||
color: #202733;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.study-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-top: 14px;
|
||||
color: #8a93a3;
|
||||
font-size: 14px;
|
||||
}
|
||||
.study-meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.study-tabs {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
}
|
||||
.study-tabs .van-tabs__content {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.study-tabs .van-tabs__wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.study-tabs .van-tabs__track {
|
||||
height: 100%;
|
||||
}
|
||||
.study-tabs .van-tab__pane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.study-intro-scroll,
|
||||
.study-catalog-scroll {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
.study-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 18px;
|
||||
color: #8b94a3;
|
||||
font-size: 16px;
|
||||
}
|
||||
.study-summary strong {
|
||||
color: #202733;
|
||||
font-size: 20px;
|
||||
}
|
||||
.study-card {
|
||||
margin: 18px;
|
||||
padding: 14px 0;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, .04);
|
||||
overflow: hidden;
|
||||
}
|
||||
.study-chapter {
|
||||
padding: 0 18px;
|
||||
}
|
||||
.study-chapter-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0 16px;
|
||||
color: #202733;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.study-chapter-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #ff9b2f;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.study-resource {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 14px 0 12px 18px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
}
|
||||
.study-resource-icon {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
margin-top: 2px;
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.study-resource-icon.video {
|
||||
background: #ffbd2e;
|
||||
}
|
||||
.study-resource-icon.audio {
|
||||
background: #33b3a6;
|
||||
}
|
||||
.study-resource-icon.image {
|
||||
background: #5a94f2;
|
||||
}
|
||||
.study-resource-icon.doc {
|
||||
background: #ff6868;
|
||||
}
|
||||
.study-resource-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.study-resource-title {
|
||||
color: #5c6470;
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.study-resource-status {
|
||||
margin-top: 8px;
|
||||
color: #a2a9b5;
|
||||
font-size: 15px;
|
||||
}
|
||||
.study-intro {
|
||||
padding: 18px;
|
||||
color: #4e5969;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
background: #ffffff;
|
||||
min-height: 180px;
|
||||
}
|
||||
.study-player-popup {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0;
|
||||
background: #000000;
|
||||
}
|
||||
.study-player-head {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
color: #ffffff;
|
||||
background: #101828;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, .08);
|
||||
}
|
||||
.study-player-title {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.study-player-body {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
background: #000000;
|
||||
}
|
||||
.study-media {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
background: #000000;
|
||||
border-radius: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
.study-audio-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 220px;
|
||||
border-radius: 0;
|
||||
background: linear-gradient(135deg, #10233f, #0f766e);
|
||||
}
|
||||
.study-audio-wrap audio {
|
||||
width: 88%;
|
||||
}
|
||||
.study-image-preview {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
border-radius: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
.study-doc-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
.study-file-fallback {
|
||||
padding: 46px 18px;
|
||||
color: #7a8798;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
background: #ffffff;
|
||||
border-radius: 0;
|
||||
}
|
||||
.study-player-exit {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.study-player-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-left: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="learning-study-page" v-cloak>
|
||||
<div class="study-hero">
|
||||
<div class="study-back" @click="goBack"><van-icon name="arrow-left"></van-icon></div>
|
||||
<img v-if="coverUrl" :src="coverUrl" :alt="course.courseName">
|
||||
<div v-else class="study-hero-empty">{{ course.courseName || '课程学习' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="study-title-block">
|
||||
<div class="study-title">{{ course.courseName || '课程学习' }}</div>
|
||||
<div class="study-meta">
|
||||
<span class="study-meta-item"><van-icon name="eye-o"></van-icon>{{ studyCountText }}</span>
|
||||
<span class="study-meta-item"><van-icon name="bookmark-o"></van-icon>收藏</span>
|
||||
<span class="study-meta-item"><van-icon name="share-o"></van-icon>分享</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-tabs v-model="activeTab" class="study-tabs" color="#2d9cff" line-width="34px" title-active-color="#202733">
|
||||
<van-tab title="课程介绍" name="intro">
|
||||
<div class="study-intro-scroll">
|
||||
<div class="study-intro">{{ course.courseIntro || '暂无课程介绍' }}</div>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="课程目录" name="catalog">
|
||||
<div class="study-catalog-scroll">
|
||||
<div class="study-summary">
|
||||
<div>共 <strong>{{ chapterCount }}</strong> 个章节,<strong>{{ totalMinutes }}</strong> 分钟</div>
|
||||
<div>总学习进度 <strong>{{ totalProgress }}%</strong></div>
|
||||
</div>
|
||||
|
||||
<div v-for="chapter in catalogList" :key="chapter.id" class="study-card">
|
||||
<div class="study-chapter">
|
||||
<div class="study-chapter-head" @click="selectChapter(chapter)">
|
||||
<span class="study-chapter-dot"></span>
|
||||
<span>{{ chapter.title }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="resource in chapter.resources"
|
||||
:key="resource.id"
|
||||
class="study-resource"
|
||||
@click="openResource(resource)">
|
||||
<div class="study-resource-icon" :class="resourceIconClass(resource)">
|
||||
<van-icon :name="resourceIcon(resource)"></van-icon>
|
||||
</div>
|
||||
<div class="study-resource-main">
|
||||
<div class="study-resource-title">{{ resource.title }}</div>
|
||||
<div class="study-resource-status">{{ statusText(resource) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-empty v-if="!catalogList.length" description="暂无课程目录"></van-empty>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
|
||||
<van-popup v-model="playerVisible" position="bottom" class="study-player-popup" :overlay="false">
|
||||
<div class="study-player-head">
|
||||
<div class="study-player-title">{{ selectedResource.title || '学习资料' }}</div>
|
||||
<div class="study-player-actions">
|
||||
<span v-if="selectedResource.resourceType === 'video'" @click="enterVideoFullscreen">横屏</span>
|
||||
<span @click="closePlayer">退出</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="study-player-body">
|
||||
<video
|
||||
v-if="selectedResource.resourceType === 'video'"
|
||||
ref="mediaPlayer"
|
||||
class="study-media"
|
||||
:src="fileUrl"
|
||||
controls
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-video-player-type="h5"
|
||||
x5-video-orientation="landscape|portrait"
|
||||
x-webkit-airplay="allow"
|
||||
@play="startStudy"
|
||||
@timeupdate="saveProgress"
|
||||
@ended="finishStudy(false)">
|
||||
</video>
|
||||
<div v-else-if="selectedResource.resourceType === 'audio'" class="study-audio-wrap">
|
||||
<audio
|
||||
ref="mediaPlayer"
|
||||
:src="fileUrl"
|
||||
controls
|
||||
@play="startStudy"
|
||||
@timeupdate="saveProgress"
|
||||
@ended="finishStudy(false)">
|
||||
</audio>
|
||||
</div>
|
||||
<img v-else-if="selectedResource.resourceType === 'image'" class="study-image-preview" :src="fileUrl" :alt="selectedResource.title">
|
||||
<iframe v-else-if="canInlinePreview(selectedResource)" class="study-doc-frame" :src="inlinePreviewUrl"></iframe>
|
||||
<div v-else class="study-file-fallback">
|
||||
<p>该资料暂不支持内嵌预览</p>
|
||||
<van-button type="info" size="small" @click="openFile">打开资料</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
courseId: "",
|
||||
apiBase: "/platform/learning/course/display",
|
||||
recordApi: "/platform/learning/study/record",
|
||||
activeTab: "catalog",
|
||||
course: {},
|
||||
treeData: [],
|
||||
selectedResource: {},
|
||||
playerVisible: false,
|
||||
currentSegmentId: "",
|
||||
studying: false,
|
||||
pendingSeconds: 0,
|
||||
heartbeatTimer: null,
|
||||
navigatingBack: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coverUrl() {
|
||||
return this.getFileUrl(this.course.cover)
|
||||
},
|
||||
fileUrl() {
|
||||
return this.getFileUrl(this.selectedResource.fileData)
|
||||
},
|
||||
inlinePreviewUrl() {
|
||||
const id = this.getFileId(this.selectedResource.fileData)
|
||||
return id ? this.apiBase + "/pdfPreview?id=" + encodeURIComponent(id) : this.fileUrl
|
||||
},
|
||||
catalogList() {
|
||||
const result = []
|
||||
;(this.treeData || []).forEach(node => this.appendCatalogNode(node, result))
|
||||
return result
|
||||
},
|
||||
chapterCount() {
|
||||
return this.catalogList.length
|
||||
},
|
||||
totalMinutes() {
|
||||
const seconds = this.catalogList.reduce((sum, chapter) => {
|
||||
return sum + chapter.resources.reduce((value, item) => value + Number(item.durationSeconds || 0), 0)
|
||||
}, 0)
|
||||
return Math.max(0, Math.ceil(seconds / 60))
|
||||
},
|
||||
totalProgress() {
|
||||
const resources = []
|
||||
this.catalogList.forEach(chapter => resources.push.apply(resources, chapter.resources))
|
||||
if (!resources.length) return 0
|
||||
const total = resources.reduce((sum, item) => sum + Number(item.progressPercent || 0), 0)
|
||||
return Math.floor(total / resources.length)
|
||||
},
|
||||
studyCountText() {
|
||||
return this.course.lecturerName ? this.course.lecturerName : "开始学习"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getQuery(name) {
|
||||
return new URLSearchParams(window.location.search).get(name) || ""
|
||||
},
|
||||
async loadCourse() {
|
||||
const resp = await axios.post(this.apiBase + "/courseInfo", { id: this.courseId })
|
||||
if (resp.code === 0) {
|
||||
this.course = resp.data || {}
|
||||
} else {
|
||||
this.$toast(resp.msg || "课程不存在")
|
||||
}
|
||||
},
|
||||
async loadTree() {
|
||||
const resp = await axios.post(this.apiBase + "/studyTree", { courseId: this.courseId })
|
||||
if (resp.code === 0) {
|
||||
this.treeData = resp.data || []
|
||||
}
|
||||
},
|
||||
appendCatalogNode(node, result) {
|
||||
if (!node) return
|
||||
if (node.type === "resource") return
|
||||
const resources = this.collectResources(node.children || [])
|
||||
if (node.nodeType === "chapter" || resources.length) {
|
||||
result.push({
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
resources: resources
|
||||
})
|
||||
}
|
||||
;(node.children || []).forEach(child => {
|
||||
if (child.type !== "resource" && child.nodeType === "chapter") {
|
||||
this.appendCatalogNode(child, result)
|
||||
}
|
||||
})
|
||||
},
|
||||
collectResources(nodes) {
|
||||
const resources = []
|
||||
;(nodes || []).forEach(node => {
|
||||
if (node.type === "resource") {
|
||||
resources.push(node)
|
||||
} else {
|
||||
resources.push.apply(resources, this.collectResources(node.children || []))
|
||||
}
|
||||
})
|
||||
return resources
|
||||
},
|
||||
selectChapter(chapter) {
|
||||
if (chapter && chapter.resources && chapter.resources.length) {
|
||||
this.openResource(chapter.resources[0])
|
||||
}
|
||||
},
|
||||
openResource(resource) {
|
||||
if (!resource || !resource.id) return
|
||||
this.pauseMedia()
|
||||
this.finishStudy(false)
|
||||
this.selectedResource = resource
|
||||
this.playerVisible = true
|
||||
if (!["video", "audio"].includes(resource.resourceType)) {
|
||||
this.$nextTick(() => this.startStudy())
|
||||
} else {
|
||||
this.$nextTick(() => this.resumeMediaPosition())
|
||||
}
|
||||
},
|
||||
resumeMediaPosition() {
|
||||
const player = this.$refs.mediaPlayer
|
||||
const position = Number(this.selectedResource.lastPositionSeconds || 0)
|
||||
if (player && position > 0) {
|
||||
player.currentTime = position
|
||||
}
|
||||
},
|
||||
async startStudy() {
|
||||
if (!this.selectedResource.id || this.studying) return
|
||||
const resp = await axios.post(this.recordApi + "/start", {
|
||||
courseId: this.courseId,
|
||||
resourceId: this.selectedResource.id,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (resp.code !== 0) {
|
||||
this.$toast(resp.msg || "开始学习失败")
|
||||
return
|
||||
}
|
||||
this.currentSegmentId = resp.data.segmentId
|
||||
this.studying = true
|
||||
this.pendingSeconds = 0
|
||||
this.applyRecordState(resp.data)
|
||||
this.startHeartbeatTimer()
|
||||
},
|
||||
async heartbeat() {
|
||||
if (!this.currentSegmentId || this.pendingSeconds <= 0) return
|
||||
const seconds = this.pendingSeconds
|
||||
this.pendingSeconds = 0
|
||||
const resp = await axios.post(this.recordApi + "/heartbeat", {
|
||||
segmentId: this.currentSegmentId,
|
||||
activeSeconds: seconds,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (resp.code === 0) {
|
||||
this.applyRecordState(resp.data)
|
||||
}
|
||||
},
|
||||
async finishStudy(force) {
|
||||
this.pauseMedia()
|
||||
if (!this.currentSegmentId) return
|
||||
const segmentId = this.currentSegmentId
|
||||
const seconds = this.pendingSeconds
|
||||
this.pendingSeconds = 0
|
||||
this.stopHeartbeatTimer()
|
||||
this.studying = false
|
||||
this.currentSegmentId = ""
|
||||
await axios.post(this.recordApi + "/finish", {
|
||||
segmentId: segmentId,
|
||||
activeSeconds: seconds,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (force) return
|
||||
this.loadTree()
|
||||
},
|
||||
startHeartbeatTimer() {
|
||||
this.stopHeartbeatTimer()
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (!this.studying || document.hidden) return
|
||||
if (["video", "audio"].includes(this.selectedResource.resourceType)) {
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (!player || player.paused || player.ended) return
|
||||
}
|
||||
this.pendingSeconds += 1
|
||||
if (this.pendingSeconds >= 15) {
|
||||
this.heartbeat()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
stopHeartbeatTimer() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
},
|
||||
saveProgress() {
|
||||
const position = this.getMediaPosition()
|
||||
if (!position || !this.selectedResource.id) return
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", position)
|
||||
},
|
||||
applyRecordState(data) {
|
||||
if (!this.selectedResource.id || !data) return
|
||||
this.$set(this.selectedResource, "progressPercent", Number(data.progressPercent || 0))
|
||||
this.$set(this.selectedResource, "studySeconds", Number(data.studySeconds || 0))
|
||||
this.$set(this.selectedResource, "completeStatus", data.completeStatus || "studying")
|
||||
if (data.lastPositionSeconds !== undefined) {
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", Number(data.lastPositionSeconds || 0))
|
||||
}
|
||||
},
|
||||
getMediaPosition() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return 0
|
||||
const player = this.$refs.mediaPlayer
|
||||
return player ? Math.floor(player.currentTime || 0) : Number(this.selectedResource.lastPositionSeconds || 0)
|
||||
},
|
||||
statusText(resource) {
|
||||
if (!resource || resource.completeStatus === "not_started") return "未学习"
|
||||
if (resource.completeStatus === "completed") return "已完成"
|
||||
return "学习中 " + Number(resource.progressPercent || 0) + "%"
|
||||
},
|
||||
resourceIcon(resource) {
|
||||
if (resource.resourceType === "video") return "video"
|
||||
if (resource.resourceType === "audio") return "music-o"
|
||||
if (resource.resourceType === "image") return "photo-o"
|
||||
return "description"
|
||||
},
|
||||
resourceIconClass(resource) {
|
||||
if (resource.resourceType === "video") return "video"
|
||||
if (resource.resourceType === "audio") return "audio"
|
||||
if (resource.resourceType === "image") return "image"
|
||||
return "doc"
|
||||
},
|
||||
getFileUrl(value) {
|
||||
if (!value) return ""
|
||||
if (Array.isArray(value)) {
|
||||
return value.length ? (value[0].url || (value[0].response && value[0].response.data) || value[0].data || "") : ""
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const text = value.trim()
|
||||
if (!text) return ""
|
||||
if (text.startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(text)
|
||||
return files.length ? (files[0].url || (files[0].response && files[0].response.data) || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
},
|
||||
getFileId(value) {
|
||||
const url = this.getFileUrl(value)
|
||||
if (!url) return ""
|
||||
const matched = url.match(/[?&]id=([^&]+)/)
|
||||
if (matched) return decodeURIComponent(matched[1])
|
||||
if (!url.includes("/") && !url.includes(".")) return url
|
||||
return ""
|
||||
},
|
||||
canInlinePreview(resource) {
|
||||
const type = resource.resourceType || ""
|
||||
const ext = (resource.fileExt || "").toLowerCase()
|
||||
return ["pdf", "ppt", "word"].includes(type) || ["pdf", "ppt", "pptx", "doc", "docx"].includes(ext)
|
||||
},
|
||||
openFile() {
|
||||
if (this.fileUrl) {
|
||||
window.open(this.fileUrl)
|
||||
}
|
||||
},
|
||||
pauseMedia() {
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (player && player.pause) {
|
||||
player.pause()
|
||||
}
|
||||
},
|
||||
enterVideoFullscreen() {
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (!player) return
|
||||
const requestFullscreen = player.requestFullscreen || player.webkitRequestFullscreen || player.mozRequestFullScreen || player.msRequestFullscreen
|
||||
if (requestFullscreen) {
|
||||
requestFullscreen.call(player)
|
||||
} else if (player.webkitEnterFullscreen) {
|
||||
player.webkitEnterFullscreen()
|
||||
}
|
||||
if (screen.orientation && screen.orientation.lock) {
|
||||
screen.orientation.lock("landscape").catch(() => {})
|
||||
}
|
||||
},
|
||||
closePlayer() {
|
||||
this.pauseMedia()
|
||||
this.finishStudy(false)
|
||||
this.playerVisible = false
|
||||
},
|
||||
async goBack() {
|
||||
if (this.navigatingBack) return
|
||||
this.navigatingBack = true
|
||||
try {
|
||||
await this.finishStudy(true)
|
||||
} finally {
|
||||
this.navigateBack()
|
||||
}
|
||||
},
|
||||
navigateBack() {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back()
|
||||
return
|
||||
}
|
||||
$.pjax({
|
||||
url: "/platform/learning/course/h5",
|
||||
container: "#container",
|
||||
maxCacheLength: 0,
|
||||
push: false,
|
||||
replace: true,
|
||||
fragment: "#container",
|
||||
timeout: 8000
|
||||
})
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.courseId = this.getQuery("id")
|
||||
if (!this.courseId) {
|
||||
this.$toast("请选择课程")
|
||||
return
|
||||
}
|
||||
await this.loadCourse()
|
||||
await this.loadTree()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.finishStudy(true)
|
||||
this.stopHeartbeatTimer()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,371 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.my-study-page {
|
||||
min-height: 100vh;
|
||||
background: #f3f7fb;
|
||||
color: #202733;
|
||||
}
|
||||
.my-study-hero {
|
||||
position: relative;
|
||||
padding: 86px 20px 72px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #0ca7dd 0%, #0699d6 58%, #0f8fd7 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-study-hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -74px;
|
||||
bottom: -108px;
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, .08);
|
||||
}
|
||||
.my-study-status {
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.my-study-time {
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, .82);
|
||||
}
|
||||
.my-study-time strong {
|
||||
margin: 0 3px;
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.my-study-stats {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0;
|
||||
margin-top: 26px;
|
||||
text-align: center;
|
||||
}
|
||||
.my-study-stat-value {
|
||||
font-size: 23px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.my-study-stat-label {
|
||||
margin-top: 7px;
|
||||
color: rgba(255, 255, 255, .78);
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.my-study-main {
|
||||
position: relative;
|
||||
margin-top: -36px;
|
||||
padding: 0 20px 28px;
|
||||
z-index: 2;
|
||||
}
|
||||
.my-study-assistant {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-height: 70px;
|
||||
padding: 13px 14px;
|
||||
border-radius: 8px;
|
||||
background: #eef6ff;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 8px 18px rgba(17, 101, 166, .08);
|
||||
}
|
||||
.my-study-ai {
|
||||
flex-shrink: 0;
|
||||
font-size: 27px;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
color: #1a75ff;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.my-study-ai span {
|
||||
color: #6b46ff;
|
||||
}
|
||||
.my-study-assistant-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #30445c;
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.my-study-assistant-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 7px 16px;
|
||||
border-radius: 18px;
|
||||
color: #5d8fd8;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
background: #ffffff;
|
||||
}
|
||||
.my-study-section {
|
||||
margin-top: 20px;
|
||||
padding: 16px 0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.my-study-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 14px 14px;
|
||||
}
|
||||
.my-study-section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.my-study-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #9aa3b1;
|
||||
font-size: 13px;
|
||||
}
|
||||
.my-study-course-scroll {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 0 14px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.my-study-course-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.my-study-course-card {
|
||||
flex: 0 0 154px;
|
||||
width: 154px;
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
background: #f7fbff;
|
||||
}
|
||||
.my-study-cover {
|
||||
width: 154px;
|
||||
height: 88px;
|
||||
background: linear-gradient(135deg, #8657f2, #56c4ec);
|
||||
}
|
||||
.my-study-cover img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.my-study-cover-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.my-study-course-name {
|
||||
height: 48px;
|
||||
padding: 8px 10px 0;
|
||||
color: #293241;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-study-course-foot {
|
||||
padding: 8px 10px 10px;
|
||||
color: #b5bdc8;
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
.my-study-list {
|
||||
margin-top: 18px;
|
||||
}
|
||||
.my-study-list-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.my-study-list-cover {
|
||||
flex: 0 0 108px;
|
||||
width: 108px;
|
||||
height: 66px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #8657f2, #56c4ec);
|
||||
}
|
||||
.my-study-list-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.my-study-list-info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.my-study-list-title {
|
||||
color: #202733;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-study-list-meta {
|
||||
margin-top: 10px;
|
||||
color: #8f99a8;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="my-study-page" v-cloak>
|
||||
<div class="my-study-hero">
|
||||
<div class="my-study-status">
|
||||
<div class="my-study-time">
|
||||
累计学习时长 <strong>{{ summary.studyHour || 0 }}</strong> 小时 <strong>{{ summary.studyMinute || 0 }}</strong> 分钟
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-study-stats">
|
||||
<div v-for="item in statItems" :key="item.label">
|
||||
<div class="my-study-stat-value">{{ item.value }}</div>
|
||||
<div class="my-study-stat-label">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-study-main">
|
||||
<div class="my-study-assistant">
|
||||
<div class="my-study-ai"><span>AI</span>学助手</div>
|
||||
<div class="my-study-assistant-text">来问问AI学习助手,获取专属学习计划</div>
|
||||
<div class="my-study-assistant-btn">查看</div>
|
||||
</div>
|
||||
|
||||
<div class="my-study-section">
|
||||
<div class="my-study-section-head">
|
||||
<div class="my-study-section-title">我学习的课程</div>
|
||||
<div class="my-study-all" @click="scrollToList">全部 <van-icon name="arrow"></van-icon></div>
|
||||
</div>
|
||||
<div class="my-study-course-scroll">
|
||||
<div
|
||||
v-for="course in courses"
|
||||
:key="course.courseId"
|
||||
class="my-study-course-card"
|
||||
@click="openCourse(course)">
|
||||
<div class="my-study-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="my-study-cover-empty">{{ course.courseName || '课程' }}</div>
|
||||
</div>
|
||||
<div class="my-study-course-name">{{ course.courseName || '未命名课程' }}</div>
|
||||
<div class="my-study-course-foot">已学完{{ course.completedOutlineCount || 0 }}讲</div>
|
||||
</div>
|
||||
</div>
|
||||
<van-empty v-if="!loading && !courses.length" description="暂无学习课程"></van-empty>
|
||||
</div>
|
||||
|
||||
<div ref="courseList" class="my-study-list">
|
||||
<div
|
||||
v-for="course in courses"
|
||||
:key="'list_' + course.courseId"
|
||||
class="my-study-list-card"
|
||||
@click="openCourse(course)">
|
||||
<div class="my-study-list-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="my-study-cover-empty">{{ course.courseName || '课程' }}</div>
|
||||
</div>
|
||||
<div class="my-study-list-info">
|
||||
<div class="my-study-list-title">{{ course.courseName || '未命名课程' }}</div>
|
||||
<div class="my-study-list-meta">学习进度 {{ course.progressPercent || 0 }}% · {{ course.studyTimeText || '0秒' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/learning/study/record",
|
||||
summary: {},
|
||||
courses: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statItems() {
|
||||
return [
|
||||
{ label: "已学完课程", value: this.summary.completedCourseCount || 0 },
|
||||
{ label: "学习专题", value: 0 }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const [summaryResp, courseResp] = await Promise.all([
|
||||
this.$axios.post(this.apiBase + "/h5Summary"),
|
||||
this.$axios.post(this.apiBase + "/h5Courses")
|
||||
])
|
||||
if (summaryResp.code === 0) {
|
||||
this.summary = summaryResp.data || {}
|
||||
}
|
||||
if (courseResp.code === 0) {
|
||||
this.courses = courseResp.data || []
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
getCoverUrl(cover) {
|
||||
if (!cover) return ""
|
||||
if (Array.isArray(cover)) {
|
||||
return cover.length ? (cover[0].url || (cover[0].response && cover[0].response.data) || cover[0].data || "") : ""
|
||||
}
|
||||
if (typeof cover === "string") {
|
||||
const text = cover.trim()
|
||||
if (!text) return ""
|
||||
if (text.startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(text)
|
||||
return files.length ? (files[0].url || (files[0].response && files[0].response.data) || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
},
|
||||
openCourse(course) {
|
||||
if (!course || !course.courseId) return
|
||||
pjaxReplace("/platform/learning/course/h5/study?id=" + course.courseId)
|
||||
},
|
||||
scrollToList() {
|
||||
this.$refs.courseList && this.$refs.courseList.scrollIntoView({ behavior: "smooth", block: "start" })
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -35,9 +35,12 @@ const apps = {
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="tree-content">
|
||||
<!-- 应用列表 -->
|
||||
<div v-if="loading" class="module-loading">
|
||||
<van-loading size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<div :class="['app-grid', { 'app-grid-single': applications.length === 1 }]"
|
||||
v-if="applications.length > 0">
|
||||
v-else-if="isFixedCategory && applications.length > 0">
|
||||
<div v-for="app in applications"
|
||||
:key="app.id"
|
||||
class="app-grid-item"
|
||||
@@ -46,20 +49,38 @@ const apps = {
|
||||
@touchstart="onTouchStart(app)"
|
||||
@touchend="onTouchEnd"
|
||||
@touchmove="onTouchMove">
|
||||
<!-- 图标 -->
|
||||
<div class="app-grid-icon">
|
||||
<van-icon v-if="app.picIcon" :name="app.picIcon" size="45"></van-icon>
|
||||
<van-icon v-else-if="app.icon" :name="app.icon" size="28"></van-icon>
|
||||
<van-icon v-else name="apps-o" size="28"></van-icon>
|
||||
</div>
|
||||
<!-- 名称 -->
|
||||
<div class="app-grid-name">{{ app.name }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isFixedCategory && moduleSections.length > 0" class="module-sections">
|
||||
<div v-for="section in moduleSections" :key="section.id" class="module-section">
|
||||
<div class="module-title">{{ section.name }}</div>
|
||||
<div :class="['module-grid', { 'module-grid-single': section.menus.length === 1 }]">
|
||||
<div
|
||||
v-for="menu in section.menus"
|
||||
:key="menu.id"
|
||||
class="module-feature"
|
||||
@click="navigateToMenu(menu)"
|
||||
>
|
||||
<img v-if="menu.picIcon" :src="menu.picIcon" :alt="menu.name || ''" class="module-feature-img"/>
|
||||
<div v-else class="module-feature-icon">
|
||||
<van-icon :name="menu.icon || 'apps-o'" size="24"></van-icon>
|
||||
</div>
|
||||
<div class="module-feature-name">{{ menu.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<van-empty v-else description="暂无应用"></van-empty>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,6 +114,7 @@ const apps = {
|
||||
return {
|
||||
// 应用列表
|
||||
applications: [],
|
||||
moduleSections: [],
|
||||
// 固定分类列表
|
||||
categories: [
|
||||
{id: "all", name: "全部应用", icon: "fa fa-th-large"},
|
||||
@@ -111,6 +133,7 @@ const apps = {
|
||||
showMenuPopup: false,
|
||||
currentApp: {},
|
||||
currentMenus: [],
|
||||
requestSeq: 0,
|
||||
|
||||
touchTimer: null,
|
||||
isTouchMoved: false,
|
||||
@@ -124,6 +147,9 @@ const apps = {
|
||||
// 当前选中的分类ID
|
||||
currentCategoryId() {
|
||||
return this.allCategories[this.activeTab]?.id || "all"
|
||||
},
|
||||
isFixedCategory() {
|
||||
return ["all", "favorites", "recommended"].includes(this.currentCategoryId)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -145,7 +171,9 @@ const apps = {
|
||||
|
||||
// 加载应用数据
|
||||
loadApps() {
|
||||
const requestSeq = ++this.requestSeq
|
||||
this.loading = true
|
||||
this.moduleSections = []
|
||||
|
||||
// 构建请求参数
|
||||
const params = {
|
||||
@@ -172,12 +200,14 @@ const apps = {
|
||||
// 如果是"推荐应用"分类
|
||||
if (this.currentCategoryId === "recommended") {
|
||||
this.$axios.post("/platform/home/listRecommendApp", { platform: "H5" }).then((result) => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (result.code === 0) {
|
||||
this.applications = result.data || []
|
||||
this.finishLoadApps(requestSeq)
|
||||
} else {
|
||||
console.error("获取推荐应用失败:", result.msg)
|
||||
this.loading = false
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -185,28 +215,105 @@ const apps = {
|
||||
// 发送请求获取普通应用列表
|
||||
this.$axios.post("/platform/v4/apps/list?" + queryString)
|
||||
.then((result) => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (result.code === 0) {
|
||||
this.applications = result.data || []
|
||||
this.finishLoadApps(requestSeq)
|
||||
} else {
|
||||
console.error("获取应用列表失败:", result.msg)
|
||||
this.loading = false
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// 加载收藏的应用
|
||||
loadFavorites() {
|
||||
this.$axios.post("/platform/v4/apps/favorite")
|
||||
const requestSeq = this.requestSeq
|
||||
this.$axios.post("/platform/v4/apps/favorite", {platform: 'H5'})
|
||||
.then((result) => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (result.code === 0) {
|
||||
this.applications = result.data || []
|
||||
this.finishLoadApps(requestSeq)
|
||||
} else {
|
||||
console.error("获取收藏应用失败:", result.msg)
|
||||
this.loading = false
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
finishLoadApps(requestSeq) {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (this.isFixedCategory) {
|
||||
this.moduleSections = []
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.loadModuleSections(this.applications, requestSeq)
|
||||
},
|
||||
|
||||
// 加载每个模块下的功能菜单
|
||||
loadModuleSections(apps, requestSeq) {
|
||||
const list = apps || []
|
||||
if (!list.length) {
|
||||
this.moduleSections = []
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
Promise.all(list.map(app => {
|
||||
return this.$axios.post("/platform/sys/user/subAppMenus", {appId: app.id, platform: "H5"}).then((res) => {
|
||||
const menus = res.code === 0 ? (res.data || []) : []
|
||||
return {
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
app: app,
|
||||
menus: this.resolveModuleMenus(app, menus)
|
||||
}
|
||||
}).catch(() => {
|
||||
return {
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
app: app,
|
||||
menus: this.resolveModuleMenus(app, [])
|
||||
}
|
||||
})
|
||||
})).then(sections => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
this.moduleSections = sections.filter(section => section.menus.length > 0)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
resolveModuleMenus(app, menus) {
|
||||
const items = this.flattenMenus(menus || [])
|
||||
if (items.length > 0) {
|
||||
return items
|
||||
}
|
||||
if (app && app.href) {
|
||||
return [{
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
href: app.href,
|
||||
icon: app.icon,
|
||||
picIcon: app.picIcon,
|
||||
aliasName: app.aliasName
|
||||
}]
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
flattenMenus(menus) {
|
||||
const result = []
|
||||
;(menus || []).forEach(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
result.push.apply(result, this.flattenMenus(menu.children))
|
||||
} else if (menu.href) {
|
||||
result.push(menu)
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
|
||||
// 切换应用收藏状态
|
||||
toggleFavorite(appId) {
|
||||
const app = this.applications.find((a) => a.id === appId)
|
||||
@@ -254,6 +361,7 @@ const apps = {
|
||||
// 搜索
|
||||
onSearch() {
|
||||
this.applications = []
|
||||
this.moduleSections = []
|
||||
this.loadApps()
|
||||
},
|
||||
|
||||
@@ -267,12 +375,13 @@ const apps = {
|
||||
onClickNav(index) {
|
||||
this.activeTab = index
|
||||
this.applications = []
|
||||
this.moduleSections = []
|
||||
this.loadApps()
|
||||
},
|
||||
|
||||
// 打开应用
|
||||
openApp(app) {
|
||||
this.$axios.post("/platform/sys/user/subAppMenus", {appId: app.id}).then((res) => {
|
||||
this.$axios.post("/platform/sys/user/subAppMenus", {appId: app.id, platform: "H5"}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.showAppMenus(app, res.data)
|
||||
}
|
||||
@@ -321,12 +430,15 @@ const apps = {
|
||||
.apps-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
height: calc(100vh - 50px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 自定义树形选择组件样式 */
|
||||
.custom-tree-select {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
height: calc(100vh - 150px);
|
||||
background: #fff;
|
||||
}
|
||||
@@ -403,10 +515,109 @@ const apps = {
|
||||
|
||||
.tree-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.module-loading {
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a94a6;
|
||||
}
|
||||
|
||||
.module-sections {
|
||||
padding: 12px 12px 18px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.module-section {
|
||||
padding: 0 0 18px;
|
||||
}
|
||||
|
||||
.module-section + .module-section {
|
||||
border-top: 1px solid #f1f3f6;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.module-title {
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
margin-bottom: 10px;
|
||||
color: #202733;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.module-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: #1989fa;
|
||||
}
|
||||
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px 8px;
|
||||
}
|
||||
|
||||
.module-grid-single {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.module-feature {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 2px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.module-feature:active {
|
||||
background: #f2f7ff;
|
||||
}
|
||||
|
||||
.module-feature-img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.module-feature-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 6px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #2f8af5 0%, #49c3dc 100%);
|
||||
}
|
||||
|
||||
.module-feature-name {
|
||||
width: 100%;
|
||||
color: #323233;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
/* 改为自适应列数:每格最小 80px,自动换行 */
|
||||
@@ -414,7 +625,7 @@ const apps = {
|
||||
|
||||
justify-items: center; /* 子项内容居中(图标和文字) */
|
||||
justify-content: start; /* 整体网格靠左对齐,避免居中 */
|
||||
|
||||
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
@@ -590,6 +801,6 @@ const apps = {
|
||||
font-size: 16px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<div class="featured-page">
|
||||
<van-nav-bar
|
||||
title="精彩活动"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
fixed
|
||||
placeholder
|
||||
@click-left="goBack"
|
||||
></van-nav-bar>
|
||||
|
||||
<div class="featured-banner">
|
||||
<img :src="topImage" alt="">
|
||||
<div class="featured-banner__shade"></div>
|
||||
<div class="featured-banner__text">
|
||||
<div class="featured-banner__title">精彩活动</div>
|
||||
<div class="featured-banner__sub">发现更多校园工会活动</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="featured-list">
|
||||
<template v-if="activityOptions && activityOptions.length > 0">
|
||||
<div v-for="item in activityOptions" :key="item.id" class="act-item" @click="enterAct(item)">
|
||||
<van-tag class="act-item-wrapper-tag" v-if="$moment().unix() > $moment(item.endDate).unix()">
|
||||
已结束
|
||||
</van-tag>
|
||||
|
||||
<van-tag class="act-item-wrapper-tag" type="success"
|
||||
v-else-if="$moment().unix() > $moment(item.startDate).unix() && $moment().unix() < $moment(item.endDate).unix()">
|
||||
进行中
|
||||
</van-tag>
|
||||
|
||||
<van-tag class="act-item-wrapper-tag" type="success" v-else-if="$moment().unix() < $moment(item.startDate).unix()">
|
||||
即将开始
|
||||
</van-tag>
|
||||
|
||||
<div class="act-item-cover">
|
||||
<van-image v-if="item.cover" width="120" height="84" :src="item.cover"></van-image>
|
||||
</div>
|
||||
<div class="act-item-wrapper">
|
||||
<div class="content-title">{{ item.name }}</div>
|
||||
<div class="content-text">
|
||||
<div>开始时间:{{ $moment(item.startDate).format('MM-DD HH:mm') }}</div>
|
||||
<div class="mt5">结束时间:{{ $moment(item.endDate).format('MM-DD HH:mm') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<van-empty v-if="!activityOptions || activityOptions.length === 0" description="暂无精彩活动"></van-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data: function () {
|
||||
return {
|
||||
topImage: "/assets/mobile/img/home/home1.png",
|
||||
activityOptions: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack: function () {
|
||||
this.returnHome()
|
||||
},
|
||||
returnHome: function () {
|
||||
if (window.store && typeof window.store.commit === "function") {
|
||||
window.store.commit("setActiveTarBar", "home")
|
||||
}
|
||||
if (!window.$ || !$.support || !$.support.pjax) {
|
||||
window.location.replace("/platform/h5/home")
|
||||
return
|
||||
}
|
||||
var fallbackTimer = window.setTimeout(function () {
|
||||
window.location.replace("/platform/h5/home")
|
||||
}, 1200)
|
||||
$(document).one("pjax:complete", function () {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
})
|
||||
$(document).one("pjax:error pjax:timeout", function () {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
window.location.replace("/platform/h5/home")
|
||||
})
|
||||
$.pjax({
|
||||
url: "/platform/h5/home",
|
||||
container: "#container",
|
||||
fragment: "#container",
|
||||
push: false,
|
||||
replace: true,
|
||||
timeout: 8000
|
||||
})
|
||||
},
|
||||
loadTopImage: function () {
|
||||
var self = this
|
||||
this.$axios.post("/open/common/getConfigKey", { key: "AppFeaturedActivityImg" }).then(function (res) {
|
||||
if (res.code === 0 && res.data) {
|
||||
self.topImage = String(res.data).split(",")[0]
|
||||
}
|
||||
})
|
||||
},
|
||||
listActivity: function () {
|
||||
var self = this
|
||||
this.$axios.post("/platform/home/listHomeActivity").then(function (resp) {
|
||||
if (resp.code === 0) {
|
||||
self.activityOptions = (resp.data || []).filter(function (item) {
|
||||
return !self.isWelfareItem(item)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
isWelfareItem: function (item) {
|
||||
var url = ((item && item.url) || "") + "," + ((item && item.h5Url) || "")
|
||||
return url.indexOf("/welfare/") > -1
|
||||
},
|
||||
enterAct: function (item) {
|
||||
if (this.$moment().unix() < this.$moment(item.startDate).unix()) {
|
||||
this.$toast("活动即将开始")
|
||||
return
|
||||
}
|
||||
this.$pjaxReplace(item.h5Url)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.loadTopImage()
|
||||
this.listActivity()
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
body {
|
||||
background-color: #f4f7fb;
|
||||
}
|
||||
|
||||
.featured-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f4f7fb;
|
||||
padding-bottom: 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.featured-page /deep/ .van-nav-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.featured-page /deep/ .van-nav-bar__title {
|
||||
color: #1f2937;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.featured-page /deep/ .van-nav-bar .van-icon,
|
||||
.featured-page /deep/ .van-nav-bar__text {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.featured-banner {
|
||||
position: relative;
|
||||
height: 140px;
|
||||
margin: 12px 12px 10px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background-color: #e9eef6;
|
||||
box-shadow: 0 8px 22px rgba(31, 41, 55, .1);
|
||||
}
|
||||
|
||||
.featured-banner img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.featured-banner__shade {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, .42), rgba(0, 0, 0, .08));
|
||||
}
|
||||
|
||||
.featured-banner__text {
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
bottom: 18px;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, .26);
|
||||
}
|
||||
|
||||
.featured-banner__title {
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.featured-banner__sub {
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.featured-list {
|
||||
margin: 0 10px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 8px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.act-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 10px 12px;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.act-item + .act-item {
|
||||
border-top: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.featured-list /deep/ .act-item-wrapper-tag {
|
||||
position: absolute !important;
|
||||
top: 10px !important;
|
||||
left: 12px !important;
|
||||
z-index: 2;
|
||||
width: auto !important;
|
||||
min-width: 42px;
|
||||
height: 20px !important;
|
||||
flex: none !important;
|
||||
padding: 0 6px !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
border-radius: 2px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff !important;
|
||||
background-color: #1f73b7 !important;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
white-space: nowrap !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.featured-list /deep/ .act-item-wrapper-tag.van-tag--success {
|
||||
background-color: #1f73b7 !important;
|
||||
}
|
||||
|
||||
.act-item-cover {
|
||||
width: 120px;
|
||||
height: 84px;
|
||||
flex: 0 0 120px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background-color: #f2f3f5;
|
||||
}
|
||||
|
||||
.act-item-cover /deep/ .van-image,
|
||||
.act-item-cover /deep/ img {
|
||||
display: block;
|
||||
width: 120px;
|
||||
height: 84px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.act-item-wrapper {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
font-weight: 600;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
margin-top: 9px;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.mt5 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
`
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,303 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<div class="festival-page">
|
||||
<van-nav-bar
|
||||
title="节日福利"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
fixed
|
||||
placeholder
|
||||
@click-left="goBack"
|
||||
></van-nav-bar>
|
||||
|
||||
<div class="festival-banner">
|
||||
<img :src="topImage" alt="">
|
||||
<div class="festival-banner__shade"></div>
|
||||
<div class="festival-banner__text">
|
||||
<div class="festival-banner__title">节日福利</div>
|
||||
<div class="festival-banner__sub">查看可参与的节日福利活动</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="festival-list">
|
||||
<template v-if="activityOptions && activityOptions.length > 0">
|
||||
<div v-for="item in activityOptions" :key="item.id" class="act-item" @click="enterAct(item)">
|
||||
<van-tag class="act-item-wrapper-tag" v-if="$moment().unix() > $moment(item.endDate).unix()">
|
||||
已结束
|
||||
</van-tag>
|
||||
|
||||
<van-tag class="act-item-wrapper-tag" type="success"
|
||||
v-else-if="$moment().unix() > $moment(item.startDate).unix() && $moment().unix() < $moment(item.endDate).unix()">
|
||||
进行中
|
||||
</van-tag>
|
||||
|
||||
<van-tag class="act-item-wrapper-tag" type="success" v-else-if="$moment().unix() < $moment(item.startDate).unix()">
|
||||
即将开始
|
||||
</van-tag>
|
||||
|
||||
<div class="act-item-cover">
|
||||
<van-image v-if="item.cover" width="120" height="84" :src="item.cover"></van-image>
|
||||
</div>
|
||||
<div class="act-item-wrapper">
|
||||
<div class="content-title">{{ item.name }}</div>
|
||||
<div class="content-text">
|
||||
<div>开始时间:{{ $moment(item.startDate).format('MM-DD HH:mm') }}</div>
|
||||
<div class="mt5">结束时间:{{ $moment(item.endDate).format('MM-DD HH:mm') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<van-empty v-if="!activityOptions || activityOptions.length === 0" description="暂无节日福利"></van-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data: function () {
|
||||
return {
|
||||
topImage: "/assets/mobile/img/home/home2.png",
|
||||
activityOptions: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack: function () {
|
||||
this.returnHome()
|
||||
},
|
||||
returnHome: function () {
|
||||
if (window.store && typeof window.store.commit === "function") {
|
||||
window.store.commit("setActiveTarBar", "home")
|
||||
}
|
||||
if (!window.$ || !$.support || !$.support.pjax) {
|
||||
window.location.replace("/platform/h5/home")
|
||||
return
|
||||
}
|
||||
var fallbackTimer = window.setTimeout(function () {
|
||||
window.location.replace("/platform/h5/home")
|
||||
}, 1200)
|
||||
$(document).one("pjax:complete", function () {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
})
|
||||
$(document).one("pjax:error pjax:timeout", function () {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
window.location.replace("/platform/h5/home")
|
||||
})
|
||||
$.pjax({
|
||||
url: "/platform/h5/home",
|
||||
container: "#container",
|
||||
fragment: "#container",
|
||||
push: false,
|
||||
replace: true,
|
||||
timeout: 8000
|
||||
})
|
||||
},
|
||||
loadTopImage: function () {
|
||||
var self = this
|
||||
this.$axios.post("/open/common/getConfigKey", { key: "AppFestivalBenefitImg" }).then(function (res) {
|
||||
if (res.code === 0 && res.data) {
|
||||
self.topImage = String(res.data).split(",")[0]
|
||||
}
|
||||
})
|
||||
},
|
||||
listActivity: function () {
|
||||
var self = this
|
||||
this.$axios.post("/platform/home/listHomeActivity").then(function (resp) {
|
||||
if (resp.code === 0) {
|
||||
self.activityOptions = (resp.data || []).filter(function (item) {
|
||||
return self.isWelfareItem(item)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
isWelfareItem: function (item) {
|
||||
var url = ((item && item.url) || "") + "," + ((item && item.h5Url) || "")
|
||||
return url.indexOf("/welfare/") > -1
|
||||
},
|
||||
enterAct: function (item) {
|
||||
if (this.$moment().unix() < this.$moment(item.startDate).unix()) {
|
||||
this.$toast("活动即将开始")
|
||||
return
|
||||
}
|
||||
this.$pjaxReplace(item.h5Url)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.loadTopImage()
|
||||
this.listActivity()
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
body {
|
||||
background-color: #f4f7fb;
|
||||
}
|
||||
|
||||
.festival-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f4f7fb;
|
||||
padding-bottom: 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.festival-page /deep/ .van-nav-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.festival-page /deep/ .van-nav-bar__title {
|
||||
color: #1f2937;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.festival-page /deep/ .van-nav-bar .van-icon,
|
||||
.festival-page /deep/ .van-nav-bar__text {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.festival-banner {
|
||||
position: relative;
|
||||
height: 140px;
|
||||
margin: 12px 12px 10px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background-color: #e9eef6;
|
||||
box-shadow: 0 8px 22px rgba(31, 41, 55, .1);
|
||||
}
|
||||
|
||||
.festival-banner img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.festival-banner__shade {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, .42), rgba(0, 0, 0, .08));
|
||||
}
|
||||
|
||||
.festival-banner__text {
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
bottom: 18px;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, .26);
|
||||
}
|
||||
|
||||
.festival-banner__title {
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.festival-banner__sub {
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.festival-list {
|
||||
margin: 0 10px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 8px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.act-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 10px 12px;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.act-item + .act-item {
|
||||
border-top: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.festival-list /deep/ .act-item-wrapper-tag {
|
||||
position: absolute !important;
|
||||
top: 10px !important;
|
||||
left: 12px !important;
|
||||
z-index: 2;
|
||||
width: auto !important;
|
||||
min-width: 42px;
|
||||
height: 20px !important;
|
||||
flex: none !important;
|
||||
padding: 0 6px !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
border-radius: 2px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff !important;
|
||||
background-color: #1f73b7 !important;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
white-space: nowrap !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.festival-list /deep/ .act-item-wrapper-tag.van-tag--success {
|
||||
background-color: #1f73b7 !important;
|
||||
}
|
||||
|
||||
.act-item-cover {
|
||||
width: 120px;
|
||||
height: 84px;
|
||||
flex: 0 0 120px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background-color: #f2f3f5;
|
||||
}
|
||||
|
||||
.act-item-cover /deep/ .van-image,
|
||||
.act-item-cover /deep/ img {
|
||||
display: block;
|
||||
width: 120px;
|
||||
height: 84px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.act-item-wrapper {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
font-weight: 600;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
margin-top: 9px;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.mt5 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
`
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -1,37 +1,88 @@
|
||||
const home = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<van-nav-bar title="首页" class="nav-bar" placeholder fixed></van-nav-bar>
|
||||
<van-swipe :autoplay="100000" :height="200" class="home__swipe">
|
||||
<van-swipe-item>
|
||||
<img src="/assets/mobile/img/home/home1.png" alt="">
|
||||
</van-swipe-item>
|
||||
<van-swipe-item>
|
||||
<img src="/assets/mobile/img/home/home2.png" alt="">
|
||||
</van-swipe-item>
|
||||
<van-swipe-item>
|
||||
<img src="/assets/mobile/img/home/home3.png" alt="">
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
|
||||
<!--快捷入口-->
|
||||
<div class="quick-grid">
|
||||
<div class="section-title">推荐服务</div>
|
||||
<van-grid :column-num="4" icon-size="46" square clickable :border="false">
|
||||
<van-grid-item v-for="item in quickEntries"
|
||||
@click="menuClick(item)"
|
||||
:key="item.id"
|
||||
:text="item.name">
|
||||
<img :src="item.picIcon"
|
||||
slot="icon"
|
||||
style="width: 40px; height: 40px"/>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
<div class="home-hero">
|
||||
<img class="home-hero__bg" :src="currentBanner.src" alt="">
|
||||
<div class="home-hero__mask"></div>
|
||||
<div class="home-hero__logo">
|
||||
<img v-if="appLogo" class="home-hero__logo-img" :src="appLogo" alt="">
|
||||
<div v-else class="home-hero__logo-mark"></div>
|
||||
<div class="home-hero__logo-title" :class="{ 'is-placeholder': !appName }">
|
||||
<span v-if="appName">{{ appName }}</span>
|
||||
<template v-else>
|
||||
<i></i>
|
||||
<i></i>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<van-swipe
|
||||
:autoplay="4000"
|
||||
:height="193"
|
||||
class="home__swipe"
|
||||
indicator-color="#e13b4c"
|
||||
@change="bannerChange">
|
||||
<van-swipe-item v-for="banner in bannerList" :key="banner.src">
|
||||
<div class="home__swipe-card">
|
||||
<img :src="banner.src" alt="">
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
|
||||
<!--活动列表-->
|
||||
<div class="act-list">
|
||||
<div class="section-title">最新活动</div>
|
||||
<!--蹇嵎鍏ュ彛-->
|
||||
<div class="quick-grid">
|
||||
<div class="section-title">推荐服务</div>
|
||||
<div class="quick-scroll">
|
||||
<div class="quick-scroll-grid">
|
||||
<div class="quick-entry-item"
|
||||
v-for="item in quickEntries"
|
||||
@click="menuClick(item)"
|
||||
:key="item.id">
|
||||
<div class="quick-entry-icon">
|
||||
<img v-if="item.picIcon" :src="item.picIcon" alt="">
|
||||
<van-icon v-else name="apps-o" size="28"></van-icon>
|
||||
</div>
|
||||
<div class="quick-entry-name">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--娲诲姩鍒楄〃-->
|
||||
<div class="feature-panel">
|
||||
<div
|
||||
class="feature-card"
|
||||
:class="[item.className, { 'feature-card--large': item.large }]"
|
||||
v-for="item in featuredEntries"
|
||||
:key="item.name"
|
||||
@click="featureClick(item)">
|
||||
<div class="feature-card__text">
|
||||
<div class="feature-card__title">{{ item.name }}</div>
|
||||
<div class="feature-card__desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<van-icon class="feature-card__icon" :name="item.icon"></van-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="classroom-panel" @click="enterClassroom">
|
||||
<div class="classroom-panel__title">我的课堂</div>
|
||||
<div class="classroom-panel__course">{{ currentClassroomCourseName }}</div>
|
||||
<div class="classroom-panel__action">
|
||||
<span>个人学习</span>
|
||||
<van-icon name="arrow"></van-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="policy-panel">
|
||||
<div class="policy-panel__title">政策文件</div>
|
||||
<div class="policy-panel__action">
|
||||
<span>更多</span>
|
||||
<van-icon name="arrow"></van-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="act-list" v-if="showLatestActivity">
|
||||
<div class="section-title section-title--hidden">最新活动</div>
|
||||
<template v-if="activityOptions && activityOptions.length > 0">
|
||||
<div v-for="item in activityOptions" :key="item.id" class="act-item" @click="enterAct(item)">
|
||||
<van-tag class="act-item-wrapper-tag" v-if="$moment().unix() > $moment(item.endDate).unix()">
|
||||
@@ -68,57 +119,759 @@ const home = {
|
||||
data() {
|
||||
return {
|
||||
activityOptions: [],
|
||||
quickEntries: []
|
||||
quickEntries: [],
|
||||
classroomCourses: [],
|
||||
classroomCourseIndex: 0,
|
||||
classroomCourseTimer: null,
|
||||
showLatestActivity: false,
|
||||
featuredEntries: [
|
||||
{
|
||||
name: "精彩活动",
|
||||
desc: "更多精彩活动",
|
||||
icon: "fire-o",
|
||||
className: "feature-card--activity",
|
||||
large: true,
|
||||
href: "/platform/h5/featuredActivity"
|
||||
},
|
||||
{
|
||||
name: "节日福利",
|
||||
desc: "领取节日福利",
|
||||
icon: "gift-o",
|
||||
className: "feature-card--benefit",
|
||||
href: "/platform/h5/festivalBenefit"
|
||||
},
|
||||
{
|
||||
name: "劳模先进",
|
||||
desc: "先进风采展示",
|
||||
icon: "medal-o",
|
||||
className: "feature-card--model",
|
||||
href: "/platform/honor/manage/h5"
|
||||
},
|
||||
{
|
||||
name: "普惠信息",
|
||||
desc: "普惠服务信息",
|
||||
icon: "bullhorn-o",
|
||||
className: "feature-card--inclusive",
|
||||
href: ""
|
||||
}
|
||||
],
|
||||
appLogo: "",
|
||||
appName: "",
|
||||
activeBannerIndex: 0,
|
||||
bannerLoadSeq: 0,
|
||||
bannerImageCache: {},
|
||||
defaultBannerList: [
|
||||
{ src: "/assets/mobile/img/home/home1.png" },
|
||||
{ src: "/assets/mobile/img/home/home2.png" },
|
||||
{ src: "/assets/mobile/img/home/home3.png" }
|
||||
],
|
||||
bannerList: [
|
||||
{ src: "/assets/mobile/img/home/home1.png" },
|
||||
{ src: "/assets/mobile/img/home/home2.png" },
|
||||
{ src: "/assets/mobile/img/home/home3.png" }
|
||||
],
|
||||
homeCachePrefix: "zhgh:h5:home:",
|
||||
homeCacheTtl: {
|
||||
banner: 30 * 60 * 1000,
|
||||
brand: 30 * 60 * 1000,
|
||||
recommend: 10 * 60 * 1000,
|
||||
classroom: 5 * 60 * 1000,
|
||||
activity: 2 * 60 * 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentBanner() {
|
||||
return this.bannerList[this.activeBannerIndex] || this.bannerList[0] || {}
|
||||
},
|
||||
currentClassroomCourseName() {
|
||||
const course = this.classroomCourses[this.classroomCourseIndex]
|
||||
return course && course.courseName ? course.courseName : ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCacheKey(key) {
|
||||
const userId = this.$store && this.$store.state.user ? (this.$store.state.user.id || "anonymous") : "anonymous"
|
||||
return this.homeCachePrefix + userId + ":" + key
|
||||
},
|
||||
readHomeCache(key) {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(this.getCacheKey(key))
|
||||
if (!raw) {
|
||||
return null
|
||||
}
|
||||
const cache = JSON.parse(raw)
|
||||
if (!cache || !cache.expireAt || cache.expireAt < Date.now()) {
|
||||
window.localStorage.removeItem(this.getCacheKey(key))
|
||||
return null
|
||||
}
|
||||
return cache.value
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
writeHomeCache(key, value, ttl) {
|
||||
try {
|
||||
window.localStorage.setItem(this.getCacheKey(key), JSON.stringify({
|
||||
expireAt: Date.now() + ttl,
|
||||
value: value
|
||||
}))
|
||||
} catch (e) {
|
||||
// Ignore cache quota and private mode errors.
|
||||
}
|
||||
},
|
||||
parseBannerList(value) {
|
||||
if (!value || typeof value !== "string") {
|
||||
return []
|
||||
}
|
||||
return value.split(",")
|
||||
.map(item => item.trim())
|
||||
.filter(item => item)
|
||||
.map(src => ({ src }))
|
||||
},
|
||||
listHomeBanner() {
|
||||
const cached = this.readHomeCache("h5Banner")
|
||||
if (cached && cached.length) {
|
||||
this.bannerList = cached
|
||||
this.activeBannerIndex = 0
|
||||
}
|
||||
this.$axios.post("/open/common/getConfigKey", { key: "H5AppHomeImg" }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const configBannerList = this.parseBannerList(res.data)
|
||||
const nextBannerList = configBannerList.length > 0 ? configBannerList : this.defaultBannerList
|
||||
this.bannerList = nextBannerList
|
||||
this.activeBannerIndex = 0
|
||||
this.writeHomeCache("h5Banner", nextBannerList, this.homeCacheTtl.banner)
|
||||
this.cacheBannerImages(nextBannerList)
|
||||
}
|
||||
}).catch(() => {
|
||||
if (!cached) {
|
||||
this.bannerList = this.defaultBannerList
|
||||
}
|
||||
})
|
||||
},
|
||||
shouldInlineBanner(src) {
|
||||
return typeof src === "string" && src.indexOf("/platform/sys/file/download") !== -1
|
||||
},
|
||||
cacheBannerImages(list) {
|
||||
const seq = ++this.bannerLoadSeq
|
||||
Promise.all(list.map((banner) => {
|
||||
return this.inlineBannerImage(banner.src).then((src) => Object.assign({}, banner, { src }))
|
||||
})).then((cachedList) => {
|
||||
if (seq === this.bannerLoadSeq) {
|
||||
this.bannerList = cachedList
|
||||
this.activeBannerIndex = 0
|
||||
}
|
||||
})
|
||||
},
|
||||
inlineBannerImage(src) {
|
||||
if (!this.shouldInlineBanner(src)) {
|
||||
return Promise.resolve(src)
|
||||
}
|
||||
if (this.bannerImageCache[src]) {
|
||||
return Promise.resolve(this.bannerImageCache[src])
|
||||
}
|
||||
return fetch(src, {
|
||||
credentials: "same-origin",
|
||||
cache: "force-cache"
|
||||
}).then((resp) => {
|
||||
if (!resp.ok) {
|
||||
throw new Error("load image failed")
|
||||
}
|
||||
return resp.blob()
|
||||
}).then((blob) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result)
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}).then((dataUrl) => {
|
||||
this.bannerImageCache[src] = dataUrl
|
||||
return dataUrl
|
||||
}).catch(() => src)
|
||||
},
|
||||
getConfigValue(key) {
|
||||
return this.$axios.post("/open/common/getConfigKey", { key }).then((res) => {
|
||||
return res.code === 0 ? (res.data || "") : ""
|
||||
}).catch(() => "")
|
||||
},
|
||||
listHomeBrand() {
|
||||
const cached = this.readHomeCache("brand")
|
||||
if (cached) {
|
||||
this.appLogo = cached.appLogo || ""
|
||||
this.appName = cached.appName || ""
|
||||
}
|
||||
Promise.all([
|
||||
this.getConfigValue("AppLogo"),
|
||||
this.getConfigValue("AppName")
|
||||
]).then(([appLogo, appName]) => {
|
||||
this.appLogo = appLogo
|
||||
this.appName = appName
|
||||
this.writeHomeCache("brand", { appLogo, appName }, this.homeCacheTtl.brand)
|
||||
})
|
||||
},
|
||||
listRecommendApp() {
|
||||
const cached = this.readHomeCache("recommend")
|
||||
if (cached && cached.length) {
|
||||
this.quickEntries = cached
|
||||
}
|
||||
this.$axios.post("/platform/home/listRecommendService", { platform: "H5" }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.quickEntries = res.data
|
||||
this.writeHomeCache("recommend", res.data || [], this.homeCacheTtl.recommend)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
listActivity() {
|
||||
const cached = this.readHomeCache("activity")
|
||||
if (cached && cached.length) {
|
||||
this.activityOptions = cached
|
||||
}
|
||||
this.$axios.post("/platform/home/listHomeActivity").then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.activityOptions = resp.data
|
||||
this.writeHomeCache("activity", resp.data || [], this.homeCacheTtl.activity)
|
||||
}
|
||||
})
|
||||
},
|
||||
listClassroomCourses() {
|
||||
const cached = this.readHomeCache("classroom")
|
||||
if (cached && cached.length) {
|
||||
this.classroomCourses = cached
|
||||
this.classroomCourseIndex = 0
|
||||
this.startClassroomCourseTimer()
|
||||
}
|
||||
this.$axios.post("/platform/learning/course/display/pageData", {
|
||||
pageNumber: 1,
|
||||
pageSize: 20
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
this.classroomCourses = data.list || []
|
||||
this.classroomCourseIndex = 0
|
||||
this.writeHomeCache("classroom", this.classroomCourses, this.homeCacheTtl.classroom)
|
||||
this.startClassroomCourseTimer()
|
||||
}
|
||||
}).catch(() => {
|
||||
if (!cached) {
|
||||
this.classroomCourses = []
|
||||
this.classroomCourseIndex = 0
|
||||
}
|
||||
})
|
||||
},
|
||||
startClassroomCourseTimer() {
|
||||
if (this.classroomCourseTimer) {
|
||||
clearInterval(this.classroomCourseTimer)
|
||||
this.classroomCourseTimer = null
|
||||
}
|
||||
if (this.classroomCourses.length <= 1) {
|
||||
return
|
||||
}
|
||||
this.classroomCourseTimer = setInterval(() => {
|
||||
this.classroomCourseIndex = (this.classroomCourseIndex + 1) % this.classroomCourses.length
|
||||
}, 3000)
|
||||
},
|
||||
menuClick(item) {
|
||||
this.$pjaxReplace(item.href)
|
||||
},
|
||||
//点击进入活动
|
||||
featureClick(item) {
|
||||
if (item.href) {
|
||||
this.$pjaxReplace(item.href)
|
||||
}
|
||||
},
|
||||
enterClassroom() {
|
||||
this.$pjaxReplace("/platform/learning/course/h5")
|
||||
},
|
||||
bannerChange(index) {
|
||||
this.activeBannerIndex = index
|
||||
},
|
||||
//鐐瑰嚮杩涘叆娲诲姩
|
||||
enterAct(item) {
|
||||
if(this.$moment().unix() < this.$moment(item.startDate).unix()) {
|
||||
this.$toast("活动即将开始")
|
||||
return
|
||||
}
|
||||
this.$pjaxReplace(item.h5Url)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.listHomeBanner()
|
||||
this.listHomeBrand()
|
||||
this.listRecommendApp()
|
||||
this.listActivity()
|
||||
this.listClassroomCourses()
|
||||
if (this.showLatestActivity) {
|
||||
this.listActivity()
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.classroomCourseTimer) {
|
||||
clearInterval(this.classroomCourseTimer)
|
||||
this.classroomCourseTimer = null
|
||||
}
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
.home__swipe .van-swipe-item img {
|
||||
.home-hero {
|
||||
position: relative;
|
||||
min-height: 281px;
|
||||
overflow: hidden;
|
||||
padding: 26px 16px 0;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.home-hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 187px;
|
||||
z-index: 1;
|
||||
width: 180%;
|
||||
height: 96px;
|
||||
transform: translateX(-50%);
|
||||
background-color: #fff;
|
||||
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
|
||||
}
|
||||
|
||||
.home-hero__bg {
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
top: -24px;
|
||||
width: calc(100% + 48px);
|
||||
height: 239px;
|
||||
object-fit: cover;
|
||||
filter: blur(20px);
|
||||
transform: scale(1.12);
|
||||
opacity: .88;
|
||||
transition: opacity .25s ease;
|
||||
}
|
||||
|
||||
.home-hero__mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 239px;
|
||||
background: rgba(0, 0, 0, .06);
|
||||
}
|
||||
|
||||
.home-hero__logo {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin-bottom: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.home-hero__logo-mark {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid rgba(255, 255, 255, .72);
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, .2);
|
||||
backdrop-filter: blur(2px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.home-hero__logo-img {
|
||||
display: block;
|
||||
max-width: 210px;
|
||||
max-height: 36px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.home-hero__logo-title {
|
||||
min-width: 0;
|
||||
margin-left: 10px;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
text-shadow: 0 1px 8px rgba(0, 0, 0, .24);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.home-hero__logo-title.is-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 154px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.home-hero__logo-title i {
|
||||
display: block;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, .72);
|
||||
}
|
||||
|
||||
.home-hero__logo-title i + i {
|
||||
width: 66%;
|
||||
height: 7px;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.home__swipe {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: visible;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.home__swipe-card {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background-color: rgba(255, 255, 255, .26);
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.home__swipe-card img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.home__swipe /deep/ .van-swipe__track,
|
||||
.home__swipe /deep/ .van-swipe-item {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.home__swipe /deep/ .van-swipe__indicators {
|
||||
bottom: -14px;
|
||||
}
|
||||
|
||||
.home__swipe /deep/ .van-swipe__indicator {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background-color: rgba(225, 59, 76, .28);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.home__swipe /deep/ .van-swipe__indicator--active {
|
||||
background-color: #e13b4c;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
background-color: #fff;
|
||||
margin: 10px;
|
||||
margin: 10px 10px 10px;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.quick-grid .van-grid-item .van-grid-item__text {
|
||||
.quick-scroll {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.quick-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.quick-scroll-grid {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-rows: repeat(2, 82px);
|
||||
grid-auto-columns: 25%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.quick-entry-item {
|
||||
min-width: 0;
|
||||
height: 82px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.quick-entry-item:active {
|
||||
background-color: #f2f7ff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.quick-entry-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 7px;
|
||||
color: #1989fa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-entry-icon img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.quick-entry-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 85%;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
color: #323233;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.feature-panel {
|
||||
margin: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-auto-rows: 82px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
padding: 14px 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
box-shadow: 0 6px 18px rgba(42, 65, 92, .08);
|
||||
}
|
||||
|
||||
.feature-card:active {
|
||||
transform: scale(.985);
|
||||
}
|
||||
|
||||
.feature-card--large {
|
||||
grid-column: span 2;
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
.feature-card__text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.feature-card__title {
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.feature-card__desc {
|
||||
color: rgba(55, 65, 81, .64);
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.feature-card__icon {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 12px;
|
||||
font-size: 36px;
|
||||
opacity: .78;
|
||||
}
|
||||
|
||||
.feature-card--large .feature-card__icon {
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
font-size: 56px;
|
||||
}
|
||||
|
||||
.feature-card--benefit,
|
||||
.feature-card--model {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-card--benefit .feature-card__desc,
|
||||
.feature-card--model .feature-card__desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.feature-card--benefit .feature-card__title,
|
||||
.feature-card--model .feature-card__title {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.feature-card--benefit .feature-card__icon,
|
||||
.feature-card--model .feature-card__icon {
|
||||
position: static;
|
||||
display: block;
|
||||
margin: 8px auto 0;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.feature-card--inclusive .feature-card__icon {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
.feature-card--activity {
|
||||
background: linear-gradient(145deg, #fff2e5 0%, #ffe2c8 100%);
|
||||
color: #ff7a25;
|
||||
}
|
||||
|
||||
.feature-card--benefit {
|
||||
background: linear-gradient(145deg, #fff1f4 0%, #ffd5dc 100%);
|
||||
color: #f35f74;
|
||||
}
|
||||
|
||||
.feature-card--model {
|
||||
background: linear-gradient(145deg, #eef5ff 0%, #d9e8ff 100%);
|
||||
color: #5b83d7;
|
||||
}
|
||||
|
||||
.feature-card--inclusive {
|
||||
grid-column: span 2;
|
||||
background: linear-gradient(145deg, #f2eeff 0%, #ded4ff 100%);
|
||||
color: #8063dc;
|
||||
}
|
||||
|
||||
.classroom-panel {
|
||||
height: 66px;
|
||||
margin: 10px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 8px 22px rgba(42, 65, 92, .07);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.classroom-panel:active {
|
||||
transform: scale(.99);
|
||||
}
|
||||
|
||||
.classroom-panel__title {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
color: #202124;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.classroom-panel__title::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -5px;
|
||||
width: 22px;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
background-color: #e13b4c;
|
||||
}
|
||||
|
||||
.classroom-panel__course {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
margin: 0 12px;
|
||||
color: #1989fa;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.classroom-panel__action {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: #969799;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.classroom-panel__action .van-icon {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.policy-panel {
|
||||
height: 66px;
|
||||
margin: 10px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 8px 22px rgba(42, 65, 92, .07);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.policy-panel__title {
|
||||
position: relative;
|
||||
color: #202124;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.policy-panel__title::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -5px;
|
||||
width: 22px;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
background-color: #e13b4c;
|
||||
}
|
||||
|
||||
.policy-panel__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #969799;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.policy-panel__action .van-icon {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.act-list {
|
||||
@@ -186,10 +939,15 @@ const home = {
|
||||
padding: 0 0 5px 5px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.section-title--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/deep/ .home__swipe img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
/deep/ .act-item-cover img {
|
||||
|
||||
@@ -22,10 +22,10 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
<div id="page-tarbar" class="page-tarbar">
|
||||
<van-tabbar v-model="homeTabbarActive" @change="homeTabbarChange">
|
||||
<van-tabbar-item name="home" icon="wap-home-o">首页</van-tabbar-item>
|
||||
<van-tabbar-item name="apps" icon="apps-o">应用</van-tabbar-item>
|
||||
<!-- <van-tabbar-item name="work" icon="gem">工作台</van-tabbar-item>-->
|
||||
<van-tabbar-item name="todo" icon="records-o">待办</van-tabbar-item>
|
||||
<van-tabbar-item name="home" icon="wap-home-o">首页</van-tabbar-item>
|
||||
<van-tabbar-item name="msg" icon="envelop-o">消息</van-tabbar-item>
|
||||
<van-tabbar-item name="mine" icon="user-o">我的</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
@@ -33,7 +33,7 @@ layout("/layouts/platform_h5.html"){
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script nonce="${cspNonce!}">
|
||||
<!--#include("home.js"){}#-->
|
||||
<!--#include("work.js"){}#-->
|
||||
<!--#include("mine.js"){}#-->
|
||||
@@ -60,17 +60,23 @@ layout("/layouts/platform_h5.html"){
|
||||
homeTabbarChange(val) {
|
||||
this.$store.commit("setActiveTarBar", val)
|
||||
},
|
||||
initHomeTabbarActive() {
|
||||
const activeTarBar = this.$store && this.$store.state ? this.$store.state.activeTarBar : ""
|
||||
const enabledTabs = ["apps", "todo", "home", "msg", "mine"]
|
||||
if (enabledTabs.indexOf(activeTarBar) > -1) {
|
||||
this.homeTabbarActive = activeTarBar
|
||||
return
|
||||
}
|
||||
this.homeTabbarActive = "home"
|
||||
this.$store.commit("setActiveTarBar", "home")
|
||||
},
|
||||
moduleClick(val) {
|
||||
console.log(val)
|
||||
this.homeTabbarActive = "work"
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.$store.state.activeTarBar) {
|
||||
this.$store.commit("setActiveTarBar", "home")
|
||||
} else {
|
||||
this.homeTabbarActive = this.$store.state.activeTarBar
|
||||
}
|
||||
this.initHomeTabbarActive()
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ const todo = {
|
||||
onView(task) {
|
||||
const {taskId, taskKey, instanceId, businessNo, formKey, h5FormKey} = task;
|
||||
if (!h5FormKey) {
|
||||
this.$toast("当前流程没有配置地址");
|
||||
this.$toast("当前流程暂不支持手机端审核,请前往PC端页面审核!");
|
||||
return;
|
||||
}
|
||||
this.$pjaxReplace(h5FormKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey+ "&tab=" + this.activeTab)
|
||||
|
||||
Reference in New Issue
Block a user