This commit is contained in:
=
2026-05-22 11:52:46 +08:00
parent 8c467328f1
commit 3d67c3a347
23 changed files with 1040 additions and 113 deletions
@@ -401,7 +401,8 @@ public class SysRoleController {
public Object user(@Param("roleId") String roleId, @Param("searchName") String searchName, @Param("searchKeyword") String searchKeyword,
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize, @Param("pageOrderName") String pageOrderName, @Param("pageOrderBy") String pageOrderBy) {
try {
Sql sql = Sqls.create("SELECT a.*,c.name as unitname FROM sys_user a,sys_user_role b,sys_unit c WHERE a.unitid=c.id and a.id=b.userId and b.roleId=@roleId $s $o");
// sys_user_role 会按单位、工会等业务范围保存多条记录,这里只按用户维度展示一次。
Sql sql = Sqls.create("SELECT a.*,c.name as unitname FROM sys_user a,sys_unit c WHERE a.unitid=c.id and EXISTS (SELECT 1 FROM sys_user_role b WHERE b.userId=a.id and b.roleId=@roleId) $s $o");
sql.params().set("roleId", roleId);
if (Strings.isNotBlank(searchName) && Strings.isNotBlank(searchKeyword)) {
sql.vars().set("s", " and a." + searchName + " like '%" + searchKeyword + "%'");
@@ -0,0 +1,39 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSatisfactionStatisticsParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
@IocBean
@At("/platform/proposal/query/satisfactionStatistics")
@Ok("json:full")
@Api(tags = "提案满意度统计")
public class ProposalSatisfactionStatisticsController {
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/satisfactionStatistics/index.html")
@SaCheckPermission("proposal.query.satisfactionStatistics")
public void index() {
}
@At
@SaCheckPermission("proposal.query.satisfactionStatistics")
@ApiOperation("提案满意度统计")
public Result data(@Valid @Param("pageForm") ProposalSatisfactionStatisticsParam pageForm) {
return Result.success(proposalCommonService.satisfactionStatistics(pageForm));
}
}
@@ -0,0 +1,17 @@
package com.budwk.app.zhgh.democratic.proposal.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "提案满意度统计参数")
public class ProposalSatisfactionStatisticsParam {
@ApiModelProperty(name = "教代会ID")
private String sessionId;
@ApiModelProperty(name = "提案类别ID")
private String[] typeIds;
}
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.democratic.proposal.service.common;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSatisfactionStatisticsParam;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
@@ -113,4 +114,12 @@ public interface ProposalCommonService extends BaseService<ProposalInfo> {
*/
List<String> mergeProposal(String id);
/**
* 按提案类别统计立案同意提案的反馈完成率和满意率。
*
* @param param 统计筛选参数,包含届次和提案类别
* @return 每个提案类别的统计结果
*/
List<NutMap> satisfactionStatistics(ProposalSatisfactionStatisticsParam param);
}
@@ -29,6 +29,7 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.zhgh.democratic.proposal.models.*;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSatisfactionStatisticsParam;
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_session;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
@@ -513,6 +514,9 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
HashMap<String, Object> docData = new HashMap<>();
Teacher_congress_session session = dao().fetch(Teacher_congress_session.class, sessionId);
if (ObjectUtil.isEmpty(session)) {
throw new BaseException("请选择教代会后再导出年度报告");
}
docData.put("sessionName", session.getFullName());
docData.put("startDate", DateUtil.format(session.getStartDate(), "yyyy年MM月dd日"));
@@ -623,7 +627,8 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
for (int i = 0; i < transactList.size(); i++) {
NutMap v = transactList.get(i);
v.setv("index", i + 1);
v.setv("ratio", NumberUtil.decimalFormat("#.##%", v.getInt("sum") / sum));
// 当前届次没有提案时避免除以 0;有数据时使用小数计算,保证比例准确。
v.setv("ratio", sum == 0 ? "0%" : NumberUtil.decimalFormat("#.##%", v.getInt("sum") * 1.0 / sum));
}
docData.put("transacts", transactList);
@@ -649,9 +654,9 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
type.id
ORDER BY
type.CODE
""");
""");
newSessionSql.setParam("newSessionId", nextSession.getId());
List<NutMap> newSessionProposals = listMap(sql);
List<NutMap> newSessionProposals = listMap(newSessionSql);
docData.put("newSum", newSessionProposals.stream().mapToInt(v -> v.getInt("count", 0)).sum());
docData.put("newTypesSummary", newSessionProposals.stream()
@@ -683,4 +688,75 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
}
return null;
}
@Override
public List<NutMap> satisfactionStatistics(ProposalSatisfactionStatisticsParam param) {
Sql sql = Sqls.create("""
SELECT
type.id AS typeId,
type.code AS typeCode,
type.name AS typeName,
COUNT(DISTINCT info.id) AS filingCount,
COUNT(DISTINCT CASE WHEN latestFeedback.processInstanceId IS NOT NULL THEN info.id END) AS feedbackCount,
COUNT(DISTINCT CASE WHEN latestFeedback.processInstanceId IS NULL AND info.id IS NOT NULL THEN info.id END) AS unFeedbackCount,
COUNT(DISTINCT CASE WHEN latestFeedback.tf_feedback IS NOT NULL AND latestFeedback.tf_feedback != 'DISSATISFIED' THEN info.id END) AS satisfiedCount,
COUNT(DISTINCT CASE WHEN latestFeedback.tf_feedback = 'DISSATISFIED' THEN info.id END) AS dissatisfiedCount,
ROUND(
CASE
WHEN COUNT(DISTINCT info.id) = 0 THEN 0
ELSE COUNT(DISTINCT CASE WHEN latestFeedback.processInstanceId IS NOT NULL THEN info.id END) * 100.0 / COUNT(DISTINCT info.id)
END,
2
) AS completionRate,
ROUND(
CASE
WHEN COUNT(DISTINCT CASE WHEN latestFeedback.processInstanceId IS NOT NULL THEN info.id END) = 0 THEN 0
ELSE COUNT(DISTINCT CASE WHEN latestFeedback.tf_feedback IS NOT NULL AND latestFeedback.tf_feedback != 'DISSATISFIED' THEN info.id END) * 100.0 / COUNT(DISTINCT CASE WHEN latestFeedback.processInstanceId IS NOT NULL THEN info.id END)
END,
2
) AS satisfactionRate
FROM
proposal_type type
LEFT JOIN proposal_info info ON info.typeId = type.id
AND info.caseFilingResult = 'CONFIRM_FILING'
AND (@sessionId IS NULL OR @sessionId = '' OR info.sessionId = @sessionId)
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN (
SELECT
processInstanceId,
JSON_UNQUOTE(JSON_EXTRACT(variable, '$.tf_feedback')) AS tf_feedback
FROM (
SELECT
processInstanceId,
variable,
ROW_NUMBER() OVER (PARTITION BY processInstanceId ORDER BY finishTime DESC, id DESC) AS rn
FROM
wf_process_task
WHERE
taskName = 'feedback'
AND taskState = 20
) feedbackTask
WHERE feedbackTask.rn = 1
) latestFeedback ON latestFeedback.processInstanceId = ins.id
$condition
GROUP BY
type.id
ORDER BY
type.code
""");
sql.setParam("sessionId", param == null ? null : param.getSessionId());
Cnd cnd = Cnd.NEW();
if (param != null && Lang.isNotEmpty(param.getTypeIds())) {
cnd.and("type.id", "in", param.getTypeIds());
}
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
for (NutMap row : list) {
// 前端直接展示百分比文本,避免每个页面重复处理空值和数值格式。
row.put("completionRateText", ObjectUtil.defaultIfNull(row.get("completionRate"), 0).toString() + "%");
row.put("satisfactionRateText", ObjectUtil.defaultIfNull(row.get("satisfactionRate"), 0).toString() + "%");
}
return list;
}
}
@@ -34,6 +34,8 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
/**
* @ClassName CondolenceApplyController
* @Author JyuHsin
@@ -140,6 +142,16 @@ public class CondolenceMineController {
//smsService.send("20182040", "这是一条由智慧工会系统发出的测试消息。");
return Result.success(nutMap);
}
@At
@Ok("void")
@ApiOperation("导出报销单")
@SaCheckPermission("condolence.mine")
@SLog(tag = "职工慰问系统-我的申请", msg = "导出慰问报销单")
public void doExport(@Param("id") String id, HttpServletResponse response) {
condolenceService.exportReimbursement(id, response);
}
@Inject
private SmsService smsService;
}
@@ -5,29 +5,15 @@ import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.sys.param.SysMsgSummaryPageForm;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@@ -68,56 +54,16 @@ public class CondolenceReadController {
@SaCheckPermission("condolence.read")
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "month") String month,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId) {
Sql sql = Sqls.create("""
SELECT
info.*,
ins.id AS instanceId,
type.name as typeName,
ins.state instanceState,
ins.processDefineId instanceProcessDefineId,
t.displayName taskName
FROM
condolence info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN (
SELECT
processInstanceId,
displayName,
createdAt,
ROW_NUMBER() OVER (PARTITION BY processInstanceId ORDER BY createdAt DESC) AS rn
FROM wf_process_task
) t ON t.processInstanceId = ins.id AND t.rn = 1
LEFT JOIN condolence_type type ON info.type = type.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("info.helpUserName", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("info.helpLoginName", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("info.applyUnionId", "=", SecurityUtil.getUnionId());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
sql.setCondition(cnd);
Pagination<NutMap> pageVO = condolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
@Param(value = "unitId") String unitId,
@Param(value = "userState") String userState) {
Pagination<NutMap> pageVO = condolenceService.pageReadData(pageForm, year, month, type, unionId, unitId, userState);
return Result.success(NutMap.NEW()
.addv("list", pageVO.getList())
.addv("totalCount", pageVO.getTotalCount())
.addv("totalMoney", condolenceService.sumReadMoney(pageForm, year, month, type, unionId, unitId, userState)));
}
@At
@@ -126,47 +72,24 @@ public class CondolenceReadController {
@ApiOperation("导出职工慰问汇总表")
public void onExport(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "month") String month,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "userState") String userState,
HttpServletResponse response) {
//查询审核通过的数据
Sql sql = Sqls.create("""
select
con.*,
type.name as typeName
from
condolence con
left join wf_process_instance ins on ins.businessNo = con.id
left join condolence_type type on type.id = con.type
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and(ProcessInstance::getState, "=", ProcessTaskStateEnum.FINISHED.getCode());
cnd.andEX("con.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("con.applyUnionId", "=", unionId);
cnd.andEX("con.applyUnitId", "=", unitId);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("info.helpUserName", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("info.helpLoginName", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("con.applyUnionId", "=", SecurityUtil.getUnionId());
}
sql.setCondition(cnd);
List<NutMap> list = condolenceService.listMap(sql);
List<NutMap> list = condolenceService.listCompletedReadData(pageForm, year, month, type, unionId, unitId, userState);
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("慰问对象", "helpUserName", 20));
exportEntities.add(new ExcelExportEntity("慰问对象工号", "helpLoginName", 20));
exportEntities.add(new ExcelExportEntity("在职状态", "userState", 20));
exportEntities.add(new ExcelExportEntity("申请人", "applyUserName", 20));
exportEntities.add(new ExcelExportEntity("所在工会", "helpUnionName", 20));
exportEntities.add(new ExcelExportEntity("所在单位", "helpUnitName", 20));
exportEntities.add(new ExcelExportEntity("慰问类型", "typeName", 20));
exportEntities.add(new ExcelExportEntity("拟补贴金额", "money", 20));
exportEntities.add(new ExcelExportEntity("慰问金额", "money", 20));
exportEntities.add(new ExcelExportEntity("申请理由", "remark", 20));
ExportParams exportParams = new ExportParams();
@@ -174,4 +97,19 @@ public class CondolenceReadController {
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
CommonDownloadUtil.download("申报人汇总.xlsx", workbook, response);
}
@At
@Ok("void")
@SaCheckPermission("condolence.read")
@ApiOperation("导出慰问附件压缩包")
public void exportZip(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "month") String month,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "userState") String userState,
HttpServletResponse response) {
condolenceService.exportReadZip(pageForm, year, month, type, unionId, unitId, userState, response);
}
}
@@ -1,7 +1,14 @@
package com.budwk.app.zhgh.staffbenefit.condolence.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.List;
/**
* @ClassName CondolenceService
@@ -11,4 +18,68 @@ import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
* @Description TODO
*/
public interface CondolenceService extends BaseService<Condolence> {
/**
* 导出已完成流程的慰问报销单。
*
* @param id 慰问申请ID
* @param response 文件下载响应
*/
void exportReimbursement(String id, HttpServletResponse response);
/**
* 查询阅览页列表数据,月份优先于年度筛选。
*
* @param pageForm 分页筛选参数
* @param year 年度
* @param month 月份,格式 yyyy-MM
* @param type 慰问类型
* @param unionId 所属工会
* @param unitId 所属单位
* @param userState 慰问对象在职状态
* @return 阅览页分页数据
*/
Pagination<NutMap> pageReadData(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState);
/**
* 统计阅览页筛选条件下的慰问金额合计。
*
* @param pageForm 分页筛选参数
* @param year 年度
* @param month 月份,格式 yyyy-MM
* @param type 慰问类型
* @param unionId 所属工会
* @param unitId 所属单位
* @param userState 慰问对象在职状态
* @return 慰问金额合计
*/
BigDecimal sumReadMoney(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState);
/**
* 查询阅览页筛选出的已完成慰问数据。
*
* @param pageForm 分页筛选参数
* @param year 年度
* @param month 月份,格式 yyyy-MM
* @param type 慰问类型
* @param unionId 所属工会
* @param unitId 所属单位
* @param userState 慰问对象在职状态
* @return 已完成慰问数据
*/
List<NutMap> listCompletedReadData(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState);
/**
* 导出阅览页筛选出的慰问附件和报销单压缩包。
*
* @param pageForm 分页筛选参数
* @param year 年度
* @param month 月份,格式 yyyy-MM
* @param type 慰问类型
* @param unionId 所属工会
* @param unitId 所属单位
* @param userState 慰问对象在职状态
* @param response 文件下载响应
*/
void exportReadZip(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState, HttpServletResponse response);
}
@@ -1,11 +1,52 @@
package com.budwk.app.zhgh.staffbenefit.condolence.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
import com.deepoove.poi.XWPFTemplate;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* @ClassName CondolenceServiceImpl
@@ -18,7 +59,418 @@ import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class CondolenceServiceImpl extends BaseServiceImpl<Condolence> implements CondolenceService {
private static final String[] RMB_UPPER_DIGITS = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
@Inject
private FlowEngine flowEngine;
@Inject
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
@Inject
private SysFileService sysFileService;
public CondolenceServiceImpl(Dao dao) {
super(dao);
}
@Override
public void exportReimbursement(String id, HttpServletResponse response) {
NutMap info = fetchCompletedCondolenceInfo(id);
String memberName = StrUtil.blankToDefault(info.getString("helpUserName"), "慰问");
CommonDownloadUtil.download(memberName + "慰问报销单.docx", createReimbursementDocxBytes(info), response);
}
@Override
public Pagination<NutMap> pageReadData(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState) {
Sql sql = buildReadSql(pageForm, year, month, type, unionId, unitId, userState, false, true);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public BigDecimal sumReadMoney(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState) {
Sql sql = Sqls.create("""
SELECT
COALESCE(SUM(info.money), 0) AS totalMoney
FROM
condolence info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN condolence_type type ON type.id = info.type
LEFT JOIN `vw_user` helpUser ON helpUser.id = info.helpUserId
$condition
""");
sql.setCondition(buildReadCondition(pageForm, year, month, type, unionId, unitId, userState, false, false));
sql.setCallback(Sqls.callback.map());
dao().execute(sql);
NutMap result = (NutMap) sql.getResult();
return toBigDecimal(result == null ? null : result.get("totalMoney"));
}
@Override
public List<NutMap> listCompletedReadData(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState) {
Sql sql = buildReadSql(pageForm, year, month, type, unionId, unitId, userState, true, false);
return listMap(sql);
}
@Override
public void exportReadZip(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState, HttpServletResponse response) {
List<NutMap> list = listCompletedReadData(pageForm, year, month, type, unionId, unitId, userState);
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLUtil.encode("职工慰问附件.zip"));
try (ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream())) {
Set<String> entryNames = new HashSet<>();
for (int i = 0; i < list.size(); i++) {
NutMap row = list.get(i);
String folderName = uniqueEntryName(entryNames, buildRowFolderName(row, i + 1) + "/");
zipOutputStream.putNextEntry(new ZipEntry(folderName));
zipOutputStream.closeEntry();
writeZipEntry(zipOutputStream, entryNames, folderName + sanitizeFileName(row.getString("helpUserName")) + "慰问报销单.docx", createReimbursementDocxBytes(row));
writeAttachmentFolder(zipOutputStream, entryNames, folderName, row);
}
zipOutputStream.finish();
} catch (IOException e) {
log.error("职工慰问附件ZIP导出失败,错误信息:{}", e.getMessage());
}
}
/**
* 阅览页、ZIP 和 Excel 导出共用同一套筛选条件,避免不同出口查出的人员不一致。
*/
private Sql buildReadSql(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState, boolean completedOnly, boolean withTask) {
Sql sql = Sqls.create(withTask ? """
SELECT
info.*,
helpUser.userState AS userState,
ins.id AS instanceId,
type.name AS typeName,
ins.state AS instanceState,
ins.processDefineId AS instanceProcessDefineId,
t.displayName AS taskName
FROM
condolence info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN (
SELECT
processInstanceId,
displayName,
createdAt,
ROW_NUMBER() OVER (PARTITION BY processInstanceId ORDER BY createdAt DESC) AS rn
FROM wf_process_task
) t ON t.processInstanceId = ins.id AND t.rn = 1
LEFT JOIN condolence_type type ON info.type = type.id
LEFT JOIN `vw_user` helpUser ON helpUser.id = info.helpUserId
$condition
""" : """
SELECT
info.*,
helpUser.userState AS userState,
type.name AS typeName,
ins.id AS instanceId
FROM
condolence info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN condolence_type type ON type.id = info.type
LEFT JOIN `vw_user` helpUser ON helpUser.id = info.helpUserId
$condition
""");
sql.setCondition(buildReadCondition(pageForm, year, month, type, unionId, unitId, userState, completedOnly, true));
return sql;
}
/**
* 组装阅览页查询条件,月份筛选优先于年度筛选。
*/
private Cnd buildReadCondition(PageForm pageForm, Integer year, String month, String type, String unionId, String unitId, String userState, boolean completedOnly, boolean withOrder) {
Cnd cnd = Cnd.NEW();
if (completedOnly) {
cnd.and("ins.state", "=", ProcessTaskStateEnum.FINISHED.getCode());
}
cnd.andEX("info.type", "=", type);
if (StrUtil.isNotBlank(month)) {
cnd.and("DATE_FORMAT(info.createTime, '%Y-%m')", "=", month);
} else {
cnd.andEX("YEAR(info.createTime)", "=", year);
}
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
cnd.andEX("helpUser.userState", "=", userState);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("info.helpUserName", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("info.helpLoginName", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("info.applyUnionId", "=", SecurityUtil.getUnionId());
}
if (withOrder) {
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
}
return cnd;
}
private BigDecimal toBigDecimal(Object value) {
if (value == null) {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
if (value instanceof BigDecimal) {
return ((BigDecimal) value).setScale(2, RoundingMode.HALF_UP);
}
return new BigDecimal(value.toString()).setScale(2, RoundingMode.HALF_UP);
}
/**
* 根据慰问记录生成报销单 Word 字节,供单条导出和 ZIP 导出复用。
*/
private byte[] createReimbursementDocxBytes(NutMap info) {
HashMap<String, Object> docData = new HashMap<>();
docData.put("condolenceType", info.getString("typeName"));
docData.put("applyTime", formatDocDate(info.getString("createTime")));
docData.put("condolenceTime", info.getString("occurTime"));
docData.put("memberName", info.getString("helpUserName"));
docData.put("reason", info.getString("remark"));
docData.put("condolenceMoney", formatMoney(info.getDouble("money")));
docData.put("applyUserName", info.getString("applyUserName"));
docData.put("bankCardNumber", info.getString("bankCardNumber"));
docData.put("bankOfDeposit", info.getString("bankOfDeposit"));
putPictureRenderData(docData, info.getString("signature"), "applyUserSign", "applyUserSignature", "申请人签字");
// 慰问金额合计(大写)按模板格子拆成“仟、佰、拾、元、角、分”六个字段。
docData.putAll(buildMoneyDigitMap(info.getDouble("money")));
List<ProcessTaskVO> doneTaskVos = getDoneTaskVos(info.getLong("instanceId"));
putApprovalSign(docData, doneTaskVos, "分工会审核", "branchChairmanSign", "branchChairmanSignTime", "branchChairmanTime", "分工会主席填写时间");
putApprovalSign(docData, doneTaskVos, "校工会审核", "unionChairmanSign", "unionChairmanSignTime", "unionChairmanTime", "工会主席填写时间");
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("condolence"))
.render(docData)
.writeAndClose(byteArrayOutputStream);
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
log.error("慰问报销单生成失败,id:{},错误信息:{}", info.getString("id"), e.getMessage());
return new byte[0];
}
}
private void writeAttachmentFolder(ZipOutputStream zipOutputStream, Set<String> entryNames, String folderName, NutMap row) throws IOException {
String attachmentFolder = folderName + "附件/";
zipOutputStream.putNextEntry(new ZipEntry(uniqueEntryName(entryNames, attachmentFolder)));
zipOutputStream.closeEntry();
List<JSONObject> files = parseFiles(row.getString("files"));
for (int i = 0; i < files.size(); i++) {
JSONObject file = files.get(i);
String fileId = getFileId(file);
if (StrUtil.isBlank(fileId)) {
continue;
}
try {
String fileName = sanitizeFileName(StrUtil.blankToDefault(file.getStr("name"), "附件" + (i + 1)));
writeZipEntry(zipOutputStream, entryNames, attachmentFolder + fileName, sysFileService.download(fileId));
} catch (Exception e) {
log.error("慰问附件写入ZIP失败,慰问ID:{},文件ID:{},错误信息:{}", row.getString("id"), fileId, e.getMessage());
}
}
}
private List<JSONObject> parseFiles(String filesStr) {
if (StrUtil.isBlank(filesStr)) {
return new ArrayList<>();
}
try {
return JSONUtil.parseArray(filesStr).toList(JSONObject.class);
} catch (Exception e) {
log.error("慰问附件解析失败,附件内容:{}", filesStr);
return new ArrayList<>();
}
}
private String getFileId(JSONObject file) {
String filePath = "";
JSONObject response = file.getJSONObject("response");
if (response != null) {
filePath = response.getStr("data");
}
if (StrUtil.isBlank(filePath)) {
filePath = file.getStr("url");
}
if (StrUtil.isBlank(filePath)) {
filePath = file.getStr("downloadPath");
}
if (StrUtil.isBlank(filePath)) {
filePath = file.getStr("id");
}
if (StrUtil.isBlank(filePath)) {
return "";
}
int index = filePath.indexOf("id=");
if (index >= 0) {
return filePath.substring(index + 3);
}
return filePath;
}
private void writeZipEntry(ZipOutputStream zipOutputStream, Set<String> entryNames, String entryName, byte[] bytes) throws IOException {
if (bytes == null || bytes.length == 0) {
return;
}
zipOutputStream.putNextEntry(new ZipEntry(uniqueEntryName(entryNames, entryName)));
zipOutputStream.write(bytes);
zipOutputStream.closeEntry();
}
private String buildRowFolderName(NutMap row, int index) {
String name = StrUtil.blankToDefault(row.getString("helpUserName"), "慰问人");
String loginName = StrUtil.blankToDefault(row.getString("helpLoginName"), "");
String createTime = StrUtil.blankToDefault(row.getString("createTime"), "");
String folderName = index + "." + name + (StrUtil.isBlank(loginName) ? "" : "_" + loginName) + "_" + createTime;
return sanitizeFileName(folderName);
}
private String sanitizeFileName(String fileName) {
String name = StrUtil.blankToDefault(fileName, "未命名");
return name.replaceAll("[\\\\/:*?\"<>|\\r\\n]", "_");
}
private String uniqueEntryName(Set<String> entryNames, String entryName) {
String name = entryName;
int count = 1;
while (entryNames.contains(name)) {
int slashIndex = entryName.lastIndexOf("/");
int dotIndex = entryName.lastIndexOf(".");
if (dotIndex > slashIndex) {
name = entryName.substring(0, dotIndex) + "(" + count + ")" + entryName.substring(dotIndex);
} else if (entryName.endsWith("/")) {
name = entryName.substring(0, entryName.length() - 1) + "(" + count + ")/";
} else {
name = entryName + "(" + count + ")";
}
count++;
}
entryNames.add(name);
return name;
}
/**
* 只允许已完成流程导出,避免未审批完成的数据生成正式报销单。
*/
private NutMap fetchCompletedCondolenceInfo(String id) {
Sql sql = Sqls.create("""
SELECT
info.*,
type.name AS typeName,
ins.id AS instanceId
FROM
condolence info
LEFT JOIN condolence_type type ON info.type = type.id
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
WHERE
ins.state = 20 AND info.id = @id
""").setParam("id", id);
sql.setCallback(Sqls.callback.map());
dao().execute(sql);
NutMap info = (NutMap) sql.getResult();
if (info == null) {
throw new RuntimeException("慰问流程未完成或记录不存在,不能导出报销单");
}
return info;
}
/**
* 读取已办流程任务,后续从任务表单变量中取审批签字。
*/
private List<ProcessTaskVO> getDoneTaskVos(Long instanceId) {
List<ProcessTaskVO> doneTaskVos = new ArrayList<>();
List<ProcessTask> doneTaskList = flowEngine.processTaskService().getDoneTaskList(instanceId, null);
for (ProcessTask doneTask : doneTaskList) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(doneTask.getId());
doneTaskVos.add(taskVO);
}
return doneTaskVos;
}
/**
* 将指定审批节点最近一次签字和填写时间放入模板字段。
*/
private void putApprovalSign(Map<String, Object> docData, List<ProcessTaskVO> doneTaskVos, String taskName, String templateField, String... timeFields) {
doneTaskVos.stream()
.filter(task -> taskName.equals(task.getDisplayName()))
.max(Comparator.comparing(ProcessTaskVO::getCreatedAt))
.ifPresent(task -> {
Dict taskFormData = task.getTaskFormData();
String signTime = taskFormData.getStr("finishTimeForDocx");
if (StrUtil.isBlank(signTime) && task.getFinishTime() != null) {
signTime = DateUtil.format(task.getFinishTime(), "yyyy年MM月dd日");
}
for (String timeField : timeFields) {
docData.put(timeField, signTime);
}
String sign = taskFormData.getStr("tf_userSign");
if (StrUtil.isBlank(sign)) {
sign = taskFormData.getStr("tf_sign");
}
if (StrUtil.isNotBlank(sign)) {
putPictureRenderData(docData, sign, templateField);
}
});
}
/**
* 将签字图片同时写入多个模板字段,兼容不同模板占位符命名。
*/
private void putPictureRenderData(Map<String, Object> docData, String sign, String... templateFields) {
if (StrUtil.isBlank(sign)) {
return;
}
for (String templateField : templateFields) {
docData.put(templateField, sysOfficeTemplateUtil.createPictureRenderData(sign));
}
}
private String formatDocDate(String date) {
if (StrUtil.isBlank(date)) {
return "";
}
try {
return DateUtil.format(DateUtil.parse(date), "yyyy年MM月dd日");
} catch (Exception e) {
return date;
}
}
private String formatMoney(Double money) {
if (money == null) {
return "";
}
return BigDecimal.valueOf(money).setScale(2, RoundingMode.HALF_UP).toPlainString();
}
/**
* 报销单模板只预留到仟位,金额超过 9999.99 时直接提示,防止生成错误单据。
*/
private Map<String, Object> buildMoneyDigitMap(Double money) {
BigDecimal amount = money == null ? BigDecimal.ZERO : BigDecimal.valueOf(money);
amount = amount.setScale(2, RoundingMode.HALF_UP);
if (amount.compareTo(new BigDecimal("9999.99")) > 0) {
throw new RuntimeException("慰问金额超过报销单模板支持范围");
}
long cents = amount.movePointRight(2).longValue();
Map<String, Object> map = new HashMap<>();
map.put("Qian", toUpperDigit((int) (cents / 100000 % 10)));
map.put("Bai", toUpperDigit((int) (cents / 10000 % 10)));
map.put("Shi", toUpperDigit((int) (cents / 1000 % 10)));
map.put("Yuan", toUpperDigit((int) (cents / 100 % 10)));
map.put("Jiao", toUpperDigit((int) (cents / 10 % 10)));
map.put("Fen", toUpperDigit((int) (cents % 10)));
return map;
}
private String toUpperDigit(int digit) {
if (digit < 0 || digit > 9) {
return "";
}
return RMB_UPPER_DIGITS[digit];
}
}
+89 -9
View File
@@ -1,17 +1,85 @@
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="false" scanPeriod="60000" debug="false">
<!-- 定义日志文件的存储路径 -->
<!-- 日志存储路径 -->
<property name="LOG_HOME" value="./logs"/>
<!-- 控制台输出配置 -->
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- INFO级别日志文件配置 -->
<!--
#################################################################################
# #
# 全文输出日志(一个文件存储)start #
# #
#################################################################################
-->
<!-- 所有级别日志写入同一个文件 -->
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/app.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/app-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
<!-- 框架日志级别(按需调整) -->
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="org.quartz" level="INFO"/>
<logger name="org.nutz" level="DEBUG"/>
<!-- root logger:DEBUG 级别,输出到控制台和全量文件 -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ALL_FILE"/>
</root>
<!--
#################################################################################
# #
# 全文输出日志(一个文件存储)end #
# #
#################################################################################
-->
<!--
#################################################################################
# #
# debug、info、error分文件输出 start #
# #
#################################################################################
-->
<!-- DEBUG级别日志文件配置 -->
<!--<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/debug.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/debug-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
&lt;!&ndash; INFO级别日志文件配置 &ndash;&gt;
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/info.log</file>
<encoder>
@@ -24,11 +92,11 @@
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
<!-- ERROR级别日志文件配置 -->
&lt;!&ndash; ERROR级别日志文件配置 &ndash;&gt;
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<encoder>
@@ -41,19 +109,31 @@
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
<logger name="java" additivity="false" />
&lt;!&ndash; 框架日志级别 &ndash;&gt;
&lt;!&ndash; <logger name="java" additivity="false" />&ndash;&gt;
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="org.quartz" level="INFO"/>
<logger name="org.nutz" level="DEBUG"/>
<!-- 日志级别和appender的关联 -->
&lt;!&ndash; 日志级别和appender的关联 &ndash;&gt;
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</root>-->
<!--
#################################################################################
# #
# debug、info、error分文件输出 end #
# #
#################################################################################
-->
</configuration>
@@ -258,7 +258,12 @@ const user = {
this.$message.warning("当前流程没有配置地址")
return
}
window.open(formKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
var viewUrl = formKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab
// 首页待办进入提案附议 second 节点时,默认打开受邀附议页签,避免影响其他流程跳转。
if (formKey === "/platform/proposal/seconded" && taskKey === "second" && this.activeTab === "todo") {
viewUrl += "&secondedTab=inviteSeconded"
}
window.open(viewUrl)
},
getRoleNames() {
$.post("/platform/sys/role/getRoleNames").then(res => {
@@ -420,7 +420,12 @@ layout("/layouts/v4/baseLayout.html"){
this.$message.warning("当前流程没有配置地址")
return
}
window.open(formKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey)
var viewUrl = formKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey
// 待办中心进入提案附议 second 节点时,默认打开受邀附议页签,避免影响其他流程跳转。
if (formKey === "/platform/proposal/seconded" && taskKey === "second" && this.activeTab === "todo") {
viewUrl += "&secondedTab=inviteSeconded"
}
window.open(viewUrl)
},
// 获取空状态文本
@@ -464,7 +464,12 @@ layout("/layouts/v4/baseLayout.html"){
this.$message.warning("当前流程没有配置地址")
return
}
window.open(formKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey)
var viewUrl = formKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey
// 待办中心进入提案附议 second 节点时,默认打开受邀附议页签,避免影响其他流程跳转。
if (formKey === "/platform/proposal/seconded" && taskKey === "second" && this.activeTab === "todo") {
viewUrl += "&secondedTab=inviteSeconded"
}
window.open(viewUrl)
},
// 获取空状态文本
@@ -337,12 +337,13 @@ layout("/layouts/platform.html"){
},
listSession() {
this.$axios.post("/platform/proposal/common/listSession", {
orderBy: "asc"
orderBy: "desc"
}).then((res) => {
if (res.code === 0) {
this.sessionOptions = res.data
if (this.sessionOptions) {
this.$set(this.pageForm, "sessionId", this.sessionOptions[0].id)
this.$set(this.pageForm, "sessionIds", [this.sessionOptions[0].id])
this.listDelegation()
this.pageData()
}
@@ -0,0 +1,106 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="届次">
<el-select clearable filterable placeholder="请选择届次" v-model="pageForm.sessionId">
<el-option :key="item.id" :label="item.fullName" :value="item.id"
v-for="item in sessionOptions"></el-option>
</el-select>
</search-item>
<search-item label="提案类别">
<el-select clearable collapse-tags filterable multiple placeholder="请选择提案类别"
v-model="pageForm.typeIds">
<el-option :key="item.id" :label="item.name" :value="item.id"
v-for="item in typeOptions"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<el-table :data="tableData" style="width: 100%">
<el-table-column label="序号" type="index" width="50"></el-table-column>
<el-table-column label="提案类别" prop="typeName" min-width="160" sortable></el-table-column>
<el-table-column label="立案同意数" prop="filingCount" sortable></el-table-column>
<el-table-column label="已反馈数" prop="feedbackCount" sortable></el-table-column>
<el-table-column label="未反馈数" prop="unFeedbackCount" sortable></el-table-column>
<el-table-column label="完成率" prop="completionRate" sortable>
<template scope="{row}">
{{ row.completionRateText }}
</template>
</el-table-column>
<el-table-column label="满意数" prop="satisfiedCount" sortable></el-table-column>
<el-table-column label="不满意数" prop="dissatisfiedCount" sortable></el-table-column>
<el-table-column label="满意率" prop="satisfactionRate" sortable>
<template scope="{row}">
{{ row.satisfactionRateText }}
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/info.js"){}#-->
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
sessionOptions: [],
typeOptions: [],
pageForm: {
sessionId: null,
typeIds: []
}
}
},
methods: {
doSearch() {
this.pageData()
},
listSession() {
this.$axios.post("/platform/proposal/common/listSession", {
orderBy: "desc"
}).then((res) => {
if (res.code === 0) {
this.$set(this, "sessionOptions", res.data)
if (this.sessionOptions && this.sessionOptions.length > 0) {
this.$set(this.pageForm, "sessionId", this.sessionOptions[0].id)
}
this.pageData()
}
})
},
listProposalType() {
this.$axios.post("/platform/proposal/common/listProposalType").then((res) => {
if (res.code === 0) {
this.$set(this, "typeOptions", res.data)
}
})
},
pageData() {
this.$axios.post(loc() + "/data", {
pageForm: JSON.stringify(this.pageForm)
}).then((res) => {
if (res.code === 0) {
this.$set(this, "tableData", res.data)
}
})
}
},
created() {
this.listSession()
this.listProposalType()
}
})
</script>
<!--#
}
#-->
@@ -87,7 +87,7 @@ layout("/layouts/platform.html"){
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.taskState === 10" @click="openAudit(row)" size="mini" type="primary">审核
</el-button>
<el-button @click="onRevoke(row)" size="mini" type="danger">撤回
<el-button v-if="row.canRevoke" @click="onRevoke(row)" size="mini" type="danger">撤回
</el-button>
</template>
</el-table-column>
@@ -205,6 +205,14 @@ layout("/layouts/platform.html"){
}
},
methods: {
initSecondedTab: function () {
// 只响应首页待办跳转传入的提案附议专用参数,普通菜单进入仍默认主动附议。
if (GetQueryString("secondedTab") === "inviteSeconded") {
this.$set(this, "activeTab", "inviteSeconded")
this.$set(this.pageForm, "approval", false)
}
},
syncTableColumns: function () {
if (this.activeTab === "inviteSeconded") {
this.$set(this, "tableColumns", this.inviteColumns)
@@ -455,6 +463,7 @@ layout("/layouts/platform.html"){
}
},
created() {
this.initSecondedTab()
this.syncTableColumns()
this.pageData()
this.listOpenSession()
@@ -81,6 +81,10 @@ layout("/layouts/platform.html"){
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
<el-form-item label="电子签名" prop="tf_userSign"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<pc-signature v-model="formData.tf_userSign"></pc-signature>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
@@ -135,7 +139,8 @@ layout("/layouts/platform.html"){
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName
taskName: row.curTaskName,
tf_userSign: null
}
this.$refs.condolenceInfoRef.onOpen(row)
})
@@ -60,6 +60,7 @@ layout("/layouts/platform.html"){
<el-table-column label="操作" fixed="right" width="300">
<template slot-scope="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.instanceState === 20" @click="doExport(row)" size="mini" type="primary">导出报销单</el-button>
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button v-if="row.canRevoke" size="mini" type="danger" @click="onRevoke(row)">撤回</el-button>
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" @click="onDelete(row.id)" size="mini" type="danger">
@@ -102,6 +103,9 @@ layout("/layouts/platform.html"){
}
},
methods: {
doExport(row) {
window.location.href = "/platform/condolence/mine/doExport?id=" + (row.id || "")
},
onAdd() {
commonUtil.pjaxPush('/platform/condolence/apply')
},
@@ -16,6 +16,16 @@ layout("/layouts/platform.html"){
@change="doSearch"
></el-date-picker>
</search-item>
<search-item label="月份">
<el-date-picker
v-model="pageForm.month"
type="month"
value-format="yyyy-MM"
placeholder="请选择月份"
style="width: 100%"
@change="doSearch"
></el-date-picker>
</search-item>
<search-item label="姓名/工号">
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
@@ -31,6 +41,14 @@ layout("/layouts/platform.html"){
></el-option>
</el-select>
</search-item>
<search-item label="在职状态">
<dict-select v-model="pageForm.userState"
style="width: 100%"
clearable
placeholder="请选择在职状态"
code="USER_STATE"
@change="doSearch"></dict-select>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" placeholder="请选择所属工会" filterable clearable
@change="doSearch" style="width: 100%">
@@ -56,8 +74,13 @@ layout("/layouts/platform.html"){
<el-card shadow="never">
<table-tool label="申请列表">
<el-button @click="onExport" icon="el-icon-s-promotion" type="primary" size="small">导出</el-button>
<el-button @click="onExportZip" icon="el-icon-folder-opened" type="primary" size="small">导出附件ZIP</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table :data="tableData"
@sort-change="pageOrder"
:summary-method="getSummaries"
show-summary
style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
@@ -107,18 +130,46 @@ layout("/layouts/platform.html"){
tableColumns: [
{prop: 'helpUserName', label: '慰问对象'},
{prop: 'helpLoginName', label: '慰问对象工号'},
{prop: 'userState', label: '在职状态'},
{prop: 'applyUserName', label: '申请人'},
{prop: 'applyUnionName', label: '所属工会'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'typeName', label: '慰问类型'},
{prop: 'money', label: '慰问金额'},
{prop: 'taskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
unionList: [],
unitList: [],
totalMoney: 0,
}
},
methods: {
pageData(data) {
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
this.tableLoading = true
this.$axios.post(address, data ? data : this.pageForm).then((res) => {
this.tableLoading = false
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
this.$set(this, "totalMoney", Number(res.data.totalMoney || 0))
}
})
},
getSummaries(param) {
const sums = []
param.columns.forEach((column, index) => {
if (index === 0) {
sums[index] = "合计"
} else if (column.property === "money") {
sums[index] = this.totalMoney.toFixed(2)
} else {
sums[index] = ""
}
})
return sums
},
onDelete(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
@@ -136,6 +187,9 @@ layout("/layouts/platform.html"){
onExport() {
this.$downLoad(loc() + '/onExport', this.pageForm)
},
onExportZip() {
this.$downLoad(loc() + '/exportZip', this.pageForm)
},
onView(row) {
this.$refs.guava.edit(()=>{
this.$refs.condolenceInfoRef.onOpen(row)
@@ -147,8 +201,17 @@ layout("/layouts/platform.html"){
this.typeOptions = resp.data
})
},
initPageForm() {
this.$set(this.pageForm, "year", "")
this.$set(this.pageForm, "month", "")
this.$set(this.pageForm, "type", "")
this.$set(this.pageForm, "unionId", "")
this.$set(this.pageForm, "unitId", "")
this.$set(this.pageForm, "userState", "")
},
},
async created() {
this.initPageForm()
this.unionList = await this.$businessTool.listUnion()
this.unitList = await this.$businessTool.listUnit()
this.queryCondolenceType()
@@ -101,6 +101,10 @@ layout("/layouts/platform.html"){
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
<el-form-item label="电子签名" prop="tf_userSign"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<pc-signature v-model="formData.tf_userSign"></pc-signature>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
@@ -158,7 +162,8 @@ layout("/layouts/platform.html"){
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName
taskName: row.curTaskName,
tf_userSign: null
}
this.$refs.condolenceInfoRef.onOpen(row)
})
@@ -70,6 +70,17 @@ layout("/layouts/platform_h5.html"){
maxlength="100"
show-word-limit
></van-field>
<van-field
class="more-text"
name="tf_userSign"
label="电子签名"
:rules="[{ required: true, message: '请签字' }]"
required
>
<template #input>
<h5-signature v-model="formData.tf_userSign"></h5-signature>
</template>
</van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
@@ -128,7 +139,8 @@ layout("/layouts/platform_h5.html"){
this.$refs.infoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName
taskName: row.curTaskName,
tf_userSign: null
}
},
async handleTaskAction(val) {
@@ -70,6 +70,17 @@ layout("/layouts/platform_h5.html"){
maxlength="100"
show-word-limit
></van-field>
<van-field
class="more-text"
name="tf_userSign"
label="电子签名"
:rules="[{ required: true, message: '请签字' }]"
required
>
<template #input>
<h5-signature v-model="formData.tf_userSign"></h5-signature>
</template>
</van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
@@ -128,7 +139,8 @@ layout("/layouts/platform_h5.html"){
this.$refs.infoRef.onOpen(row)
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName
taskName: row.curTaskName,
tf_userSign: null
}
},
async handleTaskAction(val) {