# Conflicts:
#	src/main/resources/application-dev.yaml
This commit is contained in:
2026-06-30 08:44:59 +08:00
118 changed files with 13291 additions and 166 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.budwk</groupId>
<artifactId>zhgh_jshvc</artifactId>
<artifactId>mini</artifactId>
<version>5.6.0-plus</version>
<build>
<resources>
@@ -2,7 +2,6 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.result.Result;
@@ -10,6 +9,7 @@ import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.nutz.integration.jedis.RedisService;
import org.nutz.integration.jedis.pubsub.PubSubService;
import org.nutz.ioc.loader.annotation.Inject;
@@ -22,10 +22,13 @@ import org.nutz.mvc.upload.UploadAdaptor;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import java.util.List;
@IocBean
@At("/platform/sys/h5ScanCodeUploadFile")
@Ok("json:full")
@Api(tags = "h5扫码上传文件")
@Slf4j
public class SysH5ScanCodeUploadFileController {
@Inject
@@ -55,6 +58,8 @@ public class SysH5ScanCodeUploadFileController {
String url = sysFileService.uploadReturnUrl(SysFileEngineTypeEnum.MINIO.getValue(), file);
return Result.success().addData(url);
} catch (Exception e) {
log.error("h5扫码上传文件失败,loginName={}, fileName={}, fileSize={}",
getCurrentLoginName(), getUploadFileName(file), getUploadFileSize(file), e);
return Result.error(e.getMessage());
}
}
@@ -63,26 +68,59 @@ public class SysH5ScanCodeUploadFileController {
@At
@SLog(tag = "文件管理", msg = "同步到PC")
@SaCheckLogin
public Result syncPc(@Param("files") JSONObject[] jsonObject, @Param("timestamp") String timestamp) {
public Result syncPc(@Param("files") String files, @Param("timestamp") String timestamp) {
try {
if(ObjectUtil.isEmpty(jsonObject)){
if(ObjectUtil.isEmpty(files)){
return Result.error("请选择要同步的文件");
}
if(ObjectUtil.isEmpty(timestamp)){
return Result.error("二维码已失效,请刷新电脑端二维码后重试");
}
// 手机端以表单字符串提交附件 JSON,后端显式解析,避免参数自动转换失败导致 500。
List<NutMap> fileList = Json.fromJsonAsList(NutMap.class, files);
if(ObjectUtil.isEmpty(fileList)){
return Result.error("请选择要同步的文件");
}
ScanParams match = new ScanParams().match(RedisConstant.REDIS_KEY_WSROOM + SecurityUtil.getUserLoginname() + ":*");
ScanResult<String> scan = null;
int sendCount = 0;
do {
scan = redisService.scan(scan == null ? ScanParams.SCAN_POINTER_START : scan.getStringCursor(), match);
for (String key : scan.getResult()) {
NutMap data = NutMap.NEW().addv("action", "h5-scan-code-upload-file-" + timestamp).addv("files", jsonObject);
NutMap data = NutMap.NEW().addv("action", "h5-scan-code-upload-file-" + timestamp).addv("files", fileList);
pubSubService.fire(key, Json.toJson(data));
sendCount++;
}
} while (!scan.isCompleteIteration());
if(sendCount == 0){
return Result.error("未找到电脑端上传窗口,请保持电脑端二维码页面打开");
}
return Result.success().addData(null);
} catch (Exception e) {
log.error("h5扫码上传文件同步到PC失败,loginName={}, timestamp={}, filesLength={}",
getCurrentLoginName(), timestamp, ObjectUtil.isEmpty(files) ? 0 : files.length(), e);
return Result.error(e.getMessage());
}
}
private String getCurrentLoginName() {
try {
return SecurityUtil.getUserLoginname();
} catch (Exception e) {
return "unknown";
}
}
private String getUploadFileName(TempFile file) {
return file == null ? "" : file.getSubmittedFileName();
}
private long getUploadFileSize(TempFile file) {
return file == null ? 0 : file.getSize();
}
}
@@ -0,0 +1,85 @@
package com.budwk.app.task.job.staffmanage;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.sys.models.Sys_msg;
import com.budwk.app.sys.services.SysMsgService;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.zhgh.staffmanage.birthday.model.UserBirthdayConfig;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayMsgLogService;
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.Times;
import org.nutz.lang.util.NutMap;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.List;
/**
* 生日祝福定时任务,每天向当天生日的会员发送祝福消息。
*/
@IocBean
public class UserBirthdayJob implements Job {
@Inject
private Dao dao;
@Inject
private SysMsgService sysMsgService;
@Inject
private UserBirthdayMsgLogService userBirthdayMsgLogService;
/**
* 查询当天生日会员并发送生日贺卡链接。
*/
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
Sql sql = Sqls.create("""
SELECT
id,
loginname,
username,
unitName,
unionName,
mobile
FROM
vw_user
WHERE
MONTH(birthday) = MONTH(CURDATE())
AND DAY(birthday) = DAY(CURDATE())
AND member = 1
AND birthday IS NOT NULL
""");
sql.setCallback(Sqls.callback.maps());
dao.execute(sql);
List<NutMap> users = sql.getList(NutMap.class);
if (CollUtil.isEmpty(users)) {
return;
}
UserBirthdayConfig config = dao.fetch(UserBirthdayConfig.class, Cnd.NEW().desc("updatedAt"));
String link = Globals.AppDomain + "/platform/staffManage/birthday/manage/h5?id=" + StrUtil.blankToDefault(config == null ? "" : config.getBirthdayUrl(), "");
List<String> loginNames = users.stream().map(user -> user.getString("loginname")).distinct().toList();
String title = "请点击查收您的生日祝福";
String content = "校工会祝您:生日快乐,幸福安康!";
Sys_msg sysMsg = new Sys_msg();
sysMsg.setTitle(title);
sysMsg.setNote(content);
sysMsg.setUrl(link);
sysMsg.setType("user");
sysMsg.setSendType("hide");
sysMsg.setWechatEnterprise(true);
sysMsg.setSendAt(Times.getTS());
sysMsg.setCreatedBy("system");
sysMsgService.saveMsg(sysMsg, loginNames.toArray(String[]::new), true);
userBirthdayMsgLogService.insertLogs(users, title, content, "", link);
}
}
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
@@ -75,6 +76,12 @@ public class ActivityReimbursementApplyController {
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.activityReimbursement.apply")
@Ok("beetl:/platform/zhghh5/activity/declarereimbursement/reimbursement/apply/index.html")
public void h5Index() {
}
@At
@SaCheckPermission("activityReimbursement.apply")
@Ok("beetl:/platform/zhgh/activity/declarereimbursement/reimbursement/apply/form.html")
@@ -84,7 +91,7 @@ public class ActivityReimbursementApplyController {
@At
@ApiOperation("保存申请")
@SaCheckPermission("member.apply.submit")
@SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR)
@SLog(tag = "活动申报", msg = "保存申请,申请人: ${args[0].username}")
public Result save(@Param("data") ActivityReimbursementInfo activityReimbursementInfo) {
activityReimbursementInfo.setUserId(SecurityUtil.getUserId());
@@ -113,7 +120,7 @@ public class ActivityReimbursementApplyController {
@At
@ApiOperation("提交申请")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("member.apply.submit")
@SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR)
@SLog(tag = "活动申报", msg = "提交申请,申请人: ${args[0].username}")
public Result submit(@Param("data") ActivityReimbursementInfo activityReimbursementInfo) {
activityReimbursementInfo.setUserId(SecurityUtil.getUserId());
@@ -160,7 +167,7 @@ public class ActivityReimbursementApplyController {
@At
@ApiOperation("重新提交申请")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("member.apply.submit")
@SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR)
@SLog(tag = "活动申报", msg = "重新提交申请,申请人: ${args[0].username}")
public Result submitAgain(@Param("data") ActivityReimbursementInfo activityReimbursementInfo, @Param("taskId") Long taskId) {
if (StrUtil.isBlank(activityReimbursementInfo.getDeclareId())) {
@@ -191,7 +198,7 @@ public class ActivityReimbursementApplyController {
@At
@ApiOperation("获取当前用户活动报销")
@SaCheckPermission("activityReimbursement.apply")
@SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR)
public Result getActivityReimbursementByUser(String id, String outlayManageSource, String clubId) {
String activityType = "";
if (StrUtil.isNotBlank(id)) {
@@ -286,7 +293,7 @@ public class ActivityReimbursementApplyController {
@At
@ApiOperation("获取收款人卡号")
@SaCheckPermission("activityReimbursement.apply")
@SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR)
public Result getCardNumberByPayeeId(String username) {
Sql sql = Sqls.create("""
SELECT
@@ -315,7 +322,7 @@ public class ActivityReimbursementApplyController {
@At
@ApiOperation("获取申报的记录")
@SaCheckPermission("activityReimbursement.apply")
@SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR)
public Result getBudgetByYear(String outlayManageSource, String clubId) {
List<NutMap> budgetMoney = activityReimbursementService.getBudgetMoney(outlayManageSource, clubId);
return Result.success(budgetMoney);
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
@@ -13,6 +14,7 @@ import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Exps;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@@ -43,6 +45,12 @@ public class ActivityReimbursementBranchUnionController {
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.activityReimbursement.branchUnion")
@Ok("beetl:/platform/zhghh5/activity/declarereimbursement/reimbursement/branchUnion/index.html")
public void h5Index() {
}
@At
@SaCheckPermission("activityReimbursement.branchUnion")
@Ok("beetl:/platform/zhgh/activity/declarereimbursement/reimbursement/branchunion/form.html")
@@ -52,7 +60,7 @@ public class ActivityReimbursementBranchUnionController {
@At
@ApiOperation("活动报销,分工会审核列表")
@SaCheckPermission("activityReimbursement.branchUnion")
@SaCheckPermission(value = {"activityReimbursement.branchUnion", "h5.activityReimbursement.branchUnion"}, mode = SaMode.OR)
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
SELECT
@@ -111,6 +119,8 @@ public class ActivityReimbursementBranchUnionController {
cnd.andEX("YEAR(info.applyTime)", "=", pageParam.getYear());
if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
// 已审核列表只展示同一流程实例同一节点最新的历史任务,避免退回重提后重复显示。
cnd.and(Exps.inSql("t.id", "SELECT MAX(latestTask.id) FROM wf_process_task latestTask WHERE latestTask.processInstanceId = t.processInstanceId AND latestTask.taskName = t.taskName AND latestTask.taskState IN (" + ProcessTaskStateEnum.FINISHED.getCode() + "," + ProcessTaskStateEnum.WITHDRAW.getCode() + "," + ProcessTaskStateEnum.INTERRUPT.getCode() + ")"));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
@@ -15,6 +16,7 @@ import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Exps;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -43,9 +45,15 @@ public class ActivityReimbursementCashierController {
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.activityReimbursement.cashier")
@Ok("beetl:/platform/zhghh5/activity/declarereimbursement/reimbursement/cashier/index.html")
public void h5Index() {
}
@At
@ApiOperation("活动报销,出纳支付列表")
@SaCheckPermission("activityReimbursement.cashier")
@SaCheckPermission(value = {"activityReimbursement.cashier", "h5.activityReimbursement.cashier"}, mode = SaMode.OR)
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
SELECT
@@ -112,6 +120,8 @@ public class ActivityReimbursementCashierController {
cnd.andEX("YEAR(info.applyTime)", "=", pageParam.getYear());
if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
// 已审核列表只展示同一流程实例同一节点最新的历史任务,避免退回重提后重复显示。
cnd.and(Exps.inSql("t.id", "SELECT MAX(latestTask.id) FROM wf_process_task latestTask WHERE latestTask.processInstanceId = t.processInstanceId AND latestTask.taskName = t.taskName AND latestTask.taskState IN (" + ProcessTaskStateEnum.FINISHED.getCode() + "," + ProcessTaskStateEnum.WITHDRAW.getCode() + "," + ProcessTaskStateEnum.INTERRUPT.getCode() + ")"));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
@@ -13,6 +14,7 @@ import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Exps;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@@ -43,6 +45,12 @@ public class ActivityReimbursementClubPrincipalController {
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.activityReimbursement.clubPrincipal")
@Ok("beetl:/platform/zhghh5/activity/declarereimbursement/reimbursement/clubPrincipal/index.html")
public void h5Index() {
}
@At
@SaCheckPermission("activityReimbursement.clubPrincipal")
@Ok("beetl:/platform/zhgh/activity/declarereimbursement/reimbursement/clubprincipal/form.html")
@@ -52,7 +60,7 @@ public class ActivityReimbursementClubPrincipalController {
@At
@ApiOperation("活动报销,协会负责人审核列表")
@SaCheckPermission("activityReimbursement.clubPrincipal")
@SaCheckPermission(value = {"activityReimbursement.clubPrincipal", "h5.activityReimbursement.clubPrincipal"}, mode = SaMode.OR)
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
SELECT
@@ -111,6 +119,8 @@ public class ActivityReimbursementClubPrincipalController {
cnd.andEX("YEAR(info.applyTime)", "=", pageParam.getYear());
if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
// 已审核列表只展示同一流程实例同一节点最新的历史任务,避免退回重提后重复显示。
cnd.and(Exps.inSql("t.id", "SELECT MAX(latestTask.id) FROM wf_process_task latestTask WHERE latestTask.processInstanceId = t.processInstanceId AND latestTask.taskName = t.taskName AND latestTask.taskState IN (" + ProcessTaskStateEnum.FINISHED.getCode() + "," + ProcessTaskStateEnum.WITHDRAW.getCode() + "," + ProcessTaskStateEnum.INTERRUPT.getCode() + ")"));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
@@ -22,6 +23,7 @@ import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Exps;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -58,9 +60,15 @@ public class ActivityReimbursementDeputySchoolUnionController {
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.activityReimbursement.deputySchoolUnion")
@Ok("beetl:/platform/zhghh5/activity/declarereimbursement/reimbursement/deputySchoolUnion/index.html")
public void h5Index() {
}
@At
@ApiOperation("活动报销,校工会副主席审核")
@SaCheckPermission("activityReimbursement.deputySchoolUnion")
@SaCheckPermission(value = {"activityReimbursement.deputySchoolUnion", "h5.activityReimbursement.deputySchoolUnion"}, mode = SaMode.OR)
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
SELECT
@@ -131,6 +139,8 @@ public class ActivityReimbursementDeputySchoolUnionController {
if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
// 已审核列表只展示同一流程实例同一节点最新的历史任务,避免退回重提后重复显示。
cnd.and(Exps.inSql("t.id", "SELECT MAX(latestTask.id) FROM wf_process_task latestTask WHERE latestTask.processInstanceId = t.processInstanceId AND latestTask.taskName = t.taskName AND latestTask.taskState IN (" + ProcessTaskStateEnum.FINISHED.getCode() + "," + ProcessTaskStateEnum.WITHDRAW.getCode() + "," + ProcessTaskStateEnum.INTERRUPT.getCode() + ")"));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
@@ -140,7 +150,7 @@ public class ActivityReimbursementDeputySchoolUnionController {
} else {
cnd.orderBy("info." + pageParam.getPageOrderName(), PageUtil.getOrder(pageParam.getPageOrderBy()));
}
cnd.groupBy("info.id");
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pagination = activityReimbursementService.listPageMap(pageParam.getPageNumber(), pageParam.getPageSize(), sql);
return Result.success().addData(pagination);
@@ -148,7 +158,7 @@ public class ActivityReimbursementDeputySchoolUnionController {
@At
@ApiOperation("预算/报销汇总表")
@SaCheckPermission("activityReimbursement.deputySchoolUnion")
@SaCheckPermission(value = {"activityReimbursement.deputySchoolUnion", "h5.activityReimbursement.deputySchoolUnion"}, mode = SaMode.OR)
public Result budgetSummaryData(CommonPageParam pageParam) {
if (pageParam.getYear() == null) {
return Result.error("请选择年度");
@@ -168,7 +178,7 @@ public class ActivityReimbursementDeputySchoolUnionController {
@At
@SLog(tag = "报销", msg = "校工会副主席审核了一条记录")
@SaCheckPermission("activityReimbursement.deputySchoolUnion")
@SaCheckPermission(value = {"activityReimbursement.deputySchoolUnion", "h5.activityReimbursement.deputySchoolUnion"}, mode = SaMode.OR)
public Result doReview(@Param("data") String param, String id) {
Dict args = Json.fromJson(Dict.class, param);
if (args.getInt("submitType") == 1) {
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.controlle
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.convert.NumberChineseFormatter;
import cn.hutool.core.convert.NumberWordFormatter;
import cn.hutool.core.date.DateUtil;
@@ -89,9 +90,15 @@ public class ActivityReimbursementMineController {
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.activityReimbursement.mine")
@Ok("beetl:/platform/zhghh5/activity/declarereimbursement/reimbursement/mine/index.html")
public void h5Index() {
}
@At
@ApiOperation("活动报销,我的申请列表")
@SaCheckPermission("activityReimbursement.mine")
@SaCheckPermission(value = {"activityReimbursement.mine", "h5.activityReimbursement.mine"}, mode = SaMode.OR)
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
SELECT
@@ -215,7 +222,7 @@ public class ActivityReimbursementMineController {
@At
@Ok("void")
@SaCheckPermission("activityReimbursement.mine")
@SaCheckPermission(value = {"activityReimbursement.mine", "h5.activityReimbursement.mine"}, mode = SaMode.OR)
@SLog(tag = "活动报销", msg = "导出报销凭证")
public void doExportReimbursement(@Valid String id, HttpServletResponse response) {
HashMap<String, Object> docData = new HashMap<>();
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
@@ -15,6 +16,7 @@ import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Exps;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -45,9 +47,15 @@ public class ActivityReimbursementSchoolFinanceController {
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.activityReimbursement.schoolFinance")
@Ok("beetl:/platform/zhghh5/activity/declarereimbursement/reimbursement/schoolFinance/index.html")
public void h5Index() {
}
@At
@ApiOperation("活动报销,校工会财务审核")
@SaCheckPermission("activityReimbursement.schoolFinance")
@SaCheckPermission(value = {"activityReimbursement.schoolFinance", "h5.activityReimbursement.schoolFinance"}, mode = SaMode.OR)
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
SELECT
@@ -114,6 +122,8 @@ public class ActivityReimbursementSchoolFinanceController {
cnd.andEX("YEAR(info.applyTime)", "=", pageParam.getYear());
if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
// 已审核列表只展示同一流程实例同一节点最新的历史任务,避免退回重提后重复显示。
cnd.and(Exps.inSql("t.id", "SELECT MAX(latestTask.id) FROM wf_process_task latestTask WHERE latestTask.processInstanceId = t.processInstanceId AND latestTask.taskName = t.taskName AND latestTask.taskState IN (" + ProcessTaskStateEnum.FINISHED.getCode() + "," + ProcessTaskStateEnum.WITHDRAW.getCode() + "," + ProcessTaskStateEnum.INTERRUPT.getCode() + ")"));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
@@ -15,6 +16,7 @@ import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Exps;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -46,6 +48,12 @@ public class ActivityReimbursementSchoolPrincipalController {
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.activityReimbursement.schoolPrincipal")
@Ok("beetl:/platform/zhghh5/activity/declarereimbursement/reimbursement/schoolPrincipal/index.html")
public void h5Index() {
}
@At
@SaCheckPermission("activityReimbursement.schoolPrincipal")
@Ok("beetl:/platform/zhgh/activity/declarereimbursement/reimbursement/schoolprincipal/form.html")
@@ -55,7 +63,7 @@ public class ActivityReimbursementSchoolPrincipalController {
@At
@ApiOperation("活动报销,校工会负责人审核列表")
@SaCheckPermission("activityReimbursement.schoolPrincipal")
@SaCheckPermission(value = {"activityReimbursement.schoolPrincipal", "h5.activityReimbursement.schoolPrincipal"}, mode = SaMode.OR)
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
SELECT
@@ -123,6 +131,8 @@ public class ActivityReimbursementSchoolPrincipalController {
cnd.andEX("YEAR(info.applyTime)", "=", pageParam.getYear());
if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
// 已审核列表只展示同一流程实例同一节点最新的历史任务,避免退回重提后重复显示。
cnd.and(Exps.inSql("t.id", "SELECT MAX(latestTask.id) FROM wf_process_task latestTask WHERE latestTask.processInstanceId = t.processInstanceId AND latestTask.taskName = t.taskName AND latestTask.taskState IN (" + ProcessTaskStateEnum.FINISHED.getCode() + "," + ProcessTaskStateEnum.WITHDRAW.getCode() + "," + ProcessTaskStateEnum.INTERRUPT.getCode() + ")"));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
@@ -23,6 +23,7 @@ import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Exps;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
@@ -61,6 +62,12 @@ public class ActivityReimbursementSchoolUnionController {
public void index() {
}
@At("/h5")
@SaCheckPermission("h5.activityReimbursement.schoolUnion")
@Ok("beetl:/platform/zhghh5/activity/declarereimbursement/reimbursement/schoolUnion/index.html")
public void h5Index() {
}
@At
@SaCheckPermission("activityReimbursement.schoolUnion")
@Ok("beetl:/platform/zhgh/activity/declarereimbursement/reimbursement/schoolunion/form.html")
@@ -70,7 +77,7 @@ public class ActivityReimbursementSchoolUnionController {
@At
@ApiOperation("活动报销,校工会审核列表")
@SaCheckPermission("activityReimbursement.schoolUnion")
@SaCheckPermission(value = {"activityReimbursement.schoolUnion", "h5.activityReimbursement.schoolUnion"}, mode = SaMode.OR)
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
SELECT
@@ -139,6 +146,8 @@ public class ActivityReimbursementSchoolUnionController {
if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
// 已审核列表只展示同一流程实例同一节点最新的历史任务,避免退回重提后重复显示。
cnd.and(Exps.inSql("t.id", "SELECT MAX(latestTask.id) FROM wf_process_task latestTask WHERE latestTask.processInstanceId = t.processInstanceId AND latestTask.taskName = t.taskName AND latestTask.taskState IN (" + ProcessTaskStateEnum.FINISHED.getCode() + "," + ProcessTaskStateEnum.WITHDRAW.getCode() + "," + ProcessTaskStateEnum.INTERRUPT.getCode() + ")"));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
@@ -157,7 +166,7 @@ public class ActivityReimbursementSchoolUnionController {
@At
@SLog(tag = "报销", msg = "校工会审核了一条记录")
@SaCheckPermission("activityReimbursement.schoolUnion")
@SaCheckPermission(value = {"activityReimbursement.schoolUnion", "h5.activityReimbursement.schoolUnion"}, mode = SaMode.OR)
public Result doReview(@Param("data") String param, String id) {
activityReimbursementSchoolUnionService.doReview(param, id);
return Result.success();
@@ -1,31 +1,17 @@
package com.budwk.app.zhgh.dayofficework.buildHome.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.buildHome.models.BuildHomeLittleHouse;
import com.budwk.app.zhgh.dayofficework.buildHome.param.BuildHomeLittleHousePageForm;
import com.budwk.app.zhgh.dayofficework.buildHome.service.BuildHomeLittleHouseService;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
@@ -34,12 +20,8 @@ import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
@IocBean
@At("/platform/buildHome/littleHouse")
@@ -48,10 +30,9 @@ import java.util.stream.IntStream;
@Api(value = "建家小家建设情况")
public class BuildHomeLittleHouseController {
@Inject
private Dao dao;
@Inject
private BaseService baseService;
@Inject("refer:buildHomeLittleHouseServiceImpl")
private BuildHomeLittleHouseService buildHomeLittleHouseService;
@Inject
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
@@ -65,41 +46,47 @@ public class BuildHomeLittleHouseController {
@At
@SaCheckPermission("buildHome.littleHouse")
public Result pageData(BuildHomeLittleHousePageForm pageForm) {
Sql sql = Sqls.create("select * from build_home_little_house $condition");
Cnd cnd = Cnd.NEW();
cnd.and("unionId", "=", SecurityUtil.getUnionId());
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
cnd.where().andLike("unitName", pageForm.getSearchKeyword());
}
cnd.asc("sortNum");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
return Result.success(buildHomeLittleHouseService.pageData(pageForm));
}
@At
@SaCheckPermission("buildHome.littleHouse")
public Result currentUserOrg() {
return Result.success(buildHomeLittleHouseService.currentUserOrg());
}
@At
@SaCheckPermission("buildHome.littleHouse")
public Result save(BuildHomeLittleHouse house) {
if (house.getId() == null) {
house.setUnionId(SecurityUtil.getUnionId());
house.setUnionName(SecurityUtil.getUnionId());
}
dao.insertOrUpdate(house);
buildHomeLittleHouseService.saveHouse(house);
return Result.success();
}
@At("/delete/?")
@SaCheckPermission("buildHome.littleHouse")
public Result delete(@Valid Long id) {
dao.delete(BuildHomeLittleHouse.class, id);
buildHomeLittleHouseService.delete(id);
return Result.success();
}
@At("/detail/?")
@SaCheckPermission("buildHome.littleHouse")
public Result detail(@Valid Long id) {
BuildHomeLittleHouse house = dao.fetch(BuildHomeLittleHouse.class, id);
return Result.success(house);
return Result.success(buildHomeLittleHouseService.fetch(id));
}
@At
@SaCheckPermission("buildHome.littleHouse")
public Result saveCoordinate(Long id, Double mapX, Double mapY) {
buildHomeLittleHouseService.saveCoordinate(id, mapX, mapY);
return Result.success();
}
@At
@SaCheckPermission("buildHome.littleHouse")
public Result clearCoordinate(Long id) {
buildHomeLittleHouseService.clearCoordinate(id);
return Result.success();
}
@At("/exportXlsx")
@@ -107,23 +94,7 @@ public class BuildHomeLittleHouseController {
@Ok("void")
public void exportXlsx(HttpServletResponse response) {
try {
String unionId = SecurityUtil.getUnionId();
Cnd cnd = Cnd.where("unionId", "=", unionId);
Sys_union union = dao.fetch(Sys_union.class, unionId);
if (union == null) {
throw new BaseException("未找到ID为 " + unionId + " 的工会信息");
}
// 准备文档数据Map
Map<String, Object> docMap = new HashMap<>();
docMap.put("unionName", union.getName()); // 工会名称
docMap.put("date", DateUtil.today()); // 当前日期
// 查询小家建设数据并设置序号
List<BuildHomeLittleHouse> list = dao.query(BuildHomeLittleHouse.class, cnd);
IntStream.range(0, list.size()).forEach(i -> list.get(i).setIndex(i + 1)); // 为每条数据设置序号
docMap.put("list", list); // 将数据列表放入Map
Map<String, Object> docMap = buildHomeLittleHouseService.buildExportData();
// 配置表格渲染策略
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
@@ -1,10 +1,13 @@
package com.budwk.app.zhgh.dayofficework.buildHome.models;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@Table("build_home_little_house")
@@ -22,6 +25,11 @@ public class BuildHomeLittleHouse extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unitName;
@Column
@Comment("单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unitId;
@Column
@Comment("分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@@ -52,6 +60,21 @@ public class BuildHomeLittleHouse extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 100)
private String openTime;
@Column
@Comment("图片视频资料")
@ColDefine(type = ColType.MYSQL_JSON)
private List<JSONObject> mediaFiles;
@Column
@Comment("地图X坐标百分比")
@ColDefine(type = ColType.FLOAT, width = 8, precision = 4)
private Double mapX;
@Column
@Comment("地图Y坐标百分比")
@ColDefine(type = ColType.FLOAT, width = 8, precision = 4)
private Double mapY;
@Column
@Comment("排序")
@ColDefine(type = ColType.INT)
@@ -0,0 +1,56 @@
package com.budwk.app.zhgh.dayofficework.buildHome.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.buildHome.models.BuildHomeLittleHouse;
import com.budwk.app.zhgh.dayofficework.buildHome.param.BuildHomeLittleHousePageForm;
import java.util.Map;
public interface BuildHomeLittleHouseService extends BaseService<BuildHomeLittleHouse> {
/**
* 分页查询小家建设数据,系统管理员查看全部,其他用户查看本分工会。
*
* @param pageForm 查询分页参数。
* @return 小家建设分页数据。
*/
Pagination pageData(BuildHomeLittleHousePageForm pageForm);
/**
* 获取当前登录用户所属工会和单位,用于新增小家记录时自动带出组织信息。
*
* @return 当前用户组织信息。
*/
Map<String, String> currentUserOrg();
/**
* 保存小家建设记录,新增时写入当前用户组织,编辑时保留原组织归属。
*
* @param house 小家建设记录。
*/
void saveHouse(BuildHomeLittleHouse house);
/**
* 保存小家地图坐标,仅允许系统管理员操作。
*
* @param id 小家记录ID。
* @param mapX 地图X坐标百分比。
* @param mapY 地图Y坐标百分比。
*/
void saveCoordinate(Long id, Double mapX, Double mapY);
/**
* 清空小家地图坐标,仅允许系统管理员操作。
*
* @param id 小家记录ID。
*/
void clearCoordinate(Long id);
/**
* 准备小家建设汇总导出数据。
*
* @return 模板渲染数据。
*/
Map<String, Object> buildExportData();
}
@@ -0,0 +1,167 @@
package com.budwk.app.zhgh.dayofficework.buildHome.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.buildHome.models.BuildHomeLittleHouse;
import com.budwk.app.zhgh.dayofficework.buildHome.param.BuildHomeLittleHousePageForm;
import com.budwk.app.zhgh.dayofficework.buildHome.service.BuildHomeLittleHouseService;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
@IocBean(args = {"refer:dao"})
public class BuildHomeLittleHouseServiceImpl extends BaseServiceImpl<BuildHomeLittleHouse> implements BuildHomeLittleHouseService {
public BuildHomeLittleHouseServiceImpl(Dao dao) {
super(dao);
}
@Override
public Pagination pageData(BuildHomeLittleHousePageForm pageForm) {
Sql sql = Sqls.create("select * from build_home_little_house $condition");
Cnd cnd = Cnd.NEW();
if (!isSysAdmin()) {
cnd.and("unionId", "=", SecurityUtil.getUnionId());
}
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup searchGroup = new SqlExpressionGroup();
searchGroup.orLike("unitName", pageForm.getSearchKeyword());
searchGroup.orLike("unionName", pageForm.getSearchKeyword());
cnd.where().and(searchGroup);
}
cnd.asc("sortNum");
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public Map<String, String> currentUserOrg() {
Map<String, String> currentUserOrg = new HashMap<>();
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
String unionId = user != null && StrUtil.isNotBlank(user.getUnionId()) ? user.getUnionId() : SecurityUtil.getUnionId();
String unionName = user != null ? user.getUnionName() : null;
if (StrUtil.isBlank(unionName) && StrUtil.isNotBlank(unionId)) {
Sys_union union = dao().fetch(Sys_union.class, unionId);
unionName = union != null ? union.getName() : unionId;
}
currentUserOrg.put("unionId", unionId);
currentUserOrg.put("unionName", unionName);
currentUserOrg.put("unitId", user != null ? user.getUnitId() : SecurityUtil.getUnitId());
currentUserOrg.put("unitName", user != null ? user.getUnitName() : null);
return currentUserOrg;
}
@Override
public void saveHouse(BuildHomeLittleHouse house) {
if (house.getId() == null) {
fillCurrentUserOrg(house);
} else {
keepOriginalOrg(house);
}
dao().insertOrUpdate(house);
}
@Override
public void saveCoordinate(Long id, Double mapX, Double mapY) {
if (!isSysAdmin()) {
throw new BaseException("仅系统管理员可以采集坐标");
}
BuildHomeLittleHouse house = fetchRequiredHouse(id);
if (!isValidCoordinate(mapX) || !isValidCoordinate(mapY)) {
throw new BaseException("坐标范围必须在0到100之间");
}
dao().update(BuildHomeLittleHouse.class, Chain.make("mapX", mapX).add("mapY", mapY), Cnd.where("id", "=", house.getId()));
}
@Override
public void clearCoordinate(Long id) {
if (!isSysAdmin()) {
throw new BaseException("仅系统管理员可以清空坐标");
}
BuildHomeLittleHouse house = fetchRequiredHouse(id);
dao().update(BuildHomeLittleHouse.class, Chain.make("mapX", null).add("mapY", null), Cnd.where("id", "=", house.getId()));
}
@Override
public Map<String, Object> buildExportData() {
boolean sysAdmin = isSysAdmin();
String unionId = SecurityUtil.getUnionId();
Cnd cnd = Cnd.NEW();
if (!sysAdmin) {
cnd.and("unionId", "=", unionId);
}
cnd.asc("sortNum");
Sys_union union = dao().fetch(Sys_union.class, unionId);
if (!sysAdmin && union == null) {
throw new BaseException("未找到ID为 " + unionId + " 的工会信息");
}
List<BuildHomeLittleHouse> list = query(cnd);
IntStream.range(0, list.size()).forEach(i -> list.get(i).setIndex(i + 1));
Map<String, Object> docMap = new HashMap<>();
docMap.put("unionName", sysAdmin ? "全部工会" : union.getName());
docMap.put("date", DateUtil.today());
docMap.put("list", list);
return docMap;
}
private void fillCurrentUserOrg(BuildHomeLittleHouse house) {
Map<String, String> currentUserOrg = currentUserOrg();
house.setUnionId(currentUserOrg.get("unionId"));
house.setUnionName(currentUserOrg.get("unionName"));
house.setUnitId(currentUserOrg.get("unitId"));
house.setUnitName(currentUserOrg.get("unitName"));
}
private void keepOriginalOrg(BuildHomeLittleHouse house) {
BuildHomeLittleHouse oldHouse = dao().fetch(BuildHomeLittleHouse.class, house.getId());
if (oldHouse == null) {
fillCurrentUserOrg(house);
return;
}
house.setUnionId(oldHouse.getUnionId());
house.setUnionName(oldHouse.getUnionName());
house.setUnitId(oldHouse.getUnitId());
house.setUnitName(oldHouse.getUnitName());
}
private BuildHomeLittleHouse fetchRequiredHouse(Long id) {
if (id == null) {
throw new BaseException("小家记录ID不能为空");
}
BuildHomeLittleHouse house = dao().fetch(BuildHomeLittleHouse.class, id);
if (house == null) {
throw new BaseException("未找到小家记录");
}
return house;
}
private boolean isSysAdmin() {
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name());
}
private boolean isValidCoordinate(Double value) {
return value != null && value >= 0 && value <= 100;
}
}
@@ -0,0 +1,110 @@
package com.budwk.app.zhgh.dayofficework.caredata;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.caredata.service.CareDataLeaderService;
import io.swagger.annotations.Api;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
@IocBean
@At("/platform/careData/leader")
@Api("领导驾驶舱")
@Ok("json:full")
public class CareDataLeaderCon {
@Inject("refer:careDataLeaderServiceImpl")
private CareDataLeaderService careDataLeaderService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/careData/leader/indexOverlay.html")
@SaCheckPermission("careData.union")
public void index() {
}
@At("proposal")
@Ok("beetl:/platform/zhgh/dayofficework/careData/leader/proposalOverlay.html")
@SaCheckPermission("careData.union")
public void proposal() {
}
@At("condolence")
@Ok("beetl:/platform/zhgh/dayofficework/careData/leader/condolenceOverlay.html")
@SaCheckPermission("careData.union")
public void condolence() {
}
@At("baseUnion")
@Ok("beetl:/platform/zhgh/dayofficework/careData/leader/baseUnionOverlay.html")
@SaCheckPermission("careData.union")
public void baseUnion() {
}
@At("club")
@Ok("beetl:/platform/zhgh/dayofficework/careData/leader/clubOverlay.html")
@SaCheckPermission("careData.union")
public void club() {
}
@At("dataMetric")
@Ok("beetl:/platform/zhgh/dayofficework/careData/leader/dataMetricOverlay.html")
@SaCheckPermission("careData.union")
public void dataMetric() {
}
@At("assetData")
@Ok("beetl:/platform/zhgh/dayofficework/careData/leader/assetDataOverlay.html")
@SaCheckPermission("careData.union")
public void assetData() {
}
@At("staffHome")
@Ok("beetl:/platform/zhgh/dayofficework/careData/leader/staffHomeOverlay.html")
@SaCheckPermission("careData.union")
public void staffHome() {
}
@At
@SaCheckPermission("careData.union")
public Result proposalData(String sessionId) {
return Result.success(careDataLeaderService.proposalData(sessionId));
}
@At
@SaCheckPermission("careData.union")
public Result condolenceData(Integer year) {
return Result.success(careDataLeaderService.condolenceData(year));
}
@At
@SaCheckPermission("careData.union")
public Result baseUnionData() {
return Result.success(careDataLeaderService.baseUnionData());
}
@At
@SaCheckPermission("careData.union")
public Result clubData() {
return Result.success(careDataLeaderService.clubData());
}
@At
@SaCheckPermission("careData.union")
public Result dataMetricData(Integer year) {
return Result.success(careDataLeaderService.dataMetricData(year));
}
@At
@SaCheckPermission("careData.union")
public Result assetDataData() {
return Result.success(careDataLeaderService.assetDataData());
}
@At
@SaCheckPermission("careData.union")
public Result staffHomeData() {
return Result.success(careDataLeaderService.staffHomeData());
}
}
@@ -1,4 +1,4 @@
package com.budwk.app.zhgh.dayofficework.careData;
package com.budwk.app.zhgh.dayofficework.caredata;
import cn.dev33.satoken.annotation.SaCheckPermission;
@@ -0,0 +1,61 @@
package com.budwk.app.zhgh.dayofficework.caredata.service;
import org.nutz.lang.util.NutMap;
/**
* 领导驾驶舱数据服务。
*/
public interface CareDataLeaderService {
/**
* 查询提案概览、办结和满意率数据。
*
* @param sessionId 教代会届次ID,为空时取最新届次。
* @return 提案概览数据,包含 session、overview、caseResults。
*/
NutMap proposalData(String sessionId);
/**
* 查询慰问类型排行数据。
*
* @param year 年份,为空时取当前年份。
* @return 慰问排行数据。
*/
NutMap condolenceData(Integer year);
/**
* 查询分工会会员数排行。
*
* @return 分工会会员统计数据。
*/
NutMap baseUnionData();
/**
* 查询协会人数排行。
*
* @return 协会人员统计数据。
*/
NutMap clubData();
/**
* 查询驾驶舱中部指标数据。
*
* @param year 年份,为空时取当前年份。
* @return 预算、疗休养、荣誉、困难帮扶和报销统计数据。
*/
NutMap dataMetricData(Integer year);
/**
* 查询资产使用状态分布。
*
* @return 资产使用状态统计数据。
*/
NutMap assetDataData();
/**
* 查询职工小家地图点位数据。
*
* @return 职工小家列表数据。
*/
NutMap staffHomeData();
}
@@ -0,0 +1,475 @@
package com.budwk.app.zhgh.dayofficework.caredata.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.caredata.service.CareDataLeaderService;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@IocBean(args = {"refer:dao"})
public class CareDataLeaderServiceImpl implements CareDataLeaderService {
private static final int FINISHED_STATE = 20;
private final Dao dao;
public CareDataLeaderServiceImpl(Dao dao) {
this.dao = dao;
}
@Override
public NutMap proposalData(String sessionId) {
String currentSessionId = StrUtil.blankToDefault(sessionId, latestSessionId());
if (StrUtil.isBlank(currentSessionId)) {
return NutMap.NEW();
}
NutMap session = sessionInfo(currentSessionId);
List<NutMap> caseResults = caseResultData(currentSessionId);
NutMap overview = overviewData(currentSessionId, caseResults);
return NutMap.NEW()
.addv("session", session)
.addv("overview", overview)
.addv("caseResults", caseResults);
}
@Override
public NutMap condolenceData(Integer year) {
int targetYear = year == null ? LocalDate.now().getYear() : year;
List<NutMap> rows = condolenceTypeRows(targetYear);
long total = rows.stream().mapToLong(row -> row.getLong("count", 0L)).sum();
List<NutMap> sortedRows = new ArrayList<>(rows);
sortedRows.sort((left, right) -> Long.compare(right.getLong("count", 0L), left.getLong("count", 0L)));
List<NutMap> topRows = new ArrayList<>();
int index = 1;
for (NutMap row : sortedRows) {
if (index > 5) {
break;
}
long count = row.getLong("count", 0L);
topRows.add(NutMap.NEW()
.addv("index", index++)
.addv("typeId", row.getString("id", ""))
.addv("typeCode", row.getString("code", ""))
.addv("name", row.getString("name", ""))
.addv("count", count)
.addv("rate", percent(count, total))
.addv("rateValue", percentValue(count, total)));
}
return NutMap.NEW()
.addv("year", targetYear)
.addv("total", total)
.addv("types", topRows);
}
@Override
public NutMap baseUnionData() {
return NutMap.NEW().addv("unions", baseUnionRows());
}
@Override
public NutMap clubData() {
return NutMap.NEW().addv("clubs", clubRows());
}
@Override
public NutMap dataMetricData(Integer year) {
int targetYear = year == null ? LocalDate.now().getYear() : year;
return NutMap.NEW()
.addv("year", targetYear)
.addv("budgetTotal", schoolBudgetTotal(targetYear))
.addv("tourCount", tourCount(targetYear))
.addv("honorCount", honorCount(targetYear))
.addv("difficultCount", difficultCount(targetYear))
.addv("reimburseTotal", reimburseTotal(targetYear));
}
@Override
public NutMap assetDataData() {
List<NutMap> states = assetUsageStateRows();
long total = states.stream().mapToLong(row -> row.getLong("value", 0L)).sum();
return NutMap.NEW()
.addv("total", total)
.addv("states", states);
}
@Override
public NutMap staffHomeData() {
return NutMap.NEW().addv("houses", littleHouseRows());
}
private List<NutMap> littleHouseRows() {
Sql sql = Sqls.create("""
SELECT
id,
unionName,
unitName,
address,
mediaFiles,
mapX,
mapY,
sortNum
FROM build_home_little_house
WHERE mapX IS NOT NULL
AND mapY IS NOT NULL
$unionFilter
ORDER BY sortNum ASC, id ASC
""");
setUnionFilter(sql, "unionId", null);
return listMap(sql);
}
private String latestSessionId() {
Sql sql = Sqls.create("""
SELECT id
FROM teacher_congress_session
ORDER BY startDate DESC
LIMIT 1
""");
return firstMap(sql).getString("id", "");
}
private List<NutMap> condolenceTypeRows(int year) {
Sql sql = Sqls.create("""
SELECT
t.id,
t.code,
t.name,
COUNT(info.id) AS count
FROM condolence_type t
LEFT JOIN condolence info ON info.type = t.id
AND YEAR(info.createTime) = @year
$unionFilter
WHERE t.enable = 1
GROUP BY t.id, t.code, t.name, t.sortNum
ORDER BY count DESC, t.sortNum ASC, t.code ASC
""");
sql.setParam("year", year);
setUnionFilter(sql, "info.applyUnionId", null);
return listMap(sql);
}
private List<NutMap> baseUnionRows() {
Sql sql = Sqls.create("""
SELECT
un.id,
un.name AS unionname,
un.unionCode,
(
SELECT COUNT(1)
FROM vw_user u
WHERE u.member = 1
AND u.unionId = un.id
) AS value
FROM sys_union un
WHERE un.delFlag = 0
$unionFilter
ORDER BY un.unionCode
""");
setUnionFilter(sql, "un.id", null);
return listMap(sql);
}
private List<NutMap> clubRows() {
Sql sql = Sqls.create("""
SELECT
c.id,
c.clubName,
c.clubCode,
(
SELECT COUNT(1)
FROM club_user cu
WHERE cu.clubId = c.id
AND cu.delFlag = 0
) AS value
FROM sys_club c
WHERE c.delFlag = 0
AND c.dismiss = 0
ORDER BY c.clubCode
""");
return listMap(sql);
}
private List<NutMap> assetUsageStateRows() {
Sql sql = Sqls.create("""
SELECT
IFNULL(NULLIF(TRIM(t1.assetUsageStateName), ''), @emptyName) AS name,
SUM(IFNULL(t1.assetQuantity, 0)) AS value
FROM `asset` t1
WHERE 1 = 1
$unionFilter
GROUP BY IFNULL(NULLIF(TRIM(t1.assetUsageStateName), ''), @emptyName)
ORDER BY value DESC, name ASC
""");
sql.setParam("emptyName", "未填写");
setUnionFilter(sql, "t1.assetUseUnionId", null);
return listMap(sql);
}
private BigDecimal schoolBudgetTotal(int year) {
Sql sql = Sqls.create("""
SELECT IFNULL(SUM(totalQuota), 0) AS total
FROM outlay_manage_school
WHERE delFlag = 0
AND `year` = @year
""");
sql.setParam("year", year);
return decimalValue(firstMap(sql), "total");
}
private long tourCount(int year) {
Sql sql = Sqls.create("""
SELECT COUNT(DISTINCT info.id) AS count
FROM recuperation_enroll info
WHERE info.isNormal = 1
AND info.stateId NOT IN (2715, 2725, 2735)
AND YEAR(info.signingUptime) = @year
$unionFilter
""");
// 当前项目没有参考项目的 tour_ledger 表,疗休养人数改用现有报名表统计正常且未被驳回的数据。
sql.setParam("year", year);
setUnionFilter(sql, "info.takePartInUnionId", null);
return firstMap(sql).getLong("count", 0L);
}
private long honorCount(int year) {
Sql sql = Sqls.create("""
SELECT COUNT(h.id) AS count
FROM honor h
WHERE h.delFlag = 0
AND YEAR(h.grantDate) = @year
$unionFilter
""");
sql.setParam("year", year);
setUnionFilter(sql, "h.unionId", null);
return firstMap(sql).getLong("count", 0L);
}
private long difficultCount(int year) {
Sql sql = Sqls.create("""
SELECT COUNT(DISTINCT info.id) AS count
FROM difficult_help_info info
INNER JOIN wf_process_instance ins ON ins.businessNo = info.id
WHERE info.delFlag = 0
AND ins.state = @finished
AND YEAR(info.applyTime) = @year
$unionFilter
""");
sql.setParam("year", year);
sql.setParam("finished", FINISHED_STATE);
setUnionFilter(sql, "info.unionId", null);
return firstMap(sql).getLong("count", 0L);
}
private BigDecimal reimburseTotal(int year) {
Sql sql = Sqls.create("""
SELECT IFNULL(SUM(IFNULL(info.money, 0)), 0) AS total
FROM union_reimburse info
WHERE YEAR(info.createTime) = @year
$unionFilter
""");
// 当前项目 union_reimburse 没有参考项目的 realMoney、condolenceMoney、stateId 字段,金额按现有 money 字段统计。
sql.setParam("year", year);
setUnionFilter(sql, "info.unionId", null);
return decimalValue(firstMap(sql), "total");
}
private NutMap sessionInfo(String sessionId) {
Sql sql = Sqls.create("""
SELECT id, fullName
FROM teacher_congress_session
WHERE id = @sessionId
""");
sql.setParam("sessionId", sessionId);
return firstMap(sql);
}
private NutMap overviewData(String sessionId, List<NutMap> caseResults) {
Sql sql = Sqls.create("""
SELECT
COUNT(info.id) AS total,
SUM(CASE WHEN ins.state = @finished THEN 1 ELSE 0 END) AS doneCount
FROM proposal_info info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
WHERE info.sessionId = @sessionId
""");
sql.setParam("sessionId", sessionId);
sql.setParam("finished", FINISHED_STATE);
NutMap overview = firstMap(sql);
long filedCount = 0L;
long rejectedCount = 0L;
long suggestionCount = 0L;
for (NutMap item : caseResults) {
String name = item.getString("name", "");
long count = item.getLong("count", 0L);
if (containsAny(name, "不予", "不立案")) {
rejectedCount += count;
} else if (containsAny(name, "意见", "建议")) {
suggestionCount += count;
} else if (name.contains("立案")) {
filedCount += count;
}
}
long total = overview.getLong("total", 0L);
long doneCount = overview.getLong("doneCount", 0L);
long satisfiedCount = satisfiedCount(sessionId);
return NutMap.NEW()
.addv("total", total)
.addv("filedCount", filedCount)
.addv("suggestionCount", suggestionCount)
.addv("rejectedCount", rejectedCount)
.addv("doneCount", doneCount)
.addv("satisfiedRate", percent(satisfiedCount, doneCount));
}
private List<NutMap> caseResultData(String sessionId) {
Sql sql = Sqls.create("""
SELECT
d.code,
d.name,
COUNT(info.id) AS count
FROM sys_dict parent
INNER JOIN sys_dict d ON d.parentId = parent.id AND d.disabled = 0
LEFT JOIN proposal_info info ON info.caseFilingResult = d.code AND info.sessionId = @sessionId
WHERE parent.code = 'PROPOSAL_CASE_FILING_RESULT'
GROUP BY d.id, d.code, d.name, d.location
ORDER BY d.location
""");
sql.setParam("sessionId", sessionId);
return listMap(sql);
}
private long satisfiedCount(String sessionId) {
List<NutMap> dicts = feedbackDicts();
List<NutMap> rows = feedbackRows(sessionId);
return dicts.stream()
.filter(dict -> {
String name = dict.getString("name", "");
return name.contains("满意") && !name.contains("不满意");
})
.mapToLong(dict -> rows.stream()
.filter(row -> dict.getString("code", "").equals(row.getString("feedbackCode", "")))
.count())
.sum();
}
private List<NutMap> feedbackDicts() {
Sql sql = Sqls.create("""
SELECT d.code, d.name
FROM sys_dict parent
INNER JOIN sys_dict d ON d.parentId = parent.id AND d.disabled = 0
WHERE parent.code = 'PROPOSAL_FEEDBACK'
ORDER BY d.location
""");
return listMap(sql);
}
private List<NutMap> feedbackRows(String sessionId) {
Sql sql = Sqls.create("""
SELECT
t.variable
FROM proposal_info info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id
AND t.taskState = 20
AND t.taskName = 'feedback'
WHERE ins.state = @finished
AND info.sessionId = @sessionId
GROUP BY info.id, t.variable
""");
sql.setParam("sessionId", sessionId);
sql.setParam("finished", FINISHED_STATE);
List<NutMap> rawRows = listMap(sql);
List<NutMap> rows = new ArrayList<>();
for (NutMap row : rawRows) {
String variableStr = row.getString("variable", "");
if (StrUtil.isBlank(variableStr)) {
continue;
}
NutMap variable = Json.fromJson(NutMap.class, variableStr);
rows.add(NutMap.NEW().addv("feedbackCode", variable.getString("tf_feedback", "")));
}
return rows;
}
private void setUnionFilter(Sql sql, String columnName, String varName) {
String targetVarName = StrUtil.blankToDefault(varName, "unionFilter");
if (canViewAllUnionData()) {
sql.setVar(targetVarName, "");
} else {
sql.setVar(targetVarName, "AND " + columnName + " = @unionId");
sql.setParam("unionId", SecurityUtil.getUnionId());
}
}
private boolean canViewAllUnionData() {
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name());
}
private BigDecimal decimalValue(NutMap map, String key) {
BigDecimal value = map.getAs(key, BigDecimal.class);
return value == null ? BigDecimal.ZERO : value;
}
private List<NutMap> listMap(Sql sql) {
sql.setCallback(Sqls.callback.maps());
dao.execute(sql);
return sql.getList(NutMap.class);
}
private NutMap firstMap(Sql sql) {
sql.setCallback(Sqls.callback.map());
dao.execute(sql);
NutMap map = sql.getObject(NutMap.class);
return map == null ? NutMap.NEW() : map;
}
private boolean containsAny(String text, String... keywords) {
if (StrUtil.isBlank(text)) {
return false;
}
for (String keyword : keywords) {
if (text.contains(keyword)) {
return true;
}
}
return false;
}
private String percent(long count, long total) {
if (total <= 0) {
return "0%";
}
BigDecimal value = BigDecimal.valueOf(count)
.multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(total), 1, RoundingMode.HALF_UP);
return value.stripTrailingZeros().toPlainString() + "%";
}
private BigDecimal percentValue(long count, long total) {
if (total <= 0) {
return BigDecimal.ZERO;
}
return BigDecimal.valueOf(count)
.multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(total), 1, RoundingMode.HALF_UP)
.stripTrailingZeros();
}
}
@@ -70,12 +70,12 @@ public class QsvQuizRankServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord>
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("姓名", "userName", 20));
exportEntities.add(new ExcelExportEntity("工号", "loginName", 20));
exportEntities.add(new ExcelExportEntity("所属工会", "unionName", 30));
exportEntities.add(new ExcelExportEntity("所属单位", "unitName", 30));
exportEntities.add(new ExcelExportEntity("性别", "sex", 20));
exportEntities.add(new ExcelExportEntity("单位", "unitName", 30));
exportEntities.add(new ExcelExportEntity("分工会", "unionName", 30));
exportEntities.add(new ExcelExportEntity("联系方式", "mobile", 30));
if ("SCHEDULED".equals(mode) && pageForm.getAttemptDate() != null) {
exportEntities.add(new ExcelExportEntity("答题日期", "attemptDate", 20));
exportEntities.add(new ExcelExportEntity("答题时间", "submitTime", 20));
exportEntities.add(new ExcelExportEntity("当天得分", "totalScore", 20));
} else {
exportEntities.add(new ExcelExportEntity("得分", "sumScore", 20));
@@ -100,6 +100,7 @@ public class QsvQuizRankServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord>
u.mobile,
t1.totalScore,
t1.attemptDate,
t1.submitTime,
sum(t1.totalScore) as sumScore
FROM
`qsv_user_answer_record` t1
@@ -133,7 +134,9 @@ public class QsvQuizRankServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord>
}
cnd.groupBy("t1.userId");
cnd.desc("t1.totalScore");
cnd.asc("t1.unionName");
cnd.asc("t1.unitName");
cnd.asc("t1.loginName");
Sql sql = Sqls.create(sqlBuilder.toString());
sql.setCondition(cnd);
@@ -0,0 +1,120 @@
package com.budwk.app.zhgh.staffmanage.birthday.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService;
import com.budwk.app.zhgh.staffmanage.birthday.vo.BirthdayGreetingSendVO;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdayPageVO;
import io.swagger.annotations.Api;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
/**
* 移动端生日祝福墙控制器,提供最近生日人员、赠礼和贺卡详情能力。
*/
@IocBean
@At("/mobile/birthday/greeting")
@Ok("json:full")
@Api(tags = "移动端生日祝福墙")
public class BirthdayGreetingsController {
@Inject
private UserBirthdayService userBirthdayService;
/**
* 最近生日人员页面。
*/
@At("")
@Ok("beetl:/platform/zhghh5/birthday/greeting.html")
@SaCheckLogin
public void index() {
}
/**
* 我收到的祝福页面。
*/
@At("/greeting_info")
@Ok("beetl:/platform/zhghh5/birthday/greeting_info.html")
@SaCheckLogin
public void info() {
}
/**
* 赠送祝福页面。
*/
@At("/greeting_list")
@Ok("beetl:/platform/zhghh5/birthday/greeting_list.html")
@SaCheckLogin
public void list() {
}
/**
* 贺卡详情页面。
*/
@At("/hk")
@Ok("beetl:/platform/zhghh5/birthday/hk.html")
@SaCheckLogin
public void hk() {
}
/**
* 查询最近生日人员。
*/
@At
@SaCheckLogin
public Result pageData(UserBirthdayPageVO pageForm, String searchKeyWord) {
return Result.success(userBirthdayService.listGreetingUsers(pageForm, searchKeyWord));
}
/**
* 查询勾选的祝福对象。
*/
@At
@SaCheckLogin
public Result loadList(String greetingResult) {
return Result.success(userBirthdayService.loadGreetingUsers(greetingResult));
}
/**
* 查询祝福礼物列表。
*/
@At
@SaCheckLogin
public Result giftList() {
return Result.success(userBirthdayService.giftList());
}
/**
* 保存赠送祝福记录。
*/
@At
@SaCheckLogin
public Result sendBlessing(BirthdayGreetingSendVO vo) {
try {
userBirthdayService.sendBlessing(vo);
return Result.success();
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
}
/**
* 查询当前用户收到的祝福。
*/
@At
@SaCheckLogin
public Result giftInfo(UserBirthdayPageVO pageForm, String username) {
return Result.success(userBirthdayService.giftInfo(pageForm, username));
}
/**
* 查询单条贺卡祝福内容。
*/
@At
@SaCheckLogin
public Result findOne(String id) {
return Result.success(userBirthdayService.findGreetingInfo(id));
}
}
@@ -0,0 +1,118 @@
package com.budwk.app.zhgh.staffmanage.birthday.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.staffmanage.birthday.model.UserBirthdayConfig;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdayPageVO;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdaySendMsgVO;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import io.swagger.annotations.Api;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 生日祝福管理控制器,提供 PC 管理端列表、配置、发送和导出接口。
*/
@IocBean
@Ok("json:full")
@At("/platform/staffManage/birthday/manage")
@Api(tags = "生日祝福管理")
public class UserBirthdayManageController {
@Inject
private UserBirthdayService userBirthdayService;
/**
* PC 端生日祝福管理页。
*/
@At("")
@Ok("beetl:/platform/zhgh/staffmanage/birthday/manage/index.html")
@SaCheckPermission("staffManage.birthday.manage")
public void index() {
}
/**
* H5 生日贺卡页,文件 ID 从消息链接透传给页面。
*/
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffmanage/birthday/index.html")
@SaCheckLogin
public void h5(String id, HttpServletRequest request) {
// H5 页面需要服务端注入默认值,避免模板变量缺失时 Beetl 渲染失败。
request.setAttribute("fileId", id == null ? "" : id);
request.setAttribute("userName", SecurityUtil.getUserUsername());
}
/**
* 查询生日人员分页数据。
*/
@At
@SaCheckPermission("staffManage.birthday.manage")
public Result pageData(UserBirthdayPageVO page) {
return Result.success(userBirthdayService.pageBirthdayUsers(page));
}
/**
* 导出生日人员列表。
*/
@At
@Ok("void")
@SaCheckPermission("staffManage.birthday.manage")
public void doExport(UserBirthdayPageVO page, HttpServletResponse response) {
userBirthdayService.exportXlsx(page, response);
}
/**
* 按当前查询条件批量发送生日祝福。
*/
@At
@SaCheckPermission("staffManage.birthday.manage")
public Result sendMsgByQueryUsers(UserBirthdaySendMsgVO vo) {
try {
userBirthdayService.sendMsgByQueryUsers(vo);
return Result.success();
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
}
/**
* 给指定职工发送生日祝福。
*/
@At
@SaCheckPermission("staffManage.birthday.manage")
public Result sendMsgByUser(UserBirthdaySendMsgVO vo) {
try {
userBirthdayService.sendMsgByUser(vo);
return Result.success();
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
}
/**
* 获取生日贺卡配置,PC 管理端和 H5 展示页均会读取。
*/
@At
@SaCheckLogin
public Result getConfig() {
return Result.success(userBirthdayService.getConfig());
}
/**
* 保存生日贺卡配置。
*/
@At
@SaCheckPermission("staffManage.birthday.manage")
public Result saveOrModifyConfig(UserBirthdayConfig config) {
userBirthdayService.saveOrModifyConfig(config);
return Result.success();
}
}
@@ -0,0 +1,42 @@
package com.budwk.app.zhgh.staffmanage.birthday.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayMsgLogService;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdayMsgLogPageVO;
import io.swagger.annotations.Api;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
/**
* 生日祝福消息日志控制器。
*/
@IocBean
@Ok("json:full")
@At("/platform/staffManage/birthday/msgLog")
@Api(tags = "生日祝福消息日志")
public class UserBirthdayMsgLogController {
@Inject
private UserBirthdayMsgLogService userBirthdayMsgLogService;
/**
* PC 端生日祝福发送日志页。
*/
@At("")
@Ok("beetl:/platform/zhgh/staffmanage/birthday/msglog/index.html")
@SaCheckPermission("staffManage.birthday.msgLog")
public void index() {
}
/**
* 查询生日祝福发送日志分页数据。
*/
@At
@SaCheckPermission("staffManage.birthday.msgLog")
public Result pageData(UserBirthdayMsgLogPageVO page) {
return Result.success(userBirthdayMsgLogService.pageMsgLogs(page));
}
}
@@ -0,0 +1,54 @@
package com.budwk.app.zhgh.staffmanage.birthday.model;
import com.budwk.app.sys.models.Sys_file;
import lombok.Data;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.List;
/**
* 生日祝福墙礼物配置,用于移动端选择祝福礼物。
*/
@Data
@Table("blessing_gift")
@Comment("生日祝福墙礼物")
public class BlessingGift {
@Column
@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 giftName;
@Column
@Comment("礼物文件名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String fileName;
@Column
@Comment("礼物图片")
@ColDefine(type = ColType.MYSQL_JSON)
private List<Sys_file> files;
@Column
@Comment("排序字段")
@ColDefine(type = ColType.VARCHAR, width = 10)
private String sortField;
@Column
@Comment("创建人")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String createBy;
@Column
@Comment("创建时间")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String createDate;
}
@@ -0,0 +1,41 @@
package com.budwk.app.zhgh.staffmanage.birthday.model;
import lombok.Data;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
/**
* 生日祝福墙人员中间表,保存当期可被赠送祝福的生日人员。
*/
@Data
@Table("greeting")
@Comment("生日祝福墙人员")
public class Greeting {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("生日月日,格式MMdd")
@ColDefine(type = ColType.VARCHAR, width = 4)
private String birthday;
@Column
@Comment("职工ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("职工姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String username;
@Column
@Comment("职工工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String loginname;
}
@@ -0,0 +1,51 @@
package com.budwk.app.zhgh.staffmanage.birthday.model;
import lombok.Data;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
/**
* 生日祝福墙赠礼记录,保存赠送人与接收人的祝福内容。
*/
@Data
@Table("greeting_info")
@Comment("生日祝福墙赠礼记录")
public class GreetingInfo {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("赠送人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String presenter;
@Column
@Comment("接收人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String received;
@Column
@Comment("礼物ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String giftId;
@Column
@Comment("赠送时间")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String giftTime;
@Column
@Comment("祝福语")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String message;
@Column
@Comment("是否发送贺卡")
@ColDefine(type = ColType.BOOLEAN)
private Boolean isSend;
}
@@ -0,0 +1,29 @@
package com.budwk.app.zhgh.staffmanage.birthday.model;
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;
/**
* 生日祝福配置,保存移动端贺卡展示图等全局配置。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("user_birthday_config")
@Comment("生日祝福配置")
public class UserBirthdayConfig extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("生日贺卡文件ID")
@ColDefine(type = ColType.VARCHAR, width = 64)
private String birthdayUrl;
}
@@ -0,0 +1,69 @@
package com.budwk.app.zhgh.staffmanage.birthday.model;
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;
/**
* 生日祝福消息发送日志,记录每次发送到个人的消息内容和链接。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("user_birthday_msg_log")
@Comment("生日祝福消息日志")
public class UserBirthdayMsgLog extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("推送时间")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String pushTime;
@Column
@Comment("消息标题")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String msgTitle;
@Column
@Comment("消息内容")
@ColDefine(type = ColType.TEXT)
private String msgContent;
@Column
@Comment("接收人")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String receiveBy;
@Column
@Comment("接收人姓名")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String receiveName;
@Column
@Comment("推送人")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String pushBy;
@Column
@Comment("推送人姓名")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String pushByName;
@Column
@Comment("推送类型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String pushType;
@Column
@Comment("卡片链接")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String link;
}
@@ -0,0 +1,34 @@
package com.budwk.app.zhgh.staffmanage.birthday.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.staffmanage.birthday.model.UserBirthdayMsgLog;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdayMsgLogPageVO;
import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* 生日祝福消息日志服务。
*/
public interface UserBirthdayMsgLogService extends BaseService<UserBirthdayMsgLog> {
/**
* 分页查询生日祝福消息日志,并在查询层补齐页面展示所需的人员和推送字段别名。
*
* @param page 日志查询分页参数
* @return 消息日志分页数据
*/
Pagination pageMsgLogs(UserBirthdayMsgLogPageVO page);
/**
* 批量记录生日消息发送日志。
*
* @param users 接收人列表,至少包含 loginname 和 username
* @param title 消息标题
* @param content 消息内容
* @param imageUrl 消息图片
* @param link 跳转链接
*/
void insertLogs(List<NutMap> users, String title, String content, String imageUrl, String link);
}
@@ -0,0 +1,120 @@
package com.budwk.app.zhgh.staffmanage.birthday.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.staffmanage.birthday.model.UserBirthdayConfig;
import com.budwk.app.zhgh.staffmanage.birthday.vo.BirthdayGreetingSendVO;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdayPageVO;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdaySendMsgVO;
import org.nutz.dao.sql.Sql;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 生日祝福业务服务,统一承载 PC 管理端、H5 贺卡、移动端祝福墙所需业务。
*/
public interface UserBirthdayService extends BaseService<Sys_user> {
/**
* 构造生日人员通用查询 SQL。
*
* @param pageVO 查询参数
* @return 可直接执行的 Nutz SQL
*/
Sql commonSql(UserBirthdayPageVO pageVO);
/**
* 查询生日人员分页数据。
*
* @param pageVO 查询参数
* @return 分页结果
*/
Pagination pageBirthdayUsers(UserBirthdayPageVO pageVO);
/**
* 导出生日人员列表。
*
* @param pageVO 查询参数
* @param response 下载响应
*/
void exportXlsx(UserBirthdayPageVO pageVO, HttpServletResponse response);
/**
* 按当前筛选条件批量发送生日祝福。
*
* @param vo 发送参数和筛选条件
*/
void sendMsgByQueryUsers(UserBirthdaySendMsgVO vo);
/**
* 给指定职工发送生日祝福。
*
* @param vo 发送参数
*/
void sendMsgByUser(UserBirthdaySendMsgVO vo);
/**
* 获取当前生日贺卡配置,若不存在则返回空配置对象。
*
* @return 生日配置
*/
UserBirthdayConfig getConfig();
/**
* 保存或更新生日贺卡配置。
*
* @param config 生日配置
*/
void saveOrModifyConfig(UserBirthdayConfig config);
/**
* 查询移动端祝福墙最近生日人员。
*
* @param pageVO 分页参数
* @param searchKeyword 姓名或工号关键字
* @return 人员列表
*/
List<NutMap> listGreetingUsers(UserBirthdayPageVO pageVO, String searchKeyword);
/**
* 查询已勾选的祝福对象。
*
* @param greetingResult 逗号分隔的用户 ID
* @return 人员列表
*/
List<NutMap> loadGreetingUsers(String greetingResult);
/**
* 查询祝福礼物列表。
*
* @return 礼物列表
*/
List<NutMap> giftList();
/**
* 保存移动端祝福墙赠礼记录。
*
* @param vo 赠礼参数
*/
void sendBlessing(BirthdayGreetingSendVO vo);
/**
* 查询当前用户收到的祝福礼物。
*
* @param pageVO 分页参数
* @param username 赠送人姓名
* @return 分页结果
*/
Pagination giftInfo(UserBirthdayPageVO pageVO, String username);
/**
* 查询单条祝福记录,用于贺卡详情页展示。
*
* @param id 祝福记录 ID
* @return 祝福记录
*/
NutMap findGreetingInfo(String id);
}
@@ -0,0 +1,101 @@
package com.budwk.app.zhgh.staffmanage.birthday.service.impl;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffmanage.birthday.model.UserBirthdayMsgLog;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayMsgLogService;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdayMsgLogPageVO;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* 生日祝福消息日志服务实现。
*/
@IocBean(args = {"refer:dao"})
public class UserBirthdayMsgLogServiceImpl extends BaseServiceImpl<UserBirthdayMsgLog> implements UserBirthdayMsgLogService {
public UserBirthdayMsgLogServiceImpl(Dao dao) {
super(dao);
}
/**
* 查询消息日志列表,通过 SQL 别名适配页面字段,不修改生日日志表或系统人员表结构。
*/
@Override
public Pagination pageMsgLogs(UserBirthdayMsgLogPageVO page) {
Sql sql = Sqls.create("""
SELECT
l.id,
vu.loginname AS loginname,
IFNULL(vu.username, l.receiveName) AS username,
vu.unionName AS unionName,
vu.unitName AS unitName,
l.msgTitle,
l.msgContent,
l.pushByName,
l.pushTime,
l.pushType,
l.link
FROM
user_birthday_msg_log l
LEFT JOIN vw_user vu ON vu.id = l.receiveBy
$condition
""");
Cnd cnd = Cnd.NEW();
page.buildSearch(cnd, "l.", "vu.");
cnd.desc("l.pushTime");
sql.setCondition(cnd);
return listPageMap(page.getPageNumber(), page.getPageSize(), sql);
}
/**
* 批量记录发送日志,便于管理端追踪生日祝福触达情况。
*/
@Override
public void insertLogs(List<NutMap> users, String title, String content, String imageUrl, String link) {
if (users == null || users.isEmpty()) {
return;
}
String pushBy;
String pushByName;
String pushType;
// 根据是否存在当前登录上下文区分手动推送和系统推送,避免定时任务保存日志失败。
try {
pushBy = SecurityUtil.getUserId();
pushByName = SecurityUtil.getUserUsername();
pushType = "\u624b\u52a8\u63a8\u9001";
} catch (Exception e) {
pushBy = "system";
pushByName = "\u7cfb\u7edf\u63a8\u9001";
pushType = "\u7cfb\u7edf\u63a8\u9001";
}
String pushTime = DateUtil.now();
String finalPushBy = pushBy;
String finalPushByName = pushByName;
String finalPushType = pushType;
List<UserBirthdayMsgLog> logs = users.stream().map(user -> {
UserBirthdayMsgLog msgLog = new UserBirthdayMsgLog();
msgLog.setId(R.UU32());
msgLog.setReceiveBy(user.getString("id"));
msgLog.setReceiveName(user.getString("username"));
msgLog.setMsgTitle(title);
msgLog.setMsgContent(content);
msgLog.setPushBy(finalPushBy);
msgLog.setPushByName(finalPushByName);
msgLog.setPushType(finalPushType);
msgLog.setPushTime(pushTime);
msgLog.setLink(link);
return msgLog;
}).toList();
dao().fastInsert(logs);
}
}
@@ -0,0 +1,380 @@
package com.budwk.app.zhgh.staffmanage.birthday.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_msg;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.services.SysMsgService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.zhgh.staffmanage.birthday.model.GreetingInfo;
import com.budwk.app.zhgh.staffmanage.birthday.model.UserBirthdayConfig;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayMsgLogService;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService;
import com.budwk.app.zhgh.staffmanage.birthday.vo.BirthdayGreetingSendVO;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdayPageVO;
import com.budwk.app.zhgh.staffmanage.birthday.vo.UserBirthdaySendMsgVO;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Times;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 生日祝福业务服务实现,适配当前项目的用户视图、消息服务和文件预览路径。
*/
@IocBean(args = {"refer:dao"})
public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implements UserBirthdayService {
public UserBirthdayServiceImpl(Dao dao) {
super(dao);
}
@Inject
private SysMsgService sysMsgService;
@Inject
private UserBirthdayMsgLogService userBirthdayMsgLogService;
/**
* 构造生日人员通用 SQL,按距离下一次生日的天数升序排列。
*/
@Override
public Sql commonSql(UserBirthdayPageVO page) {
Sql sql = Sqls.create("""
SELECT
u.id,
u.loginname,
u.username,
u.sex,
DATE_FORMAT(u.birthday, '%Y-%m-%d') AS birthday,
u.mobile,
u.unitName,
u.unionName,
u.userState,
u.personType,
u.preparedBy,
DATEDIFF(
CASE
WHEN DATE_FORMAT(u.birthday, '%m-%d') >= DATE_FORMAT(NOW(), '%m-%d')
THEN CONCAT(YEAR(NOW()), '-', DATE_FORMAT(u.birthday, '%m-%d'))
ELSE CONCAT(YEAR(NOW()) + 1, '-', DATE_FORMAT(u.birthday, '%m-%d'))
END,
CURDATE()
) AS daysUntilBirthday
FROM
vw_user u
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("u.birthday", "is not", null);
cnd.and("u.member", "=", true);
page.buildSearch(cnd, "u.");
cnd.asc("daysUntilBirthday");
sql.setCondition(cnd);
return sql;
}
/**
* 执行生日人员分页查询。
*/
@Override
public Pagination pageBirthdayUsers(UserBirthdayPageVO pageVO) {
return listPageMap(pageVO.getPageNumber(), pageVO.getPageSize(), commonSql(pageVO));
}
/**
* 按管理端当前筛选条件导出生日人员,强制使用 XSSF 格式。
*/
@Override
public void exportXlsx(UserBirthdayPageVO pageVO, HttpServletResponse response) {
Sql sql = commonSql(pageVO);
List<NutMap> list = listMap(sql);
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("工号", "loginname", 20));
entities.add(new ExcelExportEntity("姓名", "username", 20));
entities.add(new ExcelExportEntity("性别", "sex", 10));
entities.add(new ExcelExportEntity("联系方式", "mobile", 20));
entities.add(new ExcelExportEntity("出生年月", "birthday", 20));
entities.add(new ExcelExportEntity("生日倒计时(天)", "daysUntilBirthday", 20));
entities.add(new ExcelExportEntity("在职状态", "userState", 20));
entities.add(new ExcelExportEntity("人员类型", "personType", 20));
entities.add(new ExcelExportEntity("人员性质", "preparedBy", 20));
entities.add(new ExcelExportEntity("所属工会", "unionName", 20));
entities.add(new ExcelExportEntity("所属单位", "unitName", 20));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, list);
CommonDownloadUtil.download("生日祝福人员.xlsx", workbook, response);
}
/**
* 按筛选条件发送生日祝福,并写入发送日志。
*/
@Override
public void sendMsgByQueryUsers(UserBirthdaySendMsgVO vo) {
validateSendParam(vo);
List<NutMap> users = listMap(commonSql(vo));
if (CollUtil.isEmpty(users)) {
throw new RuntimeException("未查询到消息接收人");
}
sendBirthdayMessage(users, vo.getTitle(), vo.getContent());
}
/**
* 给单个用户发送生日祝福,并写入发送日志。
*/
@Override
public void sendMsgByUser(UserBirthdaySendMsgVO vo) {
validateSendParam(vo);
if (StrUtil.isBlank(vo.getUserId())) {
throw new RuntimeException("未获取到消息接收人");
}
Sql sql = Sqls.create("""
SELECT
id,
loginname,
username
FROM
vw_user
WHERE
id = @id
""").setParam("id", vo.getUserId());
List<NutMap> users = listMap(sql);
if (CollUtil.isEmpty(users)) {
throw new RuntimeException("未获取到消息接收人");
}
sendBirthdayMessage(users, vo.getTitle(), vo.getContent());
}
/**
* 获取最新生日配置,没有配置时返回空对象,便于前端直接绑定。
*/
@Override
public UserBirthdayConfig getConfig() {
UserBirthdayConfig config = dao().fetch(UserBirthdayConfig.class, Cnd.NEW().desc("updatedAt"));
return ObjectUtil.defaultIfNull(config, new UserBirthdayConfig());
}
/**
* 保存生日配置,存在 ID 时更新,否则新增。
*/
@Override
public void saveOrModifyConfig(UserBirthdayConfig config) {
if (StrUtil.isBlank(config.getId())) {
dao().insert(config);
} else {
dao().updateIgnoreNull(config);
}
}
/**
* 查询祝福墙最近生日人员,排除当前登录人,避免自己给自己送祝福。
*/
@Override
public List<NutMap> listGreetingUsers(UserBirthdayPageVO pageVO, String searchKeyword) {
Sql sql = Sqls.create("""
SELECT
g.id,
g.birthday,
g.userId,
g.username,
g.loginname,
u.unitName
FROM
greeting g
LEFT JOIN vw_user u ON g.userId = u.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.where().and("g.userId", "!=", SecurityUtil.getUserId());
if (StrUtil.isNotBlank(searchKeyword)) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.orLike("g.username", searchKeyword);
group.orLike("g.loginname", searchKeyword);
cnd.and(group);
}
cnd.asc("g.birthday");
sql.setCondition(cnd);
return listMap(sql);
}
/**
* 根据移动端勾选结果查询祝福对象。
*/
@Override
public List<NutMap> loadGreetingUsers(String greetingResult) {
List<String> userIds = splitIds(greetingResult);
if (CollUtil.isEmpty(userIds)) {
return List.of();
}
Sql sql = Sqls.create("""
SELECT
id,
birthday,
userId,
username,
loginname
FROM
greeting
$condition
""");
sql.setCondition(Cnd.where("userId", "in", userIds));
return listMap(sql);
}
/**
* 查询祝福墙礼物列表。
*/
@Override
public List<NutMap> giftList() {
Sql sql = Sqls.create("""
SELECT
bg.*
FROM
blessing_gift bg
$condition
""");
sql.setCondition(Cnd.NEW().asc("bg.sortField"));
return listMap(sql);
}
/**
* 保存移动端赠礼记录,逐个接收人生成一条祝福数据。
*/
@Override
public void sendBlessing(BirthdayGreetingSendVO vo) {
List<String> userIds = splitIds(vo.getGreetingResult());
if (CollUtil.isEmpty(userIds)) {
throw new RuntimeException("请选择祝福对象");
}
String currentUserId = SecurityUtil.getUserId();
String today = DateUtil.formatDate(new java.util.Date());
List<GreetingInfo> greetingInfos = userIds.stream().map(userId -> {
GreetingInfo greetingInfo = new GreetingInfo();
greetingInfo.setPresenter(currentUserId);
greetingInfo.setReceived(userId);
greetingInfo.setGiftId(vo.getGiftId());
greetingInfo.setGiftTime(today);
greetingInfo.setMessage(vo.getMessage());
greetingInfo.setIsSend(Boolean.TRUE.equals(vo.getIsSend()));
return greetingInfo;
}).toList();
dao().insert(greetingInfos);
}
/**
* 查询当前用户收到的祝福礼物,支持按赠送人姓名模糊筛选。
*/
@Override
public Pagination giftInfo(UserBirthdayPageVO pageVO, String username) {
Sql sql = Sqls.create("""
SELECT
gi.id,
su.username,
bg.files,
gi.giftTime,
gi.message,
su.unitName AS name,
gi.isSend
FROM
greeting_info gi
LEFT JOIN vw_user su ON gi.presenter = su.id
LEFT JOIN blessing_gift bg ON gi.giftId = bg.id
$condition
""");
Cnd cnd = Cnd.where("gi.received", "=", SecurityUtil.getUserId());
if (StrUtil.isNotBlank(username)) {
cnd.where().andLike("su.username", username);
}
cnd.desc("gi.giftTime");
sql.setCondition(cnd);
return listPageMap(pageVO.getPageNumber(), pageVO.getPageSize(), sql);
}
/**
* 查询单条祝福记录,供贺卡详情页展示赠送人和祝福语。
*/
@Override
public NutMap findGreetingInfo(String id) {
Sql sql = Sqls.create("""
SELECT
su.username,
gi.message
FROM
greeting_info gi
LEFT JOIN vw_user su ON gi.presenter = su.id
WHERE
gi.id = @id
""").setParam("id", id);
List<NutMap> list = listMap(sql);
return CollUtil.isEmpty(list) ? NutMap.NEW() : list.get(0);
}
/**
* 校验消息标题和内容,避免生成空消息。
*/
private void validateSendParam(UserBirthdaySendMsgVO vo) {
if (StrUtil.isBlank(vo.getTitle())) {
throw new RuntimeException("请输入标题");
}
if (StrUtil.isBlank(vo.getContent())) {
throw new RuntimeException("请输入发送内容");
}
}
/**
* 统一发送生日消息并记录日志,保证单人和批量发送行为一致。
*/
private void sendBirthdayMessage(List<NutMap> users, String title, String content) {
UserBirthdayConfig config = getConfig();
String link = Globals.AppDomain + "/platform/staffManage/birthday/manage/h5?id=" + StrUtil.blankToDefault(config.getBirthdayUrl(), "");
List<String> loginNames = users.stream().map(user -> user.getString("loginname")).filter(StrUtil::isNotBlank).distinct().toList();
if (CollUtil.isEmpty(loginNames)) {
throw new RuntimeException("未获取到有效消息接收人");
}
Sys_msg sysMsg = new Sys_msg();
sysMsg.setTitle(title);
sysMsg.setNote(content);
sysMsg.setUrl(link);
sysMsg.setType("user");
sysMsg.setSendType("hide");
sysMsg.setWechatEnterprise(true);
sysMsg.setSendAt(Times.getTS());
sysMsg.setCreatedBy(SecurityUtil.getUserId());
sysMsgService.saveMsg(sysMsg, loginNames.toArray(String[]::new), true);
userBirthdayMsgLogService.insertLogs(users, title, content, "", link);
}
/**
* 拆分移动端传入的逗号分隔 ID,过滤空值。
*/
private List<String> splitIds(String ids) {
if (StrUtil.isBlank(ids)) {
return List.of();
}
return Arrays.stream(ids.split(",")).filter(StrUtil::isNotBlank).distinct().toList();
}
}
@@ -0,0 +1,22 @@
package com.budwk.app.zhgh.staffmanage.birthday.vo;
import lombok.Data;
/**
* 移动端祝福墙赠礼参数。
*/
@Data
public class BirthdayGreetingSendVO {
/**
* 逗号分隔的接收人 ID 列表。
*/
private String greetingResult;
/**
* 礼物 ID、祝福语和是否同步发送贺卡。
*/
private String giftId;
private String message;
private Boolean isSend;
}
@@ -0,0 +1,64 @@
package com.budwk.app.zhgh.staffmanage.birthday.vo;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.PageForm;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.Cnd;
import org.nutz.dao.util.cri.SqlExpressionGroup;
/**
* 生日祝福消息日志查询参数。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserBirthdayMsgLogPageVO extends PageForm {
/**
* 按接收人工号、姓名、标题进行模糊检索。
*/
private String keyword;
private String unionId;
private String unitId;
/**
* 构造日志列表查询条件。
*
* @param cnd Nutz 查询条件对象
*/
public void buildSearch(Cnd cnd) {
buildSearch(cnd, "", "");
}
/**
* 构造日志列表查询条件,prefix 用于适配带表别名的 SQL 查询。
*
* @param cnd Nutz 查询条件对象
* @param prefix 字段前缀,例如 l.
*/
public void buildSearch(Cnd cnd, String prefix) {
buildSearch(cnd, prefix, "");
}
/**
* 构造日志列表查询条件,分别适配日志表别名和人员视图别名。
*
* @param cnd Nutz 查询条件对象
* @param logPrefix 日志表字段前缀,例如 l.
* @param userPrefix 人员视图字段前缀,例如 vu.
*/
public void buildSearch(Cnd cnd, String logPrefix, String userPrefix) {
String alias = StrUtil.blankToDefault(logPrefix, "");
String key = StrUtil.blankToDefault(keyword, getSearchKeyword());
if (StrUtil.isNotBlank(key)) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.orLike(StrUtil.blankToDefault(userPrefix, "") + "loginname", key);
group.orLike(alias + "receiveName", key);
group.orLike(alias + "msgTitle", key);
cnd.and(group);
}
String userAlias = StrUtil.blankToDefault(userPrefix, "");
cnd.andEX(userAlias + "unionId", "=", unionId);
cnd.andEX(userAlias + "unitId", "=", unitId);
}
}
@@ -0,0 +1,92 @@
package com.budwk.app.zhgh.staffmanage.birthday.vo;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.PageForm;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.Cnd;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
import org.nutz.json.Json;
import java.util.ArrayList;
import java.util.List;
/**
* 生日人员分页查询参数,统一承载 PC 列表、导出和批量发送的筛选条件。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserBirthdayPageVO extends PageForm {
/**
* 按工号、姓名、手机号进行模糊检索。
*/
private String keyword;
/**
* 指定工会、单位、人员类型等筛选条件。
*/
private String unionId;
private String unitId;
private String personType;
private String userState;
private String sex;
private String preparedBy;
private String userStates;
private String personTypes;
private String preparedBys;
/**
* 生日月份,支持一次查询多个月份。
*/
private List<Integer> birthMonths;
/**
* 构造生日人员查询条件,prefix 用于适配不同 SQL 表别名。
*
* @param cnd Nutz 查询条件对象
* @param prefix 字段前缀,例如 u.
*/
public void buildSearch(Cnd cnd, String prefix) {
String alias = StrUtil.blankToDefault(prefix, "");
String key = StrUtil.blankToDefault(keyword, getSearchKeyword());
if (StrUtil.isNotBlank(key)) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.orLike(alias + "loginname", key);
group.orLike(alias + "username", key);
group.orLike(alias + "mobile", key);
cnd.and(group);
}
cnd.andEX(alias + "unionId", "=", unionId);
cnd.andEX(alias + "unitId", "=", unitId);
cnd.andEX(alias + "personType", "=", personType);
cnd.andEX(alias + "personType", "in", parseMultiValues(personTypes));
cnd.andEX(alias + "userState", "=", userState);
cnd.andEX(alias + "userState", "in", parseMultiValues(userStates));
cnd.andEX(alias + "sex", "=", sex);
cnd.andEX(alias + "preparedBy", "=", preparedBy);
cnd.andEX(alias + "preparedBy", "in", parseMultiValues(preparedBys));
if (CollUtil.isNotEmpty(birthMonths)) {
cnd.and(new Static("MONTH(" + alias + "birthday) IN (" + CollUtil.join(birthMonths, ",") + ")"));
}
}
/**
* 将页面多选组件传入的 JSON 数组字符串转换为查询条件列表,兼容生日模块迁移后的前端字段命名。
*/
private List<String> parseMultiValues(String value) {
if (StrUtil.isBlank(value)) {
return null;
}
try {
List<String> values = Json.fromJsonAsList(String.class, value);
return CollUtil.isEmpty(values) ? null : values;
} catch (RuntimeException e) {
List<String> values = new ArrayList<>();
values.add(value);
return values;
}
}
}
@@ -0,0 +1,23 @@
package com.budwk.app.zhgh.staffmanage.birthday.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 生日祝福发送参数,支持单人发送和按当前查询条件批量发送。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserBirthdaySendMsgVO extends UserBirthdayPageVO {
/**
* 单人发送时接收人的用户 ID。
*/
private String userId;
/**
* 消息标题和内容。
*/
private String title;
private String content;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,430 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="204px" height="204px" viewBox="0 0 204 204" enable-background="new 0 0 204 204" xml:space="preserve"> <image id="image0" width="204" height="204" x="0" y="0"
href="data:image/png;base64,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" />
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@
:action="action"
:on-change="handleChange"
:on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
@@ -26,6 +27,7 @@
:action="action"
:on-change="handleChange"
:on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-upload="beforeUpload"
@@ -67,6 +69,7 @@
:action="action"
:on-change="handleChange"
:on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview"
:on-remove="handleRemove"
:on-exceed="handleExceed"
@@ -205,15 +208,15 @@ module.exports = {
let tips = null
if (uploadNumber) {
tips = `只能上传<span style="color: red">${uploadNumber}</span>个文件;`
tips = '只能上传<span style="color: red">' + uploadNumber + '</span>个文件;'
}
if (fileAccept) {
tips = tips + `只能上传<span style="color: red">${fileAccept}</span>文件;`
tips = tips + '只能上传<span style="color: red">' + fileAccept + '</span>文件;'
} else {
tips = tips + '不限格式;'
}
if (fileSize) {
tips = tips + `单个文件大小不能超过<span style="color: red">${(fileSize / 1024 / 1024).toFixed(2)}</span>M`
tips = tips + '单个文件大小不能超过<span style="color: red">' + (fileSize / 1024 / 1024).toFixed(2) + '</span>M'
}
return tips
},
@@ -341,7 +344,7 @@ module.exports = {
if (valid) {
return true
}
this.$message.error(`文件只能是 ${this.fileAccept} 格式!`)
this.$message.error("文件只能是 " + this.fileAccept + " 格式!")
return false
},
@@ -441,15 +444,15 @@ module.exports = {
let result = []
if (response && response.code === 0 && file.status === "success") {
fileList.forEach((f) => {
if(f.response.data?.includes('webvpn')) {
if(f.response && f.response.data && f.response.data.includes('webvpn')) {
const index = f.response.data.indexOf('/platform')
f.response.data = f.response.data.substring(index)
this.$set(f.response, "data", f.response.data.substring(index))
}
result.push(f)
})
} else {
this.$message.error(response.msg)
fileList = fileList.splice(file, 1)
this.$message.error(response && response.msg ? response.msg : "文件上传失败,请稍后重试")
fileList = this.filterFileList(fileList, file)
}
if (result.length > 0) {
if (this.upload_result_category === "interval") {
@@ -484,6 +487,40 @@ module.exports = {
this.$emit("update:value", fileList)
},
handleError(error, file, fileList) {
this.$message.error(this.getUploadErrorMessage(error))
this.$emit("update:value", this.filterFileList(fileList, file))
},
filterFileList(fileList, file) {
if (!Array.isArray(fileList)) {
return []
}
return fileList.filter((item) => {
return !file || item.uid !== file.uid
})
},
getUploadErrorMessage(error) {
if (error && error.response && error.response.data && error.response.data.msg) {
return error.response.data.msg
}
if (error && error.status) {
return "文件上传失败,服务器返回" + error.status
}
return "文件上传失败,请检查文件后重试"
},
buildQrCodeAddress(timestamp) {
const currentLength = Array.isArray(this.fileList) ? this.fileList.length : 0
const remainNumber = this.upload_number ? Math.max(this.upload_number - currentLength, 0) : 0
let address = origin + "/platform/sys/h5ScanCodeUploadFile?timestamp=" + encodeURIComponent(timestamp)
address = address + "&upload_number=" + encodeURIComponent(remainNumber)
address = address + "&upload_size=" + encodeURIComponent(this.upload_size || "")
address = address + "&accept=" + encodeURIComponent(this.fileAccept || "")
return address
},
// DOM
uploadFileList() {
if (this.fileList) {
@@ -546,7 +583,7 @@ module.exports = {
mounted() {
const timestamp = new Date().getTime()
this.qrCodeAddress = origin + "/platform/sys/h5ScanCodeUploadFile?timestamp=" + timestamp
this.$set(this, "qrCodeAddress", this.buildQrCodeAddress(timestamp))
console.info("qrCodeAddress:", this.qrCodeAddress)
// watch ready
const checkReady = () => {
@@ -588,7 +625,7 @@ module.exports = {
if (validFiles.length < data.files.length) {
data.files = validFiles
this.$message.error(`文件只能是 ${this.fileAccept} 格式,已自动过滤掉不符合要求文件!`)
this.$message.error("文件只能是 " + this.fileAccept + " 格式,已自动过滤掉不符合要求文件!")
}
}
@@ -0,0 +1,47 @@
<!doctype html>
<html lang="${lang,escape}">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>数智工会领导驾驶舱</title>
<link rel="stylesheet" href="${base!}/assets/platform/css/root.css?v=20260524"/>
<script src="${base!}/assets/platform/plugins/vue/vue.js?v=20260524"></script>
<script src="${base!}/assets/platform/plugins/axios/axios.js?v=20260524"></script>
<script src="${base!}/assets/platform/plugins/jquery/jquery.js?v=20260524"></script>
<script src="${base!}/assets/platform/js/util/commonUtil.js?v=20260524"></script>
<script nonce="${cspNonce!}">
Vue.config.devtools = false
const store = {
state: {
user: JSON.parse(window.sessionStorage.getItem("user") || "null") || {permissions: [], roles: []}
}
}
window.ELEMENT = window.ELEMENT || {
Message: {
error(message) {
console.error(message || "操作失败")
}
},
Loading: {
service() {
return {
close() {}
}
}
}
}
Vue.prototype.$commonUtil = commonUtil
Vue.prototype.$auth = commonUtil.authService()
Vue.prototype.$axios = commonUtil.axiosService()
</script>
</head>
<body>
${layoutContent!}
</body>
</html>
@@ -477,10 +477,51 @@
.v4-user-section {
display: flex;
align-items: center;
gap: 24px;
gap: 18px;
height: 100%;
max-width: 355px;
max-width: 520px;
flex-shrink: 0;
padding-right: 16px;
}
.v4-retire-system-link {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 90px;
height: 30px;
padding: 0 14px;
border-radius: 18px;
color: #ffffff;
font-size: 12px;
font-weight: 700;
line-height: 1;
text-decoration: none;
white-space: nowrap;
background: linear-gradient(90deg, #f5a300 0%, #ff8e00 42%, #ff4b16 100%);
border: 1px solid rgba(255, 247, 184, 0.9);
box-shadow: inset 0 2px 4px rgba(255, 255, 255, 0.48), 0 2px 6px rgba(141, 53, 0, 0.25);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.v4-retire-system-link::before {
content: "";
position: absolute;
top: 3px;
left: 8px;
right: 8px;
height: 10px;
border-radius: 999px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0));
pointer-events: none;
}
.v4-retire-system-link:hover {
color: #ffffff;
text-decoration: none;
transform: translateY(-1px);
box-shadow: inset 0 2px 4px rgba(255, 255, 255, 0.55), 0 3px 9px rgba(141, 53, 0, 0.32);
}
.v4-notification {
@@ -613,7 +654,12 @@
}
.v4-user-section {
gap: 16px;
gap: 14px;
}
.v4-retire-system-link {
min-width: 78px;
padding: 0 10px;
}
}
@@ -692,6 +738,9 @@
</div>
<div class="v4-user-section">
<a class="v4-retire-system-link" href="http://192.168.73.133:8081/platform/login">
离退休系统
</a>
<div class="v4-user-info">
<span class="v4-user-name">${@auth.getPrincipalProperty('username')} (${@auth.getPrincipalProperty('loginname')})</span>
<!-- <i class="fa fa-angle-down"></i> -->
@@ -44,6 +44,13 @@
text-align: center;
}
.upload-tip {
margin-top: 12px;
color: #666;
font-size: 13px;
line-height: 1.6;
}
.van-uploader__tip {
color: var(--color-primary);
font-size: 14px;
@@ -78,8 +85,9 @@
></van-uploader>
<div class="upload-button">
<van-button block type="primary" @click="onSubmit">同步到电脑</van-button>
<van-button block type="primary" :disabled="submitting" @click="onSubmit">同步到电脑</van-button>
</div>
<div class="upload-tip">{{ uploadTips }}</div>
</div>
</body>
<script nonce="${cspNonce!}">
@@ -93,22 +101,97 @@
// accept: "image/*",
accept: "*",
upload_number: 9,
upload_size: 1024 * 1024 * 10,
submitting: false,
}
},
computed: {
uploadTips() {
const sizeText = (this.upload_size / 1024 / 1024).toFixed(2)
const acceptText = this.accept && this.accept !== "*" ? this.accept : "不限格式"
return "最多选择" + this.upload_number + "个文件,支持" + acceptText + ",单个文件不超过" + sizeText + "M"
}
},
created() {
this.initUploadConfig()
},
methods: {
GetQueryString(name) {
return new URLSearchParams(window.location.search).get(name)
},
initUploadConfig() {
const accept = this.GetQueryString("accept")
const uploadNumber = parseInt(this.GetQueryString("upload_number"), 10)
const uploadSize = parseInt(this.GetQueryString("upload_size"), 10)
if (accept) {
this.$set(this, "accept", accept)
}
if (!isNaN(uploadNumber)) {
this.$set(this, "upload_number", uploadNumber)
}
if (!isNaN(uploadSize) && uploadSize > 0) {
this.$set(this, "upload_size", uploadSize)
}
},
beforeRead(file) {
const files = Array.isArray(file) ? file : [file]
if (this.fileList.length + files.length > this.upload_number) {
this.$toast.fail("最多只能上传" + this.upload_number + "个文件")
return false
}
for (let i = 0; i < files.length; i++) {
const validMsg = this.validateFile(files[i])
if (validMsg) {
this.$toast.fail(validMsg)
return false
}
}
return true
},
afterRead(file) {},
buildUploadPromises(file) {
return
validateFile(file) {
if (!file) {
return "请选择要上传的文件"
}
if (file.size > this.upload_size) {
return "文件过大,请上传小于" + (this.upload_size / 1024 / 1024).toFixed(2) + "M的文件"
}
if (!this.accept || this.accept === "*") {
return ""
}
const fileName = file.name || ""
const dotIndex = fileName.lastIndexOf(".")
if (dotIndex === -1) {
return "文件必须包含有效的扩展名"
}
const fileType = fileName.substring(dotIndex).toUpperCase()
const acceptTypes = this.accept
.trim()
.split(",")
.map((type) => type.trim().toUpperCase())
if (acceptTypes.indexOf(fileType) === -1) {
return "文件只能是 " + this.accept + " 格式"
}
return ""
},
onSubmit() {
if (this.fileList.length === 0) {
this.$toast("请先选择文件")
return
}
const pendingFiles = this.fileList.filter((f) => {
return f.file
})
if (pendingFiles.length === 0) {
this.syncPc()
return
}
this.$set(this, "submitting", true)
const loading = this.$toast({
duration: 0,
overlay: true,
@@ -118,37 +201,39 @@
})
Promise.all(
this.fileList.map((f) => {
pendingFiles.map((f) => {
const formData = new FormData()
formData.append("file", f.file)
return axios.post("/platform/sys/h5ScanCodeUploadFile/upload", formData).then((axiosResp) => {
const resp = axiosResp.data
if (resp.code === 0) {
console.log(resp)
f.name = f.file.name
f.size = f.file.size
f.status = null
f.url = resp.data
f.response = resp
f.percentage = 100
f.isImage = true
this.$set(f, "name", f.file.name)
this.$set(f, "size", f.file.size)
this.$set(f, "status", null)
this.$set(f, "url", resp.data)
this.$set(f, "response", resp)
this.$set(f, "percentage", 100)
this.$set(f, "isImage", this.isImageFile(f.file.name))
delete f.file
delete f.content
console.log(f)
} else {
f.status = "fail"
this.$set(f, "status", "fail")
return Promise.reject(new Error(resp.msg || "文件上传失败"))
}
}).catch((error) => {
this.$set(f, "status", "fail")
return Promise.reject(error)
})
})
)
.then(() => {
loading.close()
console.log(this.fileList)
this.syncPc()
})
.catch((error) => {
loading.close()
this.$toast.fail(error)
this.$set(this, "submitting", false)
this.$toast.fail(this.getErrorMessage(error, "文件上传失败"))
})
},
@@ -157,15 +242,16 @@
this.$toast('请先选择文件')
return
}
console.log(this.fileList)
this.$set(this, "submitting", true)
const params = new URLSearchParams()
params.append("files", JSON.stringify(this.fileList))
params.append("timestamp", this.GetQueryString("timestamp") || "")
axios
.post(
"/platform/sys/h5ScanCodeUploadFile/syncPc",
{
files: JSON.stringify(this.fileList),
timestamp: this.GetQueryString("timestamp")
},
params.toString(),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
@@ -176,9 +262,33 @@
if (resp.data.code === 0) {
this.$toast.success("上传成功,请关注电脑端信息")
} else {
this.$toast.fail(resp.data.msg)
this.$toast.fail(resp.data.msg || "同步到电脑失败")
}
this.$set(this, "submitting", false)
})
.catch((error) => {
this.$toast.fail(this.getErrorMessage(error, "同步到电脑失败"))
this.$set(this, "submitting", false)
})
},
isImageFile(fileName) {
const name = fileName || ""
const suffix = name.substring(name.lastIndexOf(".") + 1).toLowerCase()
return ["jpg", "jpeg", "png", "gif", "svg"].indexOf(suffix) !== -1
},
getErrorMessage(error, defaultMsg) {
if (error && error.response && error.response.data && error.response.data.msg) {
return error.response.data.msg
}
if (error && error.message && error.message !== "Network Error") {
return defaultMsg + "" + error.message
}
if (error && error.response && error.response.status) {
return defaultMsg + ",服务器返回" + error.response.status
}
return defaultMsg + ",请稍后重试"
}
}
})
@@ -1,39 +1,69 @@
const form = {
template: /*language=HTML*/ `
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogVisible" :close-on-click-modal="false">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
<el-form-item label="单位" prop="unitName">
<el-input v-model="formData.unitName" placeholder="请输入单位" maxlength="100"
show-word-limit></el-input>
</el-form-item>
<el-form-item label="地点" prop="address">
<el-input v-model="formData.address" placeholder="请输入地点" maxlength="100"
show-word-limit></el-input>
</el-form-item>
<el-form-item label="面积" prop="area">
<el-input v-model="formData.area" placeholder="请输入面积" maxlength="10"
show-word-limit></el-input>
</el-form-item>
<el-form-item label="设施" prop="facility">
<el-input v-model="formData.facility" type="textarea" :rows="5" placeholder="请输入设施"
maxlength="1000" show-word-limit></el-input>
</el-form-item>
<el-form-item label="开放时间" prop="openTime">
<el-input v-model="formData.openTime" placeholder="请输入开放时间"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</div>
</el-dialog>
`,
template: [
"<el-dialog :title=\"formData.id ? '编辑' : '新增'\" :visible.sync=\"dialogVisible\" :close-on-click-modal=\"false\" width=\"60%\">",
" <el-form ref=\"formRef\" :model=\"formData\" :rules=\"rules\" label-width=\"110px\">",
" <el-row :gutter=\"20\">",
" <el-col :span=\"12\">",
" <el-form-item label=\"当前工会\" prop=\"unionName\">",
" <el-input v-model=\"formData.unionName\" disabled placeholder=\"当前登录人所在工会\"></el-input>",
" </el-form-item>",
" </el-col>",
" <el-col :span=\"12\">",
" <el-form-item label=\"当前单位\" prop=\"unitName\">",
" <el-input v-model=\"formData.unitName\" disabled placeholder=\"当前登录人所在单位\"></el-input>",
" </el-form-item>",
" </el-col>",
" </el-row>",
" <el-form-item label=\"地点\" prop=\"address\">",
" <el-input v-model=\"formData.address\" placeholder=\"请输入地点\" maxlength=\"100\" show-word-limit></el-input>",
" </el-form-item>",
" <el-form-item label=\"面积\" prop=\"area\">",
" <el-input v-model=\"formData.area\" placeholder=\"请输入面积\" maxlength=\"10\" show-word-limit></el-input>",
" </el-form-item>",
" <el-form-item label=\"设施\" prop=\"facility\">",
" <el-input v-model=\"formData.facility\" type=\"textarea\" :rows=\"5\" placeholder=\"请输入设施\" maxlength=\"1000\" show-word-limit></el-input>",
" </el-form-item>",
" <el-form-item label=\"开放时间\" prop=\"openTime\">",
" <el-input v-model=\"formData.openTime\" placeholder=\"请输入开放时间\"></el-input>",
" </el-form-item>",
" <el-form-item label=\"图片/视频资料\" prop=\"mediaFiles\">",
" <file-upload",
" :value.sync=\"formData.mediaFiles\"",
" :upload_number=\"10\"",
" upload_mode=\"drag\"",
" upload_result_category=\"array\"",
" complete_result",
" :accept=\"mediaAccept\">",
" </file-upload>",
" </el-form-item>",
" <el-row :gutter=\"20\">",
" <el-col :span=\"12\">",
" <el-form-item label=\"地图X坐标\" prop=\"mapX\">",
" <el-input v-model=\"formData.mapX\" disabled placeholder=\"请通过坐标按钮采集\"></el-input>",
" </el-form-item>",
" </el-col>",
" <el-col :span=\"12\">",
" <el-form-item label=\"地图Y坐标\" prop=\"mapY\">",
" <el-input v-model=\"formData.mapY\" disabled placeholder=\"请通过坐标按钮采集\"></el-input>",
" </el-form-item>",
" </el-col>",
" </el-row>",
" </el-form>",
" <div slot=\"footer\" class=\"dialog-footer\">",
" <el-button @click=\"closeDialog\">取消</el-button>",
" <el-button type=\"primary\" @click=\"onSubmit\">确定</el-button>",
" </div>",
"</el-dialog>"
].join(""),
data() {
return {
dialogVisible: false,
formData: {},
currentUserOrg: {},
mediaAccept: ".jpg,.jpeg,.png,.mp4,.mov",
rules: {
unitName: [{ required: true, message: "请输入单位", trigger: "blur" }],
unitName: [{ required: true, message: "未获取到当前单位", trigger: "blur" }],
unionName: [{ required: true, message: "未获取到当前工会", trigger: "blur" }],
address: [{ required: true, message: "请输入地点", trigger: "blur" }],
area: [{ required: true, message: "请输入面积", trigger: "blur" }],
facility: [{ required: true, message: "请输入设施", trigger: "blur" }],
@@ -42,23 +72,30 @@ const form = {
}
},
methods: {
onOpen(id) {
async onOpen(id) {
this.$set(this, "dialogVisible", true)
this.resetForm()
await this.loadCurrentUserOrg()
if (id) {
this.dialogVisible = true
this.resetForm()
$.post("/platform/buildHome/littleHouse/detail/" + id).then((resp) => {
if (resp.code === 0) {
this.formData = resp.data
this.$set(this, "formData", Object.assign({}, this.currentUserOrg, resp.data, {
mediaFiles: this.parseFiles(resp.data.mediaFiles)
}))
}
})
} else {
this.dialogVisible = true
this.formData = {}
this.$set(this, "formData", Object.assign({}, this.currentUserOrg, {
mediaFiles: []
}))
this.$nextTick(() => {
this.$refs.formRef.clearValidate()
})
}
},
closeDialog() {
this.$set(this, "dialogVisible", false)
},
resetForm() {
this.$nextTick(() => {
if (this.$refs.formRef) {
@@ -67,14 +104,49 @@ const form = {
}
})
},
async loadCurrentUserOrg() {
const resp = await $.post("/platform/buildHome/littleHouse/currentUserOrg")
if (resp.code === 0) {
this.$set(this, "currentUserOrg", resp.data || {})
}
},
parseFiles(value) {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
try {
return JSON.parse(value)
} catch (e) {
return []
}
},
getFileExt(file) {
const name = file.name || file.url || (file.response && file.response.data) || file.data || ""
const index = name.lastIndexOf(".")
return index > -1 ? name.substring(index + 1).toLowerCase() : ""
},
validateMediaFiles(files) {
const acceptExts = this.mediaAccept.split(",").map(v => v.replace(".", "").toLowerCase())
return (files || []).every(file => acceptExts.includes(this.getFileExt(file)))
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
$.post("/platform/buildHome/littleHouse/save", this.formData).then((resp) => {
if (!this.validateMediaFiles(this.formData.mediaFiles)) {
this.$message.warning("仅支持上传 jpg、jpeg、png、mp4、mov 格式文件")
return
}
const data = Object.assign({}, this.formData, {
mediaFiles: JSON.stringify(this.formData.mediaFiles || [])
})
$.post("/platform/buildHome/littleHouse/save", data).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit("refresh")
this.dialogVisible = false
this.closeDialog()
}
})
}
@@ -6,8 +6,8 @@ layout("/layouts/platform.html"){
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="单位名称">
<el-input placeholder="单位名称" clearable v-model="pageForm.searchKeyword"></el-input>
<search-item label="单位/工会名称">
<el-input placeholder="单位/工会名称" clearable v-model="pageForm.searchKeyword"></el-input>
</search-item>
</search>
</el-card>
@@ -18,6 +18,7 @@ layout("/layouts/platform.html"){
</table-tool>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder" ref="table" row-key="id" style="width: 100%">
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="80px"></el-table-column>
<el-table-column label="工会" prop="unionName" sortable width="220px" show-overflow-tooltip></el-table-column>
<el-table-column label="单位" prop="unitName" sortable width="200px"></el-table-column>
<el-table-column label="活动场所基本情况">
<el-table-column label="地点" prop="address" sortable width="200px"></el-table-column>
@@ -25,8 +26,9 @@ layout("/layouts/platform.html"){
<el-table-column label="设施" prop="facility" sortable></el-table-column>
<el-table-column label="开放时间" prop="openTime" sortable width="200px"></el-table-column>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200px">
<el-table-column label="操作" fixed="right" width="280px">
<template scope="{row}">
<el-button v-if="$auth.hasRole('SYSADMIN')" size="mini" type="warning" @click="openCoordinate(row)">坐标</el-button>
<el-button size="mini" type="primary" @click="$refs.littleHouseFormRef.onOpen(row.id)">修改</el-button>
<el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button>
</template>
@@ -63,12 +65,78 @@ layout("/layouts/platform.html"){
})
})
},
openCoordinate(row) {
if (!row || !row.id) {
this.$message.warning("未获取到小家记录")
return
}
if (this.hasCoordinate(row)) {
this.$confirm(
"当前记录已有坐标,是修改还是清空?",
"提示",
{
type: "warning",
distinguishCancelAndClose: true,
confirmButtonText: "修改坐标",
cancelButtonText: "清空坐标"
}
).then(() => {
this.doOpenCoordinate(row)
}).catch((action) => {
if (action === "cancel") {
this.clearCoordinate(row)
}
})
return
}
this.doOpenCoordinate(row)
},
hasCoordinate(row) {
return row
&& row.mapX !== null
&& row.mapX !== undefined
&& row.mapX !== ""
&& row.mapY !== null
&& row.mapY !== undefined
&& row.mapY !== ""
},
doOpenCoordinate(row) {
const params = [
"coordinateMode=1",
"houseId=" + encodeURIComponent(row.id),
"t=" + Date.now()
]
window.open("/platform/careData/leader?" + params.join("&"), "_blank")
},
clearCoordinate(row) {
$.post("/platform/buildHome/littleHouse/clearCoordinate", { id: row.id }).then((resp) => {
if (resp.code === 0) {
this.$message.success("坐标已清空")
this.doSearch()
}
})
},
handleCoordinateMessage(event) {
if (event.origin !== window.location.origin) {
return
}
const data = event.data || {}
if (data.type !== "littleHouseCoordinateSaved") {
return
}
this.$message.success("坐标采集成功")
this.doSearch()
},
onExport() {
this.$downLoad("/platform/buildHome/littleHouse/exportXlsx")
}
},
created() {
window.addEventListener("message", this.handleCoordinateMessage)
this.pageData()
},
beforeDestroy() {
window.removeEventListener("message", this.handleCoordinateMessage)
}
})
</script>
@@ -0,0 +1,287 @@
<!--#
layout("/layouts/platform_leader_dashboard.html"){
#-->
<script src="${base!}/assets/platform/plugins/echarts/echarts.min.js?v=20260527"></script>
<style>
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050f1f;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
[v-cloak] {
display: none;
}
.dashboard-wrap {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #050f1f;
}
.dashboard-stage {
position: relative;
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
background: url("${base!}/assets/platform/images/careData/leader/leader-bg.jpg?v=20260524") center center / 100% 100% no-repeat;
}
.asset-data-panel {
position: absolute;
left: 1.35%;
top: 69.6%;
width: 20.82%;
height: 28.06%;
background: url("${base!}/assets/platform/images/careData/leader/asset-data-panel.png?v=20260527") center center / 100% 100% no-repeat;
}
.asset-data-title {
position: absolute;
left: 0;
top: 1.3%;
width: 100%;
height: 10%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-family: "Microsoft YaHei", Arial, sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 1;
letter-spacing: 0;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.78);
white-space: nowrap;
}
.asset-data-chart {
position: absolute;
left: 3%;
top: 14%;
width: 94%;
height: 74%;
}
.asset-data-note {
position: absolute;
left: 8%;
right: 8%;
bottom: 4.5%;
overflow: hidden;
color: #315d86;
font-size: 12px;
line-height: 14px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.72);
}
.asset-data-empty {
position: absolute;
left: 0;
right: 0;
top: 48%;
z-index: 2;
color: rgba(36, 71, 105, 0.82);
font-size: 14px;
text-align: center;
pointer-events: none;
}
.fullscreen-btn {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 20;
height: 34px;
min-width: 86px;
padding: 0 14px;
border: 1px solid rgba(220, 246, 255, 0.72);
border-radius: 4px;
background: rgba(20, 101, 188, 0.58);
color: #f4fbff;
font-size: 13px;
line-height: 32px;
text-align: center;
cursor: pointer;
box-shadow: 0 0 14px rgba(64, 161, 255, 0.35);
user-select: none;
}
.fullscreen-btn:hover {
background: rgba(20, 126, 220, 0.72);
}
</style>
<div id="app" class="dashboard-wrap" v-cloak>
<main class="dashboard-stage">
<section class="asset-data-panel">
<div class="asset-data-title">&#36164;&#20135;&#25968;&#25454;</div>
<div ref="usageChart" class="asset-data-chart"></div>
<div class="asset-data-empty" v-if="!chartRows.length">&#26242;&#26080;&#25968;&#25454;</div>
<div class="asset-data-note">&#19981;&#21516;&#39068;&#33394;&#20195;&#34920;&#19981;&#21516;&#20351;&#29992;&#29366;&#20917;</div>
</section>
</main>
<button class="fullscreen-btn" type="button" @click="toggleFullscreen">{{ fullscreen ? "退出全屏" : "全屏显示" }}</button>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
chart: null,
chartRows: [],
fullscreen: false,
chartColors: ["#1c8df4", "#35c982", "#f3a64f", "#8a7cff", "#f05d80", "#2ec7c9", "#b6a2de"]
}
},
created() {
this.loadAssetData()
},
mounted() {
this.initChart()
window.addEventListener("resize", this.resizeChart)
document.addEventListener("fullscreenchange", this.syncFullscreenState)
document.addEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
beforeDestroy() {
window.removeEventListener("resize", this.resizeChart)
document.removeEventListener("fullscreenchange", this.syncFullscreenState)
document.removeEventListener("webkitfullscreenchange", this.syncFullscreenState)
if (this.chart) {
this.chart.dispose()
this.$set(this, "chart", null)
}
},
methods: {
initChart() {
if (!window.echarts || !this.$refs.usageChart) {
return
}
this.$set(this, "chart", echarts.init(this.$refs.usageChart))
this.renderChart()
},
loadAssetData() {
this.$axios.post("/platform/careData/leader/assetDataData").then(res => {
if (res.code !== 0) {
return
}
const data = res.data || {}
this.$set(this, "chartRows", (data.states || [])
.map(item => ({
name: item.name || "未填写",
value: Number(item.value || 0)
}))
.filter(item => item.value > 0))
this.renderChart()
})
},
renderChart() {
if (!this.chart) {
return
}
const total = this.chartRows.reduce((sum, item) => sum + item.value, 0)
this.chart.setOption({
color: this.chartColors,
tooltip: {
trigger: "item",
formatter: "{b}<br/>{c} ({d}%)"
},
legend: {
orient: "vertical",
right: "3%",
top: "24%",
itemWidth: 8,
itemHeight: 8,
itemGap: 9,
textStyle: {
color: "#315d86",
fontSize: 12,
lineHeight: 14
},
formatter(name) {
return name.length > 5 ? name.slice(0, 5) + "..." : name
}
},
graphic: {
type: "text",
left: "28%",
top: "47%",
style: {
text: total ? total + "\n件" : "",
fill: "#1683f4",
fontSize: 15,
fontWeight: 700,
textAlign: "center"
}
},
series: [{
name: "使用状况",
type: "pie",
radius: ["32%", "56%"],
center: ["32%", "52%"],
avoidLabelOverlap: true,
minAngle: 8,
itemStyle: {
borderColor: "rgba(255,255,255,0.9)",
borderWidth: 2
},
label: {
color: "#244769",
fontSize: 11,
formatter: "{d}%"
},
labelLine: {
length: 8,
length2: 4,
lineStyle: {
color: "rgba(36, 71, 105, 0.55)"
}
},
data: this.chartRows
}]
}, true)
},
resizeChart() {
if (this.chart) {
this.chart.resize()
}
},
toggleFullscreen() {
if (this.fullscreen) {
const exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen
exitFullscreen && exitFullscreen.call(document)
return
}
const target = document.documentElement
const requestFullscreen = target.requestFullscreen || target.webkitRequestFullscreen
if (requestFullscreen) {
const result = requestFullscreen.call(target)
if (result && result.catch) {
result.catch(() => {})
}
}
},
syncFullscreenState() {
this.$set(this, "fullscreen", !!(document.fullscreenElement || document.webkitFullscreenElement))
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,257 @@
<!--#
layout("/layouts/platform_leader_dashboard.html"){
#-->
<style>
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050f1f;
font-family: "Microsoft YaHei", "Microsoft JhengHei", Arial, sans-serif;
}
[v-cloak] {
display: none;
}
.dashboard-wrap {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #050f1f;
}
.dashboard-stage {
position: relative;
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
background: url("${base!}/assets/platform/images/careData/leader/leader-bg.jpg?v=20260524") center center / 100% 100% no-repeat;
}
.base-union-panel {
position: absolute;
right: calc(1.55% - 2px);
top: calc(60.05% + 95px);
width: calc(28.2% - 140px);
height: 28.1%;
background: url("${base!}/assets/platform/images/careData/leader/base-union-panel.png?v=20260526") center center / 100% 100% no-repeat;
}
.base-union-title {
position: absolute;
left: 0;
top: 1.3%;
width: 100%;
height: 10%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-family: "Microsoft YaHei", "Microsoft JhengHei", Arial, sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 1;
letter-spacing: 0;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.78);
white-space: nowrap;
}
.base-union-list {
position: absolute;
left: calc(8.5% - 15px);
top: 20.2%;
width: calc(83% + 35px);
height: 68.5%;
display: flex;
flex-direction: column;
gap: 5px;
overflow-x: hidden;
overflow-y: auto;
padding-right: 4px;
}
.base-union-list::-webkit-scrollbar {
width: 6px;
}
.base-union-list::-webkit-scrollbar-track {
background: rgba(110, 178, 237, 0.22);
border-radius: 6px;
}
.base-union-list::-webkit-scrollbar-thumb {
background: rgba(49, 142, 231, 0.76);
border-radius: 6px;
}
.base-union-row {
position: relative;
flex: 0 0 17.2%;
height: 17.2%;
min-height: 31px;
background-position: center center;
background-size: 100% 100%;
background-repeat: no-repeat;
color: #244769;
font-size: 12px;
line-height: 1;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.7);
}
.base-union-row:nth-child(2n + 1) {
background-image: url("${base!}/assets/platform/images/careData/leader/base-union-row-a.png?v=20260526");
}
.base-union-row:nth-child(2n) {
background-image: url("${base!}/assets/platform/images/careData/leader/base-union-row-b.png?v=20260526");
}
.base-union-name {
position: absolute;
left: 18%;
top: 50%;
width: 52%;
overflow: hidden;
transform: translateY(-50%);
text-overflow: ellipsis;
white-space: nowrap;
}
.base-union-count {
position: absolute;
right: 9.5%;
top: 50%;
transform: translateY(-50%);
color: #1d82e8;
font-size: 14px;
font-weight: 700;
white-space: nowrap;
}
.base-union-count span {
margin-left: 3px;
color: #4e6d8d;
font-size: 12px;
font-weight: 400;
}
.base-union-empty {
position: absolute;
left: 0;
right: 0;
top: 48%;
color: rgba(36, 71, 105, 0.82);
font-size: 14px;
text-align: center;
pointer-events: none;
}
.fullscreen-btn {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 20;
height: 34px;
min-width: 86px;
padding: 0 14px;
border: 1px solid rgba(220, 246, 255, 0.72);
border-radius: 4px;
background: rgba(20, 101, 188, 0.58);
color: #f4fbff;
font-size: 13px;
line-height: 32px;
text-align: center;
cursor: pointer;
box-shadow: 0 0 14px rgba(64, 161, 255, 0.35);
user-select: none;
}
.fullscreen-btn:hover {
background: rgba(20, 126, 220, 0.72);
}
</style>
<div id="app" class="dashboard-wrap" v-cloak>
<main class="dashboard-stage">
<section class="base-union-panel">
<div class="base-union-title">&#22522;&#23618;&#24037;&#20250;</div>
<div class="base-union-list" v-if="displayUnions.length">
<div class="base-union-row" v-for="item in displayUnions" :key="item.unionCode || item.id">
<div class="base-union-name" :title="item.unionname">{{ item.unionname || "--" }}</div>
<div class="base-union-count">{{ item.value || 0 }}<span>&#20154;</span></div>
</div>
</div>
<div class="base-union-empty" v-else>&#26242;&#26080;&#25968;&#25454;</div>
</section>
</main>
<button class="fullscreen-btn" type="button" @click="toggleFullscreen">{{ fullscreen ? "退出全屏" : "全屏显示" }}</button>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
unions: [],
fullscreen: false
}
},
computed: {
displayUnions() {
return this.unions
}
},
created() {
this.loadBaseUnionData()
},
mounted() {
document.addEventListener("fullscreenchange", this.syncFullscreenState)
document.addEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
beforeDestroy() {
document.removeEventListener("fullscreenchange", this.syncFullscreenState)
document.removeEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
methods: {
loadBaseUnionData() {
this.$axios.post("/platform/careData/leader/baseUnionData").then(res => {
if (res.code !== 0) {
return
}
const data = res.data || {}
this.$set(this, "unions", data.unions || [])
})
},
toggleFullscreen() {
if (this.fullscreen) {
const exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen
exitFullscreen && exitFullscreen.call(document)
return
}
const target = document.documentElement
const requestFullscreen = target.requestFullscreen || target.webkitRequestFullscreen
if (requestFullscreen) {
const result = requestFullscreen.call(target)
if (result && result.catch) {
result.catch(() => {})
}
}
},
syncFullscreenState() {
this.$set(this, "fullscreen", !!(document.fullscreenElement || document.webkitFullscreenElement))
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,252 @@
<!--#
layout("/layouts/platform_leader_dashboard.html"){
#-->
<style>
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050f1f;
font-family: "Microsoft YaHei", "Microsoft JhengHei", Arial, sans-serif;
}
[v-cloak] {
display: none;
}
.dashboard-wrap {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #050f1f;
}
.dashboard-stage {
position: relative;
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
background: url("${base!}/assets/platform/images/careData/leader/leader-bg.jpg?v=20260524") center center / 100% 100% no-repeat;
}
.club-panel {
position: absolute;
right: calc(1.55% - 3px);
top: calc(38.2% + 3px);
width: calc(28.2% - 145px);
height: 28.1%;
background: url("${base!}/assets/platform/images/careData/leader/base-union-panel.png?v=20260526") center center / 100% 100% no-repeat;
}
.club-title {
position: absolute;
left: 0;
top: 1.3%;
width: 100%;
height: 10%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-family: "Microsoft YaHei", "Microsoft JhengHei", Arial, sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 1;
letter-spacing: 0;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.78);
white-space: nowrap;
}
.club-list {
position: absolute;
left: calc(8.5% - 14px);
top: 20.2%;
width: calc(83% + 29px);
height: 68.5%;
display: flex;
flex-direction: column;
gap: 5px;
overflow-x: hidden;
overflow-y: auto;
padding-right: 4px;
}
.club-list::-webkit-scrollbar {
width: 6px;
}
.club-list::-webkit-scrollbar-track {
background: rgba(110, 178, 237, 0.22);
border-radius: 6px;
}
.club-list::-webkit-scrollbar-thumb {
background: rgba(49, 142, 231, 0.76);
border-radius: 6px;
}
.club-row {
position: relative;
flex: 0 0 17.2%;
height: 17.2%;
min-height: 31px;
background-position: center center;
background-size: 100% 100%;
background-repeat: no-repeat;
color: #244769;
font-size: 12px;
line-height: 1;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.7);
}
.club-row:nth-child(2n + 1) {
background-image: url("${base!}/assets/platform/images/careData/leader/base-union-row-a.png?v=20260526");
}
.club-row:nth-child(2n) {
background-image: url("${base!}/assets/platform/images/careData/leader/base-union-row-b.png?v=20260526");
}
.club-name {
position: absolute;
left: 18%;
top: 50%;
width: 52%;
overflow: hidden;
transform: translateY(-50%);
text-overflow: ellipsis;
white-space: nowrap;
}
.club-count {
position: absolute;
right: 9.5%;
top: 50%;
transform: translateY(-50%);
color: #1d82e8;
font-size: 14px;
font-weight: 700;
white-space: nowrap;
}
.club-count span {
margin-left: 3px;
color: #4e6d8d;
font-size: 12px;
font-weight: 400;
}
.club-empty {
position: absolute;
left: 0;
right: 0;
top: 48%;
color: rgba(36, 71, 105, 0.82);
font-size: 14px;
text-align: center;
pointer-events: none;
}
.fullscreen-btn {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 20;
height: 34px;
min-width: 86px;
padding: 0 14px;
border: 1px solid rgba(220, 246, 255, 0.72);
border-radius: 4px;
background: rgba(20, 101, 188, 0.58);
color: #f4fbff;
font-size: 13px;
line-height: 32px;
text-align: center;
cursor: pointer;
box-shadow: 0 0 14px rgba(64, 161, 255, 0.35);
user-select: none;
}
.fullscreen-btn:hover {
background: rgba(20, 126, 220, 0.72);
}
</style>
<div id="app" class="dashboard-wrap" v-cloak>
<main class="dashboard-stage">
<section class="club-panel">
<div class="club-title">&#21327;&#20250;&#31038;&#22242;</div>
<div class="club-list" v-if="clubs.length">
<div class="club-row" v-for="item in clubs" :key="item.clubCode || item.id">
<div class="club-name" :title="item.clubName">{{ item.clubName || "--" }}</div>
<div class="club-count">{{ item.value || 0 }}<span>&#20154;</span></div>
</div>
</div>
<div class="club-empty" v-else>&#26242;&#26080;&#25968;&#25454;</div>
</section>
</main>
<button class="fullscreen-btn" type="button" @click="toggleFullscreen">{{ fullscreen ? "退出全屏" : "全屏显示" }}</button>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
clubs: [],
fullscreen: false
}
},
created() {
this.loadClubData()
},
mounted() {
document.addEventListener("fullscreenchange", this.syncFullscreenState)
document.addEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
beforeDestroy() {
document.removeEventListener("fullscreenchange", this.syncFullscreenState)
document.removeEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
methods: {
loadClubData() {
this.$axios.post("/platform/careData/leader/clubData").then(res => {
if (res.code !== 0) {
return
}
const data = res.data || {}
this.$set(this, "clubs", data.clubs || [])
})
},
toggleFullscreen() {
if (this.fullscreen) {
const exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen
exitFullscreen && exitFullscreen.call(document)
return
}
const target = document.documentElement
const requestFullscreen = target.requestFullscreen || target.webkitRequestFullscreen
if (requestFullscreen) {
const result = requestFullscreen.call(target)
if (result && result.catch) {
result.catch(() => {})
}
}
},
syncFullscreenState() {
this.$set(this, "fullscreen", !!(document.fullscreenElement || document.webkitFullscreenElement))
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,282 @@
<!--#
layout("/layouts/platform_leader_dashboard.html"){
#-->
<style>
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050f1f;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
[v-cloak] {
display: none;
}
.dashboard-wrap {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #050f1f;
}
.dashboard-stage {
position: relative;
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
background: url("${base!}/assets/platform/images/careData/leader/leader-bg.jpg?v=20260524") center center / 100% 100% no-repeat;
}
.condolence-panel {
position: absolute;
left: calc(1.35% - 5px);
top: calc(38.9% - 28px);
width: calc(20.82% - 6px);
height: calc(27.65% + 70px);
background: url("${base!}/assets/platform/images/careData/leader/condolence-panel.png?v=20260526") center center / 100% 100% no-repeat;
}
.condolence-title {
position: absolute;
left: 0;
top: 1.3%;
width: 100%;
height: 10%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-family: "Microsoft YaHei", "Microsoft JhengHei", Arial, sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 1;
letter-spacing: 0;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.78);
white-space: nowrap;
}
.condolence-chart {
position: absolute;
left: 4%;
top: calc(18% + 10px);
width: 92%;
height: 72%;
display: flex;
align-items: flex-end;
justify-content: space-around;
padding: 2.5% 2.2% 7.5%;
}
.condolence-bar-item {
position: relative;
z-index: 1;
width: 16%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
}
.condolence-value {
position: absolute;
bottom: calc(17% + var(--bar-height) + 5px);
left: 50%;
transform: translateX(-50%);
color: #243f5d;
font-size: 12px;
font-weight: 700;
line-height: 14px;
white-space: nowrap;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.72);
}
.condolence-bar {
position: absolute;
bottom: 17%;
left: 50%;
width: 42%;
height: var(--bar-height);
min-height: 0;
transform: translateX(-50%);
background: linear-gradient(180deg, rgba(95, 183, 255, 0.96), rgba(35, 126, 238, 0.88));
border: 1px solid rgba(208, 241, 255, 0.66);
box-shadow: inset 0 0 10px rgba(215, 244, 255, 0.4), 0 0 10px rgba(58, 152, 245, 0.46);
}
.condolence-bar-item:nth-child(2n) .condolence-bar {
background: linear-gradient(180deg, rgba(90, 219, 162, 0.94), rgba(40, 177, 103, 0.86));
box-shadow: inset 0 0 10px rgba(224, 255, 239, 0.36), 0 0 10px rgba(54, 190, 118, 0.36);
}
.condolence-bar-item:nth-child(3) .condolence-bar {
background: linear-gradient(180deg, rgba(255, 183, 109, 0.94), rgba(229, 132, 66, 0.86));
box-shadow: inset 0 0 10px rgba(255, 236, 211, 0.34), 0 0 10px rgba(227, 142, 76, 0.36);
}
.condolence-label {
position: absolute;
left: 50%;
bottom: 0;
width: 56px;
overflow: hidden;
transform: translateX(-50%);
color: #244769;
font-size: 12px;
line-height: 14px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.68);
}
.condolence-empty {
position: absolute;
left: 0;
right: 0;
top: 43%;
color: rgba(36, 71, 105, 0.82);
font-size: 14px;
text-align: center;
pointer-events: none;
}
.fullscreen-btn {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 20;
height: 34px;
min-width: 86px;
padding: 0 14px;
border: 1px solid rgba(220, 246, 255, 0.72);
border-radius: 4px;
background: rgba(20, 101, 188, 0.58);
color: #f4fbff;
font-size: 13px;
line-height: 32px;
text-align: center;
cursor: pointer;
box-shadow: 0 0 14px rgba(64, 161, 255, 0.35);
user-select: none;
}
.fullscreen-btn:hover {
background: rgba(20, 126, 220, 0.72);
}
</style>
<div id="app" class="dashboard-wrap" v-cloak>
<main class="dashboard-stage">
<section class="condolence-panel">
<div class="condolence-title">&#24944;&#38382;&#27719;&#24635;</div>
<div class="condolence-chart">
<div class="condolence-empty" v-if="!hasData">&#26242;&#26080;&#25968;&#25454;</div>
<div class="condolence-bar-item" v-for="item in displayTypes" :key="item.index" :style="{ '--bar-height': barHeight(item) }">
<div class="condolence-value">{{ item.rate || "0%" }}</div>
<div class="condolence-bar"></div>
<div class="condolence-label" :title="item.name">{{ shortName(item.name) }}</div>
</div>
</div>
</section>
</main>
<button class="fullscreen-btn" type="button" @click="toggleFullscreen">{{ fullscreen ? "退出全屏" : "全屏显示" }}</button>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
types: [],
fullscreen: false
}
},
computed: {
hasData() {
return this.types.some(item => Number(item.count || 0) > 0)
},
maxCount() {
return Math.max(...this.types.map(item => Number(item.count || 0)), 0)
},
displayTypes() {
const rows = this.types.slice(0, 5)
while (rows.length < 5) {
rows.push({
index: rows.length + 1,
name: "",
rate: "0%",
rateValue: 0
})
}
return rows
}
},
created() {
this.loadCondolenceData()
},
mounted() {
document.addEventListener("fullscreenchange", this.syncFullscreenState)
document.addEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
beforeDestroy() {
document.removeEventListener("fullscreenchange", this.syncFullscreenState)
document.removeEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
methods: {
loadCondolenceData() {
this.$axios.post("/platform/careData/leader/condolenceData").then(res => {
if (res.code !== 0) {
return
}
const data = res.data || {}
this.$set(this, "types", data.types || [])
})
},
shortName(name) {
if (!name) {
return "--"
}
return name.length > 4 ? name.slice(0, 4) : name
},
barHeight(item) {
const count = Number(item.count || 0)
if (count <= 0 || this.maxCount <= 0) {
return "0%"
}
return Math.min(76, Math.max(8, count / this.maxCount * 76)) + "%"
},
toggleFullscreen() {
if (this.fullscreen) {
const exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen
exitFullscreen && exitFullscreen.call(document)
return
}
const target = document.documentElement
const requestFullscreen = target.requestFullscreen || target.webkitRequestFullscreen
if (requestFullscreen) {
const result = requestFullscreen.call(target)
if (result && result.catch) {
result.catch(() => {})
}
}
},
syncFullscreenState() {
this.$set(this, "fullscreen", !!(document.fullscreenElement || document.webkitFullscreenElement))
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,248 @@
<!--#
layout("/layouts/platform_leader_dashboard.html"){
#-->
<style>
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050f1f;
font-family: "Microsoft YaHei", "Microsoft JhengHei", Arial, sans-serif;
}
[v-cloak] {
display: none;
}
.dashboard-wrap {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #050f1f;
}
.dashboard-stage {
position: relative;
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
background: url("${base!}/assets/platform/images/careData/leader/leader-bg.jpg?v=20260524") center center / 100% 100% no-repeat;
}
.data-metric-panel {
position: absolute;
right: calc(1.55% - 2px);
top: 8.2%;
width: calc(28.2% - 140px);
height: 28.1%;
background: url("${base!}/assets/platform/images/careData/leader/data-metric-panel.png?v=20260527") center center / 100% 100% no-repeat;
}
.data-metric-title {
position: absolute;
left: 0;
top: 1.3%;
width: 100%;
height: 10%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-family: "Microsoft YaHei", "Microsoft JhengHei", Arial, sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 1;
letter-spacing: 0;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.78);
white-space: nowrap;
}
.data-metric-graph {
position: absolute;
left: calc(8% + 5px);
top: 21%;
width: calc(84% + 5px);
height: calc(66% + 20px);
background: url("${base!}/assets/platform/images/careData/leader/data-metric-graph.png?v=20260527") center center / contain no-repeat;
}
.data-metric-point {
position: absolute;
z-index: 1;
width: 28%;
min-width: 62px;
transform: translate(-50%, -50%);
color: #1c79dc;
font-family: "Microsoft YaHei", "Microsoft JhengHei", Arial, sans-serif;
text-align: center;
letter-spacing: 0;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.9), 0 0 10px rgba(72, 168, 255, 0.42);
pointer-events: none;
}
.data-metric-point-center {
width: 34%;
}
.data-metric-label {
margin-top: 2px;
overflow: hidden;
color: #315d86;
font-size: 12px;
line-height: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.data-metric-value {
overflow: hidden;
color: #1683f4;
font-size: 14px;
font-weight: 700;
line-height: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.data-metric-point-center .data-metric-value {
font-size: 16px;
line-height: 19px;
}
.data-metric-unit {
margin-left: 2px;
color: #4e6d8d;
font-size: 11px;
font-weight: 400;
}
.fullscreen-btn {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 20;
height: 34px;
min-width: 86px;
padding: 0 14px;
border: 1px solid rgba(220, 246, 255, 0.72);
border-radius: 4px;
background: rgba(20, 101, 188, 0.58);
color: #f4fbff;
font-size: 13px;
line-height: 32px;
text-align: center;
cursor: pointer;
box-shadow: 0 0 14px rgba(64, 161, 255, 0.35);
user-select: none;
}
.fullscreen-btn:hover {
background: rgba(20, 126, 220, 0.72);
}
</style>
<div id="app" class="dashboard-wrap" v-cloak>
<main class="dashboard-stage">
<section class="data-metric-panel">
<div class="data-metric-title">&#25968;&#25454;&#25351;&#26631;</div>
<div class="data-metric-graph">
<div
class="data-metric-point"
v-for="item in dataMetricPoints"
:key="item.key"
:class="{ 'data-metric-point-center': item.center }"
:style="{ left: item.left, top: item.top }">
<div class="data-metric-value">{{ item.value }}<span class="data-metric-unit">{{ item.unit }}</span></div>
<div class="data-metric-label">{{ item.label }}</div>
</div>
</div>
</section>
</main>
<button class="fullscreen-btn" type="button" @click="toggleFullscreen">{{ fullscreen ? "退出全屏" : "全屏显示" }}</button>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
dataMetric: {},
fullscreen: false
}
},
computed: {
dataMetricPoints() {
return [
{key: "tourCount", label: "疗休养人数", value: this.displayNumber(this.dataMetric.tourCount), unit: "人", left: "12%", top: "19%"},
{key: "honorCount", label: "劳模先进", value: this.displayNumber(this.dataMetric.honorCount), unit: "人", left: "88%", top: "19%"},
{key: "difficultCount", label: "困难人数", value: this.displayNumber(this.dataMetric.difficultCount), unit: "人", left: "12%", top: "74%"},
{key: "reimburseTotal", label: "报销总数", value: this.displayMoneyWan(this.dataMetric.reimburseTotal), unit: "万元", left: "88%", top: "74%"},
{key: "budgetTotal", label: "预算总额", value: this.displayMoneyWan(this.dataMetric.budgetTotal), unit: "万元", left: "50%", top: "50%", center: true}
]
}
},
created() {
this.loadDataMetricData()
},
mounted() {
document.addEventListener("fullscreenchange", this.syncFullscreenState)
document.addEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
beforeDestroy() {
document.removeEventListener("fullscreenchange", this.syncFullscreenState)
document.removeEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
methods: {
loadDataMetricData() {
this.$axios.post("/platform/careData/leader/dataMetricData").then(res => {
if (res.code !== 0) {
return
}
this.$set(this, "dataMetric", res.data || {})
})
},
displayNumber(value) {
return Number(value || 0)
},
displayMoneyWan(value) {
const amount = Number(value || 0) / 10000
if (amount >= 100) {
return Math.round(amount).toString()
}
if (amount >= 10) {
return amount.toFixed(1).replace(/\.0$/, "")
}
return amount.toFixed(2).replace(/\.?0+$/, "")
},
toggleFullscreen() {
if (this.fullscreen) {
const exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen
exitFullscreen && exitFullscreen.call(document)
return
}
const target = document.documentElement
const requestFullscreen = target.requestFullscreen || target.webkitRequestFullscreen
if (requestFullscreen) {
const result = requestFullscreen.call(target)
if (result && result.catch) {
result.catch(() => {})
}
}
},
syncFullscreenState() {
this.$set(this, "fullscreen", !!(document.fullscreenElement || document.webkitFullscreenElement))
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,599 @@
<!--#
layout("/layouts/platform_leader_dashboard.html"){
#-->
<style>
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #071b36;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
[v-cloak] {
display: none;
}
.leader-screen {
position: relative;
width: 100vw;
height: 100vh;
min-width: 1280px;
min-height: 720px;
overflow: hidden;
color: #eaf7ff;
background:
radial-gradient(circle at 50% 45%, rgba(255, 255, 255, 0.62) 0, rgba(135, 202, 255, 0.4) 26%, transparent 58%),
linear-gradient(135deg, #74b8f2 0%, #9acbfb 48%, #6aaee9 100%);
}
.leader-screen::before,
.leader-screen::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
}
.leader-screen::before {
border: 2px solid rgba(219, 245, 255, 0.58);
box-shadow: inset 0 0 34px rgba(24, 126, 220, 0.34);
}
.leader-screen::after {
background:
linear-gradient(rgba(255, 255, 255, 0.16) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.12) 1px, transparent 1px);
background-size: 78px 78px;
mask-image: radial-gradient(circle at 50% 50%, #000 0, transparent 76%);
}
.screen-header {
position: relative;
z-index: 2;
height: 82px;
display: flex;
align-items: flex-start;
justify-content: center;
}
.screen-header::before {
content: "";
position: absolute;
top: 0;
left: 22%;
right: 22%;
height: 72px;
border: 2px solid rgba(216, 246, 255, 0.58);
border-top: none;
border-radius: 0 0 88px 88px;
background: linear-gradient(180deg, rgba(39, 144, 255, 0.42), rgba(27, 110, 205, 0.12));
box-shadow: 0 12px 28px rgba(23, 94, 180, 0.28), inset 0 -12px 24px rgba(159, 230, 255, 0.36);
}
.screen-title {
position: relative;
z-index: 1;
margin: 0;
color: #f8fdff;
font-size: 36px;
line-height: 58px;
font-weight: 700;
text-shadow: 0 0 10px rgba(40, 140, 232, 0.9);
letter-spacing: 0;
}
.weather-box,
.time-box {
position: absolute;
top: 18px;
z-index: 3;
color: rgba(255, 255, 255, 0.94);
font-size: 14px;
line-height: 1.6;
}
.weather-box {
left: 30px;
display: flex;
align-items: center;
gap: 10px;
}
.weather-icon {
width: 28px;
height: 28px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #ffe599, #ff9d32 58%, #74c8ff 60%, #d9f4ff 100%);
box-shadow: 0 0 14px rgba(255, 207, 99, 0.45);
}
.time-box {
right: 28px;
text-align: right;
}
.screen-body {
position: relative;
z-index: 2;
height: calc(100vh - 92px);
padding: 6px 26px 22px;
display: grid;
grid-template-columns: 400px minmax(520px, 1fr) 400px;
grid-template-rows: 300px 1fr 300px;
gap: 18px 22px;
}
.panel {
position: relative;
min-width: 0;
min-height: 0;
border: 1px solid rgba(232, 249, 255, 0.62);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(77, 162, 229, 0.08));
box-shadow: inset 0 0 24px rgba(255, 255, 255, 0.18), 0 10px 28px rgba(25, 103, 183, 0.16);
overflow: hidden;
}
.panel::before,
.panel::after {
content: "";
position: absolute;
top: 15px;
width: 42px;
height: 2px;
background: #ffe078;
opacity: 0.92;
}
.panel::before {
left: 7px;
}
.panel::after {
right: 7px;
}
.panel-title {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: #f8fdff;
font-size: 17px;
font-weight: 700;
background: linear-gradient(90deg, rgba(31, 126, 238, 0.18), rgba(54, 159, 255, 0.62), rgba(31, 126, 238, 0.18));
text-shadow: 0 0 8px rgba(60, 150, 240, 0.78);
}
.proposal-panel {
grid-column: 1;
grid-row: 1;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 20px;
padding: 18px 16px 14px;
}
.metric-item {
position: relative;
height: 62px;
padding: 10px 12px 9px 70px;
border: 1px solid rgba(227, 248, 255, 0.42);
background: linear-gradient(90deg, rgba(231, 248, 255, 0.34), rgba(139, 205, 255, 0.12));
box-shadow: inset 0 -8px 12px rgba(32, 124, 218, 0.16);
}
.metric-item::after {
content: "↑";
position: absolute;
right: 24px;
bottom: 8px;
color: rgba(47, 142, 224, 0.72);
font-size: 18px;
}
.metric-icon {
position: absolute;
left: 18px;
top: 12px;
width: 38px;
height: 38px;
border-radius: 50%;
background: radial-gradient(circle, #f4fbff 0, #55c8ff 42%, rgba(34, 139, 227, 0.16) 68%);
box-shadow: 0 0 14px rgba(77, 187, 255, 0.7);
}
.metric-icon::before {
content: "";
position: absolute;
left: 11px;
right: 11px;
top: 9px;
height: 14px;
border-radius: 2px;
background: #1d97e6;
box-shadow: -7px 9px 0 -2px #1d97e6, 7px 9px 0 -2px #1d97e6;
}
.metric-label {
color: rgba(38, 74, 112, 0.86);
font-size: 14px;
line-height: 1.3;
white-space: nowrap;
}
.metric-value {
margin-top: 3px;
color: #1677d8;
font-size: 20px;
font-weight: 700;
line-height: 1.1;
}
.metric-value small {
margin-left: 3px;
color: rgba(49, 93, 132, 0.76);
font-size: 12px;
font-weight: 400;
}
.center-map {
grid-column: 2;
grid-row: 1 / 3;
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
}
.orbit {
position: absolute;
width: min(78%, 760px);
aspect-ratio: 1;
border: 1px dashed rgba(255, 255, 255, 0.6);
border-radius: 50%;
box-shadow: 0 0 60px rgba(255, 255, 255, 0.22);
}
.map-shape {
position: relative;
width: min(76%, 760px);
height: min(58%, 430px);
border-radius: 46% 54% 47% 53% / 38% 45% 55% 62%;
background:
radial-gradient(circle at 74% 33%, rgba(243, 251, 255, 0.95) 0, rgba(68, 168, 255, 0.88) 22%, transparent 23%),
radial-gradient(circle at 50% 52%, #5fb3ff 0, #2786f0 58%, #1b69cf 100%);
box-shadow: 0 18px 38px rgba(35, 115, 210, 0.36), 0 0 38px rgba(255, 255, 255, 0.6);
opacity: 0.88;
}
.map-shape::before,
.map-shape::after {
content: "";
position: absolute;
background: rgba(37, 129, 232, 0.9);
box-shadow: 0 0 22px rgba(255, 255, 255, 0.55);
}
.map-shape::before {
right: -62px;
top: 92px;
width: 120px;
height: 132px;
border-radius: 42% 58% 55% 45%;
}
.map-shape::after {
right: 42px;
bottom: -42px;
width: 55px;
height: 82px;
border-radius: 50%;
transform: rotate(18deg);
}
.center-session {
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
color: rgba(20, 76, 134, 0.8);
font-size: 15px;
font-weight: 700;
text-align: center;
}
.placeholder-list {
padding: 18px 22px;
}
.placeholder-row {
display: grid;
grid-template-columns: 34px 1fr 48px;
align-items: center;
gap: 12px;
height: 40px;
color: rgba(39, 73, 111, 0.82);
font-size: 13px;
}
.placeholder-index {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: rgba(58, 151, 236, 0.72);
font-weight: 700;
}
.placeholder-track {
height: 5px;
background: rgba(255, 255, 255, 0.38);
}
.placeholder-bar {
height: 100%;
background: linear-gradient(90deg, #41c79f, #3a96ec);
}
.right-circle {
height: calc(100% - 40px);
display: flex;
align-items: center;
justify-content: center;
}
.circle-core {
width: 128px;
height: 128px;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
background: radial-gradient(circle, #e8fbff 0, #42bbff 45%, #1f75da 100%);
box-shadow: 0 0 30px rgba(38, 135, 230, 0.46);
font-size: 14px;
}
.circle-core strong {
font-size: 34px;
line-height: 1;
}
.bottom-nav {
position: absolute;
left: 50%;
bottom: 14px;
transform: translateX(-50%);
z-index: 3;
display: flex;
gap: 28px;
}
.nav-dot {
width: 58px;
height: 58px;
border-radius: 50%;
border: 2px solid rgba(238, 251, 255, 0.72);
background: radial-gradient(circle, #ecfbff 0, #59bfff 52%, #2675d4 100%);
box-shadow: 0 0 18px rgba(32, 126, 220, 0.42);
}
@media (max-width: 1500px) {
.screen-body {
grid-template-columns: 360px minmax(480px, 1fr) 360px;
gap: 14px;
}
.metrics-grid {
gap: 12px;
padding-left: 12px;
padding-right: 12px;
}
}
</style>
<div id="app" class="leader-screen" v-cloak>
<header class="screen-header">
<div class="weather-box">
<span class="weather-icon"></span>
<span>智慧工会 · 领导驾驶舱</span>
</div>
<h1 class="screen-title">数智工会领导驾驶舱</h1>
<div class="time-box">
<div>{{ currentDate }}</div>
<div>{{ currentTime }}</div>
</div>
</header>
<main class="screen-body">
<section class="panel proposal-panel">
<div class="panel-title">提案征集与办理</div>
<div class="metrics-grid">
<div class="metric-item">
<span class="metric-icon"></span>
<div class="metric-label">提案总数</div>
<div class="metric-value">{{ overview.total || 0 }}<small></small></div>
</div>
<div class="metric-item">
<span class="metric-icon"></span>
<div class="metric-label">立案数量</div>
<div class="metric-value">{{ overview.filedCount || 0 }}<small></small></div>
</div>
<div class="metric-item">
<span class="metric-icon"></span>
<div class="metric-label">意见建议梳理</div>
<div class="metric-value">{{ overview.suggestionCount || 0 }}<small></small></div>
</div>
<div class="metric-item">
<span class="metric-icon"></span>
<div class="metric-label">不予立案</div>
<div class="metric-value">{{ overview.rejectedCount || 0 }}<small></small></div>
</div>
<div class="metric-item">
<span class="metric-icon"></span>
<div class="metric-label">办结数量</div>
<div class="metric-value">{{ overview.doneCount || 0 }}<small></small></div>
</div>
<div class="metric-item">
<span class="metric-icon"></span>
<div class="metric-label">满意率</div>
<div class="metric-value">{{ overview.satisfiedRate || '0%' }}</div>
</div>
</div>
</section>
<section class="center-map">
<div class="center-session">{{ session.fullName || '当前届次' }}</div>
<div class="orbit"></div>
<div class="map-shape"></div>
</section>
<section class="panel">
<div class="panel-title">专题指标总览</div>
<div class="right-circle">
<div class="circle-core">
<strong>{{ overview.total || 0 }}</strong>
<span>提案指标</span>
</div>
</div>
</section>
<section class="panel">
<div class="panel-title">办理进度排行</div>
<div class="placeholder-list">
<div class="placeholder-row" v-for="(item, index) in progressRows" :key="item.name">
<span class="placeholder-index">{{ pad(index + 1) }}</span>
<div>
<div>{{ item.name }}</div>
<div class="placeholder-track"><div class="placeholder-bar" :style="{width: item.rate}"></div></div>
</div>
<span>{{ item.rate }}</span>
</div>
</div>
</section>
<section class="panel">
<div class="panel-title">专题模块预留</div>
<div class="placeholder-list">
<div class="placeholder-row" v-for="(item, index) in reserveRows" :key="item">
<span class="placeholder-index">{{ pad(index + 1) }}</span>
<div>{{ item }}</div>
<span>待接入</span>
</div>
</div>
</section>
<section class="panel">
<div class="panel-title">数据趋势预留</div>
<div class="placeholder-list">
<div class="placeholder-row" v-for="item in trendRows" :key="item.name">
<span class="placeholder-index">{{ item.index }}</span>
<div>
<div>{{ item.name }}</div>
<div class="placeholder-track"><div class="placeholder-bar" :style="{width: item.rate}"></div></div>
</div>
<span>{{ item.rate }}</span>
</div>
</div>
</section>
<section class="panel">
<div class="panel-title">重点数据预留</div>
<div class="placeholder-list">
<div class="placeholder-row" v-for="item in keyRows" :key="item.name">
<span class="placeholder-index">{{ item.index }}</span>
<div>{{ item.name }}</div>
<span>{{ item.value }}</span>
</div>
</div>
</section>
</main>
<nav class="bottom-nav">
<span class="nav-dot"></span>
<span class="nav-dot"></span>
<span class="nav-dot"></span>
<span class="nav-dot"></span>
<span class="nav-dot"></span>
</nav>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
timer: null,
currentDate: "",
currentTime: "",
session: {},
overview: {},
progressRows: [
{name: "提案征集", rate: "87%"},
{name: "立案办理", rate: "82%"},
{name: "承办答复", rate: "68%"},
{name: "结果反馈", rate: "43%"},
{name: "满意评价", rate: "36%"}
],
reserveRows: ["职工关爱专题", "工会经费专题", "活动服务专题", "荣誉建设专题"],
trendRows: [
{index: "01", name: "年度提案趋势", rate: "78%"},
{index: "02", name: "办理效率趋势", rate: "64%"},
{index: "03", name: "满意度趋势", rate: "72%"}
],
keyRows: [
{index: "01", name: "专题指标标题", value: "256"},
{index: "02", name: "专题指标标题", value: "205"},
{index: "03", name: "专题指标标题", value: "123"}
]
}
},
created() {
this.updateTime()
this.$set(this, "timer", setInterval(this.updateTime, 1000))
this.loadProposalData()
},
beforeDestroy() {
clearInterval(this.timer)
},
methods: {
loadProposalData() {
this.$axios.post("/platform/careData/leader/proposalData").then(res => {
if (res.code !== 0) {
return
}
const data = res.data || {}
this.$set(this, "session", data.session || {})
this.$set(this, "overview", data.overview || {})
})
},
updateTime() {
const now = new Date()
const week = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"][now.getDay()]
this.$set(this, "currentDate", now.getFullYear() + "-" + this.pad(now.getMonth() + 1) + "-" + this.pad(now.getDate()) + " " + week)
this.$set(this, "currentTime", this.pad(now.getHours()) + ":" + this.pad(now.getMinutes()) + ":" + this.pad(now.getSeconds()))
},
pad(value) {
return String(value).padStart(2, "0")
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,142 @@
<!--#
layout("/layouts/platform_leader_dashboard.html"){
#-->
<style>
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050f1f;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
[v-cloak] {
display: none;
}
.dashboard-wrap {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #050f1f;
}
.dashboard-stage {
position: relative;
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
background: url("${base!}/assets/platform/images/careData/leader/leader-bg.jpg?v=20260524") center center / 100% 100% no-repeat;
}
.metric-value {
position: absolute;
min-width: 86px;
height: 24px;
display: flex;
align-items: center;
color: #1b83e9;
font-size: 17px;
font-weight: 700;
line-height: 1;
letter-spacing: 0;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.75);
white-space: nowrap;
}
.metric-value::before {
content: "";
position: absolute;
inset: -2px -8px;
z-index: -1;
background: rgba(189, 224, 252, 0.54);
filter: blur(3px);
}
.metric-value small {
margin-left: 3px;
color: rgba(43, 105, 159, 0.82);
font-size: 12px;
font-weight: 400;
}
.metric-total {
left: 5.8%;
top: 16.7%;
}
.metric-filed {
left: 15.9%;
top: 16.7%;
}
.metric-suggestion {
left: 5.8%;
top: 24.2%;
}
.metric-rejected {
left: 15.9%;
top: 24.2%;
}
.metric-done {
left: 5.8%;
top: 31.7%;
}
.metric-satisfied {
left: 15.9%;
top: 31.7%;
}
</style>
<div id="app" class="dashboard-wrap" v-cloak>
<main class="dashboard-stage">
<div class="metric-value metric-total">{{ displayNumber(overview.total) }}<small></small></div>
<div class="metric-value metric-filed">{{ displayNumber(overview.filedCount) }}<small></small></div>
<div class="metric-value metric-suggestion">{{ displayNumber(overview.suggestionCount) }}<small></small></div>
<div class="metric-value metric-rejected">{{ displayNumber(overview.rejectedCount) }}<small></small></div>
<div class="metric-value metric-done">{{ displayNumber(overview.doneCount) }}<small></small></div>
<div class="metric-value metric-satisfied">{{ overview.satisfiedRate || "0%" }}</div>
</main>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
overview: {}
}
},
created() {
this.loadProposalData()
},
methods: {
loadProposalData() {
this.$axios.post("/platform/careData/leader/proposalData").then(res => {
if (res.code !== 0) {
return
}
const data = res.data || {}
this.$set(this, "overview", data.overview || {})
})
},
displayNumber(value) {
return Number(value || 0)
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,260 @@
<!--#
layout("/layouts/platform_leader_dashboard.html"){
#-->
<style>
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050f1f;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
[v-cloak] {
display: none;
}
.dashboard-wrap {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #050f1f;
}
.dashboard-stage {
position: relative;
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
background: url("${base!}/assets/platform/images/careData/leader/leader-bg.jpg?v=20260524") center center / 100% 100% no-repeat;
}
.proposal-panel {
position: absolute;
left: 1.35%;
top: 8.2%;
width: 20.82%;
height: 27.65%;
background: url("${base!}/assets/platform/images/careData/leader/proposal-panel.png?v=20260526") center center / 100% 100% no-repeat;
}
.proposal-title {
position: absolute;
left: 0;
top: 1.3%;
width: 100%;
height: 10%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-family: "Microsoft YaHei", Arial, sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 1;
letter-spacing: 0;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.78);
}
.proposal-card-grid {
position: absolute;
inset: 0;
}
.proposal-card {
position: absolute;
width: 44%;
height: 19.2%;
background: var(--card-bg) center center / 100% 100% no-repeat;
}
.proposal-card-total {
left: calc(4.3% - 2px);
top: 18.5%;
}
.proposal-card-filed {
left: 51.7%;
top: 18.5%;
}
.proposal-card-total {
height: calc(25.6% + 3px);
}
.proposal-card-filed {
height: calc(25.6% + 2px);
}
.proposal-card-suggestion {
left: calc(4.3% - 1px);
top: calc(41.3% + 10px);
height: calc(19.8% + 20px);
}
.proposal-card-rejected {
left: calc(51.7% + 2px);
top: calc(41.3% + 10px);
height: calc(19.8% + 20px);
}
.proposal-card-done {
left: calc(4.3% - 1px);
top: calc(65.2% + 20px);
height: calc(19.8% + 20px);
}
.proposal-card-satisfied {
left: 51.7%;
top: calc(65.2% + 20px);
height: calc(19.8% + 20px);
}
.proposal-label {
position: absolute;
left: 38.5%;
top: 20%;
max-width: 50%;
overflow: hidden;
color: #304f70;
font-size: 12px;
line-height: 15px;
letter-spacing: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
.proposal-value {
position: absolute;
left: calc(38.5% + 23px);
top: 48%;
max-width: 50%;
overflow: hidden;
color: #1683f4;
font-size: 15px;
font-weight: 700;
line-height: 17px;
letter-spacing: 0;
text-overflow: ellipsis;
white-space: nowrap;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.85);
}
.fullscreen-btn {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 20;
height: 34px;
min-width: 86px;
padding: 0 14px;
border: 1px solid rgba(220, 246, 255, 0.72);
border-radius: 4px;
background: rgba(20, 101, 188, 0.58);
color: #f4fbff;
font-size: 13px;
line-height: 32px;
text-align: center;
cursor: pointer;
box-shadow: 0 0 14px rgba(64, 161, 255, 0.35);
user-select: none;
}
.fullscreen-btn:hover {
background: rgba(20, 126, 220, 0.72);
}
</style>
<div id="app" class="dashboard-wrap" v-cloak>
<main class="dashboard-stage">
<section class="proposal-panel">
<div class="proposal-title">&#25552;&#26696;&#24449;&#38598;&#21644;&#21150;&#29702;</div>
<div class="proposal-card-grid">
<article class="proposal-card" v-for="item in proposalMetrics" :key="item.key" :class="'proposal-card-' + item.key" :style="{ '--card-bg': 'url(' + item.bg + ')' }">
<div class="proposal-label">{{ item.label }}</div>
<div class="proposal-value">{{ item.value }}</div>
</article>
</div>
</section>
</main>
<button class="fullscreen-btn" type="button" @click="toggleFullscreen">{{ fullscreen ? "退出全屏" : "全屏显示" }}</button>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
overview: {},
fullscreen: false
}
},
computed: {
proposalMetrics() {
const imagePath = "${base!}/assets/platform/images/careData/leader/"
return [
{key: "total", label: "提案总数", value: this.displayNumber(this.overview.total), bg: imagePath + "proposal-card-total.png?v=20260526"},
{key: "filed", label: "立案数量", value: this.displayNumber(this.overview.filedCount), bg: imagePath + "proposal-card-filed.png?v=20260526"},
{key: "suggestion", label: "意见建议", value: this.displayNumber(this.overview.suggestionCount), bg: imagePath + "proposal-card-suggestion.png?v=20260526"},
{key: "rejected", label: "不予立案", value: this.displayNumber(this.overview.rejectedCount), bg: imagePath + "proposal-card-rejected.png?v=20260526"},
{key: "done", label: "办结数量", value: this.displayNumber(this.overview.doneCount), bg: imagePath + "proposal-card-done.png?v=20260526"},
{key: "satisfied", label: "满意率", value: this.overview.satisfiedRate || "0%", bg: imagePath + "proposal-card-satisfied.png?v=20260526"}
]
},
},
created() {
this.loadProposalData()
},
mounted() {
document.addEventListener("fullscreenchange", this.syncFullscreenState)
document.addEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
beforeDestroy() {
document.removeEventListener("fullscreenchange", this.syncFullscreenState)
document.removeEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
methods: {
loadProposalData() {
this.$axios.post("/platform/careData/leader/proposalData").then(res => {
if (res.code !== 0) {
return
}
const data = res.data || {}
this.$set(this, "overview", data.overview || {})
})
},
displayNumber(value) {
return Number(value || 0)
},
toggleFullscreen() {
if (this.fullscreen) {
const exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen
exitFullscreen && exitFullscreen.call(document)
return
}
const target = document.documentElement
const requestFullscreen = target.requestFullscreen || target.webkitRequestFullscreen
if (requestFullscreen) {
const result = requestFullscreen.call(target)
if (result && result.catch) {
result.catch(() => {})
}
}
},
syncFullscreenState() {
this.$set(this, "fullscreen", !!(document.fullscreenElement || document.webkitFullscreenElement))
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,173 @@
<!--#
layout("/layouts/platform_leader_dashboard.html"){
#-->
<style>
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050f1f;
font-family: "Microsoft YaHei", "Microsoft JhengHei", Arial, sans-serif;
}
[v-cloak] {
display: none;
}
.dashboard-wrap {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #050f1f;
}
.dashboard-stage {
position: relative;
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
background: url("${base!}/assets/platform/images/careData/leader/leader-bg.jpg?v=20260524") center center / 100% 100% no-repeat;
}
.staff-home-panel {
position: absolute;
left: 24.1%;
top: 22.5%;
width: 53.1%;
height: 63.5%;
overflow: hidden;
}
.staff-home-title {
position: absolute;
left: 50%;
top: 0;
z-index: 2;
min-width: 168px;
height: 30px;
padding: 0 28px;
transform: translateX(-50%);
background: linear-gradient(90deg, rgba(78, 161, 246, 0.08), rgba(79, 169, 255, 0.88), rgba(78, 161, 246, 0.08));
border-top: 1px solid rgba(214, 241, 255, 0.7);
border-bottom: 1px solid rgba(83, 173, 255, 0.48);
color: #fff;
font-size: 16px;
font-weight: 700;
line-height: 28px;
text-align: center;
letter-spacing: 0;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.85), 0 0 12px rgba(57, 151, 255, 0.72);
white-space: nowrap;
}
.staff-home-map-wrap {
position: absolute;
left: 0;
right: 0;
top: 30px;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at center, rgba(181, 223, 255, 0.3), rgba(54, 145, 235, 0.08) 56%, rgba(54, 145, 235, 0));
}
.staff-home-map {
display: block;
position: absolute;
left: -40px;
top: -100px;
width: calc(100% + 100px);
height: calc(100% + 100px);
max-width: none;
max-height: none;
object-fit: fill;
filter: drop-shadow(0 0 18px rgba(91, 177, 255, 0.52));
}
.fullscreen-btn {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 20;
height: 34px;
min-width: 86px;
padding: 0 14px;
border: 1px solid rgba(220, 246, 255, 0.72);
border-radius: 4px;
background: rgba(20, 101, 188, 0.58);
color: #f4fbff;
font-size: 13px;
line-height: 32px;
text-align: center;
cursor: pointer;
box-shadow: 0 0 14px rgba(64, 161, 255, 0.35);
user-select: none;
}
.fullscreen-btn:hover {
background: rgba(20, 126, 220, 0.72);
}
</style>
<div id="app" class="dashboard-wrap" v-cloak>
<main class="dashboard-stage">
<section class="staff-home-panel">
<div class="staff-home-title">&#32844;&#24037;&#23567;&#23478;</div>
<div class="staff-home-map-wrap">
<img class="staff-home-map" src="${base!}/assets/platform/images/careData/leader/staff-home-campus-map.png?v=20260527" alt="">
</div>
</section>
</main>
<button class="fullscreen-btn" type="button" @click="toggleFullscreen">{{ fullscreen ? "退出全屏" : "全屏显示" }}</button>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
fullscreen: false
}
},
mounted() {
document.addEventListener("fullscreenchange", this.syncFullscreenState)
document.addEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
beforeDestroy() {
document.removeEventListener("fullscreenchange", this.syncFullscreenState)
document.removeEventListener("webkitfullscreenchange", this.syncFullscreenState)
},
methods: {
toggleFullscreen() {
if (this.fullscreen) {
const exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen
exitFullscreen && exitFullscreen.call(document)
return
}
const target = document.documentElement
const requestFullscreen = target.requestFullscreen || target.webkitRequestFullscreen
if (requestFullscreen) {
const result = requestFullscreen.call(target)
if (result && result.catch) {
result.catch(() => {})
}
}
},
syncFullscreenState() {
this.$set(this, "fullscreen", !!(document.fullscreenElement || document.webkitFullscreenElement))
}
}
})
</script>
<!--#
}
#-->
@@ -67,7 +67,7 @@ layout("/layouts/platform.html"){
<el-table-column label="所属工会" prop="unionName" sortable></el-table-column>
<el-table-column label="所属单位" prop="unitName" sortable></el-table-column>
<el-table-column label="联系方式" prop="mobile" sortable></el-table-column>
<el-table-column label="答题日期" prop="attemptDate" sortable></el-table-column>
<el-table-column label="答题时间" prop="submitTime" sortable></el-table-column>
<el-table-column label="得分" prop="totalScore" sortable></el-table-column>
<el-table-column label="操作" fixed="right" width="100px" v-if="$auth.hasPermission('qsv.quiz.rank.delete')">
<template slot-scope="{row}">
@@ -0,0 +1,154 @@
const COMMON_QUERY = {
template: [
'',
' <div class="search">',
' <div class="search-item">',
' <div class="search-item-label">姓名/工号</div>',
' <div class="search-item-option">',
' <el-input placeholder="请输入姓名或工号查询" clearable v-model="pageForm.searchKeyword"></el-input>',
' </div>',
' </div>',
' <div class="search-item">',
' <div class="search-item-label">所属工会</div>',
' <div class="search-item-option">',
' <el-select @change="flushUnits" @clear="flushUnits" clearable filterable',
' placeholder="请选择所属工会" style="width: 100%;" v-model="pageForm.unionId">',
' <el-option :key="item.id" :label="item.unionname" :value="item.id"',
' v-for="item in unions"></el-option>',
' </el-select>',
' </div>',
' </div>',
' <div class="search-item">',
' <div class="search-item-label">所属单位</div>',
' <div class="search-item-option">',
' <el-select clearable filterable placeholder="请选择所属单位" style="width: 100%"',
' v-model="pageForm.unitId">',
' <el-option :key="item.id" :label="item.name" :value="item.id"',
' v-for="item in units"></el-option>',
' </el-select>',
' </div>',
' </div>',
' <div class="search-item">',
' <div class="search-item-label">在职状态</div>',
' <div class="search-item-option">',
' <dict-select v-model="pageForm.userStates" style="width: 100%" placeholder="在职状态" @change="doSearch"',
' code="USER_STATE" multiple></dict-select>',
' </div>',
' </div>',
' <div class="search-item">',
' <div class="search-item-label">人员类型</div>',
' <div class="search-item-option">',
' <dict-select v-model="pageForm.personTypes" style="width: 100%" placeholder="人员类型" @change="doSearch"',
' code="PERSON_TYPE" multiple></dict-select>',
' </div>',
' </div>',
' <div class="search-item">',
' <div class="search-item-label">人员性质</div>',
' <div class="search-item-option">',
' <dict-select v-model="pageForm.preparedBys" style="width: 100%" placeholder="人员性质" @change="doSearch"',
' code="PREPARED_BY" multiple></dict-select>',
' </div>',
' </div>',
' <div class="search-item">',
' <div class="search-item-label">生日日期</div>',
' <div class="search-item-option">',
' <el-date-picker :picker-options="pickerOptions" @change="changeDateRangeChange"',
' align="right" end-placeholder="结束日期" format="yyyy-MM-dd"',
' range-separator="-" start-placeholder="开始日期" style="width: 100%"',
' type="datetimerange" v-model="pageForm.changeDateRange"',
' value-format="yyyy-MM-dd"></el-date-picker>',
' </div>',
' </div>',
' <div class="search-query">',
' <el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>',
' </div>',
' </div>',
' '
].join(''),
data() {
return {
pickerOptions: {
shortcuts: [
{
text: "未来一周",
onClick(picker) {
const end = new Date()
const start = new Date()
end.setTime(start.getTime() + 3600 * 1000 * 24 * 7)
picker.$emit("pick", [start, end])
}
},
{
text: "未来一个月",
onClick(picker) {
const end = new Date()
const start = new Date()
end.setTime(start.getTime() + 3600 * 1000 * 24 * 30)
picker.$emit("pick", [start, end])
}
},
{
text: "未来三个月",
onClick(picker) {
const end = new Date()
const start = new Date()
end.setTime(start.getTime() + 3600 * 1000 * 24 * 90)
picker.$emit("pick", [start, end])
}
}
]
},
unions: [],
units: [],
pageForm: {
searchKeyword: '',
unionId: '',
unitId: '',
userStates: [],
personTypes: [],
preparedBys: [],
changeDateRange: [],
}
}
},
methods: {
doSearch(){
const pageForm = clone(this.pageForm)
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
this.$emit('search', pageForm)
},
changeDateRangeChange(val){
if (val && val.length > 0) {
this.pageForm.startDate = val[0]
this.pageForm.endDate = val[1]
} else {
this.pageForm.startDate = null
this.pageForm.endDate = null
}
this.doSearch()
},
async initData(){
if (this.$auth.hasRoleOr('sysadmin, SchoolUnionMemberAdmin, SchoolUnionAdmin')) {
this.unions = await getUnions(this.pageForm.unionId)
this.units = await getUnits()
} else {
const user = JSON.parse(window.sessionStorage.getItem('user'))
this.unions = await getUnions(user.union.id)
this.units = await getUnits(user.union.id)
}
},
async flushUnits(){
this.$set(this.pageForm, "unitId", null)
this.units = []
if (this.pageForm.unionId) {
this.units = await getUnits(this.pageForm.unionId)
}
}
},
async created() {
this.initData()
}
}
@@ -0,0 +1,296 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<common-query ref="commonQueryRef" @search="search"></common-query>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="数据列表(默认展示近15天的数据,搜索会忽略年份)">
<el-button type="primary" size="small" @click="doExport">导出Excel</el-button>
<el-button type="primary" size="small" @click="openSendMsgByQuery">发送通知</el-button>
<el-button type="primary" size="small" @click="openPicSet">配置图片</el-button>
</table-tool>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder" ref="table"
row-key="id" style="width: 100%">
<el-table-column :index="indexMethod"
header-align="center"
label="序号" type="index" width="80px"></el-table-column>
<el-table-column :label="column.label"
:prop="column.prop"
:sortable="column.sortable"
:width="column.width"
header-align="center"
min-width="100px"
show-overflow-tooltip
v-for="column in tableColumns">
</el-table-column>
<el-table-column fixed="right" label="操作" width="250px">
<template slot-scope="{row}">
<el-button @click="openView(row)" size="mini" type="primary">人员信息</el-button>
<el-button @click="openSendMsgByUser(row)" size="mini" type="primary">发送通知</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #view>
<member-info ref="memberInfoRef"></member-info>
</template>
</guava>
<el-dialog
:close-on-click-modal="false"
:visible.sync="sendMsgByQueryDialogVisible"
title="请在下方填写需要发送的内容"
width="50%"
>
<el-form :model="sendMsgByQueryFormData" label-width="80px" ref="sendMsgByQueryForm">
<el-form-item
:rules="[{required: true, message: '请输入发送标题', trigger: ['blur', 'change']}]"
label="发送标题"
prop="title">
<el-input
:rows="6"
placeholder="请输入发送标题"
v-model="sendMsgByQueryFormData.title">
</el-input>
</el-form-item>
<el-form-item
:rules="[{required: true, message: '请选择发送内容', trigger: ['blur', 'change']}]"
label="发送内容"
prop="content">
<el-input
:rows="6"
placeholder="请输入内容"
type="textarea"
v-model="sendMsgByQueryFormData.content">
</el-input>
</el-form-item>
</el-form>
<span class="dialog-footer" slot="footer">
<el-button @click="sendMsgByQueryDialogVisible = false">取 消</el-button>
<el-button @click="sendMsgByQueryUsers" type="primary">确 定</el-button>
</span>
</el-dialog>
<!-- 根据行内数据发送消息 -->
<el-dialog
:close-on-click-modal="false"
:visible.sync="sendMsgByUserDialogVisible"
title="请在下方填写需要发送的内容"
width="50%"
>
<el-form :model="sendMsgByUserFormData" label-width="80px" ref="sendForm">
<el-form-item label="发送对象" prop="userInfo">
<el-input placeholder="请输入发送对象" readonly v-model="sendMsgByUserFormData.userInfo"></el-input>
</el-form-item>
<el-form-item
:rules="[{required: true, message: '请输入发送标题', trigger: ['blur', 'change']}]"
label="发送标题"
prop="title">
<el-input
placeholder="请输入发送标题"
v-model="sendMsgByUserFormData.title">
</el-input>
</el-form-item>
<el-form-item
:rules="[{required: true, message: '请选择发送内容', trigger: ['blur', 'change']}]"
label="发送内容"
prop="content">
<el-input
:rows="6"
placeholder="请输入内容"
type="textarea"
v-model="sendMsgByUserFormData.content">
</el-input>
</el-form-item>
</el-form>
<span class="dialog-footer" slot="footer">
<el-button @click="sendMsgByUserDialogVisible = false">取 消</el-button>
<el-button @click="sendMsgByUser" type="primary">确 定</el-button>
</span>
</el-dialog>
<el-dialog
:close-on-click-modal="false"
:visible.sync="dialogVisible"
title="生日贺卡图片设置"
width="50%"
>
<el-form :model="formData" label-width="80px" ref="formRef">
<el-form-item label="背景图片" prop="picUrl">
<file-upload :upload_number="1"
:upload_size="1024 * 1024 * 5"
:value.sync="formData.picUrl"
accept=".jpg,.jpeg,.png"
upload_mode="image"
upload_result_category="interval"
upload_result_type="id"></file-upload>
</el-form-item>
<el-form-item label="贺卡图片" prop="birthdayUrl">
<file-upload :upload_number="1"
:upload_size="1024 * 1024 * 5"
:value.sync="formData.birthdayUrl"
accept=".jpg,.jpeg,.png"
upload_mode="image"
upload_result_category="interval"
upload_result_type="id"></file-upload>
</el-form-item>
</el-form>
<span class="dialog-footer" slot="footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button @click="doSubmit" type="primary">确 定</el-button>
</span>
</el-dialog>
</div>
<script>
<!--#include("commonQuery.js"){}#-->
<!--#include("/platform/zhgh/staffmanage/member/common/info/memberInfo.js"){}#-->
new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
return {
pageForm: {},
tableColumns: [
{ prop: "loginname", label: "工号", sortable: true },
{ prop: "username", label: "姓名", sortable: true },
{ prop: "sex", label: "性别", sortable: true },
{ prop: "mobile", label: "联系方式", sortable: true },
{ prop: "birthday", label: "出生年月", sortable: true },
{ prop: "daysUntilBirthday", label: "生日倒计时(天)", sortable: true },
{ prop: "userState", label: "在职状态", sortable: true, width: "100px" },
{ prop: "personType", label: "人员类型", sortable: true, width: "120px" },
{ prop: "preparedBy", label: "人员性质", sortable: true, width: "120px" },
{ prop: "unionName", label: "所属工会", sortable: true, width: "160px" },
{ prop: "unitName", label: "所属单位", sortable: true, width: "160px" }
],
// 根据查询条件推送消息相关
sendMsgByQueryDialogVisible: false,
sendMsgByQueryFormData: {},
// 根据行内发送消息相关
sendMsgByUserDialogVisible: false,
sendMsgByUserFormData: {},
// 图片配置
dialogVisible: false,
formData: {
picUrl: '',
birthdayUrl: ''
}
}
},
components: {
'common-query': COMMON_QUERY,
'member-info': MEMBER_INFO,
},
methods: {
async openPicSet() {
const resp = await $.post('/platform/staffManage/birthday/manage/getConfig')
if (resp.code === 0) {
this.formData = resp.data
}
this.dialogVisible = true
},
doSubmit() {
this.$confirm('确定要提交吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const resp = await $.post('/platform/staffManage/birthday/manage/saveOrModifyConfig', this.formData)
if (resp.code === 0) {
this.$message.success('保存成功')
this.dialogVisible = false
} else {
this.$message.error(resp.msg)
}
})
},
openSendMsgByUser(row) {
this.sendMsgByUserFormData.userId = row.id
this.sendMsgByUserFormData.userInfo = row.username
this.sendMsgByUserDialogVisible = true
},
async sendMsgByUser() {
const valid = await this.$refs['sendForm'].validate()
if (!valid) return
const confirm = await this.$confirm('您确定要向' + this.sendMsgByUserFormData.userInfo + '数据发送信息吗?', '提示', {
confirmButtonText: '确定',
cancelButtonTest: '取消',
type: 'warning'
})
if (confirm === "confirm") {
const resp = await $.post('/platform/staffManage/birthday/manage/sendMsgByUser',
this.sendMsgByUserFormData
)
if (resp.code === 0) {
this.sendMsgByUserDialogVisible = false
this.$message.success(resp.msg)
} else {
this.$message.warning(resp.msg)
}
}
},
openSendMsgByQuery() {
this.sendMsgByQueryFormData = JSON.parse(JSON.stringify(this.pageForm))
this.sendMsgByQueryDialogVisible = true
},
async sendMsgByQueryUsers() {
const valid = await this.$refs['sendMsgByQueryForm'].validate()
if (!valid) return
const confirm = await this.$confirm('您确定根据查询条件发送信息吗?', '提示', {
confirmButtonText: '确定',
cancelButtonTest: '取消',
type: 'warning'
})
if (confirm === "confirm") {
const resp = await $.post('/platform/staffManage/birthday/manage/sendMsgByQueryUsers', this.sendMsgByQueryFormData)
if (resp.code === 0) {
this.sendMsgByQueryDialogVisible = false
this.$message.success(resp.msg)
} else {
this.$message.warning(resp.msg)
}
}
},
doExport() {
this.$downLoad("/platform/staffManage/birthday/manage/doExport", this.pageForm)
},
openView(row) {
this.$refs.guava.view()
this.$nextTick(() => {
this.$refs.memberInfoRef.onOpen(row.id)
})
},
search(pageForm){
this.pageForm = { ...this.pageForm, ...pageForm }
this.pageData()
},
},
async created() {
this.pageData()
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,74 @@
const COMMON_QUERY = {
template: [
'',
' <div class="search">',
' <div class="search-item">',
' <div class="search-item-label">姓名/工号</div>',
' <div class="search-item-option">',
' <el-input placeholder="请输入姓名或工号查询" clearable v-model="pageForm.searchKeyword"></el-input>',
' </div>',
' </div>',
' <div class="search-item">',
' <div class="search-item-label">所属工会</div>',
' <div class="search-item-option">',
' <el-select @change="flushUnits" @clear="flushUnits" clearable filterable',
' placeholder="请选择所属工会" style="width: 100%;" v-model="pageForm.unionId">',
' <el-option :key="item.id" :label="item.unionname" :value="item.id"',
' v-for="item in unions"></el-option>',
' </el-select>',
' </div>',
' </div>',
' <div class="search-item">',
' <div class="search-item-label">所属单位</div>',
' <div class="search-item-option">',
' <el-select clearable filterable placeholder="请选择所属单位" style="width: 100%"',
' v-model="pageForm.unitId">',
' <el-option :key="item.id" :label="item.name" :value="item.id"',
' v-for="item in units"></el-option>',
' </el-select>',
' </div>',
' </div>',
' <div class="search-query">',
' <el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>',
' </div>',
' </div>',
' '
].join(''),
data() {
return {
unions: [],
units: [],
pageForm: {
searchKeyword: '',
unionId: '',
unitId: '',
}
}
},
methods: {
doSearch(){
this.$emit('search', this.pageForm)
},
async initData(){
if (this.$auth.hasRoleOr('sysadmin, SchoolUnionMemberAdmin, SchoolUnionAdmin')) {
this.unions = await getUnions(this.pageForm.unionId)
this.units = await getUnits()
} else {
const user = JSON.parse(window.sessionStorage.getItem('user'))
this.unions = await getUnions(user.union.id)
this.units = await getUnits(user.union.id)
}
},
async flushUnits(){
this.$set(this.pageForm, "unitId", null)
this.units = []
if (this.pageForm.unionId) {
this.units = await getUnits(this.pageForm.unionId)
}
}
},
async created() {
this.initData()
}
}

Some files were not shown because too many files have changed in this diff Show More