From 34ae5037f9d4f2028295a147f7acbe9888db2f34 Mon Sep 17 00:00:00 2001 From: zhouhefeng Date: Wed, 21 Oct 2026 14:18:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E7=94=B3=E6=8A=A5=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E7=AB=AF=E5=AE=A1=E6=A0=B8=20=E6=B4=BB=E5=8A=A8?= =?UTF-8?q?=E6=8A=A5=E9=94=80=E6=89=8B=E6=9C=BA=E7=AB=AF=E5=AE=A1=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/activitybx/AccountingCon.java | 155 ++---- .../controller/activitybx/AssociationCon.java | 144 ++--- .../jf/controller/activitybx/DivisionCon.java | 146 ++--- .../jf/controller/activitybx/UnionCon.java | 192 ++----- ...tyReimbursementClubChairmanController.java | 104 ++-- ...ReimbursementSchoolChairmanController.java | 97 ++-- ...ReimbursementSchoolDirectorController.java | 96 ++-- ...yReimbursementUnionChairmanController.java | 106 ++-- .../zhgh/jf/handle/funds/FundsToHandler.java | 16 +- .../reimbursement/ReimbursementToHandler.java | 16 +- .../zhgh/jf/service/ActivityBxService.java | 37 ++ .../service/impl/ActivityBxServiceImpl.java | 500 ++++++++++++++++++ .../static/assets/mobile/js/popupHistory.js | 128 +++++ .../static/components/sign/index.vue | 239 ++++----- .../static/components/sign/mobileSign.vue | 176 ++++-- .../jf/activitybx/accounting/index.html | 18 + .../jf/activitybx/association/index.html | 135 +++++ .../mobile/jf/activitybx/association/info.js | 104 ++++ .../mobile/jf/activitybx/association/page.js | 272 ++++++++++ .../mobile/jf/activitybx/division/index.html | 18 + .../mobile/jf/activitybx/review/index.html | 223 ++++++++ .../views/mobile/jf/activitybx/review/info.js | 146 +++++ .../views/mobile/jf/activitybx/review/page.js | 353 +++++++++++++ .../mobile/jf/activitybx/union/index.html | 18 + .../jf/reimbursement/clubChairman/index.html | 18 + .../mobile/jf/reimbursement/review/index.html | 233 ++++++++ .../mobile/jf/reimbursement/review/info.js | 147 +++++ .../mobile/jf/reimbursement/review/page.js | 381 +++++++++++++ .../reimbursement/schoolChairman/index.html | 18 + .../reimbursement/schoolDirector/index.html | 18 + .../jf/reimbursement/unionChairman/index.html | 18 + src/main/resources/views/mobile/platform.html | 6 +- .../views/platform/common/signature.html | 105 ++-- 33 files changed, 3388 insertions(+), 995 deletions(-) create mode 100644 src/main/resources/static/assets/mobile/js/popupHistory.js create mode 100644 src/main/resources/views/mobile/jf/activitybx/accounting/index.html create mode 100644 src/main/resources/views/mobile/jf/activitybx/association/index.html create mode 100644 src/main/resources/views/mobile/jf/activitybx/association/info.js create mode 100644 src/main/resources/views/mobile/jf/activitybx/association/page.js create mode 100644 src/main/resources/views/mobile/jf/activitybx/division/index.html create mode 100644 src/main/resources/views/mobile/jf/activitybx/review/index.html create mode 100644 src/main/resources/views/mobile/jf/activitybx/review/info.js create mode 100644 src/main/resources/views/mobile/jf/activitybx/review/page.js create mode 100644 src/main/resources/views/mobile/jf/activitybx/union/index.html create mode 100644 src/main/resources/views/mobile/jf/reimbursement/clubChairman/index.html create mode 100644 src/main/resources/views/mobile/jf/reimbursement/review/index.html create mode 100644 src/main/resources/views/mobile/jf/reimbursement/review/info.js create mode 100644 src/main/resources/views/mobile/jf/reimbursement/review/page.js create mode 100644 src/main/resources/views/mobile/jf/reimbursement/schoolChairman/index.html create mode 100644 src/main/resources/views/mobile/jf/reimbursement/schoolDirector/index.html create mode 100644 src/main/resources/views/mobile/jf/reimbursement/unionChairman/index.html diff --git a/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/AccountingCon.java b/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/AccountingCon.java index 9065404..394155f 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/AccountingCon.java +++ b/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/AccountingCon.java @@ -1,74 +1,50 @@ package io.v.nutz.zhgh.jf.controller.activitybx; import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.model.Goods; -import io.v.nutz.base.service.BaseService; -import io.v.nutz.base.utils.DateUtil; -import io.v.nutz.base.utils.MsgApi; -import io.v.nutz.base.utils.Roles; -import io.v.nutz.base.utils.Vi; -import io.v.nutz.sys.models.Sys_signature; -import io.v.nutz.sys.models.Sys_user; -import io.v.nutz.sys.models.Sys_user_role; -import io.v.nutz.sys.services.SysSignatureService; -import io.v.nutz.web.commons.utils.ShiroUtil; -import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler; -import io.v.nutz.zhgh.jf.model.ActivityBx; import io.v.nutz.zhgh.jf.service.ActivityBxService; import org.apache.shiro.authz.annotation.RequiresPermissions; 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.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; -import org.nutz.json.Json; -import org.nutz.lang.Lang; import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; +import org.nutz.json.Json; -import java.util.List; - -/** - * @author zhf - * @date 2020/11/14 14:55 - * @description 校工会会计核报 - */ +/** 校工会负责人审核:PC 与手机共用业务接口,审核逻辑统一在 Service 中处理。 */ @IocBean @Ok("json") @At("/platform/jf/activity_bx/accounting") public class AccountingCon { - - - @Inject - private Vi vi; - @Inject private ActivityBxService activityBxService; - @Inject - private BaseService baseService; - - @Inject - private MsgApi msgApi; - - @Inject - private SysSignatureService sysSignatureService; - - @At("") @Ok("beetl:/platform/jf/activitybx/accounting.html") @RequiresPermissions("sys.jf.activity_bx.accounting") public void index() { } + /** 手机审核入口,使用独立页面目录及现有 PC 权限。 */ + @At + @Ok("beetl:/mobile/jf/activitybx/accounting/index.html") + @RequiresPermissions("sys.jf.activity_bx.accounting") + public void mobile() { + } + /** + * year 为年度,isAudit 为1全部/2已审核/3未审核;unitId、unionId 为筛选组织ID; + * searchName 为 username/loginname,searchKeyword 为姓名或工号(空值不筛选)。 + * pageNumber 从1开始,pageSize 为1至100;pageOrderName 为 bxr_unionname/activity_type/absName/apply_type/apply_time/state_id, + * pageOrderBy 为 asc/desc(兼容 ascending/descending),未指定有效排序时按状态、申请时间排序。 + * 返回分页对象,list 为申请数组,totalCount 为符合当前权限及筛选的总数。 + */ @At @ViReturn + @RequiresPermissions("sys.jf.activity_bx.accounting") public Object pageData(@Param(value = "year", required = false) Integer year, @Param(value = "isAudit", required = false) Integer isAudit, @Param(value = "unitId", required = false) String unitId, @@ -78,89 +54,44 @@ public class AccountingCon { @Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize, @Param(value = "pageOrderName", required = false) String pageOrderName, @Param(value = "pageOrderBy", required = false) String pageOrderBy) { - Sql sql = Sqls.create(""" - SELECT - act.*, - u.loginname bxr_loginname, - u.unionname bxr_unionname, - s.state_name, - s.state_color, - abs.`name` absName - FROM - `activity_bx` act - LEFT JOIN `user` u ON act.user_id = u.id - LEFT JOIN state s ON act.state_id = s.state_id - LEFT JOIN activity_basic_settings abs ON abs.`code` = act.projectJfCode - $condition - """); - - Cnd cnd = Cnd.NEW(); - - if (isAudit == 1) { - cnd.and("act.state_id", ">=", 1020); - } else { - cnd.and("act.state_id", isAudit == 2 ? ">" : "=", 1020); - } - if (ShiroUtil.hasRole("xghng")) { - cnd.and("act.projectTypeCode", "=", 50002); - } - - - Vi.cndPlus(cnd, "act.year", "=", year); - if (Vi.isNotBlank(searchName, searchKeyword)) { - cnd.where().andLike(searchName, searchKeyword); - } - - if (Vi.isNotBlank(pageOrderName, pageOrderBy)) { - cnd.orderBy(pageOrderName, pageOrderBy); - } else { - cnd.asc("act.state_id").desc("act.apply_time"); - } - Vi.cndPlus(cnd, "u.unitid", "=", unitId); - Vi.cndPlus(cnd, "u.unionid", "=", unionId); - sql.setCondition(cnd); - return activityBxService.listPageMap(pageNumber, pageSize, sql); + return activityBxService.reviewPage("accounting", new NutMap().addv("year", year).addv("isAudit", isAudit) + .addv("unitId", unitId).addv("unionId", unionId).addv("searchName", searchName) + .addv("searchKeyword", searchKeyword).addv("pageNumber", pageNumber).addv("pageSize", pageSize) + .addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy)); } + /** id 为申请主键;返回详情 Map,canReview 表示该申请是否仍处于当前待审节点。 */ @At @ViReturn - public Object getGoods(String activity_id) { - return baseService.dao().query(Goods.class, Cnd.where("reid", "=", activity_id)); + @RequiresPermissions("sys.jf.activity_bx.accounting") + public Object findOne(@Param("id") String id) { + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动申请"); + return activityBxService.reviewDetail("accounting", id); } + /** + * data 为 JSON 对象字符串:id 为申请ID,idea 为1至500字意见,sign 为图片数据或签名路径,flag 为布尔值(true通过/false拒绝)。 + * 返回统一结果对象,code=0 为成功,msg 为提示;失败不推进审核状态。 + */ @At @ViReturn @Aop(TransAop.READ_COMMITTED) - public Object doReview(String data) { - NutMap nutMap = Json.fromJson(NutMap.class, data); - ActivityBx aid = activityBxService.fetch(nutMap.getString("id")); - aid.setState_id(nutMap.getBoolean("flag") ? "1040" : "1025"); - aid.setAccounting_name(ShiroUtil.getPrincipalProperty("id").toString()); - aid.setAccounting_time(DateUtil.getDate()); - aid.setAccounting_idea(nutMap.getString("idea")); - - if (Strings.isNotBlank(nutMap.getString("sign"))) { - aid.setAccounting(sysSignatureService.insert(new Sys_signature(nutMap.getString("sign"))).getId()); - } - activityBxService.update(aid); - String nodeName = ""; - if (nutMap.getBoolean("flag")) { - nodeName = "校工会主席审核"; - Sys_user_role sys_user_role = activityBxService.dao().fetch(Sys_user_role.class, Cnd.where("roleId", "=", Roles.GH_WYH_ZX)); - if (Lang.isNotEmpty(sys_user_role)) { - Sys_user user = activityBxService.dao().fetch(Sys_user.class, sys_user_role.getUserId()); - msgApi.sendWxMsg("活动申报", "", "text", aid.getUser_name() + "申报了一条活动【" + aid.getActivity_name() + "】,请前往智慧工会进行审核。", List.of(user.getLoginname()), null); - FundsToHandler.CREATE_SCHOOL_TASK.exec(aid.getId(), null, List.of(user.getLoginname())); - } - } else { - nodeName = "校工会负责人拒绝"; - Sys_user user = activityBxService.dao().fetch(Sys_user.class, aid.getUser_id()); - msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】被校工会负责人拒绝,审核意见:" + aid.getAccounting_idea(), List.of(user.getLoginname()), null); - FundsToHandler.REFUSE_TASK.exec(aid.getId(), new NutMap().addv("option", nodeName), null); - } - FundsToHandler.COMPLETE_SCHOOL_FZR_TASK.exec(aid.getId(), new NutMap().addv("option", nodeName), null); + @RequiresPermissions("sys.jf.activity_bx.accounting") + public Object doReview(@Param("data") String data) { + if (Strings.isBlank(data)) throw new IllegalArgumentException("请填写审核信息"); + NutMap form = Json.fromJson(NutMap.class, data); + if (form == null) throw new IllegalArgumentException("审核参数格式错误"); + activityBxService.reviewActivity("accounting", form); return null; } + /** activity_id 为原活动申请ID;返回物品明细数组,保留原 PC 查询地址并检查数据范围。 */ + @At + @ViReturn + @RequiresPermissions("sys.jf.activity_bx.accounting") + public Object getGoods(@Param("activity_id") String activity_id) { + return activityBxService.reviewGoods("accounting", activity_id); + } + } diff --git a/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/AssociationCon.java b/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/AssociationCon.java index d53e1da..8e56c4b 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/AssociationCon.java +++ b/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/AssociationCon.java @@ -1,21 +1,9 @@ package io.v.nutz.zhgh.jf.controller.activitybx; import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.utils.DateUtil; -import io.v.nutz.base.utils.MsgApi; -import io.v.nutz.base.utils.Vi; -import io.v.nutz.sys.models.Sys_signature; -import io.v.nutz.sys.models.Sys_user; -import io.v.nutz.sys.services.SysSignatureService; -import io.v.nutz.web.commons.utils.ShiroUtil; -import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler; -import io.v.nutz.zhgh.jf.model.ActivityBx; import io.v.nutz.zhgh.jf.service.ActivityBxService; import org.apache.shiro.authz.annotation.RequiresPermissions; 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.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; @@ -25,41 +13,37 @@ import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; -import java.util.List; - -/** - * @author zhf - * @date 2020/12/6 15:23 - * @description 协会负责人审核 - */ +/** 协会活动申报审核:PC、手机共用查询和审核入口。 */ @IocBean @Ok("json") @At("/platform/jf/activity_bx/association") +@RequiresPermissions("sys.jf.activity_bx.association") public class AssociationCon { - - - @Inject - private Vi vi; - @Inject private ActivityBxService activityBxService; - @Inject - private SysSignatureService sysSignatureService; - @Inject - private MsgApi msgApi; - + /** PC 审核页面,沿用原访问地址。 */ @At("") @Ok("beetl:/platform/jf/activitybx/association.html") - @RequiresPermissions("sys.jf.activity_bx.association") public void index() { } + /** 手机审核页面,使用独立目录,不依赖旧的 declare 页面。 */ + @At + @Ok("beetl:/mobile/jf/activitybx/association/index.html") + public void mobile() { + } + /** + * year 为年度,isAudit 为 1全部/2已审核/3未审核(默认),clubId 为协会ID; + * unitId、unionId、searchName、searchKeyword 保留 PC 筛选,pageNumber/pageSize 为分页, + * pageOrderName/pageOrderBy 为允许的排序字段和方向。返回 Pagination,list 为申请,totalCount 为总数。 + */ @At @ViReturn public Object pageData(@Param(value = "year", required = false) Integer year, @Param(value = "isAudit", required = false) Integer isAudit, + @Param(value = "clubId", required = false) String clubId, @Param(value = "unitId", required = false) String unitId, @Param(value = "unionId", required = false) String unionId, @Param(value = "searchName", required = false) String searchName, @@ -67,82 +51,46 @@ public class AssociationCon { @Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize, @Param(value = "pageOrderName", required = false) String pageOrderName, @Param(value = "pageOrderBy", required = false) String pageOrderBy) { - Sql sql = Sqls.create(""" - SELECT - act.*, - u.loginname bxr_loginname, - u.unionname bxr_unionname, - s.state_name, - s.state_color, - abs.`name` absName - FROM - `activity_bx` act - LEFT JOIN `user` u ON act.user_id = u.id - LEFT JOIN sys_unit un ON u.unitid = un.id - LEFT JOIN state s ON act.state_id = s.state_id - LEFT JOIN activity_basic_settings abs ON abs.`code` = act.projectJfCode - $condition - """); - Cnd cnd = Cnd.NEW(); - - if (isAudit == 1) { - cnd.and("act.state_id", ">=", 1000); - } else { - cnd.and("act.state_id", isAudit == 2 ? ">" : "=", 1000); - } - - cnd.and("act.activity_type", "=", 40003); - if (!ShiroUtil.hasAnyRoles("sysadmin")) { - cnd.and("act.clubId", "in", vi.getMangeClubStr()); - } - Vi.cndPlus(cnd, "act.year", "=", year); - if (Vi.isNotBlank(searchName, searchKeyword)) { - cnd.where().andLike(searchName, searchKeyword); - } - - if (Vi.isNotBlank(pageOrderName, pageOrderBy)) { - cnd.orderBy(pageOrderName, pageOrderBy); - } else { - cnd.asc("act.state_id").desc("act.apply_time"); - } - Vi.cndPlus(cnd, "u.unitid", "=", unitId); - Vi.cndPlus(cnd, "u.unionid", "=", unionId); - sql.setCondition(cnd); - return activityBxService.listPageMap(pageNumber, pageSize, sql); + return activityBxService.associationPage(new NutMap().addv("year", year).addv("isAudit", isAudit) + .addv("clubId", clubId).addv("unitId", unitId).addv("unionId", unionId) + .addv("searchName", searchName).addv("searchKeyword", searchKeyword) + .addv("pageNumber", pageNumber).addv("pageSize", pageSize) + .addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy)); } + /** 无参数;返回当前用户可管理的协会列表,每项 id 为筛选值、name 为显示名称。 */ + @At + @ViReturn + public Object clubs() { + return activityBxService.associationClubs(); + } + /** id 为活动申报主键;返回申请详情 Map,包含预算、附件、审核记录和 canReview 可审核标识。 */ + @At + @ViReturn + public Object findOne(@Param("id") String id) { + if (Strings.isBlank(id)) { + throw new IllegalArgumentException("请选择活动申请"); + } + return activityBxService.associationDetail(id); + } + + /** + * id 为申报主键,idea 为1至500字意见,sign 为签字图片数据或现有签字路径,flag 为true通过/false拒绝。 + * 审核人和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为结果提示,data 为空。 + */ @At @ViReturn @Aop(TransAop.READ_COMMITTED) - public Object doReview(@Param(value = "id", required = false) String id, - @Param(value = "idea", required = false) String idea, - @Param(value = "sign", required = false) String sign, - @Param(value = "flag", required = false) Boolean flag) { - ActivityBx aid = activityBxService.fetch(id); - aid.setState_id(flag ? "1020" : "1005"); - aid.setAssociation_principal_id(ShiroUtil.getPrincipalProperty("id").toString()); - aid.setAssociation_principal_time(DateUtil.getDate()); - aid.setAssociation_principal_idea(idea); - if (Strings.isNotBlank(sign)) { - aid.setAssociation_principalSign_id(sysSignatureService.insert(new Sys_signature(sign)).getId()); + public Object doReview(@Param("id") String id, @Param("idea") String idea, + @Param("sign") String sign, @Param("flag") Boolean flag) { + if (Strings.isBlank(id) || flag == null) { + throw new IllegalArgumentException("请选择活动申请和审核结果"); } - activityBxService.update(aid); - - String nodeName = ""; - if (flag) { - msgApi.sendWxMsg("活动申报", "", "text", aid.getUser_name() + "申报了一条活动【" + aid.getActivity_name() + "】,请前往智慧工会进行审核。", List.of("1019911404"), null); - FundsToHandler.CREATE_SCHOOL_FZR_TASK.exec(id, new NutMap().addv("userId", aid.getUser_id()), List.of("1019911404")); - nodeName = "校工会负责人审核"; - } else { - Sys_user user = activityBxService.dao().fetch(Sys_user.class, aid.getUser_id()); - msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】被协会拒绝,审核意见:" + aid.getAssociation_principal_idea(), List.of(user.getLoginname()), null); - nodeName = "协会审核拒绝"; - FundsToHandler.REFUSE_TASK.exec(id, new NutMap().addv("option", nodeName), null); + if (Strings.isBlank(idea) || idea.trim().length() > 500 || Strings.isBlank(sign)) { + throw new IllegalArgumentException("请填写1至500字审核意见并签字"); } - FundsToHandler.COMPLETE_CLUB_TASK.exec(id, new NutMap().addv("option", nodeName), null); + activityBxService.reviewAssociation(id, idea.trim(), sign, flag); return null; } - - } diff --git a/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/DivisionCon.java b/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/DivisionCon.java index 9e1acb2..b07b610 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/DivisionCon.java +++ b/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/DivisionCon.java @@ -1,67 +1,49 @@ package io.v.nutz.zhgh.jf.controller.activitybx; import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.utils.DateUtil; -import io.v.nutz.base.utils.MsgApi; -import io.v.nutz.base.utils.Roles; -import io.v.nutz.base.utils.Vi; -import io.v.nutz.sys.models.Sys_signature; -import io.v.nutz.sys.models.Sys_user; -import io.v.nutz.sys.models.Sys_user_role; -import io.v.nutz.sys.services.SysSignatureService; -import io.v.nutz.web.commons.utils.ShiroUtil; -import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler; -import io.v.nutz.zhgh.jf.model.ActivityBx; import io.v.nutz.zhgh.jf.service.ActivityBxService; import org.apache.shiro.authz.annotation.RequiresPermissions; 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.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; -import org.nutz.lang.Lang; import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; -import java.util.List; - -/** - * @author zhf - * @date 2020/11/14 10:17 - * @description 基层工会核批 - */ +/** 基层工会主席审核:PC 与手机共用业务接口,审核逻辑统一在 Service 中处理。 */ @IocBean @Ok("json") @At("/platform/jf/activity_bx/division") public class DivisionCon { - - - @Inject - private Vi vi; - - @Inject - private MsgApi msgApi; - @Inject private ActivityBxService activityBxService; - @Inject - private SysSignatureService sysSignatureService; - - @At("") @Ok("beetl:/platform/jf/activitybx/division.html") @RequiresPermissions("sys.jf.activity_bx.division") public void index() { } + /** 手机审核入口,使用独立页面目录及现有 PC 权限。 */ + @At + @Ok("beetl:/mobile/jf/activitybx/division/index.html") + @RequiresPermissions("sys.jf.activity_bx.division") + public void mobile() { + } + + /** + * year 为年度,isAudit 为1全部/2已审核/3未审核;unitId、unionId 为筛选组织ID; + * searchName 为 username/loginname,searchKeyword 为姓名或工号(空值不筛选)。 + * pageNumber 从1开始,pageSize 为1至100;pageOrderName 为 bxr_unionname/activity_type/absName/apply_type/apply_time/state_id, + * pageOrderBy 为 asc/desc(兼容 ascending/descending),未指定有效排序时按状态、申请时间排序。 + * 返回分页对象,list 为申请数组,totalCount 为符合当前权限及筛选的总数。 + */ @At @ViReturn + @RequiresPermissions("sys.jf.activity_bx.division") public Object pageData(@Param(value = "year", required = false) Integer year, @Param(value = "isAudit", required = false) Integer isAudit, @Param(value = "unitId", required = false) String unitId, @@ -71,87 +53,31 @@ public class DivisionCon { @Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize, @Param(value = "pageOrderName", required = false) String pageOrderName, @Param(value = "pageOrderBy", required = false) String pageOrderBy) { - Sql sql = Sqls.create(""" - SELECT - act.*, - u.loginname bxr_loginname, - u.unionname bxr_unionname, - s.state_name, - s.state_color, - abs.`name` absName - FROM - `activity_bx` act - LEFT JOIN `user` u ON act.user_id = u.id - LEFT JOIN sys_unit su ON su.id = u.unitid - LEFT JOIN sys_union un ON su.unionid = un.id - LEFT JOIN state s ON act.state_id = s.state_id - LEFT JOIN activity_basic_settings abs ON abs.`code` = act.projectJfCode - $condition - """); - - Cnd cnd = Cnd.NEW(); - - if (isAudit == 1) { - cnd.and("act.state_id", ">=", 1010); - } else { - cnd.and("act.state_id", isAudit == 2 ? ">" : "=", 1010); - } - - - if (!ShiroUtil.hasAnyRoles("sysadmin")) { - if (ShiroUtil.hasAnyRoles(new String[]{"H04", "gh01", "gh02"})) { - cnd.and("un.id", "=", vi.getUnionId()); - } - } - Vi.cndPlus(cnd, "act.year", "=", year); - if (Vi.isNotBlank(searchName, searchKeyword)) { - cnd.where().andLike(searchName, searchKeyword); - } - - if (Vi.isNotBlank(pageOrderName, pageOrderBy)) { - cnd.orderBy(pageOrderName, pageOrderBy); - } else { - cnd.asc("act.state_id").desc("act.apply_time"); - } - Vi.cndPlus(cnd, "u.unitid", "=", unitId); - Vi.cndPlus(cnd, "u.unionid", "=", unionId); - sql.setCondition(cnd); - return activityBxService.listPageMap(pageNumber, pageSize, sql); + return activityBxService.reviewPage("division", new NutMap().addv("year", year).addv("isAudit", isAudit) + .addv("unitId", unitId).addv("unionId", unionId).addv("searchName", searchName) + .addv("searchKeyword", searchKeyword).addv("pageNumber", pageNumber).addv("pageSize", pageSize) + .addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy)); } + /** id 为申请主键;返回详情 Map,canReview 表示该申请是否仍处于当前待审节点。 */ + @At + @ViReturn + @RequiresPermissions("sys.jf.activity_bx.division") + public Object findOne(@Param("id") String id) { + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动申请"); + return activityBxService.reviewDetail("division", id); + } + + /** id 为申请ID,idea 为1至500字意见,sign 为图片数据或签名路径,flag 为true通过/false拒绝;返回统一结果,code=0 为成功,msg 为提示。 */ @At @ViReturn @Aop(TransAop.READ_COMMITTED) - public Object doReview(@Param(value = "id", required = false) String id, - @Param(value = "idea", required = false) String idea, - @Param(value = "sign", required = false) String sign, - @Param(value = "flag", required = false) Boolean flag) { - ActivityBx aid = activityBxService.fetch(id); - aid.setState_id(flag ? "1020" : "1015"); - aid.setDivision_chairman_name(ShiroUtil.getPrincipalProperty("id").toString()); - aid.setDivision_chairman_time(DateUtil.getDate()); - aid.setDivision_chairman_idea(idea); - if (Strings.isNotBlank(sign)) { - aid.setDivision_chairman(sysSignatureService.insert(new Sys_signature(sign)).getId()); - } - activityBxService.update(aid); - String nodeName = ""; - if (flag) { - nodeName = "校工会负责人审核"; -// Sys_user_role sys_user_role = activityBxService.dao().fetch(Sys_user_role.class, Cnd.where("roleId", "=", Roles.GH_WYH_ZX)); -// if (Lang.isNotEmpty(sys_user_role)) { -// Sys_user user = activityBxService.dao().fetch(Sys_user.class, sys_user_role.getUserId()); - Sys_user user = activityBxService.dao().fetch(Sys_user.class, Cnd.where("loginname","=","1019911404")); - msgApi.sendWxMsg("活动申报", "", "text", aid.getUser_name() + "申报的【" + aid.getActivity_name() + "】,请前往智慧工会进行审核。", List.of(user.getLoginname()), null); - FundsToHandler.CREATE_SCHOOL_FZR_TASK.exec(id, null, List.of(user.getLoginname())); -// } - } else { - nodeName = "基层工会主席拒绝"; - Sys_user user = activityBxService.dao().fetch(Sys_user.class, aid.getUser_id()); - msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】被基层工会主席拒绝,审核意见:" + aid.getDivision_chairman_idea(), List.of(user.getLoginname()), null); - FundsToHandler.REFUSE_TASK.exec(id, new NutMap().addv("option", nodeName), null); - } - FundsToHandler.COMPLETE_UNION_TASK.exec(id, new NutMap().addv("option", nodeName), null); + @RequiresPermissions("sys.jf.activity_bx.division") + public Object doReview(@Param("id") String id, @Param("idea") String idea, + @Param("sign") String sign, @Param("flag") Boolean flag) { + if (Strings.isBlank(id) || flag == null) throw new IllegalArgumentException("请选择申请和审核结果"); + activityBxService.reviewActivity("division", new NutMap().addv("id", id).addv("idea", idea) + .addv("sign", sign).addv("flag", flag)); return null; } diff --git a/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/UnionCon.java b/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/UnionCon.java index 81b8255..1dd0454 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/UnionCon.java +++ b/src/main/java/io/v/nutz/zhgh/jf/controller/activitybx/UnionCon.java @@ -1,70 +1,50 @@ package io.v.nutz.zhgh.jf.controller.activitybx; import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.model.Goods; -import io.v.nutz.base.utils.DateUtil; -import io.v.nutz.base.utils.MsgApi; -import io.v.nutz.base.utils.Vi; -import io.v.nutz.sys.models.Sys_signature; -import io.v.nutz.sys.models.Sys_user; -import io.v.nutz.sys.models.User; -import io.v.nutz.sys.services.SysSignatureService; -import io.v.nutz.web.commons.utils.ShiroUtil; -import io.v.nutz.zhgh.activity.models.ActivityTissue; -import io.v.nutz.zhgh.activity.services.ActivityTissueService; -import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler; -import io.v.nutz.zhgh.jf.model.ActivityBx; import io.v.nutz.zhgh.jf.service.ActivityBxService; import org.apache.shiro.authz.annotation.RequiresPermissions; 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.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; -import org.nutz.json.Json; import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; +import org.nutz.json.Json; -import java.util.List; - -/** - * @author zhf - * @date 2020/11/14 16:03 - * @description 校工会主席审核 - */ +/** 校工会主席审核:PC 与手机共用业务接口,审核逻辑统一在 Service 中处理。 */ @IocBean @Ok("json") @At("/platform/jf/activity_bx/union") public class UnionCon { - - - @Inject - private Vi vi; - - @Inject - private MsgApi msgApi; - @Inject private ActivityBxService activityBxService; - @Inject - private SysSignatureService sysSignatureService; - - @At("") @Ok("beetl:/platform/jf/activitybx/union.html") @RequiresPermissions("sys.jf.activity_bx.union") public void index() { } + /** 手机审核入口,使用独立页面目录及现有 PC 权限。 */ + @At + @Ok("beetl:/mobile/jf/activitybx/union/index.html") + @RequiresPermissions("sys.jf.activity_bx.union") + public void mobile() { + } + /** + * year 为年度,isAudit 为1全部/2已审核/3未审核;unitId、unionId 为筛选组织ID; + * searchName 为 username/loginname,searchKeyword 为姓名或工号(空值不筛选)。 + * pageNumber 从1开始,pageSize 为1至100;pageOrderName 为 bxr_unionname/activity_type/absName/apply_type/apply_time/state_id, + * pageOrderBy 为 asc/desc(兼容 ascending/descending),未指定有效排序时按状态、申请时间排序。 + * 返回分页对象,list 为申请数组,totalCount 为符合当前权限及筛选的总数。 + */ @At @ViReturn + @RequiresPermissions("sys.jf.activity_bx.union") public Object pageData(@Param(value = "year", required = false) Integer year, @Param(value = "isAudit", required = false) Integer isAudit, @Param(value = "unitId", required = false) String unitId, @@ -74,129 +54,35 @@ public class UnionCon { @Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize, @Param(value = "pageOrderName", required = false) String pageOrderName, @Param(value = "pageOrderBy", required = false) String pageOrderBy) { - Sql sql = Sqls.create(""" - SELECT - act.*, - u.loginname bxr_loginname, - u.unionname bxr_unionname, - s.state_name, - s.state_color, - abs.`name` absName - FROM - `activity_bx` act - LEFT JOIN `user` u ON act.user_id = u.id - LEFT JOIN state s ON act.state_id = s.state_id - LEFT JOIN activity_basic_settings abs ON abs.`code` = act.projectJfCode - $condition - """); - - Cnd cnd = Cnd.NEW(); - - if (isAudit == 1) { - cnd.and("act.state_id", ">=", 1040); - } else { - cnd.and("act.state_id", isAudit == 2 ? ">" : "=", 1040); - } - - - Vi.cndPlus(cnd, "act.year", "=", year); - if (Vi.isNotBlank(searchName, searchKeyword)) { - cnd.where().andLike(searchName, searchKeyword); - } - - if (Vi.isNotBlank(pageOrderName, pageOrderBy)) { - cnd.orderBy(pageOrderName, pageOrderBy); - } else { - cnd.asc("act.state_id").desc("act.apply_time"); - } - - Vi.cndPlus(cnd, "u.unitid", "=", unitId); - Vi.cndPlus(cnd, "u.unionid", "=", unionId); - sql.setCondition(cnd); - return activityBxService.listPageMap(pageNumber, pageSize, sql); + return activityBxService.reviewPage("union", new NutMap().addv("year", year).addv("isAudit", isAudit) + .addv("unitId", unitId).addv("unionId", unionId).addv("searchName", searchName) + .addv("searchKeyword", searchKeyword).addv("pageNumber", pageNumber).addv("pageSize", pageSize) + .addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy)); } + /** id 为申请主键;返回详情 Map,canReview 表示该申请是否仍处于当前待审节点。 */ + @At + @ViReturn + @RequiresPermissions("sys.jf.activity_bx.union") + public Object findOne(@Param("id") String id) { + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动申请"); + return activityBxService.reviewDetail("union", id); + } - @Inject - private io.v.nutz.zhgh.jf.service.jfUseService jfUseService; - - @Inject - private io.v.nutz.zhgh.jf.service.jfYjghService jfYjghService; - - @Inject - private ActivityTissueService activityTissueService; - @Inject - private io.v.nutz.zhgh.jf.service.jfClubService jfClubService; - - + /** + * data 为 JSON 对象字符串:id 为申请ID,idea 为1至500字意见,sign 为图片数据或签名路径,flag 为布尔值(true通过/false拒绝)。 + * activity_money 为可选的非负实际费用,不提交时保留原费用。 + * 返回统一结果对象,code=0 为成功,msg 为提示;失败不推进审核状态。 + */ @At @ViReturn @Aop(TransAop.READ_COMMITTED) - public Object doReview(String data) { - NutMap nutMap = Json.fromJson(NutMap.class, data); - - ActivityBx aid = activityBxService.fetch(nutMap.getString("id")); - aid.setState_id(nutMap.getBoolean("flag") ? "1060" : "1045"); - aid.setUnion_chairman_name(ShiroUtil.getPrincipalProperty("id").toString()); - aid.setUnion_chairman_time(DateUtil.getDate()); - aid.setUnion_chairman_idea(nutMap.getString("idea")); - aid.setActivity_money(nutMap.getDouble("activity_money")); - List goods = nutMap.getAsList("goods", Goods.class); - aid.setGoods(goods); - - if (Strings.isNotBlank(nutMap.getString("sign"))) { - aid.setUnion_chairman(sysSignatureService.insert(new Sys_signature(nutMap.getString("sign"))).getId()); - } - - String nodeName = ""; - if (nutMap.getBoolean("flag")) { - User user = activityBxService.dao().fetch(User.class, Cnd.where("id", "=", aid.getUser_id())); - msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】已审核通过,请前往智慧工会进行查看。", List.of(user.getLoginname()), null); - - - if (aid.getActivity_id() == null) { - ActivityTissue activityTissue = new ActivityTissue(); - activityTissue.setId(aid.getId()); - activityTissue.setApplyTime(DateUtil.getDate()); - activityTissue.setType(aid.getActivity_type()); - activityTissue.setActivityCode(aid.getProjectCode()); - activityTissue.setName(aid.getActivity_name()); - activityTissue.setUserId(aid.getUser_id()); - activityTissue.setStartPlannedDate(aid.getStartPlannedDate()); - activityTissue.setEndPlannedDate(aid.getEndPlannedDate()); - activityTissue.setStartTime(aid.getStartTime()); - activityTissue.setEndTime(aid.getEndTime()); - activityTissue.setAddress(aid.getAddress()); - if (aid.getActivity_type() == 40002) { - activityTissue.setUnionId(user.getUnionid()); - } else if (aid.getActivity_type() == 40003) { - activityTissue.setClubId(aid.getClubId()); - } - activityTissue.setProjectTypeCode("50006"); - activityBxService.insert(activityTissue); - aid.setActivity_id(activityTissue.getId()); - } - nodeName = "审核通过"; - } else { - Sys_user user = activityBxService.dao().fetch(Sys_user.class, aid.getUser_id()); - msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】被校工会主席拒绝,审核意见:" + aid.getUnion_chairman_idea(), List.of(user.getLoginname()), null); - FundsToHandler.REFUSE_TASK.exec(aid.getId(), new NutMap().addv("option", nodeName), null); - } - activityBxService.update(aid); - FundsToHandler.COMPLETE_SCHOOL_TASK.exec(aid.getId(), new NutMap().addv("option", nodeName), null); - - /*jf_yjgh jfYjgh = jfYjghService.fetch(Cnd.where("unionId", "=", vi.getUnionId(aid.getUser_id())).and("year", "=", Calendar.getInstance().get(Calendar.YEAR))); - if (jfYjgh != null) { - jf_use use = new jf_use(); - use.setAdjust_id(jfYjgh.getId()); - use.setApply_id(aid.getId()); - use.setProject(aid.getActivity_name()); - use.setAdjust_money((double) Math.round(aid.getActivity_money())); - use.setAdjust_person(ShiroUtil.getPrincipalProperty("id").toString()); - use.setAdjust_time(DateUtil.getDate()); - jfUseService.addUse(use, false); - }*/ - + @RequiresPermissions("sys.jf.activity_bx.union") + public Object doReview(@Param("data") String data) { + if (Strings.isBlank(data)) throw new IllegalArgumentException("请填写审核信息"); + NutMap form = Json.fromJson(NutMap.class, data); + if (form == null) throw new IllegalArgumentException("审核参数格式错误"); + activityBxService.reviewActivity("union", form); return null; } diff --git a/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementClubChairmanController.java b/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementClubChairmanController.java index 41144c8..4def2c7 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementClubChairmanController.java +++ b/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementClubChairmanController.java @@ -1,64 +1,47 @@ package io.v.nutz.zhgh.jf.controller.reimbursement; - -import cn.hutool.core.util.StrUtil; import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.utils.Vi; -import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler; -import io.v.nutz.zhgh.jf.service.ActivityBxService; import io.v.nutz.base.model.Audit; import io.v.nutz.base.query.PageForm; -import io.v.nutz.web.commons.utils.ShiroUtil; +import io.v.nutz.zhgh.jf.service.ActivityBxService; import io.v.nutz.web.commons.slog.annotation.SLog; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.nutz.aop.interceptor.ioc.TransAop; -import org.nutz.dao.Chain; -import org.nutz.dao.Cnd; -import org.nutz.dao.Sqls; -import org.nutz.dao.sql.Sql; import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; -import java.util.Date; - -/** - * @version 1.0 - * @Author zzr - * @name:ActivityReimbursementClubChairmanController - * @Date 2024/4/26 14:58 - * @注释 活动报销,协会负责人审核 - */ +/** 活动报销:协会负责人审核,PC 与手机共用列表、数据范围和审核服务。 */ @IocBean @Ok("json:full") @At("/platform/jf/reimbursement/clubChairman") public class ActivityReimbursementClubChairmanController { - - @Inject - private Vi vi; - @Inject private ActivityBxService activityBxService; + /** 保留现有 PC 页面入口。 */ @At("") @Ok("beetl:/platform/jf/reimbursement/clubChairman.html") @RequiresPermissions("sys.jf.reimbursement.clubChairman") public void index() { } + /** 手机页面可携带 id 报销主键和 mode=audit/view;权限名称与 PC 一致。 */ + @At + @Ok("beetl:/mobile/jf/reimbursement/clubChairman/index.html") + @RequiresPermissions("sys.jf.reimbursement.clubChairman") + public void mobile() { + } /** - * 活动报销协会审核 - * @param pageForm - * @param year - * @param clubId - * @param activityName - * @param isAudit 0,全部;1,已审核;2,未审核 - * @return + * pageForm 含 pageNumber/pageSize、searchName(u.username/u.loginname)、searchKeyword 和排序字段/方向; + * year 为年度,clubId/unionId/unitId 为组织ID,activityName 为活动名,isAudit 为0全部/1已审核/2未审核。 + * 返回统一结果,code=0 时 data 为 Pagination,list 为本页记录、totalCount 为总数。 */ @At @ViReturn @@ -66,57 +49,38 @@ public class ActivityReimbursementClubChairmanController { public Object pageData(PageForm pageForm, @Param(value = "year", required = false) Integer year, @Param(value = "clubId", required = false) String clubId, + @Param(value = "unionId", required = false) String unionId, + @Param(value = "unitId", required = false) String unitId, @Param(value = "activityName", required = false) String activityName, @Param(value = "isAudit", required = false) String isAudit) { - Sql sql = activityBxService.activityReimbursementSql(); - Cnd cnd = Cnd.NEW(); - cnd.andEX("act.`year`","=",year); - if (StrUtil.isNotBlank(activityName)) { - cnd.and(Cnd.likeEX("act.activity_name", activityName)); - } - // 姓名/工号查询由页面通过 PageForm 统一传入,这里按所选字段做模糊匹配。 - if (StrUtil.isNotBlank(pageForm.getSearchName()) && StrUtil.isNotBlank(pageForm.getSearchKeyword())) { - cnd.and(Cnd.likeEX(pageForm.getSearchName(), pageForm.getSearchKeyword())); - } - - cnd.and("act.reimbursement_state_id","is not",null); - - //1已审核;2未审核;0全部 - cnd.and("act.reimbursement_state_id", "1".equals(isAudit) ? ">" : "2".equals(isAudit) ? "=" : ">=", 1000); - - if (ShiroUtil.hasAnyRoles("sysadmin,A06")){ - cnd.andEX("act.clubId","=",clubId); - } else { - cnd.and("act.clubId","=",vi.getClubId()); - } - sql.setCondition(cnd); - return activityBxService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql); + return activityBxService.reimbursementPage("clubChairman", pageForm, NutMap.NEW() + .addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId) + .addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit)); } + /** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */ + @At + @ViReturn + @RequiresPermissions("sys.jf.reimbursement.clubChairman") + public Object findOne(@Param("id") String id) { + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动报销申请"); + return activityBxService.reimbursementDetail("clubChairman", id); + } - + /** + * id 为报销主键;audit 为 JSON 对象,auditOpinion 为1至500字意见,auditSign 为签字图片或历史路径, + * auditPass 为 true通过/false拒绝。人员和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为提示,data 为空。 + */ @At @ViReturn @Aop(TransAop.READ_COMMITTED) @RequiresPermissions("sys.jf.reimbursement.clubChairman") @SLog(type = "文体活动", tag = "活动报销", msg = "协会负责人审核", param = true, result = true) - public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){ - audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString()); - audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString()); - audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString()); - audit.setAuditTime(new Date()); - activityBxService.dao().insert(audit); - - Chain chain = Chain.make("reimbursement_club_audit_id", audit.getId()); - chain.add("reimbursement_state_id", audit.getAuditPass() ? 1020 : 1005); - activityBxService.update(chain,Cnd.where("id","=",id)); - - ReimbursementToHandler.COMPLETE_CLUB_TASK.exec(id, NutMap.NEW().addv("nodeName",audit.getAuditPass() ? "协会负责人通过" : "协会负责人拒绝")); - if (audit.getAuditPass()) { - ReimbursementToHandler.CREATE_SCHOOL_DIRECTOR_TASK.exec(id,null); - } else { - ReimbursementToHandler.REFUSE_UNION_TASK.exec(id,NutMap.NEW().addv("nodeName","协会负责人拒绝")); + public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) { + if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) { + throw new IllegalArgumentException("请选择活动报销申请并填写审核信息"); } + activityBxService.reviewReimbursement("clubChairman", id, audit); return null; } } diff --git a/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementSchoolChairmanController.java b/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementSchoolChairmanController.java index 4d48ce1..ca399de 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementSchoolChairmanController.java +++ b/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementSchoolChairmanController.java @@ -1,105 +1,86 @@ package io.v.nutz.zhgh.jf.controller.reimbursement; -import cn.hutool.core.util.StrUtil; import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.utils.Vi; -import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler; -import io.v.nutz.zhgh.jf.service.ActivityBxService; import io.v.nutz.base.model.Audit; import io.v.nutz.base.query.PageForm; -import io.v.nutz.web.commons.utils.ShiroUtil; +import io.v.nutz.zhgh.jf.service.ActivityBxService; import io.v.nutz.web.commons.slog.annotation.SLog; import org.apache.shiro.authz.annotation.RequiresPermissions; -import org.nutz.dao.Chain; -import org.nutz.dao.Cnd; -import org.nutz.dao.Sqls; -import org.nutz.dao.sql.Sql; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; -import java.util.Date; - - -/** - * @version 1.0 - * @Author zzr - * @name:ActivityReimbursementSchoolChairmanController - * @Date 2024/4/26 14:58 - * @注释 报销,校工会主席审核 - */ +/** 活动报销:校工会主席审核,PC 与手机共用列表、数据范围和审核服务。 */ @IocBean @Ok("json:full") @At("/platform/jf/reimbursement/schoolChairman") public class ActivityReimbursementSchoolChairmanController { - @Inject private ActivityBxService activityBxService; + /** 保留现有 PC 页面入口。 */ @At("") @Ok("beetl:/platform/jf/reimbursement/schoolChairman.html") @RequiresPermissions("sys.jf.reimbursement.schoolChairman") public void index() { } + /** 手机页面可携带 id 报销主键和 mode=audit/view;权限名称与 PC 一致。 */ + @At + @Ok("beetl:/mobile/jf/reimbursement/schoolChairman/index.html") + @RequiresPermissions("sys.jf.reimbursement.schoolChairman") + public void mobile() { + } + /** + * pageForm 含 pageNumber/pageSize、searchName(u.username/u.loginname)、searchKeyword 和排序字段/方向; + * year 为年度,clubId/unionId/unitId 为组织ID,activityName 为活动名,isAudit 为0全部/1已审核/2未审核。 + * 返回统一结果,code=0 时 data 为 Pagination,list 为本页记录、totalCount 为总数。 + */ @At @ViReturn @RequiresPermissions("sys.jf.reimbursement.schoolChairman") public Object pageData(PageForm pageForm, @Param(value = "year", required = false) Integer year, + @Param(value = "clubId", required = false) String clubId, @Param(value = "unionId", required = false) String unionId, @Param(value = "unitId", required = false) String unitId, - @Param(value = "clubId",required = false) String clubId, @Param(value = "activityName", required = false) String activityName, @Param(value = "isAudit", required = false) String isAudit) { - Sql sql = activityBxService.activityReimbursementSql(); - Cnd cnd = Cnd.NEW(); - cnd.andEX("act.`year`","=",year); - if (StrUtil.isNotBlank(activityName)) { - cnd.and(Cnd.likeEX("act.activity_name", activityName)); - } - // 姓名/工号查询由页面通过 PageForm 统一传入,这里按所选字段做模糊匹配。 - if (StrUtil.isNotBlank(pageForm.getSearchName()) && StrUtil.isNotBlank(pageForm.getSearchKeyword())) { - cnd.and(Cnd.likeEX(pageForm.getSearchName(), pageForm.getSearchKeyword())); - } - - //1已审核;2未审核;0全部 - cnd.and("act.reimbursement_state_id", "1".equals(isAudit) ? ">" : "2".equals(isAudit) ? "=" : ">=", 1040); - - cnd.andEX("u.unionid","=",unionId); - cnd.andEX("u.unitId","=",unitId); - cnd.andEX("act.clubId","=",clubId); - sql.setCondition(cnd); - return activityBxService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql); + return activityBxService.reimbursementPage("schoolChairman", pageForm, NutMap.NEW() + .addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId) + .addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit)); } - - + /** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */ @At @ViReturn @RequiresPermissions("sys.jf.reimbursement.schoolChairman") + public Object findOne(@Param("id") String id) { + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动报销申请"); + return activityBxService.reimbursementDetail("schoolChairman", id); + } + + /** + * id 为报销主键;audit 为 JSON 对象,auditOpinion 为1至500字意见,auditSign 为签字图片或历史路径, + * auditPass 为 true通过/false拒绝。人员和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为提示,data 为空。 + */ + @At + @ViReturn + @Aop(TransAop.READ_COMMITTED) + @RequiresPermissions("sys.jf.reimbursement.schoolChairman") @SLog(type = "文体活动", tag = "活动报销", msg = "校工会主席审核", param = true, result = true) - public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){ - audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString()); - audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString()); - audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString()); - audit.setAuditTime(new Date()); - activityBxService.dao().insert(audit); - - Chain chain = Chain.make("reimbursement_school_chairman_id", audit.getId()); - chain.add("reimbursement_state_id", audit.getAuditPass() ? 1061 : 1045); - activityBxService.update(chain,Cnd.where("id","=",id)); - - ReimbursementToHandler.COMPLETE_SCHOOL_CHAIRMAN_TASK.exec(id, NutMap.NEW().addv("nodeName",audit.getAuditPass() ? "校工会主席通过" : "校工会主席拒绝")); - if (audit.getAuditPass()) { - ReimbursementToHandler.COMPLETE_PROCESS.exec(id,null); - } else { - ReimbursementToHandler.REFUSE_UNION_TASK.exec(id,NutMap.NEW().addv("nodeName","校工会主席拒绝")); + public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) { + if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) { + throw new IllegalArgumentException("请选择活动报销申请并填写审核信息"); } + activityBxService.reviewReimbursement("schoolChairman", id, audit); return null; } } diff --git a/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementSchoolDirectorController.java b/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementSchoolDirectorController.java index 3c0bde1..53ccc25 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementSchoolDirectorController.java +++ b/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementSchoolDirectorController.java @@ -1,104 +1,86 @@ package io.v.nutz.zhgh.jf.controller.reimbursement; -import cn.hutool.core.util.StrUtil; import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.utils.Vi; -import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler; -import io.v.nutz.zhgh.jf.service.ActivityBxService; import io.v.nutz.base.model.Audit; import io.v.nutz.base.query.PageForm; -import io.v.nutz.web.commons.utils.ShiroUtil; +import io.v.nutz.zhgh.jf.service.ActivityBxService; import io.v.nutz.web.commons.slog.annotation.SLog; import org.apache.shiro.authz.annotation.RequiresPermissions; -import org.nutz.dao.Chain; -import org.nutz.dao.Cnd; -import org.nutz.dao.sql.Sql; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; -import java.util.Date; - -/** - * @version 1.0 - * @Author zzr - * @name:ActivityReimbursementSchoolDirectorController - * @Date 2024/4/26 14:58 - * @注释 报销校工会负责人审核 - */ +/** 活动报销:校工会负责人审核,PC 与手机共用列表、数据范围和审核服务。 */ @IocBean @Ok("json:full") @At("/platform/jf/reimbursement/schoolDirector") public class ActivityReimbursementSchoolDirectorController { - @Inject private ActivityBxService activityBxService; + /** 保留现有 PC 页面入口。 */ @At("") @Ok("beetl:/platform/jf/reimbursement/schoolDirector.html") @RequiresPermissions("sys.jf.reimbursement.schoolDirector") public void index() { } + /** 手机页面可携带 id 报销主键和 mode=audit/view;权限名称与 PC 一致。 */ + @At + @Ok("beetl:/mobile/jf/reimbursement/schoolDirector/index.html") + @RequiresPermissions("sys.jf.reimbursement.schoolDirector") + public void mobile() { + } + /** + * pageForm 含 pageNumber/pageSize、searchName(u.username/u.loginname)、searchKeyword 和排序字段/方向; + * year 为年度,clubId/unionId/unitId 为组织ID,activityName 为活动名,isAudit 为0全部/1已审核/2未审核。 + * 返回统一结果,code=0 时 data 为 Pagination,list 为本页记录、totalCount 为总数。 + */ @At @ViReturn @RequiresPermissions("sys.jf.reimbursement.schoolDirector") public Object pageData(PageForm pageForm, @Param(value = "year", required = false) Integer year, + @Param(value = "clubId", required = false) String clubId, @Param(value = "unionId", required = false) String unionId, @Param(value = "unitId", required = false) String unitId, - @Param(value = "clubId",required = false) String clubId, @Param(value = "activityName", required = false) String activityName, @Param(value = "isAudit", required = false) String isAudit) { - Sql sql = activityBxService.activityReimbursementSql(); - Cnd cnd = Cnd.NEW(); - cnd.andEX("act.`year`","=",year); - if (StrUtil.isNotBlank(activityName)) { - cnd.and(Cnd.likeEX("act.activity_name", activityName)); - } - // 姓名/工号查询由页面通过 PageForm 统一传入,这里按所选字段做模糊匹配。 - if (StrUtil.isNotBlank(pageForm.getSearchName()) && StrUtil.isNotBlank(pageForm.getSearchKeyword())) { - cnd.and(Cnd.likeEX(pageForm.getSearchName(), pageForm.getSearchKeyword())); - } - - //1已审核;2未审核;0全部 - cnd.and("act.reimbursement_state_id", "1".equals(isAudit) ? ">" : "2".equals(isAudit) ? "=" : ">=", 1020); - - cnd.andEX("u.unionid","=",unionId); - cnd.andEX("u.unitId","=",unitId); - cnd.andEX("act.clubId","=",clubId); - sql.setCondition(cnd); - return activityBxService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql); + return activityBxService.reimbursementPage("schoolDirector", pageForm, NutMap.NEW() + .addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId) + .addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit)); } - - + /** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */ @At @ViReturn @RequiresPermissions("sys.jf.reimbursement.schoolDirector") + public Object findOne(@Param("id") String id) { + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动报销申请"); + return activityBxService.reimbursementDetail("schoolDirector", id); + } + + /** + * id 为报销主键;audit 为 JSON 对象,auditOpinion 为1至500字意见,auditSign 为签字图片或历史路径, + * auditPass 为 true通过/false拒绝。人员和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为提示,data 为空。 + */ + @At + @ViReturn + @Aop(TransAop.READ_COMMITTED) + @RequiresPermissions("sys.jf.reimbursement.schoolDirector") @SLog(type = "文体活动", tag = "活动报销", msg = "校工会负责人审核", param = true, result = true) - public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){ - audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString()); - audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString()); - audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString()); - audit.setAuditTime(new Date()); - activityBxService.dao().insert(audit); - - Chain chain = Chain.make("reimbursement_school_director_id", audit.getId()); - //通过,校工会主席审核 - chain.add("reimbursement_state_id", audit.getAuditPass() ? 1040 : 1025); - activityBxService.update(chain,Cnd.where("id","=",id)); - - ReimbursementToHandler.COMPLETE_SCHOOL_DIRECTOR_TASK.exec(id, NutMap.NEW().addv("nodeName",audit.getAuditPass() ? "校工会负责人通过" : "校工会负责人拒绝")); - if (audit.getAuditPass()) { - ReimbursementToHandler.CREATE_SCHOOL_CHAIRMAN_TASK.exec(id,null); - } else { - ReimbursementToHandler.REFUSE_UNION_TASK.exec(id,NutMap.NEW().addv("nodeName","校工会负责人拒绝")); + public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) { + if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) { + throw new IllegalArgumentException("请选择活动报销申请并填写审核信息"); } + activityBxService.reviewReimbursement("schoolDirector", id, audit); return null; } } diff --git a/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementUnionChairmanController.java b/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementUnionChairmanController.java index c6459bc..4a41c88 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementUnionChairmanController.java +++ b/src/main/java/io/v/nutz/zhgh/jf/controller/reimbursement/ActivityReimbursementUnionChairmanController.java @@ -1,114 +1,86 @@ package io.v.nutz.zhgh.jf.controller.reimbursement; -import cn.hutool.core.util.StrUtil; import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.utils.Vi; -import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler; -import io.v.nutz.zhgh.jf.service.ActivityBxService; import io.v.nutz.base.model.Audit; import io.v.nutz.base.query.PageForm; -import io.v.nutz.web.commons.utils.ShiroUtil; +import io.v.nutz.zhgh.jf.service.ActivityBxService; import io.v.nutz.web.commons.slog.annotation.SLog; import org.apache.shiro.authz.annotation.RequiresPermissions; -import org.nutz.dao.Chain; -import org.nutz.dao.Cnd; -import org.nutz.dao.Sqls; -import org.nutz.dao.sql.Sql; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; -import java.util.Date; - - -/** - * @version 1.0 - * @Author zzr - * @name:ActivityReimbursementUnionChairmanController - * @Date 2024/4/26 14:58 - * @注释 活动报销,分工会主席审核 - */ +/** 活动报销:分工会主席审核,PC 与手机共用列表、数据范围和审核服务。 */ @IocBean @Ok("json:full") @At("/platform/jf/reimbursement/unionChairman") public class ActivityReimbursementUnionChairmanController { - @Inject private ActivityBxService activityBxService; + /** 保留现有 PC 页面入口。 */ @At("") @Ok("beetl:/platform/jf/reimbursement/unionChairman.html") @RequiresPermissions("sys.jf.reimbursement.unionChairman") public void index() { } + /** 手机页面可携带 id 报销主键和 mode=audit/view;权限名称与 PC 一致。 */ + @At + @Ok("beetl:/mobile/jf/reimbursement/unionChairman/index.html") + @RequiresPermissions("sys.jf.reimbursement.unionChairman") + public void mobile() { + } + /** + * pageForm 含 pageNumber/pageSize、searchName(u.username/u.loginname)、searchKeyword 和排序字段/方向; + * year 为年度,clubId/unionId/unitId 为组织ID,activityName 为活动名,isAudit 为0全部/1已审核/2未审核。 + * 返回统一结果,code=0 时 data 为 Pagination,list 为本页记录、totalCount 为总数。 + */ @At @ViReturn @RequiresPermissions("sys.jf.reimbursement.unionChairman") public Object pageData(PageForm pageForm, @Param(value = "year", required = false) Integer year, + @Param(value = "clubId", required = false) String clubId, @Param(value = "unionId", required = false) String unionId, @Param(value = "unitId", required = false) String unitId, @Param(value = "activityName", required = false) String activityName, @Param(value = "isAudit", required = false) String isAudit) { - Sql sql = activityBxService.activityReimbursementSql(); - Cnd cnd = Cnd.NEW(); - cnd.andEX("act.`year`","=",year); - if (StrUtil.isNotBlank(activityName)) { - cnd.and(Cnd.likeEX("act.activity_name", activityName)); - } - // 姓名/工号查询由页面通过 PageForm 统一传入,这里按所选字段做模糊匹配。 - if (StrUtil.isNotBlank(pageForm.getSearchName()) && StrUtil.isNotBlank(pageForm.getSearchKeyword())) { - cnd.and(Cnd.likeEX(pageForm.getSearchName(), pageForm.getSearchKeyword())); - } - - //1已审核;2未审核;0全部 - cnd.and("act.reimbursement_state_id", "1".equals(isAudit) ? ">" : "2".equals(isAudit) ? "=" : ">=", 1010); - - if (ShiroUtil.hasAnyRoles("sysadmin,A06")){ - cnd.andEX("u.unionid","=",unionId); - } else { - cnd.and("u.unionid","=", Vi.getUnionId()); - } - cnd.andEX("u.unitId","=",unitId); - sql.setCondition(cnd); - return activityBxService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql); + return activityBxService.reimbursementPage("unionChairman", pageForm, NutMap.NEW() + .addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId) + .addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit)); } - - /** - * 基层工会活动报销 - * 基层工会活动——>基层工会主席审核——>校工会主席审核——>流程结束 - * @param id - * @param audit - * @return - */ + /** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */ @At @ViReturn @RequiresPermissions("sys.jf.reimbursement.unionChairman") + public Object findOne(@Param("id") String id) { + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动报销申请"); + return activityBxService.reimbursementDetail("unionChairman", id); + } + + /** + * id 为报销主键;audit 为 JSON 对象,auditOpinion 为1至500字意见,auditSign 为签字图片或历史路径, + * auditPass 为 true通过/false拒绝。人员和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为提示,data 为空。 + */ + @At + @ViReturn + @Aop(TransAop.READ_COMMITTED) + @RequiresPermissions("sys.jf.reimbursement.unionChairman") @SLog(type = "文体活动", tag = "活动报销", msg = "分工会主席审核", param = true, result = true) - public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){ - audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString()); - audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString()); - audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString()); - audit.setAuditTime(new Date()); - activityBxService.dao().insert(audit); - - Chain chain = Chain.make("reimbursement_union_audit_id", audit.getId()); - //通过,校工会主席审核 - chain.add("reimbursement_state_id", audit.getAuditPass() ? 1020 : 1015); - activityBxService.update(chain,Cnd.where("id","=",id)); - - ReimbursementToHandler.COMPLETE_UNION_TASK.exec(id, NutMap.NEW().addv("nodeName",audit.getAuditPass() ? "基层工会通过" : "基层工会拒绝")); - if (audit.getAuditPass()) { - ReimbursementToHandler.CREATE_SCHOOL_DIRECTOR_TASK.exec(id,null); - } else { - ReimbursementToHandler.REFUSE_UNION_TASK.exec(id,NutMap.NEW().addv("nodeName","基层工会拒绝")); + public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) { + if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) { + throw new IllegalArgumentException("请选择活动报销申请并填写审核信息"); } + activityBxService.reviewReimbursement("unionChairman", id, audit); return null; } } diff --git a/src/main/java/io/v/nutz/zhgh/jf/handle/funds/FundsToHandler.java b/src/main/java/io/v/nutz/zhgh/jf/handle/funds/FundsToHandler.java index 78e5134..383e77f 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/handle/funds/FundsToHandler.java +++ b/src/main/java/io/v/nutz/zhgh/jf/handle/funds/FundsToHandler.java @@ -107,8 +107,8 @@ public enum FundsToHandler { loginNames, "/platform/jf/activity_bx/association", "/platform/jf/activity_bx/association", - "/platform/jf/activity_bx/association", - "/platform/jf/activity_bx/association" + "/platform/jf/activity_bx/association/mobile?id=" + reimbursementId + "&mode=audit", + "/platform/jf/activity_bx/association/mobile?id=" + reimbursementId + "&mode=view" ); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "协会负责人审核"); @@ -144,8 +144,8 @@ public enum FundsToHandler { loginNames, "/platform/jf/activity_bx/division", "/platform/jf/activity_bx/division", - "/platform/jf/activity_bx/division", - "/platform/jf/activity_bx/division" + "/platform/jf/activity_bx/division/mobile?id=" + reimbursementId + "&mode=audit", + "/platform/jf/activity_bx/division/mobile?id=" + reimbursementId + "&mode=view" ); localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "基层工会审核"); @@ -181,8 +181,8 @@ public enum FundsToHandler { loginNames, "/platform/jf/activity_bx/accounting", "/platform/jf/activity_bx/accounting", - "/platform/jf/activity_bx/accounting", - "/platform/jf/activity_bx/accounting" + "/platform/jf/activity_bx/accounting/mobile?id=" + reimbursementId + "&mode=audit", + "/platform/jf/activity_bx/accounting/mobile?id=" + reimbursementId + "&mode=view" ); localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "校工会负责人审核"); @@ -217,8 +217,8 @@ public enum FundsToHandler { loginNames, "/platform/jf/activity_bx/union", "/platform/jf/activity_bx/union", - "/platform/jf/activity_bx/union", - "/platform/jf/activity_bx/union" + "/platform/jf/activity_bx/union/mobile?id=" + reimbursementId + "&mode=audit", + "/platform/jf/activity_bx/union/mobile?id=" + reimbursementId + "&mode=view" ); localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "校工会主席审核"); diff --git a/src/main/java/io/v/nutz/zhgh/jf/handle/reimbursement/ReimbursementToHandler.java b/src/main/java/io/v/nutz/zhgh/jf/handle/reimbursement/ReimbursementToHandler.java index a3e66f4..a78af78 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/handle/reimbursement/ReimbursementToHandler.java +++ b/src/main/java/io/v/nutz/zhgh/jf/handle/reimbursement/ReimbursementToHandler.java @@ -129,8 +129,8 @@ public enum ReimbursementToHandler { unionLeader, "/platform/jf/reimbursement/unionChairman", "/platform/jf/reimbursement/unionChairman", - "/platform/jf/reimbursement/unionChairman", - "/platform/jf/reimbursement/unionChairman" + "/platform/jf/reimbursement/unionChairman/mobile?id=" + reimbursementId + "&mode=audit", + "/platform/jf/reimbursement/unionChairman/mobile?id=" + reimbursementId + "&mode=view" ); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "基层工会审核"); @@ -173,8 +173,8 @@ public enum ReimbursementToHandler { clubLeader, "/platform/jf/reimbursement/clubChairman", "/platform/jf/reimbursement/clubChairman", - "/platform/jf/reimbursement/clubChairman", - "/platform/jf/reimbursement/clubChairman" + "/platform/jf/reimbursement/clubChairman/mobile?id=" + reimbursementId + "&mode=audit", + "/platform/jf/reimbursement/clubChairman/mobile?id=" + reimbursementId + "&mode=view" ); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "协会负责人审核"); @@ -214,8 +214,8 @@ public enum ReimbursementToHandler { List.of("1019911404"), "/platform/jf/reimbursement/schoolDirector", "/platform/jf/reimbursement/schoolDirector", - "/platform/jf/reimbursement/schoolDirector", - "/platform/jf/reimbursement/schoolDirector" + "/platform/jf/reimbursement/schoolDirector/mobile?id=" + reimbursementId + "&mode=audit", + "/platform/jf/reimbursement/schoolDirector/mobile?id=" + reimbursementId + "&mode=view" ); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "校工会负责人审核"); @@ -257,8 +257,8 @@ public enum ReimbursementToHandler { schoolChairmanLeader, "/platform/jf/reimbursement/schoolChairman", "/platform/jf/reimbursement/schoolChairman", - "/platform/jf/reimbursement/schoolChairman", - "/platform/jf/reimbursement/schoolChairman" + "/platform/jf/reimbursement/schoolChairman/mobile?id=" + reimbursementId + "&mode=audit", + "/platform/jf/reimbursement/schoolChairman/mobile?id=" + reimbursementId + "&mode=view" ); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "校工会主席审核"); diff --git a/src/main/java/io/v/nutz/zhgh/jf/service/ActivityBxService.java b/src/main/java/io/v/nutz/zhgh/jf/service/ActivityBxService.java index a85079a..db49f64 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/service/ActivityBxService.java +++ b/src/main/java/io/v/nutz/zhgh/jf/service/ActivityBxService.java @@ -2,11 +2,15 @@ package io.v.nutz.zhgh.jf.service; import io.v.nutz.zhgh.jf.model.ActivityBx; import io.v.nutz.base.service.ViService; +import io.v.nutz.base.model.Audit; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; import org.nutz.lang.util.NutMap; import java.io.OutputStream; +import java.util.List; +import cn.wizzer.framework.page.Pagination; +import io.v.nutz.sys.models.Sys_club; /** * @author zhf @@ -20,6 +24,39 @@ public interface ActivityBxService extends ViService { NutMap queryOne(String id); + /** 按年度、协会、审核状态及 PC 筛选条件分页查询;始终限制为当前用户管理的协会活动。 */ + Pagination associationPage(NutMap filters); + + /** 返回当前用户管理的协会,供手机筛选;管理员可查看全部协会。 */ + List associationClubs(); + + /** 校验协会数据权限后返回详情,canReview 标识当前是否仍可审核。 */ + NutMap associationDetail(String id); + + /** 校验待审状态,记录意见和签字,并处理下一环节待办及通知。 */ + void reviewAssociation(String id, String idea, String sign, boolean flag); + + /** stage 为 Controller 固定的 division/accounting/union;filters 包含 PC 兼容筛选和分页参数。 */ + Pagination reviewPage(String stage, NutMap filters); + + /** 按审核节点及登录用户数据范围读取申请,返回详情及 canReview 标识。 */ + NutMap reviewDetail(String stage, String id); + + /** 校验申请可访问后返回原 PC 所需物品明细。 */ + List reviewGoods(String stage, String id); + + /** form 包含 id/idea/sign/flag,校验当前待审状态后记录审核并执行原节点流转。 */ + void reviewActivity(String stage, NutMap form); + + /** stage 为服务端固定的报销节点;pageForm 为分页/姓名工号/排序,filters 为年度、组织、活动名和0全部/1已审/2未审。返回 list/totalCount。 */ + Pagination reimbursementPage(String stage, io.v.nutz.base.query.PageForm pageForm, NutMap filters); + + /** id 为活动报销主键;按节点数据范围校验,返回详情、报销状态名称及 canReview 是否可审核。 */ + NutMap reimbursementDetail(String stage, String id); + + /** id 为报销主键,audit 仅接收 auditOpinion/auditSign/auditPass;服务端记录身份、事务更新状态并流转,无返回值。 */ + void reviewReimbursement(String stage, String id, Audit audit); + void exportByView(String id, OutputStream outputStream); void exportByCon(String id, OutputStream outputStream); diff --git a/src/main/java/io/v/nutz/zhgh/jf/service/impl/ActivityBxServiceImpl.java b/src/main/java/io/v/nutz/zhgh/jf/service/impl/ActivityBxServiceImpl.java index feaf750..c0def8c 100644 --- a/src/main/java/io/v/nutz/zhgh/jf/service/impl/ActivityBxServiceImpl.java +++ b/src/main/java/io/v/nutz/zhgh/jf/service/impl/ActivityBxServiceImpl.java @@ -13,6 +13,18 @@ import io.v.nutz.zhgh.activity.models.ActivityTissue; import io.v.nutz.base.utils.DateUtil; import io.v.nutz.base.utils.Html2Text; import io.v.nutz.base.utils.Vi; +import io.v.nutz.base.utils.MsgApi; +import io.v.nutz.base.utils.Roles; +import cn.wizzer.framework.page.Pagination; +import io.v.nutz.sys.models.Sys_club; +import io.v.nutz.sys.models.Sys_signature; +import io.v.nutz.sys.models.Sys_user; +import io.v.nutz.sys.models.Sys_user_role; +import io.v.nutz.sys.models.User; +import io.v.nutz.sys.services.SysSignatureService; +import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler; +import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler; +import io.v.nutz.base.query.PageForm; import io.v.nutz.base.model.Goods; import io.v.nutz.zhgh.jf.model.ActivityBx; import io.v.nutz.zhgh.jf.model.ActivityFile; @@ -24,6 +36,7 @@ import io.v.nutz.web.commons.base.Globals; import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.boot.starter.ftp.FtpService; import org.nutz.dao.Cnd; +import org.nutz.dao.Chain; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; @@ -43,6 +56,7 @@ import java.util.Base64; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -61,6 +75,492 @@ public class ActivityBxServiceImpl extends ViServiceImpl implements @Inject private FtpService ftpService; + @Inject + private SysSignatureService sysSignatureService; + @Inject + private MsgApi msgApi; + + /** 报销节点由各 Controller 固定,不能由客户端将申请送往其他审核环节。 */ + private int reimbursementState(String stage) { + return switch (stage) { + case "clubChairman" -> 1000; + case "unionChairman" -> 1010; + case "schoolDirector" -> 1020; + case "schoolChairman" -> 1040; + default -> throw new IllegalArgumentException("不支持的报销审核环节"); + }; + } + + /** 沿用四个 PC 节点的数据范围,列表、详情和审核共用;未关联组织时拒绝访问。 */ + private Cnd reimbursementScope(String stage) { + Cnd scope = Cnd.where("act.reimbursement_state_id", ">=", reimbursementState(stage)); + if (!ShiroUtil.hasAnyRoles("sysadmin,A06")) { + if ("clubChairman".equals(stage)) { + String clubId = vi.getClubId(); + if (Strings.isBlank(clubId)) throw new IllegalArgumentException("当前用户未关联负责的协会"); + scope.and("act.clubId", "=", clubId); + } else if ("unionChairman".equals(stage)) { + String unionId = Vi.getUnionId(); + if (Strings.isBlank(unionId)) throw new IllegalArgumentException("当前用户未关联基层工会"); + scope.and("u.unionid", "=", unionId); + } + } + return scope; + } + + /** PC 与手机共用报销筛选口径,字段白名单避免把客户端输入作为 SQL 列名执行。 */ + @Override + public Pagination reimbursementPage(String stage, PageForm pageForm, NutMap filters) { + Cnd cnd = reimbursementScope(stage); + int audit = filters.getInt("isAudit", 2); + if (audit < 0 || audit > 2) throw new IllegalArgumentException("审核状态筛选无效"); + cnd.and("act.reimbursement_state_id", audit == 1 ? ">" : audit == 2 ? "=" : ">=", reimbursementState(stage)); + Vi.cndPlus(cnd, "act.year", "=", filters.get("year")); + String activityName = filters.getString("activityName"); + if (Strings.isNotBlank(activityName)) cnd.and(Cnd.likeEX("act.activity_name", activityName.trim())); + // 组织筛选仍遵循原节点:协会页不加单位条件,基层工会页不加协会条件。 + if (!"clubChairman".equals(stage)) { + Vi.cndPlus(cnd, "u.unitId", "=", filters.getString("unitId")); + if (!"unionChairman".equals(stage) || ShiroUtil.hasAnyRoles("sysadmin,A06")) { + Vi.cndPlus(cnd, "u.unionid", "=", filters.getString("unionId")); + } + } + if (!"unionChairman".equals(stage) && (!"clubChairman".equals(stage) || ShiroUtil.hasAnyRoles("sysadmin,A06"))) { + Vi.cndPlus(cnd, "act.clubId", "=", filters.getString("clubId")); + } + Map searchColumns = Map.of("u.username", "u.username", "u.loginname", "u.loginname"); + String searchColumn = searchColumns.get(Strings.sNull(pageForm.getSearchName())); + if (searchColumn != null && Strings.isNotBlank(pageForm.getSearchKeyword())) { + cnd.and(Cnd.likeEX(searchColumn, pageForm.getSearchKeyword().trim())); + } + Map orderColumns = Map.ofEntries( + Map.entry("activity_name", "act.activity_name"), Map.entry("user_name", "act.user_name"), + Map.entry("bxr_loginname", "u.loginname"), Map.entry("unionname", "u.unionname"), + Map.entry("unitname", "u.unitname"), Map.entry("clubName", "sc.name"), + Map.entry("activity_type", "act.activity_type"), Map.entry("absName", "abs.name"), + Map.entry("reimbursement_apply_time", "act.reimbursement_apply_time"), + Map.entry("reimbursement_state_id", "act.reimbursement_state_id")); + String orderColumn = orderColumns.get(Strings.sNull(pageForm.getPageOrderName())); + String direction = Strings.sNull(pageForm.getPageOrderBy()); + if (orderColumn != null && List.of("asc", "desc", "ascending", "descending").contains(direction)) { + cnd.orderBy(orderColumn, direction.startsWith("asc") ? "asc" : "desc"); + } else { + cnd.asc("act.reimbursement_state_id").desc("act.reimbursement_apply_time"); + } + // 相同状态和时间也有固定顺序,防止手机连续分页时重复或遗漏记录。 + cnd.desc("act.id"); + Sql sql = activityReimbursementSql(); + sql.setCondition(cnd); + int page = pageForm.getPageNumber() == null ? 1 : Math.max(1, pageForm.getPageNumber()); + int size = pageForm.getPageSize() == null ? 10 : Math.min(100, Math.max(1, pageForm.getPageSize())); + return listPageMap(page, size, sql); + } + + /** 按节点校验可访问记录,禁止凭 ID 跨协会/工会读取收款信息或提交审核。 */ + private NutMap requireReimbursement(String stage, String id) { + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动报销申请"); + Sql sql = activityReimbursementSql(); + sql.setCondition(reimbursementScope(stage).and("act.id", "=", id)); + sql.setCallback(Sqls.callback.map()); + dao().execute(sql); + NutMap record = (NutMap) sql.getResult(); + if (record == null || record.isEmpty()) throw new IllegalArgumentException("报销申请不存在或无权访问"); + return record; + } + + /** 复用现有附件和审核记录组装,仅为报销页覆盖状态名称,不改变申报详情接口。 */ + @Override + public NutMap reimbursementDetail(String stage, String id) { + NutMap record = requireReimbursement(stage, id); + NutMap detail = queryOne(id); + if (detail == null) throw new IllegalArgumentException("报销申请不存在"); + detail.setv("application_state_name", detail.get("state_name")); + detail.setv("state_name", record.get("state_name")); + detail.setv("state_color", record.get("state_color")); + detail.setv("clubName", record.get("clubName")); + detail.setv("bxr_unionname", record.get("unionname")); + detail.setv("bxr_unitname", record.get("unitname")); + detail.setv("canReview", detail.getInt("reimbursement_state_id") == reimbursementState(stage)); + return detail; + } + + /** 校验意见、签字和当前状态;只接收业务字段,审核身份、时间与主键由服务端生成。 */ + @Override + @Aop(TransAop.READ_COMMITTED) + public void reviewReimbursement(String stage, String id, Audit input) { + if (input == null || input.getAuditPass() == null || Strings.isBlank(input.getAuditOpinion()) + || input.getAuditOpinion().trim().length() > 500 || Strings.isBlank(input.getAuditSign())) { + throw new IllegalArgumentException("请填写1至500字审核意见并签字,选择审核结果"); + } + int state = reimbursementState(stage); + NutMap record = requireReimbursement(stage, id); + if (record.getInt("reimbursement_state_id") != state) throw new IllegalArgumentException("该报销已被处理,请刷新列表"); + boolean pass = input.getAuditPass(); + int nextState = pass ? (state <= 1010 ? 1020 : state == 1020 ? 1040 : 1061) : state + 5; + // 条件更新先取得审核权;PC、手机同时提交时只有一方能写审核记录及创建后续待办。 + int changed = dao().update(ActivityBx.class, Chain.make("reimbursement_state_id", nextState), + Cnd.where("id", "=", id).and("reimbursement_state_id", "=", state)); + if (changed != 1) throw new IllegalArgumentException("该报销已被处理,请刷新列表"); + Audit audit = new Audit().setAuditPass(pass).setAuditOpinion(input.getAuditOpinion().trim()) + .setAuditSign(input.getAuditSign()).setAuditor(ShiroUtil.getPrincipalProperty("id").toString()) + .setUsername(ShiroUtil.getPrincipalProperty("username").toString()) + .setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString()).setAuditTime(new java.util.Date()); + dao().insert(audit); + String auditField = switch (stage) { + case "clubChairman" -> "reimbursement_club_audit_id"; + case "unionChairman" -> "reimbursement_union_audit_id"; + case "schoolDirector" -> "reimbursement_school_director_id"; + default -> "reimbursement_school_chairman_id"; + }; + dao().update(ActivityBx.class, Chain.make(auditField, audit.getId()), Cnd.where("id", "=", id)); + String title = switch (stage) { + case "clubChairman" -> "协会负责人"; + case "unionChairman" -> "基层工会"; + case "schoolDirector" -> "校工会负责人"; + default -> "校工会主席"; + }; + String nodeName = title + (pass ? "通过" : "拒绝"); + ReimbursementToHandler complete = switch (stage) { + case "clubChairman" -> ReimbursementToHandler.COMPLETE_CLUB_TASK; + case "unionChairman" -> ReimbursementToHandler.COMPLETE_UNION_TASK; + case "schoolDirector" -> ReimbursementToHandler.COMPLETE_SCHOOL_DIRECTOR_TASK; + default -> ReimbursementToHandler.COMPLETE_SCHOOL_CHAIRMAN_TASK; + }; + // 保持原报销待办流转及节点文案;拒绝先完成当前任务,再结束流程。 + complete.exec(id, NutMap.NEW().addv("nodeName", nodeName)); + if (!pass) { + ReimbursementToHandler.REFUSE_UNION_TASK.exec(id, NutMap.NEW().addv("nodeName", nodeName)); + } else if (state <= 1010) { + ReimbursementToHandler.CREATE_SCHOOL_DIRECTOR_TASK.exec(id, null); + } else if (state == 1020) { + ReimbursementToHandler.CREATE_SCHOOL_CHAIRMAN_TASK.exec(id, null); + } else { + ReimbursementToHandler.COMPLETE_PROCESS.exec(id, null); + } + } + + /** 节点由服务端 Controller 固定传入,不接受客户端自行指定审核环节。 */ + private int reviewState(String stage) { + return switch (stage) { + case "division" -> 1010; + case "accounting" -> 1020; + case "union" -> 1040; + default -> throw new IllegalArgumentException("不支持的审核环节"); + }; + } + + /** 与原 PC 列表保持一致:基层工会限定归属,女工角色在负责人节点仅查看女工项目。 */ + private Cnd reviewScope(String stage) { + reviewState(stage); + Cnd scope = Cnd.NEW(); + if ("division".equals(stage) && !ShiroUtil.hasAnyRoles("sysadmin") + && ShiroUtil.hasAnyRoles(new String[]{"H04", "gh01", "gh02"})) { + String unionId = Vi.getUnionId(); + if (Strings.isBlank(unionId)) throw new IllegalArgumentException("当前用户未关联基层工会"); + scope.and("un.id", "=", unionId); + } + if ("accounting".equals(stage) && ShiroUtil.hasRole("xghng")) { + scope.and("act.projectTypeCode", "=", 50002); + } + return scope; + } + + /** 三个节点共享查询结构;保留 PC 字段名,同时提供手机详情需要的组织名称。 */ + private Sql reviewSql() { + return Sqls.create(""" + SELECT act.*, u.loginname bxr_loginname, u.unionname bxr_unionname, + u.unitname bxr_unitname, s.state_name, s.state_color, + abs.name absName, cl.name clubName + FROM activity_bx act + LEFT JOIN `user` u ON act.user_id = u.id + LEFT JOIN sys_unit su ON su.id = u.unitid + LEFT JOIN sys_union un ON su.unionid = un.id + LEFT JOIN state s ON act.state_id = s.state_id + LEFT JOIN activity_basic_settings abs ON abs.code = act.projectJfCode + LEFT JOIN sys_club cl ON cl.id = act.clubId + $condition + """); + } + + @Override + public Pagination reviewPage(String stage, NutMap filters) { + Cnd cnd = reviewScope(stage); + int audit = filters.getInt("isAudit", 3); + cnd.and("act.state_id", audit == 1 ? ">=" : audit == 2 ? ">" : "=", reviewState(stage)); + Vi.cndPlus(cnd, "act.year", "=", filters.get("year")); + Vi.cndPlus(cnd, "u.unitid", "=", filters.getString("unitId")); + Vi.cndPlus(cnd, "u.unionid", "=", filters.getString("unionId")); + Map searches = Map.of("username", "u.username", "loginname", "u.loginname"); + String search = searches.get(filters.getString("searchName", "")); + if (search != null && Strings.isNotBlank(filters.getString("searchKeyword"))) { + cnd.where().andLike(search, filters.getString("searchKeyword").trim()); + } + // 排序仅允许 PC 已提供的字段,避免将客户端任意字符串当成 SQL 字段或方向。 + Map orders = Map.of("bxr_unionname", "u.unionname", "activity_type", "act.activity_type", + "absName", "abs.name", "apply_type", "act.apply_type", "apply_time", "act.apply_time", "state_id", "act.state_id"); + String orderColumn = orders.get(filters.getString("pageOrderName", "")); + String direction = filters.getString("pageOrderBy", ""); + if (orderColumn != null && List.of("asc", "desc", "ascending", "descending").contains(direction)) { + cnd.orderBy(orderColumn, direction.startsWith("asc") ? "asc" : "desc"); + } else { + cnd.asc("act.state_id").desc("act.apply_time"); + } + cnd.desc("act.id"); + Sql sql = reviewSql(); + sql.setCondition(cnd); + return listPageMap(Math.max(1, filters.getInt("pageNumber", 1)), + Math.min(100, Math.max(1, filters.getInt("pageSize", 10))), sql); + } + + /** id 为申请ID;列表、详情及提交使用同一节点范围,防止通过替换 ID 跨范围读取或审批。 */ + private NutMap requireReviewRecord(String stage, String id) { + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动申请"); + Sql sql = reviewSql(); + sql.setCondition(reviewScope(stage).and("act.id", "=", id).and("act.state_id", ">=", reviewState(stage))); + sql.setCallback(Sqls.callback.map()); + dao().execute(sql); + NutMap record = (NutMap) sql.getResult(); + if (record == null || record.isEmpty()) throw new IllegalArgumentException("申请不存在或无权访问"); + return record; + } + + @Override + public NutMap reviewDetail(String stage, String id) { + NutMap record = requireReviewRecord(stage, id); + NutMap detail = queryOne(id); + if (detail == null) throw new IllegalArgumentException("申请不存在"); + detail.setv("canReview", detail.getInt("state_id") == reviewState(stage)); + detail.setv("clubName", record.getString("clubName")); + detail.setv("bxr_unionname", record.getString("bxr_unionname")); + detail.setv("bxr_unitname", record.getString("bxr_unitname")); + return detail; + } + + @Override + public List reviewGoods(String stage, String id) { + requireReviewRecord(stage, id); + return dao().query(Goods.class, Cnd.where("reid", "=", id)); + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void reviewActivity(String stage, NutMap form) { + int state = reviewState(stage); + String id = form.getString("id"); + String idea = form.getString("idea"); + String sign = form.getString("sign"); + if (!(form.get("flag") instanceof Boolean) || Strings.isBlank(id) || Strings.isBlank(idea) + || idea.trim().length() > 500 || Strings.isBlank(sign)) { + throw new IllegalArgumentException("请填写1至500字审核意见并签字,选择审核结果"); + } + boolean flag = form.getBoolean("flag"); + NutMap record = requireReviewRecord(stage, id); + if (record.getInt("state_id") != state) throw new IllegalArgumentException("该申请已被处理,请刷新列表"); + ActivityBx activity = fetch(id); + if (activity == null) throw new IllegalArgumentException("申请不存在"); + String field = "division".equals(stage) ? "division_chairman" : "accounting".equals(stage) ? "accounting" : "union_chairman"; + String title = "division".equals(stage) ? "基层工会主席" : "accounting".equals(stage) ? "校工会负责人" : "校工会主席"; + String next = "division".equals(stage) ? "校工会负责人审核" : "accounting".equals(stage) ? "校工会主席审核" : "审核通过"; + String nodeName = flag ? next : title + "拒绝"; + int nextState = flag ? (state == 1010 ? 1020 : state == 1020 ? 1040 : 1060) : state + 5; + Chain changes = Chain.make("state_id", String.valueOf(nextState)).add(field + "_name", ShiroUtil.getPrincipalProperty("id").toString()) + .add(field + "_time", DateUtil.getDate()).add(field + "_idea", idea.trim()); + // PC 已取消费用编辑;没有提交该字段时保留原值,不能用缺省空值覆盖实际费用。 + if ("union".equals(stage) && form.containsKey("activity_money") && form.get("activity_money") != null) { + Double money = form.getDouble("activity_money"); + if (money == null || !Double.isFinite(money) || money < 0) throw new IllegalArgumentException("实际费用格式错误"); + changes.add("activity_money", money); + } + // 先用待审状态条件取得本次审核权,并发失败不写签字、不创建活动、不发送通知。 + int changed = dao().update(ActivityBx.class, changes, Cnd.where("id", "=", id).and("state_id", "=", state)); + if (changed != 1) throw new IllegalArgumentException("该申请已被处理,请刷新列表"); + String signatureId = sysSignatureService.insert(new Sys_signature(sign)).getId(); + dao().update(ActivityBx.class, Chain.make(field, signatureId), Cnd.where("id", "=", id)); + + // 完成当前任务后再拒绝流程,确保审核人、审核意见及完成时间能够写入当前待办。 + FundsToHandler complete = "division".equals(stage) ? FundsToHandler.COMPLETE_UNION_TASK + : "accounting".equals(stage) ? FundsToHandler.COMPLETE_SCHOOL_FZR_TASK : FundsToHandler.COMPLETE_SCHOOL_TASK; + complete.exec(id, new NutMap().addv("option", nodeName), null); + if (!flag) { + FundsToHandler.REFUSE_TASK.exec(id, new NutMap().addv("nodeName", nodeName), null); + Sys_user applicant = dao().fetch(Sys_user.class, activity.getUser_id()); + if (applicant != null) { + msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + activity.getActivity_name() + + "】被" + title + "拒绝,审核意见:" + idea.trim(), List.of(applicant.getLoginname()), null); + } + return; + } + if ("union".equals(stage)) { + createApprovedActivity(activity); + Sys_user applicant = dao().fetch(Sys_user.class, activity.getUser_id()); + if (applicant != null) msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + activity.getActivity_name() + + "】已审核通过,请前往智慧工会进行查看。", List.of(applicant.getLoginname()), null); + return; + } + Sys_user reviewer; + if ("division".equals(stage)) { + // 沿用 PC 指定的校工会负责人账号,不改变待办接收规则。 + reviewer = dao().fetch(Sys_user.class, Cnd.where("loginname", "=", "1019911404")); + } else { + Sys_user_role role = dao().fetch(Sys_user_role.class, Cnd.where("roleId", "=", Roles.GH_WYH_ZX)); + reviewer = role == null ? null : dao().fetch(Sys_user.class, role.getUserId()); + } + if (reviewer == null || Strings.isBlank(reviewer.getLoginname())) { + throw new IllegalArgumentException("未配置下一环节审核人,请联系管理员"); + } + List reviewers = List.of(reviewer.getLoginname()); + FundsToHandler create = "division".equals(stage) ? FundsToHandler.CREATE_SCHOOL_FZR_TASK : FundsToHandler.CREATE_SCHOOL_TASK; + create.exec(id, null, reviewers); + msgApi.sendWxMsg("活动申报", "", "text", activity.getUser_name() + "申报了一条活动【" + activity.getActivity_name() + + "】,请前往智慧工会进行审核。", reviewers, null); + } + + /** 主席通过后沿用 PC 的活动生成字段;已有活动关联时不重复生成,事务随审核一起提交。 */ + @Aop(TransAop.READ_COMMITTED) + protected void createApprovedActivity(ActivityBx activity) { + if (Strings.isNotBlank(activity.getActivity_id())) return; + User applicant = dao().fetch(User.class, Cnd.where("id", "=", activity.getUser_id())); + if (applicant == null) throw new IllegalArgumentException("申请人信息不存在"); + ActivityTissue tissue = new ActivityTissue(); + tissue.setId(activity.getId()); + tissue.setApplyTime(DateUtil.getDate()); + tissue.setType(activity.getActivity_type()); + tissue.setActivityCode(activity.getProjectCode()); + tissue.setName(activity.getActivity_name()); + tissue.setUserId(activity.getUser_id()); + tissue.setStartPlannedDate(activity.getStartPlannedDate()); + tissue.setEndPlannedDate(activity.getEndPlannedDate()); + tissue.setStartTime(activity.getStartTime()); + tissue.setEndTime(activity.getEndTime()); + tissue.setAddress(activity.getAddress()); + if (Integer.valueOf(40002).equals(activity.getActivity_type())) tissue.setUnionId(applicant.getUnionid()); + else if (Integer.valueOf(40003).equals(activity.getActivity_type())) tissue.setClubId(activity.getClubId()); + tissue.setProjectTypeCode("50006"); + dao().insert(tissue); + dao().update(ActivityBx.class, Chain.make("activity_id", tissue.getId()), Cnd.where("id", "=", activity.getId())); + } + + @Override + public Pagination associationPage(NutMap filters) { + Sql sql = Sqls.create(""" + SELECT act.*, u.loginname bxr_loginname, u.unionname bxr_unionname, + s.state_name, s.state_color, abs.name absName, cl.name clubName + FROM activity_bx act + LEFT JOIN `user` u ON act.user_id = u.id + LEFT JOIN state s ON act.state_id = s.state_id + LEFT JOIN activity_basic_settings abs ON abs.code = act.projectJfCode + LEFT JOIN sys_club cl ON cl.id = act.clubId + $condition + """); + Cnd cnd = associationScope("act."); + int audit = filters.getInt("isAudit", 3); + // “已审核”沿用 PC 的环节口径,包括拒绝和进入后续环节的申请。 + cnd.and("act.state_id", audit == 1 ? ">=" : audit == 2 ? ">" : "=", 1000); + Vi.cndPlus(cnd, "act.year", "=", filters.get("year")); + Vi.cndPlus(cnd, "act.clubId", "=", filters.getString("clubId")); + Vi.cndPlus(cnd, "u.unitid", "=", filters.getString("unitId")); + Vi.cndPlus(cnd, "u.unionid", "=", filters.getString("unionId")); + Map searchColumns = Map.of("username", "u.username", "loginname", "u.loginname"); + String searchColumn = searchColumns.get(filters.getString("searchName", "")); + if (searchColumn != null && Strings.isNotBlank(filters.getString("searchKeyword"))) { + cnd.where().andLike(searchColumn, filters.getString("searchKeyword")); + } + // 不将客户端字段名直接拼入 SQL;兼容 PC 表格已有排序字段。 + Map orderColumns = Map.of( + "bxr_unionname", "u.unionname", "activity_type", "act.activity_type", + "absName", "abs.name", "apply_type", "act.apply_type", + "apply_time", "act.apply_time", "state_id", "act.state_id"); + String orderColumn = orderColumns.get(filters.getString("pageOrderName", "")); + String order = filters.getString("pageOrderBy", ""); + if (orderColumn != null && List.of("asc", "desc", "ascending", "descending").contains(order)) { + cnd.orderBy(orderColumn, order.startsWith("asc") ? "asc" : "desc"); + } else { + cnd.asc("act.state_id").desc("act.apply_time"); + } + cnd.desc("act.id"); + sql.setCondition(cnd); + return listPageMap(Math.max(1, filters.getInt("pageNumber", 1)), + Math.min(100, Math.max(1, filters.getInt("pageSize", 10))), sql); + } + + /** alias 为查询表别名前缀(实体查询传空串);协会范围来自登录用户角色,不能由前端放宽。 */ + private Cnd associationScope(String alias) { + Cnd cnd = Cnd.where(alias + "activity_type", "=", 40003); + if (!ShiroUtil.hasAnyRoles("sysadmin")) { + cnd.and(alias + "clubId", "in", vi.getMangeClubStr()); + } + return cnd; + } + + @Override + public List associationClubs() { + Cnd cnd = Cnd.NEW(); + if (!ShiroUtil.hasAnyRoles("sysadmin")) { + cnd.and("id", "in", vi.getMangeClubStr()); + } + return dao().query(Sys_club.class, cnd.asc("name")); + } + + /** 详情和提交均校验数据归属,禁止仅凭业务 ID 读取或审核其他协会的申请。 */ + private ActivityBx requireAssociation(String id) { + ActivityBx activity = fetch(associationScope("").and("id", "=", id).and("state_id", ">=", 1000)); + if (activity == null) { + throw new IllegalArgumentException("申请不存在或无权访问"); + } + return activity; + } + + @Override + public NutMap associationDetail(String id) { + requireAssociation(id); + NutMap detail = queryOne(id); + if (detail == null) { + throw new IllegalArgumentException("申请不存在"); + } + detail.setv("canReview", "1000".equals(detail.getString("state_id"))); + Sys_club club = dao().fetch(Sys_club.class, detail.getString("clubId")); + detail.setv("clubName", club == null ? detail.getString("fundsUnitName") : club.getName()); + return detail; + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void reviewAssociation(String id, String idea, String sign, boolean flag) { + if (Strings.isBlank(id) || Strings.isBlank(idea) || idea.trim().length() > 500 || Strings.isBlank(sign)) { + throw new IllegalArgumentException("请填写完整的审核信息"); + } + ActivityBx activity = requireAssociation(id); + // 带状态条件的更新取得本次审核权;并发请求只有一个能成功,失败请求不产生签字、待办或通知。 + int changed = dao().update(ActivityBx.class, Chain.make("state_id", flag ? "1020" : "1005") + .add("association_principal_id", ShiroUtil.getUserId()) + .add("association_principal_time", DateUtil.getDate()) + .add("association_principal_idea", idea.trim()), + associationScope("").and("id", "=", id).and("state_id", "=", 1000)); + if (changed != 1) { + throw new IllegalArgumentException("该申请已被处理,请刷新列表"); + } + String signatureId = sysSignatureService.insert(new Sys_signature(sign)).getId(); + dao().update(ActivityBx.class, Chain.make("association_principalSign_id", signatureId), + Cnd.where("id", "=", id)); + String nodeName = flag ? "校工会负责人审核" : "协会审核拒绝"; + // 先完成协会任务并记录处理人,再进入下一环节或结束流程,避免拒绝时先关闭任务丢失处理记录。 + FundsToHandler.COMPLETE_CLUB_TASK.exec(id, new NutMap().addv("option", nodeName), null); + if (flag) { + List reviewers = List.of("1019911404"); + FundsToHandler.CREATE_SCHOOL_FZR_TASK.exec(id, + new NutMap().addv("userId", activity.getUser_id()), reviewers); + msgApi.sendWxMsg("活动申报", "", "text", activity.getUser_name() + "申报了一条活动【" + + activity.getActivity_name() + "】,请前往智慧工会进行审核。", reviewers, null); + } else { + FundsToHandler.REFUSE_TASK.exec(id, new NutMap().addv("nodeName", nodeName), null); + Sys_user user = dao().fetch(Sys_user.class, activity.getUser_id()); + if (user != null) { + msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + activity.getActivity_name() + + "】被协会拒绝,审核意见:" + idea.trim(), List.of(user.getLoginname()), null); + } + } + } + @Override @Aop(TransAop.READ_COMMITTED) public void doAdd(ActivityBx activityBx) { diff --git a/src/main/resources/static/assets/mobile/js/popupHistory.js b/src/main/resources/static/assets/mobile/js/popupHistory.js new file mode 100644 index 0000000..2eca60e --- /dev/null +++ b/src/main/resources/static/assets/mobile/js/popupHistory.js @@ -0,0 +1,128 @@ +/** + * H5 弹框统一历史栈。页面注册一次 sync 回调;每次 open 增加一层历史, + * close/clear 等待浏览器回退后再同步界面,避免返回键越过弹框直接离开业务页。 + * 同一窗口只安装一个 popstate 监听器;未接入的页面不受影响。 + */ +window.mobilePopupHistory = window.mobilePopupHistory || (() => { + let current = null + let sequence = 0 + const marker = '__mobilePopup' + + const snapshot = (session, depth) => Object.assign({}, window.history.state || {}, { + [marker]: {id: session.id, depth} + }) + const publish = (session) => { + session.sync(session.layers.slice()) + session.listeners.forEach((sync, name) => sync(session.layers.includes(name))) + } + const onPop = (event) => { + const session = current + if (!session) return + const state = event.state && event.state[marker] + const depth = state && state.id === session.id ? state.depth : 0 + // 浏览器前进到已关闭的弹框记录时不恢复旧业务内容,而将该条记录标记为当前深度。 + if (depth > session.layers.length) { + window.history.replaceState(snapshot(session, session.layers.length), '') + } else { + session.layers = session.layers.slice(0, depth) + } + publish(session) + const resolve = session.pending + session.pending = null + if (resolve) resolve() + // 普通表单的公共弹框自行创建临时会话,最后一层关闭后释放,不占用下一业务页。 + if (session.automatic && !session.layers.length) session.api.dispose() + } + window.addEventListener('popstate', onPop) + + const manager = { + /** 公共导航先关闭最上层弹框;返回 true 表示已消费本次返回操作,不应跳转业务页。 */ + closeTop() { + if (!current || !current.layers.length) return false + current.api.close() + return true + }, + /** + * key 为页面名称,sync 接收从底层到顶层的弹框名称数组。 + * 返回 open/close/clear/dispose;close、clear 返回 Promise,完成后可安全跳转。 + */ + register(key, sync) { + if (current) { + throw new Error('请先注销上一页面的弹框历史回调') + } + const session = {id: key + '-' + Date.now() + '-' + (++sequence), sync, layers: [], pending: null, waiting: null, listeners: new Map()} + current = session + window.history.replaceState(snapshot(session, 0), '') + const back = (count) => { + if (session.pending) return session.waiting + if (!count || current !== session) return Promise.resolve(true) + session.waiting = new Promise((resolve) => { + session.pending = resolve + window.history.go(-count) + }) + return session.waiting + } + session.back = back + session.api = { + open(name) { + if (current !== session || session.pending || session.layers.includes(name)) return + session.layers.push(name) + window.history.pushState(snapshot(session, session.layers.length), '') + publish(session) + }, + close() { + return back(session.layers.length ? 1 : 0) + }, + clear() { + // 清理可能恰逢手机返回事件尚未落地,先等该次回退完成,再清除剩余层。 + if (session.pending) return session.waiting.then(() => back(session.layers.length)) + return back(session.layers.length) + }, + dispose() { + if (current !== session) return + session.sync = () => {} + session.listeners.clear() + current = null + const resolve = session.pending + session.pending = null + if (resolve) resolve(false) + } + } + return session.api + }, + /** + * 公共组件创建独立层;key 为组件名称,sync(boolean) 同步显隐。 + * open 加入当前页面栈,无页面栈时自动创建;close 返回回退完成的 Promise。 + * dispose 解除回调并清除本层及其上层记录,供组件销毁或页面跳转前调用。 + */ + createLayer(key, sync) { + const name = key + '-' + (++sequence) + let owner = null + const close = () => { + if (!owner || owner !== current) return Promise.resolve() + const session = owner + if (session.pending) return session.waiting.then(close) + const index = session.layers.indexOf(name) + return session.back(index < 0 ? 0 : session.layers.length - index) + } + return { + open() { + if (!current) { + manager.register(key, () => {}) + current.automatic = true + } + if (owner && owner !== current) owner.listeners.delete(name) + owner = current + owner.listeners.set(name, sync) + owner.api.open(name) + }, + close, + dispose() { + if (owner) owner.listeners.delete(name) + return close() + } + } + } + } + return manager +})() diff --git a/src/main/resources/static/components/sign/index.vue b/src/main/resources/static/components/sign/index.vue index 6b4f02c..a4b3af0 100644 --- a/src/main/resources/static/components/sign/index.vue +++ b/src/main/resources/static/components/sign/index.vue @@ -1,192 +1,131 @@ diff --git a/src/main/resources/static/components/sign/mobileSign.vue b/src/main/resources/static/components/sign/mobileSign.vue index bf03f52..405107a 100644 --- a/src/main/resources/static/components/sign/mobileSign.vue +++ b/src/main/resources/static/components/sign/mobileSign.vue @@ -1,23 +1,30 @@ @@ -95,14 +150,51 @@ module.exports = { width: 100%; } -.main-panel { +.h5-signature-sheet.van-popup { + display: flex; + flex-direction: column; + height: 35vh; + min-height: 260px; + max-height: 90vh; + padding-bottom: env(safe-area-inset-bottom); + box-sizing: border-box; + overflow: hidden; + background: #fff; +} + +.h5-signature-heading { + flex: 0 0 44px; + display: grid; + grid-template-columns: 64px 1fr 64px; + align-items: center; + text-align: center; + border-bottom: 1px solid #f2f3f5; + color: #323233; + font-size: 16px; + font-weight: 500; +} + +.h5-signature-heading button { + height: 44px; + padding: 0; + border: 0; + background: transparent; + color: #1989fa; + font-size: 14px; +} + +.h5-signature-heading button:disabled { + opacity: .5; +} + +.h5-signature .main-panel { position: relative; flex-direction: column-reverse; display: flex; row-gap: 10px; } -.main-panel .van-image { +.h5-signature .van-image { min-height: 150px; display: block; width: 90%; @@ -111,7 +203,7 @@ module.exports = { border-radius: 8px; } -.main-panel .open-button { +.h5-signature .open-button { margin: 0 auto; width: 90%; height: 36px; diff --git a/src/main/resources/views/mobile/jf/activitybx/accounting/index.html b/src/main/resources/views/mobile/jf/activitybx/accounting/index.html new file mode 100644 index 0000000..0f0ed5d --- /dev/null +++ b/src/main/resources/views/mobile/jf/activitybx/accounting/index.html @@ -0,0 +1,18 @@ + + + + diff --git a/src/main/resources/views/mobile/jf/activitybx/association/index.html b/src/main/resources/views/mobile/jf/activitybx/association/index.html new file mode 100644 index 0000000..e8a6ce4 --- /dev/null +++ b/src/main/resources/views/mobile/jf/activitybx/association/index.html @@ -0,0 +1,135 @@ + + +
+ + + + + + 加载协会中 + + + + + + +
+

{{ item.activity_name }}

+

{{ item.user_name }}({{ item.bxr_loginname }})

+

{{ item.clubName || item.fundsUnitName || '—' }}

+

¥{{ money(item.funds_money) }}{{ shortDate(item.apply_time) }}

+ +
+ +
+
+ + + +
+ 加载申请中 + + 重新加载 + + +
+

审核信息

+ + + + + + +
+
+
+
+ + 去审核 + 关闭 +
+
+ + + + + + 申请附件 + + +
+ + diff --git a/src/main/resources/views/mobile/jf/activitybx/association/info.js b/src/main/resources/views/mobile/jf/activitybx/association/info.js new file mode 100644 index 0000000..94d9979 --- /dev/null +++ b/src/main/resources/views/mobile/jf/activitybx/association/info.js @@ -0,0 +1,104 @@ +/** 协会活动申报详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */ +const associationInfo = { + template: /*language=HTML*/ ` +
+
+

{{ data.activity_name }}

+ + + + + + + + + + +
+
+

活动方案及简介

+
{{ activityText || '暂无' }}
+ 活动方案配图 +
+
+

预算明细

+ + +
+
+

申请附件

+ +

暂无附件

+

申请人签字

+ 申请人签字 +

暂无签字

+
+
+

{{ audit.title }}

+ + + + +
+ +
+ `, + props: { + data: {type: Object, default: () => ({})} + }, + computed: { + budgets() { + return this.asList(this.data.budgets) + }, + files() { + return this.asList(this.data.files) + }, + activityText() { + // 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。 + const container = document.createElement('div') + container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n') + container.querySelectorAll('script,style').forEach((node) => node.remove()) + return container.textContent.trim() + }, + activityImages() { + // 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。 + const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html') + return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '') + .filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url)) + }, + audits() { + const data = this.data + return [ + {key: 'association', title: '协会负责人审核', id: data.association_principal_id, + name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s}, + {key: 'accounting', title: '校工会负责人审核', id: data.accounting_name, + name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s}, + {key: 'union', title: '校工会主席审核', id: data.union_chairman_name, + name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s} + ].filter((audit) => audit.id) + } + }, + methods: { + /** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */ + asList(value) { + if (Array.isArray(value)) return value + if (!value) return [] + try { + const list = JSON.parse(value) + return Array.isArray(list) ? list : [] + } catch (error) { + return [] + } + }, + money(value) { + return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + }, + /** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */ + signUrl(value) { + return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '') + } + } +} diff --git a/src/main/resources/views/mobile/jf/activitybx/association/page.js b/src/main/resources/views/mobile/jf/activitybx/association/page.js new file mode 100644 index 0000000..4dbb841 --- /dev/null +++ b/src/main/resources/views/mobile/jf/activitybx/association/page.js @@ -0,0 +1,272 @@ +// 独立手机审核页面;不复用旧 declare 页面及其有缺陷的滚动分页。 +const vue = new Vue({ + el: '#app', + components: {'association-info': associationInfo}, + data() { + return { + baseUrl: '/platform/jf/activity_bx/association', + pageForm: {year: new Date().getFullYear(), clubId: '', isAudit: 3, pageNumber: 1, pageSize: 10}, + tableData: [], tableLoading: false, refreshing: false, finished: false, listError: false, + listSerial: 0, listBusy: false, detailSerial: 0, + clubs: [{text: '全部协会', value: ''}], clubsLoading: false, + auditOptions: [{text: '未审核', value: 3}, {text: '已审核', value: 2}, {text: '全部', value: 1}], + filterOpen: {year: false, clubId: false, isAudit: false}, filterClosing: false, + popups: {filter: false, detail: false, sign: false, confirm: false, preview: false}, + detail: {}, detailId: '', detailLoading: false, auditMode: false, + formData: {id: '', idea: '', sign: ''}, formLoading: false, reviewFlag: true, + reviewerName: '', reviewDate: '', previewUrl: '', previewIsImage: false + } + }, + computed: { + yearOptions() { + return Array.from({length: 11}, (_, index) => { + const year = new Date().getFullYear() - index + return {text: year + '年', value: year} + }) + } + }, + methods: { + money(value) { + return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + }, + shortDate(value) { + return value ? String(value).slice(5, 10) : '—' + }, + setRefreshing(value) { this.refreshing = value }, + setTableLoading(value) { this.tableLoading = value }, + setListError(value) { this.listError = value }, + setIdea(value) { this.$set(this.formData, 'idea', value) }, + /** 筛选切换或提交后,从第一页替换数据;旧请求晚到时不能混入当前筛选结果。 */ + doSearch() { + this.listSerial++ + this.listBusy = false + this.$set(this.pageForm, 'pageNumber', 1) + this.tableData = [] + this.finished = false + this.listError = false + this.pageData() + }, + /** 列表返回 data.list 和 totalCount;失败保留页码供重试,成功后才推进下一页。 */ + pageData() { + if (this.listBusy || this.finished) return + const serial = this.listSerial + this.listBusy = true + this.tableLoading = true + this.listError = false + $.post(this.baseUrl + '/pageData', Object.assign({}, this.pageForm)) + .then((res) => { + if (serial !== this.listSerial || this._isDestroyed) return + if (res.code !== 0 || !res.data) { + this.listError = true + this.$toast(res.msg || '列表加载失败') + return + } + const list = res.data.list || [] + this.tableData = this.tableData.concat(list) + this.finished = !list.length || this.tableData.length >= Number(res.data.totalCount) + this.$set(this.pageForm, 'pageNumber', this.pageForm.pageNumber + 1) + }) + .fail(() => { + if (serial === this.listSerial && !this._isDestroyed) this.listError = true + }) + .always(() => { + if (serial !== this.listSerial || this._isDestroyed) return + this.listBusy = false + this.tableLoading = false + this.refreshing = false + }) + }, + loadClubs() { + this.clubsLoading = true + $.post(this.baseUrl + '/clubs') + .then((res) => { + if (res.code === 0) { + this.clubs = [{text: '全部协会', value: ''}].concat((res.data || []).map((club) => ({text: club.name, value: club.id}))) + } else { + this.$toast(res.msg || '协会加载失败') + } + }) + .fail(() => this.$toast('协会加载失败,请重新打开筛选')) + .always(() => { this.clubsLoading = false }) + }, + /** key 为年度/协会/状态字段,open 为菜单展开状态;三个菜单切换时共用一层历史。 */ + setFilterOpen(key, open) { + this.$set(this.filterOpen, key, open) + if (open && key === 'clubId' && this.clubs.length === 1 && !this.clubsLoading) this.loadClubs() + // Vant 切换菜单会同时触发旧菜单 close 和新菜单 open,等本轮更新结束后统一判断。 + this.$nextTick(() => { + if (this._isDestroyed || this.filterClosing) return + if (Object.values(this.filterOpen).some(Boolean)) { + if (!this.popups.filter) this.$options.popupSession.open('filter') + } else if (this.popups.filter) { + this.filterClosing = true + this.$options.popupSession.close().then(() => { this.filterClosing = false }) + } + }) + }, + /** key 为筛选字段,value 为对应选项值;仅值改变时刷新,菜单收起事件负责回退历史。 */ + selectFilter(key, value) { + this.$set(this.pageForm, key, value) + this.doSearch() + }, + /** 每次打开申请清空上次的意见和签字,防止在不同申请之间串用审核数据。 */ + openDetail(id, audit) { + if (this.formLoading) return + this.detailId = id + this.detail = {} + this.auditMode = audit + this.formData = {id, idea: '', sign: ''} + this.reviewDate = moment().format('YYYY-MM-DD') + this.$options.popupSession.open('detail') + this.loadDetail(id) + }, + loadDetail(id) { + const serial = ++this.detailSerial + this.detailLoading = true + $.post(this.baseUrl + '/findOne', {id}) + .then((res) => { + if (serial !== this.detailSerial || !this.popups.detail || this._isDestroyed) return + if (res.code === 0 && res.data) { + this.detail = res.data + if (!this.detail.canReview) this.auditMode = false + } else { + this.$toast(res.msg || '详情加载失败') + } + }) + .fail(() => { + if (serial === this.detailSerial && !this._isDestroyed) this.$toast('详情加载失败,请重试') + }) + .always(() => { + if (serial === this.detailSerial && !this._isDestroyed) this.detailLoading = false + }) + }, + startAudit() { + this.auditMode = true + this.$nextTick(() => { + const body = this.$el.querySelector('.association-detail-body') + if (body) body.scrollTop = body.scrollHeight + }) + }, + openSignature() { + if (!this.formLoading) this.$options.popupSession.open('sign') + }, + /** signature 是 mobile-sign 回传的本次签字 Base64,随审核请求一起保存。 */ + setSignature(signature) { + this.$set(this.formData, 'sign', signature) + }, + confirmReview(flag) { + if (this.formLoading || !this.detail.canReview) return + if (!this.formData.idea.trim() || !this.formData.sign) { + this.$toast('请填写审核意见并签字') + return + } + this.reviewFlag = flag + this.$options.popupSession.open('confirm') + }, + /** Vant 用 done 结束按钮 loading;显隐仍由历史栈驱动,避免组件自行关闭后留下历史层。 */ + beforeReviewClose(action, done) { + if (this.formLoading) { + done(false) + return + } + if (action === 'confirm') { + this.submitReview().always(() => done(false)) + } else { + this.$options.popupSession.close().then(() => done(false)) + } + }, + /** id/idea/sign/flag 与 PC 共用;审核时间和人员由服务器取值,不信任前端身份字段。 */ + submitReview() { + if (this.formLoading) return + this.formLoading = true + return $.post(this.baseUrl + '/doReview', Object.assign({}, this.formData, {flag: this.reviewFlag})) + .then((res) => { + if (this._isDestroyed) return + if (res.code === 0) { + this.$toast.success('审核成功') + this.$options.popupSession.clear().then(() => { + this.formData = {id: '', idea: '', sign: ''} + this.doSearch() + }) + } else { + this.$toast(res.msg || '审核失败') + // 关闭确认层后重读状态;若已被 PC 处理则切换为只读,保留未提交的意见便于核对。 + if (this.popups.confirm) this.$options.popupSession.close().then(() => this.loadDetail(this.detailId)) + } + }) + .fail(() => this.$toast('提交失败,请检查网络后重试')) + .always(() => { this.formLoading = false }) + }, + /** 图片使用文件流,文档沿用项目预览接口;预览自身同样占一层历史。 */ + previewFile(file) { + if (file.imageUrl) { + this.previewIsImage = true + this.previewUrl = file.imageUrl + this.$options.popupSession.open('preview') + return + } + if (!file.id) { + this.$toast('附件信息不完整') + return + } + this.previewIsImage = /\.(png|jpe?g|gif|webp|bmp)$/i.test(file.filename || file.filepath || '') + const streamUrl = '/file_server/fileStreamPreview?id=' + encodeURIComponent(file.id) + this.previewUrl = this.previewIsImage + ? streamUrl + : '/assets/platform/plugins/pdfJs/web/viewer.html?file=' + encodeURIComponent(streamUrl) + this.$options.popupSession.open('preview') + }, + closePopup() { + if (this.formLoading) { + this.$toast('正在提交,请稍候') + return + } + this.$options.popupSession.close() + }, + back() { + if (Object.values(this.popups).some(Boolean)) { + this.closePopup() + return + } + this.$options.popupSession.clear().then(() => { + this.$destroy() + pjaxReplace('/mobile/index') + }) + } + }, + created() { + // 仅注册状态同步回调;popstate 由公共管理器统一监听。 + this.$options.popupSession = window.mobilePopupHistory.register('associationAudit', (layers) => { + Object.keys(this.popups).forEach((name) => this.$set(this.popups, name, layers.includes(name))) + // 系统返回或页面返回先收起下拉菜单;由公共历史栈驱动,页面不另绑 popstate。 + if (!layers.includes('filter')) { + Object.keys(this.filterOpen).forEach((key) => { + this.$set(this.filterOpen, key, false) + const dropdown = this.$refs[key + 'Filter'] + if (dropdown) dropdown.toggle(false) + }) + } + }) + const user = JSON.parse(window.sessionStorage.getItem('user') || '{}') + this.reviewerName = user.username || '' + this.loadClubs() + this.doSearch() + const query = new URLSearchParams(window.location.search) + const id = query.get('id') + if (id) this.openDetail(id, query.get('mode') === 'audit') + // PJAX 不会自动销毁旧 Vue 实例,在替换容器前注销页面回调。 + $(document).on('pjax:beforeReplace.associationAudit', () => this.$destroy()) + }, + beforeDestroy() { + this.listSerial++ + this.detailSerial++ + const session = this.$options.popupSession + if (Object.values(this.popups).some(Boolean)) { + session.clear().then(() => session.dispose()) + } else { + // PJAX 替换后可能立即创建新页面;空栈必须同步注销,不能等 Promise 回调再释放注册。 + session.dispose() + } + $(document).off('pjax:beforeReplace.associationAudit') + } +}) diff --git a/src/main/resources/views/mobile/jf/activitybx/division/index.html b/src/main/resources/views/mobile/jf/activitybx/division/index.html new file mode 100644 index 0000000..cff4dcf --- /dev/null +++ b/src/main/resources/views/mobile/jf/activitybx/division/index.html @@ -0,0 +1,18 @@ + + + + diff --git a/src/main/resources/views/mobile/jf/activitybx/review/index.html b/src/main/resources/views/mobile/jf/activitybx/review/index.html new file mode 100644 index 0000000..f8c7766 --- /dev/null +++ b/src/main/resources/views/mobile/jf/activitybx/review/index.html @@ -0,0 +1,223 @@ + +
+ + + + + + 加载中 + + + +
{{ searchSummary }}更多筛选
+
+ + +
+

{{ item.activity_name }}

+

{{ item.user_name }}({{ item.bxr_loginname }})

+

{{ item.bxr_unionname || item.fundsUnitName || '—' }}

+

¥{{ money(item.funds_money) }}{{ shortDate(item.apply_time) }}

+ +
+ +
+
+ + + +
+ 加载申请中 + + 重新加载 + + +
+

审核信息

+ + + + + + +
+
+
+
+ + 去审核 + 关闭 +
+
+ + + + + + 申请附件 + + + + +
+

更多筛选

+ +
+
+
+

所属单位

+ +
+
+
+

查询人员

+
+ {{ option.text }} +
+
+ +
+
+

排序方式

+
+ + {{ option.text }} + +
+
+

排列顺序

+
+ {{ option.text }} +
+
+
+
+ +
+ + + +
+ + + + +
+
+
diff --git a/src/main/resources/views/mobile/jf/activitybx/review/info.js b/src/main/resources/views/mobile/jf/activitybx/review/info.js new file mode 100644 index 0000000..d8e3672 --- /dev/null +++ b/src/main/resources/views/mobile/jf/activitybx/review/info.js @@ -0,0 +1,146 @@ +/** 活动申报审核详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */ +const activityReviewInfo = { + template: /*language=HTML*/ ` +
+
+

{{ data.activity_name }}

+ + + + + + + + + + + + + + + + +
+
+

活动方案及简介

+
{{ activityText || '暂无' }}
+ 活动方案配图 +
+
+

预算明细

+
+ + +
+ +
+
+

申请附件

+ +

暂无附件

+

申请人签字

+ 申请人签字 +

暂无签字

+
+
+

{{ group.title }}

+ +

暂无附件

+
+
+

{{ audit.title }}

+ + + + +
+ +
+ `, + props: { + data: {type: Object, default: () => ({})} + }, + computed: { + activityType() { + return {40001: '校工会活动', 40002: '基层工会活动', 40003: '协会活动', 40004: '女职工活动'}[this.data.activity_type] || '—' + }, + reimbursementFiles() { + if (!this.data.reimbursement_state_id) return [] + return [{key: 'billFiles', title: '发票'}, {key: 'photoFiles', title: '活动报道照片/总结/链接'}, {key: 'otherFiles', title: '奖品/纪念品/服装/道具等活动人员名单'}] + .map((group) => Object.assign({}, group, {files: this.asList(this.data[group.key])})) + }, + budgets() { + return this.asList(this.data.budgets) + }, + files() { + return this.asList(this.data.files) + }, + activityText() { + // 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。 + const container = document.createElement('div') + container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n') + container.querySelectorAll('script,style').forEach((node) => node.remove()) + return container.textContent.trim() + }, + activityImages() { + // 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。 + const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html') + return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '') + .filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url)) + }, + audits() { + const data = this.data + const records = [ + {key: 'division', title: '基层工会主席审核', id: data.division_chairman_name, name: data.dc_username, time: data.division_chairman_time, idea: data.division_chairman_idea, sign: data.dc_s}, + {key: 'association', title: '协会负责人审核', id: data.association_principal_id, + name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s}, + {key: 'accounting', title: '校工会负责人审核', id: data.accounting_name, + name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s}, + {key: 'confirm', title: '校工会副主席审核', id: data.confirm_trade_name, name: data.ct_username, time: data.confirm_trade_time, idea: data.confirm_trade_idea, sign: data.ct_s}, + {key: 'cashier', title: '校工会出纳审核', id: data.union_cashier_name, name: data.uc_username, time: data.union_cashier_time, idea: data.union_cashier_idea, sign: data.uc_s}, + {key: 'union', title: '校工会主席审核', id: data.union_chairman_name, + name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s} + ].filter((audit) => audit.id) + // PC 详情同时展示已有报销审批记录;只读呈现,不把报销流程混入当前申请审核。 + const reimbursementAudits = [ + {key: 'reimbursement_club_audit', title: '协会负责人审核(报销)'}, + {key: 'reimbursement_union_audit', title: '基层工会主席审核(报销)'}, + {key: 'reimbursement_school_director', title: '校工会负责人审核(报销)'}, + {key: 'reimbursement_school_chairman', title: '校工会主席审核(报销)'} + ].filter((item) => data[item.key]).map((item) => { + const audit = data[item.key] + return {key: item.key, title: item.title, name: audit.username, time: audit.auditTime, idea: audit.auditOpinion, sign: audit.auditSign} + }) + return records.concat(reimbursementAudits) + } + }, + methods: { + /** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */ + asList(value) { + if (Array.isArray(value)) return value + if (!value) return [] + try { + const list = JSON.parse(value) + return Array.isArray(list) ? list : [] + } catch (error) { + return [] + } + }, + money(value) { + return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + }, + /** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */ + signUrl(value) { + return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '') + } + } +} diff --git a/src/main/resources/views/mobile/jf/activitybx/review/page.js b/src/main/resources/views/mobile/jf/activitybx/review/page.js new file mode 100644 index 0000000..12e86fe --- /dev/null +++ b/src/main/resources/views/mobile/jf/activitybx/review/page.js @@ -0,0 +1,353 @@ +// 三个节点共用协会页的手机交互,节点与提示文案由独立入口配置。 +const vue = new Vue({ + el: '#app', + components: {'review-info': activityReviewInfo}, + data() { + return { + config: activityReviewConfig, + baseUrl: '/platform/jf/activity_bx/' + activityReviewConfig.stage, + pageForm: {year: new Date().getFullYear(), unionId: '', unitId: '', isAudit: 3, pageNumber: 1, pageSize: 10, searchName: 'username', searchKeyword: '', pageOrderName: '', pageOrderBy: 'desc'}, + tableData: [], tableLoading: false, refreshing: false, finished: false, listError: false, + listSerial: 0, listBusy: false, detailSerial: 0, + unions: [{text: '全部工会', value: ''}], units: [{text: '全部单位', value: ''}], organizationsLoading: false, unitsSerial: 0, + moreForm: {}, choiceKey: '', choiceQuery: '', + auditOptions: [{text: '未审核', value: 3}, {text: '已审核', value: 2}, {text: '全部', value: 1}], + filterOpen: {year: false, organization: false, isAudit: false}, filterClosing: false, + popups: {filter: false, detail: false, sign: false, confirm: false, preview: false, more: false, choice: false}, + detail: {}, detailId: '', detailLoading: false, auditMode: false, + formData: {id: '', idea: '', sign: ''}, formLoading: false, reviewFlag: true, + reviewerName: '', reviewDate: '', previewUrl: '', previewIsImage: false + } + }, + computed: { + organizationKey() { return this.config.canChooseUnion ? 'unionId' : 'unitId' }, + organizationOptions() { return this.config.canChooseUnion ? this.unions : this.units }, + /** 查询类型直接切换草稿字段,输入提示同步变化;确定筛选前不触发列表请求。 */ + moreSearchPlaceholder() { return this.moreForm.searchName === 'loginname' ? '请输入工号' : '请输入姓名' }, + searchSummary() { + const parts = [] + if (this.pageForm.searchKeyword) parts.push(this.pageForm.searchKeyword) + if (this.config.canChooseUnion && this.pageForm.unitId) parts.push(this.optionText('unitId', this.pageForm.unitId)) + if (this.pageForm.pageOrderName) parts.push(this.optionText('pageOrderName', this.pageForm.pageOrderName)) + return parts.join(' · ') || '按姓名、工号、单位筛选' + }, + choiceOptions() { + return { + unitId: this.units, + searchName: [{text: '姓名', value: 'username'}, {text: '工号', value: 'loginname'}], + pageOrderName: [{text: '默认排序', value: ''}, {text: '所属工会', value: 'bxr_unionname'}, {text: '活动主体类型', value: 'activity_type'}, {text: '经费类型', value: 'absName'}, {text: '申请类型', value: 'apply_type'}, {text: '申请时间', value: 'apply_time'}, {text: '申请状态', value: 'state_id'}], + pageOrderBy: [{text: '升序', value: 'asc'}, {text: '降序', value: 'desc'}] + } + }, + choiceTitle() { return {unitId: '所属单位', searchName: '查询类型', pageOrderName: '排序字段', pageOrderBy: '排序方式'}[this.choiceKey] || '请选择' }, + filteredChoices() { + return (this.choiceOptions[this.choiceKey] || []).filter((item) => item.text.includes(this.choiceQuery.trim())) + }, + yearOptions() { + return [{text: '全部年份', value: ''}].concat(Array.from({length: 11}, (_, index) => { + const year = new Date().getFullYear() - index + return {text: year + '年', value: year} + })) + } + }, + methods: { + money(value) { + return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + }, + shortDate(value) { + return value ? String(value).slice(5, 10) : '—' + }, + setRefreshing(value) { this.refreshing = value }, + setTableLoading(value) { this.tableLoading = value }, + setListError(value) { this.listError = value }, + setIdea(value) { this.$set(this.formData, 'idea', value) }, + /** 筛选切换或提交后,从第一页替换数据;旧请求晚到时不能混入当前筛选结果。 */ + doSearch() { + this.listSerial++ + this.listBusy = false + this.$set(this.pageForm, 'pageNumber', 1) + this.tableData = [] + this.finished = false + this.listError = false + this.pageData() + }, + /** 列表返回 data.list 和 totalCount;失败保留页码供重试,成功后才推进下一页。 */ + pageData() { + if (this.listBusy || this.finished) return + const serial = this.listSerial + this.listBusy = true + this.tableLoading = true + this.listError = false + $.post(this.baseUrl + '/pageData', Object.assign({}, this.pageForm)) + .then((res) => { + if (serial !== this.listSerial || this._isDestroyed) return + if (res.code !== 0 || !res.data) { + this.listError = true + this.$toast(res.msg || '列表加载失败') + return + } + const list = res.data.list || [] + this.tableData = this.tableData.concat(list) + this.finished = !list.length || this.tableData.length >= Number(res.data.totalCount) + this.$set(this.pageForm, 'pageNumber', this.pageForm.pageNumber + 1) + }) + .fail(() => { + if (serial === this.listSerial && !this._isDestroyed) this.listError = true + }) + .always(() => { + if (serial !== this.listSerial || this._isDestroyed) return + this.listBusy = false + this.tableLoading = false + this.refreshing = false + }) + }, + /** 工会和单位沿用 PC 的公共选项接口;接口返回 data 数组,组织权限仍由服务端控制。 */ + loadOrganizations() { + this.organizationsLoading = true + $.post('/platform/vi/common/unions', {}) + .then((res) => { + if (this._isDestroyed) return + if (res.code === 0) this.unions = [{text: '全部工会', value: ''}].concat((res.data || []).map((item) => ({text: item.unionname, value: item.id}))) + else this.$toast(res.msg || '工会加载失败') + }) + .fail(() => this.$toast('工会加载失败,请重试')) + .always(() => { this.organizationsLoading = false }) + this.loadUnits() + }, + /** 更换工会后清空旧单位;序号避免前一次请求晚返回而覆盖新工会的选项。 */ + loadUnits() { + const serial = ++this.unitsSerial + return $.post('/platform/vi/common/units', {unionId: this.config.canChooseUnion ? this.pageForm.unionId : this.config.currentUnionId}) + .then((res) => { + if (serial !== this.unitsSerial || this._isDestroyed) return + if (res.code === 0) this.units = [{text: '全部单位', value: ''}].concat((res.data || []).map((item) => ({text: item.name, value: item.id}))) + else this.$toast(res.msg || '单位加载失败') + }) + .fail(() => { if (serial === this.unitsSerial && !this._isDestroyed) this.$toast('单位加载失败,请重试') }) + }, + openMore() { + this.moreForm = Object.assign({}, this.pageForm) + this.loadUnits() + this.$options.popupSession.open('more') + }, + setMoreField(key, value) { this.$set(this.moreForm, key, value) }, + setChoiceQuery(value) { this.choiceQuery = value }, + /** 选择项弹框压在更多筛选上层,返回时只退出当前选择,不丢弃其他筛选值。 */ + openChoice(key) { + this.choiceKey = key + this.choiceQuery = '' + this.$options.popupSession.open('choice') + }, + selectChoice(value) { + this.$set(this.moreForm, this.choiceKey, value) + this.$options.popupSession.close() + }, + optionText(key, value) { + const option = (this.choiceOptions[key] || []).find((item) => item.value === value) + return option ? option.text : '请选择' + }, + resetMore() { + this.moreForm = Object.assign({}, this.moreForm, {unitId: this.config.canChooseUnion ? '' : this.pageForm.unitId, searchName: 'username', searchKeyword: '', pageOrderName: '', pageOrderBy: 'desc'}) + }, + applyMore() { + const values = this.moreForm + this.$options.popupSession.close().then(() => { + if (this._isDestroyed) return + const keys = ['searchName', 'searchKeyword', 'pageOrderName', 'pageOrderBy'] + if (this.config.canChooseUnion) keys.push('unitId') + keys.forEach((key) => this.$set(this.pageForm, key, values[key])) + this.doSearch() + }) + }, + /** key 为年度/组织/状态字段,open 为菜单展开状态;三个菜单切换时共用一层历史。 */ + setFilterOpen(key, open) { + this.$set(this.filterOpen, key, open) + if (open && key === 'organization') { + if (this.organizationOptions.length === 1 && !this.organizationsLoading) this.loadOrganizations() + } + // Vant 切换菜单会同时触发旧菜单 close 和新菜单 open,等本轮更新结束后统一判断。 + this.$nextTick(() => { + if (this._isDestroyed || this.filterClosing) return + if (Object.values(this.filterOpen).some(Boolean)) { + if (!this.popups.filter) this.$options.popupSession.open('filter') + } else if (this.popups.filter) { + this.filterClosing = true + this.$options.popupSession.close().then(() => { this.filterClosing = false }) + } + }) + }, + /** key 为筛选字段,value 为对应选项值;仅值改变时刷新,菜单收起事件负责回退历史。 */ + selectFilter(key, value) { + this.$set(this.pageForm, key, value) + if (key === 'unionId') { + this.$set(this.pageForm, 'unitId', '') + this.units = [{text: '全部单位', value: ''}] + this.loadUnits() + } + this.doSearch() + }, + /** 每次打开申请清空上次的意见和签字,防止在不同申请之间串用审核数据。 */ + openDetail(id, audit) { + if (this.formLoading) return + this.detailId = id + this.detail = {} + this.auditMode = audit + this.formData = {id, idea: '', sign: ''} + this.reviewDate = moment().format('YYYY-MM-DD') + this.$options.popupSession.open('detail') + this.loadDetail(id) + }, + loadDetail(id) { + const serial = ++this.detailSerial + this.detailLoading = true + $.post(this.baseUrl + '/findOne', {id}) + .then((res) => { + if (serial !== this.detailSerial || !this.popups.detail || this._isDestroyed) return + if (res.code === 0 && res.data) { + this.detail = res.data + if (!this.detail.canReview) this.auditMode = false + } else { + this.$toast(res.msg || '详情加载失败') + } + }) + .fail(() => { + if (serial === this.detailSerial && !this._isDestroyed) this.$toast('详情加载失败,请重试') + }) + .always(() => { + if (serial === this.detailSerial && !this._isDestroyed) this.detailLoading = false + }) + }, + startAudit() { + this.auditMode = true + this.$nextTick(() => { + const body = this.$el.querySelector('.review-detail-body') + if (body) body.scrollTop = body.scrollHeight + }) + }, + openSignature() { + if (!this.formLoading) this.$options.popupSession.open('sign') + }, + /** signature 是 mobile-sign 回传的本次签字 Base64,随审核请求一起保存。 */ + setSignature(signature) { + this.$set(this.formData, 'sign', signature) + }, + confirmReview(flag) { + if (this.formLoading || !this.detail.canReview) return + if (!this.formData.idea.trim() || !this.formData.sign) { + this.$toast('请填写审核意见并签字') + return + } + this.reviewFlag = flag + this.$options.popupSession.open('confirm') + }, + /** Vant 用 done 结束按钮 loading;显隐仍由历史栈驱动,避免组件自行关闭后留下历史层。 */ + beforeReviewClose(action, done) { + if (this.formLoading) { + done(false) + return + } + if (action === 'confirm') { + this.submitReview().always(() => done(false)) + } else { + this.$options.popupSession.close().then(() => done(false)) + } + }, + /** id/idea/sign/flag 与 PC 共用;审核时间和人员由服务器取值,不信任前端身份字段。 */ + submitReview() { + if (this.formLoading) return + this.formLoading = true + const form = Object.assign({}, this.formData, {flag: this.reviewFlag}) + const params = this.config.stage === 'division' ? form : {data: JSON.stringify(form)} + return $.post(this.baseUrl + '/doReview', params) + .then((res) => { + if (this._isDestroyed) return + if (res.code === 0) { + this.$toast.success('审核成功') + this.$options.popupSession.clear().then(() => { + if (this._isDestroyed) return + this.formData = {id: '', idea: '', sign: ''} + this.doSearch() + }) + } else { + this.$toast(res.msg || '审核失败') + // 关闭确认层后重读状态;若已被 PC 处理则切换为只读,保留未提交的意见便于核对。 + if (this.popups.confirm) this.$options.popupSession.close().then(() => this.loadDetail(this.detailId)) + } + }) + .fail(() => this.$toast('提交失败,请检查网络后重试')) + .always(() => { this.formLoading = false }) + }, + /** 图片使用文件流,文档沿用项目预览接口;预览自身同样占一层历史。 */ + previewFile(file) { + if (file.imageUrl) { + this.previewIsImage = true + this.previewUrl = file.imageUrl + this.$options.popupSession.open('preview') + return + } + if (!file.id) { + this.$toast('附件信息不完整') + return + } + this.previewIsImage = /\.(png|jpe?g|gif|webp|bmp)$/i.test(file.filename || file.filepath || '') + const streamUrl = '/file_server/fileStreamPreview?id=' + encodeURIComponent(file.id) + this.previewUrl = this.previewIsImage + ? streamUrl + : '/assets/platform/plugins/pdfJs/web/viewer.html?file=' + encodeURIComponent(streamUrl) + this.$options.popupSession.open('preview') + }, + closePopup() { + if (this.formLoading) { + this.$toast('正在提交,请稍候') + return + } + this.$options.popupSession.close() + }, + back() { + if (Object.values(this.popups).some(Boolean)) { + this.closePopup() + return + } + this.$options.popupSession.clear().then(() => { + this.$destroy() + pjaxReplace('/mobile/index') + }) + } + }, + created() { + // 仅注册状态同步回调;popstate 由公共管理器统一监听。 + this.$options.popupSession = window.mobilePopupHistory.register('activityBxReview-' + this.config.stage, (layers) => { + Object.keys(this.popups).forEach((name) => this.$set(this.popups, name, layers.includes(name))) + // 系统返回或页面返回先收起下拉菜单;由公共历史栈驱动,页面不另绑 popstate。 + if (!layers.includes('filter')) { + Object.keys(this.filterOpen).forEach((key) => { + this.$set(this.filterOpen, key, false) + const dropdown = this.$refs[key + 'Filter'] + if (dropdown) dropdown.toggle(false) + }) + } + }) + const user = JSON.parse(window.sessionStorage.getItem('user') || '{}') + this.reviewerName = user.username || '' + this.loadOrganizations() + this.doSearch() + const query = new URLSearchParams(window.location.search) + const id = query.get('id') + if (id) this.openDetail(id, query.get('mode') === 'audit') + // PJAX 不会自动销毁旧 Vue 实例,在替换容器前注销页面回调。 + $(document).on('pjax:beforeReplace.activityBxReview', () => this.$destroy()) + }, + beforeDestroy() { + this.listSerial++ + this.detailSerial++ + this.unitsSerial++ + const session = this.$options.popupSession + if (Object.values(this.popups).some(Boolean)) { + session.clear().then(() => session.dispose()) + } else { + // PJAX 替换后可能立即创建新页面;空栈必须同步注销,不能等 Promise 回调再释放注册。 + session.dispose() + } + $(document).off('pjax:beforeReplace.activityBxReview') + } +}) diff --git a/src/main/resources/views/mobile/jf/activitybx/union/index.html b/src/main/resources/views/mobile/jf/activitybx/union/index.html new file mode 100644 index 0000000..9796330 --- /dev/null +++ b/src/main/resources/views/mobile/jf/activitybx/union/index.html @@ -0,0 +1,18 @@ + + + + diff --git a/src/main/resources/views/mobile/jf/reimbursement/clubChairman/index.html b/src/main/resources/views/mobile/jf/reimbursement/clubChairman/index.html new file mode 100644 index 0000000..9374c9b --- /dev/null +++ b/src/main/resources/views/mobile/jf/reimbursement/clubChairman/index.html @@ -0,0 +1,18 @@ + + + + diff --git a/src/main/resources/views/mobile/jf/reimbursement/review/index.html b/src/main/resources/views/mobile/jf/reimbursement/review/index.html new file mode 100644 index 0000000..ca08d25 --- /dev/null +++ b/src/main/resources/views/mobile/jf/reimbursement/review/index.html @@ -0,0 +1,233 @@ + +
+ + + + + + 加载中 + + + +
{{ searchSummary }}更多筛选
+
+ + +
+

{{ item.activity_name }}

+

{{ item.user_name }}({{ item.bxr_loginname }})

+

{{ config.stage === 'clubChairman' ? (item.clubName || item.fundsUnitName || '—') : (item.unionname || item.fundsUnitName || '—') }}

+

¥{{ money(item.activity_money) }}{{ shortDate(item.reimbursement_apply_time) }}

+ +
+ +
+
+ + + +
+ 加载报销中 + + 重新加载 + + +
+

审核信息

+ + + + + + +
+
+
+
+ + 去审核 + 关闭 +
+
+ + + + + + 报销附件 + + + + +
+

更多筛选

+ +
+
+
+

所属单位

+ +
+
+

所属协会

+ +
+
+

活动名称

+ +
+
+
+

查询人员

+
+ {{ option.text }} +
+
+ +
+
+

排序方式

+
+ + {{ option.text }} + +
+
+

排列顺序

+
+ {{ option.text }} +
+
+
+
+ +
+ + + +
+ 加载选项中 + + + + +
+
+
diff --git a/src/main/resources/views/mobile/jf/reimbursement/review/info.js b/src/main/resources/views/mobile/jf/reimbursement/review/info.js new file mode 100644 index 0000000..873ce0b --- /dev/null +++ b/src/main/resources/views/mobile/jf/reimbursement/review/info.js @@ -0,0 +1,147 @@ +/** 活动报销审核详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */ +const reimbursementInfo = { + template: /*language=HTML*/ ` +
+
+

{{ data.activity_name }}

+ + + + + + + + + + + + + + + + +
+
+

活动方案及简介

+
{{ activityText || '暂无' }}
+ 活动方案配图 +
+
+

费用及收款信息

+
+ + +
+ + +
+
+

申请附件

+ +

暂无附件

+

申请人签字

+ 申请人签字 +

暂无签字

+
+
+

{{ group.title }}

+ +

暂无附件

+
+
+

{{ audit.title }}

+ + + + +
+ +
+ `, + props: { + data: {type: Object, default: () => ({})} + }, + computed: { + activityType() { + return {40001: '校工会活动', 40002: '基层工会活动', 40003: '协会活动', 40004: '女职工活动'}[this.data.activity_type] || '—' + }, + reimbursementFiles() { + if (!this.data.reimbursement_state_id) return [] + return [{key: 'billFiles', title: '发票'}, {key: 'photoFiles', title: '活动报道照片/总结/链接'}, {key: 'otherFiles', title: '奖品/纪念品/服装/道具等活动人员名单'}] + .map((group) => Object.assign({}, group, {files: this.asList(this.data[group.key])})) + }, + budgets() { + return this.asList(this.data.budgets) + }, + files() { + return this.asList(this.data.files) + }, + activityText() { + // 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。 + const container = document.createElement('div') + container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n') + container.querySelectorAll('script,style').forEach((node) => node.remove()) + return container.textContent.trim() + }, + activityImages() { + // 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。 + const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html') + return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '') + .filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url)) + }, + audits() { + const data = this.data + const records = [ + {key: 'division', title: '基层工会主席审核(申报)', id: data.division_chairman_name, name: data.dc_username, time: data.division_chairman_time, idea: data.division_chairman_idea, sign: data.dc_s}, + {key: 'association', title: '协会负责人审核(申报)', id: data.association_principal_id, + name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s}, + {key: 'accounting', title: '校工会负责人审核(申报)', id: data.accounting_name, + name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s}, + {key: 'confirm', title: '校工会副主席审核(申报)', id: data.confirm_trade_name, name: data.ct_username, time: data.confirm_trade_time, idea: data.confirm_trade_idea, sign: data.ct_s}, + {key: 'cashier', title: '校工会出纳审核(申报)', id: data.union_cashier_name, name: data.uc_username, time: data.union_cashier_time, idea: data.union_cashier_idea, sign: data.uc_s}, + {key: 'union', title: '校工会主席审核(申报)', id: data.union_chairman_name, + name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s} + ].filter((audit) => audit.id) + // 按 PC 详情展示报销审核记录,并与前期申报记录明确区分。 + const reimbursementAudits = [ + {key: 'reimbursement_club_audit', title: '协会负责人审核(报销)'}, + {key: 'reimbursement_union_audit', title: '基层工会主席审核(报销)'}, + {key: 'reimbursement_school_director', title: '校工会负责人审核(报销)'}, + {key: 'reimbursement_school_chairman', title: '校工会主席审核(报销)'} + ].filter((item) => data[item.key]).map((item) => { + const audit = data[item.key] + return {key: item.key, title: item.title, name: audit.username, time: audit.auditTime, idea: audit.auditOpinion, sign: audit.auditSign} + }) + return records.concat(reimbursementAudits) + } + }, + methods: { + /** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */ + asList(value) { + if (Array.isArray(value)) return value + if (!value) return [] + try { + const list = JSON.parse(value) + return Array.isArray(list) ? list : [] + } catch (error) { + return [] + } + }, + money(value) { + return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + }, + /** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */ + signUrl(value) { + return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '') + } + } +} diff --git a/src/main/resources/views/mobile/jf/reimbursement/review/page.js b/src/main/resources/views/mobile/jf/reimbursement/review/page.js new file mode 100644 index 0000000..3165b6c --- /dev/null +++ b/src/main/resources/views/mobile/jf/reimbursement/review/page.js @@ -0,0 +1,381 @@ +// 四个报销节点沿用活动申报手机页交互;入口固定节点,报销状态与审核字段独立。 +const vue = new Vue({ + el: '#app', + components: {'reimbursement-info': reimbursementInfo}, + data() { + return { + config: reimbursementReviewConfig, + baseUrl: '/platform/jf/reimbursement/' + reimbursementReviewConfig.stage, + pageForm: {year: new Date().getFullYear(), unionId: '', unitId: '', clubId: '', activityName: '', isAudit: 2, pageNumber: 1, pageSize: 10, searchName: 'u.username', searchKeyword: '', pageOrderName: '', pageOrderBy: 'desc'}, + tableData: [], tableLoading: false, refreshing: false, finished: false, listError: false, + listSerial: 0, listBusy: false, detailSerial: 0, + unions: [{text: '全部工会', value: ''}], units: [{text: '全部单位', value: ''}], organizationsLoading: false, unitsLoading: false, unitsSerial: 0, + clubs: [{text: '全部协会', value: ''}], clubsLoading: false, + moreForm: {}, choiceKey: '', choiceQuery: '', + auditOptions: [{text: '未审核', value: 2}, {text: '已审核', value: 1}, {text: '全部', value: 0}], + filterOpen: {year: false, organization: false, isAudit: false}, filterClosing: false, + popups: {filter: false, detail: false, sign: false, confirm: false, preview: false, more: false, choice: false}, + detail: {}, detailId: '', detailLoading: false, auditMode: false, + formData: {id: '', auditOpinion: '', auditSign: ''}, formLoading: false, reviewFlag: true, + reviewerName: '', reviewDate: '', previewUrl: '', previewIsImage: false + } + }, + computed: { + organizationKey() { return this.config.stage === 'clubChairman' ? 'clubId' : this.config.canChooseUnion ? 'unionId' : 'unitId' }, + organizationOptions() { return this.config.stage === 'clubChairman' ? this.clubs : this.config.canChooseUnion ? this.unions : this.units }, + organizationLoading() { return this.config.stage === 'clubChairman' ? this.clubsLoading : this.config.canChooseUnion ? this.organizationsLoading : this.unitsLoading }, + /** 查询类型直接切换草稿字段,输入提示同步变化;确定筛选前不触发列表请求。 */ + moreSearchPlaceholder() { return this.moreForm.searchName === 'u.loginname' ? '请输入工号' : '请输入姓名' }, + searchSummary() { + const parts = [] + if (this.pageForm.activityName) parts.push(this.pageForm.activityName) + if (this.config.stage !== 'clubChairman' && this.pageForm.clubId) parts.push(this.optionText('clubId', this.pageForm.clubId)) + if (this.pageForm.searchKeyword) parts.push(this.pageForm.searchKeyword) + if (this.config.canChooseUnion && this.pageForm.unitId) parts.push(this.optionText('unitId', this.pageForm.unitId)) + if (this.pageForm.pageOrderName) parts.push(this.optionText('pageOrderName', this.pageForm.pageOrderName)) + return parts.join(' · ') || '按活动名称、姓名、工号筛选' + }, + choiceOptions() { + return { + unitId: this.units, + clubId: this.clubs, + searchName: [{text: '姓名', value: 'u.username'}, {text: '工号', value: 'u.loginname'}], + pageOrderName: [{text: '默认排序', value: ''}, {text: '所属工会', value: 'unionname'}, {text: '活动主体类型', value: 'activity_type'}, {text: '经费类型', value: 'absName'}, {text: '申请时间', value: 'reimbursement_apply_time'}, {text: '申请状态', value: 'reimbursement_state_id'}], + pageOrderBy: [{text: '升序', value: 'asc'}, {text: '降序', value: 'desc'}] + } + }, + choiceTitle() { return {clubId: '所属协会', unitId: '所属单位', searchName: '查询类型', pageOrderName: '排序字段', pageOrderBy: '排序方式'}[this.choiceKey] || '请选择' }, + filteredChoices() { + return (this.choiceOptions[this.choiceKey] || []).filter((item) => item.text.includes(this.choiceQuery.trim())) + }, + yearOptions() { + return [{text: '全部年份', value: ''}].concat(Array.from({length: 11}, (_, index) => { + const year = new Date().getFullYear() - index + return {text: year + '年', value: year} + })) + } + }, + methods: { + money(value) { + return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + }, + shortDate(value) { + return value ? String(value).slice(5, 10) : '—' + }, + setRefreshing(value) { this.$set(this, 'refreshing', value) }, + setTableLoading(value) { this.$set(this, 'tableLoading', value) }, + setListError(value) { this.$set(this, 'listError', value) }, + setIdea(value) { this.$set(this.formData, 'auditOpinion', value) }, + /** 筛选切换或提交后,从第一页替换数据;旧请求晚到时不能混入当前筛选结果。 */ + doSearch() { + this.listSerial++ + this.listBusy = false + this.$set(this.pageForm, 'pageNumber', 1) + this.tableData = [] + this.finished = false + this.listError = false + this.pageData() + }, + /** 列表返回 data.list 和 totalCount;失败保留页码供重试,成功后才推进下一页。 */ + pageData() { + if (this.listBusy || this.finished) return + const serial = this.listSerial + this.listBusy = true + this.tableLoading = true + this.listError = false + $.post(this.baseUrl + '/pageData', Object.assign({}, this.pageForm)) + .then((res) => { + if (serial !== this.listSerial || this._isDestroyed) return + if (res.code !== 0 || !res.data) { + this.listError = true + this.$toast(res.msg || '列表加载失败') + return + } + const list = res.data.list || [] + this.tableData = this.tableData.concat(list) + this.finished = !list.length || this.tableData.length >= Number(res.data.totalCount) + this.$set(this.pageForm, 'pageNumber', this.pageForm.pageNumber + 1) + }) + .fail(() => { + if (serial === this.listSerial && !this._isDestroyed) this.listError = true + }) + .always(() => { + if (serial !== this.listSerial || this._isDestroyed) return + this.listBusy = false + this.tableLoading = false + this.refreshing = false + }) + }, + /** 返回 data 数组的组织接口沿用 PC;组织数据范围由服务端控制。 */ + loadOrganizations() { + if (this.config.showClub) this.loadClubs() + if (!this.config.showUnit) return + if (this.config.canChooseUnion) { + this.organizationsLoading = true + $.post('/platform/vi/common/unions', {isPermission: false}) + .then((res) => { + if (this._isDestroyed) return + if (res.code === 0) this.unions = [{text: '全部工会', value: ''}].concat((res.data || []).map((item) => ({text: item.unionname, value: item.id}))) + else this.$toast(res.msg || '工会加载失败') + }) + .fail(() => { if (!this._isDestroyed) this.$toast('工会加载失败,请重试') }) + .always(() => { this.organizationsLoading = false }) + } + this.loadUnits() + }, + /** 协会选项返回 stid/name,与 PC getClubsByRole 的筛选值一致。 */ + loadClubs() { + if (this.clubsLoading) return + this.clubsLoading = true + $.post('/platform/vi/common/getClubsByRole', {}) + .then((res) => { + if (this._isDestroyed) return + if (res.code === 0) this.clubs = [{text: '全部协会', value: ''}].concat((res.data || []).map((item) => ({text: item.name, value: item.stid}))) + else this.$toast(res.msg || '协会加载失败') + }) + .fail(() => { if (!this._isDestroyed) this.$toast('协会加载失败,请重试') }) + .always(() => { this.clubsLoading = false }) + }, + /** 更换工会清空单位;序号避免旧响应覆盖新选项。无单位筛选的协会页不请求该接口。 */ + loadUnits() { + if (!this.config.showUnit) return + const serial = ++this.unitsSerial + this.unitsLoading = true + return $.post('/platform/vi/common/units', {unionId: this.config.canChooseUnion ? this.pageForm.unionId : this.config.currentUnionId}) + .then((res) => { + if (serial !== this.unitsSerial || this._isDestroyed) return + if (res.code === 0) this.units = [{text: '全部单位', value: ''}].concat((res.data || []).map((item) => ({text: item.name, value: item.id}))) + else this.$toast(res.msg || '单位加载失败') + }) + .fail(() => { if (serial === this.unitsSerial && !this._isDestroyed) this.$toast('单位加载失败,请重试') }) + .always(() => { if (serial === this.unitsSerial) this.unitsLoading = false }) + }, + openMore() { + this.moreForm = Object.assign({}, this.pageForm) + this.loadUnits() + if (this.config.showClub && this.clubs.length === 1) this.loadClubs() + this.$options.popupSession.open('more') + }, + setMoreField(key, value) { this.$set(this.moreForm, key, value) }, + setChoiceQuery(value) { this.$set(this, 'choiceQuery', value) }, + /** 选择项弹框压在更多筛选上层,返回时只退出当前选择,不丢弃其他筛选值。 */ + openChoice(key) { + this.choiceKey = key + this.choiceQuery = '' + this.$options.popupSession.open('choice') + }, + selectChoice(value) { + this.$set(this.moreForm, this.choiceKey, value) + this.$options.popupSession.close() + }, + optionText(key, value) { + const option = (this.choiceOptions[key] || []).find((item) => item.value === value) + return option ? option.text : '请选择' + }, + resetMore() { + this.moreForm = Object.assign({}, this.moreForm, {unitId: this.config.canChooseUnion ? '' : this.pageForm.unitId, searchName: 'u.username', searchKeyword: '', activityName: '', clubId: this.config.stage === 'clubChairman' ? this.pageForm.clubId : '', pageOrderName: '', pageOrderBy: 'desc'}) + }, + applyMore() { + const values = this.moreForm + this.$options.popupSession.close().then(() => { + if (this._isDestroyed) return + const keys = ['searchName', 'searchKeyword', 'activityName', 'pageOrderName', 'pageOrderBy'] + if (this.config.showClub && this.config.stage !== 'clubChairman') keys.push('clubId') + if (this.config.canChooseUnion) keys.push('unitId') + keys.forEach((key) => this.$set(this.pageForm, key, values[key])) + this.doSearch() + }) + }, + /** key 为年度/组织/状态字段,open 为菜单展开状态;三个菜单切换时共用一层历史。 */ + setFilterOpen(key, open) { + this.$set(this.filterOpen, key, open) + if (open && key === 'organization') { + if (this.organizationOptions.length === 1 && !this.organizationLoading) this.loadOrganizations() + } + // Vant 切换菜单会同时触发旧菜单 close 和新菜单 open,等本轮更新结束后统一判断。 + this.$nextTick(() => { + if (this._isDestroyed || this.filterClosing) return + if (Object.values(this.filterOpen).some(Boolean)) { + if (!this.popups.filter) this.$options.popupSession.open('filter') + } else if (this.popups.filter) { + this.filterClosing = true + this.$options.popupSession.close().then(() => { this.filterClosing = false }) + } + }) + }, + /** key 为筛选字段,value 为对应选项值;仅值改变时刷新,菜单收起事件负责回退历史。 */ + selectFilter(key, value) { + if (this.pageForm[key] === value) return + this.$set(this.pageForm, key, value) + if (key === 'unionId') { + this.$set(this.pageForm, 'unitId', '') + this.units = [{text: '全部单位', value: ''}] + this.loadUnits() + } + this.doSearch() + }, + /** 每次打开申请清空上次的意见和签字,防止在不同申请之间串用审核数据。 */ + openDetail(id, audit) { + if (this.formLoading) return + this.detailId = id + this.detail = {} + this.auditMode = audit + this.formData = {id, auditOpinion: '', auditSign: ''} + this.reviewDate = moment().format('YYYY-MM-DD') + this.$options.popupSession.open('detail') + this.loadDetail(id) + }, + loadDetail(id) { + const serial = ++this.detailSerial + this.detailLoading = true + $.post(this.baseUrl + '/findOne', {id}) + .then((res) => { + if (serial !== this.detailSerial || !this.popups.detail || this._isDestroyed) return + if (res.code === 0 && res.data) { + this.detail = res.data + if (!this.detail.canReview) this.auditMode = false + } else { + this.$toast(res.msg || '详情加载失败') + } + }) + .fail(() => { + if (serial === this.detailSerial && !this._isDestroyed) this.$toast('详情加载失败,请重试') + }) + .always(() => { + if (serial === this.detailSerial && !this._isDestroyed) this.detailLoading = false + }) + }, + startAudit() { + this.auditMode = true + this.$nextTick(() => { + const body = this.$el.querySelector('.reimbursement-review-detail-body') + if (body) body.scrollTop = body.scrollHeight + }) + }, + openSignature() { + if (!this.formLoading) this.$options.popupSession.open('sign') + }, + /** signature 是 mobile-sign 回传的本次签字 Base64,随审核请求一起保存。 */ + setSignature(signature) { + this.$set(this.formData, 'auditSign', signature) + }, + confirmReview(flag) { + if (this.formLoading || !this.detail.canReview) return + if (!this.formData.auditOpinion.trim() || this.formData.auditOpinion.trim().length > 500 || !this.formData.auditSign) { + this.$toast('请填写1至500字审核意见并签字') + return + } + this.reviewFlag = flag + this.$options.popupSession.open('confirm') + }, + /** Vant 用 done 结束按钮 loading;显隐仍由历史栈驱动,避免组件自行关闭后留下历史层。 */ + beforeReviewClose(action, done) { + if (this.formLoading) { + done(false) + return + } + if (action === 'confirm') { + this.submitReview().always(() => done(false)) + } else { + this.$options.popupSession.close().then(() => done(false)) + } + }, + /** id 为报销主键,audit 为意见/签字/通过标识 JSON;返回 code=0 成功,身份与时间由服务端确定。 */ + submitReview() { + if (this.formLoading) return + this.formLoading = true + const audit = {auditOpinion: this.formData.auditOpinion.trim(), auditSign: this.formData.auditSign, auditPass: this.reviewFlag} + return $.post(this.baseUrl + '/doAuditing', {id: this.formData.id, audit: JSON.stringify(audit)}) + .then((res) => { + if (this._isDestroyed) return + if (res.code === 0) { + this.$toast.success('审核成功') + this.$options.popupSession.clear().then(() => { + if (this._isDestroyed) return + this.formData = {id: '', auditOpinion: '', auditSign: ''} + this.doSearch() + }) + } else { + this.$toast(res.msg || '审核失败') + // 关闭确认层后重读状态;若已被 PC 处理则切换为只读,保留未提交的意见便于核对。 + if (this.popups.confirm) this.$options.popupSession.close().then(() => this.loadDetail(this.detailId)) + } + }) + .fail(() => this.$toast('提交失败,请检查网络后重试')) + .always(() => { this.formLoading = false }) + }, + /** 图片使用文件流,文档沿用项目预览接口;预览自身同样占一层历史。 */ + previewFile(file) { + if (file.imageUrl) { + this.previewIsImage = true + this.previewUrl = file.imageUrl + this.$options.popupSession.open('preview') + return + } + if (!file.id) { + this.$toast('附件信息不完整') + return + } + this.previewIsImage = /\.(png|jpe?g|gif|webp|bmp)$/i.test(file.filename || file.filepath || '') + const streamUrl = '/file_server/fileStreamPreview?id=' + encodeURIComponent(file.id) + this.previewUrl = this.previewIsImage + ? streamUrl + : '/assets/platform/plugins/pdfJs/web/viewer.html?file=' + encodeURIComponent(streamUrl) + this.$options.popupSession.open('preview') + }, + closePopup() { + if (this.formLoading) { + this.$toast('正在提交,请稍候') + return + } + this.$options.popupSession.close() + }, + /** 有弹框时仅关闭顶层;列表返回沿用活动申报页的 PJAX 首页入口,避免浏览器历史指向其他页面。 */ + back() { + if (Object.values(this.popups).some(Boolean)) { + this.closePopup() + return + } + this.$options.popupSession.clear().then(() => { + this.$destroy() + pjaxReplace('/mobile/index') + }) + } + }, + created() { + // 仅注册状态同步回调;popstate 由公共管理器统一监听。 + this.$options.popupSession = window.mobilePopupHistory.register('reimbursementReview-' + this.config.stage, (layers) => { + Object.keys(this.popups).forEach((name) => this.$set(this.popups, name, layers.includes(name))) + // 系统返回或页面返回先收起下拉菜单;由公共历史栈驱动,页面不另绑 popstate。 + if (!layers.includes('filter')) { + Object.keys(this.filterOpen).forEach((key) => { + this.$set(this.filterOpen, key, false) + const dropdown = this.$refs[key + 'Filter'] + if (dropdown) dropdown.toggle(false) + }) + } + }) + const user = JSON.parse(window.sessionStorage.getItem('user') || '{}') + this.reviewerName = user.username || '' + this.loadOrganizations() + this.doSearch() + const query = new URLSearchParams(window.location.search) + const id = query.get('id') + if (id) this.openDetail(id, query.get('mode') === 'audit') + // PJAX 不会自动销毁旧 Vue 实例,在替换容器前注销页面回调。 + $(document).on('pjax:beforeReplace.reimbursementReview', () => this.$destroy()) + }, + beforeDestroy() { + this.listSerial++ + this.detailSerial++ + this.unitsSerial++ + const session = this.$options.popupSession + if (Object.values(this.popups).some(Boolean)) { + session.clear().then(() => session.dispose()) + } else { + // PJAX 替换后可能立即创建新页面;空栈必须同步注销,不能等 Promise 回调再释放注册。 + session.dispose() + } + $(document).off('pjax:beforeReplace.reimbursementReview') + } +}) diff --git a/src/main/resources/views/mobile/jf/reimbursement/schoolChairman/index.html b/src/main/resources/views/mobile/jf/reimbursement/schoolChairman/index.html new file mode 100644 index 0000000..7506635 --- /dev/null +++ b/src/main/resources/views/mobile/jf/reimbursement/schoolChairman/index.html @@ -0,0 +1,18 @@ + + + + diff --git a/src/main/resources/views/mobile/jf/reimbursement/schoolDirector/index.html b/src/main/resources/views/mobile/jf/reimbursement/schoolDirector/index.html new file mode 100644 index 0000000..4958ecb --- /dev/null +++ b/src/main/resources/views/mobile/jf/reimbursement/schoolDirector/index.html @@ -0,0 +1,18 @@ + + + + diff --git a/src/main/resources/views/mobile/jf/reimbursement/unionChairman/index.html b/src/main/resources/views/mobile/jf/reimbursement/unionChairman/index.html new file mode 100644 index 0000000..e99d7be --- /dev/null +++ b/src/main/resources/views/mobile/jf/reimbursement/unionChairman/index.html @@ -0,0 +1,18 @@ + + + + diff --git a/src/main/resources/views/mobile/platform.html b/src/main/resources/views/mobile/platform.html index 8e5098d..412e231 100644 --- a/src/main/resources/views/mobile/platform.html +++ b/src/main/resources/views/mobile/platform.html @@ -25,6 +25,8 @@ + + @@ -79,7 +81,9 @@ } })() - function pjaxReplace(url){ + window.pjaxReplace = (url) => { + // 普通表单的导航也先消费公共签字层,避免携带未回退的弹框历史进入下一页面。 + if (window.mobilePopupHistory.closeTop()) return $.pjax({ url: url, container: "#container", diff --git a/src/main/resources/views/platform/common/signature.html b/src/main/resources/views/platform/common/signature.html index f363701..cb7e485 100644 --- a/src/main/resources/views/platform/common/signature.html +++ b/src/main/resources/views/platform/common/signature.html @@ -1,53 +1,49 @@ - + 签字 - - + + + + -
- +
+ + +
- +