This commit is contained in:
=
2026-06-27 17:25:09 +08:00
parent 830aa06b2f
commit a9a118cd3e
68 changed files with 7609 additions and 119 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>
@@ -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());
}
}
@@ -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();
}
}
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

@@ -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>
@@ -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>
<!--#
}
#-->
@@ -0,0 +1,850 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar title="活动报销申请" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-form ref="formRef" class="form-container">
<van-cell-group title="申请人信息" class="form-section">
<van-field label="申请人姓名" v-model="formData.userName" readonly required :rules="[{ required: true }]"></van-field>
<van-field label="申请人工号" v-model="formData.loginName" readonly required :rules="[{ required: true }]"></van-field>
<van-field label="所属分工会" v-model="formData.unionName" readonly></van-field>
</van-cell-group>
<van-cell-group title="报销信息" class="form-section">
<van-field label="报销身份" v-model="formData.reimbursementIdentityName" placeholder="请选择报销身份"
readonly is-link required :rules="[{ required: true }]" :disabled="!!taskId"
@click="openIdentityPicker"></van-field>
<van-popup v-model="showIdentityPicker" position="bottom" round>
<van-picker show-toolbar :columns="reimbursementIdentityOption.map(function(i){return i.name})"
@confirm="onIdentityConfirm" @cancel="showIdentityPicker = false"></van-picker>
</van-popup>
<van-field label="经费类型" v-model="formData.outlayManageSourceName" placeholder="请选择经费类型"
readonly is-link required :rules="[{ required: true }]" @click="showBudgetTypePicker = true"></van-field>
<van-popup v-model="showBudgetTypePicker" position="bottom" round>
<van-picker show-toolbar :columns="budgetTypeOption.map(function(i){return i.name})"
@confirm="onBudgetTypeConfirm" @cancel="showBudgetTypePicker = false"></van-picker>
</van-popup>
<van-field label="报销模式" v-model="formData.activityReimbursementModeName" placeholder="请选择报销模式"
readonly is-link required :rules="[{ required: true }]" :disabled="isDailyReimbursementFixed"
@click="openModePicker"></van-field>
<van-popup v-model="showModePicker" position="bottom" round>
<van-picker show-toolbar :columns="modeOption.map(function(i){return i.name})"
@confirm="onModeConfirm" @cancel="showModePicker = false"></van-picker>
</van-popup>
<van-field label="所属协会" v-model="formData.clubName" placeholder="请选择协会"
v-if="showClubField" readonly is-link required :rules="[{ required: true }]"
@click="showClubPicker = true"></van-field>
<van-popup v-model="showClubPicker" position="bottom" round>
<van-picker show-toolbar :columns="clubOption.map(function(i){return i.clubName})"
@confirm="onClubConfirm" @cancel="showClubPicker = false"></van-picker>
</van-popup>
<van-field label="项目名称" v-model="formData.activityName" placeholder="请选择活动"
v-if="formData.activityReimbursementMode === 'activity'" readonly is-link required
:rules="[{ required: true }]" @click="openActivityPicker"></van-field>
<van-popup v-model="showActivityPicker" position="bottom" round>
<van-picker show-toolbar :columns="activityOptions.map(function(i){return i.activityName + '(' + i.declareUnitName + ')'})"
@confirm="onActivityConfirm" @cancel="showActivityPicker = false"></van-picker>
</van-popup>
<div class="inline-action" v-if="formData.activityReimbursementMode === 'activity' && formData.declareId">
<van-button native-type="button" block plain type="info" @click="viewDeclare">查看申报信息</van-button>
</div>
<van-field label="项目名称" v-model="formData.activityName" placeholder="请输入项目名称"
v-if="formData.activityReimbursementMode !== 'activity'" required
:rules="[{ required: true }]"></van-field>
<van-field label="扣款预算" v-model="formData.deductionBudgetName" placeholder="请选择扣款预算"
readonly is-link required :rules="[{ required: true }]" @click="showBudgetPicker = true"></van-field>
<van-popup v-model="showBudgetPicker" position="bottom" round>
<van-picker show-toolbar :columns="budgetColumns"
@confirm="onBudgetConfirm" @cancel="showBudgetPicker = false"></van-picker>
</van-popup>
<template v-if="formData.activityReimbursementMode === 'activity'">
<van-field label="实际开始时间" v-model="formData.startTime" placeholder="请选择实际开始时间"
readonly is-link required :rules="[{ required: true }]" @click="openDatePicker('startTime')"></van-field>
<van-field label="实际结束时间" v-model="formData.endTime" placeholder="请选择实际结束时间"
readonly is-link required :rules="[{ required: true }]" @click="openDatePicker('endTime')"></van-field>
<van-field label="活动计划时间" name="planDate" :value="planDateText" placeholder="请选择带有活动计划时间的项目"
readonly required :rules="[{ required: true, message: '请选择带有活动计划时间的项目' }]"></van-field>
</template>
</van-cell-group>
<van-popup v-model="showDatePicker" position="bottom" round>
<van-datetime-picker v-model="datePickerValue" type="date" title="请选择日期"
@confirm="onDateConfirm" @cancel="showDatePicker = false"></van-datetime-picker>
</van-popup>
<van-cell-group class="form-section">
<template #title>
<div class="section-title-row">
<span>费用</span>
<div>
<van-tag size="large" type="primary" color="#ff3b30"
v-if="formData.budgets && formData.budgets.length > 1"
@click="removeActiveBudget">删除费用</van-tag>
<van-tag size="large" type="primary" color="#1867b0" @click="addBudget">添加费用</van-tag>
</div>
</div>
</template>
<van-tabs v-model="activeBudgetIndex" type="card" color="#0e78c5" animated
v-if="formData.budgets && formData.budgets.length > 0">
<van-tab v-for="(item,index) in formData.budgets" :key="index" :name="index + ''" :title="'费用' + (index + 1)">
<div class="fee-card">
<van-field label="费用项目名称" v-model="item.name" placeholder="请输入费用项目名称"
required :rules="[{ required: true }]"></van-field>
<van-field label="预算费用" v-model="item.budgetPrice" placeholder="请输入预算费用"
type="number" v-if="formData.activityReimbursementMode === 'activity'"
required :rules="[{ required: true }]"></van-field>
<van-field label="实际费用" v-model="item.actualPrice" placeholder="请输入实际费用"
type="number" required :rules="[{ required: true }]"></van-field>
<van-field label="收款人" v-model="item.username" placeholder="请输入收款人"
required :rules="[{ required: true }]" @input="getCardNumberByPayeeId(item)"></van-field>
<van-field label="支行名称" v-model="item.bankName" placeholder="请输入支行名称"
required :rules="[{ required: true }]"></van-field>
<van-field label="报销卡号" v-model="item.bankCardNum" placeholder="请输入报销卡号"
type="number" required :rules="[{ required: true }]"></van-field>
<van-field label="备注" v-model="item.remark" placeholder="请输入备注"></van-field>
</div>
</van-tab>
</van-tabs>
</van-cell-group>
<van-cell-group title="附件" class="form-section">
<van-field class="direction-column-field" name="billFiles" label="发票等">
<template #input>
<h5-file-upload slot="input" :value.sync="formData.billFiles" :upload_number="20"
upload_result_category="array" complete_result></h5-file-upload>
</template>
</van-field>
<template v-if="formData.activityReimbursementMode === 'activity'">
<van-field class="direction-column-field" name="photoFiles" label="活动报道照片/总结/链接">
<template #input>
<h5-file-upload slot="input" :value.sync="formData.photoFiles" :upload_number="20"
upload_result_category="array" complete_result></h5-file-upload>
</template>
</van-field>
<van-field class="direction-column-field" name="otherFiles" label="奖品/纪念品/服装/道具等活动人员名单">
<template #input>
<h5-file-upload slot="input" :value.sync="formData.otherFiles" :upload_number="20"
upload_result_category="array" complete_result></h5-file-upload>
</template>
</van-field>
</template>
</van-cell-group>
<van-cell-group title="签字" class="form-section">
<van-field class="direction-column-field" name="sign" label="" v-model="formData.sign" required
:rules="[{ required: true, message: '请签字' }]">
<template #input>
<h5-signature v-model="formData.sign" slot="input"></h5-signature>
</template>
</van-field>
</van-cell-group>
<div class="form-actions">
<van-button native-type="button" round type="info" plain @click="onSave">保存</van-button>
<van-button round type="info" @click="onSubmit" v-if="!taskId">提交</van-button>
<van-button round type="info" @click="onFinishTask" v-else>提交</van-button>
</div>
</van-form>
<van-action-sheet v-model="declareDialogVisible" title="申报信息">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="活动名称">{{ declareViewData.activityName || '暂无' }}</van-cell>
<van-cell title="活动类型">{{ getActivityTypeName(declareViewData.activityType) || '暂无' }}</van-cell>
<van-cell title="申请人">{{ formatUser(declareViewData.userName, declareViewData.loginName) }}</van-cell>
<van-cell :title="declareViewData.activityType === 'CLUB' ? '申请协会' : '申请单位'">{{ declareViewData.declareUnitName || '暂无' }}</van-cell>
<van-cell title="联系方式">{{ declareViewData.mobile || '暂无' }}</van-cell>
<van-cell title="申请时间">{{ formatDate(declareViewData.applyTime) }}</van-cell>
<van-cell title="活动计划时间">{{ formatRange(declareViewData.planStartTime, declareViewData.planEndTime) }}</van-cell>
<van-cell title="活动人数">{{ declareViewData.activityNumber || '暂无' }}</van-cell>
<van-cell class="direction-column-cell" title="活动地址">{{ declareViewData.activityAddress || '暂无' }}</van-cell>
</van-cell-group>
<van-cell-group title="活动预算费用">
<van-cell v-for="(item,index) in declareViewData.budgets" :key="index" class="direction-column-cell" :title="'费用' + (index + 1)">
<div class="fee-row">费用项目名称:{{ item.name || '暂无' }}</div>
<div class="fee-row">预算费用:{{ item.budgetPrice || 0 }}</div>
</van-cell>
<van-empty v-if="!declareViewData.budgets || declareViewData.budgets.length === 0" image="/assets/platform/plugins/vant-green/images/nodata/nodata.png" description="暂无数据"></van-empty>
</van-cell-group>
<van-cell-group title="活动方案及附件">
<van-cell class="direction-column-cell" title="活动方案及简介">
<div v-html="declareViewData.activityContent || '暂无'"></div>
</van-cell>
<van-cell class="direction-column-cell" title="附件">
<file-preview v-if="declareViewData.files && declareViewData.files.length > 0" :files="declareViewData.files" complete_result></file-preview>
<span v-else>暂无</span>
</van-cell>
<van-cell class="direction-column-cell" title="签字">
<van-image v-if="declareViewData.sign" :src="declareViewData.sign" class="signature-image"></van-image>
<span v-else>暂无</span>
</van-cell>
</van-cell-group>
</div>
</van-action-sheet>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: '#app',
store,
dicts: ['ACTIVITY_BUDGET_TYPE'],
data() {
return {
bizId: GetQueryString('bizId'),
taskId: GetQueryString('taskId'),
formData: {
id: GetQueryString('businessId'),
budgets: [],
billFiles: [],
photoFiles: [],
otherFiles: []
},
activeBudgetIndex: '0',
reimbursementIdentityOption: [],
budgetTypeOption: [],
modeOption: [
{name: '活动报销', code: 'activity'},
{name: '日常报销', code: 'daily'}
],
clubOption: [],
activityOptions: [],
budgetOption: [],
showIdentityPicker: false,
showBudgetTypePicker: false,
showModePicker: false,
showClubPicker: false,
showActivityPicker: false,
showBudgetPicker: false,
showDatePicker: false,
declareDialogVisible: false,
declareViewData: {},
activityDeclareReimbursementList: [],
datePickerValue: new Date(),
dateField: ''
}
},
computed: {
showClubField() {
return this.formData.outlayManageSource === 'ACTIVITY_BUDGET_TYPE_THREE' || this.formData.reimbursementIdentity === 'club'
},
isDailyReimbursementFixed() {
return !this.hasSchoolLevelRole() && this.formData.outlayManageSource === 'ACTIVITY_BUDGET_TYPE_ONE'
},
filteredBudgetOption() {
if (this.hasSchoolLevelRole()) {
return this.budgetOption
}
if (this.formData.outlayManageSource === 'ACTIVITY_BUDGET_TYPE_ONE') {
if (this.hasUnionRole() || this.hasClubRole()) {
return this.budgetOption.filter((item) => {
if (this.hasUnionRole() && this.hasClubRole()) {
return item.isUnionUse === true || item.isClubUse === true
}
if (this.hasUnionRole()) {
return item.isUnionUse === true
}
if (this.hasClubRole()) {
return item.isClubUse === true
}
return true
})
}
return []
}
return this.budgetOption
},
budgetColumns() {
return this.filteredBudgetOption.map((item) => {
return this.getBudgetLabel(item)
})
},
planDateText() {
if (this.formData.planStartTime && this.formData.planEndTime) {
return this.$moment(this.formData.planStartTime).format('YYYY-MM-DD') + ' ~ ' + this.$moment(this.formData.planEndTime).format('YYYY-MM-DD')
}
return ''
}
},
methods: {
hasUnionRole() {
return this.$auth.hasPermission('activityReimbursement.apply.unionHas')
},
hasClubRole() {
return this.$auth.hasPermission('activityReimbursement.apply.clubHas')
},
hasSchoolLevelRole() {
return this.$auth.hasPermission('activityReimbursement.apply.schoolHas')
},
openIdentityPicker() {
if (!this.taskId) {
this.showIdentityPicker = true
}
},
openModePicker() {
if (!this.isDailyReimbursementFixed) {
this.showModePicker = true
}
},
openActivityPicker() {
if (!this.formData.outlayManageSource) {
this.$toast.fail('请先选择经费类型')
return
}
if (this.showClubField && !this.formData.clubId) {
this.$toast.fail('请先选择所属协会')
return
}
this.showActivityPicker = true
},
onIdentityConfirm(value, index) {
const item = this.reimbursementIdentityOption[index]
this.$set(this.formData, 'reimbursementIdentity', item.code)
this.$set(this.formData, 'reimbursementIdentityName', item.name)
this.reimbursementIdentityChange(item.code)
this.showIdentityPicker = false
},
onBudgetTypeConfirm(value, index) {
const item = this.budgetTypeOption[index]
this.$set(this.formData, 'outlayManageSource', item.code)
this.$set(this.formData, 'outlayManageSourceName', item.name)
this.outlayManageSourceChange(item.code)
this.showBudgetTypePicker = false
},
onModeConfirm(value, index) {
const item = this.modeOption[index]
this.$set(this.formData, 'activityReimbursementMode', item.code)
this.$set(this.formData, 'activityReimbursementModeName', item.name)
if (item.code === 'daily') {
this.$set(this.formData, 'declareId', null)
this.$set(this.formData, 'startTime', null)
this.$set(this.formData, 'endTime', null)
this.$set(this.formData, 'planStartTime', null)
this.$set(this.formData, 'planEndTime', null)
this.$set(this, 'declareViewData', {})
}
this.getActivityReimbursementByUser()
this.showModePicker = false
},
onClubConfirm(value, index) {
const item = this.clubOption[index]
this.$set(this.formData, 'clubId', item.id)
this.$set(this.formData, 'clubName', item.clubName)
this.$set(this.formData, 'declareUnitName', item.clubName)
this.$set(this.formData, 'deductionBudgetId', null)
this.$set(this.formData, 'deductionBudgetName', null)
this.getBudgetByYear()
this.getActivityReimbursementByUser()
this.showClubPicker = false
},
onActivityConfirm(value, index) {
const item = this.activityOptions[index]
this.activityChange(item.id)
this.showActivityPicker = false
},
onBudgetConfirm(value, index) {
const item = this.filteredBudgetOption[index]
this.$set(this.formData, 'deductionBudgetId', item.id)
this.$set(this.formData, 'deductionBudgetName', this.getBudgetLabel(item))
this.showBudgetPicker = false
},
reimbursementIdentityChange(val) {
this.$set(this.formData, 'deductionBudgetId', null)
this.$set(this.formData, 'deductionBudgetName', null)
this.$set(this.formData, 'unionId', this.$store.state.user.union.id)
if (val === 'school') {
this.$set(this.formData, 'declareUnitName', '校工会')
} else if (val === 'union') {
this.$set(this.formData, 'declareUnitName', this.$store.state.user.union.name)
} else if (val === 'club') {
this.$set(this.formData, 'declareUnitName', this.formData.clubName || '')
}
},
outlayManageSourceChange(val) {
this.$set(this.formData, 'clubId', null)
this.$set(this.formData, 'clubName', null)
this.$set(this.formData, 'deductionBudgetId', null)
this.$set(this.formData, 'deductionBudgetName', null)
this.$set(this.formData, 'declareId', null)
this.$set(this.formData, 'activityName', null)
this.$set(this.formData, 'budgets', [])
this.$set(this, 'activeBudgetIndex', '0')
this.$set(this.formData, 'startTime', null)
this.$set(this.formData, 'endTime', null)
this.$set(this.formData, 'planStartTime', null)
this.$set(this.formData, 'planEndTime', null)
this.$set(this, 'declareViewData', {})
if (val === 'ACTIVITY_BUDGET_TYPE_ONE' && !this.hasSchoolLevelRole()) {
this.$set(this.formData, 'activityReimbursementMode', 'daily')
this.$set(this.formData, 'activityReimbursementModeName', '日常报销')
}
this.getBudgetByYear()
this.getActivityReimbursementByUser()
},
getBudgetByYear() {
this.budgetOption = []
this.$axios.post('/platform/activityReimbursement/apply/getBudgetByYear', {
outlayManageSource: this.formData.outlayManageSource,
clubId: this.formData.clubId
}).then((res) => {
if (res.code === 0) {
this.budgetOption = res.data || []
}
})
},
getBudgetLabel(item) {
if (!item) {
return ''
}
const shortName = this.getShortBudgetName(item.activityMatter)
if (this.hasSchoolLevelRole()) {
return this.getBudgetMoneyText(item) + ',名称:' + shortName
}
if (this.formData.outlayManageSource === 'ACTIVITY_BUDGET_TYPE_ONE' && (this.hasUnionRole() || this.hasClubRole())) {
return '名称:' + shortName
}
return this.getBudgetMoneyText(item) + ',名称:' + shortName
},
// 预算名称过长会挤掉金额,手机端选择器中统一截断名称并优先展示金额。
getShortBudgetName(name) {
const text = name || '暂无'
if (text.length > 12) {
return text.substring(0, 12) + '...'
}
return text
},
getBudgetMoneyText(item) {
return '预算:' + this.formatBudgetMoney(item.totalBudgetMoney) + ',已用:' + this.formatBudgetMoney(item.usedBudgetMoney)
},
formatBudgetMoney(value) {
if (this.isEmptyValue(value)) {
return '0'
}
return value
},
getActivityReimbursementByUser() {
if (this.formData.activityReimbursementMode === 'daily') {
this.activityOptions = []
return
}
this.$axios.post('/platform/activityReimbursement/apply/getActivityReimbursementByUser', {
id: this.bizId,
outlayManageSource: this.formData.outlayManageSource,
clubId: this.formData.clubId
}).then((res) => {
if (res.code === 0) {
this.activityOptions = res.data || []
}
})
},
activityChange(id) {
this.$axios.post('/platform/activityDeclare/mine/findOne', {id: id}).then((res) => {
if (res.code === 0) {
const data = this.normalizeDeclareData(res.data || {})
this.$set(this, 'declareViewData', data)
this.mergeFormData(data)
this.$set(this.formData, 'declareId', id)
this.$set(this.formData, 'id', this.bizId ? this.bizId : null)
this.fillCurrentUser()
}
})
},
mergeFormData(data) {
Object.keys(data).forEach((key) => {
this.$set(this.formData, key, data[key])
})
},
normalizeDeclareData(data) {
data.budgets = this.parseJsonArray(data.budgets)
data.files = this.parseJsonArray(data.files)
return data
},
viewDeclare() {
if (!this.formData.declareId) {
this.$toast.fail('请先选择项目')
return
}
if (this.declareViewData && this.declareViewData.id === this.formData.declareId) {
this.declareDialogVisible = true
return
}
this.$axios.post('/platform/activityDeclare/mine/findOne', {id: this.formData.declareId}).then((res) => {
if (res.code === 0) {
this.$set(this, 'declareViewData', this.normalizeDeclareData(res.data || {}))
this.declareDialogVisible = true
}
})
},
addBudget() {
const budgets = this.formData.budgets || []
budgets.push({})
this.$set(this.formData, 'budgets', budgets)
this.$set(this, 'activeBudgetIndex', String(budgets.length - 1))
},
removeActiveBudget() {
this.removeBudget(Number(this.activeBudgetIndex || 0))
},
removeBudget(index) {
const budgets = this.formData.budgets || []
if (budgets.length <= 1) {
this.$toast.fail('至少保留一条费用')
return
}
budgets.splice(index, 1)
this.$set(this.formData, 'budgets', budgets)
const active = index - 1 >= 0 ? index - 1 : 0
this.$set(this, 'activeBudgetIndex', String(active))
},
getCardNumberByPayeeId(row) {
if (!row.username) {
return
}
this.$axios.post('/platform/activityReimbursement/apply/getCardNumberByPayeeId', {username: row.username}).then((res) => {
if (res.code === 0 && res.data) {
this.$set(row, 'bankCardNum', res.data.bankCardNum)
this.$set(row, 'bankName', res.data.bankName)
}
})
},
openDatePicker(field) {
this.dateField = field
this.datePickerValue = this.formData[field] ? new Date(this.formData[field]) : new Date()
this.showDatePicker = true
},
onDateConfirm(value) {
this.$set(this.formData, this.dateField, this.$moment(value).format('YYYY-MM-DD'))
this.showDatePicker = false
},
isEmptyValue(value) {
return value === null || value === undefined || value === ''
},
isInvalidNumber(value, min) {
if (this.isEmptyValue(value)) {
return true
}
const numberValue = Number(value)
return isNaN(numberValue) || numberValue < min
},
validateReimbursementRelation() {
if (this.formData.reimbursementIdentity === 'union' && this.formData.outlayManageSource === 'ACTIVITY_BUDGET_TYPE_THREE') {
this.$toast.fail('报销身份如果是分工会,不能报销协会经费')
return false
}
if (this.formData.reimbursementIdentity === 'club' && this.formData.outlayManageSource === 'ACTIVITY_BUDGET_TYPE_TWO') {
this.$toast.fail('报销身份如果是协会,不能报销分工会经费')
return false
}
return true
},
// 按 PC 端必填规则逐项校验,避免自定义组件和条件字段漏校验。
validateForm() {
if (this.isEmptyValue(this.formData.reimbursementIdentity)) {
this.$toast.fail('请选择报销身份')
return false
}
if (this.isEmptyValue(this.formData.outlayManageSource)) {
this.$toast.fail('请选择经费类型')
return false
}
if (this.isEmptyValue(this.formData.activityReimbursementMode)) {
this.$toast.fail('请选择报销模式')
return false
}
if (this.showClubField && this.isEmptyValue(this.formData.clubId)) {
this.$toast.fail('请选择所属协会')
return false
}
if (this.formData.activityReimbursementMode === 'activity') {
if (this.isEmptyValue(this.formData.declareId)) {
this.$toast.fail('请选择活动')
return false
}
} else if (this.isEmptyValue(this.formData.activityName)) {
this.$toast.fail('请输入项目名称')
return false
}
if (this.isEmptyValue(this.formData.deductionBudgetId)) {
this.$toast.fail('请选择扣款预算')
return false
}
if (this.formData.activityReimbursementMode === 'activity') {
if (this.isEmptyValue(this.formData.startTime)) {
this.$toast.fail('请选择实际开始时间')
return false
}
if (this.isEmptyValue(this.formData.endTime)) {
this.$toast.fail('请选择实际结束时间')
return false
}
if (this.isEmptyValue(this.formData.planStartTime) || this.isEmptyValue(this.formData.planEndTime)) {
this.$toast.fail('请选择带有活动计划时间的项目')
return false
}
}
if (!this.formData.budgets || this.formData.budgets.length === 0) {
this.$toast.fail('请填写费用')
return false
}
for (let i = 0; i < this.formData.budgets.length; i++) {
const item = this.formData.budgets[i] || {}
const label = '费用' + (i + 1)
if (this.isEmptyValue(item.name)) {
this.$toast.fail('请填写' + label + '费用项目名称')
return false
}
if (this.formData.activityReimbursementMode === 'activity' && this.isInvalidNumber(item.budgetPrice, 1)) {
this.$toast.fail('请填写' + label + '预算费用')
return false
}
if (this.isInvalidNumber(item.actualPrice, 0)) {
this.$toast.fail('请填写' + label + '实际费用')
return false
}
if (this.isEmptyValue(item.username)) {
this.$toast.fail('请填写' + label + '收款人')
return false
}
if (this.isEmptyValue(item.bankName)) {
this.$toast.fail('请填写' + label + '支行名称')
return false
}
if (this.isEmptyValue(item.bankCardNum)) {
this.$toast.fail('请填写' + label + '报销卡号')
return false
}
}
if (this.isEmptyValue(this.formData.sign)) {
this.$toast.fail('请签字')
return false
}
return this.validateReimbursementRelation()
},
onSave() {
if (!this.validateReimbursementRelation()) {
return
}
this.$dialog.confirm({title: '提示', message: '您确定保存吗?'}).then(() => {
this.$axios.post('/platform/activityReimbursement/apply/save', {data: JSON.stringify(this.formData)}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.$pjaxReplace('/platform/activityReimbursement/mine/h5')
}
})
}).catch(() => {})
},
onSubmit() {
if (!this.validateForm()) {
return
}
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({title: '提示', message: '您确定要提交申请吗?'}).then(() => {
this.$axios.post('/platform/activityReimbursement/apply/submit', {data: JSON.stringify(this.formData)}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.$pjaxReplace('/platform/activityReimbursement/mine/h5')
}
})
}).catch(() => {})
}).catch(() => {})
},
onFinishTask() {
if (!this.validateForm()) {
return
}
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({title: '提示', message: '您确定要提交申请吗?'}).then(() => {
this.$axios.post('/platform/activityReimbursement/apply/submitAgain', {
data: JSON.stringify(this.formData),
taskId: this.taskId
}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.$pjaxReplace('/platform/activityReimbursement/mine/h5')
}
})
}).catch(() => {})
}).catch(() => {})
},
parseJsonArray(value) {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
try {
return JSON.parse(value)
} catch (e) {
return []
}
},
findOne() {
this.$axios.post('/platform/activityReimbursement/mine/findOne', {id: this.bizId}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
data.billFiles = this.parseJsonArray(data.billFiles)
data.photoFiles = this.parseJsonArray(data.photoFiles)
data.otherFiles = this.parseJsonArray(data.otherFiles)
data.budgets = this.parseJsonArray(data.budgets)
this.$set(this, 'activeBudgetIndex', '0')
this.$set(this, 'formData', {})
this.mergeFormData(data)
if (data.declareId) {
this.$set(this, 'declareViewData', this.normalizeDeclareData({
id: data.declareId,
activityName: data.activityName,
activityType: data.activityType,
userName: data.userName,
loginName: data.loginName,
declareUnitName: data.declareUnitName,
mobile: data.mobile,
applyTime: data.applyTime,
planStartTime: data.planStartTime,
planEndTime: data.planEndTime,
activityNumber: data.activityNumber,
activityAddress: data.activityAddress,
budgets: data.budgets,
activityContent: data.activityContent,
files: data.files,
sign: data.sign
}))
}
this.patchDisplayNames()
this.getBudgetByYear()
this.getActivityReimbursementByUser()
}
})
},
patchDisplayNames() {
const mode = this.modeOption.find((item) => item.code === this.formData.activityReimbursementMode)
if (mode) {
this.$set(this.formData, 'activityReimbursementModeName', mode.name)
}
const type = this.budgetTypeOption.find((item) => item.code === this.formData.outlayManageSource)
if (type) {
this.$set(this.formData, 'outlayManageSourceName', type.name)
}
const identity = this.reimbursementIdentityOption.find((item) => item.code === this.formData.reimbursementIdentity)
if (identity) {
this.$set(this.formData, 'reimbursementIdentityName', identity.name)
}
},
fillCurrentUser() {
const user = this.$store.state.user
this.$set(this.formData, 'userId', user.id)
this.$set(this.formData, 'userName', user.username)
this.$set(this.formData, 'loginName', user.loginname)
if (user.union) {
this.$set(this.formData, 'unionId', user.union.id)
this.$set(this.formData, 'unionName', user.union.name)
}
if (user.unit) {
this.$set(this.formData, 'unitId', user.unit.id)
this.$set(this.formData, 'unitName', user.unit.name)
}
},
formatDate(value) {
return value ? this.$moment(value).format('YYYY-MM-DD') : '暂无'
},
formatRange(start, end) {
if (!start && !end) {
return '暂无'
}
return this.formatDate(start) + ' ~ ' + this.formatDate(end)
},
formatUser(userName, loginName) {
if (!userName && !loginName) {
return '暂无'
}
return (userName || '') + (loginName ? '(' + loginName + ')' : '')
},
getActivityTypeName(type) {
const data = this.activityDeclareReimbursementList.find(function(item) {
return item.name === type
})
return data ? data.typeName : type
},
initBudgetType() {
return this.$businessTool.getDictOptions('ACTIVITY_BUDGET_TYPE').then((resp) => {
const result = []
const typeOne = resp.find((item) => item.code === 'ACTIVITY_BUDGET_TYPE_ONE')
if (typeOne) {
result.push(typeOne)
}
if (this.hasSchoolLevelRole()) {
resp.forEach((item) => {
if (['ACTIVITY_BUDGET_TYPE_TWO', 'ACTIVITY_BUDGET_TYPE_THREE'].includes(item.code)) {
result.push(item)
}
})
} else {
if (this.hasUnionRole()) {
const typeTwo = resp.find((item) => item.code === 'ACTIVITY_BUDGET_TYPE_TWO')
if (typeTwo) {
result.push(typeTwo)
}
}
if (this.hasClubRole()) {
const typeThree = resp.find((item) => item.code === 'ACTIVITY_BUDGET_TYPE_THREE')
if (typeThree) {
result.push(typeThree)
}
}
}
this.budgetTypeOption = result
})
},
initReimbursementIdentity() {
if (this.hasSchoolLevelRole()) {
this.reimbursementIdentityOption.push({name: '校工会', code: 'school'})
}
if (this.hasUnionRole()) {
this.reimbursementIdentityOption.push({name: '分工会', code: 'union'})
}
if (this.hasClubRole()) {
this.reimbursementIdentityOption.push({name: '协会', code: 'club'})
}
},
async init() {
this.clubOption = await this.$businessTool.listCLubByRole()
this.activityDeclareReimbursementList = await this.$businessTool.getEnumOptions('ActivityDeclareReimbursement')
this.initReimbursementIdentity()
await this.initBudgetType()
if (this.bizId) {
this.findOne()
} else {
this.fillCurrentUser()
}
}
},
created() {
this.init()
}
})
</script>
<style>
.section-title-row {
align-items: center;
display: flex;
justify-content: space-between;
width: 100%;
}
.inline-action {
padding: 10px 16px 0;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,75 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar :title="title" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search v-model="pageForm.searchKeyword" :show-action="false" :reverse-color="false"
input-align="left" placeholder="请输入申请人姓名或工号搜索" @search="doSearch"></van-search>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityReimbursementMode" :options="modeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.outlayManageSource" :options="budgetTypeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.budgetReimburseType" :options="budgetReimburseTypeOptions" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.auditText" @change="onAuditChange">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list :api="api" :page_form.sync="pageForm" ref="tableListRef" title="curTaskName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="申请人工号">{{ row.loginName }}</table-column>
<table-column label="申请人">{{ row.userName }}</table-column>
<table-column label="报销模式">{{ row.activityReimbursementMode === 'activity' ? '活动报销' : '日常报销' }}</table-column>
<table-column label="经费类型"><dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE" :value="row.outlayManageSource"></dict-tag></table-column>
<table-column label="项目名称">{{ row.activityName }}</table-column>
<table-column label="申报单位">{{ row.declareUnitName }}</table-column>
<table-column label="实际金额">{{ row.actualMoney }}</table-column>
<table-column label="申请时间">{{ row.applyTime }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)"><i class="fa fa-eye"></i><span>查看</span></div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)"><i class="fa fa-edit"></i><span>审核</span></div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)"><i class="fa fa-undo"></i><span>撤回</span></div>
</template>
</table-list>
<h5-activity-reimbursement-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{ formData.taskName }}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见" v-model="formData.tf_opinion" required type="textarea"
name="tf_opinion" rows="4" autosize placeholder="请填写审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"></van-field>
<van-field name="tf_userSign" label="签字" v-model="formData.tf_userSign" required :rules="[{ required: true, message: '请签字' }]">
<template #input>
<h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature>
</template>
</van-field>
<div class="form-actions">
<van-button @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" @click="handleTaskAction(1)">同意</van-button>
</div>
</van-form>
</div>
</div>
</h5-activity-reimbursement-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
<!--#include('../common/auditPage.js'){}#-->
createActivityReimbursementAuditPage({
title: '分工会审核',
api: '/platform/activityReimbursement/branchUnion/pageData'
})
</script>
<!--#
}
#-->
@@ -0,0 +1,74 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar :title="title" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search v-model="pageForm.searchKeyword" :show-action="false" :reverse-color="false"
input-align="left" placeholder="请输入申请人姓名或工号搜索" @search="doSearch"></van-search>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityReimbursementMode" :options="modeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.outlayManageSource" :options="budgetTypeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.budgetReimburseType" :options="budgetReimburseTypeOptions" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.auditText" @change="onAuditChange">
<van-tab title="已支付" name="1"></van-tab>
<van-tab title="未支付" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list :api="api" :page_form.sync="pageForm" ref="tableListRef" title="curTaskName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="申请人工号">{{ row.loginName }}</table-column>
<table-column label="申请人">{{ row.userName }}</table-column>
<table-column label="报销模式">{{ row.activityReimbursementMode === 'activity' ? '活动报销' : '日常报销' }}</table-column>
<table-column label="经费类型"><dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE" :value="row.outlayManageSource"></dict-tag></table-column>
<table-column label="项目名称">{{ row.activityName }}</table-column>
<table-column label="申报单位">{{ row.declareUnitName }}</table-column>
<table-column label="实际金额">{{ row.actualMoney }}</table-column>
<table-column label="申请时间">{{ row.applyTime }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)"><i class="fa fa-eye"></i><span>查看</span></div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)"><i class="fa fa-edit"></i><span>支付</span></div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)"><i class="fa fa-undo"></i><span>撤回</span></div>
</template>
</table-list>
<h5-activity-reimbursement-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{ formData.taskName }}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="支付意见" v-model="formData.tf_opinion" required type="textarea"
name="tf_opinion" rows="4" autosize placeholder="请填写支付意见"
:rules="[{ required: true, message: '请填写支付意见' }]"></van-field>
<van-field name="tf_userSign" label="签字" v-model="formData.tf_userSign" required :rules="[{ required: true, message: '请签字' }]">
<template #input><h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature></template>
</van-field>
<div class="form-actions">
<van-button @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" @click="handleTaskAction(1)">确认</van-button>
</div>
</van-form>
</div>
</div>
</h5-activity-reimbursement-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
<!--#include('../common/auditPage.js'){}#-->
createActivityReimbursementAuditPage({
title: '出纳支付',
api: '/platform/activityReimbursement/cashier/pageData',
opinionMessage: '请填写支付意见'
})
</script>
<!--#
}
#-->
@@ -0,0 +1,73 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar :title="title" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search v-model="pageForm.searchKeyword" :show-action="false" :reverse-color="false"
input-align="left" placeholder="请输入申请人姓名或工号搜索" @search="doSearch"></van-search>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityReimbursementMode" :options="modeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.outlayManageSource" :options="budgetTypeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.budgetReimburseType" :options="budgetReimburseTypeOptions" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.auditText" @change="onAuditChange">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list :api="api" :page_form.sync="pageForm" ref="tableListRef" title="curTaskName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="申请人工号">{{ row.loginName }}</table-column>
<table-column label="申请人">{{ row.userName }}</table-column>
<table-column label="报销模式">{{ row.activityReimbursementMode === 'activity' ? '活动报销' : '日常报销' }}</table-column>
<table-column label="经费类型"><dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE" :value="row.outlayManageSource"></dict-tag></table-column>
<table-column label="项目名称">{{ row.activityName }}</table-column>
<table-column label="申报单位">{{ row.declareUnitName }}</table-column>
<table-column label="实际金额">{{ row.actualMoney }}</table-column>
<table-column label="申请时间">{{ row.applyTime }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)"><i class="fa fa-eye"></i><span>查看</span></div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)"><i class="fa fa-edit"></i><span>审核</span></div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)"><i class="fa fa-undo"></i><span>撤回</span></div>
</template>
</table-list>
<h5-activity-reimbursement-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{ formData.taskName }}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见" v-model="formData.tf_opinion" required type="textarea"
name="tf_opinion" rows="4" autosize placeholder="请填写审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"></van-field>
<van-field name="tf_userSign" label="签字" v-model="formData.tf_userSign" required :rules="[{ required: true, message: '请签字' }]">
<template #input><h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature></template>
</van-field>
<div class="form-actions">
<van-button @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" @click="handleTaskAction(1)">同意</van-button>
</div>
</van-form>
</div>
</div>
</h5-activity-reimbursement-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
<!--#include('../common/auditPage.js'){}#-->
createActivityReimbursementAuditPage({
title: '协会负责人审核',
api: '/platform/activityReimbursement/clubPrincipal/pageData'
})
</script>
<!--#
}
#-->
@@ -0,0 +1,170 @@
function createActivityReimbursementAuditPage(options) {
return new Vue({
el: options.el || '#app',
store,
dicts: ['ACTIVITY_BUDGET_TYPE', 'ACTIVITY_BUDGET_REIMBURSE_TYPE'],
components: {
'h5-activity-reimbursement-info': H5_ACTIVITY_REIMBURSEMENT_INFO
},
data() {
return {
title: options.title,
api: options.api,
reviewUrl: options.reviewUrl || '',
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
audit: false,
auditText: '0',
searchKeyword: '',
activityReimbursementMode: '',
outlayManageSource: '',
budgetReimburseType: ''
},
modeOptions: [
{text: '全部报销模式', value: ''},
{text: '活动报销', value: 'activity'},
{text: '日常报销', value: 'daily'}
],
budgetTypeOptions: [{text: '全部经费类型', value: ''}],
budgetReimburseTypeOptions: [{text: '全部预算类型', value: ''}],
formData: {},
showApprovalForm: false,
opinionMessage: options.opinionMessage || '请填写审批意见'
}
},
methods: {
async onReady() {
await this.initDictOptions()
this.doSearch()
},
async initDictOptions() {
const budgetTypeList = await this.getDictOptionList('ACTIVITY_BUDGET_TYPE')
const budgetReimburseTypeList = await this.getDictOptionList('ACTIVITY_BUDGET_REIMBURSE_TYPE')
this.$set(this, 'budgetTypeOptions', this.buildDropdownOptions('全部经费类型', budgetTypeList))
this.$set(this, 'budgetReimburseTypeOptions', this.buildDropdownOptions('全部预算类型', budgetReimburseTypeList))
},
// 下拉选项依赖异步字典加载,统一兜底可避免首次进入时 Vant 下拉渲染为空。
async getDictOptionList(dictType) {
try {
if (this.$businessTool && this.$businessTool.getDictOptions) {
const data = await this.$businessTool.getDictOptions(dictType)
return data || []
}
} catch (error) {
if (this.dict && this.dict.type && this.dict.type[dictType]) {
return this.dict.type[dictType]
}
return []
}
if (this.dict && this.dict.type && this.dict.type[dictType]) {
return this.dict.type[dictType]
}
return []
},
buildDropdownOptions(defaultText, list) {
return [{text: defaultText, value: ''}].concat((list || []).map((item) => {
return {
text: item.name || item.label || item.text,
value: item.code || item.value || ''
}
}))
},
onAuditChange(val) {
this.$set(this.pageForm, 'audit', val === '1')
this.doSearch()
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onAudit(row) {
this.showApprovalForm = true
this.$set(this, 'formData', {})
this.$set(this.formData, 'id', row.id)
this.$set(this.formData, 'processTaskId', row.taskId)
this.$set(this.formData, 'taskName', row.curTaskName)
this.$set(this.formData, 'tf_opinion', null)
this.$set(this.formData, 'tf_userSign', null)
this.$refs.infoRef.onOpen(row)
},
handleTaskAction(val) {
if (!this.validateApprovalForm()) {
return
}
this.$refs.formRef.validate().then(() => {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要提交吗?'
}).then(() => {
this.submitReview(val)
}).catch(() => {})
}).catch(() => {})
},
// 自定义签字组件在 van-field 插槽中,需要先按真实表单值做提交前校验。
validateApprovalForm() {
if (!this.formData.tf_opinion) {
this.$toast.fail(this.opinionMessage)
return false
}
if (!this.formData.tf_userSign) {
this.$toast.fail('请签字')
return false
}
return true
},
submitReview(val) {
const data = {
id: this.formData.id,
processTaskId: this.formData.processTaskId,
tf_opinion: this.formData.tf_opinion,
tf_userSign: this.formData.tf_userSign,
submitType: val
}
if (this.reviewUrl) {
this.$axios.post(this.reviewUrl, {
data: JSON.stringify(data),
id: this.formData.id
}).then(this.afterSubmit)
} else {
this.$axios.post('/flow/common/executeTask', {
data: JSON.stringify(data)
}).then(this.afterSubmit)
}
},
afterSubmit(res) {
if (res.code === 0) {
this.$toast.success(res.msg)
this.$refs.infoRef.onClose()
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?'
}).then(() => {
this.$axios.post('/flow/common/revokeTask', {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
})
}).catch(() => {})
},
doSearch() {
this.$nextTick(() => {
this.$set(this.pageForm, 'pageNumber', 1)
this.$set(this.pageForm, 'totalCount', 0)
this.$refs.tableListRef.doSearch()
})
}
}
})
}
@@ -0,0 +1,170 @@
const H5_ACTIVITY_REIMBURSEMENT_INFO = {
template: [
'<van-action-sheet v-model="visible" title="查看详情">',
' <div class="detail-container activity-reimbursement-detail">',
' <van-cell-group title="基本信息">',
' <van-cell title="报销模式">{{ viewData.activityReimbursementMode === "activity" ? "活动报销" : "日常报销" }}</van-cell>',
' <van-cell title="经费类型">',
' <dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE" :value="viewData.outlayManageSource"></dict-tag>',
' </van-cell>',
' <van-cell title="扣款预算">{{ viewData.deductionBudgetName || "暂无" }}</van-cell>',
' <van-cell title="项目名称">{{ viewData.activityName || "暂无" }}</van-cell>',
' <van-cell title="申请人">{{ formatUser(viewData.userName, viewData.loginName) }}</van-cell>',
' <van-cell :title="viewData.outlayManageSource === \'ACTIVITY_BUDGET_TYPE_THREE\' ? \'申请协会\' : \'申请单位\'">{{ viewData.declareUnitName || "暂无" }}</van-cell>',
' <van-cell title="申请时间">{{ viewData.applyTime || "暂无" }}</van-cell>',
' <template v-if="viewData.activityReimbursementMode === \'activity\'">',
' <van-cell title="活动计划时间">{{ formatRange(viewData.planStartTime, viewData.planEndTime) }}</van-cell>',
' <van-cell title="实际活动时间">{{ formatRange(viewData.startTime, viewData.endTime) }}</van-cell>',
' <van-cell title="活动人数">{{ viewData.activityNumber || "暂无" }}</van-cell>',
' <van-cell class="direction-column-cell" title="活动地址">{{ viewData.activityAddress || "暂无" }}</van-cell>',
' </template>',
' </van-cell-group>',
' <van-cell-group title="费用">',
' <van-cell v-for="(item,index) in viewData.budgets" :key="index" class="direction-column-cell" :title="\'费用\' + (index + 1)">',
' <div class="fee-row">费用项目名称:{{ item.name || "暂无" }}</div>',
' <div class="fee-row" v-if="viewData.activityReimbursementMode === \'activity\'">预算费用:{{ item.budgetPrice || 0 }}</div>',
' <div class="fee-row">实际费用:{{ item.actualPrice || 0 }}</div>',
' <div class="fee-row">收款人:{{ item.username || "暂无" }}</div>',
' <div class="fee-row">支行名称:{{ item.bankName || "暂无" }}</div>',
' <div class="fee-row">报销卡号:{{ item.bankCardNum || "暂无" }}</div>',
' <div class="fee-row">备注:{{ item.remark || "暂无" }}</div>',
' </van-cell>',
' <van-cell title="合计">{{ totalActualMoney }} 元</van-cell>',
' </van-cell-group>',
' <van-cell-group title="附件">',
' <van-cell class="direction-column-cell" title="发票等">',
' <file-preview v-if="viewData.billFiles && viewData.billFiles.length > 0" :files="viewData.billFiles" complete_result></file-preview>',
' <span v-else>暂无</span>',
' </van-cell>',
' <template v-if="viewData.activityReimbursementMode === \'activity\'">',
' <van-cell class="direction-column-cell" title="活动报道照片/总结/链接">',
' <file-preview v-if="viewData.photoFiles && viewData.photoFiles.length > 0" :files="viewData.photoFiles" complete_result></file-preview>',
' <span v-else>暂无</span>',
' </van-cell>',
' <van-cell class="direction-column-cell" title="奖品/纪念品/服装/道具等活动人员名单">',
' <file-preview v-if="viewData.otherFiles && viewData.otherFiles.length > 0" :files="viewData.otherFiles" complete_result></file-preview>',
' <span v-else>暂无</span>',
' </van-cell>',
' </template>',
' <van-cell class="direction-column-cell" title="签字">',
' <van-image v-if="viewData.sign" :src="viewData.sign" class="signature-image"></van-image>',
' <span v-else>暂无签字</span>',
' </van-cell>',
' </van-cell-group>',
' <template v-for="task in doneTasks">',
' <van-cell-group :title="task.displayName" v-if="task.ext && task.ext.isFirstTaskNode">',
' <van-cell title="申请用户">{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})</van-cell>',
' <van-cell title="申请时间">{{ task.finishTime }}</van-cell>',
' <van-cell title="办理结果">',
' <dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>',
' </van-cell>',
' </van-cell-group>',
' <van-cell-group :title="task.displayName" v-else>',
' <van-cell title="办理用户">{{ formatTaskUser(task) }}</van-cell>',
' <van-cell title="办理时间">{{ task.finishTime }}</van-cell>',
' <van-cell title="办理结果">',
' <dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>',
' </van-cell>',
' <van-cell title="办理意见" class="direction-column-cell">',
' <div v-html="task.taskFormData && (task.taskFormData.tf_opinion || task.taskFormData.opinion)"></div>',
' </van-cell>',
' <van-cell title="签字" class="direction-column-cell">',
' <van-image v-if="task.ext && task.ext.tf_userSign" :src="task.ext.tf_userSign" class="signature-image"></van-image>',
' <span v-else>暂无</span>',
' </van-cell>',
' </van-cell-group>',
' </template>',
' </div>',
' <slot></slot>',
'</van-action-sheet>'
].join(''),
store,
dicts: ['PROCESS_TASK_SUBMIT_TYPE', 'ACTIVITY_BUDGET_TYPE'],
data() {
return {
visible: false,
viewData: {},
doneTasks: [],
row: null
}
},
computed: {
totalActualMoney() {
if (!this.viewData.budgets || this.viewData.budgets.length === 0) {
return '0.00'
}
const sum = this.viewData.budgets.reduce((total, item) => {
return total + Number(item.actualPrice || 0)
}, 0)
return sum.toFixed(2)
}
},
methods: {
onOpen(row) {
this.row = row
this.visible = true
this.getInfo()
this.getDoneTasks()
},
onClose() {
this.visible = false
},
getInfo() {
this.$axios.post('/platform/activityReimbursement/mine/findOne', {id: this.row.id}).then((res) => {
if (res.code === 0) {
this.viewData = this.normalizeInfo(res.data || {})
}
})
},
getDoneTasks() {
this.$axios.post('/flow/common/doneTasks', {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
normalizeInfo(data) {
data.budgets = this.parseJsonArray(data.budgets)
data.billFiles = this.parseJsonArray(data.billFiles)
data.photoFiles = this.parseJsonArray(data.photoFiles)
data.otherFiles = this.parseJsonArray(data.otherFiles)
return data
},
parseJsonArray(value) {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
try {
return JSON.parse(value)
} catch (e) {
return []
}
},
formatDate(value) {
return value ? this.$moment(value).format('YYYY-MM-DD') : '未填写'
},
formatRange(start, end) {
return this.formatDate(start) + ' ~ ' + this.formatDate(end)
},
formatUser(userName, loginName) {
if (!userName && !loginName) {
return '暂无'
}
return (userName || '') + (loginName ? '(' + loginName + ')' : '')
},
formatTaskUser(task) {
if (!task || !task.taskFormData) {
return '暂无'
}
const name = task.taskFormData.tf_userName || task.taskFormData.userName || ''
const account = task.taskFormData.loginName || ''
return this.formatUser(name, account)
}
},
style: [
'.activity-reimbursement-detail .fee-row{line-height:24px;}'
].join('')
}
@@ -0,0 +1,74 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar :title="title" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search v-model="pageForm.searchKeyword" :show-action="false" :reverse-color="false"
input-align="left" placeholder="请输入申请人姓名或工号搜索" @search="doSearch"></van-search>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityReimbursementMode" :options="modeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.outlayManageSource" :options="budgetTypeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.budgetReimburseType" :options="budgetReimburseTypeOptions" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.auditText" @change="onAuditChange">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list :api="api" :page_form.sync="pageForm" ref="tableListRef" title="curTaskName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="申请人工号">{{ row.loginName }}</table-column>
<table-column label="申请人">{{ row.userName }}</table-column>
<table-column label="报销模式">{{ row.activityReimbursementMode === 'activity' ? '活动报销' : '日常报销' }}</table-column>
<table-column label="经费类型"><dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE" :value="row.outlayManageSource"></dict-tag></table-column>
<table-column label="项目名称">{{ row.activityName }}</table-column>
<table-column label="申报单位">{{ row.declareUnitName }}</table-column>
<table-column label="实际金额">{{ row.actualMoney }}</table-column>
<table-column label="申请时间">{{ row.applyTime }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)"><i class="fa fa-eye"></i><span>查看</span></div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)"><i class="fa fa-edit"></i><span>审核</span></div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)"><i class="fa fa-undo"></i><span>撤回</span></div>
</template>
</table-list>
<h5-activity-reimbursement-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{ formData.taskName }}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见" v-model="formData.tf_opinion" required type="textarea"
name="tf_opinion" rows="4" autosize placeholder="请填写审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"></van-field>
<van-field name="tf_userSign" label="签字" v-model="formData.tf_userSign" required :rules="[{ required: true, message: '请签字' }]">
<template #input><h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature></template>
</van-field>
<div class="form-actions">
<van-button @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" @click="handleTaskAction(1)">同意</van-button>
</div>
</van-form>
</div>
</div>
</h5-activity-reimbursement-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
<!--#include('../common/auditPage.js'){}#-->
createActivityReimbursementAuditPage({
title: '校工会副主席审核',
api: '/platform/activityReimbursement/deputySchoolUnion/pageData',
reviewUrl: '/platform/activityReimbursement/deputySchoolUnion/doReview'
})
</script>
<!--#
}
#-->
@@ -0,0 +1,130 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar title="我的申请" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search v-model="pageForm.searchKeyword" :show-action="false" :reverse-color="false"
input-align="left" placeholder="请输入申请人姓名或工号搜索" @search="doSearch"></van-search>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/activityReimbursement/mine/pageData" :page_form.sync="pageForm"
ref="tableListRef" title="taskName" @ready="doSearch">
<template v-slot="{index,row}">
<table-column label="申请人工号">{{ row.loginName }}</table-column>
<table-column label="申请人">{{ row.userName }}</table-column>
<table-column label="报销模式">{{ row.activityReimbursementMode === 'activity' ? '活动报销' : '日常报销' }}</table-column>
<table-column label="经费类型">
<dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE" :value="row.outlayManageSource"></dict-tag>
</table-column>
<table-column label="项目名称">{{ row.activityName }}</table-column>
<table-column label="申报单位">{{ row.declareUnitName }}</table-column>
<table-column label="实际金额">{{ row.actualMoney }}</table-column>
<table-column label="申请时间">{{ row.applyTime }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" @click="onEdit(row)" v-if="row.taskKey === 'startTask' || !row.instanceId">
<i class="fa fa-edit"></i>
<span>编辑</span>
</div>
<div class="action-btn" @click="doExportReimbursement(row)" v-if="row.instanceState === 20">
<i class="fa fa-download"></i>
<span>凭证</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<span>撤回</span>
</div>
<div class="action-btn delete" @click="onDelete(row)" v-if="row.taskKey === 'startTask' || !row.instanceId">
<i class="fa fa-trash"></i>
<span>删除</span>
</div>
</template>
</table-list>
<h5-activity-reimbursement-info ref="infoRef"></h5-activity-reimbursement-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
new Vue({
el: '#app',
store,
dicts: ['ACTIVITY_BUDGET_TYPE'],
components: {
'h5-activity-reimbursement-info': H5_ACTIVITY_REIMBURSEMENT_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
searchKeyword: ''
}
}
},
methods: {
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onEdit(row) {
this.$pjaxReplace('/platform/activityReimbursement/apply/h5?taskId=' + (row.startTaskId || '') + '&bizId=' + row.id)
},
doExportReimbursement(row) {
window.location.href = '/platform/activityReimbursement/mine/doExportReimbursement?id=' + row.id
},
onDelete(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要删除吗?'
}).then(() => {
this.$axios.post('/platform/activityReimbursement/mine/onDelete', {id: row.id}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
})
}).catch(() => {})
},
onRevoke(row) {
this.$dialog.confirm({
title: '温馨提示',
message: '您确定要撤回吗?'
}).then(() => {
this.$axios.post('/flow/common/revokeTask', {taskId: row.startTaskId}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.doSearch()
} else {
this.$toast.fail(res.msg)
}
})
}).catch(() => {})
},
doSearch() {
this.$nextTick(() => {
this.$set(this.pageForm, 'pageNumber', 1)
this.$set(this.pageForm, 'totalCount', 0)
this.$refs.tableListRef.doSearch()
})
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,73 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar :title="title" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search v-model="pageForm.searchKeyword" :show-action="false" :reverse-color="false"
input-align="left" placeholder="请输入申请人姓名或工号搜索" @search="doSearch"></van-search>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityReimbursementMode" :options="modeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.outlayManageSource" :options="budgetTypeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.budgetReimburseType" :options="budgetReimburseTypeOptions" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.auditText" @change="onAuditChange">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list :api="api" :page_form.sync="pageForm" ref="tableListRef" title="curTaskName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="申请人工号">{{ row.loginName }}</table-column>
<table-column label="申请人">{{ row.userName }}</table-column>
<table-column label="报销模式">{{ row.activityReimbursementMode === 'activity' ? '活动报销' : '日常报销' }}</table-column>
<table-column label="经费类型"><dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE" :value="row.outlayManageSource"></dict-tag></table-column>
<table-column label="项目名称">{{ row.activityName }}</table-column>
<table-column label="申报单位">{{ row.declareUnitName }}</table-column>
<table-column label="实际金额">{{ row.actualMoney }}</table-column>
<table-column label="申请时间">{{ row.applyTime }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)"><i class="fa fa-eye"></i><span>查看</span></div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)"><i class="fa fa-edit"></i><span>审核</span></div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)"><i class="fa fa-undo"></i><span>撤回</span></div>
</template>
</table-list>
<h5-activity-reimbursement-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{ formData.taskName }}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见" v-model="formData.tf_opinion" required type="textarea"
name="tf_opinion" rows="4" autosize placeholder="请填写审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"></van-field>
<van-field name="tf_userSign" label="签字" v-model="formData.tf_userSign" required :rules="[{ required: true, message: '请签字' }]">
<template #input><h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature></template>
</van-field>
<div class="form-actions">
<van-button @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" @click="handleTaskAction(1)">同意</van-button>
</div>
</van-form>
</div>
</div>
</h5-activity-reimbursement-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
<!--#include('../common/auditPage.js'){}#-->
createActivityReimbursementAuditPage({
title: '校工会财务审核',
api: '/platform/activityReimbursement/schoolFinance/pageData'
})
</script>
<!--#
}
#-->
@@ -0,0 +1,73 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar :title="title" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search v-model="pageForm.searchKeyword" :show-action="false" :reverse-color="false"
input-align="left" placeholder="请输入申请人姓名或工号搜索" @search="doSearch"></van-search>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityReimbursementMode" :options="modeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.outlayManageSource" :options="budgetTypeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.budgetReimburseType" :options="budgetReimburseTypeOptions" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.auditText" @change="onAuditChange">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list :api="api" :page_form.sync="pageForm" ref="tableListRef" title="curTaskName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="申请人工号">{{ row.loginName }}</table-column>
<table-column label="申请人">{{ row.userName }}</table-column>
<table-column label="报销模式">{{ row.activityReimbursementMode === 'activity' ? '活动报销' : '日常报销' }}</table-column>
<table-column label="经费类型"><dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE" :value="row.outlayManageSource"></dict-tag></table-column>
<table-column label="项目名称">{{ row.activityName }}</table-column>
<table-column label="申报单位">{{ row.declareUnitName }}</table-column>
<table-column label="实际金额">{{ row.actualMoney }}</table-column>
<table-column label="申请时间">{{ row.applyTime }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)"><i class="fa fa-eye"></i><span>查看</span></div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)"><i class="fa fa-edit"></i><span>审核</span></div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)"><i class="fa fa-undo"></i><span>撤回</span></div>
</template>
</table-list>
<h5-activity-reimbursement-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{ formData.taskName }}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见" v-model="formData.tf_opinion" required type="textarea"
name="tf_opinion" rows="4" autosize placeholder="请填写审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"></van-field>
<van-field name="tf_userSign" label="签字" v-model="formData.tf_userSign" required :rules="[{ required: true, message: '请签字' }]">
<template #input><h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature></template>
</van-field>
<div class="form-actions">
<van-button @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" @click="handleTaskAction(1)">同意</van-button>
</div>
</van-form>
</div>
</div>
</h5-activity-reimbursement-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
<!--#include('../common/auditPage.js'){}#-->
createActivityReimbursementAuditPage({
title: '校工会主席审核',
api: '/platform/activityReimbursement/schoolPrincipal/pageData'
})
</script>
<!--#
}
#-->
@@ -0,0 +1,74 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app">
<van-nav-bar :title="title" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search v-model="pageForm.searchKeyword" :show-action="false" :reverse-color="false"
input-align="left" placeholder="请输入申请人姓名或工号搜索" @search="doSearch"></van-search>
<van-dropdown-menu>
<year-van-dropdown-item :num="5" @change="doSearch" v-model="pageForm.year"></year-van-dropdown-item>
<van-dropdown-item v-model="pageForm.activityReimbursementMode" :options="modeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.outlayManageSource" :options="budgetTypeOptions" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.budgetReimburseType" :options="budgetReimburseTypeOptions" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.auditText" @change="onAuditChange">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list :api="api" :page_form.sync="pageForm" ref="tableListRef" title="curTaskName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="申请人工号">{{ row.loginName }}</table-column>
<table-column label="申请人">{{ row.userName }}</table-column>
<table-column label="报销模式">{{ row.activityReimbursementMode === 'activity' ? '活动报销' : '日常报销' }}</table-column>
<table-column label="经费类型"><dict-tag :options="dict.type.ACTIVITY_BUDGET_TYPE" :value="row.outlayManageSource"></dict-tag></table-column>
<table-column label="项目名称">{{ row.activityName }}</table-column>
<table-column label="申报单位">{{ row.declareUnitName }}</table-column>
<table-column label="实际金额">{{ row.actualMoney }}</table-column>
<table-column label="申请时间">{{ row.applyTime }}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)"><i class="fa fa-eye"></i><span>查看</span></div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)"><i class="fa fa-edit"></i><span>审核</span></div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)"><i class="fa fa-undo"></i><span>撤回</span></div>
</template>
</table-list>
<h5-activity-reimbursement-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{ formData.taskName }}</div>
<div class="form-container">
<van-form ref="formRef">
<van-field label="审批意见" v-model="formData.tf_opinion" required type="textarea"
name="tf_opinion" rows="4" autosize placeholder="请填写审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"></van-field>
<van-field name="tf_userSign" label="签字" v-model="formData.tf_userSign" required :rules="[{ required: true, message: '请签字' }]">
<template #input><h5-signature v-model="formData.tf_userSign" slot="input"></h5-signature></template>
</van-field>
<div class="form-actions">
<van-button @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" @click="handleTaskAction(1)">同意</van-button>
</div>
</van-form>
</div>
</div>
</h5-activity-reimbursement-info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
<!--#include('../common/auditPage.js'){}#-->
createActivityReimbursementAuditPage({
title: '校工会预审',
api: '/platform/activityReimbursement/schoolUnion/pageData',
reviewUrl: '/platform/activityReimbursement/schoolUnion/doReview'
})
</script>
<!--#
}
#-->