Merge remote-tracking branch 'origin/main'

This commit is contained in:
=
2025-09-18 08:40:17 +08:00
59 changed files with 3697 additions and 4029 deletions
+7
View File
@@ -408,6 +408,13 @@
<artifactId>juel</artifactId>
<version>2.1.3</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.bytedeco</groupId>-->
<!-- <artifactId>ffmpeg-platform</artifactId>-->
<!-- <version>7.1.1-1.5.12</version>-->
<!-- </dependency>-->
</dependencies>
<dependencyManagement>
<dependencies>
@@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
/**
* 文件下载工具类,使用本类前,对参数校验的异常使用CommonResponseUtil.renderError()方法进行渲染
@@ -8,6 +8,8 @@ import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
@@ -21,17 +23,20 @@ import java.util.List;
* 流程任务开始监听器
*/
@IocBean
@Slf4j
public class ProcessTaskStartEventListener implements ProcessEventListener {
@Inject
private Dao dao;
@Inject
private GlobalMessageSendService globalMessageSendService;
@Override
public void onEvent(ProcessEvent event) {
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) {
Long sourceId = event.getSourceId();
ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
sendMessage( task);
sendMessage(task);
}
}
@@ -54,8 +59,9 @@ public class ProcessTaskStartEventListener implements ProcessEventListener {
for (ProcessTaskActor taskActor : taskActors) {
// 模拟发送消息
String message = StrUtil.format("{}您有一条待办任务,实例:{}任务{}", taskActor.getActorName(), instanceName, taskDisplayName);
System.out.println(message);
String message = StrUtil.format("您有一条待办任务,实例:{}处理环节{}", taskActor.getActorName(), instanceName, taskDisplayName);
log.info(message);
globalMessageSendService.sendMessage(taskDisplayName, message, 2, List.of(taskActor.getActorId()), null);
}
@@ -2,6 +2,7 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
@@ -11,8 +12,11 @@ import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.services.SysConfigService;
import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.google.common.net.HttpHeaders;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -23,19 +27,21 @@ import org.nutz.mvc.upload.UploadAdaptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
@IocBean
@At("/platform/sys/file")
@Ok("json:full")
@Api(tags = "文件管理")
@Slf4j
public class SysFileController {
@Inject
private SysFileService sysFileService;
@Inject
private SysConfigService sysConfigService;
private SysFileMinIoUtil sysFileMinIoUtil;
@At("")
@Ok("beetl:/platform/sys/file/index.html")
@@ -110,7 +116,7 @@ public class SysFileController {
@SaCheckLogin
@ApiOperation("文件管理-转换HTML")
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
public Result convertHtml(TempFile file){
public Result convertHtml(TempFile file) {
return Result.success().addData(sysFileService.convertHtml(file));
}
@@ -141,5 +147,64 @@ public class SysFileController {
return Result.success(files);
}
@At
@SaCheckLogin
@ApiOperation("文件管理-视频播放")
@Ok("void")
public void videoPlay(String id, HttpServletRequest request, HttpServletResponse response) {
Sys_file sys_file = sysFileService.detail(id);
byte[] bytes = SysFileMinIoUtil.getFileBytes(sys_file.getBucket(), sys_file.getStoragePath());
// 设置响应头
String fileName = sys_file.getName();
String contentType = null;
switch (FileUtil.extName(fileName).toLowerCase()) {
case "mp4" -> contentType = "video/mp4";
case "avi" -> contentType = "video/x-msvideo";
case "mkv" -> contentType = "video/x-matroska";
case "mov" -> contentType = "video/quicktime";
case "wmv" -> contentType = "video/x-ms-wmv";
default -> contentType = "application/octet-stream";
}
response.setContentType(contentType);
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
String rangeHeader = request.getHeader(HttpHeaders.RANGE);
if (StrUtil.isNotBlank(rangeHeader) && rangeHeader.startsWith("bytes=")) {
// 处理Range请求
String[] ranges = rangeHeader.substring(6).split("-");
long start = StrUtil.isNotBlank(ranges[0]) ? Long.parseLong(ranges[0]) : 0;
long end = (ranges.length > 1 && StrUtil.isNotBlank(ranges[1])) ?
Long.parseLong(ranges[1]) : bytes.length - 1;
if (start >= bytes.length || end >= bytes.length) {
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + bytes.length);
response.setStatus(416);
return;
}
int contentLength = (int) (end - start + 1);
response.setStatus(206);
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
response.setHeader(HttpHeaders.CONTENT_RANGE, StrUtil.format("bytes {}-{}/{}", start, end, bytes.length));
try (OutputStream out = response.getOutputStream()) {
out.write(bytes, (int) start, contentLength);
} catch (IOException e) {
log.error("视频播放失败", e);
}
} else {
// 非Range请求
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(bytes.length));
try (OutputStream out = response.getOutputStream()) {
out.write(bytes);
} catch (IOException e) {
log.error("视频播放失败", e);
}
}
}
}
@@ -70,7 +70,7 @@ public class SysHomeController {
String userAgentStr = req.getHeader("User-Agent");
UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
if (!userAgent.isMobile()) {
return "beetl:/platform/sys/home/index.html";
return "beetl:/layouts/v4/home.html";
} else {
return "beetl:/platform/zhghh5/sys/home/index.html";
}
@@ -222,86 +222,6 @@ public class SysHomeController {
// return Result.success(hasPermisiionList);
}
@At
@SaCheckLogin
@ApiOperation("待办列表")
@Ok("json")
public Result listTodo(@Valid PageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
task.id,
inst.processInstanceName,
inst.processInstanceInitiatorName,
inst.processInstanceNodeName,
task.formUrl AS taskFormUrl,
task.formMobileUrl AS taskFormMobileUrl
FROM
`bpm_process_task` task
INNER JOIN bpm_process_instance inst ON inst.id = task.processInstanceId
WHERE
JSON_CONTAINS( task.assignments, @loginName )
AND task.taskStatus = 'ACTIVE'
GROUP BY
task.id
ORDER BY
task.createdOn DESC
""");
sql.setParam("loginName", "\"" + SecurityUtil.getUserLoginname() + "\"");
Pagination pagination = sysUserService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@SaCheckLogin
@ApiOperation("已办列表")
@Ok("json")
public Result listDone(@Valid PageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
task.id,
inst.processInstanceName,
inst.processInstanceInitiatorName,
inst.processInstanceNodeName,
task.formUrl AS taskFormUrl,
task.formMobileUrl AS taskFormMobileUrl
FROM
`bpm_process_task` task
INNER JOIN bpm_process_instance inst ON inst.id = task.processInstanceId
WHERE
JSON_CONTAINS( task.assignments, @loginName )
AND task.taskStatus in ('COMPLETE','TRANSFER')
GROUP BY
task.id
ORDER BY
task.createdOn DESC
""");
sql.setParam("loginName", "\"" + SecurityUtil.getUserLoginname() + "\"");
Pagination pagination = sysUserService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@SaCheckLogin
@ApiOperation("我的发起")
@Ok("json")
public Result listMyInitiation(@Valid PageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
id,
processInstanceName,
processInstanceInitiatorName,
processInstanceNodeName,
processInstanceUrl
FROM
bpm_process_instance
WHERE
processInstanceInitiatorLoginName = @loginName
ORDER BY processInstanceInitiationTime DESC
""");
sql.setParam("loginName", SecurityUtil.getUserLoginname());
Pagination pagination = sysUserService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@SaCheckLogin
@@ -2,6 +2,7 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import com.budwk.app.sys.services.SysUserService;
import io.swagger.annotations.Api;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -13,6 +14,7 @@ import javax.servlet.http.HttpServletRequest;
@IocBean
@At("/platform/v4")
@Api(value = "V4首页")
public class SysHomeV4Controller {
@Inject
@@ -27,9 +29,6 @@ public class SysHomeV4Controller {
}
/**
* 首页门户
*/
@At("/home")
@Ok("beetl:/layouts/v4/home.html")
@SaCheckLogin
@@ -37,13 +36,6 @@ public class SysHomeV4Controller {
}
@At("/home2")
@Ok("beetl:/layouts/v4/home2.html")
@SaCheckLogin
public void home2() {
}
/**
* 应用中心
*/
@@ -64,6 +56,27 @@ public class SysHomeV4Controller {
}
/**
* 待办中心
*/
@At("/todo")
@Ok("beetl:/layouts/v4/todo.html")
@SaCheckLogin
public void todo(){
}
/**
* 消息中心
*/
@At("/msg")
@Ok("beetl:/layouts/v4/msg.html")
@SaCheckLogin
public void msg(){
}
/**
* 子系统
* @param appId 应用ID
@@ -2,6 +2,7 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
@@ -339,7 +340,11 @@ public class SysUserController {
@SaCheckLogin
public Result subAppMenus(@Param("appId") String appId) {
List<Sys_menu> menus = sysUserService.getMenus(SecurityUtil.getUserId());
List<Sys_menu> list = Sys_menu.createTreeMenus(menus, appId);
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();
return Result.success().addData(self);
}
return Result.success(list);
}
@@ -0,0 +1,166 @@
package com.budwk.app.sys.controller.v4;
import cn.dev33.satoken.annotation.SaCheckLogin;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.message.models.GlobalMessage;
import com.budwk.app.zhgh.dayofficework.message.models.GlobalMessageReceiver;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageService;
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.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@IocBean
@At("/platform/v4/msg")
@Api(value = "消息中心", tags = "消息中心接口")
@Ok("json:full")
public class SysV4MsgController {
@Inject
private Dao dao;
@Inject
private GlobalMessageService globalMessageService;
@Inject
private GlobalMessageSendService globalMessageSendService;
@At
@ApiOperation("获取消息列表")
@SaCheckLogin
public Result pageData(PageForm pageForm, @Param("isRead") int isRead, @Param("type") Integer type, HttpServletRequest req) {
Sql sql = Sqls.create("""
SELECT
m.id,
m.title,
m.content,
m.type,
r.isRead,
r.readTime
FROM
global_message m
INNER JOIN global_message_receiver r ON m.id = r.messageId
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("m.status", "=", 2);
cnd.and("r.receiverId", "=", SecurityUtil.getUserId());
if (isRead == 0) {
cnd.and("r.isRead", "=", 0);
} else if (isRead == 1) {
cnd.and("r.isRead", "=", 1);
}
cnd.andEX("m.type", "=", type);
cnd.desc("m.sendTime");
sql.setCondition(cnd);
String title = "审批待办";
String content = "您有一个新的审批任务需要处理,请及时登录系统查看。";
List<String> receiverIds = Arrays.asList("17a7f8ad3ee947b4a26175049a9c253d");
// 自动发送到所有启用的渠道
// globalMessageSendService.sendMessage(title, content, 2, receiverIds, null);
Pagination pagination = globalMessageService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At("/stats")
@ApiOperation("获取消息统计信息")
@SaCheckLogin
public Result getStats() {
List<GlobalMessage> messageList = dao.query(GlobalMessage.class, Cnd.where(GlobalMessage::getStatus, "=", 2));
List<String> messageIds = messageList.stream().map(GlobalMessage::getId).toList();
// 查询总数
int totalCount = dao.count(GlobalMessageReceiver.class, Cnd.where(GlobalMessageReceiver::getReceiverId, "=", SecurityUtil.getUserId()).and(GlobalMessageReceiver::getMessageId, "in", messageIds));
// 查询未读数
int unreadCount = dao.count(GlobalMessageReceiver.class, Cnd.where(GlobalMessageReceiver::getReceiverId, "=", SecurityUtil.getUserId()).and(GlobalMessageReceiver::getIsRead, "=", 0).and(GlobalMessageReceiver::getMessageId, "in", messageIds));
// 查询已读数
int readCount = totalCount - unreadCount;
NutMap stats = NutMap.NEW()
.addv("total", totalCount)
.addv("unread", unreadCount)
.addv("read", readCount);
// .addv("typeStats", typeStats);
return Result.success(stats);
}
@At("/detail/?")
@ApiOperation("获取消息详情")
@SaCheckLogin
public Result getDetail(String messageId) {
GlobalMessageReceiver receiver = dao.fetch(GlobalMessageReceiver.class, Cnd.where(GlobalMessageReceiver::getMessageId, "=", messageId).and(GlobalMessageReceiver::getReceiverId, "=", SecurityUtil.getUserId()));
dao.fetchLinks(receiver, "globalMessage");
// 标记为已读
if (receiver != null && !receiver.getIsRead()) {
receiver.setIsRead(true);
receiver.setReadTime(new Date());
dao.update(receiver);
}
return Result.success(receiver);
}
@At("/read/?")
@ApiOperation("标记消息为已读")
public Result markAsRead(String messageId) {
GlobalMessageReceiver receiver = dao.fetch(GlobalMessageReceiver.class, Cnd.where("messageId", "=", messageId).and("receiverId", "=", SecurityUtil.getUserId()));
if (receiver != null && !receiver.getIsRead()) {
receiver.setIsRead(true);
receiver.setReadTime(new Date());
dao.update(receiver);
}
return Result.success();
}
@At("/read/batch")
@ApiOperation("批量标记消息为已读")
@SaCheckLogin
public Result batchMarkAsRead(@Param("messageIds") String[] messageIds) {
List<GlobalMessageReceiver> receivers = dao.query(GlobalMessageReceiver.class, Cnd.where("messageId", "in", messageIds).and("receiverId", "=", SecurityUtil.getUserId()));
for (GlobalMessageReceiver receiver : receivers) {
receiver.setIsRead(true);
receiver.setReadTime(new Date());
}
dao.update(receivers);
return Result.success();
}
@At("/read/all")
@ApiOperation("标记所有消息为已读")
@SaCheckLogin
public Result markAllAsRead() {
List<GlobalMessageReceiver> receivers = dao.query(GlobalMessageReceiver.class, Cnd.where("receiverId", "=", SecurityUtil.getUserId()));
for (GlobalMessageReceiver receiver : receivers) {
receiver.setIsRead(true);
receiver.setReadTime(new Date());
}
dao.update(receivers);
return Result.success();
}
}
@@ -132,21 +132,4 @@ public class Sys_menu extends BaseModel implements Serializable {
//子菜单
private List<Sys_menu> children = new ArrayList<>();
/**
* 前端菜单数据
*
* @param menus
* @param parentId
* @return
*/
public static List<Sys_menu> createTreeMenus(List<Sys_menu> menus, String parentId) {
List<Sys_menu> filterMenus = menus.stream().filter(v -> StringUtils.defaultString(v.getParentId(), "").equals(StringUtils.defaultString(parentId, ""))).collect(Collectors.toList());
for (Sys_menu menu : filterMenus) {
List<Sys_menu> childMenus = createTreeMenus(menus, menu.getId());
menu.setChildren(childMenus);
}
return new ArrayList<>(filterMenus);
}
}
@@ -147,7 +147,7 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
@Override
public Sys_file detail(String id) {
return null;
return fetch(id);
}
@Override
@@ -181,13 +181,13 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
String[] parts = srcValue.split(";base64,");
String base64Content = parts[1];
try{
try {
//转为在线地址 base64很喜欢被信息中心拦截
byte[] imageBytes = Base64.getDecoder().decode(base64Content);
String imgUrl = storageFile(SysFileEngineTypeEnum.MINIO.getValue(), genFileKey(R.UU32(), R.UU32() + ".png"), imageBytes, false);
String replacement = "<img src=\"" + imgUrl + "\">";
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
}catch (Exception e){
} catch (Exception e) {
matcher.appendReplacement(sb, Matcher.quoteReplacement("<img src=>"));
}
}
@@ -13,6 +13,7 @@ import com.budwk.app.base.exception.UnknownAccountException;
import com.budwk.app.base.interceptor.sLog.SLogService;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.PwdUtil;
import com.budwk.app.base.utils.SysMenuUtil;
import com.budwk.app.sys.models.*;
import com.budwk.app.sys.services.SysMenuService;
import com.budwk.app.sys.services.SysRoleService;
@@ -126,27 +127,27 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
List<Sys_module> userModules = dao().query(Sys_module.class, Cnd.where("id", "in", allModuleIds.toArray()).asc(Sys_module::getSortNum));
//设置pc菜单到模块
if (Lang.isNotEmpty(pcModuleIds)) {
//用户的pc端模块
List<Sys_module> pcModules = userModules.stream().filter(module -> pcModuleIds.contains(module.getId())).toList();
//pc菜单转为树结构
List<Sys_menu> pcTreeMenus = Sys_menu.createTreeMenus(pcMenus, null);
for (Sys_module pcModule : pcModules) {
pcModule.setMenus(pcTreeMenus.stream().filter(menu -> menu.getModuleId().equals(pcModule.getId())).collect(Collectors.toList()));
}
user.setPcModuleMenus(pcModules);
}
//设置h5菜单到模块
if (Lang.isNotEmpty(h5ModuleIds)) {
//用户的h5端模块
List<Sys_module> h5Modules = userModules.stream().filter(module -> h5ModuleIds.contains(module.getId())).toList();
List<Sys_menu> h5TreeMenus = Sys_menu.createTreeMenus(h5Menus, null);
for (Sys_module h5Module : h5Modules) {
h5Module.setMenus(h5TreeMenus.stream().filter(menu -> menu.getModuleId().equals(h5Module.getId())).collect(Collectors.toList()));
}
user.setH5ModuleMenus(h5Modules);
}
// if (Lang.isNotEmpty(pcModuleIds)) {
// //用户的pc端模块
// List<Sys_module> pcModules = userModules.stream().filter(module -> pcModuleIds.contains(module.getId())).toList();
// //pc菜单转为树结构
// List<Sys_menu> pcTreeMenus = SysMenuUtil.createTreeMenus(pcMenus, null);
// for (Sys_module pcModule : pcModules) {
// pcModule.setMenus(pcTreeMenus.stream().filter(menu -> menu.getModuleId().equals(pcModule.getId())).collect(Collectors.toList()));
// }
// user.setPcModuleMenus(pcModules);
// }
//
// //设置h5菜单到模块
// if (Lang.isNotEmpty(h5ModuleIds)) {
// //用户的h5端模块
// List<Sys_module> h5Modules = userModules.stream().filter(module -> h5ModuleIds.contains(module.getId())).toList();
// List<Sys_menu> h5TreeMenus = SysMenuUtil.createTreeMenus(h5Menus, null);
// for (Sys_module h5Module : h5Modules) {
// h5Module.setMenus(h5TreeMenus.stream().filter(menu -> menu.getModuleId().equals(h5Module.getId())).collect(Collectors.toList()));
// }
// user.setH5ModuleMenus(h5Modules);
// }
return user;
}
@@ -1,6 +1,7 @@
package com.budwk.app.web.commons.auth.service;
import cn.dev33.satoken.stp.StpUtil;
import com.budwk.app.base.utils.SysMenuUtil;
import com.budwk.app.sys.models.Sys_menu;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.services.SysUserService;
@@ -64,7 +65,7 @@ public class AuthService {
public Object getSubAppMenus(String appId) {
List<Sys_menu> menus = sysUserService.getMenus(SecurityUtil.getUserId());
List<Sys_menu> pcTreeMenus = Sys_menu.createTreeMenus(menus, appId);
List<Sys_menu> pcTreeMenus = SysMenuUtil.createTreeMenus(menus, appId);
return pcTreeMenus;
}
}
@@ -5,6 +5,8 @@ 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_file;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.budwk.app.zhgh.dayofficework.edu.models.EduChapters;
import com.budwk.app.zhgh.dayofficework.edu.models.EduCourses;
import com.budwk.app.zhgh.dayofficework.edu.models.EduVideos;
@@ -132,6 +134,15 @@ public class EduCoursesController {
@SLog(tag = "理论学习课程", msg = "添加课程视频")
public Result insert(EduVideos eduVideos) {
dao.insert(eduVideos);
String url = eduVideos.getUrl();
String id = url.split("id=")[1];
Sys_file file = dao.fetch(Sys_file.class, id);
byte[] bytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
// 获取视频时长
return Result.success();
}
@@ -115,20 +115,19 @@ public class H5EduController {
}
// 获取课程总视频数
// int totalVideos = dao.count("edu_videos v",
// "LEFT JOIN edu_chapters c ON v.chapterId = c.id",
// Cnd.where("c.courseId", "=", courseId));
int totalVideos = 10;
List<EduChapters> chapters = dao.query(EduChapters.class, Cnd.where(EduChapters::getCourseId, "=", courseId));
List<String> chapterIds = chapters.stream().map(EduChapters::getId).toList();
int totalVideos = dao.count(EduVideos.class, Cnd.where(EduVideos::getChapterId, "in", chapterIds));
// 获取已完成的视频ID列表
List<EduStudyRecords> completedRecords = dao.query(EduStudyRecords.class,
Cnd.where("userId", "=", userId)
.and("courseId", "=", courseId)
.and("completed", "=", true));
Cnd.where(EduStudyRecords::getUserId, "=", userId)
.and(EduStudyRecords::getCourseId, "=", courseId)
.and(EduStudyRecords::getIsCompleted, "=", 1));
List<String> completedVideoIds = completedRecords.stream()
.map(EduStudyRecords::getVideoId)
.collect(java.util.stream.Collectors.toList());
.map(EduStudyRecords::getVideoId)
.collect(java.util.stream.Collectors.toList());
int completedVideos = completedVideoIds.size();
@@ -196,13 +195,8 @@ public class H5EduController {
return Result.error("观看时长不能为空或小于0");
}
String userId = SecurityUtil.getUserId();
if (userId == null) {
return Result.error("用户未登录");
}
EduStudyRecords record = eduStudyRecordsService.saveOrUpdateWatchProgress(
userId, videoId, courseId, watchedDuration);
SecurityUtil.getUserId(), videoId, courseId, watchedDuration);
return Result.success(record);
} catch (Exception e) {
@@ -8,8 +8,6 @@ import java.util.List;
/**
* @ClassName EduStudyRecordsService
* @Description 学习记录服务接口
* @Author AI Assistant
* @Date 2024/01/01
*/
public interface EduStudyRecordsService extends BaseService<EduStudyRecords> {
@@ -79,17 +77,17 @@ public interface EduStudyRecordsService extends BaseService<EduStudyRecords> {
// Getters and Setters
public int getTotalVideos() { return totalVideos; }
public void setTotalVideos(int totalVideos) { this.totalVideos = totalVideos; }
public int getCompletedVideos() { return completedVideos; }
public void setCompletedVideos(int completedVideos) { this.completedVideos = completedVideos; }
public int getTotalDuration() { return totalDuration; }
public void setTotalDuration(int totalDuration) { this.totalDuration = totalDuration; }
public int getWatchedDuration() { return watchedDuration; }
public void setWatchedDuration(int watchedDuration) { this.watchedDuration = watchedDuration; }
public double getCompletionRate() { return completionRate; }
public void setCompletionRate(double completionRate) { this.completionRate = completionRate; }
}
}
}
@@ -1,5 +1,6 @@
package com.budwk.app.zhgh.dayofficework.edu.service.impl;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.edu.models.EduStudyRecords;
import com.budwk.app.zhgh.dayofficework.edu.models.EduVideos;
@@ -17,8 +18,6 @@ import java.util.List;
/**
* @ClassName EduStudyRecordsServiceImpl
* @Description 学习记录服务实现类
* @Author AI Assistant
* @Date 2024/01/01
*/
@Slf4j
@IocBean(args = {"refer:dao"})
@@ -74,7 +73,7 @@ public class EduStudyRecordsServiceImpl extends BaseServiceImpl<EduStudyRecords>
}
} catch (Exception e) {
log.error("保存观看进度失败", e);
throw new RuntimeException("保存观看进度失败", e);
throw new BaseException("保存观看进度失败", e);
}
}
@@ -8,6 +8,7 @@ import lombok.experimental.Accessors;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
import java.util.List;
@Table("global_message")
@@ -74,6 +75,11 @@ public class GlobalMessage extends BaseModel {
@ColDefine(type = ColType.INT)
private Integer status;
@Column
@Comment("发送时间")
@ColDefine(type = ColType.DATETIME)
private Date sendTime;
@Column
@Comment("发送成功")
@ColDefine(type = ColType.BOOLEAN)
@@ -54,6 +54,7 @@ public class GlobalMessageReceiver extends BaseModel {
@ColDefine(type = ColType.DATETIME)
private Date readTime;
@One(target = GlobalMessage.class, field = "messageId")
private GlobalMessage globalMessage;
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.message.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.message.models.GlobalMessage;
public interface GlobalMessageService extends BaseService<GlobalMessage> {
}
@@ -0,0 +1,14 @@
package com.budwk.app.zhgh.dayofficework.message.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.message.models.GlobalMessage;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class GlobalMessageServiceImpl extends BaseServiceImpl<GlobalMessage> implements GlobalMessageService {
public GlobalMessageServiceImpl(Dao dao) {
super(dao);
}
}
@@ -6,6 +6,7 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvBank;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
@@ -49,7 +50,7 @@ public class QsvActivityController {
public Result pageData(@Valid PageForm pageForm, Integer year, String title) {
Cnd cnd = Cnd.NEW();
cnd.andEX("YEAR(startTime)", "=", year);
cnd.and(Cnd.likeEX("title",title));
cnd.and(Cnd.likeEX("title", title));
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
@@ -79,7 +80,7 @@ public class QsvActivityController {
return Result.success(activity);
}
// 删除问卷
// 删除问卷
@At
@SaCheckPermission("qsv.activity")
@ApiOperation("删除问卷")
@@ -90,7 +91,7 @@ public class QsvActivityController {
}
// 保存问卷题目
// 保存问卷题目
@At
@SaCheckPermission("qsv.activity")
@Aop(TransAop.READ_COMMITTED)
@@ -143,15 +144,23 @@ public class QsvActivityController {
return Result.success();
}
// 查询问卷题目
// 查询问卷题目
@At
@SaCheckPermission("qsv.activity")
@ApiOperation("查询问卷题目")
@SLog(type = "qsv.activity", tag = "查询问卷题目", msg = "查询问卷题目")
public Result listSubjects(@Valid String activityId) {
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
dao.fetchLinks(subjects, "options",Cnd.NEW().asc("sortNum"));
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
return Result.success(subjects);
}
@At
@SaCheckPermission("qsv.activity")
@ApiOperation("查询问卷题库")
public Result listBank(@Valid String category) {
List<QsvBank> list = dao.query(QsvBank.class, Cnd.where(QsvBank::getCategory, "=", category));
return Result.success(list);
}
}
@@ -0,0 +1,94 @@
package com.budwk.app.zhgh.dayofficework.qsv.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
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.zhgh.dayofficework.qsv.models.QsvBank;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvBankService;
import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.Arrays;
@IocBean
@At("/platform/qsv/bank")
@Ok("json:full")
@ApiOperation("问卷调查题库管理")
public class QsvBankController {
@Inject
private Dao dao;
@Inject
private QsvBankService qsvBankService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/qsv/bank/index.html")
@SaCheckPermission("qsv.bank")
public void index() {
}
@At
@SaCheckPermission("qsv.bank")
public Result pageData(PageForm pageForm) {
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
cnd.where().andLike(QsvBank::getTitle, pageForm.getSearchKeyword());
}
Pagination pagination = qsvBankService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
@At
@SaCheckPermission("qsv.bank")
@ApiOperation("保存题库")
public Result save(@Param("qsvBank") QsvBank qsvBank) {
dao.insert(qsvBank);
return Result.success();
}
@At
@SaCheckPermission("qsv.bank")
@ApiOperation("修改题库")
public Result update(@Param("qsvBank") QsvBank qsvBank) {
dao.updateIgnoreNull(qsvBank);
return Result.success();
}
@At
@SaCheckPermission("qsv.bank")
public Result findOne(@Valid String id) {
QsvBank qsvBank = dao.fetch(QsvBank.class, id);
return Result.success(qsvBank);
}
@At
@SaCheckPermission("qsv.bank")
@ApiOperation("删除题库")
public Result delete(@Valid String id) {
dao.delete(QsvBank.class, id);
return Result.success();
}
@At
@SaCheckPermission("qsv.bank")
@ApiOperation("保存题库题目")
public Result saveSubjects(@Valid String id, @Param("subjects") QsvSubject[] subjects) {
QsvBank qsvBank = dao.fetch(QsvBank.class, id);
qsvBank.setSubjects(Arrays.asList(subjects));
dao.updateIgnoreNull(qsvBank);
return Result.success();
}
}
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
@@ -25,13 +26,13 @@ public class H5QsvController {
@At("")
@Ok("beetl:/platform/zhghh5/dayofficework/qsv/index.html")
@SaCheckLogin
@SaCheckPermission("h5.qsv")
public void index() {
}
@At
@SaCheckLogin
@SaCheckPermission("h5.qsv")
public Result pageData(@Valid PageForm pageForm, @Valid String category) {
Cnd cnd = Cnd.NEW();
cnd.andEX("category", "=", category);
@@ -0,0 +1,45 @@
package com.budwk.app.zhgh.dayofficework.qsv.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
@Table("qsv_bank")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("问卷调查题库")
public class QsvBank extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("题库名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String title;
@Column
@Comment("说明描述")
@ColDefine(type = ColType.TEXT)
private String description;
@Column
@Comment("所属模块(quiz, survey, vote)")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String category;
@Column
@Comment("题目")
@ColDefine(type = ColType.MYSQL_JSON)
private List<QsvSubject> subjects;
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.qsv.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvBank;
public interface QsvBankService extends BaseService<QsvBank> {
}
@@ -0,0 +1,14 @@
package com.budwk.app.zhgh.dayofficework.qsv.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvBank;
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvBankService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class QsvBankServiceImpl extends BaseServiceImpl<QsvBank> implements QsvBankService {
public QsvBankServiceImpl(Dao dao) {
super(dao);
}
}
@@ -46,8 +46,8 @@ public class SuggestionBoxMineController {
@At
@Ok("beetl:/platform/zhghh5/democratic/suggestionBox/mine/index.html")
@SaCheckLogin
public void h5Index() {
@SaCheckPermission("h5.suggestionBox.mine")
public void h5() {
}
@@ -24,6 +24,7 @@ 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.List;
@IocBean
@@ -48,8 +49,8 @@ public class SuggestionBoxWriteController {
@At
@Ok("beetl:/platform/zhghh5/democratic/suggestionBox/write/index.html")
@SaCheckLogin
public void h5Index(){
@SaCheckPermission("h5.suggestionBox.write")
public void h5(){
}
@@ -58,6 +59,7 @@ public class SuggestionBoxWriteController {
@ApiOperation("保存申请")
@SLog(tag = "建言献策-填写申请", msg = "保存申请")
public Result save(@Param("data") SuggestionBox suggestionBox) {
suggestionBox.setSubmitTime(new Date());
dao.insertOrUpdate(suggestionBox);
return Result.success();
}
@@ -69,6 +71,7 @@ public class SuggestionBoxWriteController {
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "建言献策-填写申请", msg = "提交申请")
public Result submit(@Param("data") SuggestionBox suggestionBox) {
suggestionBox.setSubmitTime(new Date());
dao.insertOrUpdate(suggestionBox);
// 开启流程实例
Dict args = Dict.create();
@@ -1,6 +1,5 @@
package com.budwk.app.zhgh.democratic.suggestionBox.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
@@ -43,8 +42,8 @@ public class SuggestionXghController {
@At
@Ok("beetl:/platform/zhghh5/democratic/suggestionBox/xgh/index.html")
@SaCheckPermission("suggestionBox.xgh")
public void h5Index() {
@SaCheckPermission("h5.suggestionBox.xgh")
public void h5() {
}
@@ -44,11 +44,11 @@ function pjaxReplace(url, data = {}) {
}
$.pjax({
url: url,
container: "#container",
container: "#sub-app-container-main-content-body",
maxCacheLength: 0,
push: false,
replace: true,
fragment: "#container",
fragment: "#sub-app-container-main-content-body",
timeout: 8000,
data
})
@@ -48,13 +48,13 @@ const commonUtil = {
let loading = null
if(isMobile){
if (isMobile) {
loading = vant.Toast.loading({
duration: 0,
forbidClick: true,
message: '导出中,请耐心等待'
});
}else{
} else {
loading = ELEMENT.Loading.service({
lock: true,
text: "导出中,请耐心等待",
@@ -64,7 +64,7 @@ const commonUtil = {
}
Vue.prototype.$axios
.post(url, data, { responseType: "blob" })
.post(url, data, {responseType: "blob"})
.then((response) => {
if (response.data.type === "application/json") {
try {
@@ -102,9 +102,9 @@ const commonUtil = {
loading.close()
})
.finally(() => {
if(isMobile){
if (isMobile) {
loading.clear()
}else{
} else {
loading.close()
}
})
@@ -234,6 +234,7 @@ const commonUtil = {
function doSm2Encrypt(msgString) {
return sm2.doEncrypt(msgString, publicKey, cipherMode)
}
// SM2数组加密
function doSm2ArrayEncrypt(msgString) {
return sm2.doEncrypt(msgString, publicKey, cipherMode)
@@ -243,6 +244,24 @@ const commonUtil = {
doSm2Encrypt,
doSm2ArrayEncrypt
}
},
// pjax跳转
pjaxPush(url, data = {}) {
const {pathname, search} = location
if (url === pathname + search) {
return
}
$.pjax({
url: url,
container: "#sub-app-container-main-content-body",
maxCacheLength: 0,
push: false,
replace: true,
fragment: "#sub-app-container-main-content-body",
timeout: 8000,
data
})
}
}
@@ -259,7 +278,8 @@ function GetQueryString(name) {
}
function clearAllTimers() {
const maxTimeoutId = setTimeout(function () {}, 0)
const maxTimeoutId = setTimeout(function () {
}, 0)
for (let i = 0; i <= maxTimeoutId; i++) {
clearTimeout(i)
clearInterval(i)
@@ -291,10 +311,10 @@ function base64ToFile(base64Data, filename) {
}
// 使用Blob对象创建File对象
const blob = new Blob([uInt8Array], { type: contentType })
const blob = new Blob([uInt8Array], {type: contentType})
blob.lastModifiedDate = new Date()
blob.name = filename
return new File([blob], filename, { type: contentType })
return new File([blob], filename, {type: contentType})
}
@@ -1,8 +1,72 @@
/**
* Minified by jsDelivr using clean-css v5.3.3.
* Original file: /npm/nprogress@0.2.0/nprogress.css
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
#nprogress{pointer-events:none}#nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0,-4px);-ms-transform:rotate(3deg) translate(0,-4px);transform:rotate(3deg) translate(0,-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
/*# sourceMappingURL=/sm/4400c1e6b434bc414f3870cc8d155b3335f1f32d1a07381e06cc8a8bd869de73.map */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: #29d;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #29d;
border-left-color: #29d;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@@ -1,179 +1,252 @@
<template>
<div class="guava-main-content">
<div v-show="v === 'index'" key="index" class="transition-item">
<slot></slot>
</div>
<el-card v-if="v === 'edit'" key="edit" shadow="never" class="animated-card">
<template #header>
<el-row type="flex">
<el-col :span="12">
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="v = 'index'">返回</el-button>
</el-col>
<el-col :span="12" style="display: flex; justify-content: flex-end">
<slot name="edit_func"></slot>
</el-col>
</el-row>
</template>
<div :class="{ card_scroll: edit_scroll }">
<slot name="edit"></slot>
</div>
</el-card>
<el-card v-if="v === 'view'" key="view" shadow="never" class="animated-card">
<template #header>
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="v = 'index'">返回</el-button>
</template>
<div :class="{ card_scroll: !view_page_scroll }">
<slot name="view"></slot>
</div>
</el-card>
<el-card v-if="v === 'approval'" key="approval" shadow="never" class="animated-card">
<template #header>
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="v = 'index'">返回</el-button>
</template>
<div :class="{ card_scroll: approval_scroll }">
<slot name="approval"></slot>
</div>
</el-card>
<el-card v-if="v === 'public'" key="public" shadow="never" class="animated-card">
<div slot="header" class="clearfix">
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="back()">返回</el-button>
</div>
<div :class="{ card_scroll: public_card_scroll }">
<slot name="public"></slot>
</div>
</el-card>
<div class="guava-main-content">
<div v-show="v === 'index'" key="index" class="transition-item">
<slot></slot>
</div>
<div v-if="v === 'edit'" key="edit" class="page-container animated-card">
<div class="page-header">
<div class="header-left">
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="v = 'index'">返回</el-button>
</div>
<div class="header-right">
<slot name="edit_func"></slot>
</div>
</div>
<div class="page-content">
<slot name="edit"></slot>
</div>
<div class="page-footer" v-if="$slots.edit_func">
<slot name="edit_func"></slot>
</div>
</div>
<div v-if="v === 'view'" key="view" class="page-container animated-card">
<div class="page-header">
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="v = 'index'">返回</el-button>
</div>
<div class="page-content">
<slot name="view"></slot>
</div>
<div class="page-footer" v-if="$slots.view_footer">
<slot name="view_footer"></slot>
</div>
</div>
<div v-if="v === 'approval'" key="approval" class="page-container animated-card">
<div class="page-header">
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="v = 'index'">返回</el-button>
</div>
<div class="page-content">
<slot name="approval"></slot>
</div>
<div class="page-footer" v-if="$slots.approval_footer">
<slot name="approval_footer"></slot>
</div>
</div>
<div v-if="v === 'public'" key="public" class="page-container animated-card">
<div class="page-header">
<el-button icon="el-icon-back" style="font-size: 16px; padding: 0" type="text" @click="back()">返回</el-button>
</div>
<div class="page-content">
<slot name="public"></slot>
</div>
<div class="page-footer" v-if="$slots.public_footer">
<slot name="public_footer"></slot>
</div>
</div>
</div>
</template>
<script>
module.exports = {
props: {
value: {
type: String,
default: "index"
},
//true card-body edit view
edit_scroll: {
type: Boolean,
default: false
},
view_scroll: {
type: Boolean,
default: true
},
approval_scroll: {
type: Boolean,
default: false
},
//view
view_page_scroll: {
type: Boolean,
default: false
},
public_card_scroll: {
type: Boolean,
default: true
}
props: {
value: {
type: String,
default: "index"
},
data() {
return {
v: "index"
}
//true card-body edit view
edit_scroll: {
type: Boolean,
default: false
},
watch: {
v(newValue, oldValue) {
this.$emit("vchange", { newValue, oldValue })
this.$emit("input", newValue)
}
view_scroll: {
type: Boolean,
default: true
},
methods: {
index(callback = () => {}) {
this.v = "index"
this.$nextTick(() => {
callback()
})
},
edit(callback = () => {}) {
this.v = "edit"
this.$nextTick(() => {
callback()
})
},
view(callback = () => {}) {
this.v = "view"
this.$nextTick(() => {
callback()
})
},
approval(callback = () => {}) {
this.v = "approval"
this.$nextTick(() => {
callback()
})
},
public(callback = () => {}) {
this.v = "public"
this.$nextTick(() => {
callback()
})
},
back(callback = () => {}) {
this.v = "index"
this.$nextTick(() => {
callback()
})
}
approval_scroll: {
type: Boolean,
default: false
},
//view
view_page_scroll: {
type: Boolean,
default: false
},
public_card_scroll: {
type: Boolean,
default: true
}
},
data() {
return {
v: "index"
}
},
watch: {
value: {
handler(newVal) {
this.v = newVal
},
immediate: true
},
v(newValue, oldValue) {
this.$emit("vchange", { newValue, oldValue })
this.$emit("input", newValue)
}
},
methods: {
index(callback = () => {}) {
this.v = "index"
this.$nextTick(() => {
callback()
})
},
edit(callback = () => {}) {
this.v = "edit"
this.$nextTick(() => {
callback()
})
},
view(callback = () => {}) {
this.v = "view"
this.$nextTick(() => {
callback()
})
},
approval(callback = () => {}) {
this.v = "approval"
this.$nextTick(() => {
callback()
})
},
public(callback = () => {}) {
this.v = "public"
this.$nextTick(() => {
callback()
})
},
back(callback = () => {}) {
this.v = "index"
this.$nextTick(() => {
callback()
})
}
}
}
</script>
<style>
<style scoped>
.guava-main-content {
min-height: 100%;
/*height: 100vh;
overflow: hidden;*/
}
.page-container {
height: calc(100vh - 84px);
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
display: flex;
flex-direction: column;
animation-name: cardAnimation;
animation-duration: 0.3s;
animation-fill-mode: both;
}
.page-header {
flex-shrink: 0;
padding: 12px 24px;
border-bottom: 1px solid #ebeef5;
background: #fff;
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
flex: 1;
}
.header-right {
flex: 1;
display: flex;
justify-content: flex-end;
}
.header-right button{
padding: 9px 15px;
font-size: 12px;
}
.page-content {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
position: relative;
}
/* 隐藏滚动条但保持滚动功能 */
.page-content::-webkit-scrollbar {
width: 6px;
}
.page-content::-webkit-scrollbar-track {
background: transparent;
}
.page-content::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.1);
border-radius: 3px;
}
.page-content::-webkit-scrollbar-thumb:hover {
background: rgba(0,0,0,0.2);
}
.page-footer{
flex-shrink: 0;
padding: 12px 10px;
border-top: 1px solid rgb(235, 238, 245);
display: flex;
justify-content: end;
align-items: center;
}
@keyframes cardAnimation {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animated-card {
animation-name: cardAnimation;
animation-duration: 0.5s;
animation-fill-mode: both;
margin-top: 0 !important;
animation-name: cardAnimation;
animation-duration: 0.3s;
animation-fill-mode: both;
}
@keyframes cardAnimation {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card_scroll {
max-height: calc(100vh - 56px - 40px - 30px - 50px - 34px);
overflow-y: auto;
position: relative;
}
.card_scroll {
overflow: scroll;
}
.card_scroll::-webkit-scrollbar {
display: none;
}
.card_scroll::-webkit-scrollbar-vertical {
display: none;
}
.card_scroll::-webkit-scrollbar-horizontal {
display: none;
.transition-item {
height: 100%;
}
</style>
+102 -50
View File
@@ -112,6 +112,7 @@ layout("/layouts/v4/baseLayout.html"){
.content-body {
min-height: 400px;
height: 100%;
}
@media (max-width: 768px) {
@@ -137,7 +138,8 @@ layout("/layouts/v4/baseLayout.html"){
</div>
<el-scrollbar>
<el-menu unique-opened :default-active="activeMenuIndex" :default-openeds="openedMenus" @select="menuSelect">
<el-menu unique-opened :default-active="activeMenuIndex" :default-openeds="openedMenus"
@select="menuSelect">
<menu-list :menus="leftMenus" :depth="0"></menu-list>
</el-menu>
</el-scrollbar>
@@ -145,11 +147,6 @@ layout("/layouts/v4/baseLayout.html"){
<!-- 右侧主体区域 -->
<main class="sub-app-container-main-content" id="sub-app-container-main-content">
<!-- <header class="content-header">-->
<!-- <h1 class="content-title"></h1>-->
<!-- &lt;!&ndash; <p class="content-subtitle"></p>&ndash;&gt;-->
<!-- </header>-->
<div class="content-body" id="sub-app-container-main-content-body">${layoutContent!}</div>
</main>
@@ -169,7 +166,7 @@ layout("/layouts/v4/baseLayout.html"){
})
$(document).on("pjax:send", function () {
NProgress.configure({ parent: ".sub-app-container-main-content" })
NProgress.configure({parent: ".sub-app-container-main-content"})
NProgress.start()
$("#sub-app-container-main-content").hide()
})
@@ -190,7 +187,8 @@ layout("/layouts/v4/baseLayout.html"){
try {
const app = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app"))
$("#sub-app-container #sidebar-menu .menu-header .menu-title").text(app?.name)
} catch (e) {}
} catch (e) {
}
}
document.addEventListener("DOMContentLoaded", function () {
@@ -209,36 +207,41 @@ layout("/layouts/v4/baseLayout.html"){
"menu-list": {
name: "menu-list",
template:
/*language=HTML*/
`
<div>
<template v-for="(item, index) in menus">
<el-submenu :index="item.id" v-if="item.children && item.children.length > 0">
<template #title>
<i :class="item.icon" v-if="item.icon" :size="18"></i>
<i v-else class="el-icon-paperclip"></i>
<span>{{ item.name }}</span>
</template>
<menu-list :menus="item.children" :depth="depth + 1" />
</el-submenu>
<el-menu-item :index="item.id" v-else>
<a :href="item.href" data-pjax>
<i :class="item.icon" v-if="item.icon" :size="18"></i>
<i v-else :class="item.icon ? item.icon : 'el-icon-menu'"></i>
<span>{{ item.name }}</span>
</a>
</el-menu-item>
</template>
</div>
`,
/*language=HTML*/
`
<div>
<template v-for="(item, index) in menus">
<el-submenu :index="item.id" v-if="item.children && item.children.length > 0">
<template #title>
<i :class="item.icon" v-if="item.icon" :size="18"></i>
<i v-else class="el-icon-paperclip"></i>
<span>{{ item.name }}</span>
</template>
<menu-list :menus="item.children" :depth="depth + 1"/>
</el-submenu>
<el-menu-item :index="item.id" v-else>
<a :href="item.href" data-pjax>
<i :class="item.icon" v-if="item.icon" :size="18"></i>
<i v-else :class="item.icon ? item.icon : 'el-icon-menu'"></i>
<span>{{ item.name }}</span>
</a>
</el-menu-item>
</template>
</div>
`,
props: ["menus", "depth"]
}
},
data() {
return {
// 菜单数据
menus: [],
// 当前激活的菜单索引
activeMenuIndex: null,
openedMenus: []
// 当前打开的菜单索引
openedMenus: [],
// 默认选中菜单是否加载中
defaultSelectLoading: true
}
},
computed: {
@@ -260,12 +263,13 @@ layout("/layouts/v4/baseLayout.html"){
const app = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app"))
window.sessionStorage.setItem("zhgh_sub_app_left_menu_active_index-" + app.id, index)
window.sessionStorage.setItem("zhgh_sub_app_left_menu_opens-" + app.id, JSON.stringify(indexPath))
} catch (e) {}
} catch (e) {
}
},
// 获取菜单
getMenus(appId) {
if (!appId) return
$.get("/platform/sys/user/subAppMenus", { appId }).then((res) => {
this.$axios.post("/platform/sys/user/subAppMenus", {appId}).then((res) => {
if (res.code === 0) {
this.menus = res.data
window.sessionStorage.setItem("zhgh_sub_app_menus", JSON.stringify(res.data))
@@ -276,6 +280,7 @@ layout("/layouts/v4/baseLayout.html"){
})
},
// 设置应用信息
setAppInfo(app) {
if (app) {
// 储存到缓存
@@ -294,26 +299,73 @@ layout("/layouts/v4/baseLayout.html"){
this.openedMenus = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app_left_menu_opens-" + app.id)) || []
console.log(this.openedMenus)
console.log(this.activeMenuIndex)
} catch (e) {}
if (!this.activeMenuIndex) {
this.defaultSelect()
}
} catch (e) {
}
},
// 默认选中
defaultSelect() {
// 查找第一个有href的子菜单
function findFirstChildWithHref(menus) {
for (const menu of menus) {
// 如果当前菜单有href,返回它
if (menu.href) {
return {
menu: menu,
parentIds: [menu.parentId]
};
}
// 如果有子菜单,递归查找
if (menu.children && menu.children.length > 0) {
const result = findFirstChildWithHref(menu.children);
if (result) {
// 添加当前菜单的ID到父ID集合
result.parentIds.push(menu.id);
return result;
}
}
}
return null;
}
const result = findFirstChildWithHref(this.menus)
if (result) {
this.activeMenuIndex = result.menu.id
this.openedMenus = result.parentIds
this.$nextTick(() => {
this.menuSelect(this.activeMenuIndex, this.openedMenus)
// 使用pjax跳转
commonUtil.pjaxPush(result.menu.href)
})
}
},
init() {
// 页面加载时获取到正确的菜单
const pathname = window.location.pathname
if (!pathname.startsWith("/platform/v4/subApp")) {
this.$axios.post("/platform/sys/user/rootMenuByPath", {pathname}).then((res) => {
if (res.code === 0) {
this.setAppInfo(res.data)
this.getMenus(res.data?.id)
}
})
} else {
const appId = GetQueryString("appId")
if (!appId) return
this.getMenus(appId)
}
}
},
mounted() {
// 页面加载时获取到正确的菜单
const pathname = window.location.pathname
if (!pathname.startsWith("/platform/v4/subApp")) {
$.get("/platform/sys/user/rootMenuByPath", { pathname }).then((res) => {
if (res.code === 0) {
this.setAppInfo(res.data)
this.getMenus(res.data?.id)
}
})
} else {
const appId = GetQueryString("appId")
if (!appId) {
return
}
this.getMenus(appId)
}
this.init()
}
})
</script>
@@ -165,12 +165,12 @@
} else {
window.location.href = "/platform/h5/home"
}
func && func()
func && typeof func === "function" && func()
}
Vue.mixin({
methods: {
historyBack: function(func) {
historyBack: function(func = ()=>{}) {
historyBack(func);
}
}
@@ -34,8 +34,8 @@
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
<!-- pjax是异步加载html片段的工具,模拟前端路由机制 -->
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
<!--nprogress 配合pjax使用-->
<link ref="stylesheet" href="${base!}/assets/platform/plugins/nprogress/nprogress.css"/>
<!-- nprogress 配合pjax使用 -->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/nprogress/nprogress.css" />
<script src="${base!}/assets/platform/plugins/nprogress/nprogress.js"></script>
<!-- 农历插件 -->
@@ -100,6 +100,8 @@
ELEMENT.Dialog.props.closeOnClickModal.default = false
ELEMENT.Dialog.props.top.default = "50px"
ELEMENT.Table.props.border.default = true
ELEMENT.TableColumn.props.headerAlign.default = 'center'
ELEMENT.TableColumn.props.align.default = 'center'
ELEMENT.TableColumn.props.showOverflowTooltip = { type: Boolean, default: true }
Vue.use(ELEMENT, {
zIndex: 20000
@@ -606,7 +608,7 @@
</div>
<nav class="v4-nav">
<a href="/platform/v4/home2" data-pjax class="v4-nav-item">
<a href="/platform/v4/home" data-pjax class="v4-nav-item">
<i class="fa fa-home"></i>
首页
</a>
@@ -618,10 +620,14 @@
<i class="fa fa-th-large"></i>
服务中心
</a>
<a href="/flow/todoCenter" data-pjax class="v4-nav-item">
<a href="/platform/v4/todo" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
待办中心
</a>
<a href="/platform/v4/msg" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
消息中心
</a>
</nav>
</div>
@@ -666,7 +672,7 @@
var currentPath = window.location.pathname
var footer = $("#v4-footer")
if (currentPath === "/platform/v4/home2") {
if (currentPath === "/platform/v4/home") {
footer.show()
} else {
footer.hide()
File diff suppressed because it is too large Load Diff
@@ -1,936 +0,0 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<style>
/* 主容器 */
.oa-container {
position: relative;
}
/* 主要内容区域 */
.oa-main-content {
margin: 0 auto;
padding-bottom: 20px;
background: url(/assets/platform/img/v4/home-bg.png);
background-size: cover;
}
/* 背景图和搜索统计区域容器 */
.oa-hero-section {
position: relative;
height: 350px;
overflow: hidden;
}
.oa-background-image {
width: 100%;
height: 100%;
}
.oa-background-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 搜索和统计区域 */
.oa-search-stats-section {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: grid;
grid-template-columns: 1fr 500px;
gap: 40px;
align-items: center;
width: 1500px;
z-index: 10;
}
/* 搜索区域 */
.oa-search-section {
text-align: left;
border-radius: 20px;
padding: 40px;
}
.oa-search-container {
display: flex;
max-width: 600px;
background: white;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.oa-search-input {
flex: 1;
padding: 15px 25px;
border: none;
outline: none;
font-size: 16px;
background: transparent;
}
.oa-search-btn {
background: #ff6b35;
color: white;
border: none;
padding: 15px 30px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
.oa-search-btn:hover {
background: #e55a2b;
}
/* 数字提醒区域 */
.oa-stats-section {
background: rgba(0, 109, 185, 0.7);
padding: 20px 25px;
}
.oa-stats-title {
color: white;
font-size: 20px;
font-weight: 600;
margin: 0 0 15px 0;
text-align: left;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.oa-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.oa-stat-card {
background: #ffffff;
padding: 20px;
display: flex;
flex-direction: column;
gap: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(255, 255, 255, 0.8);
position: relative;
overflow: hidden;
}
.oa-stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #4a90e2, #357abd);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.oa-stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
cursor: pointer;
}
.oa-stat-card:hover::before {
transform: scaleX(1);
}
.oa-stat-top {
display: flex;
align-items: center;
gap: 12px;
}
.oa-stat-icon {
font-size: 28px;
color: #4a90e2;
flex-shrink: 0;
transition: all 0.3s ease;
}
.oa-stat-card:hover .oa-stat-icon {
transform: scale(1.1);
color: #357abd;
}
.oa-stat-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.oa-stat-number {
font-size: 24px;
font-weight: 700;
color: #2c3e50;
line-height: 1;
transition: color 0.3s ease;
}
.oa-stat-card:hover .oa-stat-number {
color: #4a90e2;
}
.oa-stat-label {
font-weight: 500;
line-height: 1;
text-align: left;
font-size: 18px;
}
/* 活动中心 */
.oa-box-1-section {
display: grid;
grid-template-columns: 400px 1fr;
gap: 30px;
border-radius: 6px;
overflow: hidden;
width: 1500px;
margin: 20px auto 0;
}
/* 左侧用户信息 */
.oa-user-panel {
background: rgba(0, 109, 185, 0.9);
color: white;
padding: 25px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
max-height: 410px;
}
.oa-user-panel::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
z-index: 0;
}
.oa-user-panel > * {
position: relative;
z-index: 1;
}
.oa-user-avatar {
width: 90px;
height: 90px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
margin-bottom: 15px;
border: 3px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
}
.oa-user-avatar:hover {
transform: scale(1.05);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.oa-user-name {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.oa-user-info {
line-height: 1.6;
margin-bottom: 20px;
background: rgba(255, 255, 255, 0.05);
padding: 15px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
text-align: left;
width: 100%;
}
.oa-user-info .info-item {
margin-bottom: 8px;
display: flex;
align-items: center;
line-height: 1.6;
width: 100%;
}
.oa-user-panel .info-label {
font-weight: 600;
margin-right: 8px;
flex-shrink: 0;
min-width: 50px;
max-width: 60px;
text-align: left;
}
.oa-user-panel .info-value {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.oa-user-homepage-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
padding: 10px 20px;
border-radius: 25px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.oa-user-homepage-btn:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
color: white;
text-decoration: none;
}
/* 右侧活动列表 */
.oa-activity-content {
padding: 20px;
background: #ffffff;
max-height: 410px;
display: flex;
flex-direction: column;
min-width: 0; /* 允许flex子项收缩 */
overflow: hidden; /* 防止内容溢出 */
}
.oa-activity-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
flex-shrink: 0; /* 防止头部被压缩 */
}
.oa-activity-tabs {
display: flex;
gap: 10px;
}
.oa-activity-tab {
padding: 10px 0;
font-size: 20px;
color: #666;
cursor: pointer;
transition: color 0.3s ease;
position: relative;
background: none;
border: none;
outline: none;
}
.oa-activity-tab:hover {
color: #4a90e2;
}
.oa-activity-tab.active {
color: #4a90e2;
font-weight: 500;
}
.oa-activity-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #4a90e2;
border-radius: 1px;
}
/* 活动轮播样式 */
.oa-activity-carousel {
flex: 1;
margin-top: 15px;
}
.oa-activity-carousel .el-carousel,.oa-activity-carousel .el-carousel .el-carousel__container {
height: 100%;
}
.oa-activity-carousel .el-carousel__item {
display: flex;
justify-content: center;
align-items: center;
}
.oa-activity-card {
width: 100%;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
height: 100%;
position: relative;
cursor: pointer;
}
.oa-activity-image {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.oa-activity-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.oa-activity-card:hover .oa-activity-image img {
transform: scale(1.05);
}
/* 图片上的文字叠加层 */
.oa-activity-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
padding: 40px 20px 20px;
color: white;
}
.oa-activity-title {
font-size: 18px;
font-weight: 600;
color: white;
margin-bottom: 8px;
line-height: 1.4;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.oa-activity-meta {
font-size: 14px;
color: white;
margin-bottom: 15px;
line-height: 1.5;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
/* 无活动时的提示区域 */
.oa-no-activity {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
background: #f8f9fa;
border-radius: 12px;
color: #6c757d;
text-align: center;
}
.oa-no-activity-icon {
font-size: 48px;
margin-bottom: 16px;
color: #dee2e6;
}
.oa-no-activity-title {
font-size: 18px;
font-weight: 500;
margin-bottom: 8px;
color: #495057;
}
.oa-no-activity-desc {
font-size: 14px;
color: #6c757d;
}
/* box2 - 系统通知公告 */
.oa-box-2-section {
width: 1500px;
margin: 30px auto 0;
background: #ffffff;
overflow: hidden;
}
.oa-notice-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
color: #000;
}
.oa-notice-header-content {
display: flex;
align-items: center;
gap: 12px;
position: relative;
}
.oa-notice-header-content::after {
content: '';
position: absolute;
bottom: -10px;
left: 0;
right: 0;
height: 2px;
background-color: #000000;
}
.oa-notice-header-icon {
font-size: 24px;
}
.oa-notice-header-title {
font-size: 20px;
font-weight: 600;
margin: 0;
}
.oa-notice-header-more {
flex-shrink: 0;
}
.oa-header-more-btn {
background: transparent;
border: none;
color: #666;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border-radius: 4px;
transition: all 0.3s ease;
}
.oa-header-more-btn:hover {
background: #f5f5f5;
color: #4a90e2;
}
.oa-header-more-btn i {
font-size: 12px;
transition: transform 0.3s ease;
}
.oa-header-more-btn:hover i {
transform: translateX(2px);
}
.oa-notice-content {
padding: 10px;
}
.oa-notice-list {
display: grid;
gap: 15px;
}
.oa-notice-item {
display: flex;
align-items: flex-start;
padding: 15px 20px;
background: #ffffff;
border-bottom: 1px solid #e8e8e8;
transition: all 0.2s ease;
cursor: pointer;
}
.oa-notice-item:hover {
background: #f5f5f5;
}
.oa-notice-item:last-child {
border-bottom: none;
}
.oa-notice-item-icon {
width: 16px;
height: 16px;
color: #666;
margin-right: 12px;
margin-top: 2px;
flex-shrink: 0;
}
.oa-notice-item-content {
flex: 1;
min-width: 0;
}
.oa-notice-item-title {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.oa-notice-item-desc {
font-size: 14px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.oa-notice-item-date {
margin-left: 15px;
flex-shrink: 0;
white-space: nowrap;
font-size: 16px;
}
</style>
<div class="oa-container" id="v4-home-app">
<!-- 顶部导航 -->
<!-- 主要内容 -->
<div class="oa-main-content">
<!-- 背景图和搜索统计区域容器 -->
<div class="oa-hero-section">
<!-- 背景图 -->
<div class="oa-background-image">
<img
src="https://i.cpu.edu.cn/mnews/_upload/article/images/ed/dc/eff0e4b84ee6b2d7e770ed1de071/0f515087-45e3-4dbf-acf4-e7096d6cbdc0.jpg"
alt=""
/>
</div>
<!-- 搜索和统计区域 -->
<div class="oa-search-stats-section">
<!-- 搜索区域 -->
<div class="oa-search-section">
<!-- <h1 class="oa-search-title">精业济群</h1>-->
<div class="oa-search-container">
<input type="text" class="oa-search-input" placeholder="请输入您要查询的关键字"
v-model="searchQuery" />
<button class="oa-search-btn" @click="performSearch">搜索</button>
</div>
</div>
<!-- 数字提醒 -->
<div class="oa-stats-section">
<h3 class="oa-stats-title">提醒</h3>
<div class="oa-stats-grid">
<div class="oa-stat-card" @click="handleStatClick('todo')">
<div class="oa-stat-top">
<div class="oa-stat-icon">
<i class="fa fa-clock-o"></i>
</div>
<div class="oa-stat-number">{{ stats.todo }}</div>
</div>
<div class="oa-stat-label">待办</div>
</div>
<div class="oa-stat-card" @click="handleStatClick('done')">
<div class="oa-stat-top">
<div class="oa-stat-icon">
<i class="fa fa-check-circle"></i>
</div>
<div class="oa-stat-number">{{ stats.done }}</div>
</div>
<div class="oa-stat-label">已办</div>
</div>
<div class="oa-stat-card" @click="handleStatClick('notification')">
<div class="oa-stat-top">
<div class="oa-stat-icon">
<i class="fa fa-bell"></i>
</div>
<div class="oa-stat-number">{{ stats.notifications }}</div>
</div>
<div class="oa-stat-label">消息</div>
</div>
<div class="oa-stat-card" @click="handleStatClick('started')">
<div class="oa-stat-top">
<div class="oa-stat-icon">
<i class="fa fa-file-text"></i>
</div>
<div class="oa-stat-number">{{ stats.started }}</div>
</div>
<div class="oa-stat-label">申请</div>
</div>
</div>
</div>
</div>
</div>
<!-- box1 -->
<div class="oa-box-1-section">
<!-- 左侧用户信息 -->
<div class="oa-user-panel">
<div class="oa-user-avatar">
<i class="fa fa-user"></i>
</div>
<div class="oa-user-name">${@auth.getPrincipalProperty('username')}</div>
<div class="oa-user-info">
<div class="info-item">
<span class="info-label">工号:</span>
<span class="info-value">${@auth.getPrincipalProperty('loginname')}</span>
</div>
<div class="info-item">
<span class="info-label">性别:</span>
<span class="info-value">${@auth.getPrincipalProperty('sex')}</span>
</div>
<div class="info-item">
<span class="info-label">单位:</span>
<span class="info-value">
<!--#if(!isEmpty(@auth.getPrincipalProperty('unit'))){#-->
${@auth.getPrincipalProperty('unit').getName()}
<!--#}#-->
</span>
</div>
<div class="info-item">
<span class="info-label">工会:</span>
<span class="info-value">
<!--#if(!isEmpty(@auth.getPrincipalProperty('union'))){#-->
${@auth.getPrincipalProperty('union').getName()}
<!--#}#-->
</span>
</div>
</div>
<a href="/platform/v4/personCenter" class="oa-user-homepage-btn" target="_blank">
<i class="fa fa-home"></i>
个人主页
</a>
</div>
<!-- 右侧活动列表 -->
<div class="oa-activity-content">
<div class="oa-activity-header">
<div class="oa-activity-tabs">
<div class="oa-activity-tab" :class="{active: activeTab === 'recent'}"
@click="setActiveTab('recent')">最新活动
</div>
<div class="oa-activity-tab" :class="{active: activeTab === 'history'}"
@click="setActiveTab('history')">历史活动
</div>
</div>
</div>
<div class="oa-activity-carousel">
<div v-if="currentActivityList.length === 0" class="oa-no-activity">
<div class="oa-no-activity-icon">
<i class="fa fa-calendar-o"></i>
</div>
<div class="oa-no-activity-title">暂无活动信息</div>
<div class="oa-no-activity-desc">{{ activeTab === 'recent' ? '当前没有最新活动' : '当前没有历史活动'
}}
</div>
</div>
<el-carousel v-else :autoplay="true" arrow="always" indicator-position="none">
<el-carousel-item v-for="activity in currentActivityList" :key="activity.id">
<div class="oa-activity-card">
<div class="oa-activity-image">
<img
src="https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?w=800&auto=format&fit=crop"
:alt="activity.name" />
<div class="oa-activity-overlay">
<div class="oa-activity-title">{{ activity.name }}</div>
<div class="oa-activity-meta">{{ activity.startDate }} ~ {{ activity.endDate
}}
</div>
</div>
</div>
</div>
</el-carousel-item>
</el-carousel>
</div>
</div>
</div>
<!-- box2 - 系统通知公告 -->
<div class="oa-box-2-section">
<div class="oa-notice-header">
<div class="oa-notice-header-content">
<div class="oa-notice-header-icon">
<i class="fa fa-bullhorn"></i>
</div>
<h2 class="oa-notice-header-title">系统通知公告</h2>
</div>
<div class="oa-notice-header-more">
<button class="oa-header-more-btn">
更多
<i class="fa fa-angle-right"></i>
</button>
</div>
</div>
<div class="oa-notice-content">
<div class="oa-notice-list">
<div v-for="notice in noticeList" :key="notice.id" class="oa-notice-item" @click="viewNotice(notice)">
<div class="oa-notice-item-icon">
<i class="fa fa-bullhorn"></i>
</div>
<div class="oa-notice-item-content">
<div class="oa-notice-item-title">{{ notice.title }}</div>
<div class="oa-notice-item-desc">{{ notice.content }}</div>
</div>
<div class="oa-notice-item-date">{{ notice.publishDate }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const vue = new Vue({
el: "#v4-home-app",
data() {
return {
searchQuery: "",
activeTab: "recent",
noticeTab: "system",
currentIndex: 0,
stats: {
todo: 3,
done: 0,
notifications: 7,
started: 9
},
activityList: [],
noticeList: [
{
id: 1,
type: 'system',
title: '系统维护通知',
content: '系统将于本周六晚上22:00-24:00进行例行维护,期间可能影响正常使用,请提前做好相关准备。',
publishDate: '2024-01-15',
isRead: false
},
{
id: 2,
type: 'announcement',
title: '新功能上线公告',
content: '移动端应用已正式上线,支持手机端办公审批,欢迎大家下载使用。',
publishDate: '2024-01-14',
isRead: true
},
{
id: 3,
type: 'urgent',
title: '紧急安全提醒',
content: '近期发现钓鱼邮件攻击,请勿点击可疑链接,如有疑问请联系IT部门。',
publishDate: '2024-01-13',
isRead: false
}
]
}
},
computed: {
recentActivities() {
const now = new Date()
return this.activityList
.filter(activity => new Date(activity.endDate) >= now)
.sort((a, b) => new Date(b.startDate) - new Date(a.startDate))
},
historyActivities() {
const now = new Date()
return this.activityList
.filter(activity => new Date(activity.endDate) < now)
.sort((a, b) => new Date(b.startDate) - new Date(a.startDate))
},
currentActivityList() {
return this.activeTab === "recent" ? this.recentActivities : this.historyActivities
}
},
mounted() {
// 初始化数据
this.getActivity()
this.getStatistics()
},
methods: {
performSearch() {
if (this.searchQuery.trim()) {
console.log("搜索:", this.searchQuery)
// 实现搜索逻辑
}
},
setActiveTab(tab) {
this.activeTab = tab
this.currentIndex = 0 // 切换标签时重置索引
},
getActivity() {
$.get("/platform/home/listHomeActivity").then((res) => {
if (res.code === 0) {
this.activityList = res.data
}
})
},
viewNotice(notice) {
console.log('查看通知:', notice.title)
// 标记为已读
notice.isRead = true
// 这里可以添加查看详情的逻辑
},
handleStatClick(val){
if(['todo', 'done', 'application']){
window.open('/flow/todoCenter?status=' + val)
}
},
async getStatistics() {
try {
const res = await $.post("/flow/todoCenter/statistics")
if (res.code === 0) {
const { todoCount, doneCount, startedCount } = res.data
this.stats.todo = todoCount
this.stats.done = doneCount
this.stats.started = startedCount
}
} catch (error) {
this.$message.error("获取统计数据失败")
console.error("获取统计数据失败:", error)
}
},
}
})
</script>
<!--#}#-->
@@ -0,0 +1,444 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<style>
.message-center {
padding: 20px;
}
.statistics-section {
margin-bottom: 20px;
}
.stat-card {
border: none;
height: 100%;
cursor: pointer;
transition: all 0.3s ease;
}
.stat-card .el-card__body {
padding: 20px;
}
.stat-content {
display: flex;
align-items: center;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
font-size: 28px;
}
.all-icon {
background-color: rgba(64, 158, 255, 0.1);
color: #409eff;
}
.unread-icon {
background-color: rgba(245, 108, 108, 0.1);
color: #f56c6c;
}
.read-icon {
background-color: rgba(103, 194, 58, 0.1);
color: #67c23a;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
/* 搜索区域样式 */
.filter-section {
margin-bottom: 20px;
}
.filter-section .el-card__body {
padding: 15px 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
}
.search-form .el-form-item {
margin-bottom: 0;
margin-right: 15px;
}
/* 消息列表区域样式 */
.message-section .el-card__header {
padding: 0;
border-bottom: none;
}
/* 优化后的 tabs 样式 */
.message-header .el-tabs__header {
margin: 0;
padding: 0 20px;
background-color: #fff;
}
.message-header .el-tabs__nav-wrap::after {
height: 1px;
background-color: #ebeef5;
}
.message-header .el-tabs__item {
height: 50px;
line-height: 50px;
font-size: 15px;
color: #606266;
padding: 0 20px;
transition: all 0.3s;
}
.message-header .el-tabs__item.is-active {
color: var(--color-primary);
font-weight: 500;
}
.message-header .el-tabs__active-bar {
height: 3px;
border-radius: 3px;
}
.message-title {
font-weight: 500;
color: #303133;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
padding: 10px 0;
}
</style>
<div id="app" v-cloak>
<div class="message-center">
<!-- 统计数据区域 -->
<el-row :gutter="20" class="statistics-section">
<el-col :span="8">
<el-card shadow="never" class="stat-card">
<div class="stat-content">
<div class="stat-icon all-icon">
<i class="el-icon-message"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{stats.total}}</div>
<div class="stat-label">全部消息</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="stat-card">
<div class="stat-content">
<div class="stat-icon unread-icon">
<i class="el-icon-bell"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{stats.unread}}</div>
<div class="stat-label">未读消息</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="stat-card">
<div class="stat-content">
<div class="stat-icon read-icon">
<i class="el-icon-finished"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{stats.read}}</div>
<div class="stat-label">已读消息</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 搜索筛选区域 -->
<el-card shadow="never" class="filter-section">
<el-form :inline="true" :model="filters" class="search-form">
<el-form-item>
<el-select v-model="filters.type" placeholder="消息类型" clearable @change="loadMessages"
style="width: 150px;">
<el-option label="全部类型" value=""></el-option>
<el-option label="系统公告" value="1"></el-option>
<el-option label="消息通知" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="pageForm.isRead == 0">
<el-button size="medium" type="primary" @click="markAllAsRead" :loading="loading">
<i class="el-icon-check"></i> 全部标记已读
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 消息列表区域 -->
<el-card shadow="never" class="message-section">
<div slot="header" class="message-header">
<el-tabs v-model="pageForm.isRead" @tab-click="handleTabClick">
<el-tab-pane label="全部消息" name="-1">
<span slot="label">
<i class="el-icon-message"></i>
全部消息
</span>
</el-tab-pane>
<el-tab-pane label="未读消息" name="0">
<span slot="label">
<i class="el-icon-bell"></i>
未读消息
</span>
</el-tab-pane>
<el-tab-pane label="已读消息" name="1">
<span slot="label">
<i class="el-icon-finished"></i>
已读消息
</span>
</el-tab-pane>
</el-tabs>
</div>
<!-- 表格展示 -->
<el-table v-loading="loading" :data="messages" style="width: 100%" :key="currentTab"
empty-text="暂无消息"
:header-cell-style="{backgroundColor: '#f5f7fa'}">
<el-table-column prop="type" label="消息类型" width="120">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.type === 1 ? 'danger' : 'primary'">
{{getMessageTypeName(scope.row.type)}}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span class="message-title">{{scope.row.title}}</span>
</template>
</el-table-column>
<el-table-column prop="content" label="内容预览" min-width="250" show-overflow-tooltip>
<template slot-scope="scope">
{{getContentPreview(scope.row.content)}}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="接收时间" width="180">
<template slot-scope="scope">
{{formatTime(scope.row.createdAt)}}
</template>
</el-table-column>
<el-table-column prop="isRead" label="状态" width="100">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.isRead ? 'success' : 'warning'">
{{scope.row.isRead ? '已读' : '未读'}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="viewMessage(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="messages.length > 0">
<el-pagination
background
@current-change="handlePageChange"
:current-page="pageForm.pageNumber"
:page-size="pageForm.pageSize"
layout="prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</div>
</el-card>
</div>
<!-- 消息详情弹窗 -->
<el-dialog
:visible.sync="detailVisible"
:title="currentMessage?.globalMessage?.title"
width="60%"
class="message-detail-dialog"
>
<div v-if="currentMessage.id">
<div style="text-align: center; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #eee;">
<div style="font-size: 18px; font-weight: 500; margin-bottom: 8px;">{{currentMessage.title}}</div>
<div style="font-size: 14px; color: #666;">
<span>{{getMessageTypeName(currentMessage?.globalMessage?.type)}}</span>
<span style="margin: 0 8px;"></span>
<span>{{formatTime(currentMessage.createdAt)}}</span>
</div>
</div>
<div style="line-height: 1.8; font-size: 15px;" v-html="currentMessage?.globalMessage?.content"></div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
currentTab: 'all',
loading: false,
messages: [],
stats: {
total: 0,
unread: 0,
read: 0
},
filters: {
type: ''
},
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0
},
detailVisible: false,
currentMessage: {}
}
},
methods: {
async loadStats() {
try {
const resp = await this.$axios.get('/platform/v4/msg/stats')
if (resp.code === 0) {
this.stats = resp.data
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
},
async loadMessages() {
this.loading = true
try {
const resp = await this.$axios.post('/platform/v4/msg/pageData', this.pageForm)
if (resp.code === 0) {
this.messages = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
} else {
this.$message.error(resp.msg)
}
} catch (error) {
this.$message.error('加载消息失败')
} finally {
this.loading = false
}
},
async viewMessage(message) {
this.$axios.post(`/platform/v4/msg/detail/` + message.id).then(async res => {
if (res.code === 0) {
this.currentMessage = res.data
this.detailVisible = true
// 如果是未读消息,标记为已读
if (!message.isRead) {
await this.markAsRead(message.id)
this.loadStats()
this.loadMessages()
}
}
})
},
async markAsRead(messageId) {
try {
await this.$axios.post(`/platform/v4/msg/read/` + messageId)
} catch (error) {
console.error('标记已读失败:', error)
}
},
async markAllAsRead() {
this.$confirm('确定要将所有未读消息标记为已读吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
this.loading = true
try {
const resp = await this.$axios.post('/platform/v4/msg/read/all')
if (resp.code === 0) {
this.$message.success('操作成功')
this.loadMessages()
this.loadStats()
} else {
this.$message.error(resp.msg)
}
} catch (error) {
this.$message.error('操作失败')
} finally {
this.loading = false
}
})
},
handleTabClick(tab) {
this.pageForm.pageNumber = 1
this.loadMessages()
},
handlePageChange(pageNumber) {
this.pageForm.pageNumber = pageNumber
this.loadMessages()
},
getMessageTypeName(type) {
return type === 1 ? '系统公告' : '消息通知'
},
getContentPreview(content) {
if (!content) return ''
// 移除HTML标签
const text = content.replace(/<[^>]*>/g, '')
return text.length > 50 ? text.substring(0, 50) + '...' : text
},
formatTime(timestamp) {
return this.$moment(timestamp).format('YYYY-MM-DD HH:mm')
}
},
created() {
this.loadStats()
this.loadMessages()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,448 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<style>
.task-todo-center {
padding: 20px;
}
/* 统计卡片样式 */
.statistics-section {
margin-bottom: 20px;
}
.stat-card {
border: none;
height: 100%;
}
.stat-card .el-card__body {
padding: 20px;
}
.stat-content {
display: flex;
align-items: center;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
font-size: 28px;
}
.todo-icon {
background-color: rgba(64, 158, 255, 0.1);
color: #409eff;
}
.done-icon {
background-color: rgba(103, 194, 58, 0.1);
color: #67c23a;
}
.started-icon {
background-color: rgba(230, 162, 60, 0.1);
color: #e6a23c;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
/* 搜索区域样式 */
.filter-section {
margin-bottom: 20px;
}
.filter-section .el-card__body {
padding: 15px 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
}
.search-form .el-form-item {
margin-bottom: 0;
margin-right: 15px;
}
/* 任务列表区域样式 */
.task-section .el-card__header {
padding: 0;
border-bottom: none;
}
/* 优化后的 tabs 样式 */
.task-header .el-tabs__header {
margin: 0;
padding: 0 20px;
background-color: #fff;
}
.task-header .el-tabs__nav-wrap::after {
height: 1px;
background-color: #ebeef5;
}
.task-header .el-tabs__item {
height: 50px;
line-height: 50px;
font-size: 15px;
color: #606266;
padding: 0 20px;
transition: all 0.3s;
}
.task-header .el-tabs__item.is-active {
color: #409eff;
font-weight: 500;
}
.task-header .el-tabs__active-bar {
height: 3px;
border-radius: 3px;
}
.task-title {
font-weight: 500;
color: #303133;
}
/* 表格行样式 */
.el-table .hover-row {
background-color: #f5f7fa;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
padding: 10px 0;
}
</style>
<div id="app" v-cloak>
<div class="task-todo-center">
<!-- 统计数据区域 -->
<el-row :gutter="20" class="statistics-section">
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon todo-icon">
<i class="el-icon-s-claim"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{todoCount}}</div>
<div class="stat-label">待办任务</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon done-icon">
<i class="el-icon-finished"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{doneCount}}</div>
<div class="stat-label">已办任务</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon started-icon">
<i class="el-icon-s-promotion"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{startedCount}}</div>
<div class="stat-label">我发起的</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 搜索筛选区域 -->
<el-card shadow="never" class="filter-section">
<el-form :inline="true" :model="pageForm" class="search-form">
<el-form-item>
<el-input v-model="pageForm.searchKeyword" placeholder="请输入关键词搜索"
prefix-icon="el-icon-search"
clearable></el-input>
</el-form-item>
<el-form-item>
<el-select v-model="pageForm.category" placeholder="所有流程分类" clearable>
<el-option v-for="type in categoryOptions" :key="type.id" :label="type.name"
:value="type.id"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 任务列表区域 -->
<el-card shadow="never" class="task-section">
<div slot="header" class="task-header">
<el-tabs v-model="activeTab" @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-tabs>
</div>
<!-- 表格展示 -->
<el-table v-loading="loading" :data="tasks" style="width: 100%" :key="activeTab"
:header-cell-style="{backgroundColor: '#f5f7fa'}">
<el-table-column prop="processInstanceName" label="流程名称" min-width="180" show-overflow-tooltip>
<template slot-scope="scope">
<span class="task-title">{{scope.row.variable?.instanceName}}</span>
</template>
</el-table-column>
<el-table-column prop="taskName" label="任务节点" min-width="150"
show-overflow-tooltip></el-table-column>
<el-table-column prop="categoryName" label="流程分类" min-width="150" show-overflow-tooltip>
<template slot-scope="{row}">{{categoryOptions.find(item => item.id === row.category)?.name}}
</template>
</el-table-column>
<el-table-column prop="initiatorName" label="申请人" min-width="120">
<template slot-scope="{row}">{{row.variable.initiatorName}}</template>
</el-table-column>
<!-- <el-table-column label="时间" min-width="180">-->
<!-- <template slot-scope="scope">-->
<!-- <div v-if="activeTab === 'todo'">-->
<!-- <i class="el-icon-time"></i>-->
<!-- {{scope.row.createdAt}}-->
<!-- </div>-->
<!-- <div v-else-if="activeTab === 'done'">-->
<!-- <i class="el-icon-check"></i>-->
<!-- {{scope.row.finishTime}}-->
<!-- </div>-->
<!-- <div v-else-if="activeTab === 'started'">-->
<!-- <i class="el-icon-s-promotion"></i>-->
<!-- {{scope.row.createdAt}}-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column v-if="activeTab === 'started'" label="状态" width="100">
<template slot-scope="{row}">{{processStatusMap[row.state]?.text}}</template>
</el-table-column>
<el-table-column label="操作" width="100px" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="openView(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- 空状态展示 -->
<el-empty v-if="tasks.length === 0" :description="getEmptyText()"></el-empty>
<!-- 分页 -->
<div class="pagination-container" v-if="tasks.length > 0">
<el-pagination
background
@current-change="handleCurrentChange"
:current-page="pageForm.pageNumber"
:page-size="pageForm.pageSize"
layout="prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</div>
</el-card>
</div>
</div>
<script>
new Vue({
el: "#app",
data() {
return {
// 统计数据
todoCount: 0,
doneCount: 0,
startedCount: 0,
// 查询表单
pageForm: {
pageNumber: 1,
pageSize: 10,
todoCount: 0,
keyword: "",
category: ""
},
// 流程类型选项
categoryOptions: [],
// 任务列表
tasks: [],
loading: false,
// 标签页
activeTab: "todo",
// 当前任务
currentTask: null,
dialogVisible: false,
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"}
},
// 任务状态枚举
taskStateEnum: {
DOING: 10,
FINISHED: 20,
WITHDRAW: 30,
INTERRUPT: 40,
PENDING: 50,
ABANDON: 99
}
}
},
created() {
this.initData()
},
methods: {
// 初始化数据
async initData() {
await Promise.all([this.getStatistics(), this.listCategory(), this.getTasks()])
},
// 获取统计数据
async getStatistics() {
try {
const res = await $.post("/flow/todoCenter/statistics")
if (res.code === 0) {
const {todoCount, doneCount, startedCount} = res.data
this.todoCount = todoCount
this.doneCount = doneCount
this.startedCount = startedCount
}
} catch (error) {
this.$message.error("获取统计数据失败")
console.error("获取统计数据失败:", error)
}
},
// 获取流程类型
async listCategory() {
this.$axios.post("/flow/category/list").then((res) => {
if (res.code === 0) {
this.categoryOptions = res.data
}
})
},
// 获取任务列表
async getTasks() {
this.loading = true
try {
const res = await $.post("/flow/todoCenter/" + this.activeTab, this.pageForm)
if (res.code === 0) {
this.tasks = res.data.list
this.pageForm.totalCount = res.data.totalCount
this.dialogVisible = false
}
} catch (error) {
this.$message.error("获取任务列表失败")
console.error("获取任务列表失败:", error)
} finally {
this.loading = false
}
},
// 处理标签页点击
handleTabClick(tab) {
this.activeTab = tab.name
this.pageForm.pageNumber = 1
this.getTasks()
},
// 搜索
search() {
this.pageForm.pageNumber = 1
this.getTasks()
this.getStatistics()
},
// 重置搜索
reset() {
this.pageForm.searchKeyword = ""
this.pageForm.category = ""
this.search()
},
// 处理页码变化
handleCurrentChange(val) {
this.pageForm.pageNumber = val
this.getTasks()
},
// 处理任务
async openView(task) {
console.log(task)
const {taskId, taskKey, taskState, instanceId, businessNo, formKey} = task
if (!formKey) {
this.$message.warning("当前流程没有配置地址")
return
}
window.open(formKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey)
},
// 获取空状态文本
getEmptyText() {
switch (this.activeTab) {
case "todo":
return "您当前没有需要办理的任务,辛苦了"
case "done":
return "您当前没有已办理的任务记录"
case "started":
return "您当前没有发起的流程"
default:
return "暂无数据"
}
}
}
})
</script>
<!--#
}
#-->
@@ -1,93 +0,0 @@
const activity = {
template: /*language=HTML*/ `
<div class="home-activity">
<el-card shadow="never">
<div class="card-title" slot="header">最新活动</div>
<div v-if="activityOptions && activityOptions.length > 0" v-for="item in activityOptions"
:key="item.id">
<div style="width: 100%">
<div style="background-color: #fff; position: relative; margin-bottom: 10px">
<div style="display: flex; justify-content: center; align-items: center;border-bottom: 1px solid var(--border-color-lighter);padding-bottom: 8px">
<div
style="position: relative;flex-shrink: 1;width: 150px; height: 90px;margin-right: 10px; ">
<el-image :src="item.cover" alt="" fit="cover"
style="width: 100%; height: 100%; border-radius: 8px;">
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
<el-tag class="act-item-tag" type="danger" size="mini"
v-if="$moment().unix() > $moment(item.endDate).unix()">
已结束
</el-tag>
<el-tag class="act-item-tag" type="success" size="mini"
v-else-if="$moment().unix() < $moment(item.endDate).unix()">
进行中
</el-tag>
</div>
<div style="flex: 1;height: 90px;display: flex;flex-direction: column;justify-content: space-between;">
<div style="color: #111">
<i class="el-icon-star-on" style="color: red; font-size: 17px"></i>{{item.name}}
</div>
<div style="font-size: 12px; color: #999">
开始时间{{$moment(item.startDate).format('MM-DD HH:mm')}}
</div>
<div style="font-size: 12px; color: #999">
结束时间{{$moment(item.endDate).format('MM-DD HH:mm')}}
</div>
</div>
<div @click="enterActivity(item)">
<el-button size="mini" type="primary">查看<i class="el-icon-s-promotion"></i>
</el-button>
</div>
</div>
</div>
</div>
</div>
<el-empty v-if="activityOptions.length === 0" description="暂无活动"></el-empty>
</el-card>
</div>
`,
data() {
return {
activityOptions: []
}
},
methods: {
listActivity() {
this.$axios.post("/platform/home/listHomeActivity").then((resp) => {
if (resp.code === 0) {
this.activityOptions = resp.data
}
})
},
enterActivity(item) {
this.$store.dispatch("pjaxRoute", item.url)
}
},
created() {
this.listActivity()
},
style: /*language=CSS*/ `
.home-activity {
min-width: 500px;
}
.home-activity .el-card {
width: 100%;
overflow-y: auto;
}
.home-activity .el-empty {
padding: 40px 0 !important;
}
.home-activity .act-item-tag{
position: absolute;
right: 0;
bottom: 0;
z-index: 1;
}
`
}
@@ -1,76 +0,0 @@
const appModule = {
template: /*language=HTML*/ `
<div class="home-app-module">
<el-card shadow="never">
<div class="card-title" slot="header">平台入口</div>
<div style="display: flex;gap: 20px">
<template v-for="item in entranceModules">
<div :key="item.id" class="v-item-handle" @click="enterEntranceModules(item)" >
<el-image :src="item.icon" style="height: 50px;width: 50px;border-radius: 50%"></el-image>
<span class="v-item-handle-label">
{{ item.name }}
</span>
</div>
</template>
</div>
</el-card>
</div>
`,
store,
data() {
return {}
},
computed: {
entranceModules() {
return this.$store.getters.pcModules
}
},
methods: {
enterEntranceModules(item) {
this.$store.commit("setActiveMenuModuleId", item.id)
}
},
created() {},
style: /*language=CSS*/ `
.v-item-handle {
width: 110px;
height: 110px;
border: 1px solid #f3efef;
/*/ / background-color: rgb(248, 248, 248);*/
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
transition: all 500ms;
color: #0779e4;
cursor: pointer;
border-radius: 5px;
}
.v-item-handle-label {
font-size: 14px;
margin-top: 5px;
}
.v-item-handle .el-image {
transition: width 0.3s ease,
height 0.3s ease;
transform-origin: center; /* 缩放的中心点 */
}
.v-item-handle:hover .el-image {
width: 60px !important;
height: 60px !important;
}
.v-item-handle:hover .fa {
color: white !important;
}
.v-item-handle span {
transition: all 500ms;
color: #000;
font-size: 16px;
}
`
}
@@ -1,127 +0,0 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.el-card {
border-radius: 8px;
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #f5f7fa;
color: #909399;
}
.el-icon-picture-outline {
font-size: 30px;
}
.el-card {
height: 100%;
}
.layout-grid-container {
display: grid;
grid-template-columns: repeat(3, minmax(100px, 1fr));
grid-template-rows: 200px 200px auto;
gap: 10px;
width: calc(100% - 0px);
height: calc(100vh - 126px);
margin: 0 auto;
overflow: hidden;
box-sizing: border-box;
}
.layout-grid-container .grid-item {
color: white;
border-radius: 8px;
}
.appModule-grid {
grid-column: 1 / 3; /* 元素1占据第1和第2列 */
grid-row: 1 / 2; /* 元素1占据第1行 */
}
.activity-grid {
grid-column: 3 / 4; /* 元素2占据第3列 */
grid-row: 1 / 3; /* 元素2跨越第1和第2行 */
}
.quickEntry-grid {
grid-column: 1 / 3; /* 元素3占据第1和第2列 */
grid-row: 2 / 3; /* 元素3占据第2行 */
}
.news-grid,
.todo-grid,
.user-grid {
grid-column: span 1; /* 每个元素占据1列 */
grid-row: 3 / 4; /* 所有元素都在第3行 */
}
</style>
<div id="app" v-cloak style="height: 100%">
<div class="layout-grid-container">
<app-module class="appModule-grid grid-item"></app-module>
<quick-entry class="quickEntry-grid grid-item"></quick-entry>
<activity class="activity-grid grid-item"></activity>
<news class="news-grid grid-item"></news>
<todo class="todo-grid grid-item"></todo>
<user-info class="user-grid grid-item"></user-info>
</div>
</div>
<script>
<!--#include("userInfo.js"){}#-->
<!--#include("todo.js"){}#-->
<!--#include("quickEntry.js"){}#-->
<!--#include("activity.js"){}#-->
<!--#include("appModule.js"){}#-->
<!--#include("news.js"){}#-->
new Vue({
el: "#app",
components: {
"icon-selector": httpVueLoader("/components/plugins/sysIconSelector/index.vue?v=" + new Date().getTime()),
"user-info": userInfo,
todo,
"quick-entry": quickEntry,
activity,
"app-module": appModule,
news: news
},
store,
data: function () {
return {
quickEntries: [],
activityOptions: [],
processInstanceOptions: []
}
},
methods: {
listQuickEntry() {
this.$axios.post("/platform/home/listQuickEntry").then((res) => {
if (res.code === 0) {
this.quickEntries = res.data
}
})
},
listActivity() {
this.$axios.post("/platform/h5/listHomeActivity").then((resp) => {
if (resp.code === 0) {
this.activityOptions = resp.data
}
})
},
enterActivity(item) {
this.$store.dispatch("pjaxRoute", item.url)
}
},
created() {
this.listQuickEntry()
this.listActivity()
}
})
</script>
<!--# } #-->
@@ -1,81 +0,0 @@
const news = {
template: /*language=HTML*/ `
<div class="home-news">
<el-tabs v-model="activeName">
<el-tab-pane v-for="(item,index) in newsData" :key="index" :label="item.label" :name="item.label" v-if="item.value.length > 0">
<div v-for="(n,i) in item.value" :key="i" @click="openUrl(n.href)">
<span v-if="n.time" class="news_time">{{ n.time }}</span>
<span class="news_text">{{ n.text }}</span>
</div>
</el-tab-pane>
</el-tabs>
</div>
`,
data() {
return {
newsData: [],
activeName: 0
}
},
methods: {
async getNews() {
const resp = await this.$axios.post("/platform/home/getNews")
if (resp.data.length > 0) {
this.activeName = resp.data[0].label
}
this.newsData = resp.data
},
openUrl(href) {
const url = this.isHttpOrHttps(href) ? href : "https://gonghui.njupt.edu.cn/" + href
window.open(url)
},
isHttpOrHttps(url) {
return /^(http:\/\/|https:\/\/)/i.test(url)
}
},
async created() {
await this.getNews()
},
style: /*language=CSS*/ `
.home-news {
background: #ffffff;
padding: 15px;
border-radius: 8px;
overflow-y: auto;
}
.home-news .el-tabs {
height: 100%;
}
.home-news .el-tabs__content {
height: calc(100% - 55px);
overflow-y: auto;
}
.home-news .el-tab-pane {
line-height: 38px;
color: black;
}
.home-news .el-tab-pane .news_time {
color: white;
padding: 4px 10px;
border-radius: 10px;
background: #1867b0;
}
.home-news .el-tab-pane div {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
height: 50px;
line-height: 50px;
}
.home-news .el-tab-pane .news_text:hover {
color: #1867b0;
}
`
}
@@ -1,116 +0,0 @@
const quickEntry = {
template: /*language=HTML*/ `
<div class="home-quick-entry">
<el-card shadow="never">
<div class="card-title" slot="header">快速入口</div>
<div class="quickEntries">
<div v-for="item in quickEntries" :key="item.id" @click="$store.dispatch('pjaxRoute',item.href)" class="quickEntryItem">
<svg-icon :name="item.icon" :size="40" style="color: var(--color-primary)"></svg-icon>
<span>{{item.name}}</span>
</div>
</div>
</el-card>
</div>
`,
data() {
return {
quickEntries: []
}
},
methods: {
listQuickEntry() {
this.$axios.post("/platform/home/listQuickEntry", { platform: "PC" }).then((res) => {
if (res.code === 0) {
this.quickEntries = res.data
}
})
}
},
created() {
this.listQuickEntry()
},
style: /*language=CSS*/ `
.home-quick-entry .el-card {
width: 100%;
}
.home-quick-entry .el-card .el-card__body {
overflow-x: auto;
overflow-y: auto;
height: calc(100% - 53.5px);
}
.home-quick-entry .quickEntries {
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.home-quick-entry .quickEntries .quickEntryItem img {
max-width: 50px;
}
.home-quick-entry .quickEntries .quickEntryItem {
font-size: 16px;
height: 90px;
max-height: 90px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 10px;
transition: transform 0.3s ease-in-out,
box-shadow 0.3s ease-in-out;
background: #f3f3f3;
border-radius: 10px;
}
.home-quick-entry .quickEntries .quickEntryItem span {
margin-top: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.home-quick-entry .quickEntries .quickEntryItem:hover {
cursor: pointer;
background: #f3f3f3;
border-radius: 10px;
border: 1px solid var(--color-primary);
box-shadow:
0 0 12px rgba(33, 112, 80, 0.4), /* 外发光 */
inset 0 0 8px rgba(33, 112, 80, 0.2); /* 内发光 */
background: rgba(33, 112, 80, 0.05); /* 悬浮背景微透主题色 */
}
.home-quick-entry .quickEntries .quickEntryItem:hover .svg-icon {
transform: scale(1.1); /* 图标放大10% */
transition: transform 0.3s ease;
}
.home-quick-entry .quickEntries .quickEntryItem:hover span {
color: var(--color-primary); /* 使用主题色或自定义颜色 */
}
@keyframes shake {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
50% {
transform: translateX(5px);
}
75% {
transform: translateX(-5px);
}
100% {
transform: translateX(0);
}
}
`
}
@@ -1,85 +0,0 @@
const todo = {
template: /*language=HTML*/ `
<div class="home-todo">
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="待处理" name="listTodo"></el-tab-pane>
<el-tab-pane label="已处理" name="listDone"></el-tab-pane>
<el-tab-pane label="我的发起" name="listMyInitiation"></el-tab-pane>
</el-tabs>
<el-table :data="tableData" :border="false" size="small" class="mt10" height="calc(100% - 111px)">
<el-table-column label="序号" type="index"></el-table-column>
<el-table-column label="标题" prop="processInstanceName" show-overflow-tooltip></el-table-column>
<el-table-column label="发起人" prop="processInstanceInitiatorName"></el-table-column>
<el-table-column label="当前环节" prop="processInstanceNodeName" show-overflow-tooltip></el-table-column>
<el-table-column label="操作">
<template slot-scope="{row}">
<el-link type="primary" @click="openView(row)">查看</el-link>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</div>
`,
store,
mixins: [initTableMixins],
data() {
return {
activeName: "listTodo",
pageForm: {
searchName: "",
searchKeyword: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
audit: false
}
}
},
methods: {
handleClick() {
this.pageData()
},
openView(row) {
if (this.activeName === "listMyInitiation") {
this.$store.dispatch("pjaxRoute", row.processInstanceUrl)
} else {
this.$store.dispatch("pjaxRoute", row.taskFormUrl)
}
},
pageData() {
this.$axios.post("/platform/home/" + this.activeName, this.pageForm).then((resp) => {
if (resp.code === 0) {
this.tableData = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
}
})
}
},
created() {
this.handleClick()
},
style: /*language=CSS*/ `
.home-todo {
background: #ffffff;
padding: 15px;
border-radius: 8px;
overflow-y: auto;
color: #000000;
}
.home-todo .el-tabs {
/*height: 100%;*/
}
.home-todo .el-tabs__content {
height: calc(100% - 55px);
overflow-y: auto;
}
/*.home-todo .el-tabs__item{*/
/* color: #000000;*/
/*}*/
`
}
@@ -1,91 +0,0 @@
<!--#include("/platform/zhgh/staffmanage/member/common/info/memberInfo.js"){}#-->
const userInfo = {
template: /*language=HTML*/ `
<div class="home-user-info">
<el-card shadow="never">
<div class="card-title" slot="header">个人信息</div>
<div class="info">
<div>👨🏫我的姓名{{ $store.state.user.username }}</div>
<div>🆔我的工号{{ $store.state.user.loginname }}</div>
<div>我的性别{{ $store.state.user.sex }}</div>
<div>🟢我的单位{{ $store.state.user.unit.name }}</div>
<div>🟤我的工会{{ $store.state.user.union.name }}</div>
<div>📕系统角色
<span style="font-size: 14px;color: #666;" v-for="role in $store.state.user.roles">
{{role.name}}
</span>
</div>
</div>
</el-card>
<el-dialog :visible.sync="signatureDialogVisible" title="我的签字" width="35%">
<div class="flx">
<div v-if="!showSignature" class="p10">
<el-image style="width: 100%;border: 1px dashed #000" :src="userSignatureUrl"
v-if="userSignatureUrl">
<template slot="error">获取签名失败</template>
</el-image>
<el-empty v-else description="暂无签名"></el-empty>
</div>
<div class="flx-center-center" style="flex-direction: column;">
<qrcode :options="{ width: 126 }" :value="signatureAddress" class="signature-qrcode"></qrcode>
<el-button icon="el-icon-refresh" size="small" type="primary" @click="getUserSignature(true)">
刷新
</el-button>
</div>
</div>
<el-alert title="扫描二维码可重新签名,手机上签名成功后可点击刷新查看。" type="success"></el-alert>
</el-dialog>
<el-dialog :visible.sync="memberInfoDialogVisible" title="我的信息">
<member-info :id="$store.state.user.id"></member-info>
</el-dialog>
</div>
`,
store,
data() {
return {
showSignature: false,
userSignatureUrl: null,
signatureDialogVisible: false,
signatureAddress: window.location.origin + "/platform/signature/scanPcCode?origin=user",
memberInfoDialogVisible: false
}
},
components: {
"member-info": MEMBER_INFO
},
mounted() {
},
methods: {
openSignature() {
this.signatureDialogVisible = true
this.getUserSignature()
},
getUserSignature(isRefresh = false) {
this.$axios.post("/platform/signature/get").then((res) => {
if (res.code === 0) {
if (isRefresh) {
this.$message.success("刷新成功")
} else {
if (!res.data.signature) {
this.$message.warning("未获取到签字信息")
}
}
this.userSignatureUrl = res.data.signature
}
})
},
},
style: /*language=CSS*/ `
.home-user-info .el-card {
width: 100%;
overflow: auto;
}
.home-user-info .info {
line-height: 40px;
color: black;
}
`
}
@@ -0,0 +1,34 @@
const subjectBank = {
template: /*language=HTML*/ `
<el-dialog :visible.sync="visible" title="题库" append-to-body width="700px">
</el-dialog>
`,
data() {
return {
visible: false,
category: null,
list: []
}
},
methods: {
onOpen(category) {
debugger
this.visible = true
this.category = category
this.listBank()
},
listBank() {
this.$axios.post('/platform/qsv/activity/listBank', {category: this.category}).then(res => {
if (res.code === 0) {
this.list = res.data
}
})
},
},
style: /*language=CSS*/ `
`
}
@@ -1,6 +1,7 @@
<!--#include('optionImg.js'){}#-->
<!--#include('setting.js'){}#-->
<!--#include('txtImport.js'){}#-->
<!--#include('bank.js'){}#-->
const subjectForm = {
template: /*language=HTML*/ `
@@ -193,6 +194,11 @@ const subjectForm = {
<div style="border-top: 1px solid var(--border-color-lighter);padding: 12px;text-align: right;">
<!-- <el-button @click="visible = false">取消</el-button>-->
<el-button type="primary" icon="el-icon-plus" @click="openBank">
从题库选择
</el-button>
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
从文本导入
</el-button>
@@ -207,12 +213,14 @@ const subjectForm = {
<option-img ref="optionImgRef" @confirm="onOptionImgConfirm"></option-img>
<setting ref="settingRef" @confirm="onSettingConfirm"></setting>
<txt-import ref="txtImportRef" @confirm="onTxtImportConfirm"></txt-import>
<bank ref="bankRef" @confirm="onBankConfirm"></bank>
</div>
`,
components: {
'option-img': optionImg,
'setting': setting,
'txt-import': txtImport
'txt-import': txtImport,
'bank': subjectBank
},
data() {
return {
@@ -375,6 +383,16 @@ const subjectForm = {
this.$message.success('成功导入' + importedQuestions.length + '道题目');
},
// 打开题库
openBank(){
this.$refs.bankRef.onOpen(this.activity.category)
},
// 从题库选择
onBankConfirm(subject){
},
//保存
saveQuestionnaire() {
$
@@ -0,0 +1,72 @@
const basicForm = {
template: /*language=HTML*/ `
<el-dialog :visible.sync="dialogVisible" title="基础设置" width="70%">
<div style="overflow-y: auto">
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="150px">
<el-form-item label="类型" prop="category">
<el-radio-group v-model="formData.category" size="small">
<el-radio v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :label="item.code"
:key="item.code"
border>
{{item.label}}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" maxlength="50"
show-word-limit></el-input>
</el-form-item>
<el-form-item label="说明" prop="description">
<text-editor v-model="formData.description" :height="150"></text-editor>
</el-form-item>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="onSubmit"> </el-button>
</div>
</el-dialog>
`,
dicts: ["ACTIVITY_QSV_CATEGORY", "ACTIVITY_QSV_MODE", "ACTIVITY_QSV_REPEAT_MODE", "ACTIVITY_QSV_SCORE_MODE"],
data() {
return {
dialogVisible: false,
formData: {
category: ''
},
formRules: {
category: [{required: true, message: "请选择类型", trigger: "change"}],
title: [{required: true, message: "请输入标题", trigger: "change"}],
},
}
},
methods: {
onOpen(id) {
this.dialogVisible = true
if (id) {
$.post("/platform/qsv/bank/findOne", {id}).then((res) => {
if (res.code === 0) {
this.formData = res.data
}
})
} else {
this.formData = {}
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
$.post("/platform/qsv/bank/" + (this.formData.id ? "update" : "save"), {qsvBank: JSON.stringify(this.formData)}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.dialogVisible = false
this.$emit("refresh", null)
}
})
}
})
}
}
}
@@ -0,0 +1,100 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="标题">
<el-input placeholder="标题" v-model="pageForm.title" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="管理列表">
<el-button @click="$refs.basicFormRef.onOpen()" size="small" type="primary" class="mr5">新增</el-button>
</table-tool>
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="标题" prop="title" sortable></el-table-column>
<el-table-column label="类型" prop="category" sortable width="200">
<template scope="{row}">
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="450px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
<el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<subject-form ref="subjectFormRef" @refresh="pageData"></subject-form>
</template>
</guava>
<basic-form ref="basicFormRef" @refresh="pageData"></basic-form>
</div>
<script>
<!--#include('basicForm.js'){}#-->
<!--#include('subjectForm.js'){}#-->
new Vue({
el: "#app",
dicts: ["ACTIVITY_QSV_CATEGORY"],
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
"subject-form": subjectForm
},
data() {
return {
}
},
methods: {
// 编辑
openEdit(row) {
this.$refs.basicFormRef.onOpen(row.id)
},
// 题目设置
openSubject(id) {
this.$refs.guava.edit(() => {
this.$refs.subjectFormRef.onOpen(id)
})
},
// 删除
onDelete(id) {
this.$confirm("您确认删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
$.post("/platform/qsv/activity/delete", {id}).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
}
})
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,551 @@
<!--#include('../activity/optionImg.js'){}#-->
<!--#include('../activity/setting.js'){}#-->
<!--#include('../activity/txtImport.js'){}#-->
const subjectForm = {
template: /*language=HTML*/ `
<div class="subject-form-dialog">
<el-form ref="form" :model="formData" :rules="formRules" label-width="120px">
<div style="min-height:50vh;overflow-y: auto">
<draggable v-model="subjects" handle=".drag-handler">
<transition-group>
<div v-for="(subject, subjectIndex) in subjects" :key="subject.id" class="subject-item">
<div class="subject-header">
<div style="display: flex; align-items: center;">
<i class="el-icon-rank drag-handler"></i>
<span style="font-weight: bold; margin-right: 10px;"> {{subjectIndex + 1}} </span>
<el-input
v-model="subject.title"
placeholder="请输入题目"
style="flex: 1">
</el-input>
</div>
<div class="subject-meta">
<div>
题目类型:
<el-select
v-model="subject.type"
@change="subjectTypeChange(subject, subjectIndex)"
placeholder="请选择题目类型"
style="width: 200px;">
<el-option label="单选题" value="radio">
<i class="el-icon-circle-check subject-type-icon"></i>
</el-option>
<el-option label="多选题" value="checkbox">
<i class="el-icon-check subject-type-icon"></i>
</el-option>
<el-option label="填空题" value="text"
v-if="activity.category!=='QUIZ'">
<i class="el-icon-edit subject-type-icon"></i>
</el-option>
<el-option label="判断题" value="judge">
<i class="el-icon-right subject-type-icon"></i>
</el-option>
</el-select>
</div>
<div v-if="subject.type === 'checkbox'">
最大选择数:
<el-input-number v-model="subject.maxMulti"
placeholder="最多可选数量"></el-input-number>
</div>
<!-- <el-input-->
<!-- v-model="subject.hint"-->
<!-- placeholder="题目提示信息"-->
<!-- style="width: 200px;">-->
<!-- <template slot="prepend">提示</template>-->
<!-- </el-input>-->
<div v-if="activity.category==='QUIZ'">
题目分数:
<el-input-number
v-model="subject.score"
:min="0"
:max="100"
placeholder="分数">
<template slot="prepend">分数</template>
</el-input-number>
</div>
<div v-if="activity.category==='QUIZ' && activity.mode==='SCHEDULED'">
显示日期:
<el-date-picker
v-model="subject.displayDate"
type="date"
placeholder="显示日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd">
</el-date-picker>
</div>
</div>
</div>
<template>
<draggable v-model="subject.options" handle=".option-drag-handle"
v-if="subject.type !== 'text'">
<transition-group>
<div v-for="(option, optionIndex) in subject.options"
:key="option.id"
class="option-item">
<i class="el-icon-rank option-drag-handle"></i>
<div class="option-content">
<el-input
v-model="option.text"
:placeholder="'选项'+optionIndex + 1">
<template slot="prepend">选项{{optionIndex + 1}}</template>
</el-input>
<!-- <el-input-->
<!-- v-model="option.hint"-->
<!-- placeholder="选项提示"-->
<!-- style="margin-top: 5px;">-->
<!-- </el-input>-->
<div v-if="option.imageUrl" style="margin-top: 10px;">
<img :src="option.imageUrl" class="image-preview" alt="">
</div>
</div>
<div class="option-tools">
<template v-if="activity.category!=='QUIZ'">
<div class="option-img-box" v-if="option.imgUrl">
<i class="el-icon-remove"
@click="removeOptionImg(subjectIndex,optionIndex)"></i>
<img :src="option.imgUrl" alt=""
@click="openOptionImg(subjectIndex,optionIndex,option)">
</div>
<i v-if="!option.imgUrl" class="el-icon-picture-outline"
style="font-size: 40px;cursor: pointer;" title="上传图片"
@click="openOptionImg(subjectIndex,optionIndex,option)"></i>
</template>
<el-switch
v-if="activity.category==='QUIZ'"
v-model="option.isCorrect"
active-text="正确答案">
</el-switch>
<el-button
type="primary"
icon="el-icon-setting"
size="mini"
@click="openSetting(subjectIndex,optionIndex,option)">
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="removeOption(subject, optionIndex)">
</el-button>
</div>
</div>
</transition-group>
</draggable>
<div class="subject-toolbar">
<div>
<el-button
v-if="subject.type !== 'text'"
type="primary"
icon="el-icon-plus"
size="small"
@click="addOption(subject)">
添加选项
</el-button>
</div>
<div>
<el-button
type="danger"
icon="el-icon-delete"
size="small"
@click="removeSubject(subjectIndex)">
删除题目
</el-button>
</div>
<!-- <div>-->
<!-- <el-switch-->
<!-- v-model="subject.required"-->
<!-- active-text="必答题">-->
<!-- </el-switch>-->
<!-- <el-switch-->
<!-- v-model="subject.showDate"-->
<!-- class="date-visible"-->
<!-- active-text="显示日期">-->
<!-- </el-switch>-->
<!-- </div>-->
</div>
</template>
<div class="meta-info">
<!-- 创建时间{{ formatDate(subject.createTime) }}-->
<!-- <el-button-->
<!-- type="danger"-->
<!-- icon="el-icon-delete"-->
<!-- size="mini"-->
<!-- style="float: right;"-->
<!-- @click="removeSubject(index)">-->
<!-- 删除题目-->
<!-- </el-button>-->
</div>
</div>
</transition-group>
</draggable>
<el-empty description="描述文字" v-if="subjects.length===0"></el-empty>
</div>
</el-form>
<div style="border-top: 1px solid var(--border-color-lighter);padding: 12px;text-align: right;">
<!-- <el-button @click="visible = false">取消</el-button>-->
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
从文本导入
</el-button>
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
添加题目
</el-button>
<el-button type="primary" icon="el-icon-check" @click="saveQuestionnaire">
保存题目
</el-button>
</div>
<option-img ref="optionImgRef" @confirm="onOptionImgConfirm"></option-img>
<setting ref="settingRef" @confirm="onSettingConfirm"></setting>
<txt-import ref="txtImportRef" @confirm="onTxtImportConfirm"></txt-import>
</div>
`,
components: {
'option-img': optionImg,
'setting': setting,
'txt-import': txtImport
},
data() {
return {
id: null,
visible: false,
formData: {},
subjects: [],
previewDialogVisible: false,
activity: {},
formRules: {}
}
},
methods: {
onOpen(id) {
this.visible = true
if (id) {
this.id = id
$.post("/platform/qsv/bank/findOne", {id}).then((res) => {
if (res.code === 0) {
this.activity = res.data
this.subjects = res.data.subjects || []
}
})
}
},
//生成随机id
generateId() {
return Date.now() + Math.random().toString(36).substr(2, 9)
},
//添加题目
addSubject() {
this.subjects.push({
id: this.generateId(),
title: "",
hint: "",
type: "radio",
score: 0,
displayDate: "",
options: [
{
id: this.generateId(),
text: "选项1",
hint: "",
imgUrl: null,
isCorrect: false
},
{
id: this.generateId(),
text: "选项2",
hint: "",
imgUrl: null,
isCorrect: false
}
],
})
},
//题目类型切换
subjectTypeChange(subject, subjectIndex) {
if (subject.type === 'text') {
subject.options = []
}
},
//删除题目
removeSubject(index) {
this.$confirm("确认删除该题目?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.subjects.splice(index, 1)
this.$message.success("删除成功")
})
.catch(() => {
})
},
//添加选项
addOption(subject) {
subject.options.push({
id: this.generateId(),
text: "选项" + subject.options.length + 1,
hint: "",
imgUrl: null,
isCorrect: false
})
},
//删除选项
removeOption(subject, optionIndex) {
subject.options.splice(optionIndex, 1)
},
//打开选项上传图片
openOptionImg(subjectIndex, optionIndex, option) {
this.$refs.optionImgRef.onOpen(option.imgUrl, {
subjectIndex,
optionIndex
})
},
//选项图片上传成功回调
onOptionImgConfirm({img, ext}) {
this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "imgUrl", img)
},
//删除选项图片
removeOptionImg(subjectIndex, optionIndex) {
this.$set(this.subjects[subjectIndex].options[optionIndex], "imgUrl", null)
console.log(this.subjects[subjectIndex])
},
//打开选项设置
openSetting(subjectIndex, optionIndex, option) {
this.$refs.settingRef.onOpen(option, {
subjectIndex,
optionIndex
})
},
//选项设置确认
onSettingConfirm({option, ext}) {
this.subjects[ext.subjectIndex].options[ext.optionIndex] = option
console.log(this.subjects[ext.subjectIndex].options[ext.optionIndex])
// this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "isCorrect", option.isCorrect)
// this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "hint", option.hint)
},
//从文本导入
openTxtImport() {
this.$refs.txtImportRef.visible = true;
},
//处理导入的题目
onTxtImportConfirm(importedQuestions) {
if (!importedQuestions || importedQuestions.length === 0) {
return;
}
// 计算新题目的起始序号
const startSortNum = this.subjects.length + 1;
// 更新导入题目的序号
importedQuestions.forEach((question, index) => {
question.sortNum = startSortNum + index;
});
// 将导入的题目添加到现有题目列表
this.subjects.push(...importedQuestions);
this.$message.success('成功导入' + importedQuestions.length + '道题目');
},
//保存
saveQuestionnaire() {
this.$axios.post("/platform/qsv/bank/saveSubjects", {
id: this.activity.id,
subjects: JSON.stringify(this.subjects)
}).then((res) => {
if (res.code === 0) {
this.visible = false
this.$message.success(res.msg)
this.$emit("refresh", null)
}
})
}
},
style: /*language=CSS*/ `
/*.subject-form-dialog,.subject-form-dialog .el-dialog__body {*/
/* background: rgb(248, 249, 250);*/
/*}*/
::v-deep .questionnaire-editor {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
::v-deep .editor-header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
::v-deep .subject-item {
background: white;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border: 1px dashed #d9d9d9;
}
::v-deep .subject-item:hover {
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1);
/*transform: translateY(-1px);*/
}
::v-deep .subject-header {
border-bottom: 1px solid #eee;
padding-bottom: 15px;
margin-bottom: 15px;
}
::v-deep .subject-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
}
::v-deep .toolbar {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 1000;
}
::v-deep .toolbar-btn {
margin: 5px 0;
width: 100%;
}
::v-deep .option-item {
background: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
}
::v-deep .option-drag-handle {
cursor: move;
margin-right: 10px;
color: #909399;
}
::v-deep .option-content {
flex-grow: 1;
margin-right: 10px;
}
::v-deep .option-tools {
display: flex;
align-items: center;
gap: 10px;
}
::v-deep .preview-dialog {
max-width: 800px;
margin: 0 auto;
}
::v-deep .date-visible {
margin-left: 20px;
}
::v-deep .subject-meta {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 15px;
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
}
::v-deep .image-preview {
max-width: 200px;
max-height: 200px;
margin-top: 10px;
border-radius: 4px;
border: 1px solid #dcdfe6;
}
::v-deep .drag-handler {
cursor: move;
color: #909399;
margin-right: 10px;
}
::v-deep .meta-info {
color: #909399;
font-size: 12px;
margin-top: 5px;
}
::v-deep .subject-type-icon {
margin-right: 5px;
}
::v-deep .option-img-box {
position: relative;
width: 40px;
height: 40px;
cursor: pointer;
flex-shrink: 0
}
::v-deep .option-img-box:hover .el-icon-remove {
display: block;
}
::v-deep .option-img-box img {
width: 100%;
height: 100%;
}
::v-deep .option-img-box .el-icon-remove {
position: absolute;
top: -7px;
right: -7px;
color: red;
display: none;
}
::v-deep .option-img-box .el-icon-remove:hover {
transform: scale(1.1);
}
`
}
@@ -42,18 +42,18 @@ layout("/layouts/platform_h5.html"){
<div v-else-if="videoInfo" class="video-player">
<video
ref="videoElement"
class="video-element"
:src="videoInfo.url"
controls
preload="metadata"
@loadedmetadata="onVideoLoaded"
@canplay="onVideoCanPlay"
@play="onVideoPlay"
@pause="onVideoPause"
@timeupdate="onTimeUpdate"
@ended="onVideoEnded"
@error="onVideoError"
ref="videoElement"
class="video-element"
:src="videoSrc"
controls
preload="metadata"
@loadedmetadata="onVideoLoaded"
@canplay="onVideoCanPlay"
@play="onVideoPlay"
@pause="onVideoPause"
@timeupdate="onTimeUpdate"
@ended="onVideoEnded"
@error="onVideoError"
>
您的浏览器不支持视频播放
</video>
@@ -63,227 +63,241 @@ layout("/layouts/platform_h5.html"){
</div>
<!-- 进度保存提示 -->
<van-toast v-model="showProgressToast" message="进度已保存" :duration="1000" />
<van-toast v-model="showProgressToast" message="进度已保存" :duration="1000"/>
<!-- 完成学习弹窗 -->
<van-dialog
v-model="showCompletionDialog"
title="恭喜完成学习!"
message="您已完成本视频的学习,学习记录已保存。"
show-cancel-button
cancel-button-text="继续观看"
confirm-button-text="返回课程"
@confirm="backToCourse"
v-model="showCompletionDialog"
title="恭喜完成学习!"
message="您已完成本视频的学习,学习记录已保存。"
show-cancel-button
cancel-button-text="继续观看"
confirm-button-text="返回课程"
@confirm="backToCourse"
></van-dialog>
</div>
<script>
const vue = new Vue({
el: "#app",
store,
data() {
return {
videoInfo: null,
loading: true,
videoId: null,
courseId: null,
videoTitle: '视频播放',
isPlaying: false,
progressSaveTimer: null,
showProgressToast: false,
showCompletionDialog: false,
savedProgress: 0
}
},
methods: {
pageBack(){
this.historyBack(this.clearProgressSave)
const vue = new Vue({
el: "#app",
store,
data() {
return {
videoInfo: null,
loading: true,
videoId: null,
courseId: null,
videoTitle: '视频播放',
isPlaying: false,
progressSaveTimer: null,
showProgressToast: false,
showCompletionDialog: false,
savedProgress: 0,
progressSet: false // 标记进度是否已设置,避免死循环
}
},
// 加载视频信息
async loadVideoInfo() {
if (!this.videoId) {
this.$toast.fail('缺少视频ID参数');
this.loading = false;
return;
computed: {
videoSrc() {
if (this.videoInfo && this.videoInfo.url) {
const id = this.videoInfo.url.split('id=')[1];
console.log('/platform/sys/file/videoPlay?id=' + id)
return '/platform/sys/file/videoPlay?id=' + id;
}
return null
}
},
try {
const { code, data, msg } = await this.$axios.post('/platform/h5/edu/video/detail', {
videoId: this.videoId
});
methods: {
pageBack() {
this.historyBack(this.clearProgressSave)
},
if (code === 0 && data) {
this.videoInfo = data;
this.$nextTick(() => {
this.loadWatchProgress();
// 加载视频信息
async loadVideoInfo() {
if (!this.videoId) {
this.$toast.fail('缺少视频ID参数');
this.loading = false;
return;
}
try {
const {code, data, msg} = await this.$axios.post('/platform/h5/edu/video/detail', {
videoId: this.videoId
});
} else {
this.$toast.fail(msg || '加载视频信息失败');
if (code === 0 && data) {
this.videoInfo = data;
this.$nextTick(() => {
this.loadWatchProgress();
});
} else {
this.$toast.fail(msg || '加载视频信息失败');
}
} catch (error) {
this.$toast.fail('网络错误');
}
} catch (error) {
this.$toast.fail('网络错误');
}
this.loading = false;
},
this.loading = false;
},
// 视频加载完成
onVideoLoaded() {
console.log('视频元数据加载完成');
},
// 视频加载完成
onVideoLoaded() {
console.log('视频元数据加载完成');
},
// 视频可以播放
onVideoCanPlay() {
console.log('视频可以播放');
// 视频可以播放时,设置观看进度
this.setWatchProgress();
},
// 视频可以播放
onVideoCanPlay() {
console.log('视频可以播放');
// 视频可以播放时,设置观看进度
this.setWatchProgress();
},
// 视频开始播放
onVideoPlay() {
this.isPlaying = true;
this.startProgressSave();
},
// 视频开始播放
onVideoPlay() {
this.isPlaying = true;
this.startProgressSave();
},
// 视频暂停
onVideoPause() {
this.isPlaying = false;
this.saveProgress();
},
// 时间更新
onTimeUpdate() {
console.log('时间更新')
// 可以在这里添加进度更新逻辑
},
// 视频播放结束
onVideoEnded() {
this.isPlaying = false;
this.markVideoCompleted();
},
// 视频加载错误
onVideoError() {
this.$toast.fail('视频加载失败');
},
// 加载观看进度
async loadWatchProgress() {
if (!this.videoId || !this.courseId) return;
try {
const { code, data } = await this.$axios.post('/platform/h5/edu/study/record', {
videoId: this.videoId
});
if (code === 0 && data && data.watchedDuration > 0) {
this.savedProgress = data.watchedDuration;
}
} catch (error) {
console.error('加载观看进度失败:', error);
}
},
// 设置观看进度
setWatchProgress() {
if (this.savedProgress > 0) {
const video = this.$refs.videoElement;
if (video && video.readyState >= 3) {
// 使用setTimeout确保DOM更新完成
setTimeout(() => {
try {
video.currentTime = this.savedProgress;
console.log('设置观看进度成功:', this.savedProgress);
} catch (error) {
console.error('设置观看进度失败:', error);
}
}, 100);
} else {
console.log('视频还未准备好,readyState:', video ? video.readyState : 'video不存在');
}
}
},
// 保存观看进度
async saveProgress() {
const video = this.$refs.videoElement;
if (!video || !this.videoId || !this.courseId) return;
try {
await $.post('/platform/h5/edu/study/progress/save', {
videoId: this.videoId,
courseId: this.courseId,
watchedDuration: Math.floor(video.currentTime)
});
this.showProgressToast = true;
} catch (error) {
console.error('保存进度失败:', error);
}
},
// 开始定时保存进度
startProgressSave() {
this.clearProgressSave();
this.progressSaveTimer = setInterval(() => {
// 视频暂停
onVideoPause() {
this.isPlaying = false;
this.saveProgress();
}, 10000); // 每10秒保存一次
},
},
// 清除定时保存
clearProgressSave() {
if (this.progressSaveTimer) {
clearInterval(this.progressSaveTimer);
this.progressSaveTimer = null;
}
},
// 时间更新
onTimeUpdate() {
console.log('时间更新')
// 可以在这里添加进度更新逻辑
},
// 标记视频完成
async markVideoCompleted() {
if (!this.videoId || !this.courseId) return;
// 视频播放结束
onVideoEnded() {
this.isPlaying = false;
this.markVideoCompleted();
},
try {
const { code } = await $.post('/platform/h5/edu/study/complete', {
videoId: this.videoId,
courseId: this.courseId
});
// 视频加载错误
onVideoError() {
this.$toast.fail('视频加载失败');
},
if (code === 0) {
this.showCompletionDialog = true;
// 加载观看进度
async loadWatchProgress() {
if (!this.videoId || !this.courseId) return;
try {
const {code, data} = await this.$axios.post('/platform/h5/edu/study/record', {
videoId: this.videoId
});
if (code === 0 && data && data.watchedDuration > 0) {
this.savedProgress = data.watchedDuration;
}
} catch (error) {
console.error('加载观看进度失败:', error);
}
},
// 设置观看进度
setWatchProgress() {
if (this.savedProgress > 0 && !this.progressSet) {
const video = this.$refs.videoElement;
if (video && video.readyState >= 3) {
// 使用setTimeout确保DOM更新完成
setTimeout(() => {
try {
this.progressSet = true; // 标记进度已设置,避免重复设置
video.currentTime = this.savedProgress;
console.log('设置观看进度成功:', this.savedProgress);
} catch (error) {
console.error('设置观看进度失败:', error);
}
}, 100);
} else {
console.log('视频还未准备好,readyState:', video ? video.readyState : 'video不存在');
}
}
},
// 保存观看进度
async saveProgress() {
const video = this.$refs.videoElement;
if (!video || !this.videoId || !this.courseId) return;
try {
await $.post('/platform/h5/edu/study/progress/save', {
videoId: this.videoId,
courseId: this.courseId,
watchedDuration: Math.floor(video.currentTime)
});
this.showProgressToast = true;
} catch (error) {
console.error('保存进度失败:', error);
}
},
// 开始定时保存进度
startProgressSave() {
this.clearProgressSave();
this.progressSaveTimer = setInterval(() => {
this.saveProgress();
}, 10000); // 每10秒保存一次
},
// 清除定时保存
clearProgressSave() {
if (this.progressSaveTimer) {
clearInterval(this.progressSaveTimer);
this.progressSaveTimer = null;
}
},
// 标记视频完成
async markVideoCompleted() {
if (!this.videoId || !this.courseId) return;
try {
const {code} = await $.post('/platform/h5/edu/study/complete', {
videoId: this.videoId,
courseId: this.courseId
});
if (code === 0) {
this.showCompletionDialog = true;
}
} catch (error) {
console.error('标记完成失败:', error);
}
},
// 返回课程
backToCourse() {
if (this.courseId) {
} else {
this.historyBack();
}
} catch (error) {
console.error('标记完成失败:', error);
}
},
// 返回课程
backToCourse() {
if (this.courseId) {
mounted() {
// 获取URL参数
const urlParams = new URLSearchParams(window.location.search);
this.videoId = urlParams.get('videoId');
this.courseId = urlParams.get('courseId');
this.videoTitle = decodeURIComponent(urlParams.get('title') || '视频播放');
} else {
this.historyBack();
}
this.loadVideoInfo();
},
beforeDestroy() {
// 页面销毁前保存进度
this.saveProgress();
this.clearProgressSave();
}
},
mounted() {
// 获取URL参数
const urlParams = new URLSearchParams(window.location.search);
this.videoId = urlParams.get('videoId');
this.courseId = urlParams.get('courseId');
this.videoTitle = decodeURIComponent(urlParams.get('title') || '视频播放');
this.loadVideoInfo();
},
beforeDestroy() {
// 页面销毁前保存进度
this.saveProgress();
this.clearProgressSave();
}
});
});
</script>
<!--#
@@ -156,7 +156,7 @@ layout("/layouts/platform_h5.html"){
if (this.timerInterval) {
clearInterval(this.timerInterval)
}
pjaxReplace('platform/h5/qsv')
historyBack()
},
//答题计时器
@@ -27,8 +27,8 @@ const SUGGESTION_BOX_INFO = {
</van-cell>
<van-cell title="办理时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
:value="task.ext.caseFilingResult"></dict-tag>
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" v-if="!task.ext.isFirstTaskNode" class="direction-column-cell">
<div v-html="task.taskFormData.tf_opinion"></div>
@@ -20,7 +20,7 @@ layout("/layouts/platform_h5.html"){
<template v-slot="{index,row}">
<table-column label="申请人">{{row.userName}}</table-column>
<table-column label="申请时间">{{row.submitTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</table-column>
<table-column label="当前节点">{{row.taskName}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
@@ -91,7 +91,7 @@ layout("/layouts/platform_h5.html"){
// 编辑
onEdit(row) {
this.$pjaxReplace("/platform/h5/suggestionBox/write?bizId=" + row.id + "&taskId=" + row.startTaskId)
this.$pjaxReplace("/platform/suggestionBox/write/h5?bizId=" + row.id + "&taskId=" + (row.startTaskId || ''))
},
// 撤回
@@ -73,7 +73,7 @@ layout("/layouts/platform_h5.html"){
this.$axios.post('/platform/suggestionBox/write/save', {data: JSON.stringify(this.formData)}).then(res => {
if (res.code === 0) {
this.$toast.success("保存成功");
pjaxReplace("/platform/h5/suggestionBox/mine")
pjaxReplace("/platform/suggestionBox/mine/h5")
}
})
}).catch(() => {
@@ -96,7 +96,7 @@ layout("/layouts/platform_h5.html"){
this.$axios.post('/platform/suggestionBox/write/submit', {data: JSON.stringify(this.formData)}).then(res => {
if (res.code === 0) {
this.$toast.success("提交成功");
pjaxReplace("/platform/h5/suggestionBox/mine")
pjaxReplace("/platform/suggestionBox/mine/h5")
}
})
}).catch(() => {
@@ -265,6 +265,11 @@ const apps = {
// 显示应用菜单弹框
showAppMenus(app, menus) {
if(app.href && menus.length === 0){
this.$pjaxReplace(app.href)
return;
}
if (!menus || menus.length === 0) {
this.$toast('该应用暂无可用功能')
return
@@ -92,7 +92,7 @@ const todo = {
</div>
<div v-else class="empty-section">
<van-empty :description="getEmptyText()"/>
<van-empty :description="getEmptyText()"></van-empty>
</div>
</van-list>
</van-pull-refresh>