Compare commits

..
11 Commits
Author SHA1 Message Date
c-hongqw1 54c90f9d73 路径统一替换为模板根目录下的完整路径 2026-10-26 11:50:33 +08:00
c-hongqw1 b86bbcc7ea 协会签字适配 2026-10-22 09:43:53 +08:00
c-zhouhf1 9d51c70adb Merge branch 'main' of http://gitea.dd3skj.com/c-hongqw1/zhgh_cpu
# Conflicts:
#	src/main/resources/static/components/sign/mobileSign.vue
2026-10-22 08:49:06 +08:00
c-zhouhf1 be401ab0f0 签字优化 2026-10-22 08:47:43 +08:00
c-hongqw1 97977b0d05 手机端协会入会增加审核、双端待办优化,协会入会申请增加双端待办,并给会长、秘书发送企业微信消息,这两个角色同时拥有审核权限 2026-10-21 16:36:50 +08:00
c-hongqw1 85117e8b70 手机端协会入会增加审核、双端待办优化,协会入会申请增加双端待办,并给会长、秘书发送企业微信消息,这两个角色同时拥有审核权限 2026-10-21 16:20:13 +08:00
c-zhouhf1 e46758d12d Merge branch 'main' of http://gitea.dd3skj.com/c-hongqw1/zhgh_cpu 2026-10-21 14:19:01 +08:00
c-zhouhf1 34ae5037f9 活动申报手机端审核
活动报销手机端审核
2026-10-21 14:18:45 +08:00
c-hongqw1 0e2c5d49d0 打包配置 2026-10-21 09:52:37 +08:00
c-zhangr1 3e688495fc Merge remote-tracking branch 'origin/main' 2026-10-21 09:29:48 +08:00
c-zhangr1 c8c432c7df 拉取人员接口:18开头的工号 且 人员类型为空 且 人员类型不为“非合同制”的人员,将这些人的人员类型改为“合同制”且设置为会员
会员档案查询:分工会主席只能查询当前分工会的人员的数据
2026-10-21 09:28:49 +08:00
74 changed files with 4562 additions and 1379 deletions
+3 -11
View File
@@ -20,10 +20,9 @@
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version> <version>3.7.0</version>
<configuration> <configuration>
<source>14</source> <source>${java.version}</source>
<target>14</target> <target>${java.version}</target>
<useIncrementalCompilation>false</useIncrementalCompilation> <useIncrementalCompilation>false</useIncrementalCompilation>
<compilerArgs>--enable-preview</compilerArgs>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
@@ -70,13 +69,6 @@
<artifactId>nutzboot-maven-plugin</artifactId> <artifactId>nutzboot-maven-plugin</artifactId>
<version>${nutzboot.version}</version> <version>${nutzboot.version}</version>
</plugin> </plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>15</source>
<target>15</target>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
<repositories> <repositories>
@@ -175,7 +167,7 @@
<jsoup.version>1.12.1</jsoup.version> <jsoup.version>1.12.1</jsoup.version>
<easy-captcha.version>1.6.2</easy-captcha.version> <easy-captcha.version>1.6.2</easy-captcha.version>
<jaxb-api.version>2.3.1</jaxb-api.version> <jaxb-api.version>2.3.1</jaxb-api.version>
<lombok.version>1.18.12</lombok.version> <lombok.version>1.18.16</lombok.version>
<jodconverter.version>2.2.2</jodconverter.version> <jodconverter.version>2.2.2</jodconverter.version>
<nutzboot.version>2.4.2-SNAPSHOT</nutzboot.version> <nutzboot.version>2.4.2-SNAPSHOT</nutzboot.version>
</properties> </properties>
+5 -12
View File
@@ -25,7 +25,8 @@
<poi.version>4.1.2</poi.version> <poi.version>4.1.2</poi.version>
<easy-captcha.version>1.6.2</easy-captcha.version> <easy-captcha.version>1.6.2</easy-captcha.version>
<tencentcloud-sdk-java.version>4.0.11</tencentcloud-sdk-java.version> <tencentcloud-sdk-java.version>4.0.11</tencentcloud-sdk-java.version>
<lombok.version>1.18.12</lombok.version> <!-- 使用支持 JDK 15 的 Lombok 版本,匹配本地打包及线上 Java 版本。 -->
<lombok.version>1.18.16</lombok.version>
<v.version>1.0.1</v.version> <v.version>1.0.1</v.version>
<hutool-all.version>5.7.20</hutool-all.version> <hutool-all.version>5.7.20</hutool-all.version>
<jodconverter.version>2.2.2</jodconverter.version> <jodconverter.version>2.2.2</jodconverter.version>
@@ -762,10 +763,10 @@
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version> <version>3.7.0</version>
<configuration> <configuration>
<source>14</source> <!-- Java 15 已正式支持文本块,无需启用预览特性。 -->
<target>14</target> <source>${java.version}</source>
<target>${java.version}</target>
<useIncrementalCompilation>false</useIncrementalCompilation> <useIncrementalCompilation>false</useIncrementalCompilation>
<compilerArgs>--enable-preview</compilerArgs>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
@@ -816,14 +817,6 @@
<artifactId>nutzboot-maven-plugin</artifactId> <artifactId>nutzboot-maven-plugin</artifactId>
<version>${nutzboot.version}</version> <version>${nutzboot.version}</version>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>15</source>
<target>15</target>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
<repositories> <repositories>
@@ -9,7 +9,7 @@ import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.Daos; import org.nutz.dao.util.Daos;
import org.nutz.dao.util.cri.Static; import io.v.nutz.sys.services.SysLocalProcessService;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -29,6 +29,9 @@ public class SysLocalProcessController {
@Inject @Inject
private Dao dao; private Dao dao;
@Inject
private SysLocalProcessService localProcessService;
/** /**
* 查询待办 * 查询待办
* @param mode 1待处理 2已处理 3我发起的 * @param mode 1待处理 2已处理 3我发起的
@@ -39,28 +42,7 @@ public class SysLocalProcessController {
@Ok("json:full") @Ok("json:full")
public Result todoList(Integer mode) { public Result todoList(Integer mode) {
if (mode == 1 || mode == 2) { if (mode == 1 || mode == 2) {
Sql sql = Sqls.create(""" return Result.success(localProcessService.getTaskList(mode));
SELECT
t.taskNodeName,
t.createdByUserName,
t.formUrl,
t.formUrlView,
t.createdOn,
t2.processName,
t2.nodeName
FROM
sys_local_process_instance_task t
LEFT JOIN sys_local_process_instance t2 ON t2.processUniqueId = t.processUniqueId
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and(new Static("JSON_CONTAINS( t.assignments, '\"" + ShiroUtil.getPrincipalProperty("loginname") + "\"')"));
cnd.and("t.status", "=", mode);
cnd.and("t.taskDeleteFlag", "=", 0);
cnd.desc("t2.processInitiationTime");
sql.setCondition(cnd);
List<NutMap> list = (List<NutMap>) Daos.query(dao, sql.toString(), Sqls.callback.maps());
return Result.success(list);
} else if (mode == 3) { } else if (mode == 3) {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
@@ -2,25 +2,19 @@ package io.v.nutz.sys.controllers.platform.sys.club.applyIn;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import io.v.nutz.base.annontation.ViReturn; import io.v.nutz.base.annontation.ViReturn;
import io.v.nutz.base.model.Audit;
import io.v.nutz.base.service.BaseService; import io.v.nutz.base.service.BaseService;
import io.v.nutz.base.utils.MsgApi; import io.v.nutz.base.utils.MsgApi;
import io.v.nutz.base.utils.PageUtil; import io.v.nutz.base.utils.PageUtil;
import io.v.nutz.base.utils.Roles;
import io.v.nutz.sys.constant.club.ClubRegistAuditState; import io.v.nutz.sys.constant.club.ClubRegistAuditState;
import io.v.nutz.sys.controllers.platform.sys.handler.ClubUserApplyToDoHandler;
import io.v.nutz.sys.models.Sys_club;
import io.v.nutz.sys.models.Sys_club_user;
import io.v.nutz.sys.services.SysClubService; import io.v.nutz.sys.services.SysClubService;
import io.v.nutz.sys.services.SysClubUserService; import io.v.nutz.sys.services.SysClubUserService;
import io.v.nutz.web.commons.utils.ShiroUtil; import io.v.nutz.web.commons.utils.ShiroUtil;
import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record; import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
@@ -35,7 +29,6 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import java.util.Date;
import java.util.List; import java.util.List;
/** /**
@@ -85,9 +78,31 @@ public class ClubApplyAuditController {
@Inject @Inject
private SysClubUserService sysClubUserService; private SysClubUserService sysClubUserService;
/**
* 无请求参数,查询当前会话用户的协会个人待办;无审核权限时返回空列表。
* ViReturn 返回 code、msg、data,data 为包含 title、name、count、mobileHref 的卡片列表。
*/
@At @At
@ViReturn @ViReturn
@RequiresPermissions(value = {"sys.club.sh", "sys.club.schoolsh"}, logical = Logical.OR) @RequiresAuthentication
public Object mobileTodo() {
return sysClubUserService.getMobileReviewTodo();
}
/**
* 无请求参数;data 返回本人实际办理的协会已办卡片,applyId 用于打开详情,其余字段见服务接口。
* 未授权或无已办记录时返回空列表;不接收前端指定审核人。
*/
@At
@ViReturn
@RequiresAuthentication
public Object mobileCompleted() {
return sysClubUserService.getMobileReviewCompleted();
}
@At
@ViReturn
@RequiresPermissions("sys.club.sh")
public Object pageData(@Param(value = "clubid", required = false) String clubid, public Object pageData(@Param(value = "clubid", required = false) String clubid,
@Param(value = "searchName", required = false) String searchName, @Param(value = "searchName", required = false) String searchName,
@Param(value = "searchKeyword", required = false) String searchKeyword, @Param(value = "searchKeyword", required = false) String searchKeyword,
@@ -100,6 +115,7 @@ public class ClubApplyAuditController {
@Param(value = "userState", required = false) String userState, @Param(value = "userState", required = false) String userState,
@Param(value = "sex", required = false) String sex, @Param(value = "sex", required = false) String sex,
@Param(value = "unitid", required = false) String unitid) { @Param(value = "unitid", required = false) String unitid) {
// 所属单位取人员资料;独立别名避免与申请表 unitName 重名,并用全小写匹配 Record 返回键。
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
scu.*, scu.*,
@@ -108,6 +124,7 @@ public class ClubApplyAuditController {
u.sex, u.sex,
u.userState, u.userState,
u.unionname, u.unionname,
u.unitname AS applicantunitname,
club.name clubname, club.name clubname,
club.code, club.code,
u.personType rylx u.personType rylx
@@ -139,12 +156,12 @@ public class ClubApplyAuditController {
cnd.andEX("u.sex", "=", sex); cnd.andEX("u.sex", "=", sex);
cnd.andEX("u.unitid", "=", unitid); cnd.andEX("u.unitid", "=", unitid);
if (!ShiroUtil.hasAnyRoles(new String[]{"sysadmin", "A06"})) { // 超管取得全部协会;普通审核人无所属协会时返回空列表,不能省略范围条件。
if (ShiroUtil.hasAnyRoles("club01, club02, club03, club04")) { List<String> reviewClubIds = sysClubUserService.getReviewClubIds();
cnd.and(new Static(""" if (reviewClubIds.isEmpty()) {
scu.clubid IN ( SELECT stid FROM sys_user_role WHERE roleid in ('%s','%s','%s','%s') AND userid = '%s' AND stid IS NOT NULL ) cnd.and(new Static("1 = 0"));
""".formatted(Roles.club01, Roles.club02, Roles.club03, Roles.club04, ShiroUtil.getPrincipalProperty("id")))); } else {
} cnd.and("scu.clubid", "in", reviewClubIds);
} }
if (Strings.isNotBlank(searchName) && Strings.isNotBlank(searchKeyword)) { if (Strings.isNotBlank(searchName) && Strings.isNotBlank(searchKeyword)) {
@@ -160,45 +177,28 @@ public class ClubApplyAuditController {
} }
/**
* id 为入会申请主键数组,flag 为是否通过,auditOpinion 为审核意见,auditSign 为签字内容,
* isjf 为可选缴费标记;返回 null,由 ViReturn 包装为包含 code、msg、data 的 JSON 结果。
*/
@At @At
@ViReturn @ViReturn
@RequiresAuthentication @RequiresPermissions("sys.club.sh")
@RequiresRoles(value = {"sysadmin", "club01", "club03"}, logical = Logical.OR)
@Aop(TransAop.READ_COMMITTED)
public Object check(String[] id, Boolean flag, String auditOpinion, public Object check(String[] id, Boolean flag, String auditOpinion,
String auditSign, String auditSign,
@Param(value = "isjf", required = false) Boolean isjf) { @Param(value = "isjf", required = false) Boolean isjf) {
Audit audit = new Audit(); if (id == null || id.length == 0 || flag == null) {
audit.setAuditPass(flag); throw new IllegalArgumentException("请选择申请并提供审核结果");
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString()); }
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString()); for (String applyId : id) {
audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString()); if (Strings.isBlank(applyId)) {
audit.setAuditOpinion(auditOpinion); throw new IllegalArgumentException("申请编号不能为空");
audit.setAuditTime(new Date());
audit.setAuditSign(auditSign);
Audit insert = sysClubService.dao().insert(audit);
Cnd cnd = Cnd.NEW();
cnd.where().andInStrArray("id", id);
sysClubUserService.update(Chain.make("status", flag ? 5 : 2)
.add("isjf", isjf).add("clubAuditId", insert.getId()), cnd);
List<Sys_club_user> clubUsers = sysClubUserService.dao().query(Sys_club_user.class, Cnd.where("id", "in", id));
for (Sys_club_user clubUser : clubUsers) {
Sys_club club = sysClubService.fetch(clubUser.getClubid());
Sql sql = Sqls.fetchRecord("select username,loginname from sys_user where id = @id").setParam("id", clubUser.getUserid());
sysClubUserService.dao().execute(sql);
Record userRecord = (Record) sql.getResult();
if (flag) {
String content = "您申请加入的" + club.getName() + "已经审批通过。";
System.out.println(userRecord.getString("username"));
System.out.println(userRecord.getString("loginname"));
System.out.println(content);
} else {
String content = "您申请加入的" + club.getName() + "被审批拒绝了,拒绝理由:" + auditOpinion;
System.out.println(userRecord.getString("username"));
System.out.println(content);
} }
} }
sysClubUserService.reviewApplications(id, flag, auditOpinion, auditSign, isjf);
return null; return null;
} }
@@ -206,7 +206,7 @@ public class ClubApplyAuditController {
//获取当前登录用户负责的社团 //获取当前登录用户负责的社团
@At @At
@ViReturn @ViReturn
@RequiresAuthentication @RequiresPermissions("sys.club.sh")
public Object getMyClub(@Param(value = "searchKeyword", required = false) String searchKeyword, public Object getMyClub(@Param(value = "searchKeyword", required = false) String searchKeyword,
@Param(value = "pageNumber", required = false) int pageNumber, @Param(value = "pageNumber", required = false) int pageNumber,
@Param(value = "pageSize", required = false) int pageSize, @Param(value = "pageSize", required = false) int pageSize,
@@ -235,12 +235,12 @@ public class ClubApplyAuditController {
cnd.and("state", "=", ClubRegistAuditState.SCHOOL_PASS); cnd.and("state", "=", ClubRegistAuditState.SCHOOL_PASS);
cnd.and("isjs", "=", false); cnd.and("isjs", "=", false);
if (!ShiroUtil.hasAnyRoles(new String[]{"sysadmin", "A06"})) { // 超管取得全部协会;普通审核人无所属协会时返回空列表,不能省略范围条件。
if (ShiroUtil.hasAnyRoles("club01, club02, club03, club04")) { List<String> reviewClubIds = sysClubUserService.getReviewClubIds();
cnd.and(new Static(""" if (reviewClubIds.isEmpty()) {
club.id IN ( SELECT stid FROM sys_user_role WHERE roleid in ('%s','%s','%s','%s') AND userid = '%s' AND stid IS NOT NULL ) cnd.and(new Static("1 = 0"));
""".formatted(Roles.club01, Roles.club02, Roles.club03, Roles.club04, ShiroUtil.getPrincipalProperty("id")))); } else {
} cnd.and("club.id", "in", reviewClubIds);
} }
if (StrUtil.isNotBlank(clubType)) { if (StrUtil.isNotBlank(clubType)) {
cnd.and("club.type", "=", clubType); cnd.and("club.type", "=", clubType);
@@ -257,7 +257,7 @@ public class ClubApplyAuditController {
//获取社团已审或者未审人员 //获取社团已审或者未审人员
@At @At
@ViReturn @ViReturn
@RequiresAuthentication @RequiresPermissions("sys.club.sh")
public Object getClubUser(@Param(value = "id", required = false) String id, public Object getClubUser(@Param(value = "id", required = false) String id,
@Param(value = "auditType", required = false) String auditType, @Param(value = "auditType", required = false) String auditType,
@Param(value = "type", required = false) int type) { @Param(value = "type", required = false) int type) {
@@ -310,12 +310,12 @@ public class ClubApplyAuditController {
} }
cnd.andEX("scu.clubid", "=", id); cnd.andEX("scu.clubid", "=", id);
if (!ShiroUtil.hasAnyRoles(new String[]{"sysadmin", "A06"})) { // 超管取得全部协会;普通审核人无所属协会时返回空列表,不能省略范围条件。
if (ShiroUtil.hasAnyRoles("club01, club02, club03, club04")) { List<String> reviewClubIds = sysClubUserService.getReviewClubIds();
cnd.and(new Static(""" if (reviewClubIds.isEmpty()) {
club.id IN ( SELECT stid FROM sys_user_role WHERE roleid in ('%s','%s','%s','%s') AND userid = '%s' AND stid IS NOT NULL ) cnd.and(new Static("1 = 0"));
""".formatted(Roles.club01, Roles.club02, Roles.club03, Roles.club04, ShiroUtil.getPrincipalProperty("id")))); } else {
} cnd.and("club.id", "in", reviewClubIds);
} }
sql.setCondition(cnd); sql.setCondition(cnd);
List<Record> list = sysClubService.list(sql); List<Record> list = sysClubService.list(sql);
@@ -171,56 +171,7 @@ public class ClubApplyInController {
|| (sys_club_user.getOtherThing() != null && sys_club_user.getOtherThing().length() > 100)) { || (sys_club_user.getOtherThing() != null && sys_club_user.getOtherThing().length() > 100)) {
throw new IllegalArgumentException("联系电话最多20字,紧急联系人最多50字,特别说明最多100字"); throw new IllegalArgumentException("联系电话最多20字,紧急联系人最多50字,特别说明最多100字");
} }
sysClubUserService.clear(Cnd.where("clubid", "=", sys_club_user.getClubid()).and("userid", "=", ShiroUtil.getPrincipalProperty("id"))); sysClubUserService.submitApplication(sys_club_user);
String dateTime = DateUtil.getDateTime();
sys_club_user.setUserid(ShiroUtil.getPrincipalProperty("id").toString());
sys_club_user.setJoin_time(dateTime);
sys_club_user.setSf(2);
sys_club_user.setStatus(1);
sys_club_user.setIsjf(false);
sys_club_user.setIsNormal(true);
sys_club_user.setGiveMoney(true);
Sys_club_user clubUser = sysClubUserService.insert(sys_club_user);
//插入后,发起待办流程
ClubUserApplyToDoHandler.START_PROCESS.exec(clubUser);
//发任务给协会会长
ClubUserApplyToDoHandler.CREATE_CLUB_TASK.exec(clubUser);
//完成申请人的任务(申请拒绝的情况)
ClubUserApplyToDoHandler.COMPLETE_APPLY_TASK.exec(clubUser);
//根据社团id查询社团负责人,发短信
Sql sql = Sqls.create("""
SELECT
u.loginname as loginName,
u.username as userName,
u.unitid as unitId,
u.unitname as unitName,
u.mobile,
c.`name`
FROM
sys_user_role r
LEFT JOIN
`user` u ON r.userId = u.id
LEFT JOIN
sys_club c ON c.id = r.stid
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("stid", "=", sys_club_user.getClubid());
cnd.and("roleId", "in", Lang.array(Roles.club01, Roles.club02, Roles.club03, Roles.club04));
sql.setCondition(cnd);
List<NutMap> list = baseService.listMap(sql);
String platformUsername = ShiroUtil.getPlatformUsername();
list.forEach(item -> {
String content = "%s老师您好,%s老师于%s申请加入%s,请登陆智慧工会进行审核!".formatted(item.getString("userName")
, platformUsername, dateTime, item.getString("name"));
//msgApi.sendWxMsg(person, content);
});
return null; return null;
} }
@@ -84,12 +84,12 @@ public enum ClubUserApplyToDoHandler {
}, },
/** /**
* 创建协会会长任务 * 为申请所属协会的会长、秘书长创建审核任务
*/ */
CREATE_CLUB_TASK() { CREATE_CLUB_TASK() {
@Override @Override
public void exec(Sys_club_user clubUser) { public void exec(Sys_club_user clubUser) {
//查询协会审核人 // 角色关联的 stid 为所属协会主键,按本次申请 clubid 筛选,避免跨协会派发。
Sql sqlStr = Sqls.create(""" Sql sqlStr = Sqls.create("""
SELECT SELECT
u.loginname u.loginname
@@ -100,7 +100,9 @@ public enum ClubUserApplyToDoHandler {
GROUP BY GROUP BY
sur.userId sur.userId
"""); """);
sqlStr.setCondition(Cnd.NEW().and("sur.roleId", "in", Lang.array(Roles.club01, Roles.club02, Roles.club03, Roles.club04))); sqlStr.setCondition(Cnd.NEW()
.and("sur.stid", "=", clubUser.getClubid())
.and("sur.roleId", "in", Lang.array(Roles.club01, Roles.club03)));
Sql sql = Sqls.queryString(sqlStr.toString()); Sql sql = Sqls.queryString(sqlStr.toString());
dao.execute(sql); dao.execute(sql);
List<String> loginNames = sql.getList(String.class); List<String> loginNames = sql.getList(String.class);
@@ -3,6 +3,47 @@ package io.v.nutz.sys.services;
import cn.wizzer.framework.base.service.BaseService; import cn.wizzer.framework.base.service.BaseService;
import io.v.nutz.sys.models.Sys_club_user; import io.v.nutz.sys.models.Sys_club_user;
import java.util.List;
import org.nutz.lang.util.NutMap;
public interface SysClubUserService extends BaseService<Sys_club_user> { public interface SysClubUserService extends BaseService<Sys_club_user> {
/**
* PC、H5 共用的入会提交入口:保存申请、创建审核待办并通知所属协会的会长和秘书长。
* 无返回值;保存或待办失败时抛出异常回滚,消息发送异常仅记录日志。
* @param application 已通过接口必填及长度校验的入会资料,clubid 为申请协会主键;
* 申请人、申请时间和待审核状态由服务端确定,不采信客户端传值
*/
void submitApplication(Sys_club_user application);
/**
* 按当前登录用户确定可查看及审核的协会,不接收客户端用户 ID 或代审标志。
* @return 协会主键列表:超管返回全部协会,其他人仅返回本人担任会长或秘书长的协会;无资格时为空
*/
List<String> getReviewClubIds();
/**
* 查询当前登录人的本年度协会审核待办,不接收客户端用户或协会参数。
* @return 首页卡片列表;无待办返回空列表,卡片包含 title、name、count 和 mobileHref
* 仅统计已分配给本人且仍具有所属协会会长或秘书长资格的有效待办,超管代审不额外派单
*/
List<NutMap> getMobileReviewTodo();
/**
* 查询当前登录人本年度实际完成的协会审核,不接收前端审核人参数。
* @return 已办列表,包含 businessType、applyId、moduleName、auditState、auditTime、mobileUrl;无记录为空列表
*/
List<NutMap> getMobileReviewCompleted();
/**
* 保存协会审核结果,并完成每条申请的审核待办及流程。
* 无返回值;保存或关闭流程失败时抛出异常并回滚事务。
* @param ids 入会申请主键数组,支持单条及批量审核
* @param pass true 通过(状态 5),false 拒绝(状态 2)
* @param auditOpinion 审核意见
* @param auditSign 审核签字内容
* @param isjf 可选缴费标记,沿用现有保存规则
*/
void reviewApplications(String[] ids, Boolean pass, String auditOpinion, String auditSign, Boolean isjf);
} }
@@ -2,9 +2,19 @@ package io.v.nutz.sys.services;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
import org.nutz.lang.util.NutMap;
public interface SysLocalProcessService { public interface SysLocalProcessService {
/**
* 查询当前登录人的任务列表,用户身份从会话获取,不由客户端指定。
* @param mode 1 待处理,2 已处理;其他值返回空列表
* @return taskNodeName、createdByUserName、formUrl、formUrlView、createdOn、processName、nodeName 组成的列表
* 协会待办须对应有效待审申请;协会已办仅归实际处理人,其他业务沿用原接收人规则
*/
List<NutMap> getTaskList(Integer mode);
/** /**
* 开始一个实例流程 * 开始一个实例流程
* *
@@ -3,14 +3,268 @@ package io.v.nutz.sys.services.impl;
import cn.wizzer.framework.base.service.BaseServiceImpl; import cn.wizzer.framework.base.service.BaseServiceImpl;
import io.v.nutz.sys.models.Sys_club_user; import io.v.nutz.sys.models.Sys_club_user;
import io.v.nutz.sys.services.SysClubUserService; import io.v.nutz.sys.services.SysClubUserService;
import io.v.nutz.base.model.Audit;
import io.v.nutz.base.utils.DateUtil;
import io.v.nutz.base.utils.MsgApi;
import io.v.nutz.base.utils.Roles;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthorizedException;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import java.util.Arrays;
import java.util.HashSet;
import io.v.nutz.sys.controllers.platform.sys.handler.ClubUserApplyToDoHandler;
import io.v.nutz.sys.models.Sys_club;
import io.v.nutz.web.commons.utils.ShiroUtil;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record;
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.ioc.loader.annotation.IocBean;
import java.util.Date;
import java.util.List;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
@Slf4j
public class SysClubUserServiceImpl extends BaseServiceImpl<Sys_club_user> implements SysClubUserService { public class SysClubUserServiceImpl extends BaseServiceImpl<Sys_club_user> implements SysClubUserService {
@Inject
private MsgApi msgApi;
public SysClubUserServiceImpl(Dao dao) { public SysClubUserServiceImpl(Dao dao) {
super(dao); super(dao);
} }
/**
* 沿用双端原有的申请替换和待办顺序;申请及待办保存完成后发送审核提醒。
* 参数、返回值和异常约定见接口,当前申请人以登录会话为准。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void submitApplication(Sys_club_user application) {
String userId = ShiroUtil.getPrincipalProperty("id").toString();
clear(Cnd.where("clubid", "=", application.getClubid()).and("userid", "=", userId));
application.setUserid(userId);
application.setJoin_time(DateUtil.getDateTime());
application.setSf(2);
application.setStatus(1);
application.setIsjf(false);
application.setIsNormal(true);
application.setGiveMoney(true);
Sys_club_user clubUser = insert(application);
// 新建本次流程及会长、秘书长待办,再完成申请人被退回时的提交任务。
ClubUserApplyToDoHandler.START_PROCESS.exec(clubUser);
ClubUserApplyToDoHandler.CREATE_CLUB_TASK.exec(clubUser);
ClubUserApplyToDoHandler.COMPLETE_APPLY_TASK.exec(clubUser);
notifyApplicationReviewers(clubUser);
}
/**
* 按已保存申请的 clubid 查询审核角色,复用日常慰问的企业微信消息卡片发送方式。
* 无返回值;缺少接收人工号、协会资料或通知异常时记录日志,不中断申请提交。
*/
private void notifyApplicationReviewers(Sys_club_user application) {
try {
// stid 限定申请所属协会;按工号去重,兼任会长、秘书长也只接收一条消息。
Sql sql = Sqls.queryString("""
SELECT DISTINCT TRIM(u.loginname)
FROM sys_user_role sur
INNER JOIN `user` u ON u.id = sur.userid
WHERE sur.stid = @clubId
AND sur.roleId IN (@chairman, @secretary)
AND u.loginname IS NOT NULL AND TRIM(u.loginname) <> ''
""");
sql.setParam("clubId", application.getClubid());
sql.setParam("chairman", Roles.club01);
sql.setParam("secretary", Roles.club03);
dao().execute(sql);
List<String> loginNames = sql.getList(String.class);
if (loginNames.isEmpty()) {
log.warn("入会申请审核提醒未发送:未配置会长、秘书长或有效工号,申请ID={},协会ID={}",
application.getId(), application.getClubid());
return;
}
Sys_club club = dao().fetch(Sys_club.class, application.getClubid());
if (club == null) {
log.warn("入会申请审核提醒未发送:协会不存在,申请ID={},协会ID={}",
application.getId(), application.getClubid());
return;
}
String content = "您好,%s老师于%s申请加入【%s】,请您前往门户网站或点击此消息链接登录智慧工会平台进行审核。"
.formatted(ShiroUtil.getPlatformUsername(), application.getJoin_time(), club.getName());
// 统一使用 H5 审核入口,消息开关、开发环境禁发及频率限制由 MsgApi 控制。
msgApi.sendWxMsg("智慧工会【协会入会】", "https://zhghydd.cpu.edu.cn/platform/sys/club/sh/mobile",
"textcard", content, loginNames, "");
} catch (Exception e) {
// 通知渠道故障或频率限制不应使申请失败,保留申请 ID 便于排查未送达原因。
log.error("入会申请审核提醒发送异常,申请ID=" + application.getId(), e);
}
}
/**
* 首页个人待办必须同时满足接收人、当前任职、申请及流程状态,不复用超管全协会代审范围。
* 返回首页卡片结构见接口;按申请去重,防止重复任务或兼任角色导致数量翻倍。
*/
@Override
public List<NutMap> getMobileReviewTodo() {
if (!ShiroUtil.hasPermission("sys.club.sh")
|| !ShiroUtil.hasAnyRoles(new String[]{"club01", "club03"})) {
return List.of();
}
Sql sql = Sqls.fetchInt("""
SELECT COUNT(DISTINCT cu.id)
FROM sys_club_user cu
INNER JOIN sys_local_process_instance_task t
ON t.processUniqueId = CONCAT('sys_club_user@', cu.id)
INNER JOIN sys_local_process_instance p ON p.processUniqueId = t.processUniqueId
WHERE cu.status = 1 AND cu.sf = 2 AND cu.isNormal = 1
AND t.taskUniqueId = 'clubAuditId' AND t.status = 1
AND t.taskDeleteFlag = 0 AND t.delFlag = 0
AND p.processInstanceStatus = 1 AND p.processDeleteFlag = 0 AND p.delFlag = 0
AND t.createdOn >= MAKEDATE(YEAR(CURRENT_DATE), 1)
AND t.createdOn < MAKEDATE(YEAR(CURRENT_DATE) + 1, 1)
AND JSON_CONTAINS(t.assignments, JSON_QUOTE(@loginName))
AND EXISTS (
SELECT 1 FROM sys_user_role sur
WHERE sur.userId = @userId AND sur.stid = cu.clubid
AND sur.roleId IN (@chairman, @secretary)
)
""");
// 登录用户及工号均取自会话;参数绑定避免工号中的特殊字符影响 JSON 或 SQL。
sql.setParam("loginName", ShiroUtil.getPrincipalProperty("loginname"));
sql.setParam("userId", ShiroUtil.getPlatformUid());
sql.setParam("chairman", Roles.club01);
sql.setParam("secretary", Roles.club03);
dao().execute(sql);
int count = sql.getInt();
if (count == 0) return List.of();
return List.of(NutMap.NEW().setv("title", "协会入会申请")
.setv("name", "协会入会审核").setv("count", count)
.setv("mobileHref", "/platform/sys/club/sh/mobile"));
}
/**
* 已办以审核记录中的实际审核人为准,超管代审归超管本人,不归任务的其他接收人。
* 按审核完成年份筛选,因此往年发起、今年办理的申请仍会显示;使用 maps 保留页面字段大小写。
*/
@Override
public List<NutMap> getMobileReviewCompleted() {
if (!ShiroUtil.hasPermission("sys.club.sh")) return List.of();
Sql sql = Sqls.create("""
SELECT 'clubApply' AS businessType, cu.id AS applyId,
CONCAT('协会入会申请 · ', COALESCE(c.name, ''), ' · ', COALESCE(u.username, '')) AS moduleName,
CASE WHEN a.auditPass = 1 THEN '审核通过' ELSE '审核拒绝' END AS auditState,
DATE_FORMAT(a.auditTime, '%Y-%m-%d %H:%i:%s') AS auditTime,
'/platform/sys/club/sh/mobile' AS mobileUrl
FROM sys_club_user cu
INNER JOIN audit a ON a.id = cu.clubAuditId
LEFT JOIN sys_club c ON c.id = cu.clubid
LEFT JOIN sys_user u ON u.id = cu.userid
WHERE cu.sf = 2 AND cu.isNormal = 1 AND cu.status IN (2, 5)
AND a.auditor = @userId
AND a.auditTime >= MAKEDATE(YEAR(CURRENT_DATE), 1)
AND a.auditTime < MAKEDATE(YEAR(CURRENT_DATE) + 1, 1)
ORDER BY a.auditTime DESC, cu.id
""");
sql.setParam("userId", ShiroUtil.getPlatformUid());
sql.setCallback(Sqls.callback.maps());
dao().execute(sql);
return sql.getList(NutMap.class);
}
/** 查询当前用户可查看和审核的协会;超管允许全协会代审,其他人按实际任职限定。 */
@Override
public List<String> getReviewClubIds() {
// 超管使用服务端角色判定,不接收前端传入的代审标志;返回实际协会 ID 供查询与审核共用。
if (ShiroUtil.hasAnyRoles(new String[]{"sysadmin"})) {
Sql allClubs = Sqls.queryString("SELECT id FROM sys_club");
dao().execute(allClubs);
return allClubs.getList(String.class);
}
Sql sql = Sqls.queryString("SELECT DISTINCT stid FROM sys_user_role "
+ "WHERE userId = @userId AND roleId IN (@chairman, @secretary) "
+ "AND stid IS NOT NULL AND stid <> ''");
sql.setParam("userId", ShiroUtil.getPlatformUid());
sql.setParam("chairman", Roles.club01);
sql.setParam("secretary", Roles.club03);
dao().execute(sql);
return sql.getList(String.class);
}
/**
* 审核记录、申请状态、待办及流程在同一事务内保存,防止出现已审核但待办未关闭。
* 参数及返回约定见接口;实际审核人从当前登录会话取得。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void reviewApplications(String[] id, Boolean flag, String auditOpinion, String auditSign, Boolean isjf) {
// 所有权限及所属协会检查必须在任何写入前完成,批量混入越权申请时整批拒绝。
SecurityUtils.getSubject().checkPermission("sys.club.sh");
if (!ShiroUtil.hasAnyRoles(new String[]{"sysadmin", "club01", "club03"})) {
throw new UnauthorizedException("仅超管或申请所属协会的会长、秘书长可以审核");
}
if (id == null || id.length == 0 || flag == null
|| Arrays.stream(id).anyMatch(Strings::isBlank)) {
throw new IllegalArgumentException("请选择申请并提供审核结果");
}
// auditSign 只接受上传接口返回的 PNG 文件路径,禁止将 Base64 或任意地址写入审核记录。
if (auditSign == null || !auditSign.matches("^/signature/[0-9]{4}/[0-9]{2}/[0-9]{2}/[A-Za-z0-9_-]+[.]png$")) {
throw new IllegalArgumentException("请完成签字图片上传后再审核");
}
List<Sys_club_user> applications = dao().query(Sys_club_user.class, Cnd.where("id", "in", id));
if (applications.size() != new HashSet<>(Arrays.asList(id)).size()) {
throw new IllegalArgumentException("申请记录不存在,请刷新后重试");
}
List<String> reviewClubIds = getReviewClubIds();
for (Sys_club_user application : applications) {
if (!reviewClubIds.contains(application.getClubid())) {
throw new UnauthorizedException("仅超管或申请所属协会的会长、秘书长可以审核");
}
}
Audit audit = new Audit();
audit.setAuditPass(flag);
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString());
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString());
audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString());
audit.setAuditOpinion(auditOpinion);
audit.setAuditTime(new Date());
audit.setAuditSign(auditSign);
Audit insert = dao().insert(audit);
Cnd cnd = Cnd.NEW();
cnd.where().andInStrArray("id", id);
update(Chain.make("status", flag ? 5 : 2)
.add("isjf", isjf).add("clubAuditId", insert.getId()), cnd);
List<Sys_club_user> clubUsers = dao().query(Sys_club_user.class, Cnd.where("id", "in", id));
for (Sys_club_user clubUser : clubUsers) {
// 先记录实际审核人和意见,再结束本次流程;通过、拒绝均关闭本次待办。
ClubUserApplyToDoHandler.COMPLETE_CLUB_TASK.exec(clubUser);
ClubUserApplyToDoHandler.COMPLETE_PROCESS.exec(clubUser);
Sys_club club = dao().fetch(Sys_club.class, clubUser.getClubid());
Sql sql = Sqls.fetchRecord("select username,loginname from sys_user where id = @id").setParam("id", clubUser.getUserid());
dao().execute(sql);
Record userRecord = (Record) sql.getResult();
if (flag) {
String content = "您申请加入的" + club.getName() + "已经审批通过。";
System.out.println(userRecord.getString("username"));
System.out.println(userRecord.getString("loginname"));
System.out.println(content);
} else {
String content = "您申请加入的" + club.getName() + "被审批拒绝了,拒绝理由:" + auditOpinion;
System.out.println(userRecord.getString("username"));
System.out.println(content);
}
}
}
} }
@@ -5,6 +5,9 @@ import io.v.nutz.sys.models.Sys_local_process_instance;
import io.v.nutz.sys.models.Sys_local_process_instance_task; import io.v.nutz.sys.models.Sys_local_process_instance_task;
import io.v.nutz.sys.services.SysLocalProcessService; import io.v.nutz.sys.services.SysLocalProcessService;
import io.v.nutz.web.commons.base.Globals; import io.v.nutz.web.commons.base.Globals;
import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.base.utils.Roles;
import org.nutz.lang.util.NutMap;
import io.v.nutz.zhgh.needhandlebycpu.service.HandleCpuCommonService; import io.v.nutz.zhgh.needhandlebycpu.service.HandleCpuCommonService;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -34,6 +37,57 @@ public class SysLocalProcessServiceImpl implements SysLocalProcessService {
@Inject @Inject
private HandleCpuCommonService handleCpuCommonService; private HandleCpuCommonService handleCpuCommonService;
/**
* 只对协会入会任务补充业务有效性过滤,其他模块保持原来的接收人及任务状态规则。
* 协会已办按 actualOwnerId 归属,避免把其他接收人或超管代审算成本人办理。
*/
@Override
public List<NutMap> getTaskList(Integer mode) {
if (!Integer.valueOf(1).equals(mode) && !Integer.valueOf(2).equals(mode)) return List.of();
Sql sql = Sqls.create("""
SELECT t.taskNodeName, t.createdByUserName, t.formUrl, t.formUrlView, t.createdOn,
t2.processName, t2.nodeName
FROM sys_local_process_instance_task t
LEFT JOIN sys_local_process_instance t2 ON t2.processUniqueId = t.processUniqueId
WHERE t.status = @mode AND t.taskDeleteFlag = 0
AND (
(NOT (t.taskUniqueId = 'clubAuditId' AND LEFT(t.processUniqueId, 14) = 'sys_club_user@')
AND JSON_CONTAINS(t.assignments, JSON_QUOTE(@loginName)))
OR
(t.taskUniqueId = 'clubAuditId' AND LEFT(t.processUniqueId, 14) = 'sys_club_user@'
AND @clubPermission = 1 AND t.delFlag = 0
AND t2.processDeleteFlag = 0 AND t2.delFlag = 0
AND (
(@mode = 2 AND t.actualOwnerId = @userId)
OR
(@mode = 1 AND t2.processInstanceStatus = 1
AND JSON_CONTAINS(t.assignments, JSON_QUOTE(@loginName))
AND EXISTS (
SELECT 1 FROM sys_club_user cu
WHERE t.processUniqueId = CONCAT('sys_club_user@', cu.id)
AND cu.status = 1 AND cu.sf = 2 AND cu.isNormal = 1
AND EXISTS (
SELECT 1 FROM sys_user_role sur
WHERE sur.userId = @userId AND sur.stid = cu.clubid
AND sur.roleId IN (@chairman, @secretary)
)
))
))
)
ORDER BY t2.processInitiationTime DESC
""");
// 工号通过 JSON_QUOTE 和参数绑定处理,避免特殊字符破坏接收人匹配。
sql.setParam("mode", mode);
sql.setParam("loginName", ShiroUtil.getPrincipalProperty("loginname"));
sql.setParam("userId", ShiroUtil.getPlatformUid());
sql.setParam("clubPermission", ShiroUtil.hasPermission("sys.club.sh") ? 1 : 0);
sql.setParam("chairman", Roles.club01);
sql.setParam("secretary", Roles.club03);
sql.setCallback(Sqls.callback.maps());
dao.execute(sql);
return sql.getList(NutMap.class);
}
@Override @Override
public void startProcess(String processName, String processUniqueId, String nodeName, String processInitiatorId, String pcUrl, String mobileUrl) { public void startProcess(String processName, String processUniqueId, String nodeName, String processInitiatorId, String pcUrl, String mobileUrl) {
int count = dao.count(Sys_local_process_instance.class, Cnd.where("processUniqueId", "=", processUniqueId)); int count = dao.count(Sys_local_process_instance.class, Cnd.where("processUniqueId", "=", processUniqueId));
@@ -2,16 +2,24 @@ package io.v.nutz.web.commons.controller;
import cn.hutool.core.codec.Base64; import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.wizzer.framework.base.Result; import cn.wizzer.framework.base.Result;
import com.aliyun.core.utils.Base64Util;
import io.v.nutz.sys.models.Sys_file; import io.v.nutz.sys.models.Sys_file;
import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.zhgh.zgfw.model.UserSign;
import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.boot.starter.ftp.FtpService; import org.nutz.boot.starter.ftp.FtpService;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.integration.jedis.RedisService; import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.Ioc;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.random.R; import org.nutz.lang.random.R;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
@@ -19,6 +27,7 @@ import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.util.List;
/** /**
* TODO * TODO
@@ -30,15 +39,23 @@ import java.io.InputStream;
@At("/signature") @At("/signature")
public class SignatureController { public class SignatureController {
@Inject
private Dao dao;
@Inject @Inject
private RedisService redisService; private RedisService redisService;
// 服务由本类工厂注册,按需获取避免控制器与工厂产物形成循环注入。
@Inject("refer:$ioc")
private Ioc ioc;
@Inject @Inject
private FtpService ftpService; private FtpService ftpService;
/** 当前 Nutz 不扫描内部类,通过同文件的工厂方法注册个人签名服务及其依赖。 */
@IocBean(name = "personalSignatureService")
public PersonalSignatureServiceImpl createPersonalSignatureService(@Inject("dao") Dao dao,
@Inject("ftpService") FtpService ftp, @Inject("redisService") RedisService redis) {
return new PersonalSignatureServiceImpl(dao, ftp, redis);
}
@At("") @At("")
@Ok("re") @Ok("re")
@RequiresAuthentication @RequiresAuthentication
@@ -47,48 +64,21 @@ public class SignatureController {
} }
/** /**
* 移动端提交签名数据存到redis里 * prefix/id 为二维码对应的轮询标识;base64 兼容手写图片数据及当前用户已保存的文件路径。
* * is_value_base64 默认 true,false 时返回文件路径给 PC;返回 {code,msg,data},data 为已保存签名。
* @param prefix 前缀
* @param id 唯一id
* @param base64
* @return
*/ */
@At @At
@Ok("json") @Ok("json")
@RequiresAuthentication @RequiresAuthentication
@Aop(TransAop.READ_COMMITTED)
public Object doSub(String prefix, String id, String base64, Boolean is_value_base64) { public Object doSub(String prefix, String id, String base64, Boolean is_value_base64) {
if (is_value_base64) { if (Strings.isBlank(prefix) || Strings.isBlank(id) || Strings.isBlank(base64)) {
redisService.setex(prefix + ":" + id, 60 * 10, base64); return Result.error("缺少签字参数,请重新扫码");
} else {
String today = DateUtil.today();
String[] split = today.split("-");
String todayPath = "/signature/" + split[0] + "/" + split[1] + "/" + split[2];
String fileName = R.UU32() + ".png";
String fullPath = todayPath + "/" + fileName;
String replaceData = base64.replaceAll("data:image/png;base64,", "");
try (InputStream is = new ByteArrayInputStream(Base64.decode(replaceData))) {
boolean uploadRes = ftpService.upload(todayPath, fileName, is);
if (uploadRes) {
//上传成功记录到文件表
Sys_file sysFile = new Sys_file();
sysFile.setFilename(fileName);
sysFile.setFilepath(fullPath);
sysFile.setSource(null);
dao.insert(sysFile);
redisService.setex(prefix + ":" + id, 60 * 10, fullPath);
} else {
throw new RuntimeException("上传文件失败");
}
} catch (Exception e) {
return Result.error("签字数据上传失败");
}
} }
String sign = ioc.get(PersonalSignatureService.class, "personalSignatureService")
.submitScan(ShiroUtil.getPrincipalProperty("id").toString(),
return Result.success(); prefix, id, base64, !Boolean.FALSE.equals(is_value_base64));
return Result.success().addData(sign);
} }
@@ -108,40 +98,15 @@ public class SignatureController {
/** /**
* 签字base64转图片链接 * data 为 data:image/...;base64,... 图片字符串;返回 {code,msg,data},data 是上传后的文件路径。
*
* @param data
* @return
*/ */
@At @At
@Ok("json") @Ok("json")
@RequiresAuthentication @RequiresAuthentication
@Aop(TransAop.READ_COMMITTED)
public Result uploadSignData(String data) { public Result uploadSignData(String data) {
String today = DateUtil.today(); if (Strings.isBlank(data)) return Result.error("请先签字");
String[] split = today.split("-"); return Result.success().addData(ioc.get(PersonalSignatureService.class, "personalSignatureService").uploadImage(data));
String todayPath = "/signature/" + split[0] + "/" + split[1] + "/" + split[2];
String fileName = R.UU32() + ".png";
String fullPath = todayPath + "/" + fileName;
String replaceData = data.replaceAll("data:image/png;base64,", "");
try (InputStream is = new ByteArrayInputStream(Base64.decode(replaceData))) {
boolean uploadRes = ftpService.upload(todayPath, fileName, is);
if (uploadRes) {
//上传成功记录到文件表
Sys_file sysFile = new Sys_file();
sysFile.setFilename(fileName);
sysFile.setFilepath(fullPath);
sysFile.setSource(null);
dao.insert(sysFile);
} else {
throw new RuntimeException("上传文件失败");
}
} catch (Exception e) {
return Result.error("签字数据上传失败");
}
return Result.success().addData(fullPath);
} }
@@ -158,4 +123,128 @@ public class SignatureController {
} }
} }
/** 个人签名服务与控制器同文件定义,查询、上传和按类型保存由独立 IoC 服务承接。 */
public interface PersonalSignatureService {
/** userId 为登录用户ID;base64 为签名类型;返回同类型签名原值,未找到返回 null。 */
String findPersonal(String userId, boolean base64);
/** sign 为手写 Base64 或本人已有文件路径;按 base64 类型保存,返回实际保存值。 */
String savePersonal(String userId, String sign, boolean base64);
/** data 为带图片类型头的 Base64;上传并记录文件信息,返回文件路径。 */
String uploadImage(String data);
/** prefix/id 对应 PC 轮询;保存本人签名后发布结果,返回与 base64 参数一致的数据格式。 */
String submitScan(String userId, String prefix, String id, String sign, boolean base64);
}
public static class PersonalSignatureServiceImpl implements PersonalSignatureService {
private Dao dao;
private FtpService ftpService;
private RedisService redisService;
public PersonalSignatureServiceImpl(Dao dao, FtpService ftpService, RedisService redisService) {
this.dao = dao;
this.ftpService = ftpService;
this.redisService = redisService;
}
/** 只把有明确图片数据头的内容归为 Base64,空值和异常内容不冒充文件签名。 */
private boolean isBase64(String sign) {
return sign != null && sign.matches("^data:image/(png|jpeg|jpg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$");
}
/** 文件模式使用站内存储路径;排除 URL、路径穿越和图片数据字符串。 */
private boolean isFile(String sign) {
return sign != null && sign.startsWith("/") && !sign.startsWith("//")
&& !sign.contains("..") && !sign.contains("?") && !sign.contains("#")
&& !sign.contains("\\") && !sign.contains("\n") && !sign.contains("\r");
}
/** 同类型历史多条按 ID 固定选择,优先公共记录;更新时保留该记录原有 prefix,不批量覆盖。 */
private UserSign findRecord(String userId, boolean base64) {
if (Strings.isBlank(userId)) throw new IllegalArgumentException("请先登录");
List<UserSign> records = dao.query(UserSign.class, Cnd.where("user_id", "=", userId).asc("id"));
UserSign first = null;
for (UserSign record : records) {
if (!(base64 ? isBase64(record.getData()) : isFile(record.getData()))) continue;
if (Strings.isBlank(record.getPrefix())) return record;
if (first == null) first = record;
}
return first;
}
@Override
public String findPersonal(String userId, boolean base64) {
UserSign record = findRecord(userId, base64);
return record == null ? null : record.getData();
}
@Override
@Aop(TransAop.READ_COMMITTED)
public String savePersonal(String userId, String sign, boolean base64) {
if (Strings.isBlank(userId) || Strings.isBlank(sign)) throw new IllegalArgumentException("请先登录并签字");
// 锁定已有用户行,使同一用户的首次并发保存串行执行,无需新增类型字段或唯一索引。
Sql lock = Sqls.create("SELECT id FROM sys_user WHERE id=@id FOR UPDATE").setParam("id", userId);
lock.setCallback(Sqls.callback.map());
dao.execute(lock);
if (lock.getResult() == null) throw new IllegalArgumentException("当前用户不存在");
String saved;
if (base64) {
if (!isBase64(sign)) throw new IllegalArgumentException("当前页面需要 Base64 签名,请重新签字");
saved = sign;
} else if (isBase64(sign)) {
// 这里只上传本次新手写的图片;查询个人签名时不转换另一种类型的历史记录。
saved = uploadImage(sign);
} else {
if (!isFile(sign) || dao.fetch(UserSign.class, Cnd.where("user_id", "=", userId)
.and("data", "=", sign)) == null) {
throw new IllegalArgumentException("请使用本人的文件签名或重新签字");
}
saved = sign;
}
UserSign record = findRecord(userId, base64);
if (record == null) {
dao.insert(new UserSign(userId, saved));
} else if (!saved.equals(record.getData())) {
dao.update(UserSign.class, Chain.make("data", saved), Cnd.where("id", "=", record.getId())
.and("user_id", "=", userId));
}
return saved;
}
@Override
@Aop(TransAop.READ_COMMITTED)
public String uploadImage(String data) {
if (!isBase64(data)) throw new IllegalArgumentException("签字图片格式不正确");
String type = data.substring("data:image/".length(), data.indexOf(';'));
String directory = "/signature/" + DateUtil.format(new java.util.Date(), "yyyy/MM/dd");
String filename = R.UU32() + "." + ("jpeg".equals(type) ? "jpg" : type);
String path = directory + "/" + filename;
byte[] bytes = Base64.decode(data.substring(data.indexOf(',') + 1));
try (InputStream stream = new ByteArrayInputStream(bytes)) {
if (!ftpService.upload(directory, filename, stream)) throw new IllegalStateException("签字图片上传失败");
} catch (Exception e) {
throw new IllegalStateException("签字图片上传失败,请重试", e);
}
Sys_file file = new Sys_file();
file.setFilename(filename);
file.setFilepath(path);
file.setSource(null);
dao.insert(file);
return path;
}
@Override
@Aop(TransAop.READ_COMMITTED)
public String submitScan(String userId, String prefix, String id, String sign, boolean base64) {
if (Strings.isBlank(prefix) || Strings.isBlank(id)) throw new IllegalArgumentException("请重新扫码");
// 新版组件已保存个人签名,文件路径直接复用;旧扫码客户端仍可提交原始手写图片。
String saved = savePersonal(userId, sign, base64);
redisService.setex(prefix + ":" + id, 60 * 10, saved);
return saved;
}
}
} }
@@ -20,6 +20,7 @@ import io.v.nutz.zhgh.data.model.UserSource;
import io.v.nutz.zhgh.data.service.HistoryUserService; import io.v.nutz.zhgh.data.service.HistoryUserService;
import io.v.nutz.zhgh.data.service.SourceUserService; import io.v.nutz.zhgh.data.service.SourceUserService;
import io.v.nutz.base.lang.Enum; import io.v.nutz.base.lang.Enum;
import io.v.nutz.zhgh.member.MemberMode;
import io.v.nutz.zhgh.member.UserMode; import io.v.nutz.zhgh.member.UserMode;
import io.v.nutz.base.service.AsyncService; import io.v.nutz.base.service.AsyncService;
import io.v.nutz.base.service.impl.ViServiceImpl; import io.v.nutz.base.service.impl.ViServiceImpl;
@@ -67,6 +68,9 @@ import java.util.stream.Collectors;
@CacheDefaults(cacheName = "user_source") @CacheDefaults(cacheName = "user_source")
@Slf4j @Slf4j
public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements SourceUserService { public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements SourceUserService {
private static final String CONTRACT_PERSON_TYPE = "合同制";
private static final String NON_CONTRACT_PERSON_TYPE = "非合同制";
public SourceUserServiceImpl(Dao dao) { public SourceUserServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@@ -96,9 +100,25 @@ public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements
public void pullSourceData(Boolean isIncrement) { public void pullSourceData(Boolean isIncrement) {
Date now = new Date(); Date now = new Date();
List<UserSource> users = SourceData.users(); List<UserSource> users = SourceData.users();
// 正式用户已明确标记为非合同制时,不能因数据中心人员类型为空而被改为合同制或自动入会。
Set<String> nonContractLoginNames = sysUserService.query(Cnd.where("personType", "=", NON_CONTRACT_PERSON_TYPE))
.stream()
.map(Sys_user::getLoginname)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
asyncService.exe2(users, (user) -> { asyncService.exe2(users, (user) -> {
user.setPullTime(now); user.setPullTime(now);
UserMode.initUser(user); UserMode.initUser(user);
// 数据中心人员类型为空且工号以 18 开头时,仅非“非合同制”人员按合同制会员处理。
if (isContractMemberCandidate(user)) {
if (nonContractLoginNames.contains(user.getLoginname())) {
// 使用 null 避免后续 updateIgnoreNull 将正式用户已有的“非合同制”覆盖为空字符串。
user.setPersonType(null);
} else {
user.setPersonType(CONTRACT_PERSON_TYPE);
user.setMember(MemberMode.NORMAL.getCode());
}
}
}); });
dao().insert(users); dao().insert(users);
} }
@@ -149,6 +169,8 @@ public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements
List<Sys_user> unitChangeUserList = new CopyOnWriteArrayList<>(); List<Sys_user> unitChangeUserList = new CopyOnWriteArrayList<>();
// 删除博士后出站的会员和福利会员角色 // 删除博士后出站的会员和福利会员角色
List<String> deleteLeavingStationUserIdList = new CopyOnWriteArrayList<>(); List<String> deleteLeavingStationUserIdList = new CopyOnWriteArrayList<>();
// 拉取时识别出的合同制人员,在正式用户保存后统一补充会员字段及会员角色。
List<String> addContractMemberUserIdList = new CopyOnWriteArrayList<>();
// 创建一个固定大小的线程池 可以根据服务器性能调整线程池大小 // 创建一个固定大小的线程池 可以根据服务器性能调整线程池大小
@@ -171,9 +193,11 @@ public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements
continue; continue;
} }
boolean shouldAddContractMember = isPulledContractMember(source);
source.setMember(null); source.setMember(null);
source.setWelfareMember(null); source.setWelfareMember(null);
Sys_user user = userMap.get(source.getLoginname()); Sys_user user = userMap.get(source.getLoginname());
Sys_user systemUser = user;
UserChangeType changeType = getChangeType(source, user); UserChangeType changeType = getChangeType(source, user);
@@ -214,12 +238,14 @@ public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements
unitChangeUserList.add(u); unitChangeUserList.add(u);
} }
boolean includedInUpdate = user == null;
switch (sourceType) { switch (sourceType) {
case "all" -> { case "all" -> {
//复制属性到一个新的user对象 //复制属性到一个新的user对象
u.setId(user == null ? source.getId() : user.getId()); u.setId(user == null ? source.getId() : user.getId());
if (user != null) { if (user != null) {
needDoUpdateList.add(u); needDoUpdateList.add(u);
includedInUpdate = true;
} }
} }
case "add" -> { case "add" -> {
@@ -234,6 +260,7 @@ public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements
} }
if (Strings.isNotBlank(finalUserPartMap.get(u.getLoginname())) && user != null) { if (Strings.isNotBlank(finalUserPartMap.get(u.getLoginname())) && user != null) {
needDoUpdateList.add(u); needDoUpdateList.add(u);
includedInUpdate = true;
} }
} }
} }
@@ -256,6 +283,12 @@ public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements
histories.add(createUserHistory(user, changeTypes)); histories.add(createUserHistory(user, changeTypes));
} }
// 仅对本次实际新增或更新、且尚未入会的人员执行会员初始化,避免重复覆盖原入会时间。
if (shouldAddContractMember && includedInUpdate
&& !ObjectUtil.equal(systemUser != null ? systemUser.getMember() : null, MemberMode.NORMAL.getCode())) {
addContractMemberUserIdList.add(systemUser == null ? u.getId() : systemUser.getId());
}
} }
}, Executors.newFixedThreadPool(numberOfThreads)); }, Executors.newFixedThreadPool(numberOfThreads));
futures.add(future); futures.add(future);
@@ -277,6 +310,10 @@ public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements
massUpdatesAsync(needDoUpdateList, false, null); massUpdatesAsync(needDoUpdateList, false, null);
} }
if (Lang.isNotEmpty(addContractMemberUserIdList)) {
UserMode.addMemberRole(addContractMemberUserIdList.toArray(new String[0]));
}
if (Lang.isNotEmpty(deleteLeavingStationUserIdList)) { if (Lang.isNotEmpty(deleteLeavingStationUserIdList)) {
dao().clear(Sys_user_role.class, Cnd.where("userId", "in", deleteLeavingStationUserIdList).and("roleId", "=", Roles.MEMBER)); dao().clear(Sys_user_role.class, Cnd.where("userId", "in", deleteLeavingStationUserIdList).and("roleId", "=", Roles.MEMBER));
dao().update(Sys_user.class, Chain.make("member", 0).add("welfareMember", 0), Cnd.where("id", "in", deleteLeavingStationUserIdList)); dao().update(Sys_user.class, Chain.make("member", 0).add("welfareMember", 0), Cnd.where("id", "in", deleteLeavingStationUserIdList));
@@ -377,6 +414,33 @@ public class SourceUserServiceImpl extends ViServiceImpl<UserSource> implements
dao().clear(UserSource.class, Cnd.where("DATE(pullTime)", "<", formatAgoWeekDate)); dao().clear(UserSource.class, Cnd.where("DATE(pullTime)", "<", formatAgoWeekDate));
} }
/**
* 判断数据中心人员是否需要按合同制会员初始化。
*
* @param user 数据中心返回的人员;工号需以 18 开头且人员类型为空
* @return true 表示需要补充“合同制”人员类型并设置为会员
*/
private boolean isContractMemberCandidate(Sys_user user) {
return user != null
&& user.getLoginname() != null
&& user.getLoginname().startsWith("18")
&& StrUtil.isBlank(user.getPersonType());
}
/**
* 判断暂存人员是否为拉取阶段标记的合同制会员。
*
* @param source 人员拉取暂存数据
* @return true 表示人员类型及会员标记均符合自动入会条件
*/
private boolean isPulledContractMember(UserSource source) {
return source != null
&& source.getLoginname() != null
&& source.getLoginname().startsWith("18")
&& CONTRACT_PERSON_TYPE.equals(source.getPersonType())
&& ObjectUtil.equal(source.getMember(), MemberMode.NORMAL.getCode());
}
private UserChangeType getChangeType(Sys_user source, Sys_user user) { private UserChangeType getChangeType(Sys_user source, Sys_user user) {
//新入职 //新入职
if (user == null) { if (user == null) {
@@ -1,74 +1,50 @@
package io.v.nutz.zhgh.jf.controller.activitybx; package io.v.nutz.zhgh.jf.controller.activitybx;
import io.v.nutz.base.annontation.ViReturn; 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 io.v.nutz.zhgh.jf.service.ActivityBxService;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop; 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.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; 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.Strings;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import org.nutz.json.Json;
import java.util.List; /** 校工会负责人审核:PC 与手机共用业务接口,审核逻辑统一在 Service 中处理。 */
/**
* @author zhf
* @date 2020/11/14 14:55
* @description 校工会会计核报
*/
@IocBean @IocBean
@Ok("json") @Ok("json")
@At("/platform/jf/activity_bx/accounting") @At("/platform/jf/activity_bx/accounting")
public class AccountingCon { public class AccountingCon {
@Inject
private Vi vi;
@Inject @Inject
private ActivityBxService activityBxService; private ActivityBxService activityBxService;
@Inject
private BaseService baseService;
@Inject
private MsgApi msgApi;
@Inject
private SysSignatureService sysSignatureService;
@At("") @At("")
@Ok("beetl:/platform/jf/activitybx/accounting.html") @Ok("beetl:/platform/jf/activitybx/accounting.html")
@RequiresPermissions("sys.jf.activity_bx.accounting") @RequiresPermissions("sys.jf.activity_bx.accounting")
public void index() { 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 @At
@ViReturn @ViReturn
@RequiresPermissions("sys.jf.activity_bx.accounting")
public Object pageData(@Param(value = "year", required = false) Integer year, public Object pageData(@Param(value = "year", required = false) Integer year,
@Param(value = "isAudit", required = false) Integer isAudit, @Param(value = "isAudit", required = false) Integer isAudit,
@Param(value = "unitId", required = false) String unitId, @Param(value = "unitId", required = false) String unitId,
@@ -78,89 +54,44 @@ public class AccountingCon {
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize, @Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize,
@Param(value = "pageOrderName", required = false) String pageOrderName, @Param(value = "pageOrderName", required = false) String pageOrderName,
@Param(value = "pageOrderBy", required = false) String pageOrderBy) { @Param(value = "pageOrderBy", required = false) String pageOrderBy) {
Sql sql = Sqls.create(""" return activityBxService.reviewPage("accounting", new NutMap().addv("year", year).addv("isAudit", isAudit)
SELECT .addv("unitId", unitId).addv("unionId", unionId).addv("searchName", searchName)
act.*, .addv("searchKeyword", searchKeyword).addv("pageNumber", pageNumber).addv("pageSize", pageSize)
u.loginname bxr_loginname, .addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy));
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);
} }
/** id 为申请主键;返回详情 Map,canReview 表示该申请是否仍处于当前待审节点。 */
@At @At
@ViReturn @ViReturn
public Object getGoods(String activity_id) { @RequiresPermissions("sys.jf.activity_bx.accounting")
return baseService.dao().query(Goods.class, Cnd.where("reid", "=", activity_id)); 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 @At
@ViReturn @ViReturn
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Object doReview(String data) { @RequiresPermissions("sys.jf.activity_bx.accounting")
NutMap nutMap = Json.fromJson(NutMap.class, data); public Object doReview(@Param("data") String data) {
ActivityBx aid = activityBxService.fetch(nutMap.getString("id")); if (Strings.isBlank(data)) throw new IllegalArgumentException("请填写审核信息");
aid.setState_id(nutMap.getBoolean("flag") ? "1040" : "1025"); NutMap form = Json.fromJson(NutMap.class, data);
aid.setAccounting_name(ShiroUtil.getPrincipalProperty("id").toString()); if (form == null) throw new IllegalArgumentException("审核参数格式错误");
aid.setAccounting_time(DateUtil.getDate()); activityBxService.reviewActivity("accounting", form);
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);
return null; 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);
}
} }
@@ -1,21 +1,9 @@
package io.v.nutz.zhgh.jf.controller.activitybx; package io.v.nutz.zhgh.jf.controller.activitybx;
import io.v.nutz.base.annontation.ViReturn; 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 io.v.nutz.zhgh.jf.service.ActivityBxService;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop; 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.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; 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.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import java.util.List; /** 协会活动申报审核:PC、手机共用查询和审核入口。 */
/**
* @author zhf
* @date 2020/12/6 15:23
* @description 协会负责人审核
*/
@IocBean @IocBean
@Ok("json") @Ok("json")
@At("/platform/jf/activity_bx/association") @At("/platform/jf/activity_bx/association")
@RequiresPermissions("sys.jf.activity_bx.association")
public class AssociationCon { public class AssociationCon {
@Inject
private Vi vi;
@Inject @Inject
private ActivityBxService activityBxService; private ActivityBxService activityBxService;
@Inject /** PC 审核页面,沿用原访问地址。 */
private SysSignatureService sysSignatureService;
@Inject
private MsgApi msgApi;
@At("") @At("")
@Ok("beetl:/platform/jf/activitybx/association.html") @Ok("beetl:/platform/jf/activitybx/association.html")
@RequiresPermissions("sys.jf.activity_bx.association")
public void index() { 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 @At
@ViReturn @ViReturn
public Object pageData(@Param(value = "year", required = false) Integer year, public Object pageData(@Param(value = "year", required = false) Integer year,
@Param(value = "isAudit", required = false) Integer isAudit, @Param(value = "isAudit", required = false) Integer isAudit,
@Param(value = "clubId", required = false) String clubId,
@Param(value = "unitId", required = false) String unitId, @Param(value = "unitId", required = false) String unitId,
@Param(value = "unionId", required = false) String unionId, @Param(value = "unionId", required = false) String unionId,
@Param(value = "searchName", required = false) String searchName, @Param(value = "searchName", required = false) String searchName,
@@ -67,82 +51,46 @@ public class AssociationCon {
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize, @Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize,
@Param(value = "pageOrderName", required = false) String pageOrderName, @Param(value = "pageOrderName", required = false) String pageOrderName,
@Param(value = "pageOrderBy", required = false) String pageOrderBy) { @Param(value = "pageOrderBy", required = false) String pageOrderBy) {
Sql sql = Sqls.create(""" return activityBxService.associationPage(new NutMap().addv("year", year).addv("isAudit", isAudit)
SELECT .addv("clubId", clubId).addv("unitId", unitId).addv("unionId", unionId)
act.*, .addv("searchName", searchName).addv("searchKeyword", searchKeyword)
u.loginname bxr_loginname, .addv("pageNumber", pageNumber).addv("pageSize", pageSize)
u.unionname bxr_unionname, .addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy));
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);
} }
/** 无参数;返回当前用户可管理的协会列表,每项 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 @At
@ViReturn @ViReturn
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Object doReview(@Param(value = "id", required = false) String id, public Object doReview(@Param("id") String id, @Param("idea") String idea,
@Param(value = "idea", required = false) String idea, @Param("sign") String sign, @Param("flag") Boolean flag) {
@Param(value = "sign", required = false) String sign, if (Strings.isBlank(id) || flag == null) {
@Param(value = "flag", required = false) Boolean flag) { throw new IllegalArgumentException("请选择活动申请和审核结果");
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());
} }
activityBxService.update(aid); if (Strings.isBlank(idea) || idea.trim().length() > 500 || Strings.isBlank(sign)) {
throw new IllegalArgumentException("请填写1至500字审核意见并签字");
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);
} }
FundsToHandler.COMPLETE_CLUB_TASK.exec(id, new NutMap().addv("option", nodeName), null); activityBxService.reviewAssociation(id, idea.trim(), sign, flag);
return null; return null;
} }
} }
@@ -1,67 +1,49 @@
package io.v.nutz.zhgh.jf.controller.activitybx; package io.v.nutz.zhgh.jf.controller.activitybx;
import io.v.nutz.base.annontation.ViReturn; 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 io.v.nutz.zhgh.jf.service.ActivityBxService;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop; 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.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings; import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import java.util.List; /** 基层工会主席审核:PC 与手机共用业务接口,审核逻辑统一在 Service 中处理。 */
/**
* @author zhf
* @date 2020/11/14 10:17
* @description 基层工会核批
*/
@IocBean @IocBean
@Ok("json") @Ok("json")
@At("/platform/jf/activity_bx/division") @At("/platform/jf/activity_bx/division")
public class DivisionCon { public class DivisionCon {
@Inject
private Vi vi;
@Inject
private MsgApi msgApi;
@Inject @Inject
private ActivityBxService activityBxService; private ActivityBxService activityBxService;
@Inject
private SysSignatureService sysSignatureService;
@At("") @At("")
@Ok("beetl:/platform/jf/activitybx/division.html") @Ok("beetl:/platform/jf/activitybx/division.html")
@RequiresPermissions("sys.jf.activity_bx.division") @RequiresPermissions("sys.jf.activity_bx.division")
public void index() { 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 @At
@ViReturn @ViReturn
@RequiresPermissions("sys.jf.activity_bx.division")
public Object pageData(@Param(value = "year", required = false) Integer year, public Object pageData(@Param(value = "year", required = false) Integer year,
@Param(value = "isAudit", required = false) Integer isAudit, @Param(value = "isAudit", required = false) Integer isAudit,
@Param(value = "unitId", required = false) String unitId, @Param(value = "unitId", required = false) String unitId,
@@ -71,87 +53,31 @@ public class DivisionCon {
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize, @Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize,
@Param(value = "pageOrderName", required = false) String pageOrderName, @Param(value = "pageOrderName", required = false) String pageOrderName,
@Param(value = "pageOrderBy", required = false) String pageOrderBy) { @Param(value = "pageOrderBy", required = false) String pageOrderBy) {
Sql sql = Sqls.create(""" return activityBxService.reviewPage("division", new NutMap().addv("year", year).addv("isAudit", isAudit)
SELECT .addv("unitId", unitId).addv("unionId", unionId).addv("searchName", searchName)
act.*, .addv("searchKeyword", searchKeyword).addv("pageNumber", pageNumber).addv("pageSize", pageSize)
u.loginname bxr_loginname, .addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy));
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);
} }
/** 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 @At
@ViReturn @ViReturn
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Object doReview(@Param(value = "id", required = false) String id, @RequiresPermissions("sys.jf.activity_bx.division")
@Param(value = "idea", required = false) String idea, public Object doReview(@Param("id") String id, @Param("idea") String idea,
@Param(value = "sign", required = false) String sign, @Param("sign") String sign, @Param("flag") Boolean flag) {
@Param(value = "flag", required = false) Boolean flag) { if (Strings.isBlank(id) || flag == null) throw new IllegalArgumentException("请选择申请和审核结果");
ActivityBx aid = activityBxService.fetch(id); activityBxService.reviewActivity("division", new NutMap().addv("id", id).addv("idea", idea)
aid.setState_id(flag ? "1020" : "1015"); .addv("sign", sign).addv("flag", flag));
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);
return null; return null;
} }
@@ -1,70 +1,50 @@
package io.v.nutz.zhgh.jf.controller.activitybx; package io.v.nutz.zhgh.jf.controller.activitybx;
import io.v.nutz.base.annontation.ViReturn; 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 io.v.nutz.zhgh.jf.service.ActivityBxService;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop; 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.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Strings; import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import org.nutz.json.Json;
import java.util.List; /** 校工会主席审核:PC 与手机共用业务接口,审核逻辑统一在 Service 中处理。 */
/**
* @author zhf
* @date 2020/11/14 16:03
* @description 校工会主席审核
*/
@IocBean @IocBean
@Ok("json") @Ok("json")
@At("/platform/jf/activity_bx/union") @At("/platform/jf/activity_bx/union")
public class UnionCon { public class UnionCon {
@Inject
private Vi vi;
@Inject
private MsgApi msgApi;
@Inject @Inject
private ActivityBxService activityBxService; private ActivityBxService activityBxService;
@Inject
private SysSignatureService sysSignatureService;
@At("") @At("")
@Ok("beetl:/platform/jf/activitybx/union.html") @Ok("beetl:/platform/jf/activitybx/union.html")
@RequiresPermissions("sys.jf.activity_bx.union") @RequiresPermissions("sys.jf.activity_bx.union")
public void index() { 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 @At
@ViReturn @ViReturn
@RequiresPermissions("sys.jf.activity_bx.union")
public Object pageData(@Param(value = "year", required = false) Integer year, public Object pageData(@Param(value = "year", required = false) Integer year,
@Param(value = "isAudit", required = false) Integer isAudit, @Param(value = "isAudit", required = false) Integer isAudit,
@Param(value = "unitId", required = false) String unitId, @Param(value = "unitId", required = false) String unitId,
@@ -74,129 +54,35 @@ public class UnionCon {
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize, @Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize,
@Param(value = "pageOrderName", required = false) String pageOrderName, @Param(value = "pageOrderName", required = false) String pageOrderName,
@Param(value = "pageOrderBy", required = false) String pageOrderBy) { @Param(value = "pageOrderBy", required = false) String pageOrderBy) {
Sql sql = Sqls.create(""" return activityBxService.reviewPage("union", new NutMap().addv("year", year).addv("isAudit", isAudit)
SELECT .addv("unitId", unitId).addv("unionId", unionId).addv("searchName", searchName)
act.*, .addv("searchKeyword", searchKeyword).addv("pageNumber", pageNumber).addv("pageSize", pageSize)
u.loginname bxr_loginname, .addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy));
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);
} }
/** 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; * data 为 JSON 对象字符串:id 为申请ID,idea 为1至500字意见,sign 为图片数据或签名路径,flag 为布尔值(true通过/false拒绝)。
* activity_money 为可选的非负实际费用,不提交时保留原费用。
@Inject * 返回统一结果对象,code=0 为成功,msg 为提示;失败不推进审核状态。
private io.v.nutz.zhgh.jf.service.jfYjghService jfYjghService; */
@Inject
private ActivityTissueService activityTissueService;
@Inject
private io.v.nutz.zhgh.jf.service.jfClubService jfClubService;
@At @At
@ViReturn @ViReturn
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Object doReview(String data) { @RequiresPermissions("sys.jf.activity_bx.union")
NutMap nutMap = Json.fromJson(NutMap.class, data); public Object doReview(@Param("data") String data) {
if (Strings.isBlank(data)) throw new IllegalArgumentException("请填写审核信息");
ActivityBx aid = activityBxService.fetch(nutMap.getString("id")); NutMap form = Json.fromJson(NutMap.class, data);
aid.setState_id(nutMap.getBoolean("flag") ? "1060" : "1045"); if (form == null) throw new IllegalArgumentException("审核参数格式错误");
aid.setUnion_chairman_name(ShiroUtil.getPrincipalProperty("id").toString()); activityBxService.reviewActivity("union", form);
aid.setUnion_chairman_time(DateUtil.getDate());
aid.setUnion_chairman_idea(nutMap.getString("idea"));
aid.setActivity_money(nutMap.getDouble("activity_money"));
List<Goods> 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);
}*/
return null; return null;
} }
@@ -1,64 +1,47 @@
package io.v.nutz.zhgh.jf.controller.reimbursement; 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.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.model.Audit;
import io.v.nutz.base.query.PageForm; 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 io.v.nutz.web.commons.slog.annotation.SLog;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop; 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.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import java.util.Date; /** 活动报销:协会负责人审核,PC 与手机共用列表、数据范围和审核服务。 */
/**
* @version 1.0
* @Author zzr
* @name:ActivityReimbursementClubChairmanController
* @Date 2024/4/26 14:58
* @注释 活动报销,协会负责人审核
*/
@IocBean @IocBean
@Ok("json:full") @Ok("json:full")
@At("/platform/jf/reimbursement/clubChairman") @At("/platform/jf/reimbursement/clubChairman")
public class ActivityReimbursementClubChairmanController { public class ActivityReimbursementClubChairmanController {
@Inject
private Vi vi;
@Inject @Inject
private ActivityBxService activityBxService; private ActivityBxService activityBxService;
/** 保留现有 PC 页面入口。 */
@At("") @At("")
@Ok("beetl:/platform/jf/reimbursement/clubChairman.html") @Ok("beetl:/platform/jf/reimbursement/clubChairman.html")
@RequiresPermissions("sys.jf.reimbursement.clubChairman") @RequiresPermissions("sys.jf.reimbursement.clubChairman")
public void index() { 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() {
}
/** /**
* 活动报销协会审核 * pageForm 含 pageNumber/pageSize、searchName(u.username/u.loginname)、searchKeyword 和排序字段/方向;
* @param pageForm * year 为年度,clubId/unionId/unitId 为组织ID,activityName 为活动名,isAudit 为0全部/1已审核/2未审核。
* @param year * 返回统一结果,code=0 时 data 为 Pagination,list 为本页记录、totalCount 为总数。
* @param clubId
* @param activityName
* @param isAudit 0,全部;1,已审核;2,未审核
* @return
*/ */
@At @At
@ViReturn @ViReturn
@@ -66,57 +49,38 @@ public class ActivityReimbursementClubChairmanController {
public Object pageData(PageForm pageForm, public Object pageData(PageForm pageForm,
@Param(value = "year", required = false) Integer year, @Param(value = "year", required = false) Integer year,
@Param(value = "clubId", required = false) String clubId, @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 = "activityName", required = false) String activityName,
@Param(value = "isAudit", required = false) String isAudit) { @Param(value = "isAudit", required = false) String isAudit) {
Sql sql = activityBxService.activityReimbursementSql(); return activityBxService.reimbursementPage("clubChairman", pageForm, NutMap.NEW()
Cnd cnd = Cnd.NEW(); .addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId)
cnd.andEX("act.`year`","=",year); .addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit));
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);
} }
/** 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 @At
@ViReturn @ViReturn
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@RequiresPermissions("sys.jf.reimbursement.clubChairman") @RequiresPermissions("sys.jf.reimbursement.clubChairman")
@SLog(type = "文体活动", tag = "活动报销", msg = "协会负责人审核", param = true, result = true) @SLog(type = "文体活动", tag = "活动报销", msg = "协会负责人审核", param = true, result = true)
public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){ public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) {
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString()); if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) {
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString()); throw new IllegalArgumentException("请选择活动报销申请并填写审核信息");
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","协会负责人拒绝"));
} }
activityBxService.reviewReimbursement("clubChairman", id, audit);
return null; return null;
} }
} }
@@ -1,105 +1,86 @@
package io.v.nutz.zhgh.jf.controller.reimbursement; 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.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.model.Audit;
import io.v.nutz.base.query.PageForm; 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 io.v.nutz.web.commons.slog.annotation.SLog;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.dao.Chain; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.ioc.aop.Aop;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import java.util.Date; /** 活动报销:校工会主席审核,PC 与手机共用列表、数据范围和审核服务。 */
/**
* @version 1.0
* @Author zzr
* @name:ActivityReimbursementSchoolChairmanController
* @Date 2024/4/26 14:58
* @注释 报销,校工会主席审核
*/
@IocBean @IocBean
@Ok("json:full") @Ok("json:full")
@At("/platform/jf/reimbursement/schoolChairman") @At("/platform/jf/reimbursement/schoolChairman")
public class ActivityReimbursementSchoolChairmanController { public class ActivityReimbursementSchoolChairmanController {
@Inject @Inject
private ActivityBxService activityBxService; private ActivityBxService activityBxService;
/** 保留现有 PC 页面入口。 */
@At("") @At("")
@Ok("beetl:/platform/jf/reimbursement/schoolChairman.html") @Ok("beetl:/platform/jf/reimbursement/schoolChairman.html")
@RequiresPermissions("sys.jf.reimbursement.schoolChairman") @RequiresPermissions("sys.jf.reimbursement.schoolChairman")
public void index() { 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 @At
@ViReturn @ViReturn
@RequiresPermissions("sys.jf.reimbursement.schoolChairman") @RequiresPermissions("sys.jf.reimbursement.schoolChairman")
public Object pageData(PageForm pageForm, public Object pageData(PageForm pageForm,
@Param(value = "year", required = false) Integer year, @Param(value = "year", required = false) Integer year,
@Param(value = "clubId", required = false) String clubId,
@Param(value = "unionId", required = false) String unionId, @Param(value = "unionId", required = false) String unionId,
@Param(value = "unitId", required = false) String unitId, @Param(value = "unitId", required = false) String unitId,
@Param(value = "clubId",required = false) String clubId,
@Param(value = "activityName", required = false) String activityName, @Param(value = "activityName", required = false) String activityName,
@Param(value = "isAudit", required = false) String isAudit) { @Param(value = "isAudit", required = false) String isAudit) {
Sql sql = activityBxService.activityReimbursementSql(); return activityBxService.reimbursementPage("schoolChairman", pageForm, NutMap.NEW()
Cnd cnd = Cnd.NEW(); .addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId)
cnd.andEX("act.`year`","=",year); .addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit));
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);
} }
/** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */
@At @At
@ViReturn @ViReturn
@RequiresPermissions("sys.jf.reimbursement.schoolChairman") @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) @SLog(type = "文体活动", tag = "活动报销", msg = "校工会主席审核", param = true, result = true)
public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){ public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) {
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString()); if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) {
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString()); throw new IllegalArgumentException("请选择活动报销申请并填写审核信息");
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","校工会主席拒绝"));
} }
activityBxService.reviewReimbursement("schoolChairman", id, audit);
return null; return null;
} }
} }
@@ -1,104 +1,86 @@
package io.v.nutz.zhgh.jf.controller.reimbursement; 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.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.model.Audit;
import io.v.nutz.base.query.PageForm; 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 io.v.nutz.web.commons.slog.annotation.SLog;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.dao.Chain; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.ioc.aop.Aop;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import java.util.Date; /** 活动报销:校工会负责人审核,PC 与手机共用列表、数据范围和审核服务。 */
/**
* @version 1.0
* @Author zzr
* @name:ActivityReimbursementSchoolDirectorController
* @Date 2024/4/26 14:58
* @注释 报销校工会负责人审核
*/
@IocBean @IocBean
@Ok("json:full") @Ok("json:full")
@At("/platform/jf/reimbursement/schoolDirector") @At("/platform/jf/reimbursement/schoolDirector")
public class ActivityReimbursementSchoolDirectorController { public class ActivityReimbursementSchoolDirectorController {
@Inject @Inject
private ActivityBxService activityBxService; private ActivityBxService activityBxService;
/** 保留现有 PC 页面入口。 */
@At("") @At("")
@Ok("beetl:/platform/jf/reimbursement/schoolDirector.html") @Ok("beetl:/platform/jf/reimbursement/schoolDirector.html")
@RequiresPermissions("sys.jf.reimbursement.schoolDirector") @RequiresPermissions("sys.jf.reimbursement.schoolDirector")
public void index() { 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 @At
@ViReturn @ViReturn
@RequiresPermissions("sys.jf.reimbursement.schoolDirector") @RequiresPermissions("sys.jf.reimbursement.schoolDirector")
public Object pageData(PageForm pageForm, public Object pageData(PageForm pageForm,
@Param(value = "year", required = false) Integer year, @Param(value = "year", required = false) Integer year,
@Param(value = "clubId", required = false) String clubId,
@Param(value = "unionId", required = false) String unionId, @Param(value = "unionId", required = false) String unionId,
@Param(value = "unitId", required = false) String unitId, @Param(value = "unitId", required = false) String unitId,
@Param(value = "clubId",required = false) String clubId,
@Param(value = "activityName", required = false) String activityName, @Param(value = "activityName", required = false) String activityName,
@Param(value = "isAudit", required = false) String isAudit) { @Param(value = "isAudit", required = false) String isAudit) {
Sql sql = activityBxService.activityReimbursementSql(); return activityBxService.reimbursementPage("schoolDirector", pageForm, NutMap.NEW()
Cnd cnd = Cnd.NEW(); .addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId)
cnd.andEX("act.`year`","=",year); .addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit));
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);
} }
/** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */
@At @At
@ViReturn @ViReturn
@RequiresPermissions("sys.jf.reimbursement.schoolDirector") @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) @SLog(type = "文体活动", tag = "活动报销", msg = "校工会负责人审核", param = true, result = true)
public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){ public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) {
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString()); if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) {
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString()); throw new IllegalArgumentException("请选择活动报销申请并填写审核信息");
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","校工会负责人拒绝"));
} }
activityBxService.reviewReimbursement("schoolDirector", id, audit);
return null; return null;
} }
} }
@@ -1,114 +1,86 @@
package io.v.nutz.zhgh.jf.controller.reimbursement; 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.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.model.Audit;
import io.v.nutz.base.query.PageForm; 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 io.v.nutz.web.commons.slog.annotation.SLog;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.dao.Chain; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.ioc.aop.Aop;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import java.util.Date; /** 活动报销:分工会主席审核,PC 与手机共用列表、数据范围和审核服务。 */
/**
* @version 1.0
* @Author zzr
* @name:ActivityReimbursementUnionChairmanController
* @Date 2024/4/26 14:58
* @注释 活动报销,分工会主席审核
*/
@IocBean @IocBean
@Ok("json:full") @Ok("json:full")
@At("/platform/jf/reimbursement/unionChairman") @At("/platform/jf/reimbursement/unionChairman")
public class ActivityReimbursementUnionChairmanController { public class ActivityReimbursementUnionChairmanController {
@Inject @Inject
private ActivityBxService activityBxService; private ActivityBxService activityBxService;
/** 保留现有 PC 页面入口。 */
@At("") @At("")
@Ok("beetl:/platform/jf/reimbursement/unionChairman.html") @Ok("beetl:/platform/jf/reimbursement/unionChairman.html")
@RequiresPermissions("sys.jf.reimbursement.unionChairman") @RequiresPermissions("sys.jf.reimbursement.unionChairman")
public void index() { 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 @At
@ViReturn @ViReturn
@RequiresPermissions("sys.jf.reimbursement.unionChairman") @RequiresPermissions("sys.jf.reimbursement.unionChairman")
public Object pageData(PageForm pageForm, public Object pageData(PageForm pageForm,
@Param(value = "year", required = false) Integer year, @Param(value = "year", required = false) Integer year,
@Param(value = "clubId", required = false) String clubId,
@Param(value = "unionId", required = false) String unionId, @Param(value = "unionId", required = false) String unionId,
@Param(value = "unitId", required = false) String unitId, @Param(value = "unitId", required = false) String unitId,
@Param(value = "activityName", required = false) String activityName, @Param(value = "activityName", required = false) String activityName,
@Param(value = "isAudit", required = false) String isAudit) { @Param(value = "isAudit", required = false) String isAudit) {
Sql sql = activityBxService.activityReimbursementSql(); return activityBxService.reimbursementPage("unionChairman", pageForm, NutMap.NEW()
Cnd cnd = Cnd.NEW(); .addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId)
cnd.andEX("act.`year`","=",year); .addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit));
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);
} }
/** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */
/**
* 基层工会活动报销
* 基层工会活动——>基层工会主席审核——>校工会主席审核——>流程结束
* @param id
* @param audit
* @return
*/
@At @At
@ViReturn @ViReturn
@RequiresPermissions("sys.jf.reimbursement.unionChairman") @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) @SLog(type = "文体活动", tag = "活动报销", msg = "分工会主席审核", param = true, result = true)
public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){ public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) {
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString()); if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) {
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString()); throw new IllegalArgumentException("请选择活动报销申请并填写审核信息");
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","基层工会拒绝"));
} }
activityBxService.reviewReimbursement("unionChairman", id, audit);
return null; return null;
} }
} }
@@ -107,8 +107,8 @@ public enum FundsToHandler {
loginNames, 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", "/platform/jf/activity_bx/association/mobile?id=" + reimbursementId + "&mode=audit",
"/platform/jf/activity_bx/association" "/platform/jf/activity_bx/association/mobile?id=" + reimbursementId + "&mode=view"
); );
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "协会负责人审核"); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "协会负责人审核");
@@ -144,8 +144,8 @@ public enum FundsToHandler {
loginNames, 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", "/platform/jf/activity_bx/division/mobile?id=" + reimbursementId + "&mode=audit",
"/platform/jf/activity_bx/division" "/platform/jf/activity_bx/division/mobile?id=" + reimbursementId + "&mode=view"
); );
localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "基层工会审核"); localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "基层工会审核");
@@ -181,8 +181,8 @@ public enum FundsToHandler {
loginNames, 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", "/platform/jf/activity_bx/accounting/mobile?id=" + reimbursementId + "&mode=audit",
"/platform/jf/activity_bx/accounting" "/platform/jf/activity_bx/accounting/mobile?id=" + reimbursementId + "&mode=view"
); );
localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "校工会负责人审核"); localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "校工会负责人审核");
@@ -217,8 +217,8 @@ public enum FundsToHandler {
loginNames, 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", "/platform/jf/activity_bx/union/mobile?id=" + reimbursementId + "&mode=audit",
"/platform/jf/activity_bx/union" "/platform/jf/activity_bx/union/mobile?id=" + reimbursementId + "&mode=view"
); );
localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "校工会主席审核"); localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "校工会主席审核");
@@ -129,8 +129,8 @@ public enum ReimbursementToHandler {
unionLeader, unionLeader,
"/platform/jf/reimbursement/unionChairman", "/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" "/platform/jf/reimbursement/unionChairman/mobile?id=" + reimbursementId + "&mode=view"
); );
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "基层工会审核"); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "基层工会审核");
@@ -173,8 +173,8 @@ public enum ReimbursementToHandler {
clubLeader, clubLeader,
"/platform/jf/reimbursement/clubChairman", "/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" "/platform/jf/reimbursement/clubChairman/mobile?id=" + reimbursementId + "&mode=view"
); );
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "协会负责人审核"); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "协会负责人审核");
@@ -214,8 +214,8 @@ public enum ReimbursementToHandler {
List.of("1019911404"), List.of("1019911404"),
"/platform/jf/reimbursement/schoolDirector", "/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" "/platform/jf/reimbursement/schoolDirector/mobile?id=" + reimbursementId + "&mode=view"
); );
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "校工会负责人审核"); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "校工会负责人审核");
@@ -257,8 +257,8 @@ public enum ReimbursementToHandler {
schoolChairmanLeader, schoolChairmanLeader,
"/platform/jf/reimbursement/schoolChairman", "/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" "/platform/jf/reimbursement/schoolChairman/mobile?id=" + reimbursementId + "&mode=view"
); );
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "校工会主席审核"); localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "校工会主席审核");
@@ -2,11 +2,15 @@ package io.v.nutz.zhgh.jf.service;
import io.v.nutz.zhgh.jf.model.ActivityBx; import io.v.nutz.zhgh.jf.model.ActivityBx;
import io.v.nutz.base.service.ViService; import io.v.nutz.base.service.ViService;
import io.v.nutz.base.model.Audit;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List;
import cn.wizzer.framework.page.Pagination;
import io.v.nutz.sys.models.Sys_club;
/** /**
* @author zhf * @author zhf
@@ -20,6 +24,39 @@ public interface ActivityBxService extends ViService<ActivityBx> {
NutMap queryOne(String id); NutMap queryOne(String id);
/** 按年度、协会、审核状态及 PC 筛选条件分页查询;始终限制为当前用户管理的协会活动。 */
Pagination associationPage(NutMap filters);
/** 返回当前用户管理的协会,供手机筛选;管理员可查看全部协会。 */
List<Sys_club> 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<io.v.nutz.base.model.Goods> 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 exportByView(String id, OutputStream outputStream);
void exportByCon(String id, OutputStream outputStream); void exportByCon(String id, OutputStream outputStream);
@@ -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.DateUtil;
import io.v.nutz.base.utils.Html2Text; import io.v.nutz.base.utils.Html2Text;
import io.v.nutz.base.utils.Vi; 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.base.model.Goods;
import io.v.nutz.zhgh.jf.model.ActivityBx; import io.v.nutz.zhgh.jf.model.ActivityBx;
import io.v.nutz.zhgh.jf.model.ActivityFile; 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.aop.interceptor.ioc.TransAop;
import org.nutz.boot.starter.ftp.FtpService; import org.nutz.boot.starter.ftp.FtpService;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Chain;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
@@ -43,6 +56,7 @@ import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -61,6 +75,492 @@ public class ActivityBxServiceImpl extends ViServiceImpl<ActivityBx> implements
@Inject @Inject
private FtpService ftpService; 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<Goods> 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<String> 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<String, String> 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<String, String> 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<Sys_club> 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<String> 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 @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void doAdd(ActivityBx activityBx) { public void doAdd(ActivityBx activityBx) {
@@ -2,6 +2,7 @@ package io.v.nutz.zhgh.member.controller.info;
import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import io.v.nutz.base.annontation.ViReturn; import io.v.nutz.base.annontation.ViReturn;
@@ -153,16 +154,15 @@ public class MemberGroupInfoController {
cnd.where().andLike("jobTitle", jobTitle); cnd.where().andLike("jobTitle", jobTitle);
} }
if (!ShiroUtil.hasAnyRoles("sysadmin,H03,A06")) { // 分工会主席可能同时具有校会员管理员角色,除系统管理员和校级管理员外,必须优先限制为当前分工会。
if (ShiroUtil.hasRole("H04")) { if (ShiroUtil.hasRole("H04") && !ShiroUtil.hasAnyRoles("sysadmin,A06")) {
cnd.and("unionId", "=", Vi.getUnionId()); cnd.and("unionId", "=", Vi.getUnionId());
} else if (ShiroUtil.hasRole("ghxzzz")) { } else if (!ShiroUtil.hasAnyRoles("sysadmin,H03,A06") && ShiroUtil.hasRole("ghxzzz")) {
cnd.and("unionId", "=", Vi.getUnionId()); cnd.and("unionId", "=", Vi.getUnionId());
SqlExpressionGroup seg = new SqlExpressionGroup(); SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("unionGroupId", "in", io.v.nutz.web.commons.utils.ShiroUtil.getUnionGroupIds()); seg.or("unionGroupId", "in", io.v.nutz.web.commons.utils.ShiroUtil.getUnionGroupIds());
seg.or("unionGroupId", "is", null); seg.or("unionGroupId", "is", null);
cnd.and(seg); cnd.and(seg);
}
} }
@@ -249,7 +249,7 @@ public class MemberGroupInfoController {
@Param(value = "memberSearchName", required = false) String memberSearchName, @Param(value = "memberSearchName", required = false) String memberSearchName,
@Param(value = "memberSearchKeyWord", required = false) String memberSearchKeyWord) { @Param(value = "memberSearchKeyWord", required = false) String memberSearchKeyWord) {
try { try {
ViTool.excelResponse(response, "会员名单.xls"); ViTool.excelResponse(response, "会员名单.xlsx");
List<NutMap> dataList = memberService.listMap(dataSql(pageForm, unionId, unitId, threeUnitId, unionGroupId, Json.fromJsonAsArray(String.class, personTypes), Json.fromJsonAsArray(String.class, userStates), campus, sex, memberStatus, position, jobTitle, memberSearchName, memberSearchKeyWord)); List<NutMap> dataList = memberService.listMap(dataSql(pageForm, unionId, unitId, threeUnitId, unionGroupId, Json.fromJsonAsArray(String.class, personTypes), Json.fromJsonAsArray(String.class, userStates), campus, sex, memberStatus, position, jobTitle, memberSearchName, memberSearchKeyWord));
List<ExcelExportEntity> entityList = new ArrayList<>(); List<ExcelExportEntity> entityList = new ArrayList<>();
ExcelExportEntity no = new ExcelExportEntity("序号", "no", 10); ExcelExportEntity no = new ExcelExportEntity("序号", "no", 10);
@@ -260,7 +260,9 @@ public class MemberGroupInfoController {
propMap.forEach((k, v) -> { propMap.forEach((k, v) -> {
entityList.add(new ExcelExportEntity(v, k, 20)); entityList.add(new ExcelExportEntity(v, k, 20));
}); });
Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), entityList, dataList); ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, dataList);
workbook.write(response.getOutputStream()); workbook.write(response.getOutputStream());
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
@@ -1,21 +1,17 @@
package io.v.nutz.zhgh.mobile.common; package io.v.nutz.zhgh.mobile.common;
import cn.hutool.core.util.StrUtil;
import io.v.nutz.base.annontation.ViReturn;
import io.v.nutz.base.result.Result; import io.v.nutz.base.result.Result;
import io.v.nutz.base.service.ViService; import io.v.nutz.web.commons.controller.SignatureController.PersonalSignatureService;
import io.v.nutz.web.commons.utils.ShiroUtil; import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.zhgh.zgfw.model.UserSign; import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.nutz.dao.Chain; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings; import org.nutz.lang.Strings;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import java.util.List;
/** /**
* @author: Aaron * @author: Aaron
* @create: 2020-11-25 11:29 * @create: 2020-11-25 11:29
@@ -26,26 +22,41 @@ import java.util.List;
@Ok("json:full") @Ok("json:full")
public class updateSign { public class updateSign {
@Inject("UserSign") @Inject("personalSignatureService")
private ViService<UserSign> userSignService; private PersonalSignatureService personalSignatureService;
@At /**
* 兼容旧 Java 调用:sign 为带图片数据头的 Base64,user_id 为空或当前登录人ID。
* 默认按 Base64 类型保存;返回 Result,code=0 表示成功,data 为保存后的签名原值。
* 此重载仅用于内部调用,不标注 @At,HTTP 请求仍使用三参数入口。
*/
@RequiresAuthentication
@Aop(TransAop.READ_COMMITTED)
public Result update(String sign, String user_id) { public Result update(String sign, String user_id) {
if(StrUtil.isBlank(user_id)){ return update(sign, user_id, Boolean.TRUE);
user_id = (String) ShiroUtil.getPrincipalProperty("id");
}
if (Strings.isNotBlank(getMySign())) {
userSignService.update(Chain.make("data", sign), Cnd.where("user_id", "=", user_id));
} else {
userSignService.insert(new UserSign(user_id, sign));
}
return Result.success();
} }
/**
* sign 为新手写 Base64 或本人已有文件路径;is_value_base64 默认 true,false 保存文件类型。
* user_id 兼容旧参数但只允许当前登录人;返回 {code,msg,data},data 为保存后的签名原值。
*/
@At @At
public String getMySign() { @RequiresAuthentication
List<UserSign> userSigns = userSignService.query(Cnd.NEW().and("user_id", "=", ShiroUtil.getPrincipalProperty("id"))); @Aop(TransAop.READ_COMMITTED)
return userSigns.isEmpty() ? null : userSigns.get(0).getData(); public Result update(String sign, String user_id, Boolean is_value_base64) {
String currentUserId = ShiroUtil.getPrincipalProperty("id").toString();
if (Strings.isNotBlank(user_id) && !currentUserId.equals(user_id)) return Result.error("只能保存本人的签名");
if (Strings.isBlank(sign)) return Result.error("请先签字");
String saved = personalSignatureService.savePersonal(currentUserId, sign, !Boolean.FALSE.equals(is_value_base64));
return Result.success().addData(saved);
}
/** is_value_base64 默认 true;按当前登录用户查询对应类型,返回签名字符串,未找到返回 null。 */
@At
@RequiresAuthentication
public String getMySign(Boolean is_value_base64) {
return personalSignatureService.findPersonal(ShiroUtil.getPrincipalProperty("id").toString(),
!Boolean.FALSE.equals(is_value_base64));
} }
} }
@@ -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
})()
@@ -33,7 +33,7 @@
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="签字" :span="3"> <el-descriptions-item label="签字" :span="3">
<el-image v-if="viewData.clubAudit.auditSign" <el-image v-if="viewData.clubAudit.auditSign"
:src="viewData.clubAudit.auditSign" :src="'/signature/getSignData?path=' + encodeURIComponent(viewData.clubAudit.auditSign)"
fit="contain" fit="contain"
style="width: 300px; height: 100px"></el-image> style="width: 300px; height: 100px"></el-image>
<span v-else>暂无</span> <span v-else>暂无</span>
@@ -0,0 +1,84 @@
<template>
<div>
<van-loading v-if="loading" style="padding: 20px; text-align: center">加载中</van-loading>
<van-empty v-else-if="error" :description="error">
<van-button size="small" @click="getInfo(applyId)">重新加载</van-button>
</van-empty>
<template v-else-if="viewData.id">
<!-- 复用手机慰问详情的红色竖线标题样式。 -->
<div class="van-cell-group__title">
<div class="van-sidebar-item van-sidebar-item--select">申请信息</div>
</div>
<van-cell-group>
<van-cell title="姓名" :value="viewData.username"></van-cell>
<van-cell title="性别" :value="viewData.sex"></van-cell>
<van-cell title="工号" :value="viewData.loginname"></van-cell>
<van-cell title="出生年月" :value="viewData.birthday ? moment(viewData.birthday).format('YYYY-MM-DD') : ''"></van-cell>
<van-cell title="联系电话" :value="viewData.mobile"></van-cell>
<van-cell title="是否有特殊病史" :value="viewData.medicalHistory === true ? '是' : viewData.medicalHistory === false ? '否' : '暂无'"></van-cell>
<van-cell title="是否有特别需要说明的问题" :value="viewData.otherThing || '无'"></van-cell>
<van-cell title="紧急联系人" :value="viewData.emergencyContact || '暂无'"></van-cell>
<van-cell title="紧急联系电话" :value="viewData.emergencyMobile || '暂无'"></van-cell>
</van-cell-group>
<van-cell-group v-if="viewData.clubAudit" title="社团审核信息">
<van-cell title="审核人员" :value="viewData.clubAudit.username"></van-cell>
<van-cell title="审核时间" :value="viewData.clubAudit.auditTime"></van-cell>
<van-cell title="审核意见" :value="viewData.clubAudit.auditOpinion"></van-cell>
<van-cell title="审核签字">
<van-image v-if="viewData.clubAudit.auditSign" :src="'/signature/getSignData?path=' + encodeURIComponent(viewData.clubAudit.auditSign)" width="180" fit="contain"></van-image>
<span v-else>暂无</span>
</van-cell>
</van-cell-group>
<template v-if="handle">
<slot name="handle"></slot>
</template>
</template>
</div>
</template>
<script>
module.exports = {
name: 'mobileClubUserApplyInfo',
props: {
applyId: {type: String, default: ''},
handle: {type: Boolean, default: false}
},
data() {
return {viewData: {}, loading: false, error: '', requestVersion: 0}
},
watch: {
// 异步组件挂载后立即加载当前 ID,不依赖父页的 nextTick 和 ref 时序。
applyId: {
immediate: true,
handler(id) { this.getInfo(id) }
}
},
methods: {
// id 为入会申请主键;data 包含申请资料及可选 clubAudit 审核对象。
getInfo(id) {
const version = this.requestVersion + 1
this.$set(this, 'requestVersion', version)
this.$set(this, 'viewData', {})
this.$set(this, 'error', '')
this.$set(this, 'loading', !!id)
if (!id) return
return $.post('/platform/sys/club/sq/findOne', {id}).then(res => {
// 切换申请或组件销毁后忽略旧响应,避免显示上一位申请人的资料。
if (version !== this.requestVersion) return
if (res.code === 0 && res.data && res.data.id) {
this.$set(this, 'viewData', res.data)
} else {
this.$set(this, 'error', res.msg || '申请资料加载失败')
}
}).fail(() => {
if (version === this.requestVersion) this.$set(this, 'error', '申请资料加载失败,请重试')
}).always(() => {
if (version === this.requestVersion) this.$set(this, 'loading', false)
})
}
},
beforeDestroy() {
this.$set(this, 'requestVersion', this.requestVersion + 1)
}
}
</script>
@@ -1,192 +1,131 @@
<template> <template>
<div class="signature-wrap"> <div class="signature-wrap">
<canvas id="canvas"></canvas> <div ref="surface" class="signature-surface">
<canvas ref="canvas" aria-label="手写签字区域"></canvas>
</div>
<div class="action-buttons"> <div class="action-buttons">
<button @click="clear" class="action-button-danger" type="button">清空</button> <button @click="clear" type="button" :disabled="busy">清空</button>
<button @click="undo" class="action-button-warning" type="button">撤销</button> <button @click="undo" type="button" :disabled="busy">撤销</button>
<button @click="save" class="action-button-primary" type="button">确定</button> <button @click="save" class="action-button-primary" type="button" :disabled="busy">{{ busy ? '保存中' : '确定' }}</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
// function a() {
// // 检查当前屏幕方向
// if (window.orientation === 0 || window.orientation === 180) {
// // 竖屏状态
// console.log("竖屏状态")
// alert("当前不支持竖屏,请将设备调整为横屏!")
// } else {
// // 横屏状态,执行禁止旋转的操作
// alert("当前不支持横屏,请将设备调整为竖屏!")
// // 可以在这里使内容固定在竖屏方向
// const innerWidth = window.innerWidth
// const innerHeight = window.innerHeight
// alert(innerHeight)
// alert(innerWidth)
// }
// }
//
// window.addEventListener("orientationchange", a)
// a()
let signature = null
module.exports = { module.exports = {
name: "sysSignature", name: "sysSignature",
props: {
busy: {type: Boolean, default: false}
},
data() { data() {
return { return {initialized: false}
content: null,
initialized: false
}
}, },
methods: { methods: {
clear() { clear() {
signature.clear() if (this.$options.signaturePad && !this.busy) this.$options.signaturePad.clear()
}, },
undo() { undo() {
signature.undo() if (this.$options.signaturePad && !this.busy) this.$options.signaturePad.undo()
},
rotate() {
signature.getRotateCanvas(90)
}, },
/** save 事件返回与竖屏画布方向一致的 PNG Base64,不旋转签字图片。 */
save() { save() {
if (signature.isEmpty()) { if (this.busy) return
alert("请签字后再保存") const signature = this.$options.signaturePad
if (!signature || signature.isEmpty()) {
this.$toast("请签字后再保存")
return return
} }
// const png = signature.getPNG() this.$emit("save", signature.toDataURL())
//旋转一下 横过来
const rotateCanvas = signature.getRotateCanvas(-90)
this.$emit("save", rotateCanvas.toDataURL())
}, },
/** 使用弹框内实际可用尺寸;每个组件独立持有画布,高分屏按像素比绘制。 */
initSignature() { initSignature() {
if (this.initialized) return; if (this.initialized || this._isDestroyed) return
const canvas = this.$refs.canvas
const canvas = document.getElementById("canvas"); const surface = this.$refs.surface
if (!canvas) return; if (!canvas || !surface) return
const width = surface.clientWidth
// 检查 canvas 是否已有有效尺寸 const height = surface.clientHeight
const rect = canvas.getBoundingClientRect(); // 隐藏或尚未完成布局时不创建零尺寸画布,销毁时会取消本次重试。
if (rect.width <= 0 || rect.height <= 0) { if (width <= 0 || height <= 0) {
// 尺寸无效,稍后重试 this.$options.retryTimer = setTimeout(() => this.initSignature(), 50)
setTimeout(() => this.initSignature(), 50); return
return;
} }
this.$options.signaturePad = new SmoothSignature(canvas, {
const padding = 30; width,
const width = window.innerWidth - padding; height,
const height = window.innerHeight - padding; scale: window.devicePixelRatio || 1,
minWidth: 2,
// 设置 CSS 尺寸 maxWidth: 5,
canvas.style.width = `${width}px`; color: "#222222"
canvas.style.height = `${height}px`; })
this.initialized = true
// 高清绘制
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
// 销毁旧实例(防止重复)
if (signature) {
signature.clear();
}
signature = new SmoothSignature(canvas, {
width: width,
height: height,
scale: dpr,
minWidth: 4,
maxWidth: 10,
color: "#000000"
});
this.initialized = true;
} }
}, },
mounted() { mounted() {
// 延迟初始化,确保 DOM 渲染完成 this.$nextTick(() => this.initSignature())
this.$nextTick(() => {
this.initSignature();
});
}, },
beforeDestroy() { beforeDestroy() {
signature = null; // SmoothSignature 在 document 上绑定结束事件,反复打开时必须解除旧实例监听。
this.initialized = false; clearTimeout(this.$options.retryTimer)
if (this.$options.signaturePad) this.$options.signaturePad.removeListener()
this.$options.signaturePad = null
this.initialized = false
} }
} }
</script> </script>
<style scoped> <style scoped>
.signature-wrap { .signature-wrap {
position: fixed; flex: 1;
left: 0; min-height: 0;
right: 0;
bottom: 0;
padding: 15px;
background: #ffffff;
}
.signature-wrap .action {
width: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.signature-wrap .action-buttons {
white-space: nowrap;
transform: rotate(90deg);
display: flex;
column-gap: 10px;
position: fixed;
bottom: 0px;
left: 60px;
transform: rotate(90deg);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
row-gap: 20px; padding: 12px 16px;
background: #fff;
}
.signature-surface {
flex: 1;
min-height: 0;
overflow: hidden;
border: 1px dashed #c8c9cc;
border-radius: 8px;
background: #fafbfc;
}
.signature-surface canvas {
display: block;
width: 100%;
height: 100%;
touch-action: none;
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 12px;
flex-shrink: 0;
} }
.action-buttons button { .action-buttons button {
color: #ffffff;
position: relative;
display: inline-block;
box-sizing: border-box;
height: auto;
padding: 3px 12px;
margin: 0;
font-size: 15px;
line-height: 1.4;
text-align: center;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.15s ease;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.action-button-primary {
background-color: #1677ff;
border: 1px solid #1677ff;
}
.action-button-danger {
background-color: #ff3141;
border: 1px solid #ff3141;
}
.action-button-warning {
background-color: #ff8f1f;
border: 1px solid #ff8f1f;
}
.signature-wrap canvas {
flex: 1; flex: 1;
border-radius: 10px; height: 36px;
border: 2px dashed #ccc; padding: 0;
border: 1px solid #ebedf0;
border-radius: 6px;
background: #fff;
color: #323233;
font-size: 14px;
text-align: center;
}
.action-buttons .action-button-primary {
background: #1989fa;
border-color: #1989fa;
color: #fff;
}
.action-buttons button:disabled {
opacity: .5;
} }
</style> </style>
@@ -1,23 +1,33 @@
<template> <template>
<div class="h5-signature"> <div class="h5-signature">
<div v-if="!showSignaturePanel" class="main-panel"> <div class="main-panel">
<slot v-if="$slots.default"></slot> <slot v-if="$slots.default"></slot>
<template v-else> <template v-else>
<van-button @click="openSignature" class="open-button" size="middle" plain native-type="button">打开签字板</van-button> <van-button @click="openSignature" class="open-button" size="middle" plain native-type="button" :disabled="disabled || saving">打开签字板</van-button>
<van-image :src="signatureContent"> <!-- 默认回显仅供预览,主动使用后才回传,避免扫码页收到 input 就自动提交。 -->
<van-button v-if="canUsePersonalSignature" @click="usePersonalSignature" class="open-button" size="middle"
native-type="button" :loading="saving" :disabled="disabled || saving">使用此签名</van-button>
<van-image :src="signatureImageUrl" fit="contain">
<template v-slot:error>请点击下方打开签字板按钮进行签字</template> <template v-slot:error>请点击下方打开签字板按钮进行签字</template>
</van-image> </van-image>
</template> </template>
</div> </div>
<van-popup v-model="showSignaturePanel" :style="{ height: '100vh', width: '100vw', display: 'flex', 'align-items': 'center' }"> <van-popup :value="signaturePanelVisible" @input="onPanelInput" :close-on-popstate="false"
<signature v-if="showSignaturePanel" @save="save"></signature> position="bottom" round get-container="body" class="h5-signature-sheet"
:close-on-click-overlay="false" @opened="onPanelOpened">
<div class="h5-signature-heading">
<button type="button" :disabled="saving || disabled" @click="closeSignature">取消</button>
<span>{{ panelTitle }}</span>
<span aria-hidden="true"></span>
</div>
<signature v-if="signaturePanelVisible && boardReady" :busy="saving || disabled" @save="save"></signature>
</van-popup> </van-popup>
</div> </div>
</template> </template>
<script> <script>
module.exports = { module.exports = {
name: "h5", name: "mobileSignature",
components: { components: {
signature: httpVueLoader("/components/sign/index.vue?v=" + new Date().getTime()) signature: httpVueLoader("/components/sign/index.vue?v=" + new Date().getTime())
}, },
@@ -25,68 +35,186 @@ module.exports = {
value: { value: {
type: String, type: String,
default: "" default: ""
},
// 兼容已有 :my_sign.sync 页面;与 v-model 同时传入时优先使用 value。
my_sign: {type: String, default: ""},
// 不传 popupVisible 时保持组件自行开关;传入布尔值时由页面统一历史栈管理。
popupVisible: {type: Boolean, default: null},
// 与 PC 一致:true 使用 Base64,false 使用个人签名表中的文件路径。
is_value_base64: {type: Boolean, default: true},
// 仅接收旧页面属性以兼容模板;不再参与判断,所有主动提交均保存个人签名。
saveToProfile: {type: Boolean, default: true},
disabled: {type: Boolean, default: false},
panelTitle: {type: String, default: "签字"}
},
computed: {
controlled() {
return this.popupVisible !== null
},
signaturePanelVisible() {
return this.controlled ? this.popupVisible : this.showSignaturePanel
},
boundSignature() {
return Object.prototype.hasOwnProperty.call(this.$options.propsData || {}, "value") ? this.value : this.my_sign
},
canUsePersonalSignature() {
return !this.matchesSignatureType(this.boundSignature) && this.matchesSignatureType(this.personalSignature)
&& this.signatureContent === this.personalSignature
},
signatureImageUrl() {
// 显示地址与提交值分离:文件接口 URL 只用于 img,回填仍使用原始文件路径。
if (!this.matchesSignatureType(this.signatureContent)) return ''
return this.is_value_base64 ? this.signatureContent
: '/signature/getSignData?path=' + encodeURIComponent(this.signatureContent)
} }
}, },
watch: { watch: {
value: { boundSignature: {
handler(val) { handler(val) {
this.signatureContent = val // 业务回显优先;表单清空时恢复个人签名预览,仍须点击使用才回填。
this.signatureVersion++
this.signatureContent = this.matchesSignatureType(val) ? val : this.personalSignature
}, },
immediate: true immediate: true
},
is_value_base64() {
// 类型切换后清除另一种个人签名,只重新读取新类型,不互转历史签名。
this.signatureVersion++
this.personalSignature = ''
this.signatureContent = this.matchesSignatureType(this.boundSignature) ? this.boundSignature : ''
this.getMySign(true)
},
signaturePanelVisible(visible) {
// 每次打开重新创建画布;等待弹出动画完成后再按真实可用区域计算尺寸。
if (!visible) this.boardReady = false
} }
}, },
data() { data() {
return { return {
signatureContent: null, signatureContent: null,
showSignaturePanel: false personalSignature: '',
signatureVersion: 0,
signatureRequest: 0,
signatureLoading: false,
showSignaturePanel: false,
boardReady: false,
saving: false
} }
}, },
methods: { methods: {
openSignature() { /** value 为签名原值;只接受当前参数对应的数据,空值或另一种格式不能冒充本类型签名。 */
this.showSignaturePanel = true matchesSignatureType(value) {
if (typeof value !== 'string' || !value) return false
return this.is_value_base64 ? /^data:image\/(png|jpeg|jpg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/.test(value)
: value.startsWith('/') && !value.startsWith('//') && !/[?#\\\r\n]/.test(value) && !value.includes('..')
}, },
/** 复用签名同样经过个人签名保存;没有打开签字层时不能误关父页面的详情弹框。 */
save(signature) { usePersonalSignature() {
$.post("/mobile/common/signing/update", {sign: signature}) if (this.disabled || this.saving || !this.canUsePersonalSignature) return
return this.save(this.personalSignature, false)
},
openSignature() {
if (!this.disabled && !this.saving) this.changePanelVisible(true)
},
/** 受控模式仅发出开关请求,实际显隐由父页面在历史栈同步回调中更新。 */
changePanelVisible(visible) {
if (this.controlled) this.$emit(visible ? "open" : "close")
else if (visible) this.$options.popupLayer.open()
else this.$options.popupLayer.close()
},
closeSignature() {
if (!this.saving && !this.disabled) this.changePanelVisible(false)
},
onPanelInput(visible) {
// 受控弹框的 value 回落也会触发 input,不得再次请求关闭,否则会多退一层详情。
if (!this.controlled && !visible && this.showSignaturePanel) this.closeSignature()
},
onPanelOpened() {
this.boardReady = this.signaturePanelVisible
},
/** data 为当前模式的 Base64 或文件路径,同时通知两种已有绑定方式。 */
updateSignature(data) {
this.signatureVersion++
this.signatureContent = data
this.$emit("input", data)
this.$emit("update:my_sign", data)
},
/**
* signature 为新手写 Base64 或已读取的同类型个人签名;closePanel=false 表示在预览区复用。
* 提交 sign/is_value_base64,接口返回 {code,msg,data};data 为保存后的对应类型原值。
* 个人签名保存成功后才回填业务表单,文件模式的新手写图片由服务端上传。
*/
save(signature, closePanel = true) {
if (this.saving || this.disabled) return
// 新签字开始保存后,之前发出的个人签名查询不得覆盖本次操作。
const version = ++this.signatureVersion
const base64 = this.is_value_base64
this.saving = true
return $.post("/mobile/common/signing/update", {sign: signature, is_value_base64: base64})
.then((res) => { .then((res) => {
if (this._isDestroyed || this._isBeingDestroyed || version !== this.signatureVersion
|| base64 !== this.is_value_base64) return
if (res.code === 0) { if (res.code === 0) {
if (!this.matchesSignatureType(res.data)) {
this.$toast.fail("返回的签名格式不正确,请重试")
return
}
this.personalSignature = res.data
if (closePanel && this.signaturePanelVisible) this.changePanelVisible(false)
this.updateSignature(res.data)
this.$toast.success("保存成功") this.$toast.success("保存成功")
this.showSignaturePanel = false
this.getMySign()
} else { } else {
this.$toast.fail("保存失败") this.$toast.fail(res.msg || "保存失败")
} }
}) })
.fail(() => this.$toast.fail("签字保存失败,请重试"))
.always(() => { this.saving = false })
}, },
base64ToFile(base64Data, filename) { /**
// 将base64的数据部分提取出来 * previewOnly=true 仅初始化预览;省略参数保留旧页面主动调用时的回填行为。
const parts = base64Data.split(";base64,") * 接口传 is_value_base64,不传用户ID;按登录账号返回对应类型签名或 null,方法返回 jQuery Promise。
const contentType = parts[0].split(":")[1] * 无个人签名时保留当前内容;请求期间表单变更、重签或组件销毁后不再应用旧结果。
const raw = window.atob(parts[1]) */
const rawLength = raw.length getMySign(previewOnly = false) {
const uInt8Array = new Uint8Array(rawLength) const request = ++this.signatureRequest
const version = this.signatureVersion
for (let i = 0; i < rawLength; ++i) { this.signatureLoading = true
uInt8Array[i] = raw.charCodeAt(i) return $.get("/mobile/common/signing/getMySign", {is_value_base64: this.is_value_base64})
} .then((data) => {
if (request !== this.signatureRequest || version !== this.signatureVersion
// 使用Blob对象创建File对象 || this._isDestroyed || this._isBeingDestroyed) return
const blob = new Blob([uInt8Array], { type: contentType }) this.personalSignature = this.matchesSignatureType(data) ? data : ''
blob.lastModifiedDate = new Date() if (previewOnly) {
blob.name = filename if (!this.matchesSignatureType(this.boundSignature)) this.signatureContent = this.personalSignature
return new File([blob], filename, { type: contentType }) } else if (this.personalSignature) {
}, this.updateSignature(this.personalSignature)
async getMySign() { }
const data = await $.get("/mobile/common/signing/getMySign") })
if (data) { .fail(() => {
this.signatureContent = data // 自动预览失败仍可手写;仅主动读取失败时提示,避免阻断业务页面进入。
this.$emit("input", data) if (!previewOnly && request === this.signatureRequest && !this._isDestroyed && !this._isBeingDestroyed) {
} this.$toast.fail("个人签名读取失败,请重新签字")
}
})
.always(() => {
if (request === this.signatureRequest) this.signatureLoading = false
})
} }
}, },
async created() { created() {
//await this.getMySign() // 受控审核页已经维护 sign 层;普通页面由公共组件接入同一个全局历史管理器。
this.$options.popupLayer = window.mobilePopupHistory.createLayer("signature", (visible) => {
this.showSignaturePanel = visible
})
// 旧表单通过 PJAX 替换 DOM 时不会自动销毁 Vue,主动释放签字实例和公共栈回调。
$(document).one("pjax:beforeReplace.mobileSignature" + this._uid, () => this.$destroy())
// 所有页面均按类型预览个人签名,初始化不触发 input、保存或打开签字板。
this.getMySign(true)
}, },
beforeDestroy() {
$(document).off("pjax:beforeReplace.mobileSignature" + this._uid)
if (this.$options.popupLayer) this.$options.popupLayer.dispose()
}
} }
</script> </script>
@@ -95,14 +223,51 @@ module.exports = {
width: 100%; 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; position: relative;
flex-direction: column-reverse; flex-direction: column-reverse;
display: flex; display: flex;
row-gap: 10px; row-gap: 10px;
} }
.main-panel .van-image { .h5-signature .van-image {
min-height: 150px; min-height: 150px;
display: block; display: block;
width: 90%; width: 90%;
@@ -111,7 +276,7 @@ module.exports = {
border-radius: 8px; border-radius: 8px;
} }
.main-panel .open-button { .h5-signature .open-button {
margin: 0 auto; margin: 0 auto;
width: 90%; width: 90%;
height: 36px; height: 36px;
@@ -131,7 +131,8 @@ module.exports = {
if (this.qz) { if (this.qz) {
this.getImageUrl(this.qz) this.getImageUrl(this.qz)
} else { } else {
$.get("/mobile/common/signing/getMySign").then(data => { // 与手机共用分类查询:只读取当前模式对应的个人签名。
$.get("/mobile/common/signing/getMySign", {is_value_base64: this.is_value_base64}).then(data => {
if (data) { if (data) {
this.getImageUrl(data) this.getImageUrl(data)
this.$emit("change", data) this.$emit("change", data)
@@ -101,7 +101,7 @@ layout("/mobile/platform.html"){
left-arrow left-arrow
placeholder placeholder
fixed fixed
@click-left="pjaxReplace('/mobile/index')" @click-left="leavePage"
safe-area-inset-top safe-area-inset-top
></van-nav-bar> ></van-nav-bar>
@@ -135,7 +135,7 @@ layout("/mobile/platform.html"){
@load="onLoad"> @load="onLoad">
<div class="van-doc-card" v-for="o in tableData"> <div class="van-doc-card" v-for="o in tableData">
<div @click="openView(o.id)"> <div @click="openView(o)">
<div style="display: flex; justify-content: space-between"> <div style="display: flex; justify-content: space-between">
<div style="width: 74%"> <div style="width: 74%">
@@ -167,7 +167,7 @@ layout("/mobile/platform.html"){
<van-row> <van-row>
<van-col span="24"> <van-col span="24">
<span style="color: grey">所属单位:</span> <span style="color: grey">所属单位:</span>
<span>{{o.unitname}}</span> <span>{{o.applicantunitname}}</span>
</van-col> </van-col>
</van-row> </van-row>
<van-row> <van-row>
@@ -185,7 +185,7 @@ layout("/mobile/platform.html"){
style="margin-right: 8px; width: auto;"> style="margin-right: 8px; width: auto;">
查看 查看
</van-button> </van-button>
<van-button @click.stop="openAudit(o)" <van-button v-if="o.status == 1" @click.stop="openAudit(o)"
type="primary" size="small" color="#1867b0" type="primary" size="small" color="#1867b0"
style="margin-right: 8px; width: auto;"> style="margin-right: 8px; width: auto;">
审核 审核
@@ -203,16 +203,16 @@ layout("/mobile/platform.html"){
></van-empty> ></van-empty>
</div> </div>
<van-popup v-model="infoShow" position="right" :style="{width: '100%', height: '100%' }"> <van-popup :value="infoShow" :close-on-popstate="false" :close-on-click-overlay="false" position="right" :style="{width: '100%', height: '100%' }">
<van-nav-bar <van-nav-bar
title="详细信息" title="详细信息"
left-arrow left-arrow
placeholder placeholder
fixed fixed
@click-left="infoShow = false" @click-left="closePopup"
safe-area-inset-top safe-area-inset-top
></van-nav-bar> ></van-nav-bar>
<club-user-apply-info ref="applyInfo" :handle="handle"> <club-user-apply-info v-if="infoShow" :key="applyId" :apply-id="applyId" :handle="handle">
<template #handle> <template #handle>
<div class="van-cell-group__title"> <div class="van-cell-group__title">
<div class="van-sidebar-item van-sidebar-item--select"> <div class="van-sidebar-item van-sidebar-item--select">
@@ -243,20 +243,18 @@ layout("/mobile/platform.html"){
<div class="mb10">审核签字</div> <div class="mb10">审核签字</div>
</template> </template>
<template #input> <template #input>
<mobile-sign ref="signature" :my_sign.sync="formData.auditSign"></mobile-sign> <!-- 新版签字组件使用文件模式,回填路径供协会审核保存和详情展示。 -->
<mobile-sign :is_value_base64="false" ref="signature" v-model="formData.auditSign"
:popup-visible="signatureShow" :disabled="formLoading"
@open="openSignature" @close="closePopup"></mobile-sign>
</template> </template>
</van-field> </van-field>
<van-row gutter="20" style="margin-top: 30px;padding: 0 25px"> <!-- 与慰问分工会审核页保持一致:红色拒绝、绿色通过。 -->
<van-col span="12"> <div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button :color="themeColor" plain block @click="doReview(false)">拒 绝 <van-button type="danger" block :loading="formLoading" native-type="button" @click="doReview(false)">拒绝</van-button>
</van-button> <van-button type="primary" block :loading="formLoading" native-type="button" @click="doReview(true)">通过</van-button>
</van-col> </div>
<van-col span="12">
<van-button :color="themeColor" block @click="doReview(true)">通 过
</van-button>
</van-col>
</van-row>
</van-form> </van-form>
</template> </template>
@@ -272,6 +270,8 @@ layout("/mobile/platform.html"){
data() { data() {
return { return {
handle: false, handle: false,
applyId: '',
formLoading: false,
clubOptions: [], clubOptions: [],
userTypeOptions: [], userTypeOptions: [],
auditOptions: [ auditOptions: [
@@ -283,52 +283,71 @@ layout("/mobile/platform.html"){
radioType: 3 radioType: 3
}, },
infoShow: false, infoShow: false,
signatureShow: false,
formData: {}, formData: {},
} }
}, },
components: { components: {
'club-user-apply-info': httpVueLoader('/components/mobile/club/mobileClubUserApplyInfo.vue?v=1.0.1'), 'club-user-apply-info': httpVueLoader('/components/mobile/club/mobileClubUserApplyInfo.vue?v=1.0.4'),
'mobile-sign': httpVueLoader('/components/sign/mobileSign.vue'), 'mobile-sign': httpVueLoader('/components/sign/mobileSign.vue?v=1.0.3'),
}, },
methods: { methods: {
// 使用远端统一历史会话;签字层和详情层由同一回调驱动,避免重复压栈。
syncPopup(layers) {
this.$set(this, 'infoShow', layers.includes('info'))
this.$set(this, 'signatureShow', layers.includes('signature'))
},
closePopup() {
return this.$options.popupSession.close()
},
openSignature() {
this.$options.popupSession.open('signature')
},
leavePage() {
if (this.infoShow || this.signatureShow) return this.closePopup()
this.disposePopup()
pjaxReplace('/mobile/index')
},
disposePopup() {
if (this.$options.popupSession) this.$options.popupSession.dispose()
$(document).off('pjax:beforeSend.clubAudit')
$(document).off('pjax:beforeReplace.clubAudit')
},
openAudit(row) { openAudit(row) {
this.formData = { this.$set(this, 'formData', {
id: row.id, id: row.id,
auditTime: moment().format('YYYY-MM-DD'), auditTime: moment().format('YYYY-MM-DD'),
username: "${@shiro.getPrincipalProperty('username')}", username: "${@shiro.getPrincipalProperty('username')}",
auditOpinion: null, auditOpinion: null,
auditSign: "" auditSign: ''
}
this.infoShow = true
this.handle = true
this.$nextTick(() => {
this.$refs.applyInfo.getInfo(row.id)
}) })
this.$set(this, 'applyId', row.id)
this.$set(this, 'handle', true)
this.$options.popupSession.open('info')
}, },
openView(row) { openView(row) {
this.infoShow = true this.$set(this, 'applyId', row.id)
this.handle = false this.$set(this, 'handle', false)
this.$nextTick(() => { this.$options.popupSession.open('info')
this.$refs.applyInfo.getInfo(row.id)
})
}, },
async doReview(flag) { // id 为申请记录,flag 为是否通过;响应 code 为 0 时清理弹框历史并刷新列表。
if (!this.formData.auditOpinion) { doReview(flag) {
vant.Toast('请填写审核意见'); if (this.formLoading) return
if (!this.formData.auditOpinion || !this.formData.auditOpinion.trim()) {
vant.Toast('请填写审核意见')
return return
} }
if (!this.formData.auditSign) { if (!this.formData.auditSign) {
vant.Toast('请签字') vant.Toast('请签字')
return return
} }
// this.$refs.signature.submitSign() this.$set(this, 'formLoading', true)
this.formData.flag = flag const params = Object.assign({}, this.formData, {flag})
const resp = await $.post("/platform/sys/club/sh/check", this.formData) $.post('/platform/sys/club/sh/check', params).then(resp => {
if (resp.code === 0) { if (resp.code === 0) this.$options.popupSession.clear().then(() => this.doSearch())
this.doSearch() vant.Toast(resp.msg)
this.infoShow = false }).fail(() => vant.Toast('审核提交失败,请重试'))
} .always(() => { this.$set(this, 'formLoading', false) })
vant.Toast(resp.msg)
}, },
doSearch() { doSearch() {
this.pageForm.pageNumber = 1 this.pageForm.pageNumber = 1
@@ -336,7 +355,7 @@ layout("/mobile/platform.html"){
this.finished = false this.finished = false
this.onLoad() this.onLoad()
}, },
async onLoad() { onLoad() {
this.loading = true this.loading = true
$.post('/platform/sys/club/sh/pageData', this.pageForm).then(res => { $.post('/platform/sys/club/sh/pageData', this.pageForm).then(res => {
if (res.code === 0) { if (res.code === 0) {
@@ -347,26 +366,51 @@ layout("/mobile/platform.html"){
this.pageForm.pageNumber++ this.pageForm.pageNumber++
} }
} }
this.loading = false }).always(() => { this.$set(this, 'loading', false) })
})
}, },
async getMyMangeClub() { getMyMangeClub() {
const {data} = await $.get("/platform/sys/club/users/getMyMangeClub") return $.get('/platform/sys/club/users/getMyMangeClub').then(res => res.data || [])
return data
}, },
}, },
async created() { created() {
const data = await this.getMyMangeClub() this.$options.popupSession = window.mobilePopupHistory.register('clubAudit', this.syncPopup)
data.forEach(item => { // PJAX 离开前清理本页历史,再执行原跳转,防止返回到已关闭弹框。
this.clubOptions.push({text: item.name, value: item.id}) $(document).on('pjax:beforeSend.clubAudit', (event, xhr, options) => {
if (this.infoShow || this.signatureShow) {
event.preventDefault()
this.$options.popupSession.clear().then(() => {
this.disposePopup()
pjaxReplace(options.url)
})
return false
}
this.disposePopup()
}) })
this.clubOptions.unshift({text: '全部社团'}) // PJAX 不会自动销毁旧页面实例,替换容器前释放本页会话及事件。
const userType = await getDictOptions('UserType') $(document).one('pjax:beforeReplace.clubAudit', () => this.$destroy())
userType.forEach(item => { this.getMyMangeClub().then(data => {
this.userTypeOptions.push({text: item.name, value: item.code}) this.$set(this, 'clubOptions', [{text: '全部社团'}].concat(
data.map(item => ({text: item.name, value: item.id}))))
}) })
this.userTypeOptions.unshift({text: '人员类型'}) getDictOptions('UserType').then(data => {
this.$set(this, 'userTypeOptions', [{text: '人员类型'}].concat(
data.map(item => ({text: item.name, value: item.code}))))
})
// 首页已办传 audit=1、applyId;复用现有详情弹框和全局历史栈,只读展示审核结果。
if (GetQueryString('audit') === '1') {
this.$set(this.pageForm, 'radioType', 2)
const applyId = GetQueryString('applyId')
if (applyId) this.openView({id: applyId})
}
this.onLoad() this.onLoad()
},
beforeDestroy() {
// 空栈同步注销,防止下一页注册时旧会话尚未释放。
if (this.infoShow || this.signatureShow) {
this.$options.popupSession.clear().then(() => this.disposePopup())
} else {
this.disposePopup()
}
} }
}) })
</script> </script>
@@ -217,7 +217,7 @@ layout("/mobile/platform.html"){
</van-popup> </van-popup>
</div> </div>
<script> <script>
<!--#include('../common/info.js'){}#--> <!--#include('/mobile/difficulty/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
@@ -189,7 +189,7 @@ layout("/mobile/platform.html"){
</van-popup> </van-popup>
</div> </div>
<script> <script>
<!--#include('../common/info.js'){}#--> <!--#include('/mobile/difficulty/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
@@ -219,7 +219,7 @@ layout("/mobile/platform.html"){
</van-popup> </van-popup>
</div> </div>
<script> <script>
<!--#include('../common/info.js'){}#--> <!--#include('/mobile/difficulty/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
@@ -219,7 +219,7 @@ layout("/mobile/platform.html"){
</van-popup> </van-popup>
</div> </div>
<script> <script>
<!--#include('../common/info.js'){}#--> <!--#include('/mobile/difficulty/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
+59 -9
View File
@@ -172,7 +172,7 @@ layout("/mobile/platform.html"){
<div id="app"> <div id="app">
<!--首页--> <!--首页-->
<div style="margin-bottom: 60px" v-if="active === 0"> <div v-if="active === 0">
<!--轮播图--> <!--轮播图-->
<van-swipe :autoplay="3000" class="swiper"> <van-swipe :autoplay="3000" class="swiper">
<van-swipe-item :key="index" v-for="(image, index) in images"> <van-swipe-item :key="index" v-for="(image, index) in images">
@@ -353,7 +353,7 @@ layout("/mobile/platform.html"){
description="暂无待办" description="暂无待办"
image="https://img01.yzcdn.cn/vant/custom-empty-image.png" image="https://img01.yzcdn.cn/vant/custom-empty-image.png"
style="margin-top: 44px" style="margin-top: 44px"
v-if="needItems.length === 0" v-if="needItems.length === 0 && !clubTodoLoading"
></van-empty> ></van-empty>
</div> </div>
</van-tab> </van-tab>
@@ -376,7 +376,7 @@ layout("/mobile/platform.html"){
description="暂无已办" description="暂无已办"
image="https://img01.yzcdn.cn/vant/custom-empty-image.png" image="https://img01.yzcdn.cn/vant/custom-empty-image.png"
style="margin-top: 44px" style="margin-top: 44px"
v-if="completedItems.length === 0" v-if="completedItems.length === 0 && !clubCompletedLoading"
></van-empty> ></van-empty>
</div> </div>
</van-tab> </van-tab>
@@ -385,7 +385,7 @@ layout("/mobile/platform.html"){
</div> </div>
<!--工作台--> <!--工作台-->
<div style="margin-bottom: 60px" v-if="active === 2"> <div v-if="active === 2">
<van-row class="module" v-for="m in moduleMenus"> <van-row class="module" v-for="m in moduleMenus">
<div class="van-sidebar-item van-sidebar-item--select"> <div class="van-sidebar-item van-sidebar-item--select">
{{m.moduleName}} {{m.moduleName}}
@@ -401,7 +401,8 @@ layout("/mobile/platform.html"){
</van-row> </van-row>
</div> </div>
<van-tabbar @change="tabbrChange" v-model="active"> <!-- 按底部导航实际高度(含手机安全区域)占位,确保末行菜单可完整滚动到导航上方。 -->
<van-tabbar @change="tabbrChange" v-model="active" placeholder>
<van-tabbar-item icon="home-o">首页</van-tabbar-item> <van-tabbar-item icon="home-o">首页</van-tabbar-item>
<van-tabbar-item :dot="needCount" icon="envelop-o">我的办理</van-tabbar-item> <van-tabbar-item :dot="needCount" icon="envelop-o">我的办理</van-tabbar-item>
<van-tabbar-item icon="apps-o">工作台</van-tabbar-item> <van-tabbar-item icon="apps-o">工作台</van-tabbar-item>
@@ -509,6 +510,10 @@ layout("/mobile/platform.html"){
active: 0, active: 0,
needCount: 0, needCount: 0,
needItems: [], needItems: [],
clubTodoLoading: false,
clubTodoVersion: 0,
clubCompletedLoading: false,
clubCompletedVersion: 0,
activeName: '0', activeName: '0',
completedItems: [], completedItems: [],
bigPictureList: [], bigPictureList: [],
@@ -554,14 +559,18 @@ layout("/mobile/platform.html"){
tabbrChange(o) { tabbrChange(o) {
console.log(o) console.log(o)
sessionStorage.setItem("zhgh-mobile-home-active",o) sessionStorage.setItem("zhgh-mobile-home-active",o)
this.needItems = []
this.initData() this.initData()
}, },
toUrl(o) { toUrl(o) {
pjaxReplace(o.mobileHref) pjaxReplace(o.mobileHref)
}, },
toHref(o) { toHref(o) {
pjaxReplace(o.mobileUrl + '?audit=1'); // 协会已办携带申请 ID,直接以查看模式打开该申请,不进入待审核列表。
if (o.businessType === 'clubApply') {
pjaxReplace(o.mobileUrl + '?audit=1&applyId=' + encodeURIComponent(o.applyId))
return
}
pjaxReplace(o.mobileUrl + '?audit=1');
}, },
async getNeedItems() { async getNeedItems() {
const {data, code} = await $.get('/platform/needItems/getNeedItems') const {data, code} = await $.get('/platform/needItems/getNeedItems')
@@ -598,14 +607,55 @@ layout("/mobile/platform.html"){
this.needItems = this.needItems.concat(data.filter(v => v.count > 0)) this.needItems = this.needItems.concat(data.filter(v => v.count > 0))
} }
}, },
async initData() { // 无参数;data 返回协会个人待办卡片。每次替换协会卡片,保留其他业务待办。
getClubTodo() {
const version = this.clubTodoVersion + 1
this.$set(this, 'clubTodoVersion', version)
this.$set(this, 'clubTodoLoading', true)
return $.post('/platform/sys/club/sh/mobileTodo', {}).then(res => {
if (version !== this.clubTodoVersion) return
if (res.code === 0) {
const otherItems = this.needItems.filter(item => item.mobileHref !== '/platform/sys/club/sh/mobile')
this.$set(this, 'needItems', otherItems.concat(res.data || []))
this.$set(this, 'needCount', this.needItems.reduce((total, item) => total + (Number(item.count) || 0), 0))
} else {
vant.Toast(res.msg || '协会待办加载失败,请重试')
}
}).fail(() => {
if (version === this.clubTodoVersion) vant.Toast('协会待办加载失败,请重试')
}).always(() => {
if (version === this.clubTodoVersion) this.$set(this, 'clubTodoLoading', false)
})
},
// 已办按实际审核人查询,刷新替换而非追加,避免页签切换产生重复卡片。
getClubCompleted() {
const version = this.clubCompletedVersion + 1
this.$set(this, 'clubCompletedVersion', version)
this.$set(this, 'clubCompletedLoading', true)
return $.post('/platform/sys/club/sh/mobileCompleted', {}).then(res => {
if (version !== this.clubCompletedVersion) return
if (res.code === 0) {
const otherItems = this.completedItems.filter(item => item.businessType !== 'clubApply')
this.$set(this, 'completedItems', otherItems.concat(res.data || []))
} else {
vant.Toast(res.msg || '协会已办加载失败,请重试')
}
}).fail(() => {
if (version === this.clubCompletedVersion) vant.Toast('协会已办加载失败,请重试')
}).always(() => {
if (version === this.clubCompletedVersion) this.$set(this, 'clubCompletedLoading', false)
})
},
initData() {
// await this.getNeedItems(); // await this.getNeedItems();
// await this.getMemberCheckSelfAgenda() // await this.getMemberCheckSelfAgenda()
// await this.getWelfareCheckSelfAgenda() // await this.getWelfareCheckSelfAgenda()
// await this.getCompletes(); // await this.getCompletes();
// await this.getMsg() // await this.getMsg()
this.listActivity(); this.listActivity();
this.needCount = this.needItems.length // 首页首次进入、办理页签切换及 pageshow 返回均走此入口,刷新已处理数量。
this.getClubTodo()
this.getClubCompleted()
}, },
async setNeedItems() { async setNeedItems() {
const {data, code} = await $.get('/platform/needItems/getNeedItems') const {data, code} = await $.get('/platform/needItems/getNeedItems')
@@ -0,0 +1,18 @@
<!--#
layout("/mobile/platform.html"){
#-->
<!--#include('/mobile/jf/activitybx/review/index.html'){}#-->
<script>
// 节点配置由当前页面固定提供,业务权限及状态仍由服务端校验。
const activityReviewConfig = {
stage: 'accounting', title: '校工会负责人审核', state: 1020,
confirmMessage: '通过后将转交校工会主席审核。',
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
}
<!--#include('/mobile/jf/activitybx/review/info.js'){}#-->
<!--#include('/mobile/jf/activitybx/review/page.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,137 @@
<!--#
layout("/mobile/platform.html"){
#-->
<style>
.association-page { min-height: 100vh; background: #f6f7f9; color: #323233; font-size: 13px; }
.association-page .van-nav-bar__title { font-size: 15px; font-weight: 400; }
.association-filters .van-dropdown-menu__bar { box-shadow: none; border-top: 1px solid #f8f8f8; border-bottom: 1px solid #f8f8f8; }
.association-filters .van-dropdown-menu__item { flex: 1; min-width: 0; }
.association-filters .van-dropdown-menu__title { max-width: calc(100% - 40px); padding: 0 8px; font-size: 14px; }
.association-filters .van-dropdown-item__content { max-height: 70vh; border-radius: 0 0 15px 15px; }
.association-list { padding: 2px 0 12px; }
.association-card { margin: 12px 13px 0; padding: 11px 12px 10px; background: white; border-radius: 10px; box-shadow: 0 5px 12px rgba(30,50,70,.04); }
.association-card h3 { position: relative; margin: 0 0 7px; color: #086bc3; font-size: 15px; line-height: 21px; font-weight: 600; overflow-wrap: anywhere; }
.association-card h3:before { content: ''; position: absolute; left: -10px; top: 3px; height: 15px; border-left: 2px solid #1989fa; }
.association-card p { margin: 0; line-height: 26px; color: #85878d; overflow-wrap: anywhere; }
.association-card label { color: #242424; }
.association-card .association-pair { display: flex; gap: 10px; justify-content: space-between; }
.association-pair span:last-child { flex-shrink: 0; }
.association-card-footer { display: flex; align-items: center; gap: 8px; margin-top: 7px; padding-top: 7px; border-top: 1px solid #f5f5f5; }
.association-state { flex: 1; min-width: 0; font-size: 12px; color: #b7790c; overflow-wrap: anywhere; }
.association-card-footer .van-button { height: 25px; padding: 0 8px; font-size: 13px; border-radius: 6px; flex-shrink: 0; }
.association-popup { display: flex; flex-direction: column; overflow: hidden; background: #f6f7f9; height: 100%; width: 100%; }
.association-popup > .van-nav-bar { flex-shrink: 0; }
.association-detail-body { flex: 1; overflow-y: auto; min-height: 0; padding-bottom: 12px; }
.association-section { margin: 12px; padding: 10px 0; border-radius: 10px; background: white; overflow: hidden; }
.association-section h3 { margin: 4px 14px 8px; font-size: 15px; color: #1768b0; }
.association-section .van-cell { font-size: 13px; padding: 8px 14px; }
.association-section .van-cell__title { flex: 0 0 88px; }
.association-section .van-cell__value { white-space: pre-wrap; overflow-wrap: anywhere; }
.association-section .association-file .van-cell__title { flex: 1; overflow-wrap: anywhere; }
.association-section .association-file .van-cell__value { flex: 0 0 40px; }
.association-content-image { display: block; max-width: calc(100% - 28px); margin: 10px 14px; }
.association-description { padding: 0 14px; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; }
.association-muted { margin: 10px 14px; color: #969799; }
.association-total .van-cell__value { color: #086bc3; font-weight: 600; }
/* 共用详情中的预算分组沿用申报审核页的分隔样式。 */
.association-budget-item + .association-budget-item { border-top: 1px solid #f2f3f5; }
.association-sign-image { display: block; max-width: calc(100% - 28px); width: 240px; height: 100px; object-fit: contain; margin: 8px 14px; }
.association-actions { flex-shrink: 0; display: flex; gap: 12px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); background: white; border-top: 1px solid #eee; z-index: 2; }
.association-actions .van-button { flex: 1; border-radius: 6px; height: 40px; }
.association-section .association-sign-field { display: block; }
.association-sign-field .van-field__label { width: auto; margin: 0 0 10px; }
.association-sign-field .main-panel .van-image { width: 100%; height: 112px; min-height: 112px; overflow: hidden; border-radius: 6px; }
.association-sign-field .main-panel .open-button { width: 100%; border-radius: 6px; }
.association-preview { display: block; width: 100%; height: calc(100% - 46px); border: none; background: white; }
.association-preview-image { width: 100%; height: calc(100% - 46px); object-fit: contain; }
.association-loading { padding: 30px; text-align: center; }
</style>
<div id="app" class="association-page" v-cloak>
<van-sticky>
<van-nav-bar title="协会活动审核" left-arrow @click-left="back"></van-nav-bar>
<van-dropdown-menu class="association-filters" active-color="#1867b0"
:close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item ref="yearFilter" :options="yearOptions" :value="pageForm.year" :disabled="filterClosing"
@change="selectFilter('year', $event)"
@open="setFilterOpen('year', true)" @close="setFilterOpen('year', false)"></van-dropdown-item>
<van-dropdown-item ref="clubIdFilter" :options="clubs" :value="pageForm.clubId" :disabled="filterClosing"
@change="selectFilter('clubId', $event)"
@open="setFilterOpen('clubId', true)" @close="setFilterOpen('clubId', false)">
<van-loading v-if="clubsLoading" class="association-loading">加载协会中</van-loading>
</van-dropdown-item>
<van-dropdown-item ref="isAuditFilter" :options="auditOptions" :value="pageForm.isAudit" :disabled="filterClosing"
@change="selectFilter('isAudit', $event)"
@open="setFilterOpen('isAudit', true)" @close="setFilterOpen('isAudit', false)"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<van-pull-refresh :value="refreshing" @input="setRefreshing" @refresh="doSearch">
<van-list :value="tableLoading" :finished="finished" :error="listError" error-text="加载失败,点击重试"
:finished-text="tableData.length ? '没有更多了' : ''" :immediate-check="false"
@input="setTableLoading" @update:error="setListError" @load="pageData" class="association-list">
<article v-for="item in tableData" :key="item.id" class="association-card">
<h3>{{ item.activity_name }}</h3>
<p><label>申请人:</label>{{ item.user_name }}<span v-if="item.bxr_loginname">({{ item.bxr_loginname }})</span></p>
<p><label>所属协会:</label>{{ item.clubName || item.fundsUnitName || '—' }}</p>
<p class="association-pair"><span><label>预算:</label>¥{{ money(item.funds_money) }}</span><span><label>申请:</label>{{ shortDate(item.apply_time) }}</span></p>
<div class="association-card-footer">
<span class="association-state" :style="{color: item.state_color || '#b7790c'}">{{ item.state_name || '待协会负责人审核' }}</span>
<van-button type="info" size="small" :disabled="formLoading" @click="openDetail(item.id, false)">查看</van-button>
<van-button v-if="Number(item.state_id) === 1000" type="primary" size="small" :disabled="formLoading" @click="openDetail(item.id, true)">审核</van-button>
</div>
</article>
<van-empty v-if="!tableLoading && !listError && !tableData.length" description="暂无申请"></van-empty>
</van-list>
</van-pull-refresh>
<van-popup :value="popups.detail" position="right" class="association-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<van-nav-bar :title="auditMode ? '协会负责人审核' : '申请详情'" left-arrow @click-left="closePopup"></van-nav-bar>
<div class="association-detail-body">
<van-loading v-if="detailLoading" class="association-loading">加载申请中</van-loading>
<van-empty v-else-if="!detail.id" description="详情加载失败">
<van-button size="small" type="info" @click="loadDetail(detailId)">重新加载</van-button>
</van-empty>
<association-info v-else :data="detail" @preview="previewFile">
<section v-if="auditMode && detail.canReview" class="association-section">
<h3>审核信息</h3>
<van-field label="审核人" :value="reviewerName" readonly></van-field>
<van-field label="审核时间" :value="reviewDate" readonly></van-field>
<van-field label="审核意见" :value="formData.idea" @input="setIdea" type="textarea" rows="3"
maxlength="500" show-word-limit required placeholder="请填写审核意见" :disabled="formLoading"></van-field>
<van-field label="审核签字" required class="association-sign-field">
<template #input>
<mobile-sign :value="formData.sign" @input="setSignature" :disabled="formLoading"
:save-to-profile="false" :popup-visible="popups.sign" panel-title="审核签字"
@open="openSignature" @close="closePopup"></mobile-sign>
</template>
</van-field>
</section>
</association-info>
</div>
<div v-if="detail.id && !detailLoading" class="association-actions">
<template v-if="auditMode && detail.canReview">
<van-button type="danger" plain :disabled="formLoading" @click="confirmReview(false)">拒绝</van-button>
<van-button type="primary" :loading="formLoading" :disabled="formLoading" @click="confirmReview(true)">通过</van-button>
</template>
<van-button v-else-if="detail.canReview" type="primary" @click="startAudit">去审核</van-button>
<van-button v-else @click="closePopup">关闭</van-button>
</div>
</van-popup>
<van-dialog :value="popups.confirm" :title="reviewFlag ? '确认通过' : '确认拒绝'"
:message="reviewFlag ? '通过后将转交校工会负责人审核。' : '拒绝后将通知申请人,请核对审核意见。'"
show-cancel-button :allow-html="false" :close-on-click-overlay="false" :close-on-popstate="false"
:before-close="beforeReviewClose"></van-dialog>
<van-popup :value="popups.preview" position="right" class="association-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<van-nav-bar title="附件查看" left-arrow @click-left="closePopup"></van-nav-bar>
<img v-if="popups.preview && previewIsImage" :src="previewUrl" class="association-preview-image" alt="申请附件">
<iframe v-else-if="popups.preview" :src="previewUrl" title="申请附件" class="association-preview"></iframe>
</van-popup>
</div>
<script>
<!--#include('/mobile/jf/activitybx/review/info.js'){}#-->
<!--#include('page.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,272 @@
// 独立手机审核页面;不复用旧 declare 页面及其有缺陷的滚动分页。
const vue = new Vue({
el: '#app',
components: {'association-info': activityReviewInfo},
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')
}
})
@@ -0,0 +1,18 @@
<!--#
layout("/mobile/platform.html"){
#-->
<!--#include('/mobile/jf/activitybx/review/index.html'){}#-->
<script>
// 节点配置由当前页面固定提供,业务权限及状态仍由服务端校验。
const activityReviewConfig = {
stage: 'division', title: '基层工会主席审核', state: 1010,
confirmMessage: '通过后将转交校工会负责人审核。',
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
}
<!--#include('/mobile/jf/activitybx/review/info.js'){}#-->
<!--#include('/mobile/jf/activitybx/review/page.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,223 @@
<style>
.review-page { min-height: 100vh; background: #f6f7f9; color: #323233; font-size: 13px; }
.review-page .van-nav-bar__title { font-size: 15px; font-weight: 400; }
.review-filters .van-dropdown-menu__bar { box-shadow: none; border-top: 1px solid #f8f8f8; border-bottom: 1px solid #f8f8f8; }
.review-filters .van-dropdown-menu__item { flex: 1; min-width: 0; }
.review-filters .van-dropdown-menu__title { max-width: calc(100% - 40px); padding: 0 8px; font-size: 14px; }
.review-filters .van-dropdown-item__content { max-height: 70vh; border-radius: 0 0 15px 15px; }
.review-list { padding: 2px 0 12px; }
.review-card { margin: 12px 13px 0; padding: 11px 12px 10px; background: white; border-radius: 10px; box-shadow: 0 5px 12px rgba(30,50,70,.04); }
.review-card h3 { position: relative; margin: 0 0 7px; color: #086bc3; font-size: 15px; line-height: 21px; font-weight: 600; overflow-wrap: anywhere; }
.review-card h3:before { content: ''; position: absolute; left: -10px; top: 3px; height: 15px; border-left: 2px solid #1989fa; }
.review-card p { margin: 0; line-height: 26px; color: #85878d; overflow-wrap: anywhere; }
.review-card label { color: #242424; }
.review-card .review-pair { display: flex; gap: 10px; justify-content: space-between; }
.review-pair span:last-child { flex-shrink: 0; }
.review-card-footer { display: flex; align-items: center; gap: 8px; margin-top: 7px; padding-top: 7px; border-top: 1px solid #f5f5f5; }
.review-state { flex: 1; min-width: 0; font-size: 12px; color: #b7790c; overflow-wrap: anywhere; }
.review-card-footer .van-button { height: 25px; padding: 0 8px; font-size: 13px; border-radius: 6px; flex-shrink: 0; }
.review-popup { display: flex; flex-direction: column; overflow: hidden; background: #f6f7f9; height: 100%; width: 100%; }
.review-popup > .van-nav-bar { flex-shrink: 0; }
.review-detail-body { flex: 1; overflow-y: auto; min-height: 0; padding-bottom: 12px; }
.review-section { margin: 12px; padding: 10px 0; border-radius: 10px; background: white; overflow: hidden; }
.review-section h3 { margin: 4px 14px 8px; font-size: 15px; color: #1768b0; }
.review-section .van-cell { font-size: 13px; padding: 8px 14px; }
.review-section .van-cell__title { flex: 0 0 88px; }
.review-section .van-cell__value { white-space: pre-wrap; overflow-wrap: anywhere; }
.review-section .review-file .van-cell__title { flex: 1; overflow-wrap: anywhere; }
.review-section .review-file .van-cell__value { flex: 0 0 40px; }
.review-content-image { display: block; max-width: calc(100% - 28px); margin: 10px 14px; }
.review-description { padding: 0 14px; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; }
.review-muted { margin: 10px 14px; color: #969799; }
.review-total .van-cell__value { color: #086bc3; font-weight: 600; }
.review-sign-image { display: block; max-width: calc(100% - 28px); width: 240px; height: 100px; object-fit: contain; margin: 8px 14px; }
.review-actions { flex-shrink: 0; display: flex; gap: 12px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); background: white; border-top: 1px solid #eee; z-index: 2; }
.review-actions .van-button { flex: 1; border-radius: 6px; height: 40px; }
.review-section .review-sign-field { display: block; }
.review-sign-field .van-field__label { width: auto; margin: 0 0 10px; }
.review-sign-field .main-panel .van-image { width: 100%; height: 112px; min-height: 112px; overflow: hidden; border-radius: 6px; }
.review-sign-field .main-panel .open-button { width: 100%; border-radius: 6px; }
.review-preview { display: block; width: 100%; height: calc(100% - 46px); border: none; background: white; }
.review-preview-image { width: 100%; height: calc(100% - 46px); object-fit: contain; }
.review-loading { padding: 30px; text-align: center; }
.review-more-bar { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 7px 13px; background: white; color: #969799; font-size: 12px; }
.review-more-bar > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.review-more-popup, .review-choice-popup { display: flex; flex-direction: column; max-height: 80vh; background: #f7f8fa; }
.review-choice-popup { height: 60vh; }
.review-more-body { flex: 1; overflow-y: auto; min-height: 0; }
/* 筛选面板独立限定样式,避免影响详情、审核按钮和其他公共弹框。 */
.review-more-popup { background: #fff; border-radius: 18px 18px 0 0; overflow: hidden; }
.review-more-handle { flex-shrink: 0; width: 38px; height: 4px; margin: 8px auto 0; border-radius: 4px; background: #d5d8df; }
.review-more-header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; padding: 4px 12px 8px 18px; }
.review-more-header h2 { margin: 0; font-size: 18px; line-height: 32px; font-weight: 600; color: #20242d; }
.review-more-close.van-button { width: 36px; height: 36px; padding: 0; border: 0; color: #92969f; background: transparent; font-size: 20px; }
.review-more-popup > .review-more-body { flex: 0 1 auto; padding: 0 18px 14px; }
.review-filter-group + .review-filter-group { margin-top: 16px; }
.review-filter-label { margin: 0 0 8px; color: #252a34; font-size: 14px; line-height: 20px; font-weight: 600; }
.review-filter-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
.review-filter-row .review-filter-label { margin: 0; }
.review-unit-select.van-cell { align-items: center; padding: 8px 12px; border-radius: 8px; background: #f5f6f8; font-size: 13px; }
.review-unit-select .van-cell__title { min-width: 0; overflow-wrap: anywhere; }
.review-unit-select:after, .review-filter-input:after { display: none; }
.review-filter-input.van-cell { padding: 8px 12px; border-radius: 8px; background: #f5f6f8; font-size: 13px; }
.review-filter-input .van-field__left-icon { color: #969ba4; margin-right: 8px; }
.review-segment { display: flex; flex-shrink: 0; gap: 2px; padding: 2px; border-radius: 8px; background: #f5f6f8; }
.review-filter-option.van-button { min-width: 0; height: 30px; padding: 0 8px; border: 1px solid transparent; border-radius: 7px; background: #f5f6f8; color: #333943; font-size: 12px; }
.review-segment .review-filter-option { min-width: 58px; height: 28px; }
.review-filter-option.is-selected { background: #eaf3ff; border-color: #1989fa; color: #1989fa; font-weight: 500; }
.review-segment .review-filter-option.is-selected { border-color: transparent; }
.review-sort-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.review-sort-options .review-filter-option { position: relative; width: 100%; height: 30px; padding: 0 4px; white-space: nowrap; }
.review-sort-options .is-selected { padding-right: 15px; }
.review-sort-check { position: absolute; right: 3px; top: 50%; transform: translateY(-50%); font-size: 12px; }
.review-sort-direction { margin: 14px 0 0; }
.review-sort-direction .review-segment { background: transparent; gap: 6px; padding: 0; }
.review-more-footer { display: flex; flex-shrink: 0; gap: 10px; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); border-top: 1px solid #f1f2f5; background: #fff; }
.review-more-footer .van-button { flex: 1; height: 40px; border-radius: 8px; font-size: 14px; }
.review-more-footer .van-button--info { flex: 2; }
.review-more-footer .van-button--default { color: #60656e; border-color: #e4e7ed; }
.review-choice-popup { background: #fff; }
.review-choice-popup .van-search { padding: 8px 16px; }
.review-choice-popup .van-search__content { border-radius: 8px; background: #f5f6f8; }
.review-choice-popup .van-cell { padding: 12px 18px; font-size: 13px; }
.review-choice-popup .review-choice-selected { color: #1989fa; background: #f0f6ff; }
.review-budget-item + .review-budget-item { border-top: 1px solid #f2f3f5; }
</style>
<div id="app" class="review-page" v-cloak>
<van-sticky>
<van-nav-bar :title="config.title" left-arrow @click-left="back"></van-nav-bar>
<van-dropdown-menu class="review-filters" active-color="#1867b0"
:close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item ref="yearFilter" :options="yearOptions" :value="pageForm.year" :disabled="filterClosing"
@change="selectFilter('year', $event)"
@open="setFilterOpen('year', true)" @close="setFilterOpen('year', false)"></van-dropdown-item>
<van-dropdown-item ref="organizationFilter" :options="organizationOptions" :value="pageForm[organizationKey]" :disabled="filterClosing"
@change="selectFilter(organizationKey, $event)"
@open="setFilterOpen('organization', true)" @close="setFilterOpen('organization', false)">
<van-loading v-if="organizationsLoading" class="review-loading">加载中</van-loading>
</van-dropdown-item>
<van-dropdown-item ref="isAuditFilter" :options="auditOptions" :value="pageForm.isAudit" :disabled="filterClosing"
@change="selectFilter('isAudit', $event)"
@open="setFilterOpen('isAudit', true)" @close="setFilterOpen('isAudit', false)"></van-dropdown-item>
</van-dropdown-menu>
<div class="review-more-bar"><span>{{ searchSummary }}</span><van-button size="mini" plain type="info" @click="openMore">更多筛选</van-button></div>
</van-sticky>
<van-pull-refresh :value="refreshing" @input="setRefreshing" @refresh="doSearch">
<van-list :value="tableLoading" :finished="finished" :error="listError" error-text="加载失败,点击重试"
:finished-text="tableData.length ? '没有更多了' : ''" :immediate-check="false"
@input="setTableLoading" @update:error="setListError" @load="pageData" class="review-list">
<article v-for="item in tableData" :key="item.id" class="review-card">
<h3>{{ item.activity_name }}</h3>
<p><label>申请人:</label>{{ item.user_name }}<span v-if="item.bxr_loginname">({{ item.bxr_loginname }})</span></p>
<p><label>所属工会:</label>{{ item.bxr_unionname || item.fundsUnitName || '—' }}</p>
<p class="review-pair"><span><label>预算:</label>¥{{ money(item.funds_money) }}</span><span><label>申请:</label>{{ shortDate(item.apply_time) }}</span></p>
<div class="review-card-footer">
<span class="review-state" :style="{color: item.state_color || '#b7790c'}">{{ item.state_name || config.title }}</span>
<van-button type="info" size="small" :disabled="formLoading" @click="openDetail(item.id, false)">查看</van-button>
<van-button v-if="Number(item.state_id) === config.state" type="primary" size="small" :disabled="formLoading" @click="openDetail(item.id, true)">审核</van-button>
</div>
</article>
<van-empty v-if="!tableLoading && !listError && !tableData.length" description="暂无申请"></van-empty>
</van-list>
</van-pull-refresh>
<van-popup :value="popups.detail" position="right" class="review-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<van-nav-bar :title="auditMode ? config.title : '申请详情'" left-arrow @click-left="closePopup"></van-nav-bar>
<div class="review-detail-body">
<van-loading v-if="detailLoading" class="review-loading">加载申请中</van-loading>
<van-empty v-else-if="!detail.id" description="详情加载失败">
<van-button size="small" type="info" @click="loadDetail(detailId)">重新加载</van-button>
</van-empty>
<review-info v-else :data="detail" @preview="previewFile">
<section v-if="auditMode && detail.canReview" class="review-section">
<h3>审核信息</h3>
<van-field label="审核人" :value="reviewerName" readonly></van-field>
<van-field label="审核时间" :value="reviewDate" readonly></van-field>
<van-field label="审核意见" :value="formData.idea" @input="setIdea" type="textarea" rows="3"
maxlength="500" show-word-limit required placeholder="请填写审核意见" :disabled="formLoading"></van-field>
<van-field label="审核签字" required class="review-sign-field">
<template #input>
<mobile-sign :value="formData.sign" @input="setSignature" :disabled="formLoading"
:save-to-profile="false" :popup-visible="popups.sign" panel-title="审核签字"
@open="openSignature" @close="closePopup"></mobile-sign>
</template>
</van-field>
</section>
</review-info>
</div>
<div v-if="detail.id && !detailLoading" class="review-actions">
<template v-if="auditMode && detail.canReview">
<van-button type="danger" plain :disabled="formLoading" @click="confirmReview(false)">拒绝</van-button>
<van-button type="primary" :loading="formLoading" :disabled="formLoading" @click="confirmReview(true)">通过</van-button>
</template>
<van-button v-else-if="detail.canReview" type="primary" @click="startAudit">去审核</van-button>
<van-button v-else @click="closePopup">关闭</van-button>
</div>
</van-popup>
<van-dialog :value="popups.confirm" :title="reviewFlag ? '确认通过' : '确认拒绝'"
:message="reviewFlag ? config.confirmMessage : '拒绝后将通知申请人,请核对审核意见。'"
show-cancel-button :allow-html="false" :close-on-click-overlay="false" :close-on-popstate="false"
:before-close="beforeReviewClose"></van-dialog>
<van-popup :value="popups.preview" position="right" class="review-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<van-nav-bar title="附件查看" left-arrow @click-left="closePopup"></van-nav-bar>
<img v-if="popups.preview && previewIsImage" :src="previewUrl" class="review-preview-image" alt="申请附件">
<iframe v-else-if="popups.preview" :src="previewUrl" title="申请附件" class="review-preview"></iframe>
</van-popup>
<van-popup :value="popups.more" position="bottom" round class="review-more-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<div class="review-more-handle" aria-hidden="true"></div>
<header class="review-more-header">
<h2>更多筛选</h2>
<van-button icon="cross" class="review-more-close" aria-label="关闭更多筛选" @click="closePopup"></van-button>
</header>
<div class="review-more-body">
<section v-if="config.canChooseUnion" class="review-filter-group">
<h3 class="review-filter-label">所属单位</h3>
<van-cell :title="optionText('unitId', moreForm.unitId)" class="review-unit-select" is-link @click="openChoice('unitId')"></van-cell>
</section>
<section class="review-filter-group">
<div class="review-filter-row">
<h3 class="review-filter-label">查询人员</h3>
<div class="review-segment" role="group" aria-label="查询类型">
<van-button v-for="option in choiceOptions.searchName" :key="option.value" class="review-filter-option"
:class="{'is-selected': moreForm.searchName === option.value}" :aria-pressed="moreForm.searchName === option.value"
@click="setMoreField('searchName', option.value)">{{ option.text }}</van-button>
</div>
</div>
<van-field class="review-filter-input" left-icon="search" :value="moreForm.searchKeyword"
@input="setMoreField('searchKeyword', $event)" maxlength="100" clearable
:placeholder="moreSearchPlaceholder" :aria-label="moreSearchPlaceholder"></van-field>
</section>
<section class="review-filter-group">
<h3 class="review-filter-label">排序方式</h3>
<div class="review-sort-options" role="group" aria-label="排序字段">
<van-button v-for="option in choiceOptions.pageOrderName" :key="option.value" class="review-filter-option"
:class="{'is-selected': moreForm.pageOrderName === option.value}" :aria-pressed="moreForm.pageOrderName === option.value"
@click="setMoreField('pageOrderName', option.value)">
{{ option.text }}<van-icon v-if="moreForm.pageOrderName === option.value" name="success" class="review-sort-check"></van-icon>
</van-button>
</div>
<div v-if="moreForm.pageOrderName" class="review-filter-row review-sort-direction">
<h3 class="review-filter-label">排列顺序</h3>
<div class="review-segment" role="group" aria-label="排列顺序">
<van-button v-for="option in choiceOptions.pageOrderBy" :key="option.value" class="review-filter-option"
:class="{'is-selected': moreForm.pageOrderBy === option.value}" :aria-pressed="moreForm.pageOrderBy === option.value"
@click="setMoreField('pageOrderBy', option.value)">{{ option.text }}</van-button>
</div>
</div>
</section>
</div>
<div class="review-more-footer"><van-button @click="resetMore">重置</van-button><van-button type="info" @click="applyMore">确定筛选</van-button></div>
</van-popup>
<van-popup :value="popups.choice" position="bottom" round class="review-choice-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<van-nav-bar :title="choiceTitle" left-text="返回" @click-left="closePopup"></van-nav-bar>
<van-search v-if="choiceKey === 'unitId'" :value="choiceQuery" @input="setChoiceQuery" placeholder="搜索单位"></van-search>
<div class="review-more-body">
<van-cell v-for="option in filteredChoices" :key="option.value" :title="option.text" :class="{'review-choice-selected': moreForm[choiceKey] === option.value}" clickable @click="selectChoice(option.value)">
<van-icon v-if="moreForm[choiceKey] === option.value" name="success" color="#1989fa"></van-icon>
</van-cell>
<van-empty v-if="!filteredChoices.length" description="暂无选项"></van-empty>
</div>
</van-popup>
</div>
@@ -0,0 +1,231 @@
/** 四个活动申报审核入口共用详情;可选字段按返回数据显示,审核表单通过末尾插槽传入。 */
const activityReviewInfo = {
template: /*language=HTML*/ `
<!-- 同时保留两套入口的既有样式类,详情内容和显隐规则共用一份。 -->
<div class="review-info association-info">
<section class="review-section association-section">
<h3>{{ data.activity_name }}</h3>
<van-cell title="当前状态" :value="data.state_name"></van-cell>
<van-cell title="申请人" :value="data.user_name + '(' + (data.loginname || '—') + ')'"></van-cell>
<!-- 可选组织和类型字段由详情数据决定显隐,不根据审核入口切换展示模式。 -->
<van-cell v-if="data.bxr_unionname && String(data.bxr_unionname).trim()" title="所属工会" :value="data.bxr_unionname"></van-cell>
<van-cell v-if="data.bxr_unitname && String(data.bxr_unitname).trim()" title="所属单位" :value="data.bxr_unitname"></van-cell>
<van-cell v-if="data.activity_type" title="活动主体" :value="activityType"></van-cell>
<van-cell v-if="data.apply_type" title="申请类型" :value="Number(data.apply_type) === 1 ? '经费申报' : '报销申请'"></van-cell>
<van-cell title="联系方式" :value="data.mobile || '—'"></van-cell>
<van-cell title="申请时间" :value="data.apply_time || '—'"></van-cell>
<van-cell :title="Number(data.activity_type) === 40003 ? '承办协会' : '承办单位/协会'" :value="data.clubName || data.fundsUnitName || '—'"></van-cell>
<van-cell title="项目编号" :value="data.projectCode || '—'"></van-cell>
<van-cell title="经费类型" :value="data.projectJfName || '—'"></van-cell>
<van-cell title="计划时间" :value="(data.startPlannedDate || '—') + ' 至 ' + (data.endPlannedDate || '—')"></van-cell>
<van-cell v-if="data.startTime || data.endTime" title="活动时间" :value="(data.startTime || '—') + ' 至 ' + (data.endTime || '—')"></van-cell>
<!-- 零元是有效费用;仅未提供金额时隐藏,不能用金额真假判断。 -->
<van-cell v-if="data.activity_money != null && String(data.activity_money).trim() !== ''" title="实际费用" :value="'¥' + money(data.activity_money)"></van-cell>
<van-cell title="活动地点" :value="data.address || '—'"></van-cell>
<van-cell title="活动人数" :value="data.activity_number || '—'"></van-cell>
</section>
<section class="review-section association-section">
<h3>活动方案及简介</h3>
<div class="review-description association-description" style="white-space:normal;min-width:0"
v-html="activityHtml || '暂无'" @click="previewActivityImage"></div>
</section>
<section class="review-section association-section">
<h3>预算明细</h3>
<div v-for="(item, index) in budgets" :key="index" class="review-budget-item association-budget-item">
<van-cell :title="item.name || '费用项目'" :value="'¥' + money(item.budgetPrice)"></van-cell>
<template v-if="data.reimbursement_state_id">
<van-cell v-if="item.actualPrice != null && String(item.actualPrice).trim() !== ''" title="实际费用" :value="'¥' + money(item.actualPrice)"></van-cell>
<van-cell v-if="item.username" title="收款人" :value="item.username"></van-cell>
<van-cell v-if="item.bankName" title="支行名称" :value="item.bankName"></van-cell>
<van-cell v-if="item.bankCardNum" title="报销卡号" :value="item.bankCardNum"></van-cell>
<van-cell v-if="item.remark" title="备注" :value="item.remark"></van-cell>
</template>
</div>
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="review-total association-total"></van-cell>
</section>
<section class="review-section association-section">
<h3>申请附件</h3>
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="review-file association-file"
value="查看" is-link @click="$emit('preview', file)"></van-cell>
<p v-if="!files.length" class="review-muted association-muted">暂无附件</p>
<h3>申请人签字</h3>
<img v-if="data.us_s" class="review-sign-image association-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
<p v-else class="review-muted association-muted">暂无签字</p>
</section>
<section v-for="group in reimbursementFiles" :key="group.key" class="review-section association-section">
<h3>{{ group.title }}</h3>
<van-cell v-for="(file, index) in group.files" :key="file.id || index" :title="file.filename || file.name || '附件'"
class="review-file association-file" value="查看" is-link @click="$emit('preview', file)"></van-cell>
<p v-if="!group.files.length" class="review-muted association-muted">暂无附件</p>
</section>
<section v-for="audit in audits" :key="audit.key" class="review-section association-section">
<h3>{{ audit.title }}</h3>
<van-cell title="审核人" :value="audit.name || '—'"></van-cell>
<van-cell title="审核时间" :value="audit.time || '—'"></van-cell>
<van-cell title="审核意见" :value="audit.idea || '暂无'"></van-cell>
<img v-if="audit.sign" class="review-sign-image association-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
</section>
<slot></slot>
</div>
`,
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])}))
.filter((group) => group.files.length)
},
budgets() {
return this.asList(this.data.budgets)
},
files() {
return this.asList(this.data.files)
},
activityHtml() {
// activity_content 为历史富文本;只重建允许的内容节点,不复制脚本、事件、类名或任意内联样式。
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
const container = content.createElement('div')
const allowed = new Set(['P', 'DIV', 'SPAN', 'BR', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'DEL', 'SUB', 'SUP',
'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'PRE', 'CODE', 'HR',
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'IMG', 'FONT', 'MARK'])
const blocked = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'SVG', 'MATH', 'TEMPLATE',
'FORM', 'INPUT', 'BUTTON', 'TEXTAREA', 'SELECT', 'LINK', 'META', 'BASE'])
const appendContent = (source, parent) => {
if (source.nodeType === 3) {
parent.appendChild(content.createTextNode(source.textContent))
return
}
if (source.nodeType !== 1 || blocked.has(source.tagName)) return
const tag = source.tagName
if (!allowed.has(tag)) {
Array.from(source.childNodes).forEach((child) => appendContent(child, parent))
return
}
const node = content.createElement(tag === 'FONT' ? 'span' : tag.toLowerCase())
// 仅保留文字强调、颜色和对齐;固定宽高、定位和 Word 段落间距不带入手机页面。
const textStyles = ['color', 'backgroundColor', 'fontWeight', 'fontStyle', 'textDecorationLine', 'textAlign', 'verticalAlign']
textStyles.forEach((name) => { node.style[name] = source.style[name] })
if (tag === 'FONT' && !node.style.color) node.style.color = source.getAttribute('color') || ''
if (!node.style.textAlign && /^(left|center|right|justify)$/i.test(source.getAttribute('align') || '')) {
node.style.textAlign = source.getAttribute('align')
}
if (/^(P|DIV|H[1-6]|BLOCKQUOTE|PRE|UL|OL)$/.test(tag)) {
node.style.margin = '0 0 8px'
node.style.lineHeight = '1.8'
}
if (/^H[1-6]$/.test(tag)) node.style.fontSize = '16px'
if (tag === 'UL' || tag === 'OL') {
node.style.paddingLeft = '24px'
node.style.listStyleType = tag === 'UL' ? 'disc' : 'decimal'
if (tag === 'OL' && /^-?\d{1,6}$/.test(source.getAttribute('start') || '')) {
node.setAttribute('start', source.getAttribute('start'))
}
}
if (tag === 'PRE') {
node.style.whiteSpace = 'pre-wrap'
node.style.overflowWrap = 'anywhere'
}
if (tag === 'IMG') {
// 图片沿用原有地址范围;保留正文中的位置,点击后交给现有预览弹框和历史栈。
const url = (source.getAttribute('src') || '').trim()
if (!/^(https?:\/\/|\/(?![\/\\])|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url)) return
node.setAttribute('src', url)
node.setAttribute('alt', source.getAttribute('alt') || '活动方案配图')
node.style.cssText = 'display:block;max-width:100%;height:auto;margin:8px 0;cursor:pointer'
}
if (tag === 'TABLE') {
// 每张表单独横向滚动,保留行列关系;不让宽表撑开整个审核页面。
const scroll = content.createElement('div')
scroll.style.cssText = 'max-width:100%;overflow-x:auto;margin:8px 0;white-space:normal'
parent.appendChild(scroll)
parent = scroll
node.style.width = '100%'
node.style.borderCollapse = 'collapse'
node.style.tableLayout = 'auto'
}
if (tag === 'TD' || tag === 'TH') {
node.style.border = '1px solid #dcdfe6'
node.style.padding = '6px 8px'
node.style.minWidth = '72px'
node.style.overflowWrap = 'anywhere'
// 合并单元格只接受浏览器支持范围内的整数,保留预算表的表头和分组结构。
;['rowspan', 'colspan'].forEach((name) => {
const value = source.getAttribute(name)
if (/^\d{1,4}$/.test(value || '') && Number(value) >= 1 && Number(value) <= 1000) {
node.setAttribute(name, value)
}
})
}
Array.from(source.childNodes).forEach((child) => appendContent(child, node))
parent.appendChild(node)
}
if (!content.body.children.length) {
// 旧手机申请可能保存纯文字;保留用户输入的换行,但不把它作为 HTML 再解析。
const plain = content.createElement('div')
plain.style.whiteSpace = 'pre-wrap'
plain.textContent = content.body.textContent.trim()
container.appendChild(plain)
} else {
Array.from(content.body.childNodes).forEach((node) => appendContent(node, container))
}
return container.textContent.trim() || container.querySelector('img') ? container.innerHTML : ''
},
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: {
/** event 为正文点击事件;仅图片触发 preview,参数 imageUrl 交由父页面现有弹框处理,无返回值。 */
previewActivityImage(event) {
const target = event.target
if (target && target.tagName === 'IMG') this.$emit('preview', {imageUrl: target.getAttribute('src')})
},
/** 历史接口可能返回 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 || '')
}
}
}
@@ -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')
}
})
@@ -0,0 +1,18 @@
<!--#
layout("/mobile/platform.html"){
#-->
<!--#include('/mobile/jf/activitybx/review/index.html'){}#-->
<script>
// 节点配置由当前页面固定提供,业务权限及状态仍由服务端校验。
const activityReviewConfig = {
stage: 'union', title: '校工会主席审核', state: 1040,
confirmMessage: '确认通过后,将按原流程生成活动并通知申请人。',
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
}
<!--#include('/mobile/jf/activitybx/review/info.js'){}#-->
<!--#include('/mobile/jf/activitybx/review/page.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,18 @@
<!--#
layout("/mobile/platform.html"){
#-->
<!--#include('/mobile/jf/reimbursement/review/index.html'){}#-->
<script>
// 本页固定审核节点;服务端按相同节点校验权限、归属及报销状态。
const reimbursementReviewConfig = {
stage: 'clubChairman', title: '协会负责人审核(报销)', state: 1000,
confirmMessage: '通过后将转交校工会负责人审核。', showClub: true, showUnit: false,
canChooseUnion: false,
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
}
<!--#include('/mobile/jf/reimbursement/review/info.js'){}#-->
<!--#include('/mobile/jf/reimbursement/review/page.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,233 @@
<style>
.reimbursement-review-page { min-height: 100vh; background: #f6f7f9; color: #323233; font-size: 13px; }
.reimbursement-review-page .van-nav-bar__title { font-size: 15px; font-weight: 400; }
.reimbursement-review-filters .van-dropdown-menu__bar { box-shadow: none; border-top: 1px solid #f8f8f8; border-bottom: 1px solid #f8f8f8; }
.reimbursement-review-filters .van-dropdown-menu__item { flex: 1; min-width: 0; }
.reimbursement-review-filters .van-dropdown-menu__title { max-width: calc(100% - 40px); padding: 0 8px; font-size: 14px; }
.reimbursement-review-filters .van-dropdown-item__content { max-height: 70vh; border-radius: 0 0 15px 15px; }
.reimbursement-review-list { padding: 2px 0 12px; }
.reimbursement-review-card { margin: 12px 13px 0; padding: 11px 12px 10px; background: white; border-radius: 10px; box-shadow: 0 5px 12px rgba(30,50,70,.04); }
.reimbursement-review-card h3 { position: relative; margin: 0 0 7px; color: #086bc3; font-size: 15px; line-height: 21px; font-weight: 600; overflow-wrap: anywhere; }
.reimbursement-review-card h3:before { content: ''; position: absolute; left: -10px; top: 3px; height: 15px; border-left: 2px solid #1989fa; }
.reimbursement-review-card p { margin: 0; line-height: 26px; color: #85878d; overflow-wrap: anywhere; }
.reimbursement-review-card label { color: #242424; }
.reimbursement-review-card .reimbursement-review-pair { display: flex; gap: 10px; justify-content: space-between; }
.reimbursement-review-pair span:last-child { flex-shrink: 0; }
.reimbursement-review-card-footer { display: flex; align-items: center; gap: 8px; margin-top: 7px; padding-top: 7px; border-top: 1px solid #f5f5f5; }
.reimbursement-review-state { flex: 1; min-width: 0; font-size: 12px; color: #b7790c; overflow-wrap: anywhere; }
.reimbursement-review-card-footer .van-button { height: 25px; padding: 0 8px; font-size: 13px; border-radius: 6px; flex-shrink: 0; }
.reimbursement-review-popup { display: flex; flex-direction: column; overflow: hidden; background: #f6f7f9; height: 100%; width: 100%; }
.reimbursement-review-popup > .van-nav-bar { flex-shrink: 0; }
.reimbursement-review-detail-body { flex: 1; overflow-y: auto; min-height: 0; padding-bottom: 12px; }
.reimbursement-review-section { margin: 12px; padding: 10px 0; border-radius: 10px; background: white; overflow: hidden; }
.reimbursement-review-section h3 { margin: 4px 14px 8px; font-size: 15px; color: #1768b0; }
.reimbursement-review-section .van-cell { font-size: 13px; padding: 8px 14px; }
.reimbursement-review-section .van-cell__title { flex: 0 0 88px; }
.reimbursement-review-section .van-cell__value { white-space: pre-wrap; overflow-wrap: anywhere; }
.reimbursement-review-section .reimbursement-review-file .van-cell__title { flex: 1; overflow-wrap: anywhere; }
.reimbursement-review-section .reimbursement-review-file .van-cell__value { flex: 0 0 40px; }
.reimbursement-review-content-image { display: block; max-width: calc(100% - 28px); margin: 10px 14px; }
.reimbursement-review-description { padding: 0 14px; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; }
.reimbursement-review-muted { margin: 10px 14px; color: #969799; }
.reimbursement-review-total .van-cell__value { color: #086bc3; font-weight: 600; }
.reimbursement-review-sign-image { display: block; max-width: calc(100% - 28px); width: 240px; height: 100px; object-fit: contain; margin: 8px 14px; }
.reimbursement-review-actions { flex-shrink: 0; display: flex; gap: 12px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); background: white; border-top: 1px solid #eee; z-index: 2; }
.reimbursement-review-actions .van-button { flex: 1; border-radius: 6px; height: 40px; }
.reimbursement-review-section .reimbursement-review-sign-field { display: block; }
.reimbursement-review-sign-field .van-field__label { width: auto; margin: 0 0 10px; }
.reimbursement-review-sign-field .main-panel .van-image { width: 100%; height: 112px; min-height: 112px; overflow: hidden; border-radius: 6px; }
.reimbursement-review-sign-field .main-panel .open-button { width: 100%; border-radius: 6px; }
.reimbursement-review-preview { display: block; width: 100%; height: calc(100% - 46px); border: none; background: white; }
.reimbursement-review-preimbursement-review-image { width: 100%; height: calc(100% - 46px); object-fit: contain; }
.reimbursement-review-loading { padding: 30px; text-align: center; }
.reimbursement-review-more-bar { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 7px 13px; background: white; color: #969799; font-size: 12px; }
.reimbursement-review-more-bar > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reimbursement-review-more-popup, .reimbursement-review-choice-popup { display: flex; flex-direction: column; max-height: 80vh; background: #f7f8fa; }
.reimbursement-review-choice-popup { height: 60vh; }
.reimbursement-review-more-body { flex: 1; overflow-y: auto; min-height: 0; }
/* 筛选面板独立限定样式,避免影响详情、审核按钮和其他公共弹框。 */
.reimbursement-review-more-popup { background: #fff; border-radius: 18px 18px 0 0; overflow: hidden; }
.reimbursement-review-more-handle { flex-shrink: 0; width: 38px; height: 4px; margin: 8px auto 0; border-radius: 4px; background: #d5d8df; }
.reimbursement-review-more-header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; padding: 4px 12px 8px 18px; }
.reimbursement-review-more-header h2 { margin: 0; font-size: 18px; line-height: 32px; font-weight: 600; color: #20242d; }
.reimbursement-review-more-close.van-button { width: 36px; height: 36px; padding: 0; border: 0; color: #92969f; background: transparent; font-size: 20px; }
.reimbursement-review-more-popup > .reimbursement-review-more-body { flex: 0 1 auto; padding: 0 18px 14px; }
.reimbursement-review-filter-group + .reimbursement-review-filter-group { margin-top: 16px; }
.reimbursement-review-filter-label { margin: 0 0 8px; color: #252a34; font-size: 14px; line-height: 20px; font-weight: 600; }
.reimbursement-review-filter-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
.reimbursement-review-filter-row .reimbursement-review-filter-label { margin: 0; }
.reimbursement-review-unit-select.van-cell { align-items: center; padding: 8px 12px; border-radius: 8px; background: #f5f6f8; font-size: 13px; }
.reimbursement-review-unit-select .van-cell__title { min-width: 0; overflow-wrap: anywhere; }
.reimbursement-review-unit-select:after, .reimbursement-review-filter-input:after { display: none; }
.reimbursement-review-filter-input.van-cell { padding: 8px 12px; border-radius: 8px; background: #f5f6f8; font-size: 13px; }
.reimbursement-review-filter-input .van-field__left-icon { color: #969ba4; margin-right: 8px; }
.reimbursement-review-segment { display: flex; flex-shrink: 0; gap: 2px; padding: 2px; border-radius: 8px; background: #f5f6f8; }
.reimbursement-review-filter-option.van-button { min-width: 0; height: 30px; padding: 0 8px; border: 1px solid transparent; border-radius: 7px; background: #f5f6f8; color: #333943; font-size: 12px; }
.reimbursement-review-segment .reimbursement-review-filter-option { min-width: 58px; height: 28px; }
.reimbursement-review-filter-option.is-selected { background: #eaf3ff; border-color: #1989fa; color: #1989fa; font-weight: 500; }
.reimbursement-review-segment .reimbursement-review-filter-option.is-selected { border-color: transparent; }
.reimbursement-review-sort-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.reimbursement-review-sort-options .reimbursement-review-filter-option { position: relative; width: 100%; height: 30px; padding: 0 4px; white-space: nowrap; }
.reimbursement-review-sort-options .is-selected { padding-right: 15px; }
.reimbursement-review-sort-check { position: absolute; right: 3px; top: 50%; transform: translateY(-50%); font-size: 12px; }
.reimbursement-review-sort-direction { margin: 14px 0 0; }
.reimbursement-review-sort-direction .reimbursement-review-segment { background: transparent; gap: 6px; padding: 0; }
.reimbursement-review-more-footer { display: flex; flex-shrink: 0; gap: 10px; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); border-top: 1px solid #f1f2f5; background: #fff; }
.reimbursement-review-more-footer .van-button { flex: 1; height: 40px; border-radius: 8px; font-size: 14px; }
.reimbursement-review-more-footer .van-button--info { flex: 2; }
.reimbursement-review-more-footer .van-button--default { color: #60656e; border-color: #e4e7ed; }
.reimbursement-review-choice-popup { background: #fff; }
.reimbursement-review-choice-popup .van-search { padding: 8px 16px; }
.reimbursement-review-choice-popup .van-search__content { border-radius: 8px; background: #f5f6f8; }
.reimbursement-review-choice-popup .van-cell { padding: 12px 18px; font-size: 13px; }
.reimbursement-review-choice-popup .reimbursement-review-choice-selected { color: #1989fa; background: #f0f6ff; }
.reimbursement-review-budget-item + .reimbursement-review-budget-item { border-top: 1px solid #f2f3f5; }
</style>
<div id="app" class="reimbursement-review-page" v-cloak>
<van-sticky>
<van-nav-bar :title="config.title" left-arrow @click-left="back"></van-nav-bar>
<van-dropdown-menu class="reimbursement-review-filters" active-color="#1867b0"
:close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item ref="yearFilter" :options="yearOptions" :value="pageForm.year" :disabled="filterClosing"
@change="selectFilter('year', $event)"
@open="setFilterOpen('year', true)" @close="setFilterOpen('year', false)"></van-dropdown-item>
<van-dropdown-item ref="organizationFilter" :options="organizationOptions" :value="pageForm[organizationKey]" :disabled="filterClosing"
@change="selectFilter(organizationKey, $event)"
@open="setFilterOpen('organization', true)" @close="setFilterOpen('organization', false)">
<van-loading v-if="organizationLoading" class="reimbursement-review-loading">加载中</van-loading>
</van-dropdown-item>
<van-dropdown-item ref="isAuditFilter" :options="auditOptions" :value="pageForm.isAudit" :disabled="filterClosing"
@change="selectFilter('isAudit', $event)"
@open="setFilterOpen('isAudit', true)" @close="setFilterOpen('isAudit', false)"></van-dropdown-item>
</van-dropdown-menu>
<div class="reimbursement-review-more-bar"><span>{{ searchSummary }}</span><van-button size="mini" plain type="info" @click="openMore">更多筛选</van-button></div>
</van-sticky>
<van-pull-refresh :value="refreshing" @input="setRefreshing" @refresh="doSearch">
<van-list :value="tableLoading" :finished="finished" :error="listError" error-text="加载失败,点击重试"
:finished-text="tableData.length ? '没有更多了' : ''" :immediate-check="false"
@input="setTableLoading" @update:error="setListError" @load="pageData" class="reimbursement-review-list">
<article v-for="item in tableData" :key="item.id" class="reimbursement-review-card">
<h3>{{ item.activity_name }}</h3>
<p><label>申请人:</label>{{ item.user_name }}<span v-if="item.bxr_loginname">({{ item.bxr_loginname }})</span></p>
<p><label>{{ config.stage === 'clubChairman' ? '所属协会:' : '所属工会:' }}</label>{{ config.stage === 'clubChairman' ? (item.clubName || item.fundsUnitName || '—') : (item.unionname || item.fundsUnitName || '—') }}</p>
<p class="reimbursement-review-pair"><span><label>报销:</label>¥{{ money(item.activity_money) }}</span><span><label>申请:</label>{{ shortDate(item.reimbursement_apply_time) }}</span></p>
<div class="reimbursement-review-card-footer">
<span class="reimbursement-review-state" :style="{color: item.state_color || '#b7790c'}">{{ item.state_name || config.title }}</span>
<van-button type="info" size="small" :disabled="formLoading" @click="openDetail(item.id, false)">查看</van-button>
<van-button v-if="Number(item.reimbursement_state_id) === config.state" type="primary" size="small" :disabled="formLoading" @click="openDetail(item.id, true)">审核</van-button>
</div>
</article>
<van-empty v-if="!tableLoading && !listError && !tableData.length" description="暂无报销申请"></van-empty>
</van-list>
</van-pull-refresh>
<van-popup :value="popups.detail" position="right" class="reimbursement-review-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<van-nav-bar :title="auditMode ? config.title : '报销详情'" left-arrow @click-left="closePopup"></van-nav-bar>
<div class="reimbursement-review-detail-body">
<van-loading v-if="detailLoading" class="reimbursement-review-loading">加载报销中</van-loading>
<van-empty v-else-if="!detail.id" description="详情加载失败">
<van-button size="small" type="info" @click="loadDetail(detailId)">重新加载</van-button>
</van-empty>
<reimbursement-info v-else :data="detail" @preview="previewFile">
<section v-if="auditMode && detail.canReview" class="reimbursement-review-section">
<h3>审核信息</h3>
<van-field label="审核人" :value="reviewerName" readonly></van-field>
<van-field label="审核时间" :value="reviewDate" readonly></van-field>
<van-field label="审核意见" :value="formData.auditOpinion" @input="setIdea" type="textarea" rows="3"
maxlength="500" show-word-limit required placeholder="请填写审核意见" :disabled="formLoading"></van-field>
<van-field label="审核签字" required class="reimbursement-review-sign-field">
<template #input>
<mobile-sign :value="formData.auditSign" @input="setSignature" :disabled="formLoading"
:save-to-profile="false" :popup-visible="popups.sign" panel-title="审核签字"
@open="openSignature" @close="closePopup"></mobile-sign>
</template>
</van-field>
</section>
</reimbursement-info>
</div>
<div v-if="detail.id && !detailLoading" class="reimbursement-review-actions">
<template v-if="auditMode && detail.canReview">
<van-button type="danger" plain :disabled="formLoading" @click="confirmReview(false)">拒绝</van-button>
<van-button type="primary" :loading="formLoading" :disabled="formLoading" @click="confirmReview(true)">通过</van-button>
</template>
<van-button v-else-if="detail.canReview" type="primary" @click="startAudit">去审核</van-button>
<van-button v-else @click="closePopup">关闭</van-button>
</div>
</van-popup>
<van-dialog :value="popups.confirm" :title="reviewFlag ? '确认通过' : '确认拒绝'"
:message="reviewFlag ? config.confirmMessage : '拒绝后将结束当前报销流程,请核对审核意见。'"
show-cancel-button :allow-html="false" :close-on-click-overlay="false" :close-on-popstate="false"
:before-close="beforeReviewClose"></van-dialog>
<van-popup :value="popups.preview" position="right" class="reimbursement-review-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<van-nav-bar title="附件查看" left-arrow @click-left="closePopup"></van-nav-bar>
<img v-if="popups.preview && previewIsImage" :src="previewUrl" class="reimbursement-review-preimbursement-review-image" alt="报销附件">
<iframe v-else-if="popups.preview" :src="previewUrl" title="报销附件" class="reimbursement-review-preview"></iframe>
</van-popup>
<van-popup :value="popups.more" position="bottom" round class="reimbursement-review-more-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<div class="reimbursement-review-more-handle" aria-hidden="true"></div>
<header class="reimbursement-review-more-header">
<h2>更多筛选</h2>
<van-button icon="cross" class="reimbursement-review-more-close" aria-label="关闭更多筛选" @click="closePopup"></van-button>
</header>
<div class="reimbursement-review-more-body">
<section v-if="config.canChooseUnion" class="reimbursement-review-filter-group">
<h3 class="reimbursement-review-filter-label">所属单位</h3>
<van-cell :title="optionText('unitId', moreForm.unitId)" class="reimbursement-review-unit-select" is-link @click="openChoice('unitId')"></van-cell>
</section>
<section v-if="config.showClub && config.stage !== 'clubChairman'" class="reimbursement-review-filter-group">
<h3 class="reimbursement-review-filter-label">所属协会</h3>
<van-cell :title="optionText('clubId', moreForm.clubId)" class="reimbursement-review-unit-select" is-link @click="openChoice('clubId')"></van-cell>
</section>
<section class="reimbursement-review-filter-group">
<h3 class="reimbursement-review-filter-label">活动名称</h3>
<van-field class="reimbursement-review-filter-input" left-icon="search" :value="moreForm.activityName"
@input="setMoreField('activityName', $event)" maxlength="100" clearable placeholder="请输入活动名称"></van-field>
</section>
<section class="reimbursement-review-filter-group">
<div class="reimbursement-review-filter-row">
<h3 class="reimbursement-review-filter-label">查询人员</h3>
<div class="reimbursement-review-segment" role="group" aria-label="查询类型">
<van-button v-for="option in choiceOptions.searchName" :key="option.value" class="reimbursement-review-filter-option"
:class="{'is-selected': moreForm.searchName === option.value}" :aria-pressed="moreForm.searchName === option.value"
@click="setMoreField('searchName', option.value)">{{ option.text }}</van-button>
</div>
</div>
<van-field class="reimbursement-review-filter-input" left-icon="search" :value="moreForm.searchKeyword"
@input="setMoreField('searchKeyword', $event)" maxlength="100" clearable
:placeholder="moreSearchPlaceholder" :aria-label="moreSearchPlaceholder"></van-field>
</section>
<section class="reimbursement-review-filter-group">
<h3 class="reimbursement-review-filter-label">排序方式</h3>
<div class="reimbursement-review-sort-options" role="group" aria-label="排序字段">
<van-button v-for="option in choiceOptions.pageOrderName" :key="option.value" class="reimbursement-review-filter-option"
:class="{'is-selected': moreForm.pageOrderName === option.value}" :aria-pressed="moreForm.pageOrderName === option.value"
@click="setMoreField('pageOrderName', option.value)">
{{ option.text }}<van-icon v-if="moreForm.pageOrderName === option.value" name="success" class="reimbursement-review-sort-check"></van-icon>
</van-button>
</div>
<div v-if="moreForm.pageOrderName" class="reimbursement-review-filter-row reimbursement-review-sort-direction">
<h3 class="reimbursement-review-filter-label">排列顺序</h3>
<div class="reimbursement-review-segment" role="group" aria-label="排列顺序">
<van-button v-for="option in choiceOptions.pageOrderBy" :key="option.value" class="reimbursement-review-filter-option"
:class="{'is-selected': moreForm.pageOrderBy === option.value}" :aria-pressed="moreForm.pageOrderBy === option.value"
@click="setMoreField('pageOrderBy', option.value)">{{ option.text }}</van-button>
</div>
</div>
</section>
</div>
<div class="reimbursement-review-more-footer"><van-button @click="resetMore">重置</van-button><van-button type="info" @click="applyMore">确定筛选</van-button></div>
</van-popup>
<van-popup :value="popups.choice" position="bottom" round class="reimbursement-review-choice-popup" :close-on-popstate="false" :close-on-click-overlay="false">
<van-nav-bar :title="choiceTitle" left-text="返回" @click-left="closePopup"></van-nav-bar>
<van-search v-if="choiceKey === 'unitId' || choiceKey === 'clubId'" :value="choiceQuery" @input="setChoiceQuery" :placeholder="choiceKey === 'clubId' ? '搜索协会' : '搜索单位'"></van-search>
<div class="reimbursement-review-more-body">
<van-loading v-if="choiceKey === 'unitId' ? unitsLoading : clubsLoading" class="reimbursement-review-loading">加载选项中</van-loading>
<van-cell v-for="option in filteredChoices" :key="option.value" :title="option.text" :class="{'reimbursement-review-choice-selected': moreForm[choiceKey] === option.value}" clickable @click="selectChoice(option.value)">
<van-icon v-if="moreForm[choiceKey] === option.value" name="success" color="#1989fa"></van-icon>
</van-cell>
<van-empty v-if="!filteredChoices.length" description="暂无选项"></van-empty>
</div>
</van-popup>
</div>
@@ -0,0 +1,227 @@
/** 活动报销审核详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
const reimbursementInfo = {
template: /*language=HTML*/ `
<div class="reimbursement-review-info">
<section class="reimbursement-review-section">
<h3>{{ data.activity_name }}</h3>
<van-cell title="报销状态" :value="data.state_name"></van-cell>
<van-cell title="申请人" :value="data.user_name + '(' + (data.loginname || '—') + ')'"></van-cell>
<van-cell title="所属工会" :value="data.bxr_unionname || '—'"></van-cell>
<van-cell title="所属单位" :value="data.bxr_unitname || '—'"></van-cell>
<van-cell title="活动主体" :value="activityType"></van-cell>
<van-cell title="申请类型" :value="Number(data.apply_type) === 1 ? '经费申报' : '报销申请'"></van-cell>
<van-cell title="联系方式" :value="data.mobile || '—'"></van-cell>
<van-cell title="报销申请时间" :value="data.reimbursement_apply_time || '—'"></van-cell>
<van-cell title="承办单位/协会" :value="data.clubName || data.fundsUnitName || '—'"></van-cell>
<van-cell title="项目编号" :value="data.projectCode || '—'"></van-cell>
<van-cell title="经费类型" :value="data.projectJfName || '—'"></van-cell>
<van-cell title="计划时间" :value="(data.startPlannedDate || '—') + ' 至 ' + (data.endPlannedDate || '—')"></van-cell>
<van-cell title="活动时间" :value="data.startTime ? data.startTime + ' 至 ' + (data.endTime || '—') : '暂无'"></van-cell>
<van-cell title="实际费用" :value="'¥' + money(data.activity_money)"></van-cell>
<van-cell title="活动地点" :value="data.address || '—'"></van-cell>
<van-cell title="活动人数" :value="data.activity_number || '—'"></van-cell>
</section>
<section class="reimbursement-review-section">
<h3>活动方案及简介</h3>
<div class="reimbursement-review-description" style="white-space:normal;min-width:0"
v-html="activityHtml || '暂无'" @click="previewActivityImage"></div>
</section>
<section class="reimbursement-review-section">
<h3>费用及收款信息</h3>
<div v-for="(item, index) in budgets" :key="index" class="reimbursement-review-budget-item">
<van-cell :title="item.name || '费用项目'" :value="'¥' + money(item.budgetPrice)"></van-cell>
<template v-if="data.reimbursement_state_id">
<van-cell title="实际费用" :value="'¥' + money(item.actualPrice)"></van-cell>
<van-cell title="收款人" :value="item.username || '—'"></van-cell>
<van-cell title="支行名称" :value="item.bankName || '—'"></van-cell>
<van-cell title="报销卡号" :value="item.bankCardNum || '—'"></van-cell>
<van-cell title="备注" :value="item.remark || '—'"></van-cell>
</template>
</div>
<van-cell title="报销合计" :value="'¥' + money(data.activity_money)" class="reimbursement-review-total"></van-cell>
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="reimbursement-review-total"></van-cell>
</section>
<section class="reimbursement-review-section">
<h3>申请附件</h3>
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="reimbursement-review-file"
value="查看" is-link @click="$emit('preview', file)"></van-cell>
<p v-if="!files.length" class="reimbursement-review-muted">暂无附件</p>
<h3>申请人签字</h3>
<img v-if="data.us_s" class="reimbursement-review-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
<p v-else class="reimbursement-review-muted">暂无签字</p>
</section>
<section v-for="group in reimbursementFiles" :key="group.key" class="reimbursement-review-section">
<h3>{{ group.title }}</h3>
<van-cell v-for="(file, index) in group.files" :key="file.id || index" :title="file.filename || file.name || '附件'"
class="reimbursement-review-file" value="查看" is-link @click="$emit('preview', file)"></van-cell>
<p v-if="!group.files.length" class="reimbursement-review-muted">暂无附件</p>
</section>
<section v-for="audit in audits" :key="audit.key" class="reimbursement-review-section">
<h3>{{ audit.title }}</h3>
<van-cell title="审核人" :value="audit.name || '—'"></van-cell>
<van-cell title="审核时间" :value="audit.time || '—'"></van-cell>
<van-cell title="审核意见" :value="audit.idea || '暂无'"></van-cell>
<img v-if="audit.sign" class="reimbursement-review-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
</section>
<slot></slot>
</div>
`,
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)
},
activityHtml() {
// activity_content 为历史富文本;只重建允许的内容节点,不复制脚本、事件、类名或任意内联样式。
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
const container = content.createElement('div')
const allowed = new Set(['P', 'DIV', 'SPAN', 'BR', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'DEL', 'SUB', 'SUP',
'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'PRE', 'CODE', 'HR',
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'IMG', 'FONT', 'MARK'])
const blocked = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'SVG', 'MATH', 'TEMPLATE',
'FORM', 'INPUT', 'BUTTON', 'TEXTAREA', 'SELECT', 'LINK', 'META', 'BASE'])
const appendContent = (source, parent) => {
if (source.nodeType === 3) {
parent.appendChild(content.createTextNode(source.textContent))
return
}
if (source.nodeType !== 1 || blocked.has(source.tagName)) return
const tag = source.tagName
if (!allowed.has(tag)) {
Array.from(source.childNodes).forEach((child) => appendContent(child, parent))
return
}
const node = content.createElement(tag === 'FONT' ? 'span' : tag.toLowerCase())
// 仅保留文字强调、颜色和对齐;固定宽高、定位和 Word 段落间距不带入手机页面。
const textStyles = ['color', 'backgroundColor', 'fontWeight', 'fontStyle', 'textDecorationLine', 'textAlign', 'verticalAlign']
textStyles.forEach((name) => { node.style[name] = source.style[name] })
if (tag === 'FONT' && !node.style.color) node.style.color = source.getAttribute('color') || ''
if (!node.style.textAlign && /^(left|center|right|justify)$/i.test(source.getAttribute('align') || '')) {
node.style.textAlign = source.getAttribute('align')
}
if (/^(P|DIV|H[1-6]|BLOCKQUOTE|PRE|UL|OL)$/.test(tag)) {
node.style.margin = '0 0 8px'
node.style.lineHeight = '1.8'
}
if (/^H[1-6]$/.test(tag)) node.style.fontSize = '16px'
if (tag === 'UL' || tag === 'OL') {
node.style.paddingLeft = '24px'
node.style.listStyleType = tag === 'UL' ? 'disc' : 'decimal'
if (tag === 'OL' && /^-?\d{1,6}$/.test(source.getAttribute('start') || '')) {
node.setAttribute('start', source.getAttribute('start'))
}
}
if (tag === 'PRE') {
node.style.whiteSpace = 'pre-wrap'
node.style.overflowWrap = 'anywhere'
}
if (tag === 'IMG') {
// 图片沿用原有地址范围;保留正文中的位置,点击后交给现有预览弹框和历史栈。
const url = (source.getAttribute('src') || '').trim()
if (!/^(https?:\/\/|\/(?![\/\\])|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url)) return
node.setAttribute('src', url)
node.setAttribute('alt', source.getAttribute('alt') || '活动方案配图')
node.style.cssText = 'display:block;max-width:100%;height:auto;margin:8px 0;cursor:pointer'
}
if (tag === 'TABLE') {
// 每张表单独横向滚动,保留行列关系;不让宽表撑开整个审核页面。
const scroll = content.createElement('div')
scroll.style.cssText = 'max-width:100%;overflow-x:auto;margin:8px 0;white-space:normal'
parent.appendChild(scroll)
parent = scroll
node.style.width = '100%'
node.style.borderCollapse = 'collapse'
node.style.tableLayout = 'auto'
}
if (tag === 'TD' || tag === 'TH') {
node.style.border = '1px solid #dcdfe6'
node.style.padding = '6px 8px'
node.style.minWidth = '72px'
node.style.overflowWrap = 'anywhere'
// 合并单元格只接受浏览器支持范围内的整数,保留预算表的表头和分组结构。
;['rowspan', 'colspan'].forEach((name) => {
const value = source.getAttribute(name)
if (/^\d{1,4}$/.test(value || '') && Number(value) >= 1 && Number(value) <= 1000) {
node.setAttribute(name, value)
}
})
}
Array.from(source.childNodes).forEach((child) => appendContent(child, node))
parent.appendChild(node)
}
if (!content.body.children.length) {
// 旧手机申请可能保存纯文字;保留用户输入的换行,但不把它作为 HTML 再解析。
const plain = content.createElement('div')
plain.style.whiteSpace = 'pre-wrap'
plain.textContent = content.body.textContent.trim()
container.appendChild(plain)
} else {
Array.from(content.body.childNodes).forEach((node) => appendContent(node, container))
}
return container.textContent.trim() || container.querySelector('img') ? container.innerHTML : ''
},
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: {
/** event 为正文点击事件;仅图片触发 preview,参数 imageUrl 交由父页面现有弹框处理,无返回值。 */
previewActivityImage(event) {
const target = event.target
if (target && target.tagName === 'IMG') this.$emit('preview', {imageUrl: target.getAttribute('src')})
},
/** 历史接口可能返回 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 || '')
}
}
}
@@ -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')
}
})
@@ -0,0 +1,18 @@
<!--#
layout("/mobile/platform.html"){
#-->
<!--#include('/mobile/jf/reimbursement/review/index.html'){}#-->
<script>
// 本页固定审核节点;服务端按相同节点校验权限、归属及报销状态。
const reimbursementReviewConfig = {
stage: 'schoolChairman', title: '校工会主席审核(报销)', state: 1040,
confirmMessage: '通过后将完成本次活动报销流程。', showClub: true, showUnit: true,
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
}
<!--#include('/mobile/jf/reimbursement/review/info.js'){}#-->
<!--#include('/mobile/jf/reimbursement/review/page.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,18 @@
<!--#
layout("/mobile/platform.html"){
#-->
<!--#include('/mobile/jf/reimbursement/review/index.html'){}#-->
<script>
// 本页固定审核节点;服务端按相同节点校验权限、归属及报销状态。
const reimbursementReviewConfig = {
stage: 'schoolDirector', title: '校工会负责人审核(报销)', state: 1020,
confirmMessage: '通过后将转交校工会主席审核。', showClub: true, showUnit: true,
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')},
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
}
<!--#include('/mobile/jf/reimbursement/review/info.js'){}#-->
<!--#include('/mobile/jf/reimbursement/review/page.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,18 @@
<!--#
layout("/mobile/platform.html"){
#-->
<!--#include('/mobile/jf/reimbursement/review/index.html'){}#-->
<script>
// 本页固定审核节点;服务端按相同节点校验权限、归属及报销状态。
const reimbursementReviewConfig = {
stage: 'unionChairman', title: '基层工会主席审核(报销)', state: 1010,
confirmMessage: '通过后将转交校工会负责人审核。', showClub: false, showUnit: true,
canChooseUnion: ${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')},
currentUnionId: '${@io.v.nutz.base.utils.Vi.getUnionId()}'
}
<!--#include('/mobile/jf/reimbursement/review/info.js'){}#-->
<!--#include('/mobile/jf/reimbursement/review/page.js'){}#-->
</script>
<!--#
}
#-->
@@ -25,6 +25,8 @@
<script src="${base!}/assets/platform/plugins/jquery/jquery-1.11.1.min.js"></script> <script src="${base!}/assets/platform/plugins/jquery/jquery-1.11.1.min.js"></script>
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script> <script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
<!-- PJAX 会晚于片段内脚本加载外链脚本,弹框历史管理必须随公共布局提前加载。 -->
<script src="${base!}/assets/mobile/js/popupHistory.js"></script>
<script src="${base!}/assets/platform/plugins/jSignature/jSignature.min.js"></script> <script src="${base!}/assets/platform/plugins/jSignature/jSignature.min.js"></script>
<script src="${base!}/assets/platform/plugins/jSignature/flashcanvas.js"></script> <script src="${base!}/assets/platform/plugins/jSignature/flashcanvas.js"></script>
<script src="${base!}/assets/platform/js/pinyin.js"></script> <script src="${base!}/assets/platform/js/pinyin.js"></script>
@@ -79,7 +81,9 @@
} }
})() })()
function pjaxReplace(url){ window.pjaxReplace = (url) => {
// 普通表单的导航也先消费公共签字层,避免携带未回退的弹框历史进入下一页面。
if (window.mobilePopupHistory.closeTop()) return
$.pjax({ $.pjax({
url: url, url: url,
container: "#container", container: "#container",
@@ -48,7 +48,7 @@ layout("/mobile/platform.html"){
</div> </div>
<script> <script>
<!--#include('../common/siteInfo.js'){}#--> <!--#include('/mobile/site/common/siteInfo.js'){}#-->
<!--#include('apply.js'){}#--> <!--#include('apply.js'){}#-->
new Vue({ new Vue({
el: "#app", el: "#app",
@@ -64,8 +64,8 @@ layout("/mobile/platform.html"){
</div> </div>
<script> <script>
<!--#include('../apply/apply.js'){}#--> <!--#include('/mobile/site/apply/apply.js'){}#-->
<!--#include('../common/info.js'){}#--> <!--#include('/mobile/site/common/info.js'){}#-->
new Vue({ new Vue({
el: "#app", el: "#app",
components: { components: {
@@ -214,7 +214,7 @@ layout("/mobile/platform.html"){
</van-popup> </van-popup>
</div> </div>
<script> <script>
<!--#include('../common/info.js'){}#--> <!--#include('/mobile/site/nursing/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
@@ -193,7 +193,7 @@ layout("/mobile/platform.html"){
</van-popup> </van-popup>
</div> </div>
<script> <script>
<!--#include('../common/info.js'){}#--> <!--#include('/mobile/site/nursing/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
@@ -82,7 +82,7 @@ layout("/mobile/platform.html"){
</div> </div>
<script> <script>
<!--#include('../common/info.js'){}#--> <!--#include('/mobile/site/common/info.js'){}#-->
new Vue({ new Vue({
el: "#app", el: "#app",
components: { components: {
@@ -1,99 +1,85 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<title>签字</title> <title>签字</title>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vant/vant.css">
<script src="${base!}/assets/platform/plugins/vue/vue.js"></script> <script src="${base!}/assets/platform/plugins/vue/vue.js"></script>
<script src="${base!}/assets/platform/plugins/vant/vant.js"></script>
<script src="${base!}/assets/platform/plugins/vue-loader/httpVueLoader.js"></script> <script src="${base!}/assets/platform/plugins/vue-loader/httpVueLoader.js"></script>
<script src="${base!}/assets/platform/plugins/smooth-signature/index.umd.min.js"></script> <script src="${base!}/assets/platform/plugins/smooth-signature/index.umd.min.js"></script>
<script src="${base!}/assets/platform/plugins/jquery/jquery-1.11.1.min.js"></script> <script src="${base!}/assets/platform/plugins/jquery/jquery-1.11.1.min.js"></script>
<script src="${base!}/assets/mobile/js/popupHistory.js"></script>
<style>body { margin: 0; background: #f7f8fa; } [v-cloak] { display: none; }</style>
</head> </head>
<body> <body>
<div id="app"> <div id="app" v-cloak>
<signature @save="save"></signature> <van-nav-bar title="签字"></van-nav-bar>
<van-notice-bar v-if="subDis" text="缺少签字参数,请重新扫码。"></van-notice-bar>
<mobile-sign :value="sign" :is_value_base64="isValueBase64" :popup-visible="showSignaturePanel"
:disabled="subDis || formLoading" @input="save" @open="openSignature" @close="closeSignature"></mobile-sign>
</div> </div>
</body>
<script> <script>
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (pair[0] == variable) {
return pair[1];
}
}
return '';
}
new Vue({ new Vue({
el: "#app", el: "#app",
components: { components: {
signature: httpVueLoader("/components/sign/index.vue?v=" + new Date().getTime()) 'mobile-sign': httpVueLoader("/components/sign/mobileSign.vue?v=" + new Date().getTime())
}, },
data() { data() {
return { return {prefix: '', id: '', sign: '', isValueBase64: true, subDis: false, formLoading: false, showSignaturePanel: false}
prefix: '',
id: '',
isNull: function (val) {
if (val === null || val === undefined || val === '') {
return true
}
return false
},
}
}, },
methods: { methods: {
openSignature() {
if (!this.subDis && !this.formLoading) this.$options.popupSession.open('sign')
},
closeSignature() {
if (!this.formLoading) this.$options.popupSession.close()
},
/**
* signature 为组件已保存的个人签名:true 为 Base64,false 为文件路径。
* prefix/id 和 is_value_base64 来自二维码;接口返回 {code,msg,data},code=0 表示发布给 PC 成功。
*/
save(signature) { save(signature) {
if (this.formLoading || this.subDis) return
this.sign = signature
this.formLoading = true
$.post("/signature/doSub", { $.post("/signature/doSub", {
prefix: this.prefix, prefix: this.prefix,
id: this.id, id: this.id,
base64: signature, base64: signature,
is_value_base64: false is_value_base64: this.isValueBase64
}) })
.then((res) => { .then((res) => {
if (res.code === 0) { if (res.code === 0) {
alert("保存成功") this.$toast.success("保存成功")
this.showSignaturePanel = false this.$options.popupSession.clear()
} else { } else {
alert("保存失败") this.$toast.fail(res.msg || "保存失败,请重试")
} }
}) })
.fail(() => this.$toast.fail("保存失败,请重试"))
}, .always(() => { this.formLoading = false })
base64ToFile(base64Data, filename) {
// 将base64的数据部分提取出来
const parts = base64Data.split(";base64,")
const contentType = parts[0].split(":")[1]
const raw = window.atob(parts[1])
const rawLength = raw.length
const uInt8Array = new Uint8Array(rawLength)
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i)
}
// 使用Blob对象创建File对象
const blob = new Blob([uInt8Array], { type: contentType })
blob.lastModifiedDate = new Date()
blob.name = filename
return new File([blob], filename, { type: contentType })
} }
}, },
created: function () { created() {
if (this.isNull(getQueryVariable("id")) || this.isNull(getQueryVariable("prefix"))) { // 扫码入口也是独立页面,使用公共历史栈统一处理返回、取消及保存成功后的清理。
this.$notify({ this.$options.popupSession = window.mobilePopupHistory.register('qrSignature', (layers) => {
message: '缺少参数!', this.showSignaturePanel = layers.includes('sign')
type: 'danger' })
}); const query = new URLSearchParams(window.location.search)
this.subDis = true this.id = query.get('id') || ''
} this.prefix = query.get('prefix') || ''
this.id = getQueryVariable("id") // 缺省沿用 PC 默认 true;显式 false 使用文件签名,异常参数不默默切换类型。
this.prefix = getQueryVariable("prefix") const signType = query.get('is_value_base64')
this.isValueBase64 = signType !== 'false'
this.subDis = !this.id || !this.prefix || (signType !== null && signType !== 'true' && signType !== 'false')
},
beforeDestroy() {
const session = this.$options.popupSession
session.clear().then(() => session.dispose())
} }
}) })
</script> </script>
</body>
</html> </html>
@@ -560,7 +560,12 @@ layout("/layouts/platform.html"){
}, },
async created() { async created() {
this.pageData(); this.pageData();
if ("${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')}" === 'true') { // 分工会主席可能兼具 H03 角色,需优先按当前分工会加载查询选项。
if ("${@shiro.hasRole('H04')&&!(@shiro.hasRole('sysadmin')||@shiro.hasRole('A06'))}" === 'true') {
this.unions = await getUnions(null, true)
this.$set(this.pageForm, 'unionId', this.unions ? this.unions[0].id : null)
this.flushUnits()
} else if ("${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')}" === 'true') {
this.unions = await getUnions(null, false) this.unions = await getUnions(null, false)
} else { } else {
this.unions = await getUnions(null, true) this.unions = await getUnions(null, true)
@@ -83,7 +83,7 @@ layout("/layouts/platform.html"){
</guava> </guava>
</div> </div>
<script> <script>
<!--#include('../manage/info.js'){}#--> <!--#include('/platform/site/manage/info.js'){}#-->
<!--#include('apply.js'){}#--> <!--#include('apply.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: "#app", el: "#app",
@@ -110,8 +110,8 @@ layout("/layouts/platform.html"){
</div> </div>
<script> <script>
<!--#include('../common/applyInfo.js'){}#--> <!--#include('/platform/site/common/applyInfo.js'){}#-->
<!--#include('../apply/apply.js'){}#--> <!--#include('/platform/site/apply/apply.js'){}#-->
new Vue({ new Vue({
el: "#app", el: "#app",
mixins: [initTableMixins], mixins: [initTableMixins],
@@ -89,8 +89,8 @@ layout("/layouts/platform.html"){
</guava> </guava>
</div> </div>
<script> <script>
<!--#include("../common/info.js"){}#--> <!--#include("/platform/site/nursing/common/info.js"){}#-->
<!--#include("../common/commonQuery.js"){}#--> <!--#include("/platform/site/nursing/common/commonQuery.js"){}#-->
new Vue({ new Vue({
el: '#app', el: '#app',
@@ -49,8 +49,8 @@ layout("/layouts/platform.html"){
</guava> </guava>
</div> </div>
<script> <script>
<!--#include("../common/info.js"){}#--> <!--#include("/platform/site/nursing/common/info.js"){}#-->
<!--#include("../common/commonQuery.js"){}#--> <!--#include("/platform/site/nursing/common/commonQuery.js"){}#-->
new Vue({ new Vue({
el: '#app', el: '#app',
@@ -54,8 +54,8 @@ layout("/layouts/platform.html"){
</guava> </guava>
</div> </div>
<script> <script>
<!--#include("../common/info.js"){}#--> <!--#include("/platform/site/nursing/common/info.js"){}#-->
<!--#include("../common/commonQuery.js"){}#--> <!--#include("/platform/site/nursing/common/commonQuery.js"){}#-->
new Vue({ new Vue({
el: '#app', el: '#app',
@@ -82,7 +82,7 @@ layout("/layouts/platform.html"){
</div> </div>
<script> <script>
<!--#include('../common/applyInfo.js'){}#--> <!--#include('/platform/site/common/applyInfo.js'){}#-->
new Vue({ new Vue({
el: "#app", el: "#app",
mixins: [initTableMixins], mixins: [initTableMixins],
@@ -124,7 +124,7 @@ layout("/layouts/platform.html"){
</div> </div>
<script> <script>
<!--#include('../common/applyInfo.js'){}#--> <!--#include('/platform/site/common/applyInfo.js'){}#-->
new Vue({ new Vue({
el: "#app", el: "#app",
mixins: [initTableMixins], mixins: [initTableMixins],
@@ -114,7 +114,7 @@ layout("/layouts/platform.html"){
prop="rylx"></el-table-column> prop="rylx"></el-table-column>
<el-table-column width="100" align="center" header-align="center" label="在职状态" <el-table-column width="100" align="center" header-align="center" label="在职状态"
prop="userstate"></el-table-column>--> prop="userstate"></el-table-column>-->
<el-table-column align="center" header-align="center" label="所属单位" prop="unitname" <el-table-column align="center" header-align="center" label="所属单位" prop="applicantunitname"
show-overflow-tooltip show-overflow-tooltip
sortable></el-table-column> sortable></el-table-column>
<el-table-column align="center" header-align="center" label="申请时间" prop="join_time" <el-table-column align="center" header-align="center" label="申请时间" prop="join_time"
@@ -141,7 +141,7 @@ layout("/layouts/platform.html"){
<template scope="{row}"> <template scope="{row}">
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button> <el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="pageForm.radioType == 3" @click="openAudit(row)" size="mini" type="primary">审核</el-button> <el-button v-if="pageForm.radioType == 3" @click="openAudit(row)" size="mini" type="primary">审核</el-button>
<el-button type="primary" size="mini" @click="doExportApply(row)">导出申请表</el-button> <!-- <el-button type="primary" size="mini" @click="doExportApply(row)">导出申请表</el-button> -->
</template> </template>
</el-table-column> </el-table-column>
@@ -190,14 +190,14 @@ layout("/layouts/platform.html"){
</el-form-item> </el-form-item>
<el-form-item prop="auditSign" label="签&emsp;&emsp;字"> <el-form-item prop="auditSign" label="签&emsp;&emsp;字">
<sign prefix="club" :qz.sync="formData.auditSign"></sign> <sign :is_value_base64="false" prefix="club" :qz.sync="formData.auditSign"></sign>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div style="float: right;margin: 20px 0"> <div style="float: right;margin: 20px 0">
<el-button @click="doReview(false)">拒绝</el-button> <el-button :loading="formLoading" @click="doReview(false)">拒绝</el-button>
<el-button type="primary" @click="doReview(true)">通过</el-button> <el-button :loading="formLoading" type="primary" @click="doReview(true)">通过</el-button>
</div> </div>
</template> </template>
@@ -219,6 +219,7 @@ layout("/layouts/platform.html"){
tabKey: '', tabKey: '',
clubOptions: [], clubOptions: [],
tabloading: false, tabloading: false,
formLoading: false,
formData: { formData: {
sign: '' sign: ''
}, },
@@ -242,7 +243,7 @@ layout("/layouts/platform.html"){
} }
}, },
components: { components: {
'club-user-apply-info': httpVueLoader('/components/club/clubUserApplyInfo.vue?v=1.0.1'), 'club-user-apply-info': httpVueLoader('/components/club/clubUserApplyInfo.vue?v=1.0.2'),
'dict-select': httpVueLoader('/components/plugins/DictSelect.vue?v=1.0.0'), 'dict-select': httpVueLoader('/components/plugins/DictSelect.vue?v=1.0.0'),
}, },
methods: { methods: {
@@ -273,18 +274,39 @@ layout("/layouts/platform.html"){
} }
this.$refs.guava.edit() this.$refs.guava.edit()
}, },
async doReview(flag) { doReview(flag) {
const valid = await this.$refs.form.validate() if (this.formLoading) return
if (!valid) return this.$refs.form.validate((valid) => {
this.formData.flag = flag if (!valid || this.formLoading) return
const resp = await $.post(base + "/platform/sys/club/sh/check", this.formData) this.$set(this, 'formLoading', true)
if(resp.code === 0) { this.$set(this.formData, 'flag', flag)
this.doSearch() // 公共签字组件可能读取当前用户的个人 Base64 签名,先上传为路径再提交审核。
this.$refs.guava.index() // uploadSignData 的 data 参数为 PNG Base64,成功响应的 data 为文件路径。
this.$message({message: resp.msg, type: 'success'}) const sign = this.formData.auditSign
} else { const signRequest = sign.indexOf('data:image/png;base64,') === 0
this.$message({message: resp.msg, type: 'warning'}) ? $.post('/signature/uploadSignData', {data: sign}).then((res) => {
} return res.code === 0 && res.data
? $.Deferred().resolve(res.data).promise()
: $.Deferred().reject(res.msg || '签字上传失败').promise()
})
: $.Deferred().resolve(sign).promise()
signRequest.then((path) => {
this.$set(this.formData, 'auditSign', path)
return $.post(base + '/platform/sys/club/sh/check', this.formData)
}).then((resp) => {
if (resp.code === 0) {
this.doSearch()
this.$refs.guava.index()
this.$message({message: resp.msg, type: 'success'})
} else {
this.$message({message: resp.msg, type: 'warning'})
}
}).fail((error) => {
this.$message.error(typeof error === 'string' ? error : '审核提交失败,请重试')
}).always(() => {
this.$set(this, 'formLoading', false)
})
})
}, },
doSearch() { doSearch() {
this.pageForm.pageNumber = 1 this.pageForm.pageNumber = 1
@@ -131,7 +131,7 @@ layout("/layouts/platform.html"){
</div> </div>
<script> <script>
<!--#include('./common/info.js'){}#--> <!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
@@ -184,7 +184,7 @@ layout("/layouts/platform.html"){
<script> <script>
<!--#include('./common/info.js'){}#--> <!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
@@ -159,7 +159,7 @@ layout("/layouts/platform.html"){
</div> </div>
<script> <script>
<!--#include('./common/info.js'){}#--> <!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
@@ -194,7 +194,7 @@ layout("/layouts/platform.html"){
<script> <script>
<!--#include('./common/info.js'){}#--> <!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',
@@ -173,7 +173,7 @@ layout("/layouts/platform.html"){
<script> <script>
<!--#include('./common/info.js'){}#--> <!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: '#app', el: '#app',