活动申报手机端审核
活动报销手机端审核
This commit is contained in:
@@ -1,74 +1,50 @@
|
||||
package io.v.nutz.zhgh.jf.controller.activitybx;
|
||||
|
||||
import io.v.nutz.base.annontation.ViReturn;
|
||||
import io.v.nutz.base.model.Goods;
|
||||
import io.v.nutz.base.service.BaseService;
|
||||
import io.v.nutz.base.utils.DateUtil;
|
||||
import io.v.nutz.base.utils.MsgApi;
|
||||
import io.v.nutz.base.utils.Roles;
|
||||
import io.v.nutz.base.utils.Vi;
|
||||
import io.v.nutz.sys.models.Sys_signature;
|
||||
import io.v.nutz.sys.models.Sys_user;
|
||||
import io.v.nutz.sys.models.Sys_user_role;
|
||||
import io.v.nutz.sys.services.SysSignatureService;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler;
|
||||
import io.v.nutz.zhgh.jf.model.ActivityBx;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.json.Json;
|
||||
import org.nutz.lang.Lang;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
import org.nutz.json.Json;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author zhf
|
||||
* @date 2020/11/14 14:55
|
||||
* @description 校工会会计核报
|
||||
*/
|
||||
/** 校工会负责人审核:PC 与手机共用业务接口,审核逻辑统一在 Service 中处理。 */
|
||||
@IocBean
|
||||
@Ok("json")
|
||||
@At("/platform/jf/activity_bx/accounting")
|
||||
public class AccountingCon {
|
||||
|
||||
|
||||
@Inject
|
||||
private Vi vi;
|
||||
|
||||
@Inject
|
||||
private ActivityBxService activityBxService;
|
||||
|
||||
@Inject
|
||||
private BaseService baseService;
|
||||
|
||||
@Inject
|
||||
private MsgApi msgApi;
|
||||
|
||||
@Inject
|
||||
private SysSignatureService sysSignatureService;
|
||||
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/jf/activitybx/accounting.html")
|
||||
@RequiresPermissions("sys.jf.activity_bx.accounting")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** 手机审核入口,使用独立页面目录及现有 PC 权限。 */
|
||||
@At
|
||||
@Ok("beetl:/mobile/jf/activitybx/accounting/index.html")
|
||||
@RequiresPermissions("sys.jf.activity_bx.accounting")
|
||||
public void mobile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* year 为年度,isAudit 为1全部/2已审核/3未审核;unitId、unionId 为筛选组织ID;
|
||||
* searchName 为 username/loginname,searchKeyword 为姓名或工号(空值不筛选)。
|
||||
* pageNumber 从1开始,pageSize 为1至100;pageOrderName 为 bxr_unionname/activity_type/absName/apply_type/apply_time/state_id,
|
||||
* pageOrderBy 为 asc/desc(兼容 ascending/descending),未指定有效排序时按状态、申请时间排序。
|
||||
* 返回分页对象,list 为申请数组,totalCount 为符合当前权限及筛选的总数。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.activity_bx.accounting")
|
||||
public Object pageData(@Param(value = "year", required = false) Integer year,
|
||||
@Param(value = "isAudit", required = false) Integer isAudit,
|
||||
@Param(value = "unitId", required = false) String unitId,
|
||||
@@ -78,89 +54,44 @@ public class AccountingCon {
|
||||
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize,
|
||||
@Param(value = "pageOrderName", required = false) String pageOrderName,
|
||||
@Param(value = "pageOrderBy", required = false) String pageOrderBy) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
act.*,
|
||||
u.loginname bxr_loginname,
|
||||
u.unionname bxr_unionname,
|
||||
s.state_name,
|
||||
s.state_color,
|
||||
abs.`name` absName
|
||||
FROM
|
||||
`activity_bx` act
|
||||
LEFT JOIN `user` u ON act.user_id = u.id
|
||||
LEFT JOIN state s ON act.state_id = s.state_id
|
||||
LEFT JOIN activity_basic_settings abs ON abs.`code` = act.projectJfCode
|
||||
$condition
|
||||
""");
|
||||
|
||||
Cnd cnd = Cnd.NEW();
|
||||
|
||||
if (isAudit == 1) {
|
||||
cnd.and("act.state_id", ">=", 1020);
|
||||
} else {
|
||||
cnd.and("act.state_id", isAudit == 2 ? ">" : "=", 1020);
|
||||
}
|
||||
if (ShiroUtil.hasRole("xghng")) {
|
||||
cnd.and("act.projectTypeCode", "=", 50002);
|
||||
}
|
||||
|
||||
|
||||
Vi.cndPlus(cnd, "act.year", "=", year);
|
||||
if (Vi.isNotBlank(searchName, searchKeyword)) {
|
||||
cnd.where().andLike(searchName, searchKeyword);
|
||||
}
|
||||
|
||||
if (Vi.isNotBlank(pageOrderName, pageOrderBy)) {
|
||||
cnd.orderBy(pageOrderName, pageOrderBy);
|
||||
} else {
|
||||
cnd.asc("act.state_id").desc("act.apply_time");
|
||||
}
|
||||
Vi.cndPlus(cnd, "u.unitid", "=", unitId);
|
||||
Vi.cndPlus(cnd, "u.unionid", "=", unionId);
|
||||
sql.setCondition(cnd);
|
||||
return activityBxService.listPageMap(pageNumber, pageSize, sql);
|
||||
return activityBxService.reviewPage("accounting", new NutMap().addv("year", year).addv("isAudit", isAudit)
|
||||
.addv("unitId", unitId).addv("unionId", unionId).addv("searchName", searchName)
|
||||
.addv("searchKeyword", searchKeyword).addv("pageNumber", pageNumber).addv("pageSize", pageSize)
|
||||
.addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy));
|
||||
}
|
||||
|
||||
/** id 为申请主键;返回详情 Map,canReview 表示该申请是否仍处于当前待审节点。 */
|
||||
@At
|
||||
@ViReturn
|
||||
public Object getGoods(String activity_id) {
|
||||
return baseService.dao().query(Goods.class, Cnd.where("reid", "=", activity_id));
|
||||
@RequiresPermissions("sys.jf.activity_bx.accounting")
|
||||
public Object findOne(@Param("id") String id) {
|
||||
if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动申请");
|
||||
return activityBxService.reviewDetail("accounting", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* data 为 JSON 对象字符串:id 为申请ID,idea 为1至500字意见,sign 为图片数据或签名路径,flag 为布尔值(true通过/false拒绝)。
|
||||
* 返回统一结果对象,code=0 为成功,msg 为提示;失败不推进审核状态。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Object doReview(String data) {
|
||||
NutMap nutMap = Json.fromJson(NutMap.class, data);
|
||||
ActivityBx aid = activityBxService.fetch(nutMap.getString("id"));
|
||||
aid.setState_id(nutMap.getBoolean("flag") ? "1040" : "1025");
|
||||
aid.setAccounting_name(ShiroUtil.getPrincipalProperty("id").toString());
|
||||
aid.setAccounting_time(DateUtil.getDate());
|
||||
aid.setAccounting_idea(nutMap.getString("idea"));
|
||||
|
||||
if (Strings.isNotBlank(nutMap.getString("sign"))) {
|
||||
aid.setAccounting(sysSignatureService.insert(new Sys_signature(nutMap.getString("sign"))).getId());
|
||||
}
|
||||
activityBxService.update(aid);
|
||||
String nodeName = "";
|
||||
if (nutMap.getBoolean("flag")) {
|
||||
nodeName = "校工会主席审核";
|
||||
Sys_user_role sys_user_role = activityBxService.dao().fetch(Sys_user_role.class, Cnd.where("roleId", "=", Roles.GH_WYH_ZX));
|
||||
if (Lang.isNotEmpty(sys_user_role)) {
|
||||
Sys_user user = activityBxService.dao().fetch(Sys_user.class, sys_user_role.getUserId());
|
||||
msgApi.sendWxMsg("活动申报", "", "text", aid.getUser_name() + "申报了一条活动【" + aid.getActivity_name() + "】,请前往智慧工会进行审核。", List.of(user.getLoginname()), null);
|
||||
FundsToHandler.CREATE_SCHOOL_TASK.exec(aid.getId(), null, List.of(user.getLoginname()));
|
||||
}
|
||||
} else {
|
||||
nodeName = "校工会负责人拒绝";
|
||||
Sys_user user = activityBxService.dao().fetch(Sys_user.class, aid.getUser_id());
|
||||
msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】被校工会负责人拒绝,审核意见:" + aid.getAccounting_idea(), List.of(user.getLoginname()), null);
|
||||
FundsToHandler.REFUSE_TASK.exec(aid.getId(), new NutMap().addv("option", nodeName), null);
|
||||
}
|
||||
FundsToHandler.COMPLETE_SCHOOL_FZR_TASK.exec(aid.getId(), new NutMap().addv("option", nodeName), null);
|
||||
@RequiresPermissions("sys.jf.activity_bx.accounting")
|
||||
public Object doReview(@Param("data") String data) {
|
||||
if (Strings.isBlank(data)) throw new IllegalArgumentException("请填写审核信息");
|
||||
NutMap form = Json.fromJson(NutMap.class, data);
|
||||
if (form == null) throw new IllegalArgumentException("审核参数格式错误");
|
||||
activityBxService.reviewActivity("accounting", form);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/** activity_id 为原活动申请ID;返回物品明细数组,保留原 PC 查询地址并检查数据范围。 */
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.activity_bx.accounting")
|
||||
public Object getGoods(@Param("activity_id") String activity_id) {
|
||||
return activityBxService.reviewGoods("accounting", activity_id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
package io.v.nutz.zhgh.jf.controller.activitybx;
|
||||
|
||||
import io.v.nutz.base.annontation.ViReturn;
|
||||
import io.v.nutz.base.utils.DateUtil;
|
||||
import io.v.nutz.base.utils.MsgApi;
|
||||
import io.v.nutz.base.utils.Vi;
|
||||
import io.v.nutz.sys.models.Sys_signature;
|
||||
import io.v.nutz.sys.models.Sys_user;
|
||||
import io.v.nutz.sys.services.SysSignatureService;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler;
|
||||
import io.v.nutz.zhgh.jf.model.ActivityBx;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
@@ -25,41 +13,37 @@ import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author zhf
|
||||
* @date 2020/12/6 15:23
|
||||
* @description 协会负责人审核
|
||||
*/
|
||||
/** 协会活动申报审核:PC、手机共用查询和审核入口。 */
|
||||
@IocBean
|
||||
@Ok("json")
|
||||
@At("/platform/jf/activity_bx/association")
|
||||
@RequiresPermissions("sys.jf.activity_bx.association")
|
||||
public class AssociationCon {
|
||||
|
||||
|
||||
@Inject
|
||||
private Vi vi;
|
||||
|
||||
@Inject
|
||||
private ActivityBxService activityBxService;
|
||||
|
||||
@Inject
|
||||
private SysSignatureService sysSignatureService;
|
||||
@Inject
|
||||
private MsgApi msgApi;
|
||||
|
||||
/** PC 审核页面,沿用原访问地址。 */
|
||||
@At("")
|
||||
@Ok("beetl:/platform/jf/activitybx/association.html")
|
||||
@RequiresPermissions("sys.jf.activity_bx.association")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** 手机审核页面,使用独立目录,不依赖旧的 declare 页面。 */
|
||||
@At
|
||||
@Ok("beetl:/mobile/jf/activitybx/association/index.html")
|
||||
public void mobile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* year 为年度,isAudit 为 1全部/2已审核/3未审核(默认),clubId 为协会ID;
|
||||
* unitId、unionId、searchName、searchKeyword 保留 PC 筛选,pageNumber/pageSize 为分页,
|
||||
* pageOrderName/pageOrderBy 为允许的排序字段和方向。返回 Pagination,list 为申请,totalCount 为总数。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
public Object pageData(@Param(value = "year", required = false) Integer year,
|
||||
@Param(value = "isAudit", required = false) Integer isAudit,
|
||||
@Param(value = "clubId", required = false) String clubId,
|
||||
@Param(value = "unitId", required = false) String unitId,
|
||||
@Param(value = "unionId", required = false) String unionId,
|
||||
@Param(value = "searchName", required = false) String searchName,
|
||||
@@ -67,82 +51,46 @@ public class AssociationCon {
|
||||
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize,
|
||||
@Param(value = "pageOrderName", required = false) String pageOrderName,
|
||||
@Param(value = "pageOrderBy", required = false) String pageOrderBy) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
act.*,
|
||||
u.loginname bxr_loginname,
|
||||
u.unionname bxr_unionname,
|
||||
s.state_name,
|
||||
s.state_color,
|
||||
abs.`name` absName
|
||||
FROM
|
||||
`activity_bx` act
|
||||
LEFT JOIN `user` u ON act.user_id = u.id
|
||||
LEFT JOIN sys_unit un ON u.unitid = un.id
|
||||
LEFT JOIN state s ON act.state_id = s.state_id
|
||||
LEFT JOIN activity_basic_settings abs ON abs.`code` = act.projectJfCode
|
||||
$condition
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
|
||||
if (isAudit == 1) {
|
||||
cnd.and("act.state_id", ">=", 1000);
|
||||
} else {
|
||||
cnd.and("act.state_id", isAudit == 2 ? ">" : "=", 1000);
|
||||
}
|
||||
|
||||
cnd.and("act.activity_type", "=", 40003);
|
||||
if (!ShiroUtil.hasAnyRoles("sysadmin")) {
|
||||
cnd.and("act.clubId", "in", vi.getMangeClubStr());
|
||||
}
|
||||
Vi.cndPlus(cnd, "act.year", "=", year);
|
||||
if (Vi.isNotBlank(searchName, searchKeyword)) {
|
||||
cnd.where().andLike(searchName, searchKeyword);
|
||||
}
|
||||
|
||||
if (Vi.isNotBlank(pageOrderName, pageOrderBy)) {
|
||||
cnd.orderBy(pageOrderName, pageOrderBy);
|
||||
} else {
|
||||
cnd.asc("act.state_id").desc("act.apply_time");
|
||||
}
|
||||
Vi.cndPlus(cnd, "u.unitid", "=", unitId);
|
||||
Vi.cndPlus(cnd, "u.unionid", "=", unionId);
|
||||
sql.setCondition(cnd);
|
||||
return activityBxService.listPageMap(pageNumber, pageSize, sql);
|
||||
return activityBxService.associationPage(new NutMap().addv("year", year).addv("isAudit", isAudit)
|
||||
.addv("clubId", clubId).addv("unitId", unitId).addv("unionId", unionId)
|
||||
.addv("searchName", searchName).addv("searchKeyword", searchKeyword)
|
||||
.addv("pageNumber", pageNumber).addv("pageSize", pageSize)
|
||||
.addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy));
|
||||
}
|
||||
|
||||
/** 无参数;返回当前用户可管理的协会列表,每项 id 为筛选值、name 为显示名称。 */
|
||||
@At
|
||||
@ViReturn
|
||||
public Object clubs() {
|
||||
return activityBxService.associationClubs();
|
||||
}
|
||||
|
||||
/** id 为活动申报主键;返回申请详情 Map,包含预算、附件、审核记录和 canReview 可审核标识。 */
|
||||
@At
|
||||
@ViReturn
|
||||
public Object findOne(@Param("id") String id) {
|
||||
if (Strings.isBlank(id)) {
|
||||
throw new IllegalArgumentException("请选择活动申请");
|
||||
}
|
||||
return activityBxService.associationDetail(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* id 为申报主键,idea 为1至500字意见,sign 为签字图片数据或现有签字路径,flag 为true通过/false拒绝。
|
||||
* 审核人和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为结果提示,data 为空。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Object doReview(@Param(value = "id", required = false) String id,
|
||||
@Param(value = "idea", required = false) String idea,
|
||||
@Param(value = "sign", required = false) String sign,
|
||||
@Param(value = "flag", required = false) Boolean flag) {
|
||||
ActivityBx aid = activityBxService.fetch(id);
|
||||
aid.setState_id(flag ? "1020" : "1005");
|
||||
aid.setAssociation_principal_id(ShiroUtil.getPrincipalProperty("id").toString());
|
||||
aid.setAssociation_principal_time(DateUtil.getDate());
|
||||
aid.setAssociation_principal_idea(idea);
|
||||
if (Strings.isNotBlank(sign)) {
|
||||
aid.setAssociation_principalSign_id(sysSignatureService.insert(new Sys_signature(sign)).getId());
|
||||
public Object doReview(@Param("id") String id, @Param("idea") String idea,
|
||||
@Param("sign") String sign, @Param("flag") Boolean flag) {
|
||||
if (Strings.isBlank(id) || flag == null) {
|
||||
throw new IllegalArgumentException("请选择活动申请和审核结果");
|
||||
}
|
||||
activityBxService.update(aid);
|
||||
|
||||
String nodeName = "";
|
||||
if (flag) {
|
||||
msgApi.sendWxMsg("活动申报", "", "text", aid.getUser_name() + "申报了一条活动【" + aid.getActivity_name() + "】,请前往智慧工会进行审核。", List.of("1019911404"), null);
|
||||
FundsToHandler.CREATE_SCHOOL_FZR_TASK.exec(id, new NutMap().addv("userId", aid.getUser_id()), List.of("1019911404"));
|
||||
nodeName = "校工会负责人审核";
|
||||
} else {
|
||||
Sys_user user = activityBxService.dao().fetch(Sys_user.class, aid.getUser_id());
|
||||
msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】被协会拒绝,审核意见:" + aid.getAssociation_principal_idea(), List.of(user.getLoginname()), null);
|
||||
nodeName = "协会审核拒绝";
|
||||
FundsToHandler.REFUSE_TASK.exec(id, new NutMap().addv("option", nodeName), null);
|
||||
if (Strings.isBlank(idea) || idea.trim().length() > 500 || Strings.isBlank(sign)) {
|
||||
throw new IllegalArgumentException("请填写1至500字审核意见并签字");
|
||||
}
|
||||
FundsToHandler.COMPLETE_CLUB_TASK.exec(id, new NutMap().addv("option", nodeName), null);
|
||||
activityBxService.reviewAssociation(id, idea.trim(), sign, flag);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,67 +1,49 @@
|
||||
package io.v.nutz.zhgh.jf.controller.activitybx;
|
||||
|
||||
import io.v.nutz.base.annontation.ViReturn;
|
||||
import io.v.nutz.base.utils.DateUtil;
|
||||
import io.v.nutz.base.utils.MsgApi;
|
||||
import io.v.nutz.base.utils.Roles;
|
||||
import io.v.nutz.base.utils.Vi;
|
||||
import io.v.nutz.sys.models.Sys_signature;
|
||||
import io.v.nutz.sys.models.Sys_user;
|
||||
import io.v.nutz.sys.models.Sys_user_role;
|
||||
import io.v.nutz.sys.services.SysSignatureService;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler;
|
||||
import io.v.nutz.zhgh.jf.model.ActivityBx;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Lang;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author zhf
|
||||
* @date 2020/11/14 10:17
|
||||
* @description 基层工会核批
|
||||
*/
|
||||
/** 基层工会主席审核:PC 与手机共用业务接口,审核逻辑统一在 Service 中处理。 */
|
||||
@IocBean
|
||||
@Ok("json")
|
||||
@At("/platform/jf/activity_bx/division")
|
||||
public class DivisionCon {
|
||||
|
||||
|
||||
@Inject
|
||||
private Vi vi;
|
||||
|
||||
@Inject
|
||||
private MsgApi msgApi;
|
||||
|
||||
@Inject
|
||||
private ActivityBxService activityBxService;
|
||||
|
||||
@Inject
|
||||
private SysSignatureService sysSignatureService;
|
||||
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/jf/activitybx/division.html")
|
||||
@RequiresPermissions("sys.jf.activity_bx.division")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** 手机审核入口,使用独立页面目录及现有 PC 权限。 */
|
||||
@At
|
||||
@Ok("beetl:/mobile/jf/activitybx/division/index.html")
|
||||
@RequiresPermissions("sys.jf.activity_bx.division")
|
||||
public void mobile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* year 为年度,isAudit 为1全部/2已审核/3未审核;unitId、unionId 为筛选组织ID;
|
||||
* searchName 为 username/loginname,searchKeyword 为姓名或工号(空值不筛选)。
|
||||
* pageNumber 从1开始,pageSize 为1至100;pageOrderName 为 bxr_unionname/activity_type/absName/apply_type/apply_time/state_id,
|
||||
* pageOrderBy 为 asc/desc(兼容 ascending/descending),未指定有效排序时按状态、申请时间排序。
|
||||
* 返回分页对象,list 为申请数组,totalCount 为符合当前权限及筛选的总数。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.activity_bx.division")
|
||||
public Object pageData(@Param(value = "year", required = false) Integer year,
|
||||
@Param(value = "isAudit", required = false) Integer isAudit,
|
||||
@Param(value = "unitId", required = false) String unitId,
|
||||
@@ -71,87 +53,31 @@ public class DivisionCon {
|
||||
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize,
|
||||
@Param(value = "pageOrderName", required = false) String pageOrderName,
|
||||
@Param(value = "pageOrderBy", required = false) String pageOrderBy) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
act.*,
|
||||
u.loginname bxr_loginname,
|
||||
u.unionname bxr_unionname,
|
||||
s.state_name,
|
||||
s.state_color,
|
||||
abs.`name` absName
|
||||
FROM
|
||||
`activity_bx` act
|
||||
LEFT JOIN `user` u ON act.user_id = u.id
|
||||
LEFT JOIN sys_unit su ON su.id = u.unitid
|
||||
LEFT JOIN sys_union un ON su.unionid = un.id
|
||||
LEFT JOIN state s ON act.state_id = s.state_id
|
||||
LEFT JOIN activity_basic_settings abs ON abs.`code` = act.projectJfCode
|
||||
$condition
|
||||
""");
|
||||
|
||||
Cnd cnd = Cnd.NEW();
|
||||
|
||||
if (isAudit == 1) {
|
||||
cnd.and("act.state_id", ">=", 1010);
|
||||
} else {
|
||||
cnd.and("act.state_id", isAudit == 2 ? ">" : "=", 1010);
|
||||
}
|
||||
|
||||
|
||||
if (!ShiroUtil.hasAnyRoles("sysadmin")) {
|
||||
if (ShiroUtil.hasAnyRoles(new String[]{"H04", "gh01", "gh02"})) {
|
||||
cnd.and("un.id", "=", vi.getUnionId());
|
||||
}
|
||||
}
|
||||
Vi.cndPlus(cnd, "act.year", "=", year);
|
||||
if (Vi.isNotBlank(searchName, searchKeyword)) {
|
||||
cnd.where().andLike(searchName, searchKeyword);
|
||||
}
|
||||
|
||||
if (Vi.isNotBlank(pageOrderName, pageOrderBy)) {
|
||||
cnd.orderBy(pageOrderName, pageOrderBy);
|
||||
} else {
|
||||
cnd.asc("act.state_id").desc("act.apply_time");
|
||||
}
|
||||
Vi.cndPlus(cnd, "u.unitid", "=", unitId);
|
||||
Vi.cndPlus(cnd, "u.unionid", "=", unionId);
|
||||
sql.setCondition(cnd);
|
||||
return activityBxService.listPageMap(pageNumber, pageSize, sql);
|
||||
return activityBxService.reviewPage("division", new NutMap().addv("year", year).addv("isAudit", isAudit)
|
||||
.addv("unitId", unitId).addv("unionId", unionId).addv("searchName", searchName)
|
||||
.addv("searchKeyword", searchKeyword).addv("pageNumber", pageNumber).addv("pageSize", pageSize)
|
||||
.addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy));
|
||||
}
|
||||
|
||||
/** id 为申请主键;返回详情 Map,canReview 表示该申请是否仍处于当前待审节点。 */
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.activity_bx.division")
|
||||
public Object findOne(@Param("id") String id) {
|
||||
if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动申请");
|
||||
return activityBxService.reviewDetail("division", id);
|
||||
}
|
||||
|
||||
/** id 为申请ID,idea 为1至500字意见,sign 为图片数据或签名路径,flag 为true通过/false拒绝;返回统一结果,code=0 为成功,msg 为提示。 */
|
||||
@At
|
||||
@ViReturn
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Object doReview(@Param(value = "id", required = false) String id,
|
||||
@Param(value = "idea", required = false) String idea,
|
||||
@Param(value = "sign", required = false) String sign,
|
||||
@Param(value = "flag", required = false) Boolean flag) {
|
||||
ActivityBx aid = activityBxService.fetch(id);
|
||||
aid.setState_id(flag ? "1020" : "1015");
|
||||
aid.setDivision_chairman_name(ShiroUtil.getPrincipalProperty("id").toString());
|
||||
aid.setDivision_chairman_time(DateUtil.getDate());
|
||||
aid.setDivision_chairman_idea(idea);
|
||||
if (Strings.isNotBlank(sign)) {
|
||||
aid.setDivision_chairman(sysSignatureService.insert(new Sys_signature(sign)).getId());
|
||||
}
|
||||
activityBxService.update(aid);
|
||||
String nodeName = "";
|
||||
if (flag) {
|
||||
nodeName = "校工会负责人审核";
|
||||
// Sys_user_role sys_user_role = activityBxService.dao().fetch(Sys_user_role.class, Cnd.where("roleId", "=", Roles.GH_WYH_ZX));
|
||||
// if (Lang.isNotEmpty(sys_user_role)) {
|
||||
// Sys_user user = activityBxService.dao().fetch(Sys_user.class, sys_user_role.getUserId());
|
||||
Sys_user user = activityBxService.dao().fetch(Sys_user.class, Cnd.where("loginname","=","1019911404"));
|
||||
msgApi.sendWxMsg("活动申报", "", "text", aid.getUser_name() + "申报的【" + aid.getActivity_name() + "】,请前往智慧工会进行审核。", List.of(user.getLoginname()), null);
|
||||
FundsToHandler.CREATE_SCHOOL_FZR_TASK.exec(id, null, List.of(user.getLoginname()));
|
||||
// }
|
||||
} else {
|
||||
nodeName = "基层工会主席拒绝";
|
||||
Sys_user user = activityBxService.dao().fetch(Sys_user.class, aid.getUser_id());
|
||||
msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】被基层工会主席拒绝,审核意见:" + aid.getDivision_chairman_idea(), List.of(user.getLoginname()), null);
|
||||
FundsToHandler.REFUSE_TASK.exec(id, new NutMap().addv("option", nodeName), null);
|
||||
}
|
||||
FundsToHandler.COMPLETE_UNION_TASK.exec(id, new NutMap().addv("option", nodeName), null);
|
||||
@RequiresPermissions("sys.jf.activity_bx.division")
|
||||
public Object doReview(@Param("id") String id, @Param("idea") String idea,
|
||||
@Param("sign") String sign, @Param("flag") Boolean flag) {
|
||||
if (Strings.isBlank(id) || flag == null) throw new IllegalArgumentException("请选择申请和审核结果");
|
||||
activityBxService.reviewActivity("division", new NutMap().addv("id", id).addv("idea", idea)
|
||||
.addv("sign", sign).addv("flag", flag));
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +1,50 @@
|
||||
package io.v.nutz.zhgh.jf.controller.activitybx;
|
||||
|
||||
import io.v.nutz.base.annontation.ViReturn;
|
||||
import io.v.nutz.base.model.Goods;
|
||||
import io.v.nutz.base.utils.DateUtil;
|
||||
import io.v.nutz.base.utils.MsgApi;
|
||||
import io.v.nutz.base.utils.Vi;
|
||||
import io.v.nutz.sys.models.Sys_signature;
|
||||
import io.v.nutz.sys.models.Sys_user;
|
||||
import io.v.nutz.sys.models.User;
|
||||
import io.v.nutz.sys.services.SysSignatureService;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import io.v.nutz.zhgh.activity.models.ActivityTissue;
|
||||
import io.v.nutz.zhgh.activity.services.ActivityTissueService;
|
||||
import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler;
|
||||
import io.v.nutz.zhgh.jf.model.ActivityBx;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.json.Json;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
import org.nutz.json.Json;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author zhf
|
||||
* @date 2020/11/14 16:03
|
||||
* @description 校工会主席审核
|
||||
*/
|
||||
/** 校工会主席审核:PC 与手机共用业务接口,审核逻辑统一在 Service 中处理。 */
|
||||
@IocBean
|
||||
@Ok("json")
|
||||
@At("/platform/jf/activity_bx/union")
|
||||
public class UnionCon {
|
||||
|
||||
|
||||
@Inject
|
||||
private Vi vi;
|
||||
|
||||
@Inject
|
||||
private MsgApi msgApi;
|
||||
|
||||
@Inject
|
||||
private ActivityBxService activityBxService;
|
||||
|
||||
@Inject
|
||||
private SysSignatureService sysSignatureService;
|
||||
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/jf/activitybx/union.html")
|
||||
@RequiresPermissions("sys.jf.activity_bx.union")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** 手机审核入口,使用独立页面目录及现有 PC 权限。 */
|
||||
@At
|
||||
@Ok("beetl:/mobile/jf/activitybx/union/index.html")
|
||||
@RequiresPermissions("sys.jf.activity_bx.union")
|
||||
public void mobile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* year 为年度,isAudit 为1全部/2已审核/3未审核;unitId、unionId 为筛选组织ID;
|
||||
* searchName 为 username/loginname,searchKeyword 为姓名或工号(空值不筛选)。
|
||||
* pageNumber 从1开始,pageSize 为1至100;pageOrderName 为 bxr_unionname/activity_type/absName/apply_type/apply_time/state_id,
|
||||
* pageOrderBy 为 asc/desc(兼容 ascending/descending),未指定有效排序时按状态、申请时间排序。
|
||||
* 返回分页对象,list 为申请数组,totalCount 为符合当前权限及筛选的总数。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.activity_bx.union")
|
||||
public Object pageData(@Param(value = "year", required = false) Integer year,
|
||||
@Param(value = "isAudit", required = false) Integer isAudit,
|
||||
@Param(value = "unitId", required = false) String unitId,
|
||||
@@ -74,129 +54,35 @@ public class UnionCon {
|
||||
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize,
|
||||
@Param(value = "pageOrderName", required = false) String pageOrderName,
|
||||
@Param(value = "pageOrderBy", required = false) String pageOrderBy) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
act.*,
|
||||
u.loginname bxr_loginname,
|
||||
u.unionname bxr_unionname,
|
||||
s.state_name,
|
||||
s.state_color,
|
||||
abs.`name` absName
|
||||
FROM
|
||||
`activity_bx` act
|
||||
LEFT JOIN `user` u ON act.user_id = u.id
|
||||
LEFT JOIN state s ON act.state_id = s.state_id
|
||||
LEFT JOIN activity_basic_settings abs ON abs.`code` = act.projectJfCode
|
||||
$condition
|
||||
""");
|
||||
|
||||
Cnd cnd = Cnd.NEW();
|
||||
|
||||
if (isAudit == 1) {
|
||||
cnd.and("act.state_id", ">=", 1040);
|
||||
} else {
|
||||
cnd.and("act.state_id", isAudit == 2 ? ">" : "=", 1040);
|
||||
}
|
||||
|
||||
|
||||
Vi.cndPlus(cnd, "act.year", "=", year);
|
||||
if (Vi.isNotBlank(searchName, searchKeyword)) {
|
||||
cnd.where().andLike(searchName, searchKeyword);
|
||||
}
|
||||
|
||||
if (Vi.isNotBlank(pageOrderName, pageOrderBy)) {
|
||||
cnd.orderBy(pageOrderName, pageOrderBy);
|
||||
} else {
|
||||
cnd.asc("act.state_id").desc("act.apply_time");
|
||||
}
|
||||
|
||||
Vi.cndPlus(cnd, "u.unitid", "=", unitId);
|
||||
Vi.cndPlus(cnd, "u.unionid", "=", unionId);
|
||||
sql.setCondition(cnd);
|
||||
return activityBxService.listPageMap(pageNumber, pageSize, sql);
|
||||
return activityBxService.reviewPage("union", new NutMap().addv("year", year).addv("isAudit", isAudit)
|
||||
.addv("unitId", unitId).addv("unionId", unionId).addv("searchName", searchName)
|
||||
.addv("searchKeyword", searchKeyword).addv("pageNumber", pageNumber).addv("pageSize", pageSize)
|
||||
.addv("pageOrderName", pageOrderName).addv("pageOrderBy", pageOrderBy));
|
||||
}
|
||||
|
||||
/** id 为申请主键;返回详情 Map,canReview 表示该申请是否仍处于当前待审节点。 */
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.activity_bx.union")
|
||||
public Object findOne(@Param("id") String id) {
|
||||
if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动申请");
|
||||
return activityBxService.reviewDetail("union", id);
|
||||
}
|
||||
|
||||
@Inject
|
||||
private io.v.nutz.zhgh.jf.service.jfUseService jfUseService;
|
||||
|
||||
@Inject
|
||||
private io.v.nutz.zhgh.jf.service.jfYjghService jfYjghService;
|
||||
|
||||
@Inject
|
||||
private ActivityTissueService activityTissueService;
|
||||
@Inject
|
||||
private io.v.nutz.zhgh.jf.service.jfClubService jfClubService;
|
||||
|
||||
|
||||
/**
|
||||
* data 为 JSON 对象字符串:id 为申请ID,idea 为1至500字意见,sign 为图片数据或签名路径,flag 为布尔值(true通过/false拒绝)。
|
||||
* activity_money 为可选的非负实际费用,不提交时保留原费用。
|
||||
* 返回统一结果对象,code=0 为成功,msg 为提示;失败不推进审核状态。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Object doReview(String data) {
|
||||
NutMap nutMap = Json.fromJson(NutMap.class, data);
|
||||
|
||||
ActivityBx aid = activityBxService.fetch(nutMap.getString("id"));
|
||||
aid.setState_id(nutMap.getBoolean("flag") ? "1060" : "1045");
|
||||
aid.setUnion_chairman_name(ShiroUtil.getPrincipalProperty("id").toString());
|
||||
aid.setUnion_chairman_time(DateUtil.getDate());
|
||||
aid.setUnion_chairman_idea(nutMap.getString("idea"));
|
||||
aid.setActivity_money(nutMap.getDouble("activity_money"));
|
||||
List<Goods> goods = nutMap.getAsList("goods", Goods.class);
|
||||
aid.setGoods(goods);
|
||||
|
||||
if (Strings.isNotBlank(nutMap.getString("sign"))) {
|
||||
aid.setUnion_chairman(sysSignatureService.insert(new Sys_signature(nutMap.getString("sign"))).getId());
|
||||
}
|
||||
|
||||
String nodeName = "";
|
||||
if (nutMap.getBoolean("flag")) {
|
||||
User user = activityBxService.dao().fetch(User.class, Cnd.where("id", "=", aid.getUser_id()));
|
||||
msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】已审核通过,请前往智慧工会进行查看。", List.of(user.getLoginname()), null);
|
||||
|
||||
|
||||
if (aid.getActivity_id() == null) {
|
||||
ActivityTissue activityTissue = new ActivityTissue();
|
||||
activityTissue.setId(aid.getId());
|
||||
activityTissue.setApplyTime(DateUtil.getDate());
|
||||
activityTissue.setType(aid.getActivity_type());
|
||||
activityTissue.setActivityCode(aid.getProjectCode());
|
||||
activityTissue.setName(aid.getActivity_name());
|
||||
activityTissue.setUserId(aid.getUser_id());
|
||||
activityTissue.setStartPlannedDate(aid.getStartPlannedDate());
|
||||
activityTissue.setEndPlannedDate(aid.getEndPlannedDate());
|
||||
activityTissue.setStartTime(aid.getStartTime());
|
||||
activityTissue.setEndTime(aid.getEndTime());
|
||||
activityTissue.setAddress(aid.getAddress());
|
||||
if (aid.getActivity_type() == 40002) {
|
||||
activityTissue.setUnionId(user.getUnionid());
|
||||
} else if (aid.getActivity_type() == 40003) {
|
||||
activityTissue.setClubId(aid.getClubId());
|
||||
}
|
||||
activityTissue.setProjectTypeCode("50006");
|
||||
activityBxService.insert(activityTissue);
|
||||
aid.setActivity_id(activityTissue.getId());
|
||||
}
|
||||
nodeName = "审核通过";
|
||||
} else {
|
||||
Sys_user user = activityBxService.dao().fetch(Sys_user.class, aid.getUser_id());
|
||||
msgApi.sendWxMsg("活动申报", "", "text", "您申报的活动【" + aid.getActivity_name() + "】被校工会主席拒绝,审核意见:" + aid.getUnion_chairman_idea(), List.of(user.getLoginname()), null);
|
||||
FundsToHandler.REFUSE_TASK.exec(aid.getId(), new NutMap().addv("option", nodeName), null);
|
||||
}
|
||||
activityBxService.update(aid);
|
||||
FundsToHandler.COMPLETE_SCHOOL_TASK.exec(aid.getId(), new NutMap().addv("option", nodeName), null);
|
||||
|
||||
/*jf_yjgh jfYjgh = jfYjghService.fetch(Cnd.where("unionId", "=", vi.getUnionId(aid.getUser_id())).and("year", "=", Calendar.getInstance().get(Calendar.YEAR)));
|
||||
if (jfYjgh != null) {
|
||||
jf_use use = new jf_use();
|
||||
use.setAdjust_id(jfYjgh.getId());
|
||||
use.setApply_id(aid.getId());
|
||||
use.setProject(aid.getActivity_name());
|
||||
use.setAdjust_money((double) Math.round(aid.getActivity_money()));
|
||||
use.setAdjust_person(ShiroUtil.getPrincipalProperty("id").toString());
|
||||
use.setAdjust_time(DateUtil.getDate());
|
||||
jfUseService.addUse(use, false);
|
||||
}*/
|
||||
|
||||
@RequiresPermissions("sys.jf.activity_bx.union")
|
||||
public Object doReview(@Param("data") String data) {
|
||||
if (Strings.isBlank(data)) throw new IllegalArgumentException("请填写审核信息");
|
||||
NutMap form = Json.fromJson(NutMap.class, data);
|
||||
if (form == null) throw new IllegalArgumentException("审核参数格式错误");
|
||||
activityBxService.reviewActivity("union", form);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+34
-70
@@ -1,64 +1,47 @@
|
||||
package io.v.nutz.zhgh.jf.controller.reimbursement;
|
||||
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.v.nutz.base.annontation.ViReturn;
|
||||
import io.v.nutz.base.utils.Vi;
|
||||
import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import io.v.nutz.base.model.Audit;
|
||||
import io.v.nutz.base.query.PageForm;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import io.v.nutz.web.commons.slog.annotation.SLog;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @version 1.0
|
||||
* @Author zzr
|
||||
* @name:ActivityReimbursementClubChairmanController
|
||||
* @Date 2024/4/26 14:58
|
||||
* @注释 活动报销,协会负责人审核
|
||||
*/
|
||||
/** 活动报销:协会负责人审核,PC 与手机共用列表、数据范围和审核服务。 */
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@At("/platform/jf/reimbursement/clubChairman")
|
||||
public class ActivityReimbursementClubChairmanController {
|
||||
|
||||
@Inject
|
||||
private Vi vi;
|
||||
|
||||
@Inject
|
||||
private ActivityBxService activityBxService;
|
||||
|
||||
/** 保留现有 PC 页面入口。 */
|
||||
@At("")
|
||||
@Ok("beetl:/platform/jf/reimbursement/clubChairman.html")
|
||||
@RequiresPermissions("sys.jf.reimbursement.clubChairman")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** 手机页面可携带 id 报销主键和 mode=audit/view;权限名称与 PC 一致。 */
|
||||
@At
|
||||
@Ok("beetl:/mobile/jf/reimbursement/clubChairman/index.html")
|
||||
@RequiresPermissions("sys.jf.reimbursement.clubChairman")
|
||||
public void mobile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 活动报销协会审核
|
||||
* @param pageForm
|
||||
* @param year
|
||||
* @param clubId
|
||||
* @param activityName
|
||||
* @param isAudit 0,全部;1,已审核;2,未审核
|
||||
* @return
|
||||
* pageForm 含 pageNumber/pageSize、searchName(u.username/u.loginname)、searchKeyword 和排序字段/方向;
|
||||
* year 为年度,clubId/unionId/unitId 为组织ID,activityName 为活动名,isAudit 为0全部/1已审核/2未审核。
|
||||
* 返回统一结果,code=0 时 data 为 Pagination,list 为本页记录、totalCount 为总数。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@@ -66,57 +49,38 @@ public class ActivityReimbursementClubChairmanController {
|
||||
public Object pageData(PageForm pageForm,
|
||||
@Param(value = "year", required = false) Integer year,
|
||||
@Param(value = "clubId", required = false) String clubId,
|
||||
@Param(value = "unionId", required = false) String unionId,
|
||||
@Param(value = "unitId", required = false) String unitId,
|
||||
@Param(value = "activityName", required = false) String activityName,
|
||||
@Param(value = "isAudit", required = false) String isAudit) {
|
||||
Sql sql = activityBxService.activityReimbursementSql();
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("act.`year`","=",year);
|
||||
if (StrUtil.isNotBlank(activityName)) {
|
||||
cnd.and(Cnd.likeEX("act.activity_name", activityName));
|
||||
}
|
||||
// 姓名/工号查询由页面通过 PageForm 统一传入,这里按所选字段做模糊匹配。
|
||||
if (StrUtil.isNotBlank(pageForm.getSearchName()) && StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||
cnd.and(Cnd.likeEX(pageForm.getSearchName(), pageForm.getSearchKeyword()));
|
||||
}
|
||||
|
||||
cnd.and("act.reimbursement_state_id","is not",null);
|
||||
|
||||
//1已审核;2未审核;0全部
|
||||
cnd.and("act.reimbursement_state_id", "1".equals(isAudit) ? ">" : "2".equals(isAudit) ? "=" : ">=", 1000);
|
||||
|
||||
if (ShiroUtil.hasAnyRoles("sysadmin,A06")){
|
||||
cnd.andEX("act.clubId","=",clubId);
|
||||
} else {
|
||||
cnd.and("act.clubId","=",vi.getClubId());
|
||||
}
|
||||
sql.setCondition(cnd);
|
||||
return activityBxService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql);
|
||||
return activityBxService.reimbursementPage("clubChairman", pageForm, NutMap.NEW()
|
||||
.addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId)
|
||||
.addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit));
|
||||
}
|
||||
|
||||
/** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.reimbursement.clubChairman")
|
||||
public Object findOne(@Param("id") String id) {
|
||||
if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动报销申请");
|
||||
return activityBxService.reimbursementDetail("clubChairman", id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* id 为报销主键;audit 为 JSON 对象,auditOpinion 为1至500字意见,auditSign 为签字图片或历史路径,
|
||||
* auditPass 为 true通过/false拒绝。人员和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为提示,data 为空。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@RequiresPermissions("sys.jf.reimbursement.clubChairman")
|
||||
@SLog(type = "文体活动", tag = "活动报销", msg = "协会负责人审核", param = true, result = true)
|
||||
public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){
|
||||
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString());
|
||||
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString());
|
||||
audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString());
|
||||
audit.setAuditTime(new Date());
|
||||
activityBxService.dao().insert(audit);
|
||||
|
||||
Chain chain = Chain.make("reimbursement_club_audit_id", audit.getId());
|
||||
chain.add("reimbursement_state_id", audit.getAuditPass() ? 1020 : 1005);
|
||||
activityBxService.update(chain,Cnd.where("id","=",id));
|
||||
|
||||
ReimbursementToHandler.COMPLETE_CLUB_TASK.exec(id, NutMap.NEW().addv("nodeName",audit.getAuditPass() ? "协会负责人通过" : "协会负责人拒绝"));
|
||||
if (audit.getAuditPass()) {
|
||||
ReimbursementToHandler.CREATE_SCHOOL_DIRECTOR_TASK.exec(id,null);
|
||||
} else {
|
||||
ReimbursementToHandler.REFUSE_UNION_TASK.exec(id,NutMap.NEW().addv("nodeName","协会负责人拒绝"));
|
||||
public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) {
|
||||
if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) {
|
||||
throw new IllegalArgumentException("请选择活动报销申请并填写审核信息");
|
||||
}
|
||||
activityBxService.reviewReimbursement("clubChairman", id, audit);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+39
-58
@@ -1,105 +1,86 @@
|
||||
package io.v.nutz.zhgh.jf.controller.reimbursement;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.v.nutz.base.annontation.ViReturn;
|
||||
import io.v.nutz.base.utils.Vi;
|
||||
import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import io.v.nutz.base.model.Audit;
|
||||
import io.v.nutz.base.query.PageForm;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import io.v.nutz.web.commons.slog.annotation.SLog;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* @version 1.0
|
||||
* @Author zzr
|
||||
* @name:ActivityReimbursementSchoolChairmanController
|
||||
* @Date 2024/4/26 14:58
|
||||
* @注释 报销,校工会主席审核
|
||||
*/
|
||||
/** 活动报销:校工会主席审核,PC 与手机共用列表、数据范围和审核服务。 */
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@At("/platform/jf/reimbursement/schoolChairman")
|
||||
public class ActivityReimbursementSchoolChairmanController {
|
||||
|
||||
@Inject
|
||||
private ActivityBxService activityBxService;
|
||||
|
||||
/** 保留现有 PC 页面入口。 */
|
||||
@At("")
|
||||
@Ok("beetl:/platform/jf/reimbursement/schoolChairman.html")
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolChairman")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** 手机页面可携带 id 报销主键和 mode=audit/view;权限名称与 PC 一致。 */
|
||||
@At
|
||||
@Ok("beetl:/mobile/jf/reimbursement/schoolChairman/index.html")
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolChairman")
|
||||
public void mobile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* pageForm 含 pageNumber/pageSize、searchName(u.username/u.loginname)、searchKeyword 和排序字段/方向;
|
||||
* year 为年度,clubId/unionId/unitId 为组织ID,activityName 为活动名,isAudit 为0全部/1已审核/2未审核。
|
||||
* 返回统一结果,code=0 时 data 为 Pagination,list 为本页记录、totalCount 为总数。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolChairman")
|
||||
public Object pageData(PageForm pageForm,
|
||||
@Param(value = "year", required = false) Integer year,
|
||||
@Param(value = "clubId", required = false) String clubId,
|
||||
@Param(value = "unionId", required = false) String unionId,
|
||||
@Param(value = "unitId", required = false) String unitId,
|
||||
@Param(value = "clubId",required = false) String clubId,
|
||||
@Param(value = "activityName", required = false) String activityName,
|
||||
@Param(value = "isAudit", required = false) String isAudit) {
|
||||
Sql sql = activityBxService.activityReimbursementSql();
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("act.`year`","=",year);
|
||||
if (StrUtil.isNotBlank(activityName)) {
|
||||
cnd.and(Cnd.likeEX("act.activity_name", activityName));
|
||||
}
|
||||
// 姓名/工号查询由页面通过 PageForm 统一传入,这里按所选字段做模糊匹配。
|
||||
if (StrUtil.isNotBlank(pageForm.getSearchName()) && StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||
cnd.and(Cnd.likeEX(pageForm.getSearchName(), pageForm.getSearchKeyword()));
|
||||
}
|
||||
|
||||
//1已审核;2未审核;0全部
|
||||
cnd.and("act.reimbursement_state_id", "1".equals(isAudit) ? ">" : "2".equals(isAudit) ? "=" : ">=", 1040);
|
||||
|
||||
cnd.andEX("u.unionid","=",unionId);
|
||||
cnd.andEX("u.unitId","=",unitId);
|
||||
cnd.andEX("act.clubId","=",clubId);
|
||||
sql.setCondition(cnd);
|
||||
return activityBxService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql);
|
||||
return activityBxService.reimbursementPage("schoolChairman", pageForm, NutMap.NEW()
|
||||
.addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId)
|
||||
.addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolChairman")
|
||||
public Object findOne(@Param("id") String id) {
|
||||
if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动报销申请");
|
||||
return activityBxService.reimbursementDetail("schoolChairman", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* id 为报销主键;audit 为 JSON 对象,auditOpinion 为1至500字意见,auditSign 为签字图片或历史路径,
|
||||
* auditPass 为 true通过/false拒绝。人员和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为提示,data 为空。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolChairman")
|
||||
@SLog(type = "文体活动", tag = "活动报销", msg = "校工会主席审核", param = true, result = true)
|
||||
public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){
|
||||
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString());
|
||||
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString());
|
||||
audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString());
|
||||
audit.setAuditTime(new Date());
|
||||
activityBxService.dao().insert(audit);
|
||||
|
||||
Chain chain = Chain.make("reimbursement_school_chairman_id", audit.getId());
|
||||
chain.add("reimbursement_state_id", audit.getAuditPass() ? 1061 : 1045);
|
||||
activityBxService.update(chain,Cnd.where("id","=",id));
|
||||
|
||||
ReimbursementToHandler.COMPLETE_SCHOOL_CHAIRMAN_TASK.exec(id, NutMap.NEW().addv("nodeName",audit.getAuditPass() ? "校工会主席通过" : "校工会主席拒绝"));
|
||||
if (audit.getAuditPass()) {
|
||||
ReimbursementToHandler.COMPLETE_PROCESS.exec(id,null);
|
||||
} else {
|
||||
ReimbursementToHandler.REFUSE_UNION_TASK.exec(id,NutMap.NEW().addv("nodeName","校工会主席拒绝"));
|
||||
public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) {
|
||||
if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) {
|
||||
throw new IllegalArgumentException("请选择活动报销申请并填写审核信息");
|
||||
}
|
||||
activityBxService.reviewReimbursement("schoolChairman", id, audit);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+39
-57
@@ -1,104 +1,86 @@
|
||||
package io.v.nutz.zhgh.jf.controller.reimbursement;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.v.nutz.base.annontation.ViReturn;
|
||||
import io.v.nutz.base.utils.Vi;
|
||||
import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import io.v.nutz.base.model.Audit;
|
||||
import io.v.nutz.base.query.PageForm;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import io.v.nutz.web.commons.slog.annotation.SLog;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @version 1.0
|
||||
* @Author zzr
|
||||
* @name:ActivityReimbursementSchoolDirectorController
|
||||
* @Date 2024/4/26 14:58
|
||||
* @注释 报销校工会负责人审核
|
||||
*/
|
||||
/** 活动报销:校工会负责人审核,PC 与手机共用列表、数据范围和审核服务。 */
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@At("/platform/jf/reimbursement/schoolDirector")
|
||||
public class ActivityReimbursementSchoolDirectorController {
|
||||
|
||||
@Inject
|
||||
private ActivityBxService activityBxService;
|
||||
|
||||
/** 保留现有 PC 页面入口。 */
|
||||
@At("")
|
||||
@Ok("beetl:/platform/jf/reimbursement/schoolDirector.html")
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolDirector")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** 手机页面可携带 id 报销主键和 mode=audit/view;权限名称与 PC 一致。 */
|
||||
@At
|
||||
@Ok("beetl:/mobile/jf/reimbursement/schoolDirector/index.html")
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolDirector")
|
||||
public void mobile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* pageForm 含 pageNumber/pageSize、searchName(u.username/u.loginname)、searchKeyword 和排序字段/方向;
|
||||
* year 为年度,clubId/unionId/unitId 为组织ID,activityName 为活动名,isAudit 为0全部/1已审核/2未审核。
|
||||
* 返回统一结果,code=0 时 data 为 Pagination,list 为本页记录、totalCount 为总数。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolDirector")
|
||||
public Object pageData(PageForm pageForm,
|
||||
@Param(value = "year", required = false) Integer year,
|
||||
@Param(value = "clubId", required = false) String clubId,
|
||||
@Param(value = "unionId", required = false) String unionId,
|
||||
@Param(value = "unitId", required = false) String unitId,
|
||||
@Param(value = "clubId",required = false) String clubId,
|
||||
@Param(value = "activityName", required = false) String activityName,
|
||||
@Param(value = "isAudit", required = false) String isAudit) {
|
||||
Sql sql = activityBxService.activityReimbursementSql();
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("act.`year`","=",year);
|
||||
if (StrUtil.isNotBlank(activityName)) {
|
||||
cnd.and(Cnd.likeEX("act.activity_name", activityName));
|
||||
}
|
||||
// 姓名/工号查询由页面通过 PageForm 统一传入,这里按所选字段做模糊匹配。
|
||||
if (StrUtil.isNotBlank(pageForm.getSearchName()) && StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||
cnd.and(Cnd.likeEX(pageForm.getSearchName(), pageForm.getSearchKeyword()));
|
||||
}
|
||||
|
||||
//1已审核;2未审核;0全部
|
||||
cnd.and("act.reimbursement_state_id", "1".equals(isAudit) ? ">" : "2".equals(isAudit) ? "=" : ">=", 1020);
|
||||
|
||||
cnd.andEX("u.unionid","=",unionId);
|
||||
cnd.andEX("u.unitId","=",unitId);
|
||||
cnd.andEX("act.clubId","=",clubId);
|
||||
sql.setCondition(cnd);
|
||||
return activityBxService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql);
|
||||
return activityBxService.reimbursementPage("schoolDirector", pageForm, NutMap.NEW()
|
||||
.addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId)
|
||||
.addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolDirector")
|
||||
public Object findOne(@Param("id") String id) {
|
||||
if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动报销申请");
|
||||
return activityBxService.reimbursementDetail("schoolDirector", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* id 为报销主键;audit 为 JSON 对象,auditOpinion 为1至500字意见,auditSign 为签字图片或历史路径,
|
||||
* auditPass 为 true通过/false拒绝。人员和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为提示,data 为空。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@RequiresPermissions("sys.jf.reimbursement.schoolDirector")
|
||||
@SLog(type = "文体活动", tag = "活动报销", msg = "校工会负责人审核", param = true, result = true)
|
||||
public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){
|
||||
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString());
|
||||
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString());
|
||||
audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString());
|
||||
audit.setAuditTime(new Date());
|
||||
activityBxService.dao().insert(audit);
|
||||
|
||||
Chain chain = Chain.make("reimbursement_school_director_id", audit.getId());
|
||||
//通过,校工会主席审核
|
||||
chain.add("reimbursement_state_id", audit.getAuditPass() ? 1040 : 1025);
|
||||
activityBxService.update(chain,Cnd.where("id","=",id));
|
||||
|
||||
ReimbursementToHandler.COMPLETE_SCHOOL_DIRECTOR_TASK.exec(id, NutMap.NEW().addv("nodeName",audit.getAuditPass() ? "校工会负责人通过" : "校工会负责人拒绝"));
|
||||
if (audit.getAuditPass()) {
|
||||
ReimbursementToHandler.CREATE_SCHOOL_CHAIRMAN_TASK.exec(id,null);
|
||||
} else {
|
||||
ReimbursementToHandler.REFUSE_UNION_TASK.exec(id,NutMap.NEW().addv("nodeName","校工会负责人拒绝"));
|
||||
public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) {
|
||||
if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) {
|
||||
throw new IllegalArgumentException("请选择活动报销申请并填写审核信息");
|
||||
}
|
||||
activityBxService.reviewReimbursement("schoolDirector", id, audit);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+39
-67
@@ -1,114 +1,86 @@
|
||||
package io.v.nutz.zhgh.jf.controller.reimbursement;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.v.nutz.base.annontation.ViReturn;
|
||||
import io.v.nutz.base.utils.Vi;
|
||||
import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import io.v.nutz.base.model.Audit;
|
||||
import io.v.nutz.base.query.PageForm;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import io.v.nutz.zhgh.jf.service.ActivityBxService;
|
||||
import io.v.nutz.web.commons.slog.annotation.SLog;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* @version 1.0
|
||||
* @Author zzr
|
||||
* @name:ActivityReimbursementUnionChairmanController
|
||||
* @Date 2024/4/26 14:58
|
||||
* @注释 活动报销,分工会主席审核
|
||||
*/
|
||||
/** 活动报销:分工会主席审核,PC 与手机共用列表、数据范围和审核服务。 */
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@At("/platform/jf/reimbursement/unionChairman")
|
||||
public class ActivityReimbursementUnionChairmanController {
|
||||
|
||||
@Inject
|
||||
private ActivityBxService activityBxService;
|
||||
|
||||
/** 保留现有 PC 页面入口。 */
|
||||
@At("")
|
||||
@Ok("beetl:/platform/jf/reimbursement/unionChairman.html")
|
||||
@RequiresPermissions("sys.jf.reimbursement.unionChairman")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** 手机页面可携带 id 报销主键和 mode=audit/view;权限名称与 PC 一致。 */
|
||||
@At
|
||||
@Ok("beetl:/mobile/jf/reimbursement/unionChairman/index.html")
|
||||
@RequiresPermissions("sys.jf.reimbursement.unionChairman")
|
||||
public void mobile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* pageForm 含 pageNumber/pageSize、searchName(u.username/u.loginname)、searchKeyword 和排序字段/方向;
|
||||
* year 为年度,clubId/unionId/unitId 为组织ID,activityName 为活动名,isAudit 为0全部/1已审核/2未审核。
|
||||
* 返回统一结果,code=0 时 data 为 Pagination,list 为本页记录、totalCount 为总数。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.reimbursement.unionChairman")
|
||||
public Object pageData(PageForm pageForm,
|
||||
@Param(value = "year", required = false) Integer year,
|
||||
@Param(value = "clubId", required = false) String clubId,
|
||||
@Param(value = "unionId", required = false) String unionId,
|
||||
@Param(value = "unitId", required = false) String unitId,
|
||||
@Param(value = "activityName", required = false) String activityName,
|
||||
@Param(value = "isAudit", required = false) String isAudit) {
|
||||
Sql sql = activityBxService.activityReimbursementSql();
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("act.`year`","=",year);
|
||||
if (StrUtil.isNotBlank(activityName)) {
|
||||
cnd.and(Cnd.likeEX("act.activity_name", activityName));
|
||||
}
|
||||
// 姓名/工号查询由页面通过 PageForm 统一传入,这里按所选字段做模糊匹配。
|
||||
if (StrUtil.isNotBlank(pageForm.getSearchName()) && StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||
cnd.and(Cnd.likeEX(pageForm.getSearchName(), pageForm.getSearchKeyword()));
|
||||
}
|
||||
|
||||
//1已审核;2未审核;0全部
|
||||
cnd.and("act.reimbursement_state_id", "1".equals(isAudit) ? ">" : "2".equals(isAudit) ? "=" : ">=", 1010);
|
||||
|
||||
if (ShiroUtil.hasAnyRoles("sysadmin,A06")){
|
||||
cnd.andEX("u.unionid","=",unionId);
|
||||
} else {
|
||||
cnd.and("u.unionid","=", Vi.getUnionId());
|
||||
}
|
||||
cnd.andEX("u.unitId","=",unitId);
|
||||
sql.setCondition(cnd);
|
||||
return activityBxService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql);
|
||||
return activityBxService.reimbursementPage("unionChairman", pageForm, NutMap.NEW()
|
||||
.addv("year", year).addv("clubId", clubId).addv("unionId", unionId).addv("unitId", unitId)
|
||||
.addv("activityName", activityName).addv("isAudit", Strings.isBlank(isAudit) ? "2" : isAudit));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 基层工会活动报销
|
||||
* 基层工会活动——>基层工会主席审核——>校工会主席审核——>流程结束
|
||||
* @param id
|
||||
* @param audit
|
||||
* @return
|
||||
*/
|
||||
/** id 为报销主键;返回统一结果,data 为详情 Map,含费用、附件、审核记录、报销状态及 canReview。 */
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("sys.jf.reimbursement.unionChairman")
|
||||
public Object findOne(@Param("id") String id) {
|
||||
if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择活动报销申请");
|
||||
return activityBxService.reimbursementDetail("unionChairman", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* id 为报销主键;audit 为 JSON 对象,auditOpinion 为1至500字意见,auditSign 为签字图片或历史路径,
|
||||
* auditPass 为 true通过/false拒绝。人员和时间由服务端确定;返回统一结果,code=0 表示成功,msg 为提示,data 为空。
|
||||
*/
|
||||
@At
|
||||
@ViReturn
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@RequiresPermissions("sys.jf.reimbursement.unionChairman")
|
||||
@SLog(type = "文体活动", tag = "活动报销", msg = "分工会主席审核", param = true, result = true)
|
||||
public Object doAuditing(@Param("id") String id,@Param("audit") Audit audit){
|
||||
audit.setAuditor(ShiroUtil.getPrincipalProperty("id").toString());
|
||||
audit.setUsername(ShiroUtil.getPrincipalProperty("username").toString());
|
||||
audit.setLoginname(ShiroUtil.getPrincipalProperty("loginname").toString());
|
||||
audit.setAuditTime(new Date());
|
||||
activityBxService.dao().insert(audit);
|
||||
|
||||
Chain chain = Chain.make("reimbursement_union_audit_id", audit.getId());
|
||||
//通过,校工会主席审核
|
||||
chain.add("reimbursement_state_id", audit.getAuditPass() ? 1020 : 1015);
|
||||
activityBxService.update(chain,Cnd.where("id","=",id));
|
||||
|
||||
ReimbursementToHandler.COMPLETE_UNION_TASK.exec(id, NutMap.NEW().addv("nodeName",audit.getAuditPass() ? "基层工会通过" : "基层工会拒绝"));
|
||||
if (audit.getAuditPass()) {
|
||||
ReimbursementToHandler.CREATE_SCHOOL_DIRECTOR_TASK.exec(id,null);
|
||||
} else {
|
||||
ReimbursementToHandler.REFUSE_UNION_TASK.exec(id,NutMap.NEW().addv("nodeName","基层工会拒绝"));
|
||||
public Object doAuditing(@Param("id") String id, @Param("audit") Audit audit) {
|
||||
if (Strings.isBlank(id) || audit == null || audit.getAuditPass() == null) {
|
||||
throw new IllegalArgumentException("请选择活动报销申请并填写审核信息");
|
||||
}
|
||||
activityBxService.reviewReimbursement("unionChairman", id, audit);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,8 +107,8 @@ public enum FundsToHandler {
|
||||
loginNames,
|
||||
"/platform/jf/activity_bx/association",
|
||||
"/platform/jf/activity_bx/association",
|
||||
"/platform/jf/activity_bx/association",
|
||||
"/platform/jf/activity_bx/association"
|
||||
"/platform/jf/activity_bx/association/mobile?id=" + reimbursementId + "&mode=audit",
|
||||
"/platform/jf/activity_bx/association/mobile?id=" + reimbursementId + "&mode=view"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "协会负责人审核");
|
||||
@@ -144,8 +144,8 @@ public enum FundsToHandler {
|
||||
loginNames,
|
||||
"/platform/jf/activity_bx/division",
|
||||
"/platform/jf/activity_bx/division",
|
||||
"/platform/jf/activity_bx/division",
|
||||
"/platform/jf/activity_bx/division"
|
||||
"/platform/jf/activity_bx/division/mobile?id=" + reimbursementId + "&mode=audit",
|
||||
"/platform/jf/activity_bx/division/mobile?id=" + reimbursementId + "&mode=view"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "基层工会审核");
|
||||
@@ -181,8 +181,8 @@ public enum FundsToHandler {
|
||||
loginNames,
|
||||
"/platform/jf/activity_bx/accounting",
|
||||
"/platform/jf/activity_bx/accounting",
|
||||
"/platform/jf/activity_bx/accounting",
|
||||
"/platform/jf/activity_bx/accounting"
|
||||
"/platform/jf/activity_bx/accounting/mobile?id=" + reimbursementId + "&mode=audit",
|
||||
"/platform/jf/activity_bx/accounting/mobile?id=" + reimbursementId + "&mode=view"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "校工会负责人审核");
|
||||
@@ -217,8 +217,8 @@ public enum FundsToHandler {
|
||||
loginNames,
|
||||
"/platform/jf/activity_bx/union",
|
||||
"/platform/jf/activity_bx/union",
|
||||
"/platform/jf/activity_bx/union",
|
||||
"/platform/jf/activity_bx/union"
|
||||
"/platform/jf/activity_bx/union/mobile?id=" + reimbursementId + "&mode=audit",
|
||||
"/platform/jf/activity_bx/union/mobile?id=" + reimbursementId + "&mode=view"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("funds_apply@" + reimbursementId, "校工会主席审核");
|
||||
|
||||
@@ -129,8 +129,8 @@ public enum ReimbursementToHandler {
|
||||
unionLeader,
|
||||
"/platform/jf/reimbursement/unionChairman",
|
||||
"/platform/jf/reimbursement/unionChairman",
|
||||
"/platform/jf/reimbursement/unionChairman",
|
||||
"/platform/jf/reimbursement/unionChairman"
|
||||
"/platform/jf/reimbursement/unionChairman/mobile?id=" + reimbursementId + "&mode=audit",
|
||||
"/platform/jf/reimbursement/unionChairman/mobile?id=" + reimbursementId + "&mode=view"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "基层工会审核");
|
||||
@@ -173,8 +173,8 @@ public enum ReimbursementToHandler {
|
||||
clubLeader,
|
||||
"/platform/jf/reimbursement/clubChairman",
|
||||
"/platform/jf/reimbursement/clubChairman",
|
||||
"/platform/jf/reimbursement/clubChairman",
|
||||
"/platform/jf/reimbursement/clubChairman"
|
||||
"/platform/jf/reimbursement/clubChairman/mobile?id=" + reimbursementId + "&mode=audit",
|
||||
"/platform/jf/reimbursement/clubChairman/mobile?id=" + reimbursementId + "&mode=view"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "协会负责人审核");
|
||||
@@ -214,8 +214,8 @@ public enum ReimbursementToHandler {
|
||||
List.of("1019911404"),
|
||||
"/platform/jf/reimbursement/schoolDirector",
|
||||
"/platform/jf/reimbursement/schoolDirector",
|
||||
"/platform/jf/reimbursement/schoolDirector",
|
||||
"/platform/jf/reimbursement/schoolDirector"
|
||||
"/platform/jf/reimbursement/schoolDirector/mobile?id=" + reimbursementId + "&mode=audit",
|
||||
"/platform/jf/reimbursement/schoolDirector/mobile?id=" + reimbursementId + "&mode=view"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "校工会负责人审核");
|
||||
@@ -257,8 +257,8 @@ public enum ReimbursementToHandler {
|
||||
schoolChairmanLeader,
|
||||
"/platform/jf/reimbursement/schoolChairman",
|
||||
"/platform/jf/reimbursement/schoolChairman",
|
||||
"/platform/jf/reimbursement/schoolChairman",
|
||||
"/platform/jf/reimbursement/schoolChairman"
|
||||
"/platform/jf/reimbursement/schoolChairman/mobile?id=" + reimbursementId + "&mode=audit",
|
||||
"/platform/jf/reimbursement/schoolChairman/mobile?id=" + reimbursementId + "&mode=view"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("activity_reimbursement@" + reimbursementId, "校工会主席审核");
|
||||
|
||||
@@ -2,11 +2,15 @@ package io.v.nutz.zhgh.jf.service;
|
||||
|
||||
import io.v.nutz.zhgh.jf.model.ActivityBx;
|
||||
import io.v.nutz.base.service.ViService;
|
||||
import io.v.nutz.base.model.Audit;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import cn.wizzer.framework.page.Pagination;
|
||||
import io.v.nutz.sys.models.Sys_club;
|
||||
|
||||
/**
|
||||
* @author zhf
|
||||
@@ -20,6 +24,39 @@ public interface ActivityBxService extends ViService<ActivityBx> {
|
||||
|
||||
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 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.Html2Text;
|
||||
import io.v.nutz.base.utils.Vi;
|
||||
import io.v.nutz.base.utils.MsgApi;
|
||||
import io.v.nutz.base.utils.Roles;
|
||||
import cn.wizzer.framework.page.Pagination;
|
||||
import io.v.nutz.sys.models.Sys_club;
|
||||
import io.v.nutz.sys.models.Sys_signature;
|
||||
import io.v.nutz.sys.models.Sys_user;
|
||||
import io.v.nutz.sys.models.Sys_user_role;
|
||||
import io.v.nutz.sys.models.User;
|
||||
import io.v.nutz.sys.services.SysSignatureService;
|
||||
import io.v.nutz.zhgh.jf.handle.funds.FundsToHandler;
|
||||
import io.v.nutz.zhgh.jf.handle.reimbursement.ReimbursementToHandler;
|
||||
import io.v.nutz.base.query.PageForm;
|
||||
import io.v.nutz.base.model.Goods;
|
||||
import io.v.nutz.zhgh.jf.model.ActivityBx;
|
||||
import io.v.nutz.zhgh.jf.model.ActivityFile;
|
||||
@@ -24,6 +36,7 @@ import io.v.nutz.web.commons.base.Globals;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.boot.starter.ftp.FtpService;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
@@ -43,6 +56,7 @@ import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -61,6 +75,492 @@ public class ActivityBxServiceImpl extends ViServiceImpl<ActivityBx> implements
|
||||
@Inject
|
||||
private FtpService ftpService;
|
||||
|
||||
@Inject
|
||||
private SysSignatureService sysSignatureService;
|
||||
@Inject
|
||||
private MsgApi msgApi;
|
||||
|
||||
/** 报销节点由各 Controller 固定,不能由客户端将申请送往其他审核环节。 */
|
||||
private int reimbursementState(String stage) {
|
||||
return switch (stage) {
|
||||
case "clubChairman" -> 1000;
|
||||
case "unionChairman" -> 1010;
|
||||
case "schoolDirector" -> 1020;
|
||||
case "schoolChairman" -> 1040;
|
||||
default -> throw new IllegalArgumentException("不支持的报销审核环节");
|
||||
};
|
||||
}
|
||||
|
||||
/** 沿用四个 PC 节点的数据范围,列表、详情和审核共用;未关联组织时拒绝访问。 */
|
||||
private Cnd reimbursementScope(String stage) {
|
||||
Cnd scope = Cnd.where("act.reimbursement_state_id", ">=", reimbursementState(stage));
|
||||
if (!ShiroUtil.hasAnyRoles("sysadmin,A06")) {
|
||||
if ("clubChairman".equals(stage)) {
|
||||
String clubId = vi.getClubId();
|
||||
if (Strings.isBlank(clubId)) throw new IllegalArgumentException("当前用户未关联负责的协会");
|
||||
scope.and("act.clubId", "=", clubId);
|
||||
} else if ("unionChairman".equals(stage)) {
|
||||
String unionId = Vi.getUnionId();
|
||||
if (Strings.isBlank(unionId)) throw new IllegalArgumentException("当前用户未关联基层工会");
|
||||
scope.and("u.unionid", "=", unionId);
|
||||
}
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
/** PC 与手机共用报销筛选口径,字段白名单避免把客户端输入作为 SQL 列名执行。 */
|
||||
@Override
|
||||
public Pagination reimbursementPage(String stage, PageForm pageForm, NutMap filters) {
|
||||
Cnd cnd = reimbursementScope(stage);
|
||||
int audit = filters.getInt("isAudit", 2);
|
||||
if (audit < 0 || audit > 2) throw new IllegalArgumentException("审核状态筛选无效");
|
||||
cnd.and("act.reimbursement_state_id", audit == 1 ? ">" : audit == 2 ? "=" : ">=", reimbursementState(stage));
|
||||
Vi.cndPlus(cnd, "act.year", "=", filters.get("year"));
|
||||
String activityName = filters.getString("activityName");
|
||||
if (Strings.isNotBlank(activityName)) cnd.and(Cnd.likeEX("act.activity_name", activityName.trim()));
|
||||
// 组织筛选仍遵循原节点:协会页不加单位条件,基层工会页不加协会条件。
|
||||
if (!"clubChairman".equals(stage)) {
|
||||
Vi.cndPlus(cnd, "u.unitId", "=", filters.getString("unitId"));
|
||||
if (!"unionChairman".equals(stage) || ShiroUtil.hasAnyRoles("sysadmin,A06")) {
|
||||
Vi.cndPlus(cnd, "u.unionid", "=", filters.getString("unionId"));
|
||||
}
|
||||
}
|
||||
if (!"unionChairman".equals(stage) && (!"clubChairman".equals(stage) || ShiroUtil.hasAnyRoles("sysadmin,A06"))) {
|
||||
Vi.cndPlus(cnd, "act.clubId", "=", filters.getString("clubId"));
|
||||
}
|
||||
Map<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
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void doAdd(ActivityBx activityBx) {
|
||||
|
||||
@@ -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
|
||||
})()
|
||||
@@ -1,192 +1,131 @@
|
||||
<template>
|
||||
<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">
|
||||
<button @click="clear" class="action-button-danger" type="button">清空</button>
|
||||
<button @click="undo" class="action-button-warning" type="button">撤销</button>
|
||||
<button @click="save" class="action-button-primary" type="button">确定</button>
|
||||
<button @click="clear" type="button" :disabled="busy">清空</button>
|
||||
<button @click="undo" type="button" :disabled="busy">撤销</button>
|
||||
<button @click="save" class="action-button-primary" type="button" :disabled="busy">{{ busy ? '保存中' : '确定' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 = {
|
||||
name: "sysSignature",
|
||||
props: {
|
||||
busy: {type: Boolean, default: false}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: null,
|
||||
initialized: false
|
||||
}
|
||||
return {initialized: false}
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
signature.clear()
|
||||
if (this.$options.signaturePad && !this.busy) this.$options.signaturePad.clear()
|
||||
},
|
||||
undo() {
|
||||
signature.undo()
|
||||
},
|
||||
rotate() {
|
||||
signature.getRotateCanvas(90)
|
||||
if (this.$options.signaturePad && !this.busy) this.$options.signaturePad.undo()
|
||||
},
|
||||
/** save 事件返回与竖屏画布方向一致的 PNG Base64,不旋转签字图片。 */
|
||||
save() {
|
||||
if (signature.isEmpty()) {
|
||||
alert("请签字后再保存")
|
||||
if (this.busy) return
|
||||
const signature = this.$options.signaturePad
|
||||
if (!signature || signature.isEmpty()) {
|
||||
this.$toast("请签字后再保存")
|
||||
return
|
||||
}
|
||||
// const png = signature.getPNG()
|
||||
//旋转一下 横过来
|
||||
const rotateCanvas = signature.getRotateCanvas(-90)
|
||||
this.$emit("save", rotateCanvas.toDataURL())
|
||||
this.$emit("save", signature.toDataURL())
|
||||
},
|
||||
|
||||
/** 使用弹框内实际可用尺寸;每个组件独立持有画布,高分屏按像素比绘制。 */
|
||||
initSignature() {
|
||||
if (this.initialized) return;
|
||||
|
||||
const canvas = document.getElementById("canvas");
|
||||
if (!canvas) return;
|
||||
|
||||
// 检查 canvas 是否已有有效尺寸
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
// 尺寸无效,稍后重试
|
||||
setTimeout(() => this.initSignature(), 50);
|
||||
return;
|
||||
if (this.initialized || this._isDestroyed) return
|
||||
const canvas = this.$refs.canvas
|
||||
const surface = this.$refs.surface
|
||||
if (!canvas || !surface) return
|
||||
const width = surface.clientWidth
|
||||
const height = surface.clientHeight
|
||||
// 隐藏或尚未完成布局时不创建零尺寸画布,销毁时会取消本次重试。
|
||||
if (width <= 0 || height <= 0) {
|
||||
this.$options.retryTimer = setTimeout(() => this.initSignature(), 50)
|
||||
return
|
||||
}
|
||||
|
||||
const padding = 30;
|
||||
const width = window.innerWidth - padding;
|
||||
const height = window.innerHeight - padding;
|
||||
|
||||
// 设置 CSS 尺寸
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
|
||||
// 高清绘制
|
||||
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;
|
||||
this.$options.signaturePad = new SmoothSignature(canvas, {
|
||||
width,
|
||||
height,
|
||||
scale: window.devicePixelRatio || 1,
|
||||
minWidth: 2,
|
||||
maxWidth: 5,
|
||||
color: "#222222"
|
||||
})
|
||||
this.initialized = true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 延迟初始化,确保 DOM 渲染完成
|
||||
this.$nextTick(() => {
|
||||
this.initSignature();
|
||||
});
|
||||
this.$nextTick(() => this.initSignature())
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
signature = null;
|
||||
this.initialized = false;
|
||||
// SmoothSignature 在 document 上绑定结束事件,反复打开时必须解除旧实例监听。
|
||||
clearTimeout(this.$options.retryTimer)
|
||||
if (this.$options.signaturePad) this.$options.signaturePad.removeListener()
|
||||
this.$options.signaturePad = null
|
||||
this.initialized = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.signature-wrap {
|
||||
position: fixed;
|
||||
left: 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);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
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 {
|
||||
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;
|
||||
border-radius: 10px;
|
||||
border: 2px dashed #ccc;
|
||||
height: 36px;
|
||||
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>
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
<template>
|
||||
<div class="h5-signature">
|
||||
<div v-if="!showSignaturePanel" class="main-panel">
|
||||
<div class="main-panel">
|
||||
<slot v-if="$slots.default"></slot>
|
||||
<template v-else>
|
||||
<van-button @click="openSignature" class="open-button" size="middle" plain native-type="button">打开签字板</van-button>
|
||||
<van-image :src="signatureContent">
|
||||
<van-button @click="openSignature" class="open-button" size="middle" plain native-type="button" :disabled="disabled">打开签字板</van-button>
|
||||
<van-image :src="signatureContent" fit="contain">
|
||||
<template v-slot:error>请点击下方打开签字板按钮进行签字</template>
|
||||
</van-image>
|
||||
</template>
|
||||
</div>
|
||||
<van-popup v-model="showSignaturePanel" :style="{ height: '100vh', width: '100vw', display: 'flex', 'align-items': 'center' }">
|
||||
<signature v-if="showSignaturePanel" @save="save"></signature>
|
||||
<van-popup :value="signaturePanelVisible" @input="onPanelInput" :close-on-popstate="false"
|
||||
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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
name: "h5",
|
||||
name: "mobileSignature",
|
||||
components: {
|
||||
signature: httpVueLoader("/components/sign/index.vue?v=" + new Date().getTime())
|
||||
},
|
||||
@@ -25,68 +32,116 @@ module.exports = {
|
||||
value: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
// 兼容已有 :my_sign.sync 页面;与 v-model 同时传入时优先使用 value。
|
||||
my_sign: {type: String, default: ""},
|
||||
// 不传 popupVisible 时保持组件自行开关;传入布尔值时由页面统一历史栈管理。
|
||||
popupVisible: {type: Boolean, default: null},
|
||||
// 默认仍保存个人签名;业务只需本次签字时传 false,通过 input 回传 Base64。
|
||||
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
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
boundSignature: {
|
||||
handler(val) {
|
||||
this.signatureContent = val
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
signaturePanelVisible(visible) {
|
||||
// 每次打开重新创建画布;等待弹出动画完成后再按真实可用区域计算尺寸。
|
||||
if (!visible) this.boardReady = false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
signatureContent: null,
|
||||
showSignaturePanel: false
|
||||
showSignaturePanel: false,
|
||||
boardReady: false,
|
||||
saving: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openSignature() {
|
||||
this.showSignaturePanel = true
|
||||
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 为签名图片字符串,同时通知两种已有绑定方式,父表单提交字段保持不变。 */
|
||||
updateSignature(data) {
|
||||
this.signatureContent = data
|
||||
this.$emit("input", data)
|
||||
this.$emit("update:my_sign", data)
|
||||
},
|
||||
/** signature 为公共签字板生成的 Base64;本次签字模式不调用个人签名保存接口。 */
|
||||
save(signature) {
|
||||
$.post("/mobile/common/signing/update", {sign: signature})
|
||||
if (this.saving || this.disabled) return
|
||||
if (!this.saveToProfile) {
|
||||
this.updateSignature(signature)
|
||||
this.changePanelVisible(false)
|
||||
return
|
||||
}
|
||||
this.saving = true
|
||||
return $.post("/mobile/common/signing/update", {sign: signature})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$toast.success("保存成功")
|
||||
this.showSignaturePanel = false
|
||||
this.getMySign()
|
||||
this.changePanelVisible(false)
|
||||
return this.getMySign()
|
||||
} else {
|
||||
this.$toast.fail("保存失败")
|
||||
}
|
||||
})
|
||||
.fail(() => this.$toast.fail("签字保存失败,请重试"))
|
||||
.always(() => { this.saving = 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 })
|
||||
},
|
||||
async getMySign() {
|
||||
const data = await $.get("/mobile/common/signing/getMySign")
|
||||
if (data) {
|
||||
this.signatureContent = data
|
||||
this.$emit("input", data)
|
||||
}
|
||||
/** 沿用个人签名查询接口,返回图片数据并同步给已有 v-model 调用方。 */
|
||||
getMySign() {
|
||||
return $.get("/mobile/common/signing/getMySign").then((data) => {
|
||||
if (data) {
|
||||
this.updateSignature(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
//await this.getMySign()
|
||||
created() {
|
||||
// 受控审核页已经维护 sign 层;普通页面由公共组件接入同一个全局历史管理器。
|
||||
this.$options.popupLayer = window.mobilePopupHistory.createLayer("signature", (visible) => {
|
||||
this.showSignaturePanel = visible
|
||||
})
|
||||
// 旧表单通过 PJAX 替换 DOM 时不会自动销毁 Vue,主动释放签字实例和公共栈回调。
|
||||
$(document).one("pjax:beforeReplace.mobileSignature" + this._uid, () => this.$destroy())
|
||||
},
|
||||
beforeDestroy() {
|
||||
$(document).off("pjax:beforeReplace.mobileSignature" + this._uid)
|
||||
if (this.$options.popupLayer) this.$options.popupLayer.dispose()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -95,14 +150,51 @@ module.exports = {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
.h5-signature-sheet.van-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 35vh;
|
||||
min-height: 260px;
|
||||
max-height: 90vh;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.h5-signature-heading {
|
||||
flex: 0 0 44px;
|
||||
display: grid;
|
||||
grid-template-columns: 64px 1fr 64px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
color: #323233;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.h5-signature-heading button {
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #1989fa;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.h5-signature-heading button:disabled {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.h5-signature .main-panel {
|
||||
position: relative;
|
||||
flex-direction: column-reverse;
|
||||
display: flex;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.main-panel .van-image {
|
||||
.h5-signature .van-image {
|
||||
min-height: 150px;
|
||||
display: block;
|
||||
width: 90%;
|
||||
@@ -111,7 +203,7 @@ module.exports = {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.main-panel .open-button {
|
||||
.h5-signature .open-button {
|
||||
margin: 0 auto;
|
||||
width: 90%;
|
||||
height: 36px;
|
||||
|
||||
@@ -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,135 @@
|
||||
<!--#
|
||||
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-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('info.js'){}#-->
|
||||
<!--#include('page.js'){}#-->
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,104 @@
|
||||
/** 协会活动申报详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
||||
const associationInfo = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="association-info">
|
||||
<section class="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 title="联系方式" :value="data.mobile || '—'"></van-cell>
|
||||
<van-cell title="申请时间" :value="data.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.address || '—'"></van-cell>
|
||||
<van-cell title="活动人数" :value="data.activity_number || '—'"></van-cell>
|
||||
</section>
|
||||
<section class="association-section">
|
||||
<h3>活动方案及简介</h3>
|
||||
<div class="association-description">{{ activityText || '暂无' }}</div>
|
||||
<img v-for="(url, index) in activityImages" :key="index" :src="url" class="association-content-image"
|
||||
alt="活动方案配图" @click="$emit('preview', {imageUrl: url})">
|
||||
</section>
|
||||
<section class="association-section">
|
||||
<h3>预算明细</h3>
|
||||
<van-cell v-for="(item, index) in budgets" :key="index" :title="item.name || '费用项目'"
|
||||
:value="'¥' + money(item.budgetPrice)"></van-cell>
|
||||
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="association-total"></van-cell>
|
||||
</section>
|
||||
<section class="association-section">
|
||||
<h3>申请附件</h3>
|
||||
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="association-file"
|
||||
value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!files.length" class="association-muted">暂无附件</p>
|
||||
<h3>申请人签字</h3>
|
||||
<img v-if="data.us_s" class="association-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
|
||||
<p v-else class="association-muted">暂无签字</p>
|
||||
</section>
|
||||
<section v-for="audit in audits" :key="audit.key" class="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="association-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
|
||||
</section>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
props: {
|
||||
data: {type: Object, default: () => ({})}
|
||||
},
|
||||
computed: {
|
||||
budgets() {
|
||||
return this.asList(this.data.budgets)
|
||||
},
|
||||
files() {
|
||||
return this.asList(this.data.files)
|
||||
},
|
||||
activityText() {
|
||||
// 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n')
|
||||
container.querySelectorAll('script,style').forEach((node) => node.remove())
|
||||
return container.textContent.trim()
|
||||
},
|
||||
activityImages() {
|
||||
// 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。
|
||||
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
||||
return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '')
|
||||
.filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url))
|
||||
},
|
||||
audits() {
|
||||
const data = this.data
|
||||
return [
|
||||
{key: 'association', title: '协会负责人审核', id: data.association_principal_id,
|
||||
name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s},
|
||||
{key: 'accounting', title: '校工会负责人审核', id: data.accounting_name,
|
||||
name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s},
|
||||
{key: 'union', title: '校工会主席审核', id: data.union_chairman_name,
|
||||
name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s}
|
||||
].filter((audit) => audit.id)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||
asList(value) {
|
||||
if (Array.isArray(value)) return value
|
||||
if (!value) return []
|
||||
try {
|
||||
const list = JSON.parse(value)
|
||||
return Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
/** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */
|
||||
signUrl(value) {
|
||||
return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// 独立手机审核页面;不复用旧 declare 页面及其有缺陷的滚动分页。
|
||||
const vue = new Vue({
|
||||
el: '#app',
|
||||
components: {'association-info': associationInfo},
|
||||
data() {
|
||||
return {
|
||||
baseUrl: '/platform/jf/activity_bx/association',
|
||||
pageForm: {year: new Date().getFullYear(), clubId: '', isAudit: 3, pageNumber: 1, pageSize: 10},
|
||||
tableData: [], tableLoading: false, refreshing: false, finished: false, listError: false,
|
||||
listSerial: 0, listBusy: false, detailSerial: 0,
|
||||
clubs: [{text: '全部协会', value: ''}], clubsLoading: false,
|
||||
auditOptions: [{text: '未审核', value: 3}, {text: '已审核', value: 2}, {text: '全部', value: 1}],
|
||||
filterOpen: {year: false, clubId: false, isAudit: false}, filterClosing: false,
|
||||
popups: {filter: false, detail: false, sign: false, confirm: false, preview: false},
|
||||
detail: {}, detailId: '', detailLoading: false, auditMode: false,
|
||||
formData: {id: '', idea: '', sign: ''}, formLoading: false, reviewFlag: true,
|
||||
reviewerName: '', reviewDate: '', previewUrl: '', previewIsImage: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
yearOptions() {
|
||||
return Array.from({length: 11}, (_, index) => {
|
||||
const year = new Date().getFullYear() - index
|
||||
return {text: year + '年', value: year}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
shortDate(value) {
|
||||
return value ? String(value).slice(5, 10) : '—'
|
||||
},
|
||||
setRefreshing(value) { this.refreshing = value },
|
||||
setTableLoading(value) { this.tableLoading = value },
|
||||
setListError(value) { this.listError = value },
|
||||
setIdea(value) { this.$set(this.formData, 'idea', value) },
|
||||
/** 筛选切换或提交后,从第一页替换数据;旧请求晚到时不能混入当前筛选结果。 */
|
||||
doSearch() {
|
||||
this.listSerial++
|
||||
this.listBusy = false
|
||||
this.$set(this.pageForm, 'pageNumber', 1)
|
||||
this.tableData = []
|
||||
this.finished = false
|
||||
this.listError = false
|
||||
this.pageData()
|
||||
},
|
||||
/** 列表返回 data.list 和 totalCount;失败保留页码供重试,成功后才推进下一页。 */
|
||||
pageData() {
|
||||
if (this.listBusy || this.finished) return
|
||||
const serial = this.listSerial
|
||||
this.listBusy = true
|
||||
this.tableLoading = true
|
||||
this.listError = false
|
||||
$.post(this.baseUrl + '/pageData', Object.assign({}, this.pageForm))
|
||||
.then((res) => {
|
||||
if (serial !== this.listSerial || this._isDestroyed) return
|
||||
if (res.code !== 0 || !res.data) {
|
||||
this.listError = true
|
||||
this.$toast(res.msg || '列表加载失败')
|
||||
return
|
||||
}
|
||||
const list = res.data.list || []
|
||||
this.tableData = this.tableData.concat(list)
|
||||
this.finished = !list.length || this.tableData.length >= Number(res.data.totalCount)
|
||||
this.$set(this.pageForm, 'pageNumber', this.pageForm.pageNumber + 1)
|
||||
})
|
||||
.fail(() => {
|
||||
if (serial === this.listSerial && !this._isDestroyed) this.listError = true
|
||||
})
|
||||
.always(() => {
|
||||
if (serial !== this.listSerial || this._isDestroyed) return
|
||||
this.listBusy = false
|
||||
this.tableLoading = false
|
||||
this.refreshing = false
|
||||
})
|
||||
},
|
||||
loadClubs() {
|
||||
this.clubsLoading = true
|
||||
$.post(this.baseUrl + '/clubs')
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.clubs = [{text: '全部协会', value: ''}].concat((res.data || []).map((club) => ({text: club.name, value: club.id})))
|
||||
} else {
|
||||
this.$toast(res.msg || '协会加载失败')
|
||||
}
|
||||
})
|
||||
.fail(() => this.$toast('协会加载失败,请重新打开筛选'))
|
||||
.always(() => { this.clubsLoading = false })
|
||||
},
|
||||
/** key 为年度/协会/状态字段,open 为菜单展开状态;三个菜单切换时共用一层历史。 */
|
||||
setFilterOpen(key, open) {
|
||||
this.$set(this.filterOpen, key, open)
|
||||
if (open && key === 'clubId' && this.clubs.length === 1 && !this.clubsLoading) this.loadClubs()
|
||||
// Vant 切换菜单会同时触发旧菜单 close 和新菜单 open,等本轮更新结束后统一判断。
|
||||
this.$nextTick(() => {
|
||||
if (this._isDestroyed || this.filterClosing) return
|
||||
if (Object.values(this.filterOpen).some(Boolean)) {
|
||||
if (!this.popups.filter) this.$options.popupSession.open('filter')
|
||||
} else if (this.popups.filter) {
|
||||
this.filterClosing = true
|
||||
this.$options.popupSession.close().then(() => { this.filterClosing = false })
|
||||
}
|
||||
})
|
||||
},
|
||||
/** key 为筛选字段,value 为对应选项值;仅值改变时刷新,菜单收起事件负责回退历史。 */
|
||||
selectFilter(key, value) {
|
||||
this.$set(this.pageForm, key, value)
|
||||
this.doSearch()
|
||||
},
|
||||
/** 每次打开申请清空上次的意见和签字,防止在不同申请之间串用审核数据。 */
|
||||
openDetail(id, audit) {
|
||||
if (this.formLoading) return
|
||||
this.detailId = id
|
||||
this.detail = {}
|
||||
this.auditMode = audit
|
||||
this.formData = {id, idea: '', sign: ''}
|
||||
this.reviewDate = moment().format('YYYY-MM-DD')
|
||||
this.$options.popupSession.open('detail')
|
||||
this.loadDetail(id)
|
||||
},
|
||||
loadDetail(id) {
|
||||
const serial = ++this.detailSerial
|
||||
this.detailLoading = true
|
||||
$.post(this.baseUrl + '/findOne', {id})
|
||||
.then((res) => {
|
||||
if (serial !== this.detailSerial || !this.popups.detail || this._isDestroyed) return
|
||||
if (res.code === 0 && res.data) {
|
||||
this.detail = res.data
|
||||
if (!this.detail.canReview) this.auditMode = false
|
||||
} else {
|
||||
this.$toast(res.msg || '详情加载失败')
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
if (serial === this.detailSerial && !this._isDestroyed) this.$toast('详情加载失败,请重试')
|
||||
})
|
||||
.always(() => {
|
||||
if (serial === this.detailSerial && !this._isDestroyed) this.detailLoading = false
|
||||
})
|
||||
},
|
||||
startAudit() {
|
||||
this.auditMode = true
|
||||
this.$nextTick(() => {
|
||||
const body = this.$el.querySelector('.association-detail-body')
|
||||
if (body) body.scrollTop = body.scrollHeight
|
||||
})
|
||||
},
|
||||
openSignature() {
|
||||
if (!this.formLoading) this.$options.popupSession.open('sign')
|
||||
},
|
||||
/** signature 是 mobile-sign 回传的本次签字 Base64,随审核请求一起保存。 */
|
||||
setSignature(signature) {
|
||||
this.$set(this.formData, 'sign', signature)
|
||||
},
|
||||
confirmReview(flag) {
|
||||
if (this.formLoading || !this.detail.canReview) return
|
||||
if (!this.formData.idea.trim() || !this.formData.sign) {
|
||||
this.$toast('请填写审核意见并签字')
|
||||
return
|
||||
}
|
||||
this.reviewFlag = flag
|
||||
this.$options.popupSession.open('confirm')
|
||||
},
|
||||
/** Vant 用 done 结束按钮 loading;显隐仍由历史栈驱动,避免组件自行关闭后留下历史层。 */
|
||||
beforeReviewClose(action, done) {
|
||||
if (this.formLoading) {
|
||||
done(false)
|
||||
return
|
||||
}
|
||||
if (action === 'confirm') {
|
||||
this.submitReview().always(() => done(false))
|
||||
} else {
|
||||
this.$options.popupSession.close().then(() => done(false))
|
||||
}
|
||||
},
|
||||
/** id/idea/sign/flag 与 PC 共用;审核时间和人员由服务器取值,不信任前端身份字段。 */
|
||||
submitReview() {
|
||||
if (this.formLoading) return
|
||||
this.formLoading = true
|
||||
return $.post(this.baseUrl + '/doReview', Object.assign({}, this.formData, {flag: this.reviewFlag}))
|
||||
.then((res) => {
|
||||
if (this._isDestroyed) return
|
||||
if (res.code === 0) {
|
||||
this.$toast.success('审核成功')
|
||||
this.$options.popupSession.clear().then(() => {
|
||||
this.formData = {id: '', idea: '', sign: ''}
|
||||
this.doSearch()
|
||||
})
|
||||
} else {
|
||||
this.$toast(res.msg || '审核失败')
|
||||
// 关闭确认层后重读状态;若已被 PC 处理则切换为只读,保留未提交的意见便于核对。
|
||||
if (this.popups.confirm) this.$options.popupSession.close().then(() => this.loadDetail(this.detailId))
|
||||
}
|
||||
})
|
||||
.fail(() => this.$toast('提交失败,请检查网络后重试'))
|
||||
.always(() => { this.formLoading = false })
|
||||
},
|
||||
/** 图片使用文件流,文档沿用项目预览接口;预览自身同样占一层历史。 */
|
||||
previewFile(file) {
|
||||
if (file.imageUrl) {
|
||||
this.previewIsImage = true
|
||||
this.previewUrl = file.imageUrl
|
||||
this.$options.popupSession.open('preview')
|
||||
return
|
||||
}
|
||||
if (!file.id) {
|
||||
this.$toast('附件信息不完整')
|
||||
return
|
||||
}
|
||||
this.previewIsImage = /\.(png|jpe?g|gif|webp|bmp)$/i.test(file.filename || file.filepath || '')
|
||||
const streamUrl = '/file_server/fileStreamPreview?id=' + encodeURIComponent(file.id)
|
||||
this.previewUrl = this.previewIsImage
|
||||
? streamUrl
|
||||
: '/assets/platform/plugins/pdfJs/web/viewer.html?file=' + encodeURIComponent(streamUrl)
|
||||
this.$options.popupSession.open('preview')
|
||||
},
|
||||
closePopup() {
|
||||
if (this.formLoading) {
|
||||
this.$toast('正在提交,请稍候')
|
||||
return
|
||||
}
|
||||
this.$options.popupSession.close()
|
||||
},
|
||||
back() {
|
||||
if (Object.values(this.popups).some(Boolean)) {
|
||||
this.closePopup()
|
||||
return
|
||||
}
|
||||
this.$options.popupSession.clear().then(() => {
|
||||
this.$destroy()
|
||||
pjaxReplace('/mobile/index')
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 仅注册状态同步回调;popstate 由公共管理器统一监听。
|
||||
this.$options.popupSession = window.mobilePopupHistory.register('associationAudit', (layers) => {
|
||||
Object.keys(this.popups).forEach((name) => this.$set(this.popups, name, layers.includes(name)))
|
||||
// 系统返回或页面返回先收起下拉菜单;由公共历史栈驱动,页面不另绑 popstate。
|
||||
if (!layers.includes('filter')) {
|
||||
Object.keys(this.filterOpen).forEach((key) => {
|
||||
this.$set(this.filterOpen, key, false)
|
||||
const dropdown = this.$refs[key + 'Filter']
|
||||
if (dropdown) dropdown.toggle(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
const user = JSON.parse(window.sessionStorage.getItem('user') || '{}')
|
||||
this.reviewerName = user.username || ''
|
||||
this.loadClubs()
|
||||
this.doSearch()
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
const id = query.get('id')
|
||||
if (id) this.openDetail(id, query.get('mode') === 'audit')
|
||||
// PJAX 不会自动销毁旧 Vue 实例,在替换容器前注销页面回调。
|
||||
$(document).on('pjax:beforeReplace.associationAudit', () => this.$destroy())
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.listSerial++
|
||||
this.detailSerial++
|
||||
const session = this.$options.popupSession
|
||||
if (Object.values(this.popups).some(Boolean)) {
|
||||
session.clear().then(() => session.dispose())
|
||||
} else {
|
||||
// PJAX 替换后可能立即创建新页面;空栈必须同步注销,不能等 Promise 回调再释放注册。
|
||||
session.dispose()
|
||||
}
|
||||
$(document).off('pjax:beforeReplace.associationAudit')
|
||||
}
|
||||
})
|
||||
@@ -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,146 @@
|
||||
/** 活动申报审核详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
||||
const activityReviewInfo = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="review-info">
|
||||
<section class="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.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="review-section">
|
||||
<h3>活动方案及简介</h3>
|
||||
<div class="review-description">{{ activityText || '暂无' }}</div>
|
||||
<img v-for="(url, index) in activityImages" :key="index" :src="url" class="review-content-image"
|
||||
alt="活动方案配图" @click="$emit('preview', {imageUrl: url})">
|
||||
</section>
|
||||
<section class="review-section">
|
||||
<h3>预算明细</h3>
|
||||
<div v-for="(item, index) in budgets" :key="index" class="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.funds_money)" class="review-total"></van-cell>
|
||||
</section>
|
||||
<section class="review-section">
|
||||
<h3>申请附件</h3>
|
||||
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="review-file"
|
||||
value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!files.length" class="review-muted">暂无附件</p>
|
||||
<h3>申请人签字</h3>
|
||||
<img v-if="data.us_s" class="review-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
|
||||
<p v-else class="review-muted">暂无签字</p>
|
||||
</section>
|
||||
<section v-for="group in reimbursementFiles" :key="group.key" class="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="review-file" value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||
<p v-if="!group.files.length" class="review-muted">暂无附件</p>
|
||||
</section>
|
||||
<section v-for="audit in audits" :key="audit.key" class="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="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)
|
||||
},
|
||||
activityText() {
|
||||
// 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n')
|
||||
container.querySelectorAll('script,style').forEach((node) => node.remove())
|
||||
return container.textContent.trim()
|
||||
},
|
||||
activityImages() {
|
||||
// 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。
|
||||
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
||||
return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '')
|
||||
.filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url))
|
||||
},
|
||||
audits() {
|
||||
const data = this.data
|
||||
const records = [
|
||||
{key: 'division', title: '基层工会主席审核', id: data.division_chairman_name, name: data.dc_username, time: data.division_chairman_time, idea: data.division_chairman_idea, sign: data.dc_s},
|
||||
{key: 'association', title: '协会负责人审核', id: data.association_principal_id,
|
||||
name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s},
|
||||
{key: 'accounting', title: '校工会负责人审核', id: data.accounting_name,
|
||||
name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s},
|
||||
{key: 'confirm', title: '校工会副主席审核', id: data.confirm_trade_name, name: data.ct_username, time: data.confirm_trade_time, idea: data.confirm_trade_idea, sign: data.ct_s},
|
||||
{key: 'cashier', title: '校工会出纳审核', id: data.union_cashier_name, name: data.uc_username, time: data.union_cashier_time, idea: data.union_cashier_idea, sign: data.uc_s},
|
||||
{key: 'union', title: '校工会主席审核', id: data.union_chairman_name,
|
||||
name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s}
|
||||
].filter((audit) => audit.id)
|
||||
// PC 详情同时展示已有报销审批记录;只读呈现,不把报销流程混入当前申请审核。
|
||||
const reimbursementAudits = [
|
||||
{key: 'reimbursement_club_audit', title: '协会负责人审核(报销)'},
|
||||
{key: 'reimbursement_union_audit', title: '基层工会主席审核(报销)'},
|
||||
{key: 'reimbursement_school_director', title: '校工会负责人审核(报销)'},
|
||||
{key: 'reimbursement_school_chairman', title: '校工会主席审核(报销)'}
|
||||
].filter((item) => data[item.key]).map((item) => {
|
||||
const audit = data[item.key]
|
||||
return {key: item.key, title: item.title, name: audit.username, time: audit.auditTime, idea: audit.auditOpinion, sign: audit.auditSign}
|
||||
})
|
||||
return records.concat(reimbursementAudits)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||
asList(value) {
|
||||
if (Array.isArray(value)) return value
|
||||
if (!value) return []
|
||||
try {
|
||||
const list = JSON.parse(value)
|
||||
return Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
/** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */
|
||||
signUrl(value) {
|
||||
return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,147 @@
|
||||
/** 活动报销审核详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
||||
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">{{ activityText || '暂无' }}</div>
|
||||
<img v-for="(url, index) in activityImages" :key="index" :src="url" class="reimbursement-review-content-image"
|
||||
alt="活动方案配图" @click="$emit('preview', {imageUrl: url})">
|
||||
</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)
|
||||
},
|
||||
activityText() {
|
||||
// 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n')
|
||||
container.querySelectorAll('script,style').forEach((node) => node.remove())
|
||||
return container.textContent.trim()
|
||||
},
|
||||
activityImages() {
|
||||
// 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。
|
||||
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
||||
return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '')
|
||||
.filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url))
|
||||
},
|
||||
audits() {
|
||||
const data = this.data
|
||||
const records = [
|
||||
{key: 'division', title: '基层工会主席审核(申报)', id: data.division_chairman_name, name: data.dc_username, time: data.division_chairman_time, idea: data.division_chairman_idea, sign: data.dc_s},
|
||||
{key: 'association', title: '协会负责人审核(申报)', id: data.association_principal_id,
|
||||
name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s},
|
||||
{key: 'accounting', title: '校工会负责人审核(申报)', id: data.accounting_name,
|
||||
name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s},
|
||||
{key: 'confirm', title: '校工会副主席审核(申报)', id: data.confirm_trade_name, name: data.ct_username, time: data.confirm_trade_time, idea: data.confirm_trade_idea, sign: data.ct_s},
|
||||
{key: 'cashier', title: '校工会出纳审核(申报)', id: data.union_cashier_name, name: data.uc_username, time: data.union_cashier_time, idea: data.union_cashier_idea, sign: data.uc_s},
|
||||
{key: 'union', title: '校工会主席审核(申报)', id: data.union_chairman_name,
|
||||
name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s}
|
||||
].filter((audit) => audit.id)
|
||||
// 按 PC 详情展示报销审核记录,并与前期申报记录明确区分。
|
||||
const reimbursementAudits = [
|
||||
{key: 'reimbursement_club_audit', title: '协会负责人审核(报销)'},
|
||||
{key: 'reimbursement_union_audit', title: '基层工会主席审核(报销)'},
|
||||
{key: 'reimbursement_school_director', title: '校工会负责人审核(报销)'},
|
||||
{key: 'reimbursement_school_chairman', title: '校工会主席审核(报销)'}
|
||||
].filter((item) => data[item.key]).map((item) => {
|
||||
const audit = data[item.key]
|
||||
return {key: item.key, title: item.title, name: audit.username, time: audit.auditTime, idea: audit.auditOpinion, sign: audit.auditSign}
|
||||
})
|
||||
return records.concat(reimbursementAudits)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||
asList(value) {
|
||||
if (Array.isArray(value)) return value
|
||||
if (!value) return []
|
||||
try {
|
||||
const list = JSON.parse(value)
|
||||
return Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
money(value) {
|
||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
||||
},
|
||||
/** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */
|
||||
signUrl(value) {
|
||||
return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/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/flashcanvas.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({
|
||||
url: url,
|
||||
container: "#container",
|
||||
|
||||
@@ -1,53 +1,49 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<title>签字</title>
|
||||
<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" />
|
||||
|
||||
<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/vant/vant.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/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>
|
||||
<body>
|
||||
<div id="app">
|
||||
<signature @save="save"></signature>
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar title="签字"></van-nav-bar>
|
||||
<van-notice-bar v-if="subDis" text="缺少签字参数,请重新扫码。"></van-notice-bar>
|
||||
<mobile-sign :value="sign" :save-to-profile="false" :popup-visible="showSignaturePanel"
|
||||
:disabled="subDis || formLoading" @input="save" @open="openSignature" @close="closeSignature"></mobile-sign>
|
||||
</div>
|
||||
</body>
|
||||
<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({
|
||||
el: "#app",
|
||||
components: {
|
||||
signature: httpVueLoader("/components/sign/index.vue?v=" + new Date().getTime())
|
||||
'mobile-sign': httpVueLoader("/components/sign/mobileSign.vue?v=" + new Date().getTime())
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
prefix: '',
|
||||
id: '',
|
||||
isNull: function (val) {
|
||||
if (val === null || val === undefined || val === '') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
return {prefix: '', id: '', sign: '', subDis: false, formLoading: false, showSignaturePanel: false}
|
||||
},
|
||||
methods: {
|
||||
openSignature() {
|
||||
if (!this.subDis && !this.formLoading) this.$options.popupSession.open('sign')
|
||||
},
|
||||
closeSignature() {
|
||||
if (!this.formLoading) this.$options.popupSession.close()
|
||||
},
|
||||
/**
|
||||
* signature 为公共签字板回传的 PNG Base64;prefix/id 来自二维码,关联 PC 端轮询。
|
||||
* 保留 is_value_base64=false 的文件保存方式;接口返回 {code, msg},code=0 表示成功。
|
||||
*/
|
||||
save(signature) {
|
||||
if (this.formLoading || this.subDis) return
|
||||
this.sign = signature
|
||||
this.formLoading = true
|
||||
$.post("/signature/doSub", {
|
||||
prefix: this.prefix,
|
||||
id: this.id,
|
||||
@@ -56,44 +52,31 @@
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
alert("保存成功")
|
||||
this.showSignaturePanel = false
|
||||
this.$toast.success("保存成功")
|
||||
this.$options.popupSession.clear()
|
||||
} else {
|
||||
alert("保存失败")
|
||||
this.$toast.fail(res.msg || "保存失败,请重试")
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
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 })
|
||||
.fail(() => this.$toast.fail("保存失败,请重试"))
|
||||
.always(() => { this.formLoading = false })
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.isNull(getQueryVariable("id")) || this.isNull(getQueryVariable("prefix"))) {
|
||||
this.$notify({
|
||||
message: '缺少参数!',
|
||||
type: 'danger'
|
||||
});
|
||||
this.subDis = true
|
||||
}
|
||||
this.id = getQueryVariable("id")
|
||||
this.prefix = getQueryVariable("prefix")
|
||||
created() {
|
||||
// 扫码入口也是独立页面,使用公共历史栈统一处理返回、取消及保存成功后的清理。
|
||||
this.$options.popupSession = window.mobilePopupHistory.register('qrSignature', (layers) => {
|
||||
this.showSignaturePanel = layers.includes('sign')
|
||||
})
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
this.id = query.get('id') || ''
|
||||
this.prefix = query.get('prefix') || ''
|
||||
this.subDis = !this.id || !this.prefix
|
||||
},
|
||||
beforeDestroy() {
|
||||
const session = this.$options.popupSession
|
||||
session.clear().then(() => session.dispose())
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user