会员入会流程

This commit is contained in:
2026-10-14 11:58:06 +08:00
parent 730b342298
commit 3276d2aede
22 changed files with 1341 additions and 199 deletions
@@ -1,6 +1,8 @@
package com.budwk.app.zhgh.staffmanage.member.controller.apply;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
@@ -24,6 +26,49 @@ public class MemberApplyCommonController {
@Inject
private MemberApplyRecordService memberApplyRecordService;
/**
* PC、手机端按申请记录查看当次填写的资料。
*
* @param recordId member_apply_record 的记录 ID,不是会员 userId 或流程任务 ID
* @return Result,data 为申请快照,包含申请人资料及 applyDateTime 申请时间
*/
@At
@SaCheckLogin
@SaCheckPermission(value = {"member.apply.mine", "member.apply.branchUnionAudit", "member.apply.schoolUnionAudit",
"h5.member.apply.mine", "h5.member.apply.branch", "h5.member.apply.school"}, mode = SaMode.OR)
public Result findApplyRecord(@Valid String recordId) {
return Result.success(memberApplyRecordService.findApplyRecordForView(recordId));
}
/**
* 查询当前申请的提交和审核历史。
*
* @param recordId 申请记录 ID,不是会员 ID
* @return Result.data 为记录列表,每项含 nodeCode、nodeName、operatorName、operatorLoginName、
* handledAt、result、originalResult、opinion、signature;撤回后的原审核仍保留办理内容
*/
@At
@SaCheckLogin
@SaCheckPermission(value = {"member.apply.mine", "member.apply.branchUnionAudit", "member.apply.schoolUnionAudit",
"h5.member.apply.mine", "h5.member.apply.branch", "h5.member.apply.school"}, mode = SaMode.OR)
public Result approvalRecords(@Valid String recordId) {
return Result.success(memberApplyRecordService.listApplyApprovalRecords(recordId));
}
/**
* 为两端入会查看页面提供同一份资料变更信息。
*
* @param recordId 申请记录 ID
* @return Result.data 为变更项列表,name 为字段标题,value 为原值,newValue 为申请值
*/
@At
@SaCheckLogin
@SaCheckPermission(value = {"member.apply.mine", "member.apply.branchUnionAudit", "member.apply.schoolUnionAudit",
"h5.member.apply.mine", "h5.member.apply.branch", "h5.member.apply.school"}, mode = SaMode.OR)
public Result findApplyChanges(@Valid String recordId) {
return Result.success(memberApplyRecordService.findApplyChangesForView(recordId));
}
@At
@SaCheckLogin
public Result findOne(@Valid String id) {
@@ -58,6 +58,17 @@ public class MemberApplyController {
public void index() {
}
/**
* 查询当前用户能否新建入会申请,供 PC 入口和 H5 申请页共用。
*
* @return Result.data 为 Boolean;true 表示可新建,false 表示已有进行中的申请
*/
@At
@SaCheckPermission(value = {"member.apply.submit", "h5.member.apply.submit"}, mode = SaMode.OR)
public Result canStartNewApply() {
return Result.success(memberApplyRecordService.canStartNewApply());
}
@At
@SaCheckPermission(value = {"member.apply.submit","member.branchUnion.record","member.schoolUnion.record"}, mode = SaMode.OR)
@@ -77,6 +88,7 @@ public class MemberApplyController {
@SaCheckPermission("member.apply.submit")
@SLog(tag = "会员管理-入会申请-保存申请", msg = "保存申请")
public Result doSave(MemberApplyRecord record){
memberApplyRecordService.checkCanSaveOrSubmit(record.getId());
record.setMember(true);
record.setUserId(SecurityUtil.getUserId());
record.setChangeOrigin(MemberChangeOrigin.PERSONAL.name());
@@ -98,6 +110,7 @@ public class MemberApplyController {
@SaCheckPermission("member.apply.submit")
@SLog(tag = "会员管理-入会申请-提交申请", msg = "提交申请")
public Object doSubmit(MemberApplyRecord record) {
memberApplyRecordService.checkCanSaveOrSubmit(record.getId());
record.setMember(true);
record.setUserId(SecurityUtil.getUserId());
record.setChangeType(MemberChangeType.RESTORE.name());
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.staffmanage.member.controller.apply;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.BpmProcessConstant;
@@ -51,7 +52,7 @@ public class MemberApplyMineController {
@At
@SaCheckPermission("member.apply.mine")
@SaCheckPermission(value = {"member.apply.mine", "h5.member.apply.mine"}, mode = SaMode.OR)
public Result pageData(MemberApplyPageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
@@ -101,12 +102,9 @@ public class MemberApplyMineController {
MemberApplyPageForm.buildSearch(cnd, pageForm);
cnd.and("nd.nodeCode", "=", 10);
cnd.and("define.`code`", "=", BpmProcessConstant.MEMBER_SINGLE_APPLY.name());
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
cnd.andEX("vu.unionId","=", SecurityUtil.getUnionId());
} else {
cnd.and("vu.id", "=", SecurityUtil.getUserId());
}
// “我的申请”仅系统管理员可查看全部,其他角色固定按申请记录归属查询。
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
cnd.and("record.userId", "=", SecurityUtil.getUserId());
}
cnd.desc("record.applyDateTime");
sql.setCondition(cnd);
@@ -117,9 +115,10 @@ public class MemberApplyMineController {
@At
@ApiOperation("删除会员申请记录")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("member.apply.mine")
@SaCheckPermission(value = {"member.apply.mine", "h5.member.apply.mine"}, mode = SaMode.OR)
@SLog(tag = "会员管理-入会申请-我的申请记录", msg = "删除申请")
public Result delete(@Valid String id) {
memberApplyRecordService.checkMineRecordAccess(id);
memberApplyRecordService.delete(id);
bpmService.deleteInstance(id);
return Result.success();
@@ -129,9 +128,10 @@ public class MemberApplyMineController {
@At
@ApiOperation("个人申请撤回")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("member.apply.mine")
@SaCheckPermission(value = {"member.apply.mine", "h5.member.apply.mine"}, mode = SaMode.OR)
@SLog(tag = "会员管理-入会申请-个人申请撤回", msg = "个人申请撤回")
public Result revokeApply(@Valid String id) {
memberApplyRecordService.checkMineRecordAccess(id);
bpmService.revokeApply(id);
return Result.success();
}
@@ -1,7 +1,7 @@
package com.budwk.app.zhgh.staffmanage.member.controller.apply;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.bean.BeanUtil;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.ObjectUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.BpmProcessConstant;
@@ -9,7 +9,6 @@ import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.bpm.enums.BpmProcessTaskStatusEnum;
import com.budwk.app.bpm.enums.BpmTaskApprovalTypeEnum;
import com.budwk.app.bpm.param.BpmTaskApprovalParam;
import com.budwk.app.bpm.service.BpmService;
import com.budwk.app.sys.models.Sys_user_history;
@@ -64,7 +63,7 @@ public class MemberApplySchoolUnionAuditController {
}
@At
@SaCheckPermission("member.apply.schoolUnionAudit")
@SaCheckPermission(value = {"member.apply.schoolUnionAudit", "h5.member.apply.school"}, mode = SaMode.OR)
public Result pageData(MemberApplyPageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
@@ -123,17 +122,19 @@ public class MemberApplySchoolUnionAuditController {
}
/**
* 校工会审核入会申请,PC、H5 共用同一保存入口。
*
* @param approvalParam approval JSON 中的实例、申请、任务 ID,审核类型、意见及签名图片 URL
* @return Result,code 为 0 表示审核及签名保存成功,msg 为结果提示
*/
@At
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("会员入会校工会审核")
@SaCheckPermission("member.apply.branchUnionAudit")
@SaCheckPermission(value = {"member.apply.schoolUnionAudit", "h5.member.apply.school"}, mode = SaMode.OR)
@SLog(tag = "会员管理-会员入会申请-校工会审核", msg = "会员入会审核(校工会)")
public Result approval(@Param("approval") @Valid BpmTaskApprovalParam approvalParam) {
if (approvalParam.getBpmTaskApprovalTypeEnum().equals(BpmTaskApprovalTypeEnum.PASS)) {
MemberApplyRecord record = memberManageService.dao().fetch(MemberApplyRecord.class, Cnd.where("id", "=", approvalParam.getProcessInstanceBusinessId()));
memberManageService.compareMemberChangeInfoAndUpdateUser(record);
}
bpmService.completeTask(approvalParam.getProcessInstanceTaskId(), approvalParam.getBpmTaskApprovalTypeEnum(), null, null);
memberApplyRecordService.approveSchoolApply(approvalParam);
return Result.success();
}
@@ -141,7 +142,7 @@ public class MemberApplySchoolUnionAuditController {
@At
@ApiOperation("校工会审核撤回")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("member.apply.branchUnionAudit")
@SaCheckPermission("member.apply.schoolUnionAudit")
@SLog(tag = "会员管理-入会申请-校工会撤回", msg = "校工会入会审核撤回")
public Result revoke(@Valid String taskId) {
bpmService.revokeTask(taskId);
@@ -155,7 +156,7 @@ public class MemberApplySchoolUnionAuditController {
* @return
*/
@At
@SaCheckPermission(value = "member.apply.branchUnionAudit")
@SaCheckPermission(value = "member.apply.schoolUnionAudit")
public Result findAuditChangeInfo(@Valid String recordId){
Sys_user_history history = memberApplyRecordService.dao().fetch(Sys_user_history.class, Cnd.where("recordId", "=", recordId).or("id","=",recordId));
MemberApplyRecord record = memberApplyRecordService.dao().fetch(MemberApplyRecord.class, recordId);
@@ -1,13 +1,71 @@
package com.budwk.app.zhgh.staffmanage.member.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.bpm.param.BpmTaskApprovalParam;
import com.budwk.app.zhgh.staffmanage.member.models.MemberApplyRecord;
import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* 会员申请记录
*/
public interface MemberApplyRecordService extends BaseService<MemberApplyRecord> {
/**
* 判断当前登录人是否可以新建入会申请。
*
* @return 没有进行中的入会流程时返回 true,否则返回 false
*/
boolean canStartNewApply();
/**
* 校验当前登录人保存或提交的入会申请,已有进行中申请时仅允许操作该申请本身。
*
* @param recordId 本次申请记录 ID;新建申请时为空,编辑已有申请时传该记录 ID
* @throws com.budwk.app.base.exception.BaseException 记录不属于当前用户或存在另一条进行中申请时抛出
*/
void checkCanSaveOrSubmit(String recordId);
/**
* 校验“我的申请”记录访问范围,只有系统管理员可操作他人的申请。
*
* @param recordId member_apply_record 的记录 ID,不能为空
* @throws com.budwk.app.base.exception.BaseException 记录不存在或非系统管理员访问他人记录时抛出
*/
void checkMineRecordAccess(String recordId);
/**
* 查询当前登录人有权查看的申请快照,不使用会员当前资料替代申请内容。
*
* @param recordId member_apply_record 的申请记录 ID
* @return 该次申请保存的 MemberApplyRecord,包含申请人资料及 applyDateTime 申请时间
*/
MemberApplyRecord findApplyRecordForView(String recordId);
/**
* 查询同一次入会申请的提交、重新提交及审核任务,包含待办和撤回状态。
*
* @param recordId 申请记录 ID,按申请归属校验访问权限
* @return 每项包含节点、办理人、时间、结果、撤回前原结果、意见和签名图片 URL;
* 未办理的撤回任务不返回,无流程时返回空列表
*/
List<NutMap> listApplyApprovalRecords(String recordId);
/**
* 查询有权查看的申请所涉及的资料变更。
*
* @param recordId 申请记录 ID
* @return 变更项列表,每项含字段名、原值和新值;已审核记录优先使用保存的变更历史
*/
List<NutMap> findApplyChangesForView(String recordId);
/**
* 校工会办理入会申请,同一事务内更新会员资料并保存审核意见、签名和结果。
*
* @param approvalParam 当前实例、业务和任务标识,审核类型、意见及签名图片 URL
*/
void approveSchoolApply(BpmTaskApprovalParam approvalParam);
void startProcess(MemberApplyRecord memberApplyRecord, Long processDefineId);
}
@@ -1,15 +1,44 @@
package com.budwk.app.zhgh.staffmanage.member.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.constant.BpmProcessConstant;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.bpm.enums.BpmProcessInstanceStatusEnum;
import com.budwk.app.bpm.enums.BpmProcessNodeTypeEnum;
import com.budwk.app.bpm.enums.BpmProcessTaskStatusEnum;
import com.budwk.app.bpm.enums.BpmTaskApprovalTypeEnum;
import com.budwk.app.bpm.models.BpmProcessDefine;
import com.budwk.app.bpm.models.BpmProcessInstance;
import com.budwk.app.bpm.models.BpmProcessNodeDefine;
import com.budwk.app.bpm.models.BpmProcessTask;
import com.budwk.app.bpm.param.BpmTaskApprovalParam;
import com.budwk.app.bpm.service.BpmService;
import com.budwk.app.sys.models.Sys_user_history;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.wf2.enums.FlowConst;
import com.budwk.app.wf2.service.ProcessInstanceService;
import com.budwk.app.zhgh.staffmanage.member.models.MemberApplyRecord;
import com.budwk.app.zhgh.staffmanage.member.service.MemberApplyRecordService;
import com.budwk.app.zhgh.staffmanage.member.service.MemberManageService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class MemberApplyRecordServiceImpl extends BaseServiceImpl<MemberApplyRecord> implements MemberApplyRecordService {
@@ -20,6 +49,237 @@ public class MemberApplyRecordServiceImpl extends BaseServiceImpl<MemberApplyRec
@Inject
private ProcessInstanceService processInstanceService;
@Inject
private BpmService bpmService;
@Inject
private MemberManageService memberManageService;
/**
* 入会申请入口仅在本人没有进行中的同类流程时允许新建。
*
* @return 没有进行中的入会申请时返回 true
*/
@Override
public boolean canStartNewApply() {
return !hasRunningApply(null);
}
/**
* 保存和提交共用的重复申请校验;编辑原申请时排除该记录,避免阻断重新提交。
*
* @param recordId 本次记录 ID;新建时为 null
*/
@Override
public void checkCanSaveOrSubmit(String recordId) {
if (StrUtil.isNotBlank(recordId)) {
MemberApplyRecord record = fetch(recordId);
if (record != null && !StrUtil.equals(SecurityUtil.getUserId(), record.getUserId())) {
throw new BaseException("不能修改他人的入会申请");
}
}
if (hasRunningApply(recordId)) {
throw new BaseException("您有进行中的入会申请,请先处理已有申请");
}
}
/**
* “我的申请”操作只允许系统管理员跨用户访问,普通角色以申请记录归属为准。
*
* @param recordId 申请记录 ID,不能为空
*/
@Override
public void checkMineRecordAccess(String recordId) {
if (StrUtil.isBlank(recordId)) {
throw new BaseException("申请记录不能为空");
}
MemberApplyRecord record = fetch(recordId);
if (record == null) {
throw new BaseException("申请记录不存在");
}
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())
&& !StrUtil.equals(SecurityUtil.getUserId(), record.getUserId())) {
throw new BaseException("没有权限操作该申请记录");
}
}
/**
* 仅匹配当前登录人的入会流程实例,忽略已结束流程和本次编辑的记录。
*
* @param excludedRecordId 本次编辑的申请记录 ID,新建时为空
* @return 是否存在另一条 RUNNING 状态的入会流程
*/
private boolean hasRunningApply(String excludedRecordId) {
List<String> recordIds = query(Cnd.where("userId", "=", SecurityUtil.getUserId())
.and("delFlag", "=", false)).stream()
.map(MemberApplyRecord::getId)
.filter(id -> !StrUtil.equals(id, excludedRecordId))
.toList();
if (recordIds.isEmpty()) {
return false;
}
BpmProcessDefine define = dao().fetch(BpmProcessDefine.class,
Cnd.where("code", "=", BpmProcessConstant.MEMBER_SINGLE_APPLY.name()));
if (define == null) {
throw new BaseException("入会申请流程定义不存在");
}
return dao().count(BpmProcessInstance.class,
Cnd.where("processDefineId", "=", define.getId())
.and("processInstanceBusinessId", "in", recordIds)
.and("delFlag", "=", false)
.and("processInstanceStatus", "=", BpmProcessInstanceStatusEnum.RUNNING.name())) > 0;
}
/**
* 按申请记录归属校验查看范围,保证会员资料后来发生变化时仍展示原申请内容。
*
* @param recordId 申请记录 ID,不能为空
* @return 申请资料及申请时间;记录不存在或无权查看时抛出业务异常
*/
@Override
public MemberApplyRecord findApplyRecordForView(String recordId) {
if (StrUtil.isBlank(recordId)) {
throw new BaseException("申请记录不能为空");
}
MemberApplyRecord record = fetch(recordId);
if (record == null) {
throw new BaseException("申请记录不存在");
}
// 校级管理员可查看全校申请,申请人可查看本人保存的申请。
if (AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(),
RoleConstant.SCHOOL_UNION_MEMBER_ADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())
|| StrUtil.equals(SecurityUtil.getUserId(), record.getUserId())) {
return record;
}
// 分工会主席仅能查看申请所属分工会的记录,空工会 ID 不作为授权依据。
String unionId = SecurityUtil.getUnionId();
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
&& StrUtil.isNotBlank(unionId) && StrUtil.equals(unionId, record.getUnionId())) {
return record;
}
throw new BaseException("没有权限查看该申请记录");
}
@Override
public List<NutMap> listApplyApprovalRecords(String recordId) {
// 先校验申请访问范围,不能仅凭客户端传入的业务 ID 查询他人的审核历史。
findApplyRecordForView(recordId);
List<BpmProcessInstance> instances = dao().query(BpmProcessInstance.class,
Cnd.where("processInstanceBusinessId", "=", recordId).and("delFlag", "=", false));
if (instances.isEmpty()) {
return new ArrayList<>();
}
List<String> instanceIds = instances.stream().map(BpmProcessInstance::getId).toList();
List<BpmProcessTask> tasks = dao().query(BpmProcessTask.class,
Cnd.where("processInstanceId", "in", instanceIds).and("delFlag", "=", false)
.asc("createdOn").asc("createdAt").asc("id"));
if (tasks.isEmpty()) {
return new ArrayList<>();
}
// START、BACK 节点保留申请及重新提交记录,不能像通用审核查询那样只读取 TASK 节点。
List<String> nodeIds = tasks.stream().map(BpmProcessTask::getProcessTaskNodeId).distinct().toList();
Set<String> applyNodeIds = dao().query(BpmProcessNodeDefine.class, Cnd.where("id", "in", nodeIds)).stream()
.filter(node -> BpmProcessNodeTypeEnum.START.name().equals(node.getNodeType())
|| BpmProcessNodeTypeEnum.BACK.name().equals(node.getNodeType()))
.map(BpmProcessNodeDefine::getId).collect(Collectors.toSet());
List<NutMap> records = new ArrayList<>();
for (BpmProcessTask task : tasks) {
boolean complete = BpmProcessTaskStatusEnum.COMPLETE.name().equals(task.getTaskStatus())
|| BpmProcessTaskStatusEnum.AUTO_COMPLETE.name().equals(task.getTaskStatus())
|| BpmProcessTaskStatusEnum.TRANSFER.name().equals(task.getTaskStatus());
JSONObject savedVariable = task.getExtVariable() == null ? new JSONObject() : task.getExtVariable();
String approvalType = StrUtil.blankToDefault(savedVariable.getStr("bpmTaskApprovalType"), "");
// 撤回会把原已办任务恢复为 ACTIVE,但其完成时间和审核变量仍在;这些字段属于原审核记录。
boolean withdrawnApproval = !complete && task.getEndOn() != null
&& List.of("PASS", "REJECT", "BACK", "BACK_OTHER_NODE").contains(approvalType)
&& !applyNodeIds.contains(task.getProcessTaskNodeId());
// 后续自动生成的 REVOKE 任务未经审核,不应显示为一条只有“已撤回”的审核记录。
if (BpmProcessTaskStatusEnum.REVOKE.name().equals(task.getTaskStatus()) && !withdrawnApproval) {
continue;
}
boolean handled = complete || withdrawnApproval;
JSONObject variable = handled ? savedVariable : new JSONObject();
String originalResult = switch (approvalType) {
case "PASS" -> "同意";
case "REJECT" -> "建议撤销";
case "BACK", "BACK_OTHER_NODE" -> "退回";
default -> null;
};
String result = switch (StrUtil.blankToDefault(task.getTaskStatus(), "")) {
case "ACTIVE" -> withdrawnApproval ? "已撤回" : "待处理";
case "REVOKE" -> "已撤回";
case "TRANSFER" -> "已转办";
case "AUTO_COMPLETE" -> "自动完成";
case "COMPLETE" -> switch (approvalType) {
case "PASS" -> "同意";
case "REJECT" -> "建议撤销";
case "BACK", "BACK_OTHER_NODE" -> "退回";
case "APPLY" -> "已提交";
default -> applyNodeIds.contains(task.getProcessTaskNodeId()) ? "已提交" : "已办理";
};
default -> "暂无";
};
records.add(NutMap.NEW()
.addv("id", task.getId()).addv("nodeCode", task.getProcessTaskNodeCode())
.addv("nodeName", task.getProcessTaskNodeName())
.addv("operatorName", handled ? task.getActualOwnerUserName() : null)
.addv("operatorLoginName", handled ? task.getActualOwnerLoginName() : null)
.addv("handledAt", handled && task.getEndOn() != null ? DateUtil.formatDateTime(task.getEndOn()) : null)
.addv("result", result)
.addv("originalResult", withdrawnApproval ? originalResult : null)
.addv("opinion", variable.getStr("approvalOpinion"))
.addv("signature", variable.getStr("approvalSignature")));
}
return records;
}
@Override
public List<NutMap> findApplyChangesForView(String recordId) {
MemberApplyRecord record = findApplyRecordForView(recordId);
// 审核后会员当前资料已经更新,应优先使用当时保存的变更信息。
Sys_user_history history = dao().fetch(Sys_user_history.class,
Cnd.where("recordId", "=", recordId).or("id", "=", recordId));
return history == null ? memberManageService.compareMemberChangeInfo(record)
: memberManageService.compareMemberChangeInfo(history);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void approveSchoolApply(BpmTaskApprovalParam approvalParam) {
BpmTaskApprovalTypeEnum approvalType = approvalParam.getBpmTaskApprovalTypeEnum();
if (!List.of(BpmTaskApprovalTypeEnum.PASS, BpmTaskApprovalTypeEnum.BACK, BpmTaskApprovalTypeEnum.REJECT).contains(approvalType)) {
throw new BaseException("校工会审核类型不正确");
}
BpmProcessTask task = dao().fetch(BpmProcessTask.class, approvalParam.getProcessInstanceTaskId());
if (task == null || Boolean.TRUE.equals(task.getDelFlag())
|| !BpmProcessTaskStatusEnum.ACTIVE.name().equals(task.getTaskStatus())) {
throw new BaseException("审核任务不存在或已处理,请刷新后重试");
}
BpmProcessInstance instance = dao().fetch(BpmProcessInstance.class, task.getProcessInstanceId());
// 任务、实例和申请必须属于同一次审核,避免用另一条申请的参数更新会员资料。
if (instance == null || !StrUtil.equals(instance.getId(), approvalParam.getProcessInstanceId())
|| !StrUtil.equals(instance.getProcessInstanceBusinessId(), approvalParam.getProcessInstanceBusinessId())
|| !Integer.valueOf(50).equals(task.getProcessTaskNodeCode())) {
throw new BaseException("审核任务与申请不匹配");
}
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())
&& (task.getAssignments() == null || !task.getAssignments().contains(SecurityUtil.getUserLoginname()))) {
throw new BaseException("当前用户不是该任务的审核人");
}
MemberApplyRecord record = fetch(approvalParam.getProcessInstanceBusinessId());
if (record == null) {
throw new BaseException("申请记录不存在");
}
if (approvalType == BpmTaskApprovalTypeEnum.PASS) {
memberManageService.compareMemberChangeInfoAndUpdateUser(record);
}
// 完成任务时将意见、签名 URL、审核类型一并写入任务扩展变量,供 PC、手机端回显。
bpmService.completeTask(task.getId(), approvalType, BeanUtil.beanToMap(approvalParam), null);
}
@Override
public void startProcess(MemberApplyRecord record, Long processDefineId) {
Dict dict = Dict.create();
@@ -8,7 +8,9 @@
</template>
</div>
<van-popup v-model="showSignaturePanel" :style="{ height: '100vh', width: '100vw' }">
<van-popup v-model="showSignaturePanel" get-container="body" :close-on-click-overlay="false"
:style="{ height: '100vh', width: '100vw' }">
<van-button class="signature-close" size="small" native-type="button" @click="closeSignature">返回</van-button>
<signature @save="save"></signature>
</van-popup>
</div>
@@ -42,7 +44,12 @@ module.exports = {
},
methods: {
openSignature() {
this.showSignaturePanel = true
// 签字弹框独立占一层历史;返回键先关闭签字板,再返回审核详情。
h5PopupHistory.open(this._uid)
this.$set(this, "showSignaturePanel", true)
},
closeSignature() {
h5PopupHistory.close(this._uid)
},
save(signature) {
@@ -61,8 +68,8 @@ module.exports = {
.then((res) => {
if (res.code === 0) {
alert("保存成功")
this.showSignaturePanel = false
this.$emit("input", res.data)
this.closeSignature()
} else {
alert("保存失败")
}
@@ -87,7 +94,14 @@ module.exports = {
return new File([blob], filename, { type: contentType })
}
},
created() {}
mounted() {
h5PopupHistory.register(this._uid, () => {
this.$set(this, "showSignaturePanel", false)
})
},
beforeDestroy() {
h5PopupHistory.unregister(this._uid)
}
}
</script>
@@ -101,4 +115,11 @@ module.exports = {
.main-panel .open-button {
}
.signature-close {
position: fixed;
top: 12px;
right: 12px;
z-index: 1;
}
</style>
@@ -1,6 +1,6 @@
<template>
<div class="signature-wrap">
<canvas id="canvas"></canvas>
<canvas ref="canvas"></canvas>
<div class="action-buttons">
<button @click="clear" class="action-button-danger" type="button">清空</button>
<button @click="undo" class="action-button-warning" type="button">撤销</button>
@@ -30,7 +30,6 @@
// window.addEventListener("orientationchange", a)
// a()
let signature = null
module.exports = {
name: "sysSignature",
data() {
@@ -40,22 +39,22 @@ module.exports = {
},
methods: {
clear() {
signature.clear()
this.signature.clear()
},
undo() {
signature.undo()
this.signature.undo()
},
rotate() {
signature.getRotateCanvas(90)
this.signature.getRotateCanvas(90)
},
save() {
if (signature.isEmpty()) {
if (this.signature.isEmpty()) {
alert("请签字后再保存")
return
}
// const png = signature.getPNG()
// const png = this.signature.getPNG()
//旋转一下 横过来
const rotateCanvas = signature.getRotateCanvas(-90)
const rotateCanvas = this.signature.getRotateCanvas(-90)
this.$emit("save", rotateCanvas.toDataURL())
}
},
@@ -63,10 +62,11 @@ module.exports = {
const innerWidth = window.innerWidth
const innerHeight = window.innerHeight
const canvas = document.getElementById("canvas")
// PJAX 切换后旧弹框可能仍在 body 中,只初始化当前签字组件自己的画布。
const canvas = this.$refs.canvas
canvas.width = window.innerWidth
canvas.height = window.innerHeight
signature = new SmoothSignature(canvas, {
this.signature = new SmoothSignature(canvas, {
width: innerWidth - 30,
height: innerHeight - 30,
scale: 2,
@@ -81,6 +81,58 @@
callback(true)
}
// 所有 H5 弹框共用一个返回键监听;每打开一层都对应一条浏览器历史记录。
const h5PopupHistory = (() => {
const stack = []
const listeners = {}
let nextId = 0
let afterClear = null
window.addEventListener("popstate", (event) => {
const currentId = event.state && event.state.h5PopupId
while (stack.length && stack[stack.length - 1].id !== currentId) {
const entry = stack.pop()
if (listeners[entry.owner]) {
listeners[entry.owner]()
}
}
if (!stack.length && afterClear) {
const callback = afterClear
afterClear = null
callback()
}
})
return {
register(owner, onClose) {
listeners[owner] = onClose
},
unregister(owner) {
delete listeners[owner]
},
open(owner) {
if (!listeners[owner]) {
return
}
const id = ++nextId
stack.push({ id, owner })
// 保留 pjax 的 state.id,让弹框返回不触发业务页面重载。
window.history.pushState(Object.assign({}, window.history.state || {}, { h5PopupId: id }), "", window.location.href)
},
close(owner) {
if (stack.length && stack[stack.length - 1].owner === owner) {
window.history.back()
}
},
clear(callback) {
if (!stack.length) {
callback()
return
}
afterClear = callback
window.history.go(-stack.length)
}
}
})()
//初始化pjax
;(function initPjax() {
if ($.support.pjax) {
@@ -103,15 +155,17 @@
})()
function pjaxReplace(url, data = null) {
$.pjax({
url: url,
container: "#container",
maxCacheLength: 0,
push: false,
replace: true,
fragment: "#container",
timeout: 8000,
data: data
h5PopupHistory.clear(() => {
$.pjax({
url: url,
container: "#container",
maxCacheLength: 0,
push: false,
replace: true,
fragment: "#container",
timeout: 8000,
data: data
})
})
}
@@ -80,33 +80,44 @@ layout("/layouts/platform.html"){
</el-card>
</template>
<template #view>
<member-info ref="memberInfoRef"></member-info>
<member-apply-audit-info ref="memberApplyAuditInfoRef"></member-apply-audit-info>
</template>
<template #public>
<member-audit-change-info ref="memberAuditChangeInfoRef"></member-audit-change-info>
<member-apply-audit-info ref="memberPublicApplyInfoRef"></member-apply-audit-info>
<div v-if="showApprovalForm">
<div class="process-title">{{formData.processInstanceNodeName}}</div>
<el-form :model="formData" ref="approvalFormRef" label-position="left" label-width="80px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="审核人员">
<el-input :value="reviewerName" readonly></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审核时间">
<el-input :value="reviewTime" readonly></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="审核意见" prop="approvalOpinion" :rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.approvalOpinion"></user-opinion-textarea>
</el-form-item>
<el-form-item label="电子签名" prop="approvalSignature" :rules="[{required:false,message:'必填',trigger:['change','blur']}]">
<el-form-item label="电子签名" prop="approvalSignature" :rules="[{required:true,message:'请签字',trigger:['change','blur']}]">
<pc-signature v-model="formData.approvalSignature"></pc-signature>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button plain @click="$refs.guava.index()">取消</el-button>
<el-button type="danger" @click="doApproval('BACK')">退回重新申请</el-button>
<el-button type="danger" @click="doApproval('REJECT')">拒绝申请</el-button>
<el-button type="primary" @click="doApproval('PASS')">同意申请</el-button>
<el-button type="danger" :loading="formLoading" :disabled="formLoading" @click="doApproval('BACK')">退回重新申请</el-button>
<el-button type="danger" :loading="formLoading" :disabled="formLoading" @click="doApproval('REJECT')">拒绝</el-button>
<el-button type="primary" :loading="formLoading" :disabled="formLoading" @click="doApproval('PASS')">同意申请</el-button>
</el-row>
</div>
</template>
</guava>
</div>
<script>
<!--#include("../../common/info/memberInfo.js"){}#-->
<!--#include("../../common/audit/memberAuditChangeInfo.js"){}#-->
<!--#include("../../common/info/memberApplyAuditInfo.js"){}#-->
new Vue({
el: "#app",
store,
@@ -128,36 +139,53 @@ layout("/layouts/platform.html"){
],
unions: [],
units: [],
showApprovalForm: false
showApprovalForm: false,
reviewerName: "",
reviewTime: ""
}
},
components: {
'member-info': MEMBER_INFO,
'member-audit-change-info': MEMBER_AUDIT_CHANGE_INFO,
'member-apply-audit-info': MEMBER_APPLY_AUDIT_INFO,
},
methods: {
openView(row) {
this.$refs.guava.view(() => {
this.$refs.memberInfoRef.onOpen(row.userId)
this.$refs.memberApplyAuditInfoRef.onOpen(row.id)
})
},
openChangeView(row) {
this.$refs.guava.public(() => {
this.showApprovalForm = false
this.$refs.memberAuditChangeInfoRef.onOpen({recordId: row.id, userId: row.userId})
this.$refs.memberPublicApplyInfoRef.onOpen(row.id)
})
},
openApproval(row) {
this.$refs.guava.public(() => {
this.showApprovalForm = true
this.formData = row.approvalParam
this.$refs.memberAuditChangeInfoRef.onOpen({recordId: row.id, userId: row.userId})
// 审核与查看使用同一份申请快照及历史,当前审核信息只作为表单展示。
// 意见和签字预先声明为响应式字段,签字组件异步回填后才能触发表单校验。
this.$set(this, "formData", {
...row.approvalParam,
approvalOpinion: row.approvalParam.approvalOpinion || "",
approvalSignature: row.approvalParam.approvalSignature || ""
})
this.$set(this, "reviewerName", this.$store.state.user.username || "")
this.$set(this, "reviewTime", moment().format("YYYY-MM-DD HH:mm:ss"))
this.$refs.memberPublicApplyInfoRef.onOpen(row.id)
})
},
/**
* approvalType 传 BACK、REJECT 或 PASS;approval 为包含任务 ID、意见、签字 URL 的 JSON 字符串。
* 接口返回 Result,code 为 0 时表示审核成功,msg 为提示信息。
*/
doApproval(approvalType) {
this.formData.bpmTaskApprovalType = approvalType
if (this.formLoading) {
return
}
this.$set(this.formData, "bpmTaskApprovalType", approvalType)
this.$refs.approvalFormRef.validate((valid) => {
if (valid) {
this.formLoading = true
this.$axios
.post(loc() + "/approval", {
approval: JSON.stringify(this.formData)
@@ -169,6 +197,9 @@ layout("/layouts/platform.html"){
this.doSearch()
}
})
.finally(() => {
this.formLoading = false
})
}
})
},
@@ -9,7 +9,7 @@ layout("/layouts/platform.html"){
<search-item label="姓名/工号">
<el-input placeholder="请输入内容" clearable v-model="pageForm.searchKeyword"></el-input>
</search-item>
<template v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN', 'BRANCH_UNION_CHAIRMAN'])">
<template v-if="$auth.hasRole('SYSADMIN')">
<search-item label="所属工会">
<el-select @change="flushUnits" @clear="flushUnits" clearable filterable
placeholder="所属工会" style="width: 100%;" v-model="pageForm.unionId">
@@ -42,7 +42,8 @@ layout("/layouts/platform.html"){
<el-card shadow="never" class="mt10">
<table-tool label="申请列表">
<el-button size="small" type="primary"
<el-button size="small" type="primary" :disabled="!canStartNewApply"
:title="canStartNewApply ? '' : '已有进行中的入会申请'"
@click="$store.dispatch('pjaxRoute', '/platform/member/apply/submit')">申请入会</el-button>
</table-tool>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder" ref="table" row-key="id" style="width: 100%">
@@ -59,6 +60,10 @@ layout("/layouts/platform.html"){
<template scope="{row}" v-if="column.prop=='loginname'">
<el-link @click="openView(row)" type="primary">{{row.loginname}}</el-link>
</template>
<!-- 开始节点仍为申请人待提交阶段,仅调整列表中的显示文案。 -->
<template scope="{row}" v-if="column.prop=='processInstanceNodeName'">
{{row.processInstanceNodeCode === 10 ? '待提交' : row.processInstanceNodeName}}
</template>
</el-table-column>
<el-table-column label="操作" width="220">
<template slot-scope="{row}">
@@ -90,13 +95,13 @@ layout("/layouts/platform.html"){
</template>
<template #view>
<member-info ref="memberInfoRef"></member-info>
<member-apply-audit-info ref="memberApplyAuditInfoRef"></member-apply-audit-info>
</template>
</guava>
</div>
<script type="application/javascript">
<!--#include("../../common/info/memberInfo.js"){}#-->
<!--#include("../../common/info/memberApplyAuditInfo.js"){}#-->
new Vue({
el: "#app",
@@ -115,13 +120,22 @@ layout("/layouts/platform.html"){
{ prop: "processInstanceNodeName", label: "当前节点", sortable: true }
],
unions: [],
units: []
units: [],
canStartNewApply: false
}
},
components: {
"member-info": MEMBER_INFO,
"member-apply-audit-info": MEMBER_APPLY_AUDIT_INFO,
},
methods: {
// 入口状态以当前登录人的进行中流程为准,列表筛选条件不影响判断。
loadCanStartNewApply() {
this.$axios.post("/platform/member/apply/submit/canStartNewApply", {}).then((res) => {
if (res.code === 0) {
this.canStartNewApply = res.data === true
}
})
},
doDelete(row) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
@@ -131,6 +145,7 @@ layout("/layouts/platform.html"){
this.$axios.post(loc() + "/delete", { id: row.id }).then((resp) => {
this.$message.success(resp.msg)
this.doSearch()
this.loadCanStartNewApply()
})
})
},
@@ -148,24 +163,19 @@ layout("/layouts/platform.html"){
},
openView(row) {
this.$refs.guava.view(() => {
this.$refs.memberInfoRef.onOpen(row.userId)
this.$refs.memberApplyAuditInfoRef.onOpen(row.id)
})
},
initData() {
if (this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])) {
this.loadCanStartNewApply()
// 跨人员筛选仅系统管理员使用,其他角色的列表只查询本人申请。
if (this.$auth.hasRole("SYSADMIN")) {
this.$businessTool.listUnion(this.pageForm.unionId).then((data) => {
this.unions = data
})
this.$businessTool.listUnit().then((data) => {
this.units = data
})
} else if (this.$auth.hasRole("BRANCH_UNION_CHAIRMAN")){
this.$businessTool.listUnion(this.$store.state.user.union.id).then((data) => {
this.unions = data
})
this.$businessTool.listUnit(this.$store.state.user.union.id).then((data) => {
this.units = data
})
}
this.doSearch()
},
@@ -80,33 +80,44 @@ layout("/layouts/platform.html"){
</el-card>
</template>
<template #view>
<member-info ref="memberInfoRef"></member-info>
<member-apply-audit-info ref="memberApplyAuditInfoRef"></member-apply-audit-info>
</template>
<template #public>
<member-audit-change-info ref="memberAuditChangeInfoRef"></member-audit-change-info>
<member-apply-audit-info ref="memberPublicApplyInfoRef"></member-apply-audit-info>
<div v-if="showApprovalForm">
<div class="process-title">{{formData.processInstanceNodeName}}</div>
<el-form :model="formData" ref="approvalFormRef" label-position="left" label-width="80px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="审核人员">
<el-input :value="reviewerName" readonly></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审核时间">
<el-input :value="reviewTime" readonly></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="审核意见" prop="approvalOpinion" :rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.approvalOpinion"></user-opinion-textarea>
</el-form-item>
<el-form-item label="电子签名" prop="approvalSignature" :rules="[{required:false,message:'必填',trigger:['change','blur']}]">
<el-form-item label="电子签名" prop="approvalSignature" :rules="[{required:true,message:'请签字',trigger:['change','blur']}]">
<pc-signature v-model="formData.approvalSignature"></pc-signature>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button plain @click="$refs.guava.index()">取消</el-button>
<el-button type="danger" @click="doApproval('BACK')">退回重新申请</el-button>
<el-button type="danger" @click="doApproval('REJECT')">拒绝申请</el-button>
<el-button type="primary" @click="doApproval('PASS')">同意申请</el-button>
<el-button type="danger" :loading="formLoading" :disabled="formLoading" @click="doApproval('BACK')">退回重新申请</el-button>
<el-button type="danger" :loading="formLoading" :disabled="formLoading" @click="doApproval('REJECT')">拒绝</el-button>
<el-button type="primary" :loading="formLoading" :disabled="formLoading" @click="doApproval('PASS')">同意申请</el-button>
</el-row>
</div>
</template>
</guava>
</div>
<script>
<!--#include("../../common/info/memberInfo.js"){}#-->
<!--#include("../../common/audit/memberAuditChangeInfo.js"){}#-->
<!--#include("../../common/info/memberApplyAuditInfo.js"){}#-->
new Vue({
el: "#app",
store,
@@ -128,36 +139,53 @@ layout("/layouts/platform.html"){
],
unions: [],
units: [],
showApprovalForm: false
showApprovalForm: false,
reviewerName: "",
reviewTime: ""
}
},
components: {
'member-info': MEMBER_INFO,
'member-audit-change-info': MEMBER_AUDIT_CHANGE_INFO,
'member-apply-audit-info': MEMBER_APPLY_AUDIT_INFO,
},
methods: {
openView(row) {
this.$refs.guava.view(() => {
this.$refs.memberInfoRef.onOpen(row.userId)
this.$refs.memberApplyAuditInfoRef.onOpen(row.id)
})
},
openChangeView(row) {
this.$refs.guava.public(() => {
this.showApprovalForm = false
this.$refs.memberAuditChangeInfoRef.onOpen({recordId: row.id, userId: row.userId})
this.$refs.memberPublicApplyInfoRef.onOpen(row.id)
})
},
openApproval(row) {
this.$refs.guava.public(() => {
this.showApprovalForm = true
this.formData = row.approvalParam
this.$refs.memberAuditChangeInfoRef.onOpen({recordId: row.id, userId: row.userId})
// 审核与查看使用同一份申请快照及历史,当前审核信息只作为表单展示。
// 意见和签字预先声明为响应式字段,签字组件异步回填后才能触发表单校验。
this.$set(this, "formData", {
...row.approvalParam,
approvalOpinion: row.approvalParam.approvalOpinion || "",
approvalSignature: row.approvalParam.approvalSignature || ""
})
this.$set(this, "reviewerName", this.$store.state.user.username || "")
this.$set(this, "reviewTime", moment().format("YYYY-MM-DD HH:mm:ss"))
this.$refs.memberPublicApplyInfoRef.onOpen(row.id)
})
},
/**
* approvalType 传 BACK、REJECT 或 PASS;approval 为包含任务 ID、意见、签字 URL 的 JSON 字符串。
* 接口返回 Result,code 为 0 时表示审核成功,msg 为提示信息。
*/
doApproval(approvalType) {
this.formData.bpmTaskApprovalType = approvalType
if (this.formLoading) {
return
}
this.$set(this.formData, "bpmTaskApprovalType", approvalType)
this.$refs.approvalFormRef.validate((valid) => {
if (valid) {
this.formLoading = true
this.$axios
.post(loc() + "/approval", {
approval: JSON.stringify(this.formData)
@@ -169,6 +197,9 @@ layout("/layouts/platform.html"){
this.doSearch()
}
})
.finally(() => {
this.formLoading = false
})
}
})
},
@@ -124,7 +124,7 @@ layout("/layouts/platform.html"){
<el-table-column width="100px">
<template slot="header" slot-scope="scope">
<el-button type="primary" size="mini"
@click="formData.families.push({})">添加
@click="addFamily">添加
</el-button>
</template>
<template slot-scope="scope">
@@ -151,8 +151,8 @@ layout("/layouts/platform.html"){
<el-row v-if="!this.$store.state.user.member" justify="end" type="flex" class="mt10">
<!-- <el-row justify="end" type="flex" class="mt10">-->
<el-button v-if="formData.id" @click="$store.dispatch('pjaxRoute', '/platform/member/apply/mine')">返回</el-button>
<el-button @click="doSave" type="primary">保存申请</el-button>
<el-button @click="doSubmit" type="primary">提交申请</el-button>
<el-button @click="doSave" type="primary" :disabled="formDisabled">保存申请</el-button>
<el-button @click="doSubmit" type="primary" :disabled="formDisabled">提交申请</el-button>
</el-row>
</el-card>
</div>
@@ -185,11 +185,41 @@ layout("/layouts/platform.html"){
}
},
methods: {
// 家庭成员可能由用户资料或申请记录以 JSON 字符串返回,表格编辑需要数组。
normalizeFamilies(value) {
if (Array.isArray(value)) {
return value
}
if (typeof value === "string" && value) {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch (error) {
return []
}
}
return []
},
// 添加前确保家庭成员字段可编辑,兼容页面回填后的异常数据类型。
addFamily() {
if (!Array.isArray(this.formData.families)) {
this.$set(this.formData, "families", this.normalizeFamilies(this.formData.families))
}
this.formData.families.push({})
},
// 表单编码请求需将家庭成员数组作为 JSON 字符串传给后端,保留页面中的数组供继续编辑。
buildSubmitPayload() {
return {
...this.formData,
families: JSON.stringify(this.normalizeFamilies(this.formData.families))
}
},
init() {
if (this.id) {
this.$axios.post(loc() + "/findApplyById", { id: this.id }).then((res) => {
if (res.code === 0) {
this.formData = res.data
this.$set(this.formData, "families", this.normalizeFamilies(this.formData.families))
}
})
} else {
@@ -202,6 +232,8 @@ layout("/layouts/platform.html"){
nation,
political,
education,
userState,
preparedBy,
governmentPosition,
mobile,
email,
@@ -220,6 +252,9 @@ layout("/layouts/platform.html"){
this.$set(this.formData, "nation", nation)
this.$set(this.formData, "political", political)
this.$set(this.formData, "education", education)
// 新建申请时沿用登录用户的人事信息,保证两个字典字段在提交前有对应值。
this.$set(this.formData, "userState", userState)
this.$set(this.formData, "preparedBy", preparedBy)
this.$set(this.formData, "unitName", unit ? unit.name : null)
this.$set(this.formData, "unitId", unit ? unit.id : null)
this.$set(this.formData, "unionName", union ? union.name : null)
@@ -229,7 +264,7 @@ layout("/layouts/platform.html"){
this.$set(this.formData, "mobile", mobile)
this.$set(this.formData, "email", email)
this.$set(this.formData, "wantJoinLoveMutualAidAssociation", wantJoinLoveMutualAidAssociation)
this.$set(this.formData, "families", families ? families : [])
this.$set(this.formData, "families", this.normalizeFamilies(families))
this.$set(this.formData, "personalData", personalData)
}
},
@@ -237,7 +272,7 @@ layout("/layouts/platform.html"){
this.$confirm("保存后可在我的申请里面再次编辑,您确定要保存吗?", "提示", { type: "warning" })
.then(() => {
this.$axios
.post(loc() + "/doSave", this.formData)
.post(loc() + "/doSave", this.buildSubmitPayload())
.then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
@@ -250,9 +285,7 @@ layout("/layouts/platform.html"){
doSubmit() {
this.$confirm("您确定要提交申请吗?", "提示", { type: "warning" })
.then(() => {
this.$axios.post(loc() + "/doSubmit", {
...this.formData,
}).then((res) => {
this.$axios.post(loc() + "/doSubmit", this.buildSubmitPayload()).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.$store.dispatch("pjaxRoute", "/platform/member/apply/mine")
@@ -262,9 +295,25 @@ layout("/layouts/platform.html"){
.catch()
}
},
async created() {
this.init()
this.units = await this.$businessTool.listUnit()
created() {
this.$businessTool.listUnit().then((data) => {
this.units = data
})
if (this.id) {
this.init()
return
}
// 直接进入新建页面时也检查本人是否已有进行中的入会流程。
this.formDisabled = true
this.$axios.post(loc() + "/canStartNewApply", {}).then((res) => {
if (res.code === 0 && res.data === true) {
this.init()
this.formDisabled = false
} else {
this.$message.warning("您有进行中的入会申请,请先处理已有申请")
this.$store.dispatch("pjaxRoute", "/platform/member/apply/mine")
}
})
}
})
</script>
@@ -56,6 +56,7 @@ const MEMBER_AUDIT_CHANGE_INFO = {
</el-table>
<el-empty v-else :image-size="50" description="未获取到变更信息"></el-empty>
</div>
<slot></slot>
</el-card>
`,
props: {
@@ -0,0 +1,134 @@
const MEMBER_APPLY_AUDIT_INFO = {
template: /*language=HTML*/ `
<div v-loading="viewLoading">
<el-tabs v-model="activeName">
<el-tab-pane label="申请信息" name="apply">
<el-descriptions :column="3" border class="descriptions-form">
<el-descriptions-item label="申请时间" :span="3">{{ viewData.applyDateTime || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="工号">{{ viewData.loginname }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewData.username }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ viewData.sex }}</el-descriptions-item>
<el-descriptions-item label="民族">{{ viewData.nation }}</el-descriptions-item>
<el-descriptions-item label="在职状态">{{ viewData.userState }}</el-descriptions-item>
<el-descriptions-item label="人事编制">{{ viewData.preparedBy }}</el-descriptions-item>
<el-descriptions-item label="人员类型">{{ viewData.personType }}</el-descriptions-item>
<el-descriptions-item label="学历">{{ viewData.education }}</el-descriptions-item>
<el-descriptions-item label="政治面貌">{{ viewData.political }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ viewData.idCard }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewData.mobile }}</el-descriptions-item>
<el-descriptions-item label="电子邮箱">{{ viewData.email }}</el-descriptions-item>
<el-descriptions-item label="所属工会">{{ viewData.unionName }}</el-descriptions-item>
<el-descriptions-item label="工作单位">{{ viewData.unitName }}</el-descriptions-item>
<el-descriptions-item label="岗位职务">{{ viewData.governmentPosition }}</el-descriptions-item>
<el-descriptions-item label="是否愿意加入大病互助基金会" :span="3">{{ viewData.wantJoinLoveMutualAidAssociation ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="家庭主要成员及其工作单位" :span="3">
<el-table v-if="families.length" :data="families" size="mini" border style="width: 100%">
<el-table-column prop="relation" label="与本人关系"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="unit" label="单位"></el-table-column>
<el-table-column prop="remark" label="备注"></el-table-column>
</el-table>
<span v-else>暂无</span>
</el-descriptions-item>
<el-descriptions-item label="个人简况" :span="3">
<div v-if="viewData.personalData" v-html="viewData.personalData"></div>
<span v-else>暂无</span>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="3">{{ viewData.notes || '暂无' }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane v-if="branchRecords.length" label="分工会审核信息" name="branch">
<div class="process-title">分工会审核信息</div>
<el-descriptions v-for="record in branchRecords" :key="record.id" :column="2" border class="descriptions-form mb15">
<el-descriptions-item label="审核人员">{{ record.operatorName || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="审核时间">{{ record.handledAt || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="审核结果" :span="2">{{ record.result }}</el-descriptions-item>
<el-descriptions-item v-if="record.originalResult" label="原审核结果" :span="2">{{ record.originalResult }}</el-descriptions-item>
<el-descriptions-item label="审核意见" :span="2">{{ record.opinion || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="签字" :span="2">
<el-image v-if="record.signature" :src="record.signature" fit="contain" style="width: 180px; height: 80px"></el-image>
<span v-else>暂无</span>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane v-if="schoolRecords.length" label="校工会审核信息" name="school">
<div class="process-title">校工会审核信息</div>
<el-descriptions v-for="record in schoolRecords" :key="record.id" :column="2" border class="descriptions-form mb15">
<el-descriptions-item label="审核人员">{{ record.operatorName || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="审核时间">{{ record.handledAt || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="审核结果" :span="2">{{ record.result }}</el-descriptions-item>
<el-descriptions-item v-if="record.originalResult" label="原审核结果" :span="2">{{ record.originalResult }}</el-descriptions-item>
<el-descriptions-item label="审核意见" :span="2">{{ record.opinion || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="签字" :span="2">
<el-image v-if="record.signature" :src="record.signature" fit="contain" style="width: 180px; height: 80px"></el-image>
<span v-else>暂无</span>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
<slot></slot>
</div>
`,
data() {
return {
activeName: "apply",
viewData: {},
approvalRecords: [],
viewLoading: false,
requestId: 0
}
},
computed: {
families() {
const value = this.viewData.families
if (Array.isArray(value)) {
return value
}
if (typeof value === "string" && value) {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch (error) {
return []
}
}
return []
},
// 申请人任务不展示;20、50 节点分别为分工会和校工会审核记录。
branchRecords() {
return this.approvalRecords.filter((record) => Number(record.nodeCode) === 20 && record.handledAt)
},
schoolRecords() {
return this.approvalRecords.filter((record) => Number(record.nodeCode) === 50 && record.handledAt)
}
},
methods: {
/**
* recordId 为 member_apply_record.id。两个接口的 Result.data 分别为当次申请快照和流程记录列表;
* 记录包含 nodeCode、办理人、时间、结果、原审核结果、意见与签字 URL;只展示审核节点。
*/
onOpen(recordId) {
this.$set(this, "activeName", "apply")
this.$set(this, "viewData", {})
this.$set(this, "approvalRecords", [])
this.$set(this, "requestId", this.requestId + 1)
const requestId = this.requestId
this.$set(this, "viewLoading", true)
return Promise.all([
this.$axios.post("/platform/member/apply/common/findApplyRecord", { recordId }),
this.$axios.post("/platform/member/apply/common/approvalRecords", { recordId })
])
.then(([applyRes, recordsRes]) => {
if (requestId === this.requestId && applyRes.code === 0) {
this.$set(this, "viewData", applyRes.data || {})
this.$set(this, "approvalRecords", recordsRes.code === 0 ? recordsRes.data || [] : [])
}
})
.finally(() => {
if (requestId === this.requestId) {
this.$set(this, "viewLoading", false)
}
})
}
}
}
@@ -1,7 +1,9 @@
const MEMBER_INFO = {
template: `
<div>
template: /*language=HTML*/ `
<div v-loading="viewLoading">
<div v-if="applyRecordId" class="process-title">本次申请</div>
<el-descriptions :column="3" border class="descriptions-form">
<el-descriptions-item v-if="applyRecordId" label="申请时间" :span="3">{{ viewData.applyDateTime || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewData.username + '(' + viewData.loginname + ')' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ viewData.sex }}</el-descriptions-item>
<el-descriptions-item label="民族">{{ viewData.nation }}</el-descriptions-item>
@@ -45,20 +47,75 @@
<span v-else>无数据</span>
</el-descriptions-item>
</el-descriptions>
<div v-if="applyRecordId" class="mt20">
<div class="process-title">申请及审核记录</div>
<el-table :data="approvalRecords" border size="small" row-key="id" empty-text="暂无申请及审核记录">
<el-table-column prop="nodeName" label="节点" min-width="130"></el-table-column>
<el-table-column label="办理人" min-width="120">
<template slot-scope="scope">
{{ scope.row.operatorName || '暂无' }}
<span v-if="scope.row.operatorLoginName">({{ scope.row.operatorLoginName }})</span>
</template>
</el-table-column>
<el-table-column label="处理时间" min-width="160">
<template slot-scope="scope">{{ scope.row.handledAt || '暂无' }}</template>
</el-table-column>
<el-table-column prop="result" label="结果" min-width="100"></el-table-column>
<el-table-column label="审核意见" min-width="160">
<template slot-scope="scope">{{ scope.row.opinion || '暂无' }}</template>
</el-table-column>
<el-table-column label="签字" min-width="160">
<template slot-scope="scope">
<el-image v-if="scope.row.signature" :src="scope.row.signature" fit="contain" style="width: 150px; height: 70px"></el-image>
<span v-else>暂无</span>
</template>
</el-table-column>
</el-table>
</div>
<slot></slot>
</div>
`,
data() {
return {
viewData: {}
viewData: {},
approvalRecords: [],
applyRecordId: "",
viewLoading: false,
viewRequestId: 0
}
},
methods: {
onOpen(id) {
this.$axios.post("/platform/member/apply/common/findOne", { id }).then((res) => {
if (res.code === 0) {
this.viewData = res.data
}
})
/**
* id 为会员 userId,recordId 可选,传入时查看该次申请快照。
* 接口返回 Result,data 为会员资料或申请资料;申请资料含 applyDateTime。
*/
onOpen(id, recordId = "") {
this.$set(this, "applyRecordId", recordId)
this.$set(this, "viewData", {})
this.$set(this, "approvalRecords", [])
this.$set(this, "viewRequestId", this.viewRequestId + 1)
const requestId = this.viewRequestId
const url = recordId ? "/platform/member/apply/common/findApplyRecord" : "/platform/member/apply/common/findOne"
const params = recordId ? { recordId } : { id }
this.$set(this, "viewLoading", true)
// 入会查看同时读取申请快照和任务历史;普通会员资料查看仍只查询会员信息。
const requests = [this.$axios.post(url, params)]
if (recordId) {
requests.push(this.$axios.post("/platform/member/apply/common/approvalRecords", { recordId }))
}
return Promise.all(requests)
.then(([res, recordsRes]) => {
// 快速切换查看对象时,仅接收最后一次请求,避免显示上一条申请。
if (res.code === 0 && requestId === this.viewRequestId) {
this.$set(this, "viewData", res.data || {})
this.$set(this, "approvalRecords", recordsRes && recordsRes.code === 0 ? recordsRes.data || [] : [])
}
})
.finally(() => {
if (requestId === this.viewRequestId) {
this.$set(this, "viewLoading", false)
}
})
}
}
}
@@ -127,6 +127,10 @@ layout("/layouts/platform_h5.html"){
height: calc(100vh - 46px);
overflow: auto;
}
.approval-popup .van-tabs {
height: auto;
overflow: visible;
}
.van-cell-group__title {
padding: 6px 0;
}
@@ -211,30 +215,32 @@ layout("/layouts/platform_h5.html"){
<van-nodata :figure="23" title="暂无数据" v-else></van-nodata>
</div>
<van-popup v-model="infoShow" position="right" :style="{ height: '100%', width: '100%' }" safe-area-inset-bottom>
<van-popup v-model="infoShow" :class="{'approval-popup': showApprovalForm}" :close-on-click-overlay="false" position="right" :style="{ height: '100%', width: '100%' }" safe-area-inset-bottom>
<van-sticky>
<van-nav-bar @click-left="infoShow=false" left-arrow
<van-nav-bar @click-left="closeInfo" left-arrow
left-text="返回" title="分工会审核"></van-nav-bar>
</van-sticky>
<member-change-info ref="memberChangeInfoRef"></member-change-info>
<member-apply-audit-info ref="memberApplyAuditInfoRef"></member-apply-audit-info>
<div v-if="showApprovalForm">
<div class="process-title">{{formData.processInstanceNodeName}}</div>
<form @submit.prevent="">
<van-field label="审核人员" :value="reviewerName" readonly></van-field>
<van-field label="审核时间" :value="reviewTime" readonly></van-field>
<van-field label="审核意见" name="approvalOpinion" v-model="formData.approvalOpinion" required></van-field>
<van-field label="签字" name="signature" v-model="formData.approvalSignature" required class="direction-column-field">
<van-field label="签字" name="approvalSignature" v-model="formData.approvalSignature" required class="direction-column-field">
<h5-signature v-model="formData.approvalSignature" slot="input"></h5-signature>
</van-field>
</form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block @click.submit="doApproval('BACK')">退回</van-button>
<van-button type="danger" block @click.submit="doApproval('REJECT')">建议撤销</van-button>
<van-button type="primary" block @click.submit="doApproval('PASS')">同意</van-button>
<van-button type="danger" block :loading="formLoading" :disabled="formLoading" @click.submit="doApproval('BACK')">退回</van-button>
<van-button type="danger" block :loading="formLoading" :disabled="formLoading" @click.submit="doApproval('REJECT')">拒绝</van-button>
<van-button type="primary" block :loading="formLoading" :disabled="formLoading" @click.submit="doApproval('PASS')">同意</van-button>
</div>
</div>
</van-popup>
</div>
<script>
<!--#include("../../common/mobileMemberChangeInfo.js"){}#-->
<!--#include("../../common/mobileMemberApplyAuditInfo.js"){}#-->
new Vue({
el: '#app',
store,
@@ -256,40 +262,72 @@ layout("/layouts/platform_h5.html"){
],
tableLoading: false,
tableFinished: false,
formLoading: false,
// 签名组件通过 v-model 回填图片 URL,必须预先声明为响应式表单字段。
formData: {
approvalOpinion: "",
approvalSignature: ""
},
rules: {
approvalOpinion: [{ required: true, message: "请填写审核意见" }],
approvalSignature: [{ required: true, message: "请签字" }]
},
userId: "",
infoShow: false,
showApprovalForm: false
showApprovalForm: false,
reviewerName: "",
reviewTime: ""
}
},
components: {
'member-change-info': MOBILE_MEMBER_CHANGE_INFO
'member-apply-audit-info': MOBILE_MEMBER_APPLY_AUDIT_INFO
},
methods: {
openApproval(row) {
this.infoShow = true
// 复制当前任务参数,保留已有意见和签名,避免本次编辑修改列表中的原始数据。
this.$set(this, "formData", {
...row.approvalParam,
approvalOpinion: row.approvalParam.approvalOpinion || "",
approvalSignature: row.approvalParam.approvalSignature || ""
})
// 审核表单只展示当前审核人和时间,提交仍由原任务参数与意见、签字组成。
this.$set(this, "reviewerName", this.$store.state.user.username || "")
this.$set(this, "reviewTime", moment().format("YYYY-MM-DD HH:mm:ss"))
this.$set(this, "showApprovalForm", true)
h5PopupHistory.open(this._uid)
this.$set(this, "infoShow", true)
this.$nextTick(() => {
this.showApprovalForm = true
this.formData = row.approvalParam
this.$refs.memberChangeInfoRef.activeName = "change"
this.$refs.memberChangeInfoRef.onOpen({recordId:row.id, userId:row.userId})
this.$refs.memberApplyAuditInfoRef.onOpen(row.id)
})
},
/**
* approvalType 为 BACK(退回)、REJECT(建议撤销)或 PASS(同意)。
* approval 参数传入包含任务标识、审核意见及签名图片 URL 的 JSON 字符串;
* 接口返回 Result 对象,code 为 0 表示审核成功,msg 为结果提示。
*/
doApproval(approvalType) {
this.formData.bpmTaskApprovalType = approvalType
// 请求完成前阻止重复审核,避免同一任务被连续提交。
if (this.formLoading) {
return
}
this.$set(this.formData, "bpmTaskApprovalType", approvalType)
formRules(this.rules, this.formData, (valid) => {
if (valid) {
this.$axios
.post("/platform/member/apply/branchUnionAudit/approval", {
approval: JSON.stringify(this.formData)
})
this.$set(this, "formLoading", true)
this.$axios.post("/platform/member/apply/branchUnionAudit/approval", {
approval: JSON.stringify(this.formData)
})
.then((res) => {
if (res.code === 0) {
this.infoShow = false
this.closeInfo()
this.$toast.success(res.msg)
this.doSearch()
}
})
.finally(() => {
this.$set(this, "formLoading", false)
})
}
})
},
@@ -315,13 +353,16 @@ layout("/layouts/platform_h5.html"){
})
},
openView(row) {
this.infoShow = true
this.$set(this, "showApprovalForm", false)
h5PopupHistory.open(this._uid)
this.$set(this, "infoShow", true)
this.$nextTick(() => {
this.showApprovalForm = false
this.$refs.memberChangeInfoRef.activeName = "basic"
this.$refs.memberChangeInfoRef.onOpen({recordId:row.id, userId:row.userId})
this.$refs.memberApplyAuditInfoRef.onOpen(row.id)
})
},
closeInfo() {
h5PopupHistory.close(this._uid)
},
doSearch() {
this.pageForm.pageNumber = 1
this.tableData = []
@@ -379,6 +420,14 @@ layout("/layouts/platform_h5.html"){
})
}
},
mounted() {
h5PopupHistory.register(this._uid, () => {
this.$set(this, "infoShow", false)
})
},
beforeDestroy() {
h5PopupHistory.unregister(this._uid)
},
created() {
this.initUnion()
this.flushUnits()
@@ -28,6 +28,24 @@ layout("/layouts/platform_h5.html"){
height: 26px;
}
.apply-status-row {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 6px 4px;
}
.apply-status-text {
white-space: nowrap;
}
.apply-action-buttons {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.van-col {
line-height: 24px;
}
@@ -142,7 +160,7 @@ layout("/layouts/platform_h5.html"){
input-align="left"
@search="doSearch"
></van-search>
<van-dropdown-menu v-if="$auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')" :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-menu v-if="$auth.hasRole('SYSADMIN')" :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item :options="unionList" @change="unionChange" v-model="pageForm.unionId"></van-dropdown-item>
<van-dropdown-item :options="unitList" @change="doSearch" v-model="pageForm.unitId"></van-dropdown-item>
</van-dropdown-menu>
@@ -184,26 +202,26 @@ layout("/layouts/platform_h5.html"){
<van-divider></van-divider>
<div style="display: flex; justify-content: space-between; align-items: center">
<div>
<div class="apply-status-row">
<div class="apply-status-text">
<span style="color: grey">当前状态:</span>
<span>{{item.processInstanceNodeName}}</span>
<!-- 开始节点仍是申请人待提交阶段,仅缩短列表中的显示文案。 -->
<span>{{item.processInstanceNodeCode === 10 ? '待提交' : item.processInstanceNodeName}}</span>
</div>
<div>
<div class="apply-action-buttons">
<van-button size="small" @click="openView(item)"
type="info" style="margin-right: 4px">查看
type="info">查看
</van-button>
<van-button size="small" type="primary"
v-if="[10,40,70].includes(item.processInstanceNodeCode)"
@click="$pjaxReplace('/platform/h5/member/apply/submit?id=' + item.id)"
style="margin-right: 4px">编辑
@click="$pjaxReplace('/platform/h5/member/apply/submit?id=' + item.id)">编辑
</van-button>
<van-button size="small" @click="openRevoke(item)"
type="danger" style="margin-right: 4px"
type="danger"
v-if="[20].includes(item.processInstanceNodeCode) && !item.nextTaskIsComplete">撤销
</van-button>
<van-button size="small" @click="doDelete(item)"
type="danger" style="margin-right: 4px"
type="danger"
v-if="[10].includes(item.processInstanceNodeCode)">删除
</van-button>
</div>
@@ -214,16 +232,16 @@ layout("/layouts/platform_h5.html"){
<van-nodata :figure="23" title="暂无数据" v-else></van-nodata>
</div>
<van-popup v-model="infoShow" position="right" :style="{ height: '100%', width: '100%' }" safe-area-inset-bottom>
<van-popup v-model="infoShow" :close-on-click-overlay="false" position="right" :style="{ height: '100%', width: '100%' }" safe-area-inset-bottom>
<van-sticky>
<van-nav-bar @click-left="infoShow=false" left-arrow
<van-nav-bar @click-left="closeInfo" left-arrow
left-text="返回" title="我的申请信息"></van-nav-bar>
</van-sticky>
<member-change-info ref="memberChangeInfoRef"></member-change-info>
<member-apply-audit-info ref="memberApplyAuditInfoRef"></member-apply-audit-info>
</van-popup>
</div>
<script>
<!--#include("../../common/mobileMemberChangeInfo.js"){}#-->
<!--#include("../../common/mobileMemberApplyAuditInfo.js"){}#-->
new Vue({
el: '#app',
store,
@@ -247,7 +265,7 @@ layout("/layouts/platform_h5.html"){
}
},
components: {
'member-change-info': MOBILE_MEMBER_CHANGE_INFO
'member-apply-audit-info': MOBILE_MEMBER_APPLY_AUDIT_INFO
},
methods: {
openRevoke(row) {
@@ -293,11 +311,15 @@ layout("/layouts/platform_h5.html"){
})
},
openView(row) {
this.infoShow = true
h5PopupHistory.open(this._uid)
this.$set(this, "infoShow", true)
this.$nextTick(() => {
this.$refs.memberChangeInfoRef.onOpen({recordId:row.id, userId:row.userId})
this.$refs.memberApplyAuditInfoRef.onOpen(row.id)
})
},
closeInfo() {
h5PopupHistory.close(this._uid)
},
doSearch() {
this.pageForm.pageNumber = 1
this.tableData = []
@@ -328,18 +350,18 @@ layout("/layouts/platform_h5.html"){
this.doSearch()
},
initUnion() {
// 普通用户只看自己的申请,无需按当前工会或单位再次过滤。
if (!this.$auth.hasRole('SYSADMIN')) {
return
}
try {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? null : this.$store.state.user.union.id
this.$businessTool.listUnion(unionId).then((data) => {
this.$businessTool.listUnion().then((data) => {
this.unionList = data
this.unionList.forEach((v) => {
v.text = v.name
v.value = v.id
})
if (hasAdmin) {
this.unionList.unshift({ text: "全部工会", value: null })
}
this.unionList.unshift({ text: "全部工会", value: null })
if (this.unionList && this.unionList.length > 0) {
this.$set(this.pageForm, "unionId", this.unionList[0].value)
}
@@ -348,16 +370,17 @@ layout("/layouts/platform_h5.html"){
},
flushUnits() {
if (!this.$auth.hasRole('SYSADMIN')) {
return
}
try {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? this.pageForm.unionId : this.$store.state.user.union.id
this.$businessTool.listUnit(unionId).then((data) => {
this.$businessTool.listUnit(this.pageForm.unionId).then((data) => {
this.unitList = data
this.unitList.forEach((v) => {
v.text = v.name
v.value = v.id
})
if (hasAdmin && !this.pageForm.unionId) {
if (!this.pageForm.unionId) {
this.unitList.unshift({ text: "全部单位", value: null })
}
if (this.unitList && this.unitList.length > 0) {
@@ -367,6 +390,14 @@ layout("/layouts/platform_h5.html"){
} catch (e) {}
}
},
mounted() {
h5PopupHistory.register(this._uid, () => {
this.$set(this, "infoShow", false)
})
},
beforeDestroy() {
h5PopupHistory.unregister(this._uid)
},
created() {
this.initUnion()
this.flushUnits()
@@ -127,13 +127,17 @@ layout("/layouts/platform_h5.html"){
height: calc(100vh - 46px);
overflow: auto;
}
.approval-popup .van-tabs {
height: auto;
overflow: visible;
}
.van-cell-group__title {
padding: 6px 0;
}
</style>
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar title="会员入会分工会审核" left-text="返回" left-arrow @click-left="$pjaxReplace('/platform/h5/home')"></van-nav-bar>
<van-nav-bar title="会员入会校工会审核" left-text="返回" left-arrow @click-left="$pjaxReplace('/platform/h5/home')"></van-nav-bar>
<van-search
v-model="pageForm.searchKeyword"
placeholder="请输入工号或者姓名进行查询"
@@ -207,30 +211,32 @@ layout("/layouts/platform_h5.html"){
<van-nodata :figure="23" title="暂无数据" v-else></van-nodata>
</div>
<van-popup v-model="infoShow" position="right" :style="{ height: '100%', width: '100%' }" safe-area-inset-bottom>
<van-popup v-model="infoShow" :class="{'approval-popup': showApprovalForm}" :close-on-click-overlay="false" position="right" :style="{ height: '100%', width: '100%' }" safe-area-inset-bottom>
<van-sticky>
<van-nav-bar @click-left="infoShow=false" left-arrow
left-text="返回" title="分工会审核"></van-nav-bar>
<van-nav-bar @click-left="closeInfo" left-arrow
left-text="返回" title="校工会审核"></van-nav-bar>
</van-sticky>
<member-change-info ref="memberChangeInfoRef"></member-change-info>
<member-apply-audit-info ref="memberApplyAuditInfoRef"></member-apply-audit-info>
<div v-if="showApprovalForm">
<div class="process-title">{{formData.processInstanceNodeName}}</div>
<form @submit.prevent="">
<van-field label="审核人员" :value="reviewerName" readonly></van-field>
<van-field label="审核时间" :value="reviewTime" readonly></van-field>
<van-field label="审核意见" name="approvalOpinion" v-model="formData.approvalOpinion" required></van-field>
<van-field label="签字" name="signature" v-model="formData.approvalSignature" required class="direction-column-field">
<van-field label="签字" name="approvalSignature" v-model="formData.approvalSignature" required class="direction-column-field">
<h5-signature v-model="formData.approvalSignature" slot="input"></h5-signature>
</van-field>
</form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block @click.submit="doApproval('BACK')">退回重新填写</van-button>
<van-button type="danger" block @click.submit="doApproval('REJECT')">建议撤销</van-button>
<van-button type="primary" block @click.submit="doApproval('PASS')">同意</van-button>
<van-button type="danger" block :loading="formLoading" :disabled="formLoading" @click.submit="doApproval('BACK')">退回</van-button>
<van-button type="danger" block :loading="formLoading" :disabled="formLoading" @click.submit="doApproval('REJECT')">拒绝</van-button>
<van-button type="primary" block :loading="formLoading" :disabled="formLoading" @click.submit="doApproval('PASS')">同意</van-button>
</div>
</div>
</van-popup>
</div>
<script>
<!--#include("../../common/mobileMemberChangeInfo.js"){}#-->
<!--#include("../../common/mobileMemberApplyAuditInfo.js"){}#-->
new Vue({
el: '#app',
store,
@@ -253,40 +259,72 @@ layout("/layouts/platform_h5.html"){
],
tableLoading: false,
tableFinished: false,
formLoading: false,
// 签名组件通过 v-model 回填图片 URL,必须预先声明为响应式表单字段。
formData: {
approvalOpinion: "",
approvalSignature: ""
},
rules: {
approvalOpinion: [{ required: true, message: "请填写审核意见" }],
approvalSignature: [{ required: true, message: "请签字" }]
},
userId: "",
infoShow: false,
showApprovalForm: false
showApprovalForm: false,
reviewerName: "",
reviewTime: ""
}
},
components: {
'member-change-info': MOBILE_MEMBER_CHANGE_INFO
'member-apply-audit-info': MOBILE_MEMBER_APPLY_AUDIT_INFO
},
methods: {
openApproval(row) {
this.infoShow = true
// 复制当前任务参数,保留已有意见和签名,避免本次编辑修改列表中的原始数据。
this.$set(this, "formData", {
...row.approvalParam,
approvalOpinion: row.approvalParam.approvalOpinion || "",
approvalSignature: row.approvalParam.approvalSignature || ""
})
// 审核表单只展示当前审核人和时间,提交仍由原任务参数与意见、签字组成。
this.$set(this, "reviewerName", this.$store.state.user.username || "")
this.$set(this, "reviewTime", moment().format("YYYY-MM-DD HH:mm:ss"))
this.$set(this, "showApprovalForm", true)
h5PopupHistory.open(this._uid)
this.$set(this, "infoShow", true)
this.$nextTick(() => {
this.showApprovalForm = true
this.formData = row.approvalParam
this.$refs.memberChangeInfoRef.activeName = "change"
this.$refs.memberChangeInfoRef.onOpen({recordId:row.id, userId:row.userId})
this.$refs.memberApplyAuditInfoRef.onOpen(row.id)
})
},
/**
* approvalType 为 BACK(退回)、REJECT(建议撤销)或 PASS(同意)。
* approval 参数传入包含任务标识、审核意见及签名图片 URL 的 JSON 字符串;
* 接口返回 Result 对象,code 为 0 表示审核成功,msg 为结果提示。
*/
doApproval(approvalType) {
this.formData.bpmTaskApprovalType = approvalType
// 请求完成前阻止重复审核,避免同一任务被连续提交。
if (this.formLoading) {
return
}
this.$set(this.formData, "bpmTaskApprovalType", approvalType)
formRules(this.rules, this.formData, (valid) => {
if (valid) {
this.$axios
.post("/platform/member/apply/schoolUnionAudit/approval", {
approval: JSON.stringify(this.formData)
})
this.$set(this, "formLoading", true)
this.$axios.post("/platform/member/apply/schoolUnionAudit/approval", {
approval: JSON.stringify(this.formData)
})
.then((res) => {
if (res.code === 0) {
this.infoShow = false
this.closeInfo()
this.$toast.success(res.msg)
this.doSearch()
}
})
.finally(() => {
this.$set(this, "formLoading", false)
})
}
})
},
@@ -312,13 +350,16 @@ layout("/layouts/platform_h5.html"){
})
},
openView(row) {
this.infoShow = true
this.$set(this, "showApprovalForm", false)
h5PopupHistory.open(this._uid)
this.$set(this, "infoShow", true)
this.$nextTick(() => {
this.showApprovalForm = false
this.$refs.memberChangeInfoRef.activeName = "basic"
this.$refs.memberChangeInfoRef.onOpen({recordId:row.id, userId:row.userId})
this.$refs.memberApplyAuditInfoRef.onOpen(row.id)
})
},
closeInfo() {
h5PopupHistory.close(this._uid)
},
doSearch() {
this.pageForm.pageNumber = 1
this.tableData = []
@@ -381,6 +422,14 @@ layout("/layouts/platform_h5.html"){
})
}
},
mounted() {
h5PopupHistory.register(this._uid, () => {
this.$set(this, "infoShow", false)
})
},
beforeDestroy() {
h5PopupHistory.unregister(this._uid)
},
created() {
this.initUnion()
this.flushUnits()
@@ -191,9 +191,9 @@ layout("/layouts/platform_h5.html"){
</van-field>
</form>
<div v-if="!this.$store.state.user.member" style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button block v-if="formData.id" type="primary" @click.submit="$pjaxReplace('/platform/h5/member/apply/mine')">返 回</van-button>
<van-button block type="primary" @click.submit="doSave">保存申请</van-button>
<van-button block type="primary" @click.submit="doSubmit">提交申请</van-button>
<!-- <van-button block v-if="formData.id" type="primary" @click.submit="$pjaxReplace('/platform/h5/member/apply/mine')">返 回</van-button> -->
<van-button block type="primary" :disabled="isDisable" @click.submit="doSave">保存申请</van-button>
<van-button block type="primary" :disabled="isDisable" @click.submit="doSubmit">提交申请</van-button>
</div>
</van-cell-group>
</div>
@@ -318,7 +318,7 @@ layout("/layouts/platform_h5.html"){
return { value: item.name, text: item.name }
})
// 在职状态
const userStatusData = await this.$businessTool.getDictOptions('USER_STATUS')
const userStatusData = await this.$businessTool.getDictOptions('USER_STATE')
this.userStateColumns = userStatusData.map(item => {
return { value: item.name, text: item.name }
})
@@ -342,6 +342,9 @@ layout("/layouts/platform_h5.html"){
if (!this.id) {
this.formData.id = null
this.formData.userId = res.data.id
// 新建申请优先读取登录用户的人事信息,登录信息为空时保留接口返回值。
this.$set(this.formData, 'userState', this.$store.state.user.userState || res.data.userState)
this.$set(this.formData, 'preparedBy', this.$store.state.user.preparedBy || res.data.preparedBy)
}
if (!this.formData.families) {
this.formData.families = []
@@ -371,7 +374,21 @@ layout("/layouts/platform_h5.html"){
},
created() {
this.initDictOptions()
this.initUserData()
if (this.id) {
this.initUserData()
return
}
// H5 菜单可以直达新建页面,进入前需检查本人是否已有进行中的申请。
this.isDisable = true
this.$axios.post('/platform/member/apply/submit/canStartNewApply', {}).then((res) => {
if (res.code === 0 && res.data === true) {
this.initUserData()
this.isDisable = false
} else {
this.$toast.fail('您有进行中的入会申请,请先处理已有申请')
this.$pjaxReplace('/platform/h5/member/apply/mine')
}
})
},
})
</script>
@@ -0,0 +1,132 @@
const MOBILE_MEMBER_APPLY_AUDIT_INFO = {
template: /*language=HTML*/ `
<div>
<van-tabs v-model="activeName" sticky>
<van-tab title="申请信息" name="apply">
<van-loading v-if="viewLoading" size="24px">正在加载申请信息</van-loading>
<van-cell-group v-else-if="viewData.id">
<van-cell title="申请时间">{{ viewData.applyDateTime || '暂无' }}</van-cell>
<van-cell title="工号">{{ viewData.loginname }}</van-cell>
<van-cell title="姓名">{{ viewData.username }}</van-cell>
<van-cell title="性别">{{ viewData.sex }}</van-cell>
<van-cell title="民族">{{ viewData.nation }}</van-cell>
<van-cell title="在职状态">{{ viewData.userState }}</van-cell>
<van-cell title="人事编制">{{ viewData.preparedBy }}</van-cell>
<van-cell title="人员类型">{{ viewData.personType }}</van-cell>
<van-cell title="学历">{{ viewData.education }}</van-cell>
<van-cell title="政治面貌">{{ viewData.political }}</van-cell>
<van-cell title="证件号码">{{ viewData.idCard }}</van-cell>
<van-cell title="联系电话">{{ viewData.mobile }}</van-cell>
<van-cell title="电子邮箱">{{ viewData.email }}</van-cell>
<van-cell title="所属工会">{{ viewData.unionName }}</van-cell>
<van-cell title="工作单位">{{ viewData.unitName }}</van-cell>
<van-cell title="岗位职务">{{ viewData.governmentPosition }}</van-cell>
<van-cell title="是否愿意加入大病互助基金会">{{ viewData.wantJoinLoveMutualAidAssociation ? '是' : '否' }}</van-cell>
<van-cell title="家庭主要成员及其工作单位">
<div slot="label" v-if="families.length">
<div v-for="(family, index) in families" :key="index">
{{ family.relation }} {{ family.name }} {{ family.unit }} {{ family.remark }}
</div>
</div>
<span v-else>暂无</span>
</van-cell>
<van-cell title="个人简况">
<div slot="label" class="van-cell-rich-text" v-html="viewData.personalData"></div>
</van-cell>
<van-cell title="备注">{{ viewData.notes || '暂无' }}</van-cell>
</van-cell-group>
<van-nodata v-else :figure="23" title="暂无申请信息"></van-nodata>
</van-tab>
<van-tab v-if="branchRecords.length" title="分工会审核信息" name="branch">
<van-cell-group v-for="record in branchRecords" :key="record.id" class="mb15">
<van-cell title="审核结果">{{ record.result }}</van-cell>
<van-cell v-if="record.originalResult" title="原审核结果">{{ record.originalResult }}</van-cell>
<van-cell title="审核人员">{{ record.operatorName || '暂无' }}</van-cell>
<van-cell title="审核时间">{{ record.handledAt || '暂无' }}</van-cell>
<van-cell title="审核意见">{{ record.opinion || '暂无' }}</van-cell>
<van-cell title="签字">
<van-image v-if="record.signature" :src="record.signature" fit="contain" width="180" height="80"></van-image>
<span v-else>暂无</span>
</van-cell>
</van-cell-group>
</van-tab>
<van-tab v-if="schoolRecords.length" title="校工会审核信息" name="school">
<van-cell-group v-for="record in schoolRecords" :key="record.id" class="mb15">
<van-cell title="审核结果">{{ record.result }}</van-cell>
<van-cell v-if="record.originalResult" title="原审核结果">{{ record.originalResult }}</van-cell>
<van-cell title="审核人员">{{ record.operatorName || '暂无' }}</van-cell>
<van-cell title="审核时间">{{ record.handledAt || '暂无' }}</van-cell>
<van-cell title="审核意见">{{ record.opinion || '暂无' }}</van-cell>
<van-cell title="签字">
<van-image v-if="record.signature" :src="record.signature" fit="contain" width="180" height="80"></van-image>
<span v-else>暂无</span>
</van-cell>
</van-cell-group>
</van-tab>
</van-tabs>
<slot></slot>
</div>
`,
data() {
return {
activeName: "apply",
viewData: {},
approvalRecords: [],
viewLoading: false,
requestId: 0
}
},
computed: {
families() {
const value = this.viewData.families
if (Array.isArray(value)) {
return value
}
if (typeof value === "string" && value) {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch (error) {
return []
}
}
return []
},
// 申请人任务不展示;20、50 节点分别为分工会和校工会审核记录。
branchRecords() {
return this.approvalRecords.filter((record) => Number(record.nodeCode) === 20 && record.handledAt)
},
schoolRecords() {
return this.approvalRecords.filter((record) => Number(record.nodeCode) === 50 && record.handledAt)
}
},
methods: {
/**
* recordId 为申请记录 ID。接口返回 Result;data 分别为申请快照和流程记录列表,
* 按 nodeCode 将审核结果、意见与签字 URL 分到分工会和校工会页签。
*/
onOpen(recordId) {
this.$set(this, "activeName", "apply")
this.$set(this, "viewData", {})
this.$set(this, "approvalRecords", [])
this.$set(this, "requestId", this.requestId + 1)
const requestId = this.requestId
this.$set(this, "viewLoading", true)
return Promise.all([
this.$axios.post("/platform/member/apply/common/findApplyRecord", { recordId }),
this.$axios.post("/platform/member/apply/common/approvalRecords", { recordId })
])
.then(([applyRes, recordsRes]) => {
if (requestId === this.requestId && applyRes.code === 0) {
this.$set(this, "viewData", applyRes.data || {})
this.$set(this, "approvalRecords", recordsRes.code === 0 ? recordsRes.data || [] : [])
}
})
.finally(() => {
if (requestId === this.requestId) {
this.$set(this, "viewLoading", false)
}
})
}
}
}
@@ -1,7 +1,61 @@
const MOBILE_MEMBER_CHANGE_INFO = {
template: `
template: /*language=HTML*/ `
<div>
<van-collapse v-model="activeName" accordion>
<van-collapse-item title="本次申请" name="apply">
<van-loading v-if="applyLoading" size="24px">正在加载申请记录</van-loading>
<van-cell-group v-else-if="applyData.id">
<van-cell title="申请时间">{{ applyData.applyDateTime || '暂无' }}</van-cell>
<van-cell title="姓名">{{ applyData.username }}</van-cell>
<van-cell title="工号">{{ applyData.loginname }}</van-cell>
<van-cell title="所属工会">{{ applyData.unionName }}</van-cell>
<van-cell title="工作单位">{{ applyData.unitName }}</van-cell>
<van-cell title="岗位职务">{{ applyData.governmentPosition }}</van-cell>
<van-cell title="性别">{{ applyData.sex }}</van-cell>
<van-cell title="民族">{{ applyData.nation }}</van-cell>
<van-cell title="证件号码">{{ applyData.idCard }}</van-cell>
<van-cell title="联系电话">{{ applyData.mobile }}</van-cell>
<van-cell title="电子邮箱">{{ applyData.email }}</van-cell>
<van-cell title="在职状态">{{ applyData.userState }}</van-cell>
<van-cell title="人事编制">{{ applyData.preparedBy }}</van-cell>
<van-cell title="人员类型">{{ applyData.personType }}</van-cell>
<van-cell title="学历">{{ applyData.education }}</van-cell>
<van-cell title="政治面貌">{{ applyData.political }}</van-cell>
<van-cell title="是否愿意加入大病互助基金会">{{ applyData.wantJoinLoveMutualAidAssociation ? '是' : '否' }}</van-cell>
<van-cell title="家庭主要成员及其工作单位">
<div slot="label" v-if="applyData.families && applyData.families.length">
<div v-for="(family, index) in applyData.families" :key="index">
{{ family.relation }} {{ family.name }} {{ family.unit }} {{ family.remark }}
</div>
</div>
<span v-else>暂无</span>
</van-cell>
<van-cell title="个人简况">
<div slot="label" class="van-cell-rich-text" v-html="applyData.personalData"></div>
</van-cell>
<van-cell title="备注">{{ applyData.notes || '暂无' }}</van-cell>
</van-cell-group>
<van-nodata :figure="23" title="暂无申请记录" v-else></van-nodata>
</van-collapse-item>
<van-collapse-item title="申请及审核记录" name="approval">
<van-loading v-if="applyLoading" size="24px">正在加载审核记录</van-loading>
<template v-else-if="approvalRecords.length">
<van-cell-group v-for="record in approvalRecords" :key="record.id" class="mb15">
<van-cell :title="record.nodeName" :value="record.result"></van-cell>
<van-cell title="办理人">
{{ record.operatorName || '暂无' }}
<span v-if="record.operatorLoginName">({{ record.operatorLoginName }})</span>
</van-cell>
<van-cell title="处理时间">{{ record.handledAt || '暂无' }}</van-cell>
<van-cell title="审核意见">{{ record.opinion || '暂无' }}</van-cell>
<van-cell title="签字">
<van-image v-if="record.signature" :src="record.signature" fit="contain" width="150" height="70"></van-image>
<span v-else>暂无</span>
</van-cell>
</van-cell-group>
</template>
<van-nodata :figure="23" title="暂无申请及审核记录" v-else></van-nodata>
</van-collapse-item>
<van-collapse-item name="basic">
<template #title>
<span style="color: #409eff">会员基础信息</span>
@@ -50,24 +104,64 @@ const MOBILE_MEMBER_CHANGE_INFO = {
</table>
</van-collapse-item>
</van-collapse>
<slot></slot>
</div>
`,
data() {
return {
changeList: [],
viewData: {},
applyData: {},
approvalRecords: [],
applyLoading: false,
applyRequestId: 0,
activeName: "basic"
}
},
methods: {
removeHTMLTag,
findAuditChangeInfo(recordId) {
/**
* recordId 为当前申请记录 ID;两个接口的 Result.data 分别返回申请资料和任务历史列表。
* 历史项包含节点、办理人、处理时间、结果、意见及签名图片 URL。
* 申请快照单独展示,避免审核后更新的会员资料覆盖原申请内容。
*/
findApplyRecord(recordId) {
this.$set(this, "applyData", {})
this.$set(this, "approvalRecords", [])
this.$set(this, "applyRequestId", this.applyRequestId + 1)
const requestId = this.applyRequestId
this.$set(this, "applyLoading", false)
if (!recordId) {
return
}
this.$axios.post("/platform/member/apply/branchUnionAudit/findAuditChangeInfo", { recordId: recordId }).then((res) => {
if (res.code === 0) {
this.changeList = res.data
this.$set(this, "applyLoading", true)
return Promise.all([
this.$axios.post("/platform/member/apply/common/findApplyRecord", { recordId }),
this.$axios.post("/platform/member/apply/common/approvalRecords", { recordId })
])
.then(([res, recordsRes]) => {
// 弹框切换到另一条申请后,忽略先前尚未完成的查询响应。
if (res.code === 0 && requestId === this.applyRequestId) {
this.$set(this, "applyData", res.data || {})
this.$set(this, "approvalRecords", recordsRes.code === 0 ? recordsRes.data || [] : [])
}
})
.finally(() => {
if (requestId === this.applyRequestId) {
this.$set(this, "applyLoading", false)
}
})
},
findAuditChangeInfo(recordId) {
this.$set(this, "changeList", [])
if (!recordId) {
return
}
// 公共接口同时识别 PC、H5 权限,校工会和本人查看无需借用分工会审核权限。
const requestId = this.applyRequestId
return this.$axios.post("/platform/member/apply/common/findApplyChanges", { recordId }).then((res) => {
if (res.code === 0 && requestId === this.applyRequestId) {
this.$set(this, "changeList", res.data || [])
}
})
},
@@ -82,6 +176,11 @@ const MOBILE_MEMBER_CHANGE_INFO = {
})
},
onOpen(data) {
this.findApplyRecord(data.recordId)
// 查看默认展开本次申请;审核入口指定的“变更信息”仍按原设置展开。
if (this.activeName !== "change") {
this.$set(this, "activeName", "apply")
}
this.findAuditChangeInfo(data.recordId)
this.$nextTick(() => {
this.findUserInfo(data.userId)