18 Commits
Author SHA1 Message Date
c-chengc1 9590ad5f8c Merge remote-tracking branch 'origin/fix_职工慰问' into release_20261030问题整改 2026-10-31 08:47:22 +08:00
c-chengc1 e60d4d6c68 Merge remote-tracking branch 'origin/fix_福利疗休养问卷调查' into release_20261030问题整改 2026-10-31 08:46:57 +08:00
c-chengc1 53005c448f Merge remote-tracking branch 'origin/feature_民主管理、人员管理' into release_20261030问题整改
# Conflicts:
#	src/main/resources/static/components/plugins/sysSignature/pc.vue
2026-10-31 08:46:38 +08:00
c-chengc1 1a688d80d1 Merge remote-tracking branch 'refs/remotes/origin/fix_协会、活动整改' into release_20261030问题整改 2026-10-31 08:45:37 +08:00
c-chenxs1 81fd5cb773 feat: 特殊慰问表单优化2 2026-10-30 20:30:12 +08:00
c-chenxs1 efad953710 feat: 特殊慰问表单优化 2026-10-30 19:35:39 +08:00
c-zhangr1 ceec8eb095 协会、活动测试整改 2026-10-30 19:02:48 +08:00
c-chenxs1 e0004e410f feat: 慰问、困难帮扶优化 2026-10-30 17:57:20 +08:00
c-chenxs1 f8457386fd feat: 慰问、困难帮扶 2026-10-30 17:49:45 +08:00
c-chengc1 c1ac3db4bf fix:问卷整改h5增加“说明”显示 2026-10-30 17:12:02 +08:00
c-chengc1 6c6caa2558 fix:问卷整改
1、增加查看已答题数据时,显示总分和每题得分,以及每题正确答案
2、增加倒计时、说明
2026-10-30 16:49:50 +08:00
c-chenxs1 16cae3ed2b feat: 修复困难帮扶申请表单校验及页面加载闪烁,调整办公室主任拒绝、退回审批时不校验级别和帮扶金额。
新增特殊慰问模块,支持分工会主席统一为多人申请、Excel 导入及人员校验;完善主从表存储、表单校验、审批展示和导出,移除分工会主席审批节点,使用独立流程节点标识。
2026-10-30 16:30:48 +08:00
c-chengc1 35877f4b5c fix:疗休养整改
1、h5修改/查看报名表单增加手机号字段;
2、h5表单同步web字段必填标识显示逻辑;
3、修改/查看报名界面适配优化,样式统一;
4、h5修改/报名界面亲属信息-与本人关系选择后不正常回显问题修复,亲属信息-年龄未限制最大上限导致越界报错问题修复
2026-10-30 16:11:08 +08:00
c-zhouhf1 4f85bb08e2 经费提案整改 2026-10-30 15:50:53 +08:00
c-chengc1 001d456513 fix:疗休养模块缺少必填标识补充;疗休养报名-教职工信息手机号校验 2026-10-30 15:14:57 +08:00
c-chengc1 65f340e3b0 fix:福利模块所有页面去除三级单位、人员属性字段 2026-10-30 13:33:31 +08:00
c-zhangr1 2a305a3f23 暂提交 2026-10-30 10:56:22 +08:00
c-chenxs1 29260423d5 feat: 提取通用 Word/PDF 模板渲染方法,支持 DOC、DOCX 和循环表格 2026-10-30 10:16:46 +08:00
108 changed files with 8523 additions and 822 deletions
@@ -19,6 +19,7 @@ RoleConstant {
SCHOOL_UNION_ADMIN("校工会管理员"),
SCHOOL_UNION_CHAIRMAN("校工会主席"),
SCHOOL_UNION_VICE_CHAIRMAN("校工会副主席"),
SCHOOL_UNION_FINANCE("校工会财务"),
SCHOOL_OFFICE_DIRECTOR("校工会办公室主任"),
SCHOOL_UNION_WELFARE_ADMIN("校工会福利管理员"),
SCHOOL_UNION_MEMBER_ADMIN("校工会会员管理员"),
@@ -1,10 +1,16 @@
package com.budwk.app.base.utils;
import cn.hutool.core.io.FileUtil;
import com.aspose.cells.FontConfigs;
import com.aspose.cells.PdfSaveOptions;
import com.aspose.cells.Workbook;
import com.aspose.slides.FontsLoader;
import com.aspose.slides.LoadOptions;
import com.aspose.slides.Presentation;
import com.aspose.words.Document;
import com.aspose.words.FontSettings;
import com.aspose.words.FontSourceBase;
import com.aspose.words.MemoryFontSource;
import com.aspose.words.SaveFormat;
import com.budwk.app.base.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
@@ -12,6 +18,8 @@ import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* 文档转换
@@ -19,6 +27,81 @@ import java.io.IOException;
@Slf4j
public class OfficePlusUtil {
private static final String DEFAULT_FONT_NAME = "Noto Sans SC";
private static final String DEFAULT_FONT_RESOURCE = "/fonts/NotoSansSC-VF.ttf";
private static final Object FONT_CONFIG_LOCK = new Object();
private static volatile boolean fontConfigured;
/**
* 注册系统内置字体,并将其作为 Office 文档缺失字体或缺失字形时的默认回退字体。
* 原文档字体在服务器可用时仍优先使用,只有找不到原字体时才使用内置字体。
*/
private static void configureDefaultFont() {
if (fontConfigured) {
return;
}
synchronized (FONT_CONFIG_LOCK) {
if (fontConfigured) {
return;
}
byte[] fontData = loadDefaultFontData();
configureWordFont(fontData);
configureExcelFont(fontData);
FontsLoader.loadExternalFont(fontData);
fontConfigured = true;
}
}
/**
* 从 classpath 读取随应用打包的字体,避免依赖线上服务器安装字体。
*
* @return 默认字体的二进制数据
*/
private static byte[] loadDefaultFontData() {
try (InputStream inputStream = OfficePlusUtil.class.getResourceAsStream(DEFAULT_FONT_RESOURCE)) {
if (inputStream == null) {
throw new BaseException("系统内置默认字体不存在");
}
return inputStream.readAllBytes();
} catch (IOException e) {
log.error("读取系统内置默认字体失败", e);
throw new BaseException("读取系统内置默认字体失败");
}
}
/**
* 为 Word 转换保留已有字体源并追加内置字体,同时配置缺失字体和缺失字形回退。
*
* @param fontData 默认字体的二进制数据
*/
private static void configureWordFont(byte[] fontData) {
FontSettings fontSettings = FontSettings.getDefaultInstance();
FontSourceBase[] currentSources = fontSettings.getFontsSources();
FontSourceBase[] fontSources = Arrays.copyOf(currentSources, currentSources.length + 1);
fontSources[currentSources.length] = new MemoryFontSource(fontData);
fontSettings.setFontsSources(fontSources);
fontSettings.getSubstitutionSettings().getDefaultFontSubstitution().setDefaultFontName(DEFAULT_FONT_NAME);
try {
fontSettings.getFallbackSettings().loadNotoFallbackSettings();
} catch (Exception e) {
log.error("配置 Word 默认字体回退规则失败", e);
throw new BaseException("配置 Word 默认字体失败");
}
}
/**
* 为 Excel 转换追加内置字体,并将其设置为找不到原字体时的默认字体。
*
* @param fontData 默认字体的二进制数据
*/
private static void configureExcelFont(byte[] fontData) {
com.aspose.cells.FontSourceBase[] currentSources = FontConfigs.getFontSources();
com.aspose.cells.FontSourceBase[] fontSources = Arrays.copyOf(currentSources, currentSources.length + 1);
fontSources[currentSources.length] = new com.aspose.cells.MemoryFontSource(fontData);
FontConfigs.setFontSources(fontSources);
FontConfigs.setDefaultFontName(DEFAULT_FONT_NAME);
}
/**
* 转换
*
@@ -47,7 +130,11 @@ public class OfficePlusUtil {
public static void presentationConvertPdf(String sourcePath, String targetPath) {
Presentation presentation = null;
try {
presentation = new Presentation(sourcePath);
configureDefaultFont();
LoadOptions loadOptions = new LoadOptions();
loadOptions.setDefaultRegularFont(DEFAULT_FONT_NAME);
loadOptions.setDefaultAsianFont(DEFAULT_FONT_NAME);
presentation = new Presentation(sourcePath, loadOptions);
presentation.save(targetPath, com.aspose.slides.SaveFormat.Pdf);
} catch (Exception e) {
log.info("转换pdf报错", e);
@@ -68,10 +155,12 @@ public class OfficePlusUtil {
public static void excelConvertPdf(String sourcePath, String targetPath) {
FileOutputStream fileOS = null;
try {
configureDefaultFont();
Workbook wb = new Workbook(sourcePath);
fileOS = new FileOutputStream(targetPath);
PdfSaveOptions pdfSaveOptions = new PdfSaveOptions();
pdfSaveOptions.setOnePagePerSheet(true);
pdfSaveOptions.setDefaultFont(DEFAULT_FONT_NAME);
// for (int i = 0; i < 3; i++) {
// wb.getWorksheets().get(i).getHorizontalPageBreaks().clear();
// wb.getWorksheets().get(i).getVerticalPageBreaks().clear();
@@ -105,6 +194,7 @@ public class OfficePlusUtil {
public static void wordConvertPdf(String sourcePath, String targetPath) {
FileOutputStream os = null;
try {
configureDefaultFont();
File file = new File(targetPath);
os = new FileOutputStream(file);
Document doc = new Document(sourcePath);
@@ -6,6 +6,8 @@ import com.budwk.app.base.exception.BaseException;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.models.Sys_office_template;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.ByteArrayPictureRenderData;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
@@ -18,6 +20,12 @@ import org.nutz.ioc.loader.annotation.IocBean;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -66,6 +74,88 @@ public class SysOfficeTemplateUtil {
return new ByteArrayInputStream(fileBytes);
}
/**
* 统一渲染 DOC/DOCX 模板,保留调用方的循环表格等策略;模板输入流由调用方关闭。
*/
public byte[] renderTemplate(InputStream input, Object data, Configure config, boolean pdf, String context) throws Exception {
if (input == null) throw new BaseException("模板文件不存在");
byte[] templateBytes = input.readAllBytes();
try (ByteArrayInputStream formatInput = new ByteArrayInputStream(templateBytes)) {
if (com.aspose.words.FileFormatUtil.detectFileFormat(formatInput).getLoadFormat() != com.aspose.words.LoadFormat.DOCX) {
try (ByteArrayInputStream source = new ByteArrayInputStream(templateBytes);
ByteArrayOutputStream converted = new ByteArrayOutputStream()) {
new com.aspose.words.Document(source).save(converted, com.aspose.words.SaveFormat.DOCX);
templateBytes = converted.toByteArray();
}
}
}
byte[] wordBytes;
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateBytes);
ByteArrayOutputStream output = new ByteArrayOutputStream();
XWPFTemplate template = XWPFTemplate.compile(templateInput, config == null ? Configure.builder().build() : config)) {
template.render(data).write(output);
wordBytes = output.toByteArray();
} catch (Exception e) {
log.error("[Office模板渲染] Word模板渲染失败,context={}", context, e);
throw e;
}
log.info("[Office模板渲染] Word渲染完成,context={}, templateBytes={}, outputBytes={}", context, templateBytes.length, wordBytes.length);
if (!pdf) return wordBytes;
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(wordBytes);
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
com.aspose.words.Document document = new com.aspose.words.Document(wordInput);
configureDocumentFonts(document, context);
com.aspose.words.PdfSaveOptions options = new com.aspose.words.PdfSaveOptions();
options.setFontEmbeddingMode(com.aspose.words.PdfFontEmbeddingMode.EMBED_ALL);
document.save(output, options);
log.info("[Office模板渲染] PDF转换完成,context={}, outputBytes={}", context, output.size());
return output.toByteArray();
} catch (Exception e) {
log.error("[Office模板渲染] PDF转换失败,context={}", context, e);
throw e;
}
}
private void configureDocumentFonts(com.aspose.words.Document document, String context) throws Exception {
com.aspose.words.FontSettings settings = new com.aspose.words.FontSettings();
List<com.aspose.words.FontSourceBase> sources = new ArrayList<>(List.of(settings.getFontsSources()));
Set<String> folders = new LinkedHashSet<>();
String windowsDir = System.getenv("WINDIR");
if (StrUtil.isNotBlank(windowsDir)) folders.add(new File(windowsDir, "Fonts").getPath());
String localAppData = System.getenv("LOCALAPPDATA");
if (StrUtil.isNotBlank(localAppData)) folders.add(new File(localAppData, "Microsoft/Windows/Fonts").getPath());
folders.add(new File(System.getProperty("user.dir"), "fonts").getPath());
for (String folder : folders) {
File directory = new File(folder);
log.info("[Office模板渲染] 字体目录,context={}, path={}, readable={}", context, folder, directory.isDirectory() && directory.canRead());
if (directory.isDirectory() && directory.canRead()) sources.add(new com.aspose.words.FolderFontSource(folder, true));
}
settings.setFontsSources(sources.toArray(new com.aspose.words.FontSourceBase[0]));
Set<String> availableFonts = new LinkedHashSet<>();
for (com.aspose.words.FontSourceBase source : sources) {
for (com.aspose.words.PhysicalFontInfo font : source.getAvailableFonts()) availableFonts.add(font.getFontFamilyName());
}
String chineseFont = null;
for (String candidate : List.of("SimSun", "宋体", "Microsoft YaHei", "微软雅黑", "DengXian", "等线", "SimHei", "黑体", "FangSong", "仿宋", "Noto Serif CJK SC", "Noto Sans CJK SC")) {
if (availableFonts.contains(candidate)) {
chineseFont = candidate;
break;
}
}
log.info("[Office模板渲染] 中文字体选择,context={}, fontCount={}, chineseFont={}", context, availableFonts.size(), chineseFont);
if (chineseFont != null) {
settings.getSubstitutionSettings().getDefaultFontSubstitution().setDefaultFontName(chineseFont);
for (String name : List.of("宋体", "SimSun", "新宋体", "NSimSun", "黑体", "SimHei", "仿宋", "FangSong", "楷体", "KaiTi", "微软雅黑", "Microsoft YaHei")) {
if (!availableFonts.contains(name)) settings.getSubstitutionSettings().getTableSubstitution().setSubstitutes(name, chineseFont);
}
} else {
log.warn("[Office模板渲染] 未发现常用中文字体,context={}, availableFonts={}", context, availableFonts);
}
settings.getFallbackSettings().loadMsOfficeFallbackSettings();
document.setFontSettings(settings);
document.setWarningCallback(warning -> log.warn("[Office模板渲染] 字体或排版警告,context={}, type={}, detail={}", context, warning.getWarningType(), warning.getDescription()));
}
/**
* 签字url转换为poi的图片对象
*
@@ -41,6 +41,11 @@ public class FlowRecipientUnitSchoolLeaderHandler implements AssignmentHandler {
throw new BaseException("未找到申请记录。");
}
unitId = condolence.getHelpUnitId();
} else if ("TSWW".equals(processName)) {
com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence info = dao.fetch(
com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence.class, businessNo);
if (info == null) throw new BaseException("未找到申请记录。");
unitId = info.getHelpUnitId();
} else {
throw new BaseException("当前流程不支持获取受助人所在单位分管校领导。");
}
@@ -0,0 +1,54 @@
package com.budwk.app.flow.handler;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.flow.engine.AssignmentHandler;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.engine.model.TaskModel;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import java.util.List;
/**
* 为流程任务分配拥有校工会财务角色的用户。
*/
public class FlowSchoolUnionFinanceHandler implements AssignmentHandler {
/**
* 按校工会财务角色查询参与者,并去除重复的用户 ID。
*
* @param model 当前流程任务模型,无需额外传入人员参数
* @param execution 当前流程执行上下文,无需额外传入业务参数
* @return 用户 ID 列表;未配置角色或角色用户时返回空列表
*/
@Override
public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
Sql sql = Sqls.create("""
SELECT
userRole.userId
FROM
`sys_user_role` userRole
LEFT JOIN sys_role role ON role.id = userRole.roleId
WHERE
role.`code` = @roleCode
GROUP BY
userRole.userId
""").setParam("roleCode", RoleConstant.SCHOOL_UNION_FINANCE.name());
sql.setCallback(Sqls.callback.strList());
dao.execute(sql);
return sql.getList(String.class);
}
/**
* 返回流程设计器中展示的参与者处理类名称。
*
* @return 流程设计器下拉选项名称
*/
@Override
public String getMessage() {
return "获取校工会财务";
}
}
@@ -4,12 +4,14 @@ package com.budwk.app.zhgh.activity.sports.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.DateUtil;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.models.Sys_home_template;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.models.ActivityEvent;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
import com.budwk.app.zhgh.activity.culture.models.ActivityTissue;
@@ -28,6 +30,7 @@ import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
@@ -55,6 +58,7 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
@Override
@Aop(TransAop.READ_COMMITTED)
public String doAdd(ActivitySchool activitySchool, ActivitySchoolEvent[] events) {
events = validateAndApplySingleEventLimits(events);
ActivityBasicUnit activityBasicUnit = dao().fetch(ActivityBasicUnit.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
if (activitySchool.getActivityLevel() == 40002) {
@@ -101,6 +105,7 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
@Override
@Aop(TransAop.READ_COMMITTED)
public void doEdit(ActivitySchool activitySchool, ActivitySchoolEvent[] events) {
events = validateAndApplySingleEventLimits(events);
ActivitySchool school = dao().fetch(ActivitySchool.class, activitySchool.getId());
if (school.getActivityLevel() == 40002) {
school.setBelongUnionId(SecurityUtil.getUnionId());
@@ -161,6 +166,56 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
dao().insertOrUpdate(sysHomeActivity);
}
/**
* 校验单项人数限制,并在具体单项未设置限制时应用“所有单项”的默认值。
*
* @param events 页面提交的项目配置,允许包含 eventId 为 allItems 的默认配置
* @return 已移除 allItems 临时配置、可直接持久化的项目配置数组
*/
private ActivitySchoolEvent[] validateAndApplySingleEventLimits(ActivitySchoolEvent[] events) {
if (events == null || events.length == 0) {
return new ActivitySchoolEvent[0];
}
ActivitySchoolEvent allItems = Arrays.stream(events)
.filter(event -> event != null && "allItems".equals(event.getEventId()))
.findFirst()
.orElse(null);
List<ActivitySchoolEvent> result = new ArrayList<>();
for (ActivitySchoolEvent event : events) {
if (event == null || "allItems".equals(event.getEventId())) {
continue;
}
ActivityEvent activityEvent = dao().fetch(ActivityEvent.class, event.getEventId());
if (activityEvent != null && "1".equals(activityEvent.getProjectType())) {
applySingleEventDefaultLimit(event, allItems);
}
result.add(event);
}
return result.toArray(new ActivitySchoolEvent[0]);
}
/**
* 单项的两个限制必须同时设置;均未设置或均为0时继承“所有单项”的对应值。
*/
private void applySingleEventDefaultLimit(ActivitySchoolEvent event, ActivitySchoolEvent allItems) {
Integer athletesMaxNum = event.getAthletesMaxNum();
Integer restrictRegNumber = event.getRestrictRegNumber();
if ((athletesMaxNum == null) != (restrictRegNumber == null)) {
throw new BaseException("每队(项)运动员最多人数和每人限报项目必须同时设置");
}
boolean useDefault = athletesMaxNum == null || (athletesMaxNum == 0 && restrictRegNumber == 0);
if (!useDefault) {
return;
}
if (allItems == null || allItems.getAthletesMaxNum() == null || allItems.getRestrictRegNumber() == null) {
throw new BaseException("请先设置所有单项的运动员最多人数和每人限报项目");
}
event.setAthletesMaxNum(allItems.getAthletesMaxNum());
event.setRestrictRegNumber(allItems.getRestrictRegNumber());
}
@Aop(TransAop.READ_COMMITTED)
private void addTeam(String schoolId, ActivitySchoolEvent[] events) {
if (events.length > 0) {
@@ -271,33 +326,19 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
WHERE
app.activityId = @id
and app.status = 2
$cnd
and (@filterUnion = 0 or app.activityUnionId = @unionId)
ORDER BY
ev.allName DESC,
u.unionname DESC,
app.`team` asc
""").setParam("id", id)
.setVar("cnd", "");
if (Strings.isNotBlank(unionId)) {
sql.setVar("cnd", "and u.unionid = @unionId");
sql.setParam("unionId", unionId);
}
""").setParam("id", id);
// 参数必须声明在原始SQL中;setVar追加的字符串不会再次解析其中的@参数。
// 未选择分工会时导出整个活动,选择时按报名记录中的活动分工会过滤。
sql.setParam("filterUnion", Strings.isNotBlank(unionId) ? 1 : 0);
sql.setParam("unionId", Strings.isNotBlank(unionId) ? unionId : "");
List<Record> list = list(sql);
String activityName;
if (Lang.isNotEmpty(list)) {
activityName = list.get(0).getString("activityName");
} else {
activityName = "暂无人员";
}
List<ActivitySchoolApply> excels = new ArrayList<>();
list.forEach(z -> {
ActivitySchoolApply schoolApply = z.toPojo(ActivitySchoolApply.class);
String s = schoolApply.getIdentity().stream().map(personType::getName).toList().toString();
schoolApply.setSf(s);
excels.add(schoolApply);
});
list.forEach(record -> excels.add(toActivitySchoolApplyExport(record)));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
@@ -305,10 +346,42 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
CommonDownloadUtil.download(getUnionApplyExportFileName(unionId), workbook, response);
} catch (Exception e) {
e.printStackTrace();
throw new BaseException("导出报名信息失败:" + e.getMessage());
}
}
/**
* 将原生SQL查询结果转换为报名信息导出对象。
* identity 是数据库JSON文本,需要显式转换为身份编码集合,避免 Record.toPojo 自动转换失败导致整表无数据。
*
* @param record 导出SQL返回的一行报名数据
* @return EasyPoi 使用的 ActivitySchoolApply 导出对象
*/
private ActivitySchoolApply toActivitySchoolApplyExport(Record record) {
ActivitySchoolApply schoolApply = new ActivitySchoolApply();
schoolApply.setUnionname(record.getString("unionname"));
schoolApply.setTeam(record.getString("team"));
schoolApply.setAllName(record.getString("allName"));
schoolApply.setLoginname(record.getString("loginname"));
schoolApply.setUsername(record.getString("username"));
schoolApply.setMobile(record.getString("mobile"));
schoolApply.setIdCard(record.getString("idCard"));
schoolApply.setProfessionalLevel(record.getString("professionalLevel"));
schoolApply.setSex(record.getString("sex"));
String identityJson = record.getString("identity");
List<String> identities = Strings.isBlank(identityJson)
? new ArrayList<>()
: Json.fromJsonAsList(String.class, identityJson);
String identityName = identities.stream()
.map(personType::getName)
.filter(Strings::isNotBlank)
.collect(Collectors.joining("、"));
schoolApply.setSf(identityName);
return schoolApply;
}
private String getUnionApplyExportFileName(String unionId) {
String unionName = "分工会";
if (Strings.isNotBlank(unionId)) {
@@ -17,7 +17,6 @@ import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.club.model.ClubUser;
import com.budwk.app.zhgh.club.model.SysClub;
import com.budwk.app.zhgh.club.model.SysClubExamineRegister;
import com.budwk.app.zhgh.club.service.SysClubExamineService;
@@ -119,10 +118,7 @@ public class ClubExamineApplyController {
if(StrUtil.isBlank(clubId)) {
return Result.error("社团信息为空,请核查");
}
int count = dao.count(ClubUser.class, Cnd.where("clubId", "=", clubId));
SysClub club = dao.fetch(SysClub.class, clubId);
int money = count * (club.getDue() != null ? Integer.parseInt(club.getDue()) : 0);
return Result.success(money);
return Result.success(sysClubExamineService.getClubMemberMoney(clubId));
}
@At
@@ -175,6 +171,10 @@ public class ClubExamineApplyController {
public Result save(@Param("data") SysClubExamineRegister examineRegister,
@Param("incomeDetailed") String incomeDetailed,
@Param("incomeCensus") String incomeCensus) {
String validateMessage = sysClubExamineService.validateActivityRequired(examineRegister);
if (validateMessage != null) {
return Result.error(validateMessage);
}
if (StrUtil.isBlank(examineRegister.getId())) {
sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus);
} else {
@@ -190,6 +190,10 @@ public class ClubExamineApplyController {
public Result submit(@Param("data") SysClubExamineRegister examineRegister,
@Param("incomeDetailed") String incomeDetailed,
@Param("incomeCensus") String incomeCensus) {
String validateMessage = sysClubExamineService.validateActivityRequired(examineRegister);
if (validateMessage != null) {
return Result.error(validateMessage);
}
SysClubExamineRegister reg;
if (StrUtil.isBlank(examineRegister.getId())) {
reg = sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus);
@@ -221,6 +225,10 @@ public class ClubExamineApplyController {
@Param("incomeDetailed") String incomeDetailed,
@Param("incomeCensus") String incomeCensus,
@Param("taskId") Long taskId) {
String validateMessage = sysClubExamineService.validateActivityRequired(examineRegister);
if (validateMessage != null) {
return Result.error(validateMessage);
}
if (StrUtil.isBlank(examineRegister.getId())) {
sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus);
} else {
@@ -86,7 +86,7 @@ public class ClubExamineMineController {
//nutMap.setv("schoolName", Globals.MyConfig.getString("AppName"));
nutMap.setv("clubName", register.getClubName());
nutMap.setv("create_time", register.getFoundTime());
nutMap.setv("dues_standard", register.getDue());
nutMap.setv("dues_standard", register.getDue() == null ? "--" : register.getDue() + "元/年");
// 生成材料中的年度与列表展示的登记年度保持一致。
nutMap.setv("year", register.getYear());
nutMap.setv("registerDate", register.getRegisterDate());
@@ -91,8 +91,8 @@ public class SysClub extends BaseModel {
@Column
@Comment("会费标准")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String due;
@ColDefine(type = ColType.DOUBLE, width = 10, precision = 2)
private Double due;
@Column
@Comment("附件")
@@ -57,8 +57,8 @@ public class SysClubExamineRegister extends BaseModel {
@Column
@Comment("会费标准")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String due;
@ColDefine(type = ColType.DOUBLE, width = 10, precision = 2)
private Double due;
@Column
@Comment("年度活动情况")
@@ -24,6 +24,21 @@ public interface SysClubExamineService extends BaseService<SysClubExamineRegiste
List<NutMap> getJgUser(@Valid String clubId);
/**
* 计算指定社团的年度会费收入。
*
* @param clubId 社团ID,用于查询当前会员数量和社团会费标准
* @return 会员数量乘以会费标准后的金额;社团或会费标准为空时返回0
*/
Double getClubMemberMoney(@Valid String clubId);
/**
* 校验考核登记的两组活动明细。
* @param examineRegister 页面data参数对应的登记实体,yearActivityList为本年度活动,plans为下一年度计划
* @return String类型;通过返回null,否则返回缺失的明细行号及字段提示
*/
String validateActivityRequired(SysClubExamineRegister examineRegister);
SysClubExamineRegister doAdd(SysClubExamineRegister examineRegister, String incomeDetailed, String incomeCensus);
SysClubExamineRegister doEdit(SysClubExamineRegister examineRegister, String incomeDetailed, String incomeCensus);
@@ -13,6 +13,7 @@ import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.club.model.ClubUser;
import com.budwk.app.zhgh.club.model.SysClub;
import com.budwk.app.zhgh.club.model.SysClubExamineRegister;
import com.budwk.app.zhgh.club.model.SysClubExamineRegisterDetailed;
@@ -50,6 +51,15 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineReg
super(dao);
}
@Override
public Double getClubMemberMoney(String clubId) {
int memberCount = dao().count(ClubUser.class, Cnd.where(ClubUser::getClubId, "=", clubId));
SysClub club = dao().fetch(SysClub.class, clubId);
// 社团或会费标准为空时按0计算,保证考核登记初始化能够正常完成。
Double due = club == null || club.getDue() == null ? 0D : club.getDue();
return memberCount * due;
}
@Override
public List<NutMap> getClubUserNum(String clubId, Integer year) {
Date yearStart = DateUtil.parse(year + "-01-01 00:00:00");
@@ -345,6 +355,40 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineReg
return listMap;
}
/**
* 两组明细均至少填写一条;本年度主办单位和获奖情况保持选填。
* @param examineRegister data参数绑定的登记实体,包含yearActivityList和plans两组JSON明细
* @return 校验通过返回null,否则返回首个缺失字段的行号和名称
*/
@Override
public String validateActivityRequired(SysClubExamineRegister examineRegister) {
if (examineRegister == null) {
return "考核登记信息不能为空";
}
String message = validateActivityRows(examineRegister.getYearActivityList(), "本年度活动开展情况",
new String[]{"activityTime", "activityName", "activityAddress", "joinNum"},
new String[]{"活动时间", "活动主题/赛事名称", "活动地点", "参加人数"});
return message != null ? message : validateActivityRows(examineRegister.getPlans(), "下一年度活动计划",
new String[]{"activityName", "activityUnit", "activityAddress"},
new String[]{"活动主题/赛事名称", "活动/赛事主办单位", "活动地点"});
}
/** 检查空列表、空行及纯空白字段;数值0按已填写处理,不扩大为人数范围校验。 */
private String validateActivityRows(List<JSONObject> rows, String label, String[] fields, String[] names) {
if (rows == null || rows.isEmpty()) {
return label + "至少填写一条";
}
for (int i = 0; i < rows.size(); i++) {
JSONObject row = rows.get(i);
for (int j = 0; j < fields.length; j++) {
if (row == null || StrUtil.isBlank(row.getStr(fields[j]))) {
return label + "第" + (i + 1) + "条:" + names[j] + "不能为空";
}
}
}
return null;
}
@Override
public SysClubExamineRegister doAdd(SysClubExamineRegister examineRegister, String incomeDetailed, String incomeCensus) {
// JSON转换为实体列表(空值判断避免转换异常)
@@ -606,6 +606,9 @@ public class TourMySignupController {
if (StrUtil.isBlank(ledger.getMobile())) {
return Result.error("手机号不能为空");
}
if (!ledger.getMobile().matches("^1[3-9]\\d{9}$")) {
return Result.error("请输入正确的11位手机号");
}
return null;
}
@@ -841,6 +841,9 @@ public class TourSignupController {
if (StrUtil.isBlank(ledger.getMobile())) {
return Result.error("手机号不能为空");
}
if (!ledger.getMobile().matches("^1[3-9]\\d{9}$")) {
return Result.error("请输入正确的11位手机号");
}
return null;
}
@@ -229,14 +229,18 @@ public class ProposalMineController {
}
/**
* 删除当前用户可删除的提案。
*
* @param id 提案 ID;仅允许删除本人未发起流程或退回发起节点的提案
* @return Result,成功时返回成功结果,不满足删除条件时返回原因
*/
@At
@SaCheckPermission("proposal.mine")
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "提案管理系统-我的提案", msg = "删除提案")
public Result delete(@Valid String id) {
dao.delete(ProposalInfo.class, id);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
return Result.success();
return proposalCommonService.deleteMine(id, SecurityUtil.getUserId());
}
@At
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.democratic.proposal.service.common;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
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.ProposalQueryComprehensiveParam;
@@ -19,6 +20,15 @@ import java.util.Set;
public interface ProposalCommonService extends BaseService<ProposalInfo> {
/**
* 删除当前用户尚未发起流程或已退回发起节点的提案。
*
* @param id 待删除的提案 ID
* @param userId 当前登录用户 ID,用于核对提案创建人
* @return Result,删除成功返回 success;提案不存在、非本人或流程不允许删除时返回错误信息
*/
Result deleteMine(String id, String userId);
/**
* 按页面允许的字段白名单添加排序条件,避免前端字段直接进入SQL。
*
@@ -17,6 +17,7 @@ import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
@@ -52,6 +53,7 @@ import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import lombok.extern.slf4j.Slf4j;
import org.ddr.poi.html.HtmlRenderPolicy;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.FieldFilter;
@@ -62,6 +64,7 @@ import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.ioc.aop.Aop;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
@@ -184,6 +187,45 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
super(dao);
}
/**
* 仅允许创建人删除草稿或退回发起节点的提案,并清理其 WF 流程。
*
* @param id 待删除的提案 ID
* @param userId 当前登录用户 ID,用于核对提案创建人
* @return Result,删除成功返回 success;提案不存在、非本人或流程不允许删除时返回错误信息
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public Result deleteMine(String id, String userId) {
if (StrUtil.isBlank(id) || StrUtil.isBlank(userId)) {
return Result.error("提案不存在或无权删除");
}
ProposalInfo proposal = dao().fetch(ProposalInfo.class, id);
if (proposal == null || !Objects.equals(proposal.getCreatedBy(), userId)) {
return Result.error("提案不存在或无权删除");
}
List<ProcessInstance> instances = dao().query(ProcessInstance.class,
Cnd.where(ProcessInstance::getBusinessNo, "=", id));
// 一个提案出现多个流程实例时无法安全判断当前节点,拒绝删除并保留数据。
if (instances.size() > 1) {
return Result.error("提案流程数据异常,无法删除");
}
if (!instances.isEmpty()) {
ProcessInstance instance = instances.get(0);
List<ProcessTask> tasks = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null);
// 存在并行任务时,只有所有活动任务均位于发起节点才允许删除。
if (!Objects.equals(instance.getState(), ProcessInstanceStateEnum.DOING.getCode())
|| tasks.isEmpty() || tasks.stream().anyMatch(task -> !Objects.equals(task.getTaskName(), "startTask"))) {
return Result.error("仅未提交或退回发起节点的提案可以删除");
}
}
dao().delete(ProposalInfo.class, id);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
return Result.success();
}
/**
* 分页查询提案综合列表。
* 普通查询不关联历史任务表;仅在 origins 包含 answer 且 commonKeyword 非空时,
@@ -28,7 +28,6 @@ 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.model.CondolenceType;
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.PictureRenderData;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -48,8 +47,6 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.List;
@@ -187,27 +184,12 @@ public class CondolenceMineController {
log.info("[职工慰问导出] 渲染前图片字段,applyId={}, instanceId={}, field={}, valueType={}", id, instance.getId(), field, value == null ? "null" : value.getClass().getName());
}
String fileName = "重庆工贸职业技术学院职工慰问申请表_" + StrUtil.nullToEmpty(condolence.getHelpUserName());
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc");
ByteArrayOutputStream templateOutput = new ByteArrayOutputStream();
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc")) {
if (input == null) throw new BaseException("职工慰问申请表模板不存在");
com.aspose.words.Document document = new com.aspose.words.Document(input);
document.save(templateOutput, com.aspose.words.SaveFormat.DOCX);
log.info("[职工慰问导出] 模板转换完成,applyId={}, templateBytes={}", id, templateOutput.size());
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateOutput.toByteArray())) {
XWPFTemplate.compile(templateInput).render(docData).writeAndClose(output);
}
log.info("[职工慰问导出] Word渲染完成,applyId={}, outputBytes={}", id, output.size());
if (Boolean.TRUE.equals(print)) {
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(output.toByteArray());
ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream()) {
new com.aspose.words.Document(wordInput).save(pdfOutput, com.aspose.words.SaveFormat.PDF);
log.info("[职工慰问导出] PDF转换完成,applyId={}, outputBytes={}", id, pdfOutput.size());
CommonDownloadUtil.download(fileName + ".pdf", pdfOutput.toByteArray(), response);
}
} else {
CommonDownloadUtil.download(fileName + ".docx", output.toByteArray(), response);
}
boolean pdf = Boolean.TRUE.equals(print);
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, null, pdf, "condolence:" + id);
log.info("[职工慰问导出] 渲染完成,applyId={}, format={}, outputBytes={}", id, pdf ? "PDF" : "Word", bytes.length);
CommonDownloadUtil.download(fileName + (pdf ? ".pdf" : ".docx"), bytes, response);
} catch (Exception e) {
log.error("[职工慰问导出] 模板渲染或文件输出失败,applyId={}, instanceId={}, format={}", id, instance.getId(), Boolean.TRUE.equals(print) ? "PDF" : "Word", e);
throw e;
@@ -6,17 +6,14 @@ 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.flow.enums.ProcessInstanceStateEnum;
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;
@@ -94,6 +91,7 @@ public class CondolenceReadController {
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
@@ -106,9 +104,6 @@ public class CondolenceReadController {
cnd.and(seg);
}
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CHAIRMAN.name())) {
cnd.and("info.applyUnionId", "=", SecurityUtil.getUnionId());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
@@ -142,20 +137,17 @@ public class CondolenceReadController {
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and(ProcessInstance::getState, "=", ProcessTaskStateEnum.FINISHED.getCode());
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.andEX("con.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("YEAR(con.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() + "%");
seg.or("con.helpUserName", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("con.helpLoginName", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CHAIRMAN.name())) {
cnd.and("con.applyUnionId", "=", SecurityUtil.getUnionId());
}
sql.setCondition(cnd);
List<NutMap> list = condolenceService.listMap(sql);
@@ -43,9 +43,9 @@ public class DifficultHelpApprovalController {
}
ProcessTask task = dao.fetch(ProcessTask.class, args.getLong(FlowConst.PROCESS_TASK_ID_KEY));
if (task == null) throw new BaseException("审批任务不存在");
boolean amountApproval = "36ebafda-44c2-4bd3-bd7f-10839a575a0d".equals(task.getTaskName())
|| "715ece82-4ae3-433c-ad18-ed875f45d1e3".equals(task.getTaskName());
if (!amountApproval && !"e405f96c-5fc0-4f63-b323-e69dfe853253".equals(task.getTaskName())) {
boolean amountApproval = "715ece82-4ae3-433c-ad18-ed875f45d1e3".equals(task.getTaskName());
if (!amountApproval && !"36ebafda-44c2-4bd3-bd7f-10839a575a0d".equals(task.getTaskName())
&& !"e405f96c-5fc0-4f63-b323-e69dfe853253".equals(task.getTaskName())) {
throw new BaseException("当前任务不是困难帮扶审批任务");
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
@@ -54,20 +54,23 @@ public class DifficultHelpApprovalController {
if (StrUtil.isBlank(args.getStr("tf_sign"))) throw new BaseException("请完成审批签字");
if (StrUtil.isBlank(args.getStr("tf_opinion"))) throw new BaseException("请填写审批意见");
String level = args.getStr("level");
args.remove("level");
args.remove("tf_level");
Double money = amountApproval ? args.getDouble("money") : null;
if (amountApproval) {
if (StrUtil.isBlank(level) || money == null || !Double.isFinite(money) || money < 0) {
throw new BaseException("请填写完整的级别和帮扶金额");
if (money == null || !Double.isFinite(money) || money < 0) {
throw new BaseException("请填写有效的帮扶金额");
}
args.put("tf_level", level);
args.put("tf_money", money);
} else {
args.remove("money");
args.remove("tf_money");
}
// 参与者权限由流程引擎校验,业务入口不接受客户端指定管理员代办。
args.remove(FlowConst.EXT_ENABLE_OPERATOR);
flowCommonService.executeTask(args);
if (amountApproval && Integer.valueOf(ProcessSubmitTypeEnum.AGREE.getCode()).equals(args.getInt(FlowConst.SUBMIT_TYPE))) {
dao.update(DifficultHelpInfo.class, Chain.make("level", level).add("money", money), Cnd.where("id", "=", info.getId()));
dao.update(DifficultHelpInfo.class, Chain.make("money", money), Cnd.where("id", "=", info.getId()));
}
return Result.success();
}
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.staffbenefit.difficulthelp.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
@@ -137,7 +138,7 @@ public class DifficultHelpMineController {
@At
@ApiModelProperty("获取困难补助申请")
@SaCheckPermission("difficultHelp.mine")
@SaCheckPermission(value = {"difficultHelp.mine", "difficultHelp.reading"}, mode = SaMode.OR)
public Result findOne(String id) {
DifficultHelpInfo helpInfo = dao.fetch(DifficultHelpInfo.class, id);
return Result.success().addData(helpInfo);
@@ -9,7 +9,6 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
@@ -18,7 +17,6 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.MoneyUtil;
import com.budwk.app.base.utils.OfficePlusUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
@@ -39,7 +37,6 @@ import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommon
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo;
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
@@ -74,8 +71,6 @@ import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@@ -159,7 +154,7 @@ public class DifficultHelpReadingController {
""");
Cnd cnd = Cnd.NEW();
cnd.and("ins.state", "=", 20);
pageParam.buildSearch(cnd, "info.");
pageParam.buildSearch(cnd, "info.", false);
cnd.groupBy("info.id");
sql.setCondition(cnd);
Pagination<NutMap> pagination = difficultHelpCommonService.listPageMap(pageParam.getPageNumber(), pageParam.getPageSize(), sql);
@@ -243,6 +238,7 @@ public class DifficultHelpReadingController {
@At
@Ok("void")
@SaCheckPermission("difficultHelp.reading")
public void doExportAllApply(DifficultHelpPageParam pageParam, HttpServletResponse response) throws Exception {
Sql sql = Sqls.create("""
SELECT
@@ -273,7 +269,7 @@ public class DifficultHelpReadingController {
$condition
""");
Cnd cnd = Cnd.NEW();
pageParam.buildSearch(cnd, "info.");
pageParam.buildSearch(cnd, "info.", false);
cnd.groupBy("info.id");
sql.setCondition(cnd);
List<NutMap> list = difficultHelpCommonService.listMap(sql);
@@ -319,9 +315,9 @@ public class DifficultHelpReadingController {
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder().bind("familyList", policy).build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("difficulty_apply"), config)
.render(docData)
.write(zipOutputStream);
try (InputStream input = sysOfficeTemplateUtil.getTemplate("difficulty_apply")) {
zipOutputStream.write(sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:batch:" + record.getString("id")));
}
zipOutputStream.closeEntry();
}
zipOutputStream.flush();
@@ -379,17 +375,18 @@ public class DifficultHelpReadingController {
String[] taskKeys = {"36ebafda-44c2-4bd3-bd7f-10839a575a0d", "715ece82-4ae3-433c-ad18-ed875f45d1e3", "e405f96c-5fc0-4f63-b323-e69dfe853253"};
String[] fields = {"firstApproval", "secondApproval", "thirdApproval"};
for (String field : fields) {
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, Cnd.where("businessNo", "=", businessId).desc("id"));
if (instance == null) return;
List<ProcessTask> tasks = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
if ("startTask".equals(task.getTaskName()) || (taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
// 重新申请后,仅导出本轮审批,避免混入上一次退回或撤销前的意见。
for (String field : fields) {
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
continue;
}
@@ -453,22 +450,9 @@ public class DifficultHelpReadingController {
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
XWPFTemplate.compile(getApplyTemplate(templateName),config)
.render(docData)
.writeAndClose(byteArrayOutputStream);
byte[] bytes = byteArrayOutputStream.toByteArray();
File sourceFile = File.createTempFile("file_convert_origin", ".docx");
Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE);
File pdfFile = File.createTempFile("file_convert", ".pdf");
OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath());
CommonDownloadUtil.download(fileName, IoUtil.readBytes(FileUtil.getInputStream(pdfFile)), response);
// 删除临时文件
FileUtil.del(sourceFile);
FileUtil.del(pdfFile.toPath());
} catch (IOException e) {
log.error("困难慰问申请表导出PDF失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
try (InputStream input = getApplyTemplate(templateName)) {
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, true, "difficulthelp:" + difficultHelp.getId());
CommonDownloadUtil.download(fileName, bytes, response);
}
}
@@ -478,18 +462,15 @@ public class DifficultHelpReadingController {
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
XWPFTemplate.compile(getApplyTemplate(templateName),config)
.render(docData)
.writeAndClose(byteArrayOutputStream);
CommonDownloadUtil.download(fileName, byteArrayOutputStream.toByteArray(), response);
} catch (IOException e) {
log.error("困难慰问申请表导出Word失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
try (InputStream input = getApplyTemplate(templateName)) {
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:" + difficultHelp.getId());
CommonDownloadUtil.download(fileName, bytes, response);
}
}
@At
@Ok("void")
@SaCheckPermission("difficultHelp.reading")
public void exportSummary(DifficultHelpPageParam pageParam,String flag,
HttpServletResponse response) throws Exception {
NutMap result = new NutMap();
@@ -519,7 +500,7 @@ public class DifficultHelpReadingController {
$condition
""");
Cnd cnd = Cnd.NEW();
pageParam.buildSearch(cnd, "info.");
pageParam.buildSearch(cnd, "info.", false);
cnd.groupBy("info.id");
sql.setCondition(cnd);
List<NutMap> list = difficultHelpCommonService.listMap(sql);
@@ -562,15 +543,13 @@ public class DifficultHelpReadingController {
throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage());
}
} else if ("docx".equals(flag)) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
try (InputStream input = sysOfficeTemplateUtil.getTemplate(resource)) {
Configure config = Configure.builder()
.bind("da", new LoopRowTableRenderPolicy())
.build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(resource), config)
.render(result)
.writeAndClose(out);
CommonDownloadUtil.download(fileName + ".docx", out.toByteArray(), response);
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, result, config, false, "difficulthelp:summary:" + resource);
CommonDownloadUtil.download(fileName + ".docx", bytes, response);
}
}
}
@@ -0,0 +1,255 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.suggestionBox.models.SuggestionBox;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
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.aop.Aop;
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 java.util.List;
/**
* @ClassName SpecialCondolenceApplyController
* @Author JyuHsin
* @Date 2025/8/20 15:40
* @Version 1.0
* @Description TODO
*/
@SaCheckRole("BRANCH_UNION_CHAIRMAN")
@IocBean
@At("/platform/special_condolence/apply")
@Api("特殊慰问申请")
@Ok("json:full")
public class SpecialCondolenceApplyController {
@Inject
private Dao dao;
@Inject
private FlowEngine flowEngine;
@Inject
private SpecialCondolenceService specialcondolenceService;
@Inject
private FlowCommonService flowCommonService;
@Inject
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/apply/index.html")
@SaCheckPermission("special_condolence.apply")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/apply/index.html")
@SaCheckPermission("h5.special_condolence.apply")
public void h5Index() {}
@At
@ApiOperation("保存申请")
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
@SLog(tag = "特殊慰问系统-慰问申请", msg = "保存申请")
public Result save(@Param("data") SpecialCondolence specialcondolence) {
if(StrUtil.isBlank(specialcondolence.getId())) specialcondolence.setCreateTime(DateUtil.now());
prepareApplication(specialcondolence);
specialcondolenceService.saveApplication(specialcondolence);
return Result.success();
}
@At
@ApiOperation("提交申请")
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
@SLog(tag = "特殊慰问系统-慰问申请", msg = "提交申请")
@Aop(TransAop.READ_COMMITTED)
public Result submit(@Param("data") SpecialCondolence specialcondolence) {
if (StrUtil.isBlank(specialcondolence.getRemark()) || specialcondolence.getRemark().trim().length() > 500) throw new com.budwk.app.base.exception.BaseException("请填写500字以内的申请事由");
if (StrUtil.isBlank(specialcondolence.getSignature())) throw new com.budwk.app.base.exception.BaseException("请完成申请人签字");
if(StrUtil.isBlank(specialcondolence.getId())) specialcondolence.setCreateTime(DateUtil.now());
prepareApplication(specialcondolence);
specialcondolenceService.saveApplication(specialcondolence);
// 开启流程实例
Dict args = Dict.create();
args.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode());
args.set(FlowConst.FORM_DATA, specialcondolence);
ProcessInstance instance = flowEngine.startProcessInstanceByKey("TSWW", specialcondolence.getId(), SecurityUtil.getUserId(), args);
// 自动完成第一个申请任务
List<ProcessTask> doingTaskList = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null);
for (ProcessTask task : doingTaskList) {
flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args);
}
return Result.success();
}
@At
@ApiOperation("重新提交申请")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
public Result submitAgain(@Param("data") SpecialCondolence specialcondolence, @Param("taskId") Long taskId) {
if (StrUtil.isBlank(specialcondolence.getRemark()) || specialcondolence.getRemark().trim().length() > 500) throw new com.budwk.app.base.exception.BaseException("请填写500字以内的申请事由");
if (StrUtil.isBlank(specialcondolence.getSignature())) throw new com.budwk.app.base.exception.BaseException("请完成申请人签字");
ProcessTask task = taskId == null ? null : dao.fetch(ProcessTask.class, taskId);
ProcessInstance instance = task == null ? null : dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null || !StrUtil.equals(instance.getBusinessNo(), specialcondolence.getId())
|| !"startTask".equals(task.getTaskName()) || !SecurityUtil.getUserId().equals(instance.getOperator())) {
throw new com.budwk.app.base.exception.BaseException("申请任务不匹配");
}
prepareApplication(specialcondolence);
specialcondolenceService.saveApplication(specialcondolence);
Dict dict = Dict.create();
dict.set(FlowConst.PROCESS_TASK_ID_KEY, taskId);
dict.set(FlowConst.FORM_DATA, specialcondolence);
dict.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.RE_APPLY.getCode());
flowCommonService.executeTask(dict);
return Result.success();
}
@At
@ApiOperation("查询用户")
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
public Object listUser(String keyword) {
Sql sql = Sqls.create("""
select
u.id,
u.idCard,
u.personType,
bank.bankOfDeposit,
bank.bankUserName,
bank.bankCardNumber,
username as userName,
loginname as loginName,
sex,
mobile,
technicalTitle,
IFNULL(unitname, '暂无') as unitName,
unitid as unitId,
unionid as unionId,
unionname as unionName
from
vw_user u
LEFT JOIN (
SELECT idCard, bankOfDeposit, bankUserName, bankCardNumber,
ROW_NUMBER() OVER (PARTITION BY idCard ORDER BY createdAt DESC, id DESC) rn
FROM special_condolence_recipient WHERE delFlag = false
) bank ON bank.idCard = u.idCard AND bank.rn = 1 AND u.idCard <> ''
$condition
""");
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(View_user::getLoginname, "like", "%" + keyword + "%");
seg.or(View_user::getUsername, "like", "%" + keyword + "%");
cnd.and(seg);
}
if (StrUtil.isBlank(SecurityUtil.getUnionId())) throw new com.budwk.app.base.exception.BaseException("申请人未关联分工会");
cnd.and("u.unionId", "=", SecurityUtil.getUnionId());
sql.setCondition(cnd);
Pagination pagination = specialcondolenceService.listPageMap(1, 50, sql);
return Result.success(pagination.getList());
}
@At
@Ok("void")
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
public void importTemplate(javax.servlet.http.HttpServletResponse response) throws Exception {
String templateCode = "special_condolence_import";
try (java.io.InputStream input = sysOfficeTemplateUtil.getTemplate(templateCode)) {
com.budwk.app.base.utils.CommonDownloadUtil.download(
sysOfficeTemplateUtil.getTemplateFileName(templateCode), input.readAllBytes(), response);
}
}
@At
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
@org.nutz.mvc.annotation.AdaptBy(type = org.nutz.mvc.upload.UploadAdaptor.class, args = {"ioc:fileUpload"})
public Result importRecipients(@Param("file") org.nutz.mvc.upload.TempFile file) throws Exception {
if (file == null) throw new com.budwk.app.base.exception.BaseException("请选择Excel文件");
if (!file.getSubmittedFileName().toLowerCase(java.util.Locale.ROOT).matches(".*\\.(xlsx|xls)$")) throw new com.budwk.app.base.exception.BaseException("仅支持xlsx、xls文件");
String[] headers = {"慰问对象", "工号", "身份证号码", "开户行", "户名", "收款账户"};
String[] fields = {"helpUserName", "helpLoginName", "idCard", "bankOfDeposit", "bankUserName", "bankCardNumber"};
java.util.List<cn.hutool.json.JSONObject> result = new java.util.ArrayList<>();
try (java.io.InputStream input = new java.io.FileInputStream(file.getFile());
org.apache.poi.ss.usermodel.Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(input)) {
if (workbook.getNumberOfSheets() == 0) throw new com.budwk.app.base.exception.BaseException("Excel中没有工作表");
org.apache.poi.ss.usermodel.Sheet sheet = workbook.getSheetAt(0);
org.apache.poi.ss.usermodel.DataFormatter formatter = new org.apache.poi.ss.usermodel.DataFormatter();
org.apache.poi.ss.usermodel.Row header = sheet.getRow(0);
for (int c = 0; c < headers.length; c++) {
if (header == null || !headers[c].equals(formatter.formatCellValue(header.getCell(c)).trim())) throw new com.budwk.app.base.exception.BaseException("表头不正确,请下载导入模板");
}
if (sheet.getLastRowNum() > 1000) throw new com.budwk.app.base.exception.BaseException("最多导入1000行");
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
org.apache.poi.ss.usermodel.Row excelRow = sheet.getRow(r);
if (excelRow == null) continue;
cn.hutool.json.JSONObject row = new cn.hutool.json.JSONObject();
boolean empty = true;
for (int c = 0; c < fields.length; c++) {
org.apache.poi.ss.usermodel.Cell cell = excelRow.getCell(c);
String value = formatter.formatCellValue(cell).trim();
if (c != 1 && cell != null && cell.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) throw new com.budwk.app.base.exception.BaseException("第" + (r + 1) + "行不支持公式,请填写实际值");
if ((c == 2 || c == 5) && cell != null && cell.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC) throw new com.budwk.app.base.exception.BaseException("第" + (r + 1) + "行工号、身份证和收款账户请使用文本格式,避免数字失真");
row.set(fields[c], value);
empty = empty && value.isEmpty();
}
if (empty) continue;
for (int c = 0; c < fields.length; c++) {
if (c != 1 && row.getStr(fields[c]).isEmpty()) throw new com.budwk.app.base.exception.BaseException("第" + (r + 1) + "行:请填写" + headers[c]);
}
if (!com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceRecipients.validIdCard(row.getStr("idCard"))) throw new com.budwk.app.base.exception.BaseException("第" + (r + 1) + "行身份证号码格式不正确");
if (!row.getStr("bankCardNumber").matches("[0-9]{6,30}")) throw new com.budwk.app.base.exception.BaseException("第" + (r + 1) + "行收款账户格式不正确");
com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceRecipients.resolveUser(row, dao, "第" + (r + 1) + "行:");
result.add(row);
}
}
if (result.isEmpty()) throw new com.budwk.app.base.exception.BaseException("没有可导入的数据");
return Result.success(result);
}
private void prepareApplication(SpecialCondolence info) {
if (StrUtil.isNotBlank(info.getId())) {
SpecialCondolence existing = dao.fetch(SpecialCondolence.class, info.getId());
if (existing == null || !SecurityUtil.getUserId().equals(existing.getCreatedBy())) {
throw new com.budwk.app.base.exception.BaseException("只能修改本人申请");
}
info.setCreatedBy(existing.getCreatedBy());
}
info.setApplyUserId(SecurityUtil.getUserId());
info.setApplyUserName(SecurityUtil.getUserUsername());
info.setApplyLoginName(SecurityUtil.getUserLoginname());
info.setApplyUnionId(SecurityUtil.getUnionId());
info.setApplyUnitId(SecurityUtil.getUnitId());
}
}
@@ -0,0 +1,60 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
@IocBean
@Ok("json")
@At("/platform/special_condolence/approval")
public class SpecialCondolenceApprovalController {
@Inject
private Dao dao;
@Inject
private FlowCommonService flowCommonService;
@At
@SaCheckLogin
@Aop(TransAop.READ_COMMITTED)
public Result executeTask(@Param("data") String data) {
Dict args = Json.fromJson(Dict.class, data);
if (args == null || args.getLong(FlowConst.PROCESS_TASK_ID_KEY) == null) {
throw new BaseException("审批任务不能为空");
}
ProcessTask task = dao.fetch(ProcessTask.class, args.getLong(FlowConst.PROCESS_TASK_ID_KEY));
if (task == null) throw new BaseException("审批任务不存在");
if (!"c438972e-2f67-4907-b229-cb7aa17da147".equals(task.getTaskName())
&& !"7f560675-aeea-4254-a57e-957d03d6b786".equals(task.getTaskName())
&& !"af363a4e-fc56-42a0-9906-8c7a2814006b".equals(task.getTaskName())) {
throw new BaseException("当前任务不是特殊慰问审批任务");
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
com.budwk.app.flow.entity.ProcessDefine define = instance == null ? null : dao.fetch(com.budwk.app.flow.entity.ProcessDefine.class, instance.getProcessDefineId());
SpecialCondolence info = define == null || !"TSWW".equals(define.getName()) ? null : dao.fetch(SpecialCondolence.class, instance.getBusinessNo());
if (info == null) throw new BaseException("特殊慰问申请不存在");
if (StrUtil.isBlank(args.getStr("tf_sign"))) throw new BaseException("请完成审批签字");
if (StrUtil.isBlank(args.getStr("tf_opinion"))) throw new BaseException("请填写审批意见");
// 参与者权限由流程引擎校验,业务入口不接受客户端指定管理员代办。
args.remove(FlowConst.EXT_ENABLE_OPERATOR);
flowCommonService.executeTask(args);
return Result.success();
}
}
@@ -0,0 +1,168 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
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.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
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;
/**
* @ClassName SpecialCondolenceFourthApprovalController
* @Author JyuHsin
* @Date 2025/7/29 16:23
* @Version 1.0
* @Description TODO
*/
@IocBean
@At("/platform/special_condolence/fourthApproval")
@Api("特殊慰问四级审核")
@Ok("json:full")
public class SpecialCondolenceFourthApprovalController {
@Inject
private Dao dao;
@Inject
private SpecialCondolenceService specialcondolenceService;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/fourthApproval/index.html")
@SaCheckPermission("special_condolence.fourthApproval")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/fourthApproval/index.html")
@SaCheckPermission("h5.special_condolence.fourthApproval")
public void h5Index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"special_condolence.fourthApproval", "h5.special_condolence.fourthApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
COALESCE(recipientTotals.recipientCount, 0) AS recipientCount,
type.name as typeName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke
FROM
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN special_condolence info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN (
SELECT applyId, COUNT(*) AS recipientCount
FROM special_condolence_recipient
WHERE delFlag = false
GROUP BY applyId
) recipientTotals ON recipientTotals.applyId = info.id
LEFT JOIN special_condolence_type type ON info.type = type.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("info.id", "is not", null);
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolence::getHelpLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
cnd.and("t.taskName", "=", "af363a4e-fc56-42a0-9906-8c7a2814006b");
if (!"superadmin".equals(SecurityUtil.getUserLoginname()) && !AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name())) {
Sql unitSql = Sqls.queryString("""
SELECT DISTINCT ur.unitId
FROM sys_user_role ur
INNER JOIN sys_role r ON r.id = ur.roleId
WHERE ur.userId = @userId
AND ur.enable = true
AND r.disabled = false
AND r.code = @roleCode
AND ur.unitId IS NOT NULL
AND ur.unitId <> ''
""");
unitSql.setParam("userId", SecurityUtil.getUserId());
unitSql.setParam("roleCode", RoleConstant.UNIT_SCHOOL_LEADER.name());
dao.execute(unitSql);
List<String> managedUnitIds = unitSql.getList(String.class);
if (managedUnitIds.isEmpty()) {
cnd.and(new Static("1 = 0"));
} else {
cnd.and("info.applyUnitId", "in", managedUnitIds);
}
}
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
}
@@ -0,0 +1,303 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
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.sms.SmsService;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import com.deepoove.poi.data.PictureRenderData;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
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.dao.util.cri.SqlExpressionGroup;
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.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.List;
/**
* @ClassName SpecialCondolenceApplyController
* @Author JyuHsin
* @Date 2025/7/28 11:06
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
@At("/platform/special_condolence/mine")
@Api("特殊慰问我的")
@Ok("json:full")
public class SpecialCondolenceMineController {
@Inject
private SpecialCondolenceService specialcondolenceService;
@Inject
private FlowEngine flowEngine;
@Inject
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/mine/index.html")
@SaCheckPermission("special_condolence.mine")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/mine/index.html")
@SaCheckPermission("h5.special_condolence.mine")
public void h5Index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"special_condolence.mine", "h5.special_condolence.mine"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type) {
Sql sql = Sqls.create("""
SELECT
info.*,
COALESCE(recipientTotals.recipientCount, 0) AS recipientCount,
type.name as typeName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = t.taskParentId) = 'startTask', 1, 0) canRevoke,
(select MAX(id) from wf_process_task where processInstanceId = ins.id and taskName = 'startTask') AS startTaskId
FROM
special_condolence info
LEFT JOIN (
SELECT applyId, COUNT(*) AS recipientCount
FROM special_condolence_recipient
WHERE delFlag = false
GROUP BY applyId
) recipientTotals ON recipientTotals.applyId = info.id
LEFT JOIN special_condolence_type type ON info.type = type.id
LEFT JOIN `wf_process_instance` ins ON ins.businessNo = info.id
LEFT JOIN (
SELECT
id,
processInstanceId,
taskName,
displayName,
taskType,
performType,
taskState,
finishTime,
taskParentId,
variable,
ROW_NUMBER() OVER (PARTITION BY processInstanceId ORDER BY createdAt DESC, id DESC) AS rn
FROM wf_process_task
WHERE taskState = 10
) t ON t.processInstanceId = ins.id AND t.rn = 1
$condition
""");
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolence::getHelpLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("year(info.createTime)", "=", year);
cnd.andEX("info.type", "=", type);
cnd.and("info.createdBy", "=", SecurityUtil.getUserId());
if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
} else {
cnd.desc("info.createTime");
}
sql.setCondition(cnd);
Pagination<NutMap> pagination = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@Ok("void")
@ApiOperation("导出特殊慰问申请表")
@SaCheckPermission("special_condolence.mine")
public void doExportApply(String id, HttpServletResponse response, Boolean print) throws Exception {
log.info("[特殊慰问导出] 开始,applyId={}, userId={}, format={}", id, SecurityUtil.getUserId(), Boolean.TRUE.equals(print) ? "PDF" : "Word");
SpecialCondolence specialcondolence = specialcondolenceService.loadApplication(id);
if (specialcondolence == null) throw new BaseException("该条记录不存在");
if (!SecurityUtil.getUserId().equals(specialcondolence.getCreatedBy())) {
throw new BaseException("只能导出本人提交的申请");
}
ProcessInstance instance = specialcondolenceService.dao().fetch(ProcessInstance.class,
Cnd.where("businessNo", "=", id).desc("id"));
if (instance == null || !ProcessInstanceStateEnum.FINISHED.getCode().equals(instance.getState())) {
throw new BaseException("只能导出已完成的申请");
}
log.info("[特殊慰问导出] 流程已完成,applyId={}, instanceId={}, state={}, applicantSignPresent={}", id, instance.getId(), instance.getState(), StrUtil.isNotBlank(specialcondolence.getSignature()));
NutMap docData = Lang.obj2nutmap(specialcondolence);
docData.put("payUserName", StrUtil.nullToEmpty(specialcondolence.getBankUserName()));
SpecialCondolenceType type = specialcondolenceService.dao().fetch(SpecialCondolenceType.class, specialcondolence.getType());
docData.put("typeName", type == null ? "" : type.getName());
docData.put("signature", null);
if (StrUtil.isNotBlank(specialcondolence.getSignature())) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, specialcondolence.getSignature());
if (signature == null) throw new BaseException("申请人签字图片读取失败");
docData.put("signature", signature);
}
fillApplyApprovals(docData, instance);
for (String field : List.of("signature", "secondApprovalSign", "thirdApprovalSign", "fourthApprovalSign")) {
Object value = docData.get(field);
log.info("[特殊慰问导出] 渲染前图片字段,applyId={}, instanceId={}, field={}, valueType={}", id, instance.getId(), field, value == null ? "null" : value.getClass().getName());
}
String fileName = "重庆工贸职业技术学院特殊慰问申请表_" + StrUtil.nullToEmpty(specialcondolence.getHelpUserName());
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc")) {
if (input == null) throw new BaseException("特殊慰问申请表模板不存在");
boolean pdf = Boolean.TRUE.equals(print);
com.aspose.words.Document template = new com.aspose.words.Document(input);
template.getRange().replace("职工慰问", "特殊慰问", new com.aspose.words.FindReplaceOptions());
com.aspose.words.NodeCollection rows = template.getChildNodes(com.aspose.words.NodeType.ROW, true);
for (int i = rows.getCount() - 1; i >= 0; i--) {
com.aspose.words.Node row = rows.get(i);
if (row.getText().contains("firstApproval") || row.getText().contains("分工会主席")) row.remove();
}
java.io.ByteArrayOutputStream templateOutput = new java.io.ByteArrayOutputStream();
template.save(templateOutput, com.aspose.words.SaveFormat.DOCX);
byte[] bytes;
if (specialcondolence.getRecipients() != null && !specialcondolence.getRecipients().isEmpty()) {
com.aspose.words.Document combined = null;
for (cn.hutool.json.JSONObject person : specialcondolence.getRecipients()) {
NutMap personData = new NutMap();
personData.putAll(docData);
personData.putAll(person);
personData.put("payUserName", person.getStr("bankUserName"));
byte[] rendered = sysOfficeTemplateUtil.renderTemplate(new java.io.ByteArrayInputStream(templateOutput.toByteArray()), personData, null, false, "specialcondolence:" + id);
com.aspose.words.Document part = new com.aspose.words.Document(new java.io.ByteArrayInputStream(rendered));
if (combined == null) combined = part;
else combined.appendDocument(part, com.aspose.words.ImportFormatMode.KEEP_SOURCE_FORMATTING);
}
java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream();
combined.save(output, pdf ? com.aspose.words.SaveFormat.PDF : com.aspose.words.SaveFormat.DOCX);
bytes = output.toByteArray();
} else {
bytes = sysOfficeTemplateUtil.renderTemplate(new java.io.ByteArrayInputStream(templateOutput.toByteArray()), docData, null, pdf, "specialcondolence:" + id);
}
log.info("[特殊慰问导出] 渲染完成,applyId={}, format={}, outputBytes={}", id, pdf ? "PDF" : "Word", bytes.length);
CommonDownloadUtil.download(fileName + (pdf ? ".pdf" : ".docx"), bytes, response);
} catch (Exception e) {
log.error("[特殊慰问导出] 模板渲染或文件输出失败,applyId={}, instanceId={}, format={}", id, instance.getId(), Boolean.TRUE.equals(print) ? "PDF" : "Word", e);
throw e;
}
}
private void fillApplyApprovals(NutMap docData, ProcessInstance instance) {
String[] taskKeys = {"c438972e-2f67-4907-b229-cb7aa17da147",
"7f560675-aeea-4254-a57e-957d03d6b786", "af363a4e-fc56-42a0-9906-8c7a2814006b"};
String[] fields = {"secondApproval", "thirdApproval", "fourthApproval"};
List<String> suffixes = List.of("Opinion", "Sign", "Year", "Month", "Day");
for (String field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
List<ProcessTask> tasks = specialcondolenceService.dao().query(ProcessTask.class,
Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
log.info("[特殊慰问导出] 查询审批记录,applyId={}, instanceId={}, taskCount={}, expectedTaskKeys={}", instance.getBusinessNo(), instance.getId(), tasks.size(), List.of(taskKeys));
for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
log.info("[特殊慰问导出] 审批记录,instanceId={}, taskId={}, taskKey={}, displayName={}, state={}, finishTime={}, isFirstTaskNode={}, matchedApproval={}", instance.getId(), task.getId(), task.getTaskName(), task.getDisplayName(), task.getTaskState(), task.getFinishTime(), taskVO.getExt() == null ? null : taskVO.getExt().get("isFirstTaskNode"), List.of(taskKeys).contains(task.getTaskName()));
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
log.info("[特殊慰问导出] 申请节点重置本轮审批字段,instanceId={}, taskId={}, taskKey={}", instance.getId(), task.getId(), task.getTaskName());
// 重新申请后,仅导出本轮审批记录。
for (String field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
continue;
}
for (int i = 0; i < taskKeys.length; i++) {
if (!taskKeys[i].equals(task.getTaskName())) continue;
for (String suffix : suffixes) docData.put(fields[i] + suffix, "Sign".equals(suffix) ? null : "");
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) {
log.info("[特殊慰问导出] 跳过未完成审批,instanceId={}, taskId={}, field={}, state={}", instance.getId(), task.getId(), fields[i], task.getTaskState());
continue;
}
Dict data = taskVO.getExt() == null ? Dict.create() : taskVO.getTaskFormData();
log.info("[特殊慰问导出] 读取审批表单,instanceId={}, taskId={}, field={}, formKeys={}, signPresent={}, signReferenceLength={}", instance.getId(), task.getId(), fields[i], data.keySet(), StrUtil.isNotBlank(data.getStr("sign")), StrUtil.nullToEmpty(data.getStr("sign")).length());
if (StrUtil.isBlank(data.getStr("sign"))) log.warn("[特殊慰问导出] 已完成审批缺少签名字段,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i]);
docData.put(fields[i] + "Opinion", StrUtil.nullToEmpty(data.getStr("opinion")));
if (StrUtil.isNotBlank(data.getStr("sign"))) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign"));
if (signature == null) throw new BaseException("审批签字图片读取失败");
docData.put(fields[i] + "Sign", signature);
log.info("[特殊慰问导出] 审批签名填充完成,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i] + "Sign");
}
docData.put(fields[i] + "Year", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy"));
docData.put(fields[i] + "Month", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "M"));
docData.put(fields[i] + "Day", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "d"));
}
}
}
@At
@ApiOperation("删除")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"special_condolence.mine", "h5.special_condolence.mine"}, mode = SaMode.OR)
@SLog(tag = "特殊慰问系统-我的申请", msg = "删除特殊慰问")
public Result delete(@Param("id") String id) {
specialcondolenceService.deleteApplication(id);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
return Result.success();
}
@At
@ApiOperation("查询单个慰问申请信息")
@SaCheckPermission(value = {"special_condolence", "h5.special_condolence"}, mode = SaMode.OR)
public Result info(String id) {
SpecialCondolence specialcondolence = specialcondolenceService.loadApplication(id);
SpecialCondolenceType type = specialcondolenceService.dao().fetch(SpecialCondolenceType.class, Cnd.where(SpecialCondolenceType::getId, "=", specialcondolence.getType()));
NutMap nutMap = Lang.obj2nutmap(specialcondolence);
nutMap.put("typeName", type == null ? "" : type.getName());
return Result.success(nutMap);
}
@Inject
private SmsService smsService;
}
@@ -0,0 +1,179 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
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.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.ProcessInstanceStateEnum;
import com.budwk.app.sys.param.SysMsgSummaryPageForm;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
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;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName SpecialCondolenceReadController
* @Author JyuHsin
* @Date 2025/7/29 16:25
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "特殊慰问阅览")
@At("/platform/special_condolence/read")
public class SpecialCondolenceReadController {
@Inject
private SpecialCondolenceService specialcondolenceService;
@At("")
@SaCheckPermission("special_condolence.read")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/read/index.html")
public void index() {
}
@At
@ApiOperation("分页查询")
@SaCheckPermission("special_condolence.read")
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@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
special_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 special_condolence_type type ON info.type = type.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
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 (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 = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
@At
@Ok("void")
@SaCheckPermission("special_condolence.read")
@ApiOperation("导出特殊慰问汇总表")
public void onExport(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
HttpServletResponse response) {
//查询审核通过的数据
Sql sql = Sqls.create("""
select
con.subject,
case when recipient.id is null then con.helpUserName else recipient.helpUserName end as helpUserName,
case when recipient.id is null then con.helpLoginName else recipient.helpLoginName end as helpLoginName,
con.applyUserName,
con.helpUnionName,
con.helpUnitName,
case when recipient.id is null then type.name else recipient.typeName end as typeName,
case when recipient.id is null then con.money else recipient.money end as money,
con.remark
from
special_condolence con
left join special_condolence_recipient recipient on recipient.applyId = con.id and recipient.delFlag = false
left join wf_process_instance ins on ins.businessNo = con.id
left join special_condolence_type type on type.id = con.type
$condition
order by con.createTime desc, con.id, recipient.sortNum, recipient.id
""");
Cnd cnd = Cnd.NEW();
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.andEX("con.type", "=", type);
cnd.andEX("YEAR(con.createTime)", "=", year);
cnd.andEX("con.applyUnionId", "=", unionId);
cnd.andEX("con.applyUnitId", "=", unitId);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("con.helpUserName", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("con.helpLoginName", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
sql.setCondition(cnd);
List<NutMap> list = specialcondolenceService.listMap(sql);
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("事项", "subject", 30));
exportEntities.add(new ExcelExportEntity("慰问对象", "helpUserName", 20));
exportEntities.add(new ExcelExportEntity("慰问对象工号", "helpLoginName", 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("申请理由", "remark", 20));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
CommonDownloadUtil.download("申报人汇总.xlsx", workbook, response);
}
}
@@ -0,0 +1,140 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
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.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
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 2025/12/23 14:13
* @description 校工会预审
*/
@IocBean
@At("/platform/special_condolence/schoolPrincipalApproval")
@Api("特殊慰问二级审核")
@Ok("json:full")
public class SpecialCondolenceSecondApprovalController {
@Inject
private SpecialCondolenceService specialcondolenceService;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/schoolPrincipalApproval/index.html")
@SaCheckPermission("special_condolence.secondApproval")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/schoolPrincipalApproval/index.html")
@SaCheckPermission("h5.special_condolence.secondApproval")
public void h5Index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"special_condolence.secondApproval", "h5.special_condolence.secondApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
COALESCE(recipientTotals.recipientCount, 0) AS recipientCount,
type.name as typeName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke
FROM
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN special_condolence info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN (
SELECT applyId, COUNT(*) AS recipientCount
FROM special_condolence_recipient
WHERE delFlag = false
GROUP BY applyId
) recipientTotals ON recipientTotals.applyId = info.id
LEFT JOIN special_condolence_type type ON info.type = type.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("info.id", "is not", null);
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolence::getHelpLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
cnd.and("t.taskName", "=", "c438972e-2f67-4907-b229-cb7aa17da147");
// cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
}
@@ -0,0 +1,144 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
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.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
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;
/**
* @ClassName SpecialCondolenceThirdApprovalController
* @Author JyuHsin
* @Date 2025/7/29 16:23
* @Version 1.0
* @Description TODO
*/
@IocBean
@At("/platform/special_condolence/schoolUnionApproval")
@Api("特殊慰问三级审核")
@Ok("json:full")
public class SpecialCondolenceThirdApprovalController {
@Inject
private SpecialCondolenceService specialcondolenceService;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/schoolUnionApproval/index.html")
@SaCheckPermission("special_condolence.thirdApproval")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/schoolUnionApproval/index.html")
@SaCheckPermission("h5.special_condolence.thirdApproval")
public void h5Index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"special_condolence.thirdApproval", "h5.special_condolence.thirdApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
COALESCE(recipientTotals.recipientCount, 0) AS recipientCount,
type.name as typeName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke
FROM
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN special_condolence info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN (
SELECT applyId, COUNT(*) AS recipientCount
FROM special_condolence_recipient
WHERE delFlag = false
GROUP BY applyId
) recipientTotals ON recipientTotals.applyId = info.id
LEFT JOIN special_condolence_type type ON info.type = type.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("info.id", "is not", null);
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolence::getHelpLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
cnd.and("t.taskName", "=", "7f560675-aeea-4254-a57e-957d03d6b786");
if (!"superadmin".equals(SecurityUtil.getUserLoginname())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
}
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
}
@@ -0,0 +1,91 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
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.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceTypeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.util.cri.SqlExpressionGroup;
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 java.util.List;
/**
* @ClassName SpecialCondolenceTypeController
* @Author JyuHsin
* @Date 2025/7/29 10:00
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "特殊慰问类型")
@At("/platform/special_condolence/type")
public class SpecialCondolenceTypeController {
@Inject
private Dao dao;
@Inject
private SpecialCondolenceTypeService typeService;
@At("")
@SaCheckPermission("special_condolence.type")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/type/index.html")
public void index() {
}
@At
@ApiOperation("分页查询")
@SaCheckPermission("special_condolence.type")
public Result pageData(PageForm pageForm) {
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolenceType::getName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolenceType::getCode, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.asc("code");
Pagination pagination = typeService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
@At
@ApiOperation("新增/修改特殊慰问类型")
@SaCheckPermission("special_condolence.type")
@SLog(tag = "特殊慰问系统-慰问类型", msg = "新增/修改特殊慰问类型")
public Object onSubmit(SpecialCondolenceType type) {
typeService.insertOrUpdate(type);
return Result.success();
}
@At
@ApiOperation("删除特殊慰问类型")
@SaCheckPermission("special_condolence.type")
@SLog(tag = "特殊慰问系统-慰问类型", msg = "删除特殊慰问类型")
public Object onDelete(String id) {
typeService.delete(id);
return Result.success();
}
@At
@ApiOperation("查询特殊慰问类型")
@SaCheckLogin
public Result querySpecialCondolenceType() {
List<SpecialCondolenceType> list = typeService.query(Cnd.where(SpecialCondolenceType::getEnable, "=", true).asc(SpecialCondolenceType::getCode));
return Result.success(list);
}
}
@@ -0,0 +1,46 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.interceptor;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowInterceptor;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.zhgh.dayofficework.article.models.Article;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.json.Json;
/**
* @ClassName SpecialCondolenceSaveInterceptor
* @Author JyuHsin
* @Date 2025/7/28 11:01
* @Version 1.0
* @Description TODO
*/
public class SpecialCondolenceSaveInterceptor implements FlowInterceptor {
@Override
public void intercept(Execution execution) {
if (!com.budwk.app.web.commons.auth.utils.AuthUtil.hasRoleOr("BRANCH_UNION_CHAIRMAN")) {
throw new com.budwk.app.base.exception.BaseException("仅分工会主席可发起特殊慰问申请");
}
String formDataStr = execution.getArgs().getStr(FlowConst.FORM_DATA);
SpecialCondolence specialcondolence = Json.fromJson(SpecialCondolence.class, formDataStr);
if(StrUtil.isBlank(specialcondolence.getId())) specialcondolence.setCreateTime(DateUtil.now());
Dao dao = ServiceContext.find(Dao.class);
ServiceContext.find(com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService.class).saveApplication(specialcondolence);
// 设置流程变量
execution.getArgs().set("origin", specialcondolence.getOrigin());
int instanceId = execution.getArgs().getInt(FlowConst.PROCESS_INSTANCE_ID_KEY);
dao.update(ProcessInstance.class, Chain.make("businessNo", specialcondolence.getId()), Cnd.where(ProcessInstance::getId, "=", instanceId));
}
}
@@ -0,0 +1,310 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.model;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.model.BaseModel;
import com.budwk.app.sys.models.Sys_file;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.List;
/**
* @ClassName SpecialCondolence
* @Author JyuHsin
* @Date 2025/7/28 11:03
* @Version 1.0
* @Description TODO
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("special_condolence")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("特殊慰问")
public class SpecialCondolence extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("事项")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String subject;
@Column
@Comment("申请人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyUserId;
@Column
@Comment("申请人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String applyUserName;
@Column
@Comment("申请人工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String applyLoginName;
@Column
@Comment("申请人分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyUnionId;
@Column
@Comment("申请人分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String applyUnionName;
@Column
@Comment("申请人单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyUnitId;
@Column
@Comment("申请人单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String applyUnitName;
@Column
@Comment("补助人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String helpUserId;
@Column
@Comment("补助人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String helpUserName;
@Column
@Comment("补助人工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String helpLoginName;
@Column
@Comment("补助人分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String helpUnionId;
@Column
@Comment("补助人分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String helpUnionName;
@Column
@Comment("补助人单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String helpUnitId;
@Column
@Comment("补助人单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String helpUnitName;
@Column
@Comment("类型")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String origin;
@Column
@Comment("收款人")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String payUserId;
@Column
@Comment("收款人姓名")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String payUserName;
@Column
@Comment("收款人工号")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String payLoginName;
@Column
@Comment("慰问类型")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String type;
@Column
@Comment("慰问编码")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String typeCode;
@Column
@Comment("慰问方式")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String way;
@Column
@Comment("统一每人慰问金额")
@ColDefine(customType = "decimal(10,2)")
private java.math.BigDecimal perMoney;
@Column
@Comment("慰问金额")
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
private Double money;
@Column
@Comment("发生时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String occurTime;
@Column
@Comment("生育几孩(孩次)")
@ColDefine(type = ColType.VARCHAR, width = 10)
private String child;
@Column
@Comment("所在医院")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String hospital;
@Column
@Comment("病房床号")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String hospitalHouseNum;
@Column
@Comment("病房床号")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String hospitalHouseBedNum;
@Column
@Comment("住院病由")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String hospitalBy;
@Column
@Comment("备注")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String remark;
@Column
@Comment("附件")
@ColDefine(type = ColType.MYSQL_JSON)
private List<JSONObject> files;
@Comment("签字")
@ColDefine(type = ColType.VARCHAR, width = 255)
@Column(hump = true)
private String signature;
@Column
@Comment("申请时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String createTime;
@Column
@Comment("户名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankUserName;
@Column
@Comment("银行账号")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankCardNumber;
@Column
@Comment("开户行")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankOfDeposit;
@Column
@Comment("证明人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String certifierUserId;
@Column
@Comment("证明人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String certifierUserName;
@Column
@Comment("证明人工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String certifierLoginName;
@Column
@Comment("证明人分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String certifierUnionId;
@Column
@Comment("证明人分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String certifierUnionName;
@Column
@Comment("证明人单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String certifierUnitId;
@Column
@Comment("证明人单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String certifierUnitName;
@Column
@Comment("经办人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String handlerUserId;
@Column
@Comment("经办人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String handlerUserName;
@Column
@Comment("经办人工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String handlerLoginName;
@Column
@Comment("经办人分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String handlerUnionId;
@Column
@Comment("经办人分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String handlerUnionName;
@Column
@Comment("经办人单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String handlerUnitId;
@Column
@Comment("经办人单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String handlerUnitName;
@Column
@Comment("入院时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String hospitalizationTime;
@Column
@Comment("出院时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String leaveHospitalTime;
@Column
@Comment("当年第几次住院")
@ColDefine(type = ColType.INT)
private Integer thisYearHospitalizationNum;
@Comment("慰问对象明细,由明细表加载")
private List<JSONObject> recipients;
}
@@ -0,0 +1,100 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
@Data
@EqualsAndHashCode(callSuper = true)
@Table("special_condolence_recipient")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@TableIndexes({@Index(name = "idx_special_condolence_recipient_apply", fields = {"applyId"}, unique = false)})
@Comment("特殊慰问被申请人明细")
public class SpecialCondolenceRecipient extends BaseModel {
@Name
@PrevInsert(uu32 = true)
@ColDefine(type = ColType.VARCHAR, width = 32)
private String id;
@Column
@Comment("申请ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyId;
@Column
@Comment("慰问对象用户ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String helpUserId;
@Column
@Comment("人员类型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String personType;
@Column
@Comment("慰问对象工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String helpLoginName;
@Column
@Comment("慰问对象")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String helpUserName;
@Column
@Comment("慰问类型ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String type;
@Column
@Comment("慰问类型名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String typeName;
@Column
@Comment("慰问类型编码")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String typeCode;
@Column
@Comment("慰问方式")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String way;
@Column
@Comment("身份证号码")
@ColDefine(type = ColType.VARCHAR, width = 18)
private String idCard;
@Column
@Comment("开户行")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankOfDeposit;
@Column
@Comment("户名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankUserName;
@Column
@Comment("收款账户")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String bankCardNumber;
@Column
@Comment("慰问金额")
@ColDefine(customType = "decimal(10,2)")
private java.math.BigDecimal money;
@Column
@Comment("慰问时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String occurTime;
@Column
@Comment("行序号")
@ColDefine(type = ColType.INT)
private Integer sortNum;
}
@@ -0,0 +1,84 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.DB;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
/**
* @ClassName SpecialCondolenceType
* @Author JyuHsin
* @Date 2025/7/29 9:59
* @Version 1.0
* @Description TODO
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("special_condolence_type")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("特殊慰问类型")
public class SpecialCondolenceType extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("编码")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String code;
@Column
@Comment("名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String name;
@Column
@Comment("费用")
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
private Double money;
@Column
@Comment("慰问方式")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String way;
@Column
@Comment("是否启用")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enable;
@Column
@Comment("是否上传附件")
@ColDefine(type = ColType.BOOLEAN)
private Boolean isUploadFile;
@Column
@Comment("上传附件说明")
@ColDefine(type = ColType.VARCHAR,width = 50)
private String uploadFileDesc;
@Column
@Comment("排序字段")
@Prev({
@SQL(db = DB.MYSQL, value = "SELECT IFNULL(MAX(location),0)+1 FROM special_condolence_type"),
@SQL(db = DB.ORACLE, value = "SELECT COALESCE(MAX(location),0)+1 FROM special_condolence_type")
})
private Integer location;
@Column
@Comment("排序")
@ColDefine(type = ColType.INT)
private int sortNum;
@Column
@Comment("上传会议纪要")
@Default("0")
@ColDefine(type = ColType.BOOLEAN)
private Boolean uploadMeetingRecord;
}
@@ -0,0 +1,119 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
import org.nutz.dao.Dao;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
public final class SpecialCondolenceRecipients {
private SpecialCondolenceRecipients() {}
public static void prepare(SpecialCondolence info, Dao dao) {
if (info.getSubject() == null || info.getSubject().trim().isEmpty() || info.getSubject().trim().length() > 200) throw new BaseException("请填写200字以内的事项");
info.setSubject(info.getSubject().trim());
List<JSONObject> rows = info.getRecipients();
if (rows == null || rows.isEmpty()) throw new BaseException("请添加慰问对象");
if (rows.size() > 1000) throw new BaseException("每次申请最多1000人");
if (info.getFiles() != null && info.getFiles().size() > 5) throw new BaseException("最多上传5个附件");
if (info.getRemark() != null && info.getRemark().trim().length() > 500) throw new BaseException("申请事由最多500字");
if (info.getPerMoney() == null || info.getPerMoney().signum() <= 0 || info.getPerMoney().compareTo(new BigDecimal("10000")) > 0 || info.getPerMoney().scale() > 2) throw new BaseException("每人慰问金额须大于0且不超过10000元,最多两位小数");
try { LocalDate.parse(info.getOccurTime()); }
catch (Exception e) { throw new BaseException("请选择有效的统一慰问时间"); }
BigDecimal total = BigDecimal.ZERO;
for (int i = 0; i < rows.size(); i++) {
JSONObject row = rows.get(i);
String prefix = "第" + (i + 1) + "行:";
if (row == null) throw new BaseException(prefix + "明细不能为空");
row.set("type", info.getType());
row.set("money", info.getPerMoney().toPlainString());
row.set("occurTime", info.getOccurTime());
resolveUser(row, dao, prefix);
String[] fields = {"helpUserName", "type", "idCard", "bankOfDeposit", "bankUserName", "bankCardNumber", "money", "occurTime"};
for (String field : fields) {
String value = row.getStr(field);
if (value == null || value.trim().isEmpty()) throw new BaseException(prefix + "请填写完整的慰问对象信息");
row.set(field, value.trim());
}
if (!validIdCard(row.getStr("idCard"))) throw new BaseException(prefix + "身份证号码不正确");
if (!row.getStr("bankCardNumber").matches("[0-9]{6,30}")) throw new BaseException(prefix + "收款账户须为6至30位数字");
if (row.getStr("helpUserName").length() > 100 || row.getStr("bankOfDeposit").length() > 100 || row.getStr("bankUserName").length() > 100) throw new BaseException(prefix + "姓名、开户行、户名不能超过100字");
if (!row.getStr("money").matches("[0-9]+(\\.[0-9]{1,2})?")) throw new BaseException(prefix + "金额最多两位小数");
BigDecimal money = new BigDecimal(row.getStr("money"));
if (money.signum() <= 0 || money.compareTo(new BigDecimal("10000")) > 0) throw new BaseException(prefix + "金额须大于0且不超过10000元");
try { LocalDate.parse(row.getStr("occurTime")); }
catch (Exception e) { throw new BaseException(prefix + "慰问时间格式须为yyyy-MM-dd"); }
SpecialCondolenceType type = dao.fetch(SpecialCondolenceType.class, row.getStr("type"));
if (type == null || !Boolean.TRUE.equals(type.getEnable())) throw new BaseException(prefix + "慰问类型不存在或已停用");
row.set("typeName", type.getName());
row.set("typeCode", type.getCode());
row.set("way", type.getWay());
if (Boolean.TRUE.equals(type.getIsUploadFile()) && (info.getFiles() == null || info.getFiles().isEmpty())) throw new BaseException(prefix + "该慰问类型需要上传附件");
total = total.add(money);
}
JSONObject first = rows.get(0);
String name = first.getStr("helpUserName");
info.setHelpUserName((rows.size() > 1 && name.length() > 90 ? name.substring(0, 90) : name) + (rows.size() > 1 ? "等" + rows.size() + "人" : ""));
info.setHelpUnitId(info.getApplyUnitId());
info.setHelpUnitName(info.getApplyUnitName());
info.setHelpUnionId(info.getApplyUnionId());
info.setHelpUnionName(info.getApplyUnionName());
info.setType(first.getStr("type"));
info.setTypeCode(first.getStr("typeCode"));
info.setWay(first.getStr("way"));
info.setOccurTime(first.getStr("occurTime"));
info.setMoney(total.doubleValue());
info.setBankOfDeposit(first.getStr("bankOfDeposit"));
info.setBankUserName(first.getStr("bankUserName"));
info.setBankCardNumber(first.getStr("bankCardNumber"));
}
public static void resolveUser(JSONObject row, Dao dao, String prefix) {
String userId = row.getStr("helpUserId");
com.budwk.app.sys.views.View_user user;
String unionId = com.budwk.app.web.commons.auth.utils.SecurityUtil.getUnionId();
if (cn.hutool.core.util.StrUtil.isBlank(unionId)) throw new BaseException(prefix + "申请人未关联分工会");
if (cn.hutool.core.util.StrUtil.isNotBlank(userId)) {
user = dao.fetch(com.budwk.app.sys.views.View_user.class, org.nutz.dao.Cnd.where("id", "=", userId));
} else {
org.nutz.dao.Cnd identity = org.nutz.dao.Cnd.where("unionId", "=", unionId)
.and("idCard", "=", row.getStr("idCard")).and("username", "=", row.getStr("helpUserName"));
java.util.List<com.budwk.app.sys.views.View_user> dispatched = dao.query(com.budwk.app.sys.views.View_user.class,
identity.and("personType", "=", "劳务派遣人员"));
if (dispatched.size() > 1) throw new BaseException(prefix + "劳务派遣人员身份重复,请核对档案");
if (dispatched.size() == 1) {
user = dispatched.get(0);
} else {
if (cn.hutool.core.util.StrUtil.isBlank(row.getStr("helpLoginName"))) throw new BaseException(prefix + "普通人员必须填写工号");
java.util.List<com.budwk.app.sys.views.View_user> users = dao.query(com.budwk.app.sys.views.View_user.class,
org.nutz.dao.Cnd.where("unionId", "=", unionId).and("loginname", "=", row.getStr("helpLoginName"))
.and("username", "=", row.getStr("helpUserName")).and("idCard", "=", row.getStr("idCard")));
if (users.size() != 1) throw new BaseException(prefix + "工号、姓名、身份证不匹配或不属于申请人本工会");
user = users.get(0);
}
}
if (user == null) throw new BaseException(prefix + "慰问对象不存在");
if (!java.util.Objects.equals(user.getUnionId(), unionId)) throw new BaseException(prefix + "只能选择申请人本工会人员");
if (!"劳务派遣人员".equals(user.getPersonType()) && (cn.hutool.core.util.StrUtil.isBlank(row.getStr("helpLoginName")) || !java.util.Objects.equals(user.getLoginname(), row.getStr("helpLoginName")))) throw new BaseException(prefix + "工号与慰问对象不一致");
if (cn.hutool.core.util.StrUtil.isNotBlank(user.getIdCard()) && !user.getIdCard().equalsIgnoreCase(row.getStr("idCard"))) throw new BaseException(prefix + "身份证号码与所选人员不一致");
row.set("helpUserId", user.getId());
row.set("helpLoginName", user.getLoginname());
row.set("personType", user.getPersonType());
row.set("helpUserName", user.getUsername());
}
public static boolean validIdCard(String value) {
if (value == null || !value.matches("[1-9][0-9]{16}[0-9Xx]")) return false;
try { LocalDate.parse(value.substring(6, 14), java.time.format.DateTimeFormatter.BASIC_ISO_DATE); }
catch (Exception e) { return false; }
int[] weights = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
int sum = 0;
for (int i = 0; i < weights.length; i++) sum += (value.charAt(i) - '0') * weights[i];
return "10X98765432".charAt(sum % 11) == Character.toUpperCase(value.charAt(17));
}
}
@@ -0,0 +1,17 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
/**
* @ClassName SpecialCondolenceService
* @Author JyuHsin
* @Date 2025/7/29 11:21
* @Version 1.0
* @Description TODO
*/
public interface SpecialCondolenceService extends BaseService<SpecialCondolence> {
void saveApplication(SpecialCondolence info);
SpecialCondolence loadApplication(String id);
void deleteApplication(String id);
}
@@ -0,0 +1,14 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
/**
* @ClassName SpecialCondolenceTypeService
* @Author JyuHsin
* @Date 2025/7/29 10:03
* @Version 1.0
* @Description TODO
*/
public interface SpecialCondolenceTypeService extends BaseService<SpecialCondolenceType> {
}
@@ -0,0 +1,87 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceRecipient;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceRecipients;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.nutz.ioc.aop.Aop;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
/**
* @ClassName SpecialCondolenceServiceImpl
* @Author JyuHsin
* @Date 2025/7/29 11:22
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class SpecialCondolenceServiceImpl extends BaseServiceImpl<SpecialCondolence> implements SpecialCondolenceService {
public SpecialCondolenceServiceImpl(Dao dao) {
super(dao);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveApplication(SpecialCondolence info) {
SpecialCondolenceRecipients.prepare(info, dao());
dao().insertOrUpdate(info);
dao().clear(SpecialCondolenceRecipient.class,
Cnd.where("applyId", "=", info.getId()));
int sort = 0;
for (JSONObject row : info.getRecipients()) {
SpecialCondolenceRecipient person = new SpecialCondolenceRecipient();
person.setApplyId(info.getId());
person.setHelpUserName(row.getStr("helpUserName"));
person.setHelpUserId(row.getStr("helpUserId"));
person.setHelpLoginName(row.getStr("helpLoginName"));
person.setPersonType(row.getStr("personType"));
person.setType(row.getStr("type"));
person.setTypeName(row.getStr("typeName"));
person.setTypeCode(row.getStr("typeCode"));
person.setWay(row.getStr("way"));
person.setIdCard(row.getStr("idCard"));
person.setBankOfDeposit(row.getStr("bankOfDeposit"));
person.setBankUserName(row.getStr("bankUserName"));
person.setBankCardNumber(row.getStr("bankCardNumber"));
person.setMoney(new BigDecimal(row.getStr("money")));
person.setOccurTime(row.getStr("occurTime"));
person.setSortNum(sort++);
dao().insert(person);
}
}
@Override
public SpecialCondolence loadApplication(String id) {
SpecialCondolence info = fetch(id);
if (info != null) {
List<JSONObject> rows = new ArrayList<>();
for (SpecialCondolenceRecipient person : dao().query(
SpecialCondolenceRecipient.class,
Cnd.where("applyId", "=", id).asc("sortNum").asc("id"))) {
rows.add(JSONUtil.parseObj(person));
}
info.setRecipients(rows);
}
return info;
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteApplication(String id) {
dao().clear(SpecialCondolenceRecipient.class,
Cnd.where("applyId", "=", id));
delete(id);
}
}
@@ -0,0 +1,24 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceTypeService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
/**
* @ClassName SpecialCondolenceTypeServiceImpl
* @Author JyuHsin
* @Date 2025/7/29 10:03
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class SpecialCondolenceTypeServiceImpl extends BaseServiceImpl<SpecialCondolenceType> implements SpecialCondolenceTypeService {
public SpecialCondolenceTypeServiceImpl(Dao dao) {
super(dao);
}
}
@@ -735,7 +735,6 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
entities.add(new ExcelExportEntity("在职状态", "userState", 20));
entities.add(new ExcelExportEntity("所属工会", "welfareUnionName", 20));
entities.add(new ExcelExportEntity("所属单位", "welfareUnitName", 20));
entities.add(new ExcelExportEntity("三级单位", "threeUnitName", 20));
entities.add(new ExcelExportEntity("备注", "remark", 20));
// 设置导出参数
@@ -0,0 +1,227 @@
-- 全量初始化:完整定义特殊慰问申请主表、被申请人明细表和慰问类型表。
-- 表已存在时不执行本文件中的建表语句;本文件不复制数据、不配置角色授权和工作流。
CREATE TABLE `special_condolence` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`subject` varchar(255) DEFAULT NULL COMMENT '事项',
`applyUserId` varchar(32) DEFAULT NULL COMMENT '申请人ID',
`applyUserName` varchar(100) DEFAULT NULL COMMENT '申请人姓名',
`applyLoginName` varchar(120) DEFAULT NULL COMMENT '申请人工号',
`applyUnionId` varchar(32) DEFAULT NULL COMMENT '申请人分工会ID',
`applyUnionName` varchar(100) DEFAULT NULL COMMENT '申请人分工会',
`applyUnitId` varchar(32) DEFAULT NULL COMMENT '申请人单位ID',
`applyUnitName` varchar(100) DEFAULT NULL COMMENT '申请人单位',
`helpUserId` varchar(32) DEFAULT NULL COMMENT '补助人ID',
`helpUserName` varchar(100) DEFAULT NULL COMMENT '补助人姓名',
`helpLoginName` varchar(120) DEFAULT NULL COMMENT '补助人工号',
`helpUnionId` varchar(32) DEFAULT NULL COMMENT '补助人分工会ID',
`helpUnionName` varchar(100) DEFAULT NULL COMMENT '补助人分工会',
`helpUnitId` varchar(32) DEFAULT NULL COMMENT '补助人单位ID',
`helpUnitName` varchar(100) DEFAULT NULL COMMENT '补助人单位',
`origin` varchar(20) DEFAULT NULL COMMENT '类型',
`payUserId` varchar(32) DEFAULT NULL COMMENT '收款人',
`payUserName` varchar(30) DEFAULT NULL COMMENT '收款人姓名',
`payLoginName` varchar(20) DEFAULT NULL COMMENT '收款人工号',
`type` varchar(32) DEFAULT NULL COMMENT '慰问类型',
`typeCode` varchar(32) DEFAULT NULL COMMENT '慰问编码',
`way` varchar(20) DEFAULT NULL COMMENT '慰问方式',
`perMoney` decimal(10,2) DEFAULT NULL COMMENT '统一每人慰问金额',
`money` float(10,2) DEFAULT NULL COMMENT '慰问金额',
`occurTime` varchar(30) DEFAULT NULL COMMENT '发生时间',
`child` varchar(10) DEFAULT NULL COMMENT '生育几孩(孩次)',
`hospital` varchar(50) DEFAULT NULL COMMENT '所在医院',
`hospitalHouseNum` varchar(20) DEFAULT NULL COMMENT '病房床号',
`hospitalHouseBedNum` varchar(20) DEFAULT NULL COMMENT '病房床号',
`hospitalBy` varchar(100) DEFAULT NULL COMMENT '住院病由',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`files` json DEFAULT NULL COMMENT '附件',
`signature` varchar(255) DEFAULT NULL COMMENT '签字',
`createTime` varchar(30) DEFAULT NULL COMMENT '申请时间',
`bankUserName` varchar(100) DEFAULT NULL COMMENT '户名',
`bankCardNumber` varchar(100) DEFAULT NULL COMMENT '银行账号',
`bankOfDeposit` varchar(100) DEFAULT NULL COMMENT '开户行',
`certifierUserId` varchar(32) DEFAULT NULL COMMENT '证明人ID',
`certifierUserName` varchar(100) DEFAULT NULL COMMENT '证明人姓名',
`certifierLoginName` varchar(120) DEFAULT NULL COMMENT '证明人工号',
`certifierUnionId` varchar(32) DEFAULT NULL COMMENT '证明人分工会ID',
`certifierUnionName` varchar(100) DEFAULT NULL COMMENT '证明人分工会',
`certifierUnitId` varchar(32) DEFAULT NULL COMMENT '证明人单位ID',
`certifierUnitName` varchar(100) DEFAULT NULL COMMENT '证明人单位',
`handlerUserId` varchar(32) DEFAULT NULL COMMENT '经办人ID',
`handlerUserName` varchar(100) DEFAULT NULL COMMENT '经办人姓名',
`handlerLoginName` varchar(120) DEFAULT NULL COMMENT '经办人工号',
`handlerUnionId` varchar(32) DEFAULT NULL COMMENT '经办人分工会ID',
`handlerUnionName` varchar(100) DEFAULT NULL COMMENT '经办人分工会',
`handlerUnitId` varchar(32) DEFAULT NULL COMMENT '经办人单位ID',
`handlerUnitName` varchar(100) DEFAULT NULL COMMENT '经办人单位',
`hospitalizationTime` varchar(30) DEFAULT NULL COMMENT '入院时间',
`leaveHospitalTime` varchar(30) DEFAULT NULL COMMENT '出院时间',
`thisYearHospitalizationNum` int DEFAULT NULL COMMENT '当年第几次住院',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间(毫秒时间戳)',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间(毫秒时间戳)',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特殊慰问申请';
CREATE TABLE `special_condolence_type` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`code` varchar(30) DEFAULT NULL COMMENT '编码',
`name` varchar(50) DEFAULT NULL COMMENT '名称',
`money` float(10,2) DEFAULT NULL COMMENT '费用',
`way` varchar(20) DEFAULT NULL COMMENT '慰问方式',
`enable` tinyint(1) DEFAULT 1 COMMENT '是否启用',
`isUploadFile` tinyint(1) DEFAULT NULL COMMENT '是否上传附件',
`uploadFileDesc` varchar(50) DEFAULT NULL COMMENT '上传附件说明',
`location` int DEFAULT NULL COMMENT '排序字段',
`sortNum` int NOT NULL DEFAULT 0 COMMENT '排序',
`uploadMeetingRecord` tinyint(1) DEFAULT 0 COMMENT '上传会议纪要',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间(毫秒时间戳)',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间(毫秒时间戳)',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特殊慰问类型';
-- 一份申请对应一个流程实例;多名被申请人通过 applyId 关联同一份申请。
CREATE TABLE `special_condolence_recipient` (
`id` varchar(32) NOT NULL COMMENT '明细ID',
`applyId` varchar(32) NOT NULL COMMENT '申请主表ID',
`helpUserId` varchar(32) NOT NULL COMMENT '慰问对象用户ID',
`helpLoginName` varchar(120) DEFAULT NULL COMMENT '慰问对象工号',
`personType` varchar(50) DEFAULT NULL COMMENT '人员类型',
`helpUserName` varchar(100) NOT NULL COMMENT '慰问对象',
`type` varchar(32) NOT NULL COMMENT '慰问类型ID',
`typeName` varchar(50) DEFAULT NULL COMMENT '慰问类型名称',
`typeCode` varchar(32) DEFAULT NULL COMMENT '慰问类型编码',
`way` varchar(20) DEFAULT NULL COMMENT '慰问方式',
`idCard` varchar(18) NOT NULL COMMENT '身份证号码',
`bankOfDeposit` varchar(100) NOT NULL COMMENT '开户行',
`bankUserName` varchar(100) NOT NULL COMMENT '户名',
`bankCardNumber` varchar(30) NOT NULL COMMENT '收款账户',
`money` decimal(10,2) NOT NULL COMMENT '慰问金额',
`occurTime` varchar(30) NOT NULL COMMENT '慰问时间',
`sortNum` int NOT NULL DEFAULT 0 COMMENT '行序号',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间(毫秒时间戳)',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间(毫秒时间戳)',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_special_condolence_recipient_apply` (`applyId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特殊慰问被申请人明细';
-- 主表保留原有摘要字段兼容现有列表,money 为整份申请的金额合计。
-- 被申请人的完整信息保存在 special_condolence_recipient 中,不在主表使用 JSON 存储。
-- 新增 PC、H5 菜单,角色授权由管理员另行配置。
START TRANSACTION;
CREATE TEMPORARY TABLE special_condolence_menu_map AS
SELECT id oldId, MD5(CONCAT('special_condolence:', id)) newId
FROM sys_menu
WHERE id IN ('3b49290f7db44130b9f6394a8212321f', '024811fd168e44948e6ab84911fc26bb')
OR (href LIKE '/platform/condolence/%' AND href NOT LIKE '%/branchUnionApproval%');
CREATE TEMPORARY TABLE special_condolence_parent_map (
oldId varchar(32) NOT NULL COMMENT '原父菜单ID',
newId varchar(32) NOT NULL COMMENT '特殊慰问父菜单ID',
PRIMARY KEY (oldId)
);
INSERT INTO special_condolence_parent_map (oldId, newId)
SELECT oldId, newId FROM special_condolence_menu_map;
INSERT INTO sys_menu
(id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt,
delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName,
isRecommendApp, isRecommendService)
SELECT map.newId, COALESCE(parent.newId, m.parentId),
CONCAT('T', SUBSTRING(map.newId, 1, 7)), REPLACE(m.name, '职工慰问', '特殊慰问'),
m.aliasName, m.type, REPLACE(m.href, '/condolence/', '/special_condolence/'), m.target,
m.icon, m.showit, m.disabled,
CASE m.id WHEN '3b49290f7db44130b9f6394a8212321f' THEN 'special_condolence'
WHEN '024811fd168e44948e6ab84911fc26bb' THEN 'h5.special_condolence'
ELSE REPLACE(m.permission, 'condolence', 'special_condolence') END,
REPLACE(m.note, '职工慰问', '特殊慰问'), m.location, m.hasChildren,
m.createdBy, UNIX_TIMESTAMP()*1000, m.updatedBy, UNIX_TIMESTAMP()*1000, m.delFlag,
REPLACE(m.processKey, 'ZGWW', 'TSWW'), m.platform, m.moduleId, m.isQuickEntry,
m.picIcon, IF(m.name = '职工慰问', 't', m.initialPinyinName), m.isRecommendApp, m.isRecommendService
FROM sys_menu m JOIN special_condolence_menu_map map ON map.oldId = m.id
LEFT JOIN special_condolence_parent_map parent ON parent.oldId = m.parentId
WHERE NOT EXISTS (SELECT 1 FROM sys_menu existing WHERE existing.id = map.newId);
-- 父子菜单路径保持树形前缀关系。
UPDATE sys_menu child JOIN sys_menu parent ON parent.id = child.parentId
JOIN special_condolence_menu_map map ON map.newId = child.id
SET child.path = CONCAT(parent.path, RIGHT(child.id, 4));
DROP TEMPORARY TABLE special_condolence_menu_map;
DROP TEMPORARY TABLE special_condolence_parent_map;
-- 权限部分,不要删除
-- SYSADMIN
-- 特殊慰问 web
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '4c46696d75ff0eddf5cb57fac3e5d06d');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '3e97a91e7aa9566cdae94850038a6212'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '6c5be59ffb8d7c524ec72f2a02d6c6d2'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '7f66eb63b6ebd81c5eee9c5c2de2ed98'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '8d5ebaa091eee01abb33e89e3dcc92f8'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '9c37242f50c5fb4a9c1b2b9b9dd0e2fd'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '9f4f850904fe751db1cd13977374f99b'),
('7f4a820d5a394bb8a0a92d6699afe7c8', 'b24a5bef9ccbf7241c5bb85e80352f8f');
-- 特殊慰问 h5
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '2145812145ee6548552504b4f5a55ff7');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '42b4b5f54593cf023961ad1c9ce36f53'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '4f3212a13a54fbb2a082ee77651c9a73'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '739f64c71fe776dc3520ebb2977c2d68'),
('7f4a820d5a394bb8a0a92d6699afe7c8', 'b6c725beaf3bfe9f2b29f46855c45a24'),
('7f4a820d5a394bb8a0a92d6699afe7c8', 'eb3d1b27d51b84df1cebf24e1278e26c');
-- 校工会办公室主任
-- 特殊慰问 web
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('494ac71eb8b44d0e99e19b7fde6bd469', '4c46696d75ff0eddf5cb57fac3e5d06d'),
('494ac71eb8b44d0e99e19b7fde6bd469', '8d5ebaa091eee01abb33e89e3dcc92f8'),
('494ac71eb8b44d0e99e19b7fde6bd469', '7f66eb63b6ebd81c5eee9c5c2de2ed98');
-- 特殊慰问 h5
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('494ac71eb8b44d0e99e19b7fde6bd469', '2145812145ee6548552504b4f5a55ff7'),
('494ac71eb8b44d0e99e19b7fde6bd469', '42b4b5f54593cf023961ad1c9ce36f53');
-- 校工会主席
-- 特殊慰问 web
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('6aece46b946e4fb9ac4cb15c0cd8c3e6', '4c46696d75ff0eddf5cb57fac3e5d06d'),
('6aece46b946e4fb9ac4cb15c0cd8c3e6', '8d5ebaa091eee01abb33e89e3dcc92f8'),
('6aece46b946e4fb9ac4cb15c0cd8c3e6', 'b24a5bef9ccbf7241c5bb85e80352f8f');
-- 特殊慰问 h5
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('6aece46b946e4fb9ac4cb15c0cd8c3e6', '2145812145ee6548552504b4f5a55ff7'),
('6aece46b946e4fb9ac4cb15c0cd8c3e6', 'b6c725beaf3bfe9f2b29f46855c45a24');
-- 校工会主席
-- 特殊慰问 web
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('b3854469e6f44901b22bb791cb538431', '4c46696d75ff0eddf5cb57fac3e5d06d'),
('b3854469e6f44901b22bb791cb538431', '8d5ebaa091eee01abb33e89e3dcc92f8'),
('b3854469e6f44901b22bb791cb538431', '9c37242f50c5fb4a9c1b2b9b9dd0e2fd');
-- 特殊慰问 h5
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('b3854469e6f44901b22bb791cb538431', '2145812145ee6548552504b4f5a55ff7'),
('b3854469e6f44901b22bb791cb538431', '739f64c71fe776dc3520ebb2977c2d68');
COMMIT;
@@ -0,0 +1,92 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -380,7 +380,7 @@
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="setDialogVisible = false">取消</el-button>
<el-button type="primary" @click="doSetActivityUser" v-loading="settingLoading">确定</el-button>
<el-button type="primary" :disabled="settingLoading" @click="doSetActivityUser">确定</el-button>
</span>
</el-dialog>
@@ -646,7 +646,7 @@ module.exports = {
doSearch(showFullLoading = false) {
this.tableKey = new Date().getTime()
this.pageForm.pageNumber = 1
this.pageData(showFullLoading === true)
return this.pageData(showFullLoading === true)
},
async pageData(showFullLoading = false) {
this.tableLoading = true
@@ -679,43 +679,74 @@ module.exports = {
}
}
},
async doSetActivityUser() {
const valid = await this.$refs.setForm.validate()
if (!valid) {
doSetActivityUser() {
// 方法级状态保护与整页遮罩共同阻止慢请求期间的重复确认。
if (this.settingLoading) {
return
}
if (this.formData.groupType !== 2 && this.tableData.length === 0) {
this.$message.warning("请先指定筛选条件!")
return
}
const confirm = await this.$confirm("是否将符合搜索条件的用户设为活动人员?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
if (confirm === "confirm") {
this.settingLoading = true
const pageForm = this.normalizePageForm(this.pageForm)
Object.assign(pageForm, this.formData)
const resp = await $.post("/platform/activity/basic/scope/doSetActivityUser", {
data: JSON.stringify(pageForm)
})
if (resp.code === 0) {
this.setDialogVisible = false
await this.getActivityGroup()
this.doSearch()
this.$message.success(resp.msg)
this.formData = this.getDefaultFormData()
} else {
this.$message.error(resp.msg)
this.$refs.setForm.validate((valid) => {
if (!valid) {
return
}
this.settingLoading = false
this.$emit("update:group_id", resp.data)
this.$emit("group_change")
}
if (this.formData.groupType !== 2 && this.tableData.length === 0) {
this.$message.warning("请先指定筛选条件!")
return
}
this.$confirm("是否将符合搜索条件的用户设为活动人员?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
if (this.settingLoading) {
return
}
this.settingLoading = true
const loading = this.$loading({
lock: true,
text: "正在设置活动人员,请稍候...",
spinner: "el-icon-loading",
background: "rgba(255, 255, 255, 0.65)"
})
const pageForm = this.normalizePageForm(this.pageForm)
Object.assign(pageForm, this.formData)
$.post("/platform/activity/basic/scope/doSetActivityUser", {
data: JSON.stringify(pageForm)
}).then((resp) => {
if (resp.code !== 0) {
// 将后端业务错误转入统一失败分支,直接展示接口返回的真实原因。
throw new Error(resp.msg || "设置活动人员失败")
}
this.setDialogVisible = false
// 设置成功后等待分组和人员列表刷新完成,再解除整页操作锁定。
return this.getActivityGroup()
.then(() => this.doSearch())
.then(() => {
this.$message.success(resp.msg)
this.formData = this.getDefaultFormData()
this.$emit("update:group_id", resp.data)
this.$emit("group_change")
})
}).then(null, (error) => {
// 兼容业务错误、权限异常和网络异常,避免请求失败后页面持续加载。
const response = error && error.responseJSON ? error.responseJSON : null
const errorMessage = response && response.msg
? response.msg
: error && error.message
? error.message
: "设置活动人员失败"
this.$message.error(errorMessage)
}).always(() => {
this.settingLoading = false
loading.close()
})
})
})
},
async getActivityGroup() {
const resp = await $.get("/platform/activity/basic/scope/getActivityUserScopeGroup")
@@ -10,7 +10,7 @@
:finished="tableFinished"
finished-text="没有更多了"
@load="onLoad">
<div class="table-list-container">
<div class="table-list-container" @click="showOverflowText">
<div v-for="(row, index) in tableData" :key="index" class="table-list-item"
:data-row-id="row && row.id ? row.id : ''">
<slot name="header" :index="index" :row="row">
@@ -36,6 +36,12 @@
</div>
</div>
</van-list>
<van-popover ref="overflowPopover" :value="overflowVisible" :style="overflowAnchorStyle"
:placement="overflowPlacement" get-container="body" @input="onOverflowInput"
@opened="positionOverflow">
<div class="table-list-overflow-content">{{ overflowText }}</div>
<template #reference><span></span></template>
</van-popover>
</van-pull-refresh>
</template>
<script>
@@ -90,10 +96,138 @@ module.exports = {
tableLoading: false,
tableFinished: false,
tableRefreshing: false,
localPageForm: {...this.page_form}
localPageForm: {...this.page_form},
overflowVisible: false,
overflowText: "",
overflowTarget: null,
overflowPlacement: "top",
overflowAnchorStyle: {}
}
},
methods: {
// 只响应真正被截断的纯文字,保留卡片内按钮和业务插槽原有的点击行为。
showOverflowText(event) {
const target = event.target
if (!target || !target.closest) return
const card = target.closest(".table-list-item")
if (!card || target.closest(".item-actions, a, button, input, textarea, select, [role=button], [tabindex], [onclick]")) return
if (target.children.length || !target.textContent.trim() || this.hasOverflowClickHandler(target, card)) return
const style = window.getComputedStyle(target)
if (style.textOverflow !== "ellipsis" || (target.scrollWidth <= target.clientWidth && target.scrollHeight <= target.clientHeight)) return
const rect = target.getBoundingClientRect()
this.overflowTarget = target
this.overflowText = target.textContent.trim()
this.overflowPlacement = rect.top < 180 ? "bottom" : "top"
this.overflowAnchorStyle = {
position: "fixed",
display: "block",
pointerEvents: "none",
left: rect.left + "px",
top: rect.top + "px",
width: rect.width + "px",
height: rect.height + "px"
}
if (!this.overflowVisible) this.bindOverflowListeners()
this.overflowVisible = true
this.$nextTick(() => {
const instance = this.$refs.overflowPopover
if (instance) instance.updateLocation()
setTimeout(() => this.positionOverflow(), 50)
setTimeout(() => this.positionOverflow(), 350)
})
},
// 检查文字及其卡片内的祖先节点,避免干扰已有 Vue 点击事件。
hasOverflowClickHandler(target, card) {
const ancestors = []
let element = target
while (element && element !== card.parentElement) {
ancestors.push(element)
element = element.parentElement
}
const visit = (node) => {
if (!node) return false
const events = node.data ? [node.data.on, node.data.nativeOn] : []
if (ancestors.includes(node.elm) && events.some((handlers) => handlers &&
Object.keys(handlers).some((key) => /^[~!&]*(click|touchstart|pointerdown|mousedown)$/.test(key)))) return true
if (node.componentInstance && visit(node.componentInstance._vnode)) return true
return Array.isArray(node.children) && node.children.some((child) => visit(child))
}
return visit(this._vnode)
},
// Vant 的弹层挂在 body;校正窗口边缘的位置,避免完整文字被屏幕裁掉。
positionOverflow() {
const instance = this.$refs.overflowPopover
const popup = instance && instance.$refs.popover && instance.$refs.popover.$el
if (!this.overflowVisible || !this.overflowTarget || !popup) return
const rect = popup.getBoundingClientRect()
const anchor = this.overflowTarget.getBoundingClientRect()
if (!rect.width || !rect.height) return
const margin = 8
const left = Math.max(margin, Math.min(rect.left, window.innerWidth - rect.width - margin))
let top = rect.top
if (top < margin) top = anchor.bottom + 8
if (top + rect.height > window.innerHeight - margin) top = anchor.top - rect.height - 8
top = Math.max(margin, Math.min(top, window.innerHeight - rect.height - margin))
popup.style.left = (parseFloat(popup.style.left) || 0) + left - rect.left + "px"
popup.style.top = (parseFloat(popup.style.top) || 0) + top - rect.top + "px"
const arrow = popup.querySelector(".van-popover__arrow")
if (arrow) {
arrow.style.left = Math.max(12, Math.min(anchor.left + anchor.width / 2 - left, rect.width - 12)) + "px"
}
},
onOverflowInput(visible) {
this.overflowVisible = visible
if (!visible) {
this.overflowTarget = null
this.unbindOverflowListeners()
}
},
// 监听只在气泡打开时存在,PJAX 替换页面前也会关闭气泡并注销。
bindOverflowListeners() {
document.addEventListener("click", this.closeOverflowOnClick)
window.addEventListener("scroll", this.closeOverflowOnMove, true)
window.addEventListener("resize", this.closeOverflowOnMove)
window.addEventListener("transitionend", this.onOverflowAnimationEnd)
window.addEventListener("animationend", this.onOverflowAnimationEnd)
$(document).on("pjax:send", this.closeOverflowForNavigation)
},
unbindOverflowListeners() {
document.removeEventListener("click", this.closeOverflowOnClick)
window.removeEventListener("scroll", this.closeOverflowOnMove, true)
window.removeEventListener("resize", this.closeOverflowOnMove)
window.removeEventListener("transitionend", this.onOverflowAnimationEnd)
window.removeEventListener("animationend", this.onOverflowAnimationEnd)
$(document).off("pjax:send", this.closeOverflowForNavigation)
},
closeOverflowForNavigation() {
this.onOverflowInput(false)
},
// Vant 会在动画结束时重新计算定位,随后再次限制气泡的屏幕边界。
onOverflowAnimationEnd() {
if (this.overflowVisible) setTimeout(() => this.positionOverflow(), 0)
},
// 滚动或改变屏幕尺寸时关闭气泡,避免气泡脱离原文字位置。
closeOverflowOnMove(event) {
if (event && event.target && event.target.closest && event.target.closest(".table-list-overflow-content")) return
this.onOverflowInput(false)
},
closeOverflowOnClick(event) {
if (!this.overflowVisible) return
if (this.overflowTarget && this.overflowTarget.contains(event.target)) return
if (event.target.closest && event.target.closest(".table-list-overflow-content")) return
this.onOverflowInput(false)
},
/**
* 获取当前列表快照。
* 返回值包含已加载记录、分页参数和加载完成状态,供业务页面在进入详情前暂存列表现场。
@@ -186,10 +320,28 @@ module.exports = {
},
mounted() {
this.$emit("ready")
},
beforeDestroy() {
this.unbindOverflowListeners()
}
}
</script>
<style>
/* Vant 将气泡挂到 body,控制宽高并允许长文本在气泡内滚动。 */
.table-list-overflow-content {
max-width: min(240px, calc(100vw - 48px));
max-height: 160px;
padding: 10px 12px;
overflow-y: auto;
color: #253247;
font-size: 13px;
line-height: 1.55;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
</style>
<style scoped>
.table-list-container {
padding: 0;
@@ -0,0 +1,98 @@
<template>
<div class="h5-user-select">
<van-field v-bind="$attrs" :value="displayName" readonly clickable is-link @click="open"></van-field>
<van-popup v-model="visible" position="bottom" round get-container="body" class="h5-user-select-popup">
<div class="h5-user-select-popup__header">
<van-button native-type="button" type="default" size="small" @click="visible = false">取消</van-button>
<strong>选择慰问对象</strong>
<van-button native-type="button" type="info" size="small" :disabled="!pendingUser" @click="confirm">确认</van-button>
</div>
<van-search v-model="keyword" placeholder="请输入姓名或工号查询" @input="scheduleSearch" @search="search" @clear="search"></van-search>
<div class="h5-user-select-popup__list">
<div v-if="loading" class="h5-user-select-popup__status"><van-loading size="24px">加载中</van-loading></div>
<div v-else-if="error" class="h5-user-select-popup__status"><span class="h5-user-select-popup__error">{{ error }}</span><van-button native-type="button" size="small" @click="search">重新加载</van-button></div>
<van-empty v-else-if="!options.length" description="未找到本工会人员"></van-empty>
<template v-else>
<van-cell v-for="person in options" :key="person.id" clickable
:class="{ 'h5-user-select-popup__selected': pendingUser && pendingUser.id === person.id }"
:title="person.userName + (person.loginName ? '(' + person.loginName + ')' : '')"
:label="[person.unitName, person.personType].filter(Boolean).join(' · ')"
@click="pendingUser = person">
<van-icon v-if="pendingUser && pendingUser.id === person.id" name="success" color="#1989fa"></van-icon>
</van-cell>
</template>
</div>
</van-popup>
</div>
</template>
<script>
module.exports = {
inheritAttrs: false,
model: { prop: 'value', event: 'change' },
props: {
value: { type: String, default: '' },
displayName: { type: String, default: '' },
api: { type: String, required: true }
},
data() {
return { visible: false, keyword: '', options: [], pendingUser: null, loading: false, error: '', searchTimer: null, requestId: 0 }
},
beforeDestroy() {
clearTimeout(this.searchTimer)
this.requestId++
},
methods: {
open() {
this.keyword = ''
this.pendingUser = null
this.visible = true
this.search()
},
scheduleSearch() {
clearTimeout(this.searchTimer)
this.requestId++
this.pendingUser = null
this.loading = true
this.searchTimer = setTimeout(() => this.search(), 250)
},
async search() {
clearTimeout(this.searchTimer)
const requestId = ++this.requestId
this.loading = true
this.error = ''
this.pendingUser = null
try {
const result = await this.$axios.post(this.api, { keyword: this.keyword })
if (requestId !== this.requestId) return
if (result.code !== 0) throw new Error(result.msg || '人员加载失败')
this.options = result.data || []
} catch (error) {
if (requestId !== this.requestId) return
this.options = []
this.error = error.message || '人员加载失败'
} finally {
if (requestId === this.requestId) this.loading = false
}
},
confirm() {
if (!this.pendingUser) return
const person = this.pendingUser
this.$emit('change', person.id)
this.$emit('selected', person, person.id)
this.visible = false
}
}
}
</script>
<style>
.h5-user-select-popup { display: flex; flex-direction: column; height: 72vh; max-height: 640px; padding-bottom: env(safe-area-inset-bottom); box-sizing: border-box; }
.h5-user-select-popup__header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; padding: 14px 16px; border-bottom: 1px solid #ebedf0; }
.h5-user-select-popup__header strong { font-size: 16px; color: #323233; }
.h5-user-select-popup__header .van-button { min-width: 56px; }
.h5-user-select-popup__list { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.h5-user-select-popup__selected { background: #edf6ff; }
.h5-user-select-popup__status { display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 12px; padding: 40px 16px; }
.h5-user-select-popup__error { color: #ee0a24; }
</style>
@@ -106,7 +106,9 @@ module.exports = {
}
},
onChange(val) {
const selected = this.options.find(item => String(item[this.option_value]) === String(val)) || null
this.$emit("change", val)
this.$emit("selected", selected, val)
},
clearOptions() {
@@ -114,7 +116,7 @@ module.exports = {
}
},
created() {
this.clearOptions()
this.options = this.option_list || []
console.log(this.placeholder)
}
}
@@ -706,7 +706,7 @@
Vue.component("year-van-dropdown-item", httpVueLoader("/components/plugins/vantMore/yearVanDropDownItem.vue?v=" + h5ComponentVersion))
Vue.component("van-text-dialog", httpVueLoader("/components/plugins/vantMore/vantTextDialog.vue?v=" + h5ComponentVersion))
Vue.component("enum-tag", httpVueLoader("/components/plugins/sysEnum/EnumTag.vue?v=" + h5ComponentVersion))
Vue.component("table-list", httpVueLoader("/components/plugins/h5/TableList.vue?v=" + h5ComponentVersion))
Vue.component("table-list", httpVueLoader("/components/plugins/h5/TableList.vue?v=" + h5ComponentVersion + "_overflow1"))
Vue.component("table-column", httpVueLoader("/components/plugins/h5/TableColumn.vue?v=" + h5ComponentVersion))
Vue.component("file-preview", httpVueLoader("/components/plugins/sysFilePreview/H5Index.vue?v=" + h5ComponentVersion))
</script>
@@ -75,7 +75,7 @@
<!-- 引入 form-create 和 designer -->
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
<script src="${base!}/assets/platform/plugins/form-create/index.umd.js"></script>
<script src="${base!}/assets/platform/plugins/form-create/index.umd.js?v=20261030"></script>
<!-- swiper -->
<script src="${base!}/assets/platform/plugins/swiper/swiper-bundle.js"></script>
@@ -187,7 +187,7 @@ const act = {
}
if (act.url === "/platform/activity/apply"
&& !this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN", "BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_WENTI_SPORTS"])) {
return this.$message.warning("PC端暂未开放报名,请前往移动端报名!")
return this.$message.warning("温馨提示:您没有参加该活动的权限!")
}
window.open(act.url)
@@ -116,6 +116,69 @@ layout("/layouts/v4/baseLayout.html"){
object-fit: contain;
}
/* 最新活动的作用域样式会在组件挂载后注入,首屏先固定布局和图片尺寸,避免刷新时放大闪烁。 */
#v4-home-app .act-card .activity-section-wrapper,
#v4-home-app .act-card .section-act,
#v4-home-app .act-card .activity-swiper {
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
}
#v4-home-app .act-card .activity-swiper-container {
position: relative;
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
padding: 9px 54px 12px;
overflow: hidden;
}
#v4-home-app .act-card .activity-swiper .swiper-slide {
width: 100%;
height: auto;
min-width: 0;
flex-shrink: 0;
margin-right: 18px;
}
#v4-home-app .act-card .activity-swiper .img-box {
width: 100%;
height: 128px;
border-radius: 8px;
overflow: hidden;
}
#v4-home-app .act-card .activity-swiper .img-box img {
display: block;
width: 100% !important;
height: 100% !important;
max-width: 100% !important;
max-height: 128px !important;
object-fit: fill !important;
}
@media (min-width: 768px) {
#v4-home-app .act-card .activity-swiper .swiper-slide {
width: calc((100% - 18px) / 2);
}
}
@media (min-width: 1024px) {
#v4-home-app .act-card .activity-swiper .swiper-slide {
width: calc((100% - 54px) / 4);
}
}
@media (min-width: 1200px) {
#v4-home-app .act-card .activity-swiper .swiper-slide {
width: calc((100% - 72px) / 5);
}
}
</style>
<link rel="stylesheet" href="${base!}/assets/platform/css/home-portal.css?v=20261030-1" />
@@ -100,7 +100,7 @@ layout("/layouts/platform.html"){
size="mini" type="danger">删除
</el-button>
<el-button @click="doExportDeclare(row)" size="mini" type="primary">申请表导出</el-button>
<el-button v-if="row.instanceState === 20" @click="doExportDeclare(row)" size="mini" type="primary">申请表导出</el-button>
<el-button v-if="row.instanceState === 20" @click="doExportReimbursement(row)" size="mini"
type="primary">报销凭证导出
</el-button>
@@ -221,8 +221,7 @@ layout("/layouts/platform.html"){
placeholder="请输入姓名" filterable clearable
style="width: 100%" size="small" v-if="awardsMode==1">
<el-option
v-for="item in userList"
:disabled="item.disabled"
v-for="item in getSelectableUserList(row)"
:key="item.id"
:label="item.username+'('+item.loginname+')'+item.sex"
:value="item.id">
@@ -721,10 +720,15 @@ layout("/layouts/platform.html"){
}
return score;
},
getSelectableUserList(row) {
// 其他行已经选择的人员不再显示,当前行人员保留用于姓名正常回显。
const selectedUserIds = this.userData
.map(item => item.userId)
.filter(userId => userId)
return this.userList.filter(item => item.id === row.userId || !selectedUserIds.includes(item.id))
},
userDetailsChange(val, row) {
const useridArr = this.userData.map(v => v.userId)
this.userList.forEach(v => {
v.disabled = useridArr.includes(v.id)
if (row && row.userId === v.id) {
const o = {
unionId: v.unionId,
@@ -601,6 +601,8 @@
eventForm: {
isLeader: false
},
// 独立记录当前编辑项目,避免下拉框切换后丢失切换前项目标识。
currentEventId: "",
eventsIds: [],
rightChecked: [],
checkedEventInfo: [],
@@ -778,8 +780,10 @@
this.checkedEventInfo = this.checkedEvents.map((v) => {
v.eventId = v.id
if (v.projectType === "1") {
v.athletesMaxNum = 3
v.restrictRegNumber = 2
// “所有单项”保存默认值,具体单项以0表示尚未个性化设置。
const isAllItems = v.eventId === "allItems"
v.athletesMaxNum = isAllItems ? 3 : 0
v.restrictRegNumber = isAllItems ? 2 : 0
v.leanderNum = 0
v.coachNum = 0
v.applyUserModel = 1
@@ -791,7 +795,23 @@
return v
})
},
cacheCurrentEventForm() {
if (!this.currentEventId || !this.eventForm) {
return
}
const current = clone(this.eventForm)
current.checkedEventId = this.currentEventId
current.eventId = this.currentEventId
const index = this.checkedEventInfo.findIndex((item) => item.eventId === this.currentEventId)
if (index >= 0) {
this.checkedEventInfo.splice(index, 1, current)
} else {
this.checkedEventInfo.push(current)
}
},
async eventChange(val) {
this.cacheCurrentEventForm()
const data = this.checkedEventInfo.find((v) => v.eventId === val)
const resp = await $.get("/platform/activity/sports/common/getEventData", { id: val })
await this.$nextTick()
@@ -804,10 +824,10 @@
projectType: resp.data.projectType,
leanderNum: 0,
coachNum: 0,
athletesMaxNum: resp.data.projectType === 1 ? 3 : null,
athletesMaxNum: resp.data.projectType === 1 ? 0 : null,
restrictTotalTeam: 1,
substituteNum: 0,
restrictRegNumber: resp.data.projectType === 1 ? 2 : null,
restrictRegNumber: resp.data.projectType === 1 ? 0 : null,
isLeader: false,
applyUserModel: 1
}
@@ -815,33 +835,27 @@
this.projectType = resp.data.projectType
}
this.$set(this.eventForm, "checkedEventId", val)
this.currentEventId = val
},
async saveEvent() {
saveEvent() {
const data = clone(this.eventForm)
if (data.checkedEventId === "allItems") {
this.checkedEventInfo.forEach((v) => {
if (v.projectType === "1") {
v.applyUserModel = this.eventForm.applyUserModel
v.athletesMaxNum = data.athletesMaxNum
v.restrictRegNumber = data.restrictRegNumber
v.unionLimit = data.unionLimit
}
})
const allItemsIndex = this.checkedEventInfo.findIndex((v) => v.eventId === "allItems")
if (allItemsIndex >= 0) {
this.checkedEventInfo.splice(allItemsIndex, 1, data)
}
} else {
const info = this.checkedEventInfo.find((v) => v.eventId === data.checkedEventId)
if (info != null) {
const index = this.checkedEventInfo.findIndex((v) => v.eventId === data.checkedEventId)
this.checkedEventInfo.splice(index, 1)
if (info.projectType === "1") {
this.$set(this.eventForm, "athletesMaxNum", data.athletesMaxNum)
this.$set(this.eventForm, "restrictRegNumber", data.restrictRegNumber)
}
this.$set(this.eventForm, "checkedEventId", info.eventId)
}
data.eventId = data.checkedEventId
this.checkedEventInfo.push(data)
}
await this.doSave()
this.eventForm = clone(data)
this.currentEventId = data.checkedEventId
return this.doSave()
},
prev() {
this.active--
@@ -949,6 +963,7 @@
this.copyTemplateMode = false
this.copyTemplateName = ""
this.pushTodoFlag = false
this.currentEventId = ""
this.events = await this.getEvents(2)
await this.getActivityGroup()
this.active = 0
@@ -1028,6 +1043,7 @@
async openEdit(row, flag, copyTemplateMode = false) {
this.copyTemplateMode = !!copyTemplateMode
this.pushTodoFlag = false
this.currentEventId = ""
this.active = flag ? 0 : this.active
await this.getActivityGroup()
const { id } = row
@@ -1184,7 +1200,8 @@
}
if (this.eventForm.checkedEventId == "allItems") {
events = events.filter((v) => v.projectType == 1 && v.id != "allItems")
// “所有单项”配置随具体单项提交,由后端统一校验并补齐继承值。
events = events.filter((v) => v.projectType == 1)
} else {
this.$set(this.eventForm, "activityId", this.formData.id)
this.$set(this.eventForm, "eventId", this.eventForm.checkedEventId)
@@ -1192,9 +1209,10 @@
this.$set(this.eventForm, "startAgeDate", this.eventForm.ageDate[0])
this.$set(this.eventForm, "endAgeDate", this.eventForm.ageDate[1])
}
events = this.eventForm
const allItems = this.checkedEventInfo.find((v) => v.eventId === "allItems")
events = allItems ? [clone(allItems), this.eventForm] : [this.eventForm]
if (this.copyTemplateMode) {
this.cleanCopyTemplateData(null, [events])
this.cleanCopyTemplateData(null, events)
}
}
const loading = this.$loading({
@@ -1215,6 +1233,7 @@
this.copyTemplateMode = false
}
await this.openEdit(formData, false)
this.currentEventId = this.eventForm.checkedEventId || ""
this.$message.success(resp.msg)
} else {
this.$message.error(resp.msg)
@@ -1,4 +1,11 @@
const REGISTER_INFO_COMPONENT = {
// 默认展示换届报告,仅由不需要查看该字段的业务页面显式关闭。
props: {
hideReplaceReport: {
type: Boolean,
default: false
}
},
template: `
<div>
<el-tabs tab-position="top" v-model="activeName">
@@ -36,7 +43,7 @@ const REGISTER_INFO_COMPONENT = {
{{viewData.createTime}}
</el-descriptions-item>
<el-descriptions-item label="会费标准" :span="4">
{{viewData.due}}
{{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}
</el-descriptions-item>
<el-descriptions-item :span="4">
<template slot="label">社团介绍</template>
@@ -66,7 +73,7 @@ const REGISTER_INFO_COMPONENT = {
<template slot="label">其他附件</template>
<file-preview :files="viewData.files" complete_result></file-preview>
</el-descriptions-item>
<el-descriptions-item :span="2">
<el-descriptions-item :span="2" v-if="!hideReplaceReport">
<template slot="label">换届报告</template>
<file-preview :files="viewData.replaceReport" complete_result></file-preview>
</el-descriptions-item>
@@ -68,10 +68,12 @@ layout("/layouts/platform.html"){
</el-steps>
<!-- 第一步:基础信息表单 -->
<!-- 本表单自行提示首个必填错误,关闭全局校验弹窗增强以免重复提示。 -->
<el-form
v-show="step === 0"
:model="formData"
ref="baseForm"
no-auto-enhance
:rules="baseRules"
label-width="150px"
class="main-form"
@@ -135,7 +137,9 @@ layout("/layouts/platform.html"){
v-model="formData.due"
placeholder="请输入会费标准"
readonly
></el-input>
>
<template slot="append">元/年</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
@@ -146,7 +150,7 @@ layout("/layouts/platform.html"){
<el-form-item
class="is-required"
label="本年度活动开展情况"
prop="yearActivity"
prop="yearActivityList"
>
<span class="tip-text">说明:非赛事活动无需填写赛事举办单位、获奖情况。</span>
<el-table
@@ -638,6 +642,7 @@ layout("/layouts/platform.html"){
id: GetQueryString("bizId"),
taskId: GetQueryString("taskId"),
step: 0, // 当前步骤(0-基础信息,1-财务收支,2-成员名单)
formLoading: false,
isRegistered: false, // 是否已登记
currentClubId: "", // 当前选中社团ID
@@ -656,7 +661,7 @@ layout("/layouts/platform.html"){
year: new Date().getFullYear() + "", // 默认当前年度
clubId: "",
foundTime: "",
due: "",
due: null,
clubName: "",
yearActivityList: [{}], // 本年度活动列表
plans: [{}], // 下年度计划列表
@@ -671,7 +676,10 @@ layout("/layouts/platform.html"){
},
// 表单校验规则
baseRules: {},
baseRules: {
yearActivityList: [{validator: this.validateActivityRows, trigger: ['change', 'blur']}],
plans: [{validator: this.validateActivityRows, trigger: ['change', 'blur']}]
},
financeRules: {},
memberRules: {}
};
@@ -724,6 +732,28 @@ layout("/layouts/platform.html"){
}
},
methods: {
// 在列表级逐行校验,避免嵌套表单项未注册、空数组或纯空白文本绕过必填。
validateActivityRows(rule, value, callback) {
const currentYear = rule.field === 'yearActivityList';
const label = currentYear ? '本年度活动开展情况' : '下一年度活动计划';
const fields = currentYear
? [['activityTime', '活动时间'], ['activityName', '活动主题/赛事名称'], ['activityAddress', '活动地点'], ['joinNum', '参加人数']]
: [['activityName', '活动主题/赛事名称'], ['activityUnit', '活动/赛事主办单位'], ['activityAddress', '活动地点']];
if (!Array.isArray(value) || value.length === 0) {
callback(new Error(label + '至少填写一条'));
return;
}
for (let i = 0; i < value.length; i++) {
for (const field of fields) {
const content = value[i] && value[i][field[0]];
if (content === null || content === undefined || String(content).trim() === '') {
callback(new Error(label + '第' + (i + 1) + '条:' + field[1] + '不能为空'));
return;
}
}
}
callback();
},
/************************ 通用工具方法 ************************/
// 格式化数字(保留2位小数,空值显示0)
formatNumber(num) {
@@ -1110,7 +1140,7 @@ layout("/layouts/platform.html"){
if (!val) return;
const club = this.clubOptions.find(item => item.id === val);
// 更新社团基础信息
this.formData.due = club?.due || "";
this.formData.due = club && club.due !== null && club.due !== undefined ? club.due : null;
this.formData.foundTime = club?.foundTime
? this.$moment(club.foundTime).format("YYYY-MM-DD")
: this.$moment().format("YYYY-MM-DD");
@@ -1161,73 +1191,63 @@ layout("/layouts/platform.html"){
// 步骤前进
this.step++;
},
// 保存
async handleSave() {
try {
this.recalculateCensus();
const resp = await this.$axios.post("/platform/club/examine/apply/save", {
data: JSON.stringify(this.formData),
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed),
incomeCensus: JSON.stringify(this.financeData.incomeCensus)
});
if (resp.code === 0) {
this.$message.success(resp.msg);
commonUtil.pjaxPush('/platform/club/examine/mine');
// 三个操作共用基础信息校验,失败时返回第一步展示行号和必填字段。
validateBaseBeforeAction(action) {
if (this.formLoading) return;
this.$refs.baseForm.validate((valid, fields) => {
if (!valid) {
this.step = 0;
const firstError = Object.values(fields || {}).flat()[0];
this.$message.warning('温馨提示:' + (firstError ? firstError.message : '请补全基础信息必填项'));
return;
}
} catch (error) {
this.handleError(error, "保存失败");
}
action();
});
},
// 提交(新增)
async handleSubmit() {
try {
this.recalculateCensus();
await this.$confirm("确定要提交此申请吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
// 仅接收本页三个固定操作的接口后缀;data为登记实体JSON,另两项为财务数组JSON。
// 接口返回{code,msg,data},code为0时保存成功,否则展示后端校验提示。
requestExamineAction(action) {
if (this.formLoading) return;
this.recalculateCensus();
const params = {
data: JSON.stringify(this.formData),
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed),
incomeCensus: JSON.stringify(this.financeData.incomeCensus)
};
if (action === 'submitAgain') params.taskId = this.taskId;
this.formLoading = true;
return this.$axios.post('/platform/club/examine/apply/' + action, params)
.then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg);
commonUtil.pjaxPush('/platform/club/examine/mine');
} else {
this.$message.warning(resp.msg);
}
})
.catch(() => {
// 后端校验和网络异常已由全局axios拦截器提示,此处仅结束失败请求,避免再次弹窗。
})
.finally(() => {
this.formLoading = false;
});
const resp = await this.$axios.post("/platform/club/examine/apply/submit", {
data: JSON.stringify(this.formData),
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed),
incomeCensus: JSON.stringify(this.financeData.incomeCensus)
});
if (resp.code === 0) {
this.$message.success(resp.msg);
commonUtil.pjaxPush('/platform/club/examine/mine');
} else {
this.$message.warning(resp.msg);
}
} catch (error) {
if (error !== "cancel") {
this.handleError(error, "提交失败");
}
}
},
// 提交(任务处理)
async handleFinishTask() {
try {
this.recalculateCensus();
await this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
});
const resp = await this.$axios.post('/platform/club/examine/apply/submitAgain', {
data: JSON.stringify(this.formData),
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed),
incomeCensus: JSON.stringify(this.financeData.incomeCensus),
taskId: this.taskId
});
if (resp.code === 0) {
this.$message.success(resp.msg);
commonUtil.pjaxPush('/platform/club/examine/mine');
}
} catch (error) {
if (error !== "cancel") {
this.handleError(error, "提交失败");
}
}
handleSave() {
this.validateBaseBeforeAction(() => this.requestExamineAction('save'));
},
handleSubmit() {
this.validateBaseBeforeAction(() => {
this.$confirm('确定要提交此申请吗?', '提示', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'info'
}).then(() => this.requestExamineAction('submit')).catch(() => {});
});
},
handleFinishTask() {
this.validateBaseBeforeAction(() => {
this.$confirm('您确定要提交吗?', '提示', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(() => this.requestExamineAction('submitAgain')).catch(() => {});
});
}
}
});
@@ -10,7 +10,7 @@
<el-descriptions v-if="viewData" :column="3" border class="table_fixed">
<el-descriptions-item label="社团名称">{{ viewData.clubName }}</el-descriptions-item>
<el-descriptions-item label="成立时间">{{ viewData.foundTime }} </el-descriptions-item>
<el-descriptions-item label="会费标准">{{ viewData.due }}</el-descriptions-item>
<el-descriptions-item label="会费标准">{{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}</el-descriptions-item>
<el-descriptions-item span="3">
<template slot="label">工作总结</template>
<file-preview :files="viewData.summaryFiles" complete_result></file-preview>
@@ -73,7 +73,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="联系人邮箱">{{viewData.concatPersonEmail}}</el-descriptions-item>
<el-descriptions-item>
<template slot="label">会费标准</template>
{{viewData.due}}
{{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}
</el-descriptions-item>
<el-descriptions-item :span="2">
<template slot="label">当前人数</template>
@@ -63,7 +63,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="社团编码">{{viewData.clubCode}}</el-descriptions-item>
<el-descriptions-item label="社团类型">{{viewData.typeName}}</el-descriptions-item>
<el-descriptions-item label="发起人">{{viewData.sponsorName}}</el-descriptions-item>
<el-descriptions-item label="会费标准">{{viewData.due}}</el-descriptions-item>
<el-descriptions-item label="会费标准">{{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}</el-descriptions-item>
<el-descriptions-item label="当前人数">{{viewData.currentPeopleNum}}</el-descriptions-item>
<el-descriptions-item span="2">
<template slot="label">社团介绍</template>
@@ -24,7 +24,13 @@ const CLUB_FORM_TEMPLATE = {
</el-col>
<el-col :span="12">
<el-form-item label="会费标准" prop="due">
<el-input placeholder="请填写会费标准" v-model="formData.due">
<el-input
min="0"
placeholder="请填写会费标准"
step="0.01"
type="number"
v-model.number="formData.due"
>
<template slot="append">元/年</template>
</el-input>
</el-form-item>
@@ -156,12 +162,15 @@ const CLUB_FORM_TEMPLATE = {
return {
formData: {
clubName: "",
clubType: ""
clubType: "",
due: null
},
formRules: {
clubName: [{ required: true, message: "请填写社团名称", trigger: ["blur", "change"] }],
clubCode: [{ required: false, message: "请填写社团编码", trigger: ["blur", "change"] }],
clubType: [{ required: true, message: "请选择社团类型", trigger: ["blur", "change"] }],
// 会费标准只保存数字,单位“元/年”由页面统一展示。
due: [{ type: "number", min: 0, message: "会费标准必须为不小于0的数字", trigger: ["blur", "change"] }],
//concatPerson: [{ required: true, message: "请选择社团联系人", trigger: ["blur", "change"] }],
foundTime: [{ required: true, message: "请选择成立时间", trigger: ["blur", "change"] }]
}
@@ -73,7 +73,7 @@ layout("/layouts/platform.html"){
</el-card>
<template #public>
<register-info ref="registerInfoRef">
<register-info ref="registerInfoRef" :hide-replace-report="true">
<div v-if="showApprovalForm">
<div class="process-title">
{{formData.taskName}}
@@ -131,7 +131,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in organizationTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
<el-form-item label="所属工会" prop="unionId">
<el-form-item label="所属工会" prop="unionId" :required="formData.organizationType !== 'schoolUnion'">
<el-select v-model="formData.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%" :disabled="isUnionDisabled()">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
@@ -255,7 +255,9 @@ layout("/layouts/platform.html"){
}
}
const validatePeople = (rule, value, callback) => {
if (!value || value <= 0) {
if (value === null || value === undefined || value === "") {
callback(new Error("必填"))
} else if (!value || value <= 0) {
callback(new Error("人数必须大于0"))
} else if (this.batchForm.minGroupPeople && this.batchForm.maxGroupPeople
&& this.batchForm.minGroupPeople > this.batchForm.maxGroupPeople) {
@@ -317,15 +319,15 @@ layout("/layouts/platform.html"){
},
batchRules: {
lineId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
signupStartTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
signupEndTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
travelStartTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
travelEndTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
signupStartTime: [{ required: true, validator: validateBatchTime, trigger: ["blur", "change"] }],
signupEndTime: [{ required: true, validator: validateBatchTime, trigger: ["blur", "change"] }],
travelStartTime: [{ required: true, validator: validateBatchTime, trigger: ["blur", "change"] }],
travelEndTime: [{ required: true, validator: validateBatchTime, trigger: ["blur", "change"] }],
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactPhone: [{ validator: validateMobile, trigger: ["blur", "change"] }],
minGroupPeople: [{ validator: validatePeople, trigger: ["blur", "change"] }],
maxGroupPeople: [{ validator: validatePeople, trigger: ["blur", "change"] }],
estimatedCost: [{ validator: validateMoney, trigger: ["blur", "change"] }]
contactPhone: [{ required: true, validator: validateMobile, trigger: ["blur", "change"] }],
minGroupPeople: [{ required: true, validator: validatePeople, trigger: ["blur", "change"] }],
maxGroupPeople: [{ required: true, validator: validatePeople, trigger: ["blur", "change"] }],
estimatedCost: [{ required: true, validator: validateMoney, trigger: ["blur", "change"] }]
}
}
},
@@ -216,7 +216,7 @@ layout("/layouts/platform.html"){
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="身份证号">
<el-form-item label="身份证号" prop="idCard" :rules="requiredRules" required>
<el-input v-model="signupForm.idCard" readonly></el-input>
</el-form-item>
</el-col>
@@ -233,8 +233,8 @@ layout("/layouts/platform.html"){
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="手机号" required>
<el-input v-model="signupForm.mobile" maxlength="30" placeholder="请输入手机号"></el-input>
<el-form-item label="手机号" prop="mobile" :rules="mobileRules" required>
<el-input v-model="signupForm.mobile" maxlength="11" placeholder="请输入手机号"></el-input>
</el-form-item>
</el-col>
</el-row>
@@ -325,17 +325,17 @@ layout("/layouts/platform.html"){
<el-form :model="directRelativeForm" label-width="110px" ref="directRelativeForm">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="亲属姓名">
<el-form-item label="亲属姓名" prop="relativeName" :rules="requiredRules" required>
<el-input v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="所在单位">
<el-form-item label="所在单位" prop="unitName" :rules="requiredRules" required>
<el-input v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="亲属关系">
<el-form-item label="亲属关系" prop="relationshipCode" :rules="requiredRules" required>
<el-select v-model="directRelativeForm.relationshipCode" clearable placeholder="请选择亲属关系" style="width: 100%" @change="directRelativeChange">
<el-option v-for="item in directRelativeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
@@ -344,12 +344,12 @@ layout("/layouts/platform.html"){
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="线路名称">
<el-form-item label="线路名称" prop="lineName" :rules="requiredRules" required>
<el-input v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出行开始日期">
<el-form-item label="出行开始日期" prop="travelStartTime" :rules="requiredRules" required>
<el-date-picker
v-model="directRelativeForm.travelStartTime"
type="date"
@@ -360,7 +360,7 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出行结束日期">
<el-form-item label="出行结束日期" prop="travelEndTime" :rules="requiredRules" required>
<el-date-picker
v-model="directRelativeForm.travelEndTime"
type="date"
@@ -379,34 +379,48 @@ layout("/layouts/platform.html"){
<div class="tour-actions">
<el-button size="medium" type="primary" icon="el-icon-plus" :disabled="!signupForm.hasFamily" @click="addFamily">增加一条</el-button>
</div>
<el-table :data="familyData" border :size="tableSize" empty-text="暂无亲属信息">
<el-form :model="{ rows: familyData }" ref="familyForm" label-width="0">
<el-table class="tour-family-table" :data="familyData" border :size="tableSize" empty-text="暂无亲属信息">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" min-width="130" align="center" header-align="center">
<template slot-scope="{row}">
<template slot="header"><span style="color: #f56c6c">*</span> 家属姓名</template>
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.familyName'" :rules="requiredRules">
<el-input v-model="row.familyName" maxlength="100" placeholder="请输入"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="性别" width="110" align="center" header-align="center">
<template slot-scope="{row}">
<template slot="header"><span style="color: #f56c6c">*</span> 性别</template>
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.gender'" :rules="requiredRules">
<el-select v-model="row.gender" clearable placeholder="请选择">
<el-option label="男" value="男"></el-option>
<el-option label="女" value="女"></el-option>
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="身份证号码" min-width="180" align="center" header-align="center">
<template slot-scope="{row}">
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.idCard'" :rules="[{ required: familyContactRequired(row), validator: validateFamilyIdCard, trigger: ['blur', 'change'] }]">
<el-input v-model="row.idCard" maxlength="18" :placeholder="familyContactRequired(row) ? '16岁及以上必填' : '选填'" @blur="normalizeFamilyIdCard(row)"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="手机号" min-width="150" align="center" header-align="center">
<template slot-scope="{row}">
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.mobile'" :rules="[{ required: familyContactRequired(row), message: '必填', trigger: ['blur', 'change'] }]">
<el-input v-model="row.mobile" maxlength="30" :placeholder="familyContactRequired(row) ? '16岁及以上必填' : '选填'"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="年龄" width="110" align="center" header-align="center">
<template slot-scope="{row}">
<template slot="header"><span style="color: #f56c6c">*</span> 年龄</template>
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.age'" :rules="requiredRules">
<el-input-number v-model="row.age" :min="0" :max="120" :precision="0" controls-position="right" style="width: 100%"></el-input-number>
</el-form-item>
</template>
</el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" min-width="120" align="center" header-align="center">
@@ -427,10 +441,13 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
<el-table-column label="关系" width="120" align="center" header-align="center">
<template slot-scope="{row}">
<template slot="header"><span style="color: #f56c6c">*</span> 关系</template>
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.relationship'" :rules="requiredRules">
<el-select v-model="row.relationship" clearable placeholder="请选择">
<el-option v-for="item in familyRelationshipOptions" :key="item.code" :label="item.name" :value="item.name"></el-option>
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" header-align="center" fixed="right">
@@ -439,6 +456,8 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
</el-table>
<div v-if="familyValidationAttempted && familyData.length === 0" class="el-form-item__error" style="position: static">必填</div>
</el-form>
</div>
<span slot="footer" class="dialog-footer">
@@ -520,6 +539,10 @@ layout("/layouts/platform.html"){
</div>
<style>
/* 覆盖全局表格 div 文字颜色,保留输入组件下方校验提示的红色。 */
.tour-family-table.el-table td.el-table__cell .el-form-item__error {
color: #f56c6c;
}
.tour-section {
margin-top: 10px;
padding: 0 2px;
@@ -763,6 +786,12 @@ layout("/layouts/platform.html"){
fillBedInfo: true,
signupForm: {},
familyData: [],
familyValidationAttempted: false,
requiredRules: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
mobileRules: [
{ required: true, message: "必填", trigger: ["blur", "change"] },
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的11位手机号", trigger: ["blur", "change"] }
],
bedTypeOptions: [],
familyRelationshipOptions: [],
directRelativeOptions: [],
@@ -1161,6 +1190,11 @@ layout("/layouts/platform.html"){
this.signupForm.hasFamily = true
}
this.signupVisible = true
this.$nextTick(() => {
;[this.$refs.signupForm, this.$refs.directRelativeForm, this.$refs.familyForm].forEach(form => {
if (form) form.clearValidate()
})
})
} else {
this.$message.warning(res.msg || "查询失败")
}
@@ -1231,29 +1265,22 @@ layout("/layouts/platform.html"){
this.signupForm.hasFamily = false
}
},
// 家属表格按行校验,缺失项显示在对应输入组件下方。
validateFamilies() {
if (!this.allowFamily || !this.signupForm.hasFamily) return true
if (this.familyData.length === 0) {
this.$message.warning("请添加亲属信息")
return false
this.familyValidationAttempted = true
if (this.familyData.length === 0) return false
return new Promise(resolve => this.$refs.familyForm.validate(valid => resolve(valid)))
},
// 未满16岁的家属允许不填身份证,已填写时仍检查格式。
validateFamilyIdCard(rule, value, callback) {
if (!value) {
callback(rule.required ? new Error("必填") : undefined)
} else if (!this.isValidIdCard(value)) {
callback(new Error("请输入正确的身份证号码"))
} else {
callback()
}
const invalidBase = this.familyData.some(item => !item.familyName || item.age === null || item.age === undefined
|| !item.gender || !item.relationship)
if (invalidBase) {
this.$message.warning("请完善亲属姓名、年龄、性别和与本人关系")
return false
}
const invalidContact = this.familyData.some(item => this.familyContactRequired(item) && (!item.idCard || !item.mobile))
if (invalidContact) {
this.$message.warning("年满16周岁的亲属必须填写身份证号码和手机号")
return false
}
const invalidIdCard = this.familyData.some(item => item.idCard && !this.isValidIdCard(item.idCard))
if (invalidIdCard) {
this.$message.warning("请输入正确的身份证号码")
return false
}
return true
},
familyContactRequired(family) {
return family && family.age !== null && family.age !== undefined && Number(family.age) >= 16
@@ -1269,39 +1296,17 @@ layout("/layouts/platform.html"){
},
validateDirectRelative() {
if (!this.isDirectFamilyLine(this.signupForm)) return true
if (!this.directRelativeForm.relativeName) {
this.$message.warning("请输入亲属姓名")
return false
}
if (!this.directRelativeForm.unitName) {
this.$message.warning("请输入亲属所在单位")
return false
}
if (!this.directRelativeForm.relationshipCode) {
this.$message.warning("请选择亲属关系")
return false
}
if (!this.directRelativeForm.lineName) {
this.$message.warning("请输入直系亲属线路名称")
return false
}
if (!this.directRelativeForm.travelStartTime) {
this.$message.warning("请选择出行开始日期")
return false
}
if (!this.directRelativeForm.travelEndTime) {
this.$message.warning("请选择出行结束日期")
return false
}
return true
return new Promise(resolve => this.$refs.directRelativeForm.validate(valid => resolve(valid)))
},
submitSignup() {
if (!this.signupForm.idCard || !this.signupForm.mobile) {
this.$message.warning("请完善报名人身份证号码和手机号")
return
}
if (!this.validateFamilies()) return
if (!this.validateDirectRelative()) return
async submitSignup() {
if (this.signupLoading) return
// 同时校验各区域,一次展示所有字段错误,通过后才提交报名。
const results = await Promise.all([
new Promise(resolve => this.$refs.signupForm.validate(valid => resolve(valid))),
this.validateFamilies(),
this.validateDirectRelative()
])
if (results.some(valid => !valid)) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
const form = Object.assign({}, this.signupForm, {
families: JSON.stringify(families),
@@ -1352,6 +1357,7 @@ layout("/layouts/platform.html"){
}).catch(() => {})
},
destroySignup() {
this.familyValidationAttempted = false
this.signupForm = {}
this.familyData = []
this.directRelativeForm = {}
@@ -182,7 +182,7 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-col>
<el-col v-if="formData.outProvinceRatioType === '固定人数'" :span="12">
<el-form-item label="固定人数" prop="outProvinceFixedPeople">
<el-form-item label="固定人数" prop="outProvinceFixedPeople" :required="formData.outProvinceRatioType === '固定人数'">
<el-input-number v-model="formData.outProvinceFixedPeople" :controls="false" :min="0" :precision="0" placeholder="请输入固定人数" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
@@ -366,7 +366,9 @@ layout("/layouts/platform.html"){
}
const checkOutProvinceFixedPeople = (rule, value, callback) => {
if (this.formData.outProvinceRatioType === "固定人数"
&& (value === null || value === undefined || value === "" || value < 0)) {
&& (value === null || value === undefined || value === "")) {
callback(new Error("必填"))
} else if (this.formData.outProvinceRatioType === "固定人数" && value < 0) {
callback(new Error("固定人数必须大于等于0"))
} else {
callback()
@@ -113,7 +113,7 @@ layout("/layouts/platform.html"){
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="身份证号">
<el-form-item label="身份证号" prop="idCard" :rules="requiredRules" required>
<el-input v-model="signupForm.idCard" readonly></el-input>
</el-form-item>
</el-col>
@@ -130,8 +130,8 @@ layout("/layouts/platform.html"){
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="手机号" required>
<el-input v-model="signupForm.mobile" maxlength="30" placeholder="请输入手机号"></el-input>
<el-form-item label="手机号" prop="mobile" :rules="mobileRules" required>
<el-input v-model="signupForm.mobile" maxlength="11" placeholder="请输入手机号"></el-input>
</el-form-item>
</el-col>
</el-row>
@@ -222,17 +222,17 @@ layout("/layouts/platform.html"){
<el-form :model="directRelativeForm" label-width="110px" ref="directRelativeForm">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="亲属姓名">
<el-form-item label="亲属姓名" prop="relativeName" :rules="requiredRules" required>
<el-input v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="所在单位">
<el-form-item label="所在单位" prop="unitName" :rules="requiredRules" required>
<el-input v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="亲属关系">
<el-form-item label="亲属关系" prop="relationshipCode" :rules="requiredRules" required>
<el-select v-model="directRelativeForm.relationshipCode" clearable placeholder="请选择亲属关系" style="width: 100%" @change="directRelativeChange">
<el-option v-for="item in directRelativeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
@@ -241,12 +241,12 @@ layout("/layouts/platform.html"){
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="线路名称">
<el-form-item label="线路名称" prop="lineName" :rules="requiredRules" required>
<el-input v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出行开始日期">
<el-form-item label="出行开始日期" prop="travelStartTime" :rules="requiredRules" required>
<el-date-picker
v-model="directRelativeForm.travelStartTime"
type="date"
@@ -257,7 +257,7 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出行结束日期">
<el-form-item label="出行结束日期" prop="travelEndTime" :rules="requiredRules" required>
<el-date-picker
v-model="directRelativeForm.travelEndTime"
type="date"
@@ -276,34 +276,48 @@ layout("/layouts/platform.html"){
<div class="tour-signup-actions">
<el-button size="medium" type="primary" icon="el-icon-plus" :disabled="!signupForm.hasFamily" @click="addFamily">增加一条</el-button>
</div>
<el-table :data="familyData" border :size="tableSize" empty-text="暂无亲属信息">
<el-form :model="{ rows: familyData }" ref="familyForm" label-width="0">
<el-table class="tour-family-table" :data="familyData" border :size="tableSize" empty-text="暂无亲属信息">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" min-width="130" align="center" header-align="center">
<template slot-scope="{row}">
<template slot="header"><span style="color: #f56c6c">*</span> 家属姓名</template>
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.familyName'" :rules="requiredRules">
<el-input v-model="row.familyName" maxlength="100" placeholder="请输入"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="性别" width="110" align="center" header-align="center">
<template slot-scope="{row}">
<template slot="header"><span style="color: #f56c6c">*</span> 性别</template>
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.gender'" :rules="requiredRules">
<el-select v-model="row.gender" clearable placeholder="请选择">
<el-option label="男" value="男"></el-option>
<el-option label="女" value="女"></el-option>
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="身份证号码" min-width="180" align="center" header-align="center">
<template slot-scope="{row}">
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.idCard'" :rules="[{ required: familyContactRequired(row), validator: validateFamilyIdCard, trigger: ['blur', 'change'] }]">
<el-input v-model="row.idCard" maxlength="18" :placeholder="familyContactRequired(row) ? '16岁及以上必填' : '选填'" @blur="normalizeFamilyIdCard(row)"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="手机号" min-width="150" align="center" header-align="center">
<template slot-scope="{row}">
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.mobile'" :rules="[{ required: familyContactRequired(row), message: '必填', trigger: ['blur', 'change'] }]">
<el-input v-model="row.mobile" maxlength="30" :placeholder="familyContactRequired(row) ? '16岁及以上必填' : '选填'"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="年龄" width="110" align="center" header-align="center">
<template slot-scope="{row}">
<template slot="header"><span style="color: #f56c6c">*</span> 年龄</template>
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.age'" :rules="requiredRules">
<el-input-number v-model="row.age" :min="0" :max="120" :precision="0" controls-position="right" style="width: 100%"></el-input-number>
</el-form-item>
</template>
</el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" min-width="120" align="center" header-align="center">
@@ -336,10 +350,13 @@ layout("/layouts/platform.html"){
</el-table-column>
-->
<el-table-column label="关系" width="120" align="center" header-align="center">
<template slot-scope="{row}">
<template slot="header"><span style="color: #f56c6c">*</span> 关系</template>
<template slot-scope="{row, $index}">
<el-form-item :prop="'rows.' + $index + '.relationship'" :rules="requiredRules">
<el-select v-model="row.relationship" clearable placeholder="请选择">
<el-option v-for="item in familyRelationshipOptions" :key="item.code" :label="item.name" :value="item.name"></el-option>
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" header-align="center" fixed="right">
@@ -348,6 +365,8 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
</el-table>
<div v-if="familyValidationAttempted && familyData.length === 0" class="el-form-item__error" style="position: static">必填</div>
</el-form>
</div>
<span slot="footer" class="dialog-footer">
@@ -389,6 +408,10 @@ layout("/layouts/platform.html"){
</div>
<style>
/* 覆盖全局表格 div 文字颜色,保留输入组件下方校验提示的红色。 */
.tour-family-table.el-table td.el-table__cell .el-form-item__error {
color: #f56c6c;
}
.tour-signup-title {
border-left: 4px solid #0079c2;
color: #0079c2;
@@ -489,6 +512,12 @@ layout("/layouts/platform.html"){
fillBedInfo: true,
signupForm: {},
familyData: [],
familyValidationAttempted: false,
requiredRules: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
mobileRules: [
{ required: true, message: "必填", trigger: ["blur", "change"] },
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的11位手机号", trigger: ["blur", "change"] }
],
bedTypeOptions: [],
familyRelationshipOptions: [],
directRelativeOptions: [],
@@ -791,6 +820,11 @@ layout("/layouts/platform.html"){
this.signupForm.hasFamily = true
}
this.signupVisible = true
this.$nextTick(() => {
;[this.$refs.signupForm, this.$refs.directRelativeForm, this.$refs.familyForm].forEach(form => {
if (form) form.clearValidate()
})
})
} else {
this.$message.warning(res.msg || "查询失败")
}
@@ -847,29 +881,22 @@ layout("/layouts/platform.html"){
this.signupForm.hasFamily = false
}
},
// 家属表格按行校验,缺失项显示在对应输入组件下方。
validateFamilies() {
if (!this.allowFamily || !this.signupForm.hasFamily) return true
if (this.familyData.length === 0) {
this.$message.warning("请添加亲属信息")
return false
this.familyValidationAttempted = true
if (this.familyData.length === 0) return false
return new Promise(resolve => this.$refs.familyForm.validate(valid => resolve(valid)))
},
// 未满16岁的家属允许不填身份证,已填写时仍检查格式。
validateFamilyIdCard(rule, value, callback) {
if (!value) {
callback(rule.required ? new Error("必填") : undefined)
} else if (!this.isValidIdCard(value)) {
callback(new Error("请输入正确的身份证号码"))
} else {
callback()
}
const invalidBase = this.familyData.some(item => !item.familyName || item.age === null || item.age === undefined
|| !item.gender || !item.relationship)
if (invalidBase) {
this.$message.warning("请完善亲属姓名、年龄、性别和与本人关系")
return false
}
const invalidContact = this.familyData.some(item => this.familyContactRequired(item) && (!item.idCard || !item.mobile))
if (invalidContact) {
this.$message.warning("年满16周岁的亲属必须填写身份证号码和手机号")
return false
}
const invalidIdCard = this.familyData.some(item => item.idCard && !this.isValidIdCard(item.idCard))
if (invalidIdCard) {
this.$message.warning("请输入正确的身份证号码")
return false
}
return true
},
familyContactRequired(family) {
return family && family.age !== null && family.age !== undefined && Number(family.age) >= 16
@@ -885,39 +912,17 @@ layout("/layouts/platform.html"){
},
validateDirectRelative() {
if (!this.isDirectFamilyLine(this.signupForm)) return true
if (!this.directRelativeForm.relativeName) {
this.$message.warning("请输入亲属姓名")
return false
}
if (!this.directRelativeForm.unitName) {
this.$message.warning("请输入亲属所在单位")
return false
}
if (!this.directRelativeForm.relationshipCode) {
this.$message.warning("请选择亲属关系")
return false
}
if (!this.directRelativeForm.lineName) {
this.$message.warning("请输入直系亲属线路名称")
return false
}
if (!this.directRelativeForm.travelStartTime) {
this.$message.warning("请选择出行开始日期")
return false
}
if (!this.directRelativeForm.travelEndTime) {
this.$message.warning("请选择出行结束日期")
return false
}
return true
return new Promise(resolve => this.$refs.directRelativeForm.validate(valid => resolve(valid)))
},
submitSignup() {
if (!this.signupForm.idCard || !this.signupForm.mobile) {
this.$message.warning("请完善报名人身份证号码和手机号")
return
}
if (!this.validateFamilies()) return
if (!this.validateDirectRelative()) return
async submitSignup() {
if (this.signupLoading) return
// 同时校验各区域,一次展示所有字段错误,通过后才提交报名。
const results = await Promise.all([
new Promise(resolve => this.$refs.signupForm.validate(valid => resolve(valid))),
this.validateFamilies(),
this.validateDirectRelative()
])
if (results.some(valid => !valid)) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
const form = Object.assign({}, this.signupForm, {
families: JSON.stringify(families),
@@ -966,6 +971,7 @@ layout("/layouts/platform.html"){
}).catch(() => {})
},
destroySignup() {
this.familyValidationAttempted = false
this.signupForm = {}
this.familyData = []
this.directRelativeForm = {}
@@ -138,14 +138,14 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人手机" prop="contactPhone">
<el-form-item label="联系人手机" prop="contactPhone" required>
<el-input v-model="formData.contactPhone" maxlength="30" placeholder="请输入联系人手机"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-form-item label="邮箱" prop="email" required>
<el-input v-model="formData.email" maxlength="100" placeholder="请输入邮箱"></el-input>
</el-form-item>
</el-col>
@@ -221,8 +221,8 @@ layout("/layouts/platform.html"){
agencyName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
agencyCode: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactPhone: [{ validator: validateMobile, trigger: ["blur", "change"] }],
email: [{ validator: validateEmail, trigger: ["blur", "change"] }]
contactPhone: [{ required: true, validator: validateMobile, trigger: ["blur", "change"] }],
email: [{ required: true, validator: validateEmail, trigger: ["blur", "change"] }]
}
}
},
@@ -41,6 +41,30 @@ layout("/layouts/platform.html"){
box-sizing: border-box;
}
.qsv-online-answer-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding-right: 16px;
}
.qsv-online-quiz-timer {
flex-shrink: 0;
color: #e6a23c;
font-weight: 600;
}
.qsv-online-description {
line-height: 1.8;
overflow-wrap: break-word;
}
.qsv-online-description img {
max-width: 100%;
height: auto;
}
.qsv-online-preview-title {
margin: 0;
font-size: 18px;
@@ -90,6 +114,33 @@ layout("/layouts/platform.html"){
min-width: 72px;
}
.qsv-online-result-score {
color: #409eff;
font-size: 28px;
font-weight: 700;
}
.qsv-online-result-tags {
margin-bottom: 12px;
}
.qsv-online-result-option {
color: #303133;
}
.qsv-online-result-option.is-correct {
color: #67c23a;
}
.qsv-online-subject-result {
margin-top: 12px;
color: #f56c6c;
}
.qsv-online-subject-result.is-correct {
color: #67c23a;
}
.qsv-online-option-fill {
width: 520px;
max-width: 100%;
@@ -495,8 +546,22 @@ layout("/layouts/platform.html"){
size="1145px"
custom-class="qsv-online-drawer"
append-to-body
@closed="stopVoteCountdown">
@close="stopAnswerTimers">
<div slot="title" class="qsv-online-answer-header">
<span>{{answerDialog.title}}</span>
<span v-if="answerDialog.activity.category === 'QUIZ' && answerDialog.activity.timeLimit > 0 && !answerDialog.readonly" class="qsv-online-quiz-timer">
<i class="el-icon-time"></i> 剩余时间:{{quizRemainingText}}
</span>
</div>
<div class="qsv-online-preview-body" v-loading="answerDialog.loading">
<div v-if="answerDialog.activity.description" class="qsv-online-subject">
<div class="qsv-online-subject-title">说明</div>
<div class="qsv-online-description" v-html="answerDialog.activity.description"></div>
</div>
<div v-if="showQuizResult" class="qsv-online-subject">
<div class="qsv-online-subject-title">{{answerDialog.activity.title}}</div>
<div class="qsv-online-result-score">总分:{{quizTotalScore}} 分</div>
</div>
<div v-if="isVoteActivity" class="qsv-online-vote">
<div class="qsv-online-vote-header">
<h2 class="qsv-online-vote-title">{{answerDialog.activity.title}}</h2>
@@ -573,6 +638,11 @@ layout("/layouts/platform.html"){
</div>
<el-empty v-if="!isVoteActivity && visibleSubjects.length === 0 && !answerDialog.loading" description="暂无题目"></el-empty>
<div v-if="!isVoteActivity" v-for="(subject, index) in visibleSubjects" :key="subject.id" class="qsv-online-subject">
<div v-if="showQuizResult" class="qsv-online-result-tags">
<el-tag v-if="subject.type === 'radio'" size="small">单选</el-tag>
<el-tag v-else-if="subject.type === 'checkbox'" size="small">多选</el-tag>
<el-tag v-if="subject.score !== undefined && subject.score !== null" size="small">{{subject.score}}分</el-tag>
</div>
<div class="qsv-online-subject-title">
<span v-if="answerDialog.showIndex" class="qsv-online-subject-index">{{index + 1}}.</span>
<span>{{subject.title}}</span>
@@ -585,7 +655,7 @@ layout("/layouts/platform.html"){
:value="getRadioValue(subject)"
@input="setRadioValue(subject, $event)">
<template v-for="option in subject.options">
<el-radio :key="option.id" :label="option.id">{{option.text}}</el-radio>
<el-radio :key="option.id" :label="option.id"><span :class="{ 'qsv-online-result-option': showQuizResult, 'is-correct': showQuizResult && option.isCorrect }">{{option.text}}<span v-if="showQuizResult && option.isCorrect">(正确答案)</span></span></el-radio>
<el-input
v-if="isOptionFillVisible(subject, option)"
:key="option.id + '-fill'"
@@ -604,7 +674,7 @@ layout("/layouts/platform.html"){
:disabled="answerDialog.readonly"
@change="onCheckboxChange(subject)">
<template v-for="option in subject.options">
<el-checkbox :key="option.id" :label="option.id">{{option.text}}</el-checkbox>
<el-checkbox :key="option.id" :label="option.id"><span :class="{ 'qsv-online-result-option': showQuizResult, 'is-correct': showQuizResult && option.isCorrect }">{{option.text}}<span v-if="showQuizResult && option.isCorrect">(正确答案)</span></span></el-checkbox>
<el-input
v-if="isOptionFillVisible(subject, option)"
:key="option.id + '-fill'"
@@ -623,6 +693,10 @@ layout("/layouts/platform.html"){
:readonly="answerDialog.readonly"
v-model="subject.userFillContent"
:placeholder="'请输入' + subject.title"></el-input>
<div v-if="showQuizResult && getSubjectResult(subject)" class="qsv-online-subject-result" :class="{ 'is-correct': getSubjectResult(subject).isCorrect }">
<i :class="getSubjectResult(subject).isCorrect ? 'el-icon-circle-check' : 'el-icon-circle-close'"></i>
{{getSubjectResult(subject).isCorrect ? '回答正确' : '回答错误'}},得分:{{getSubjectResult(subject).score || 0}} 分
</div>
</div>
</div>
<div class="qsv-online-footer">
@@ -709,6 +783,10 @@ layout("/layouts/platform.html"){
minutes: 0,
seconds: 0
},
quizTimer: null,
quizStartedAt: 0,
quizElapsedSeconds: 0,
quizExpired: false,
voteTimer: null,
voteRankVisible: false,
voteDetailVisible: false,
@@ -717,6 +795,21 @@ layout("/layouts/platform.html"){
}
},
computed: {
// 仅已完成的计分答题显示成绩和正确答案,普通调查与投票保持原有展示。
showQuizResult() {
return this.answerDialog.activity.category === "QUIZ" && !!this.answerDialog.answerRecord.isFinish
},
quizRemainingSeconds() {
return Math.max(0, Number(this.answerDialog.activity.timeLimit || 0) * 60 - this.answerDialog.quizElapsedSeconds)
},
quizRemainingText() {
const seconds = this.quizRemainingSeconds
return Math.floor(seconds / 60) + '分' + String(seconds % 60).padStart(2, '0') + '秒'
},
quizTotalScore() {
const answers = this.answerDialog.answerRecord.extJson || {}
return Object.keys(answers).reduce((total, id) => total + Number(answers[id].score || 0), 0)
},
visibleSubjects() {
return this.answerDialog.subjects.filter((subject) => {
return this.isSubjectVisible(subject)
@@ -862,6 +955,10 @@ layout("/layouts/platform.html"){
this.$message.warning(unavailableMessage)
return
}
this.stopAnswerTimers()
this.answerDialog.quizStartedAt = 0
this.answerDialog.quizElapsedSeconds = 0
this.answerDialog.quizExpired = false
this.answerDialog.visible = true
this.answerDialog.loading = true
this.answerDialog.readonly = row.actionType === "view"
@@ -908,6 +1005,7 @@ layout("/layouts/platform.html"){
this.answerDialog.answerRecord = res.data || {}
this.answerDialog.readonly = !!this.answerDialog.answerRecord.isFinish
this.initAnswer()
this.startQuizTimer()
}
})
},
@@ -928,6 +1026,10 @@ layout("/layouts/platform.html"){
this.clearHiddenSubjectAnswers()
}
},
getSubjectResult(subject) {
const answers = this.answerDialog.answerRecord.extJson || {}
return answers[subject.id]
},
getRadioValue(subject) {
return subject.userSelectOptionIds && subject.userSelectOptionIds.length > 0
? subject.userSelectOptionIds[0]
@@ -985,6 +1087,43 @@ layout("/layouts/platform.html"){
}
})
},
// 使用实际经过时间计时,避免切换浏览器标签后定时器节流导致倒计时变慢。
startQuizTimer() {
this.stopQuizTimer()
if (!this.answerDialog.visible || this.answerDialog.readonly || this.answerDialog.activity.category !== "QUIZ") return
this.answerDialog.quizStartedAt = Date.now()
this.answerDialog.quizElapsedSeconds = 0
this.answerDialog.quizExpired = false
this.answerDialog.quizTimer = setInterval(() => this.updateQuizTimer(), 1000)
},
updateQuizTimer() {
if (!this.answerDialog.quizStartedAt || this.answerDialog.readonly || !this.answerDialog.visible) return
this.answerDialog.quizElapsedSeconds = Math.floor((Date.now() - this.answerDialog.quizStartedAt) / 1000)
if (Number(this.answerDialog.activity.timeLimit) > 0 && this.quizRemainingSeconds === 0
&& !this.answerDialog.quizExpired && !this.answerDialog.submitLoading) {
this.answerDialog.quizExpired = true
this.stopQuizTimer()
// 与手机端一致:超时提示重新答题,不自动提交未完成的答卷。
this.$alert("答题时间到,请重新进入答题", "提示", {
confirmButtonText: "确定",
showClose: false,
closeOnClickModal: false,
closeOnPressEscape: false
}).then(() => {
this.answerDialog.visible = false
})
}
},
stopQuizTimer() {
if (this.answerDialog.quizTimer) {
clearInterval(this.answerDialog.quizTimer)
this.answerDialog.quizTimer = null
}
},
stopAnswerTimers() {
this.stopQuizTimer()
this.stopVoteCountdown()
},
startVoteCountdown() {
this.stopVoteCountdown()
this.updateVoteCountdown()
@@ -1196,6 +1335,8 @@ layout("/layouts/platform.html"){
return true
},
submitAnswer() {
this.updateQuizTimer()
if (this.answerDialog.readonly || this.answerDialog.quizExpired || this.answerDialog.submitLoading) return
if (!this.validateAnswer()) {
return
}
@@ -1204,11 +1345,14 @@ layout("/layouts/platform.html"){
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.updateQuizTimer()
if (!this.answerDialog.visible || this.answerDialog.readonly || this.answerDialog.quizExpired || this.answerDialog.submitLoading) return
this.answerDialog.submitLoading = true
$.post("/platform/qsv/online/submitAnswer", {
answer: JSON.stringify({
activityId: this.answerDialog.activity.id,
answerRecordId: this.answerDialog.answerRecordId,
answerTime: this.answerDialog.quizElapsedSeconds,
subjects: this.submitSubjects.map((subject) => {
return {
id: subject.id,
@@ -1221,7 +1365,14 @@ layout("/layouts/platform.html"){
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.answerDialog.visible = false
if (this.answerDialog.activity.category === "QUIZ") {
// 提交后读取服务端判题结果,直接展示成绩并防止重复提交。
this.answerDialog.readonly = true
this.stopQuizTimer()
this.loadAnswerRecord()
} else {
this.answerDialog.visible = false
}
this.pageData()
} else {
this.$message.error(res.msg)
@@ -1237,7 +1388,7 @@ layout("/layouts/platform.html"){
this.openAnswerFromUrl()
},
beforeDestroy() {
this.stopVoteCountdown()
this.stopAnswerTimers()
}
})
</script>
@@ -76,8 +76,7 @@ layout("/layouts/platform.html"){
<el-button v-if="row.canRevoke" size="mini" type="danger" @click="openRevoke(row)">
撤回
</el-button>
<!-- v-if="row.taskKey === 'startTask' || !row.instanceId"-->
<el-button size="mini" type="danger" @click="del(row.id)">
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" size="mini" type="danger" @click="del(row.id)">
删除
</el-button>
</template>
@@ -62,13 +62,7 @@ layout("/layouts/platform.html"){
<el-button @click="onView(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-dropdown v-if="row.createdBy === $store.state.user.id && row.instanceState == 20" trigger="click" @command="format => doExportApply(row, format)">
<el-button size="mini" type="primary">导出</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="pdf">导出 PDF</el-dropdown-item>
<el-dropdown-item command="word">导出 Word</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button v-if="row.createdBy === $store.state.user.id && row.instanceState == 20" @click="doExportApply(row)" size="mini" type="primary">导出</el-button>
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" @click="onDelete(row.id)" size="mini" type="danger">
删除
</el-button>
@@ -109,8 +103,8 @@ layout("/layouts/platform.html"){
}
},
methods: {
doExportApply(row, format) {
this.$downLoad('/platform/condolence/mine/doExportApply?id=' + row.id + '&print=' + (format === 'pdf'))
doExportApply(row) {
this.$downLoad('/platform/condolence/mine/doExportApply?id=' + row.id + '&print=false')
},
onAdd() {
commonUtil.pjaxPush('/platform/condolence/apply')
@@ -1,8 +1,9 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<el-card shadow="never" v-if="isShow">
<div id="app" v-cloak>
<el-card shadow="never" v-if="pageLoading" v-loading="pageLoading" element-loading-text="正在加载申请信息" style="min-height: 300px"></el-card>
<el-card shadow="never" v-else-if="isShow">
<snaker-start slot="header" label="困难帮扶申请" define_key="KNBF"></snaker-start>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":" class="flow-task-form">
<el-descriptions :column="3" border>
@@ -318,6 +319,7 @@ layout("/layouts/platform.html"){
bizId: GetQueryString("bizId"),
taskId: GetQueryString("taskId"),
isShow: false,
pageLoading: true,
time: '',
blockMessage: '',
formData: {
@@ -453,10 +455,12 @@ layout("/layouts/platform.html"){
})
},
findOne(id) {
this.$axios.post('/platform/difficultHelp/mine/findOne', { id }).then(res => {
return this.$axios.post('/platform/difficultHelp/mine/findOne', { id }).then(res => {
if (res.code === 0) {
this.formData = res.data
this.queryRecipients(res.data.loginName)
} else {
throw new Error(res.msg || '申请信息加载失败,请刷新页面重试')
}
})
},
@@ -617,7 +621,7 @@ layout("/layouts/platform.html"){
return { status: 200, msg: "卡号有效。" };
},
async getIsCanPlay() {
this.$axios.post('/platform/difficultHelp/apply/getIsCanPlay').then(res => {
return this.$axios.post('/platform/difficultHelp/apply/getIsCanPlay').then(res => {
if (res.code === 0) {
// 新增申请进入页面时,优先校验当前登录人本年度是否已经申请过,已申请则直接拦截新增。
if (!this.bizId && res.data.hasCurrentYearApply && !this.$auth.hasRoleOr('BRANCH_UNION_CHAIRMAN')) {
@@ -631,16 +635,25 @@ layout("/layouts/platform.html"){
if (res.data.applyCount !== null && res.data.applyCount !== undefined && res.data.applyCount !== 0) {
this.$set(this.formData, 'applyCount', res.data.applyCount)
}
} else {
throw new Error(res.msg || '申请资格加载失败,请刷新页面重试')
}
})
}
},
async created() {
await this.getIsCanPlay();
if (this.bizId) {
this.findOne(this.bizId)
} else {
this.modeChange("1")
try {
await this.getIsCanPlay();
if (this.bizId) {
await this.findOne(this.bizId)
} else {
this.modeChange("1")
}
} catch (error) {
this.isShow = false
this.blockMessage = error.message || '申请信息加载失败,请刷新页面重试'
} finally {
this.pageLoading = false
}
}
})
@@ -1,17 +1,5 @@
<div id="difficult_help_branch_union_approval_form">
<el-form :model="formData" ref="formRef" label-width="100px" label-suffix=":">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="级别" prop="level" :rules="[{required:true,message:'请选择级别',trigger:['change','blur']}]">
<dict-select v-model="formData.level" code="DIFFICULT_LEVEL" style="width: 100%" placeholder="请选择级别"></dict-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="帮扶金额" prop="money" :rules="[{required:true,message:'请填写帮扶金额',trigger:['change','blur']}]">
<el-input-number v-model="formData.money" style="width: 100%" :precision="2" :min="0" placeholder="请输入帮扶金额"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="审批意见" prop="tf_opinion" :rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
@@ -29,8 +17,6 @@
return {
businessId: GetQueryString("businessId"),
formData: {
level: "",
money: undefined,
tf_sign: "", tf_opinion: ""
},
}
@@ -84,18 +84,6 @@ layout("/layouts/platform.html"){
<el-dialog title="设置帮扶审批信息" :visible.sync="batchVisible" width="760px">
<el-form :model="batchForm" ref="batchFormRef" label-width="130px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="级别" prop="level" :rules="[{required:true,message:'请选择级别',trigger:['change','blur']}]">
<dict-select v-model="batchForm.level" code="DIFFICULT_LEVEL" style="width: 100%" placeholder="请选择级别"></dict-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="帮扶金额(元)" prop="money" :rules="[{required:true,message:'请填写帮扶金额',trigger:['change','blur']}]">
<el-input-number v-model="batchForm.money" style="width: 100%" :precision="2" :min="0" placeholder="请输入帮扶金额"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="审批意见" prop="tf_opinion" :rules="[{required:true,whitespace:true,message:'请填写审批意见',trigger:['change','blur']}]">
<user-opinion-textarea v-model="batchForm.tf_opinion"></user-opinion-textarea>
</el-form-item>
@@ -113,18 +101,6 @@ layout("/layouts/platform.html"){
{{formData.taskName}}
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="100px" label-suffix=":">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="级别" prop="level" :rules="[{required:true,message:'请选择级别',trigger:['change','blur']}]">
<dict-select v-model="formData.level" code="DIFFICULT_LEVEL" style="width: 100%" placeholder="请选择级别"></dict-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="帮扶金额" prop="money" :rules="[{required:true,message:'请填写帮扶金额',trigger:['change','blur']}]">
<el-input-number v-model="formData.money" style="width: 100%" :precision="2" :min="0" placeholder="请输入帮扶金额"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="审批意见" prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
@@ -178,7 +154,7 @@ layout("/layouts/platform.html"){
formData: {},
batchVisible: false,
batchForm: {level: "", money: undefined, tf_sign: "", tf_opinion: ""},
batchForm: {tf_sign: "", tf_opinion: ""},
showApprovalForm: false,
selectData: []
}
@@ -208,7 +184,7 @@ layout("/layouts/platform.html"){
return;
}
this.batchForm = {level: "", money: undefined, tf_sign: "", tf_opinion: ""}
this.batchForm = {tf_sign: "", tf_opinion: ""}
this.batchVisible = true
},
async submitBatch() {
@@ -295,8 +271,6 @@ layout("/layouts/platform.html"){
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName,
level: "",
money: undefined,
tf_sign: "", tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
@@ -72,13 +72,7 @@ layout("/layouts/platform.html"){
</el-button>
<el-button v-if="row.createdBy === $store.state.user.id && row.canRevoke" @click="onRevoke(row)" size="mini" type="danger">撤回
</el-button>
<el-dropdown v-if="row.createdBy === $store.state.user.id && row.instanceState == 20" trigger="click" @command="format => doExportApply(row, format)">
<el-button size="mini" type="primary">导出</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="pdf">导出 PDF</el-dropdown-item>
<el-dropdown-item command="word">导出 Word</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button v-if="row.createdBy === $store.state.user.id && row.instanceState == 20" @click="doExportApply(row)" size="mini" type="primary">导出</el-button>
<el-button v-if="row.createdBy === $store.state.user.id && (row.taskKey === 'startTask' || !row.instanceId)" @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
@@ -187,8 +181,8 @@ layout("/layouts/platform.html"){
this.units = await this.$businessTool.listUnit((this.$store.state.user.union || {}).id || this.pageForm.unionId)
}
},
doExportApply(row, format) {
this.$downLoad('/platform/difficultHelp/reading/doExportApply?id=' + row.id + '&print=' + (format === 'pdf'))
doExportApply(row) {
this.$downLoad('/platform/difficultHelp/reading/doExportApply?id=' + row.id + '&print=false')
}
},
async created() {
@@ -178,11 +178,7 @@ layout("/layouts/platform.html"){
},
async flushUnits(){
this.$set(this.pageForm, "unitId", null)
if (this.$auth.hasRoleOr('SYSADMIN, SCHOOL_UNION_ADMIN')) {
this.units = await this.$businessTool.listUnit(this.pageForm.unionId)
} else {
this.units = await this.$businessTool.listUnit((this.$store.state.user.union || {}).id || this.pageForm.unionId)
}
this.units = await this.$businessTool.listUnit(this.pageForm.unionId)
},
exportYearPayRecord(){
this.$downLoad('/platform/difficultHelp/reading/exportYearPayRecord', this.pageForm)
@@ -204,11 +200,7 @@ layout("/layouts/platform.html"){
},
async created() {
this.pageData()
if (this.$auth.hasRoleOr('SYSADMIN, SCHOOL_UNION_ADMIN')) {
this.unions = await this.$businessTool.listUnion()
} else {
this.unions = await this.$businessTool.listUnion((this.$store.state.user.union || {}).id)
}
this.unions = await this.$businessTool.listUnion()
}
})
</script>
@@ -1,11 +1,6 @@
<div id="difficult_help_branch_union_approval_form">
<el-form :model="formData" ref="formRef" label-width="100px" label-suffix=":">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="级别" prop="level" :rules="[{required:true,message:'请选择级别',trigger:['change','blur']}]">
<dict-select v-model="formData.level" code="DIFFICULT_LEVEL" style="width: 100%" placeholder="请选择级别"></dict-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="帮扶金额" prop="money" :rules="[{required:true,message:'请填写帮扶金额',trigger:['change','blur']}]">
<el-input-number v-model="formData.money" style="width: 100%" :precision="2" :min="0" placeholder="请输入帮扶金额"></el-input-number>
@@ -29,7 +24,6 @@
return {
businessId: GetQueryString("businessId"),
formData: {
level: "",
money: undefined,
tf_sign: "", tf_opinion: ""
},
@@ -84,11 +84,6 @@ layout("/layouts/platform.html"){
<el-dialog title="设置帮扶审批信息" :visible.sync="batchVisible" width="760px">
<el-form :model="batchForm" ref="batchFormRef" label-width="130px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="级别" prop="level" :rules="[{required:true,message:'请选择级别',trigger:['change','blur']}]">
<dict-select v-model="batchForm.level" code="DIFFICULT_LEVEL" style="width: 100%" placeholder="请选择级别"></dict-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="帮扶金额(元)" prop="money" :rules="[{required:true,message:'请填写帮扶金额',trigger:['change','blur']}]">
<el-input-number v-model="batchForm.money" style="width: 100%" :precision="2" :min="0" placeholder="请输入帮扶金额"></el-input-number>
@@ -113,11 +108,6 @@ layout("/layouts/platform.html"){
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="100px" label-suffix=":">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="级别" prop="level" :rules="[{required:true,message:'请选择级别',trigger:['change','blur']}]">
<dict-select v-model="formData.level" code="DIFFICULT_LEVEL" style="width: 100%" placeholder="请选择级别"></dict-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="帮扶金额" prop="money" :rules="[{required:true,message:'请填写帮扶金额',trigger:['change','blur']}]">
<el-input-number v-model="formData.money" style="width: 100%" :precision="2" :min="0" placeholder="请输入帮扶金额"></el-input-number>
@@ -177,7 +167,7 @@ layout("/layouts/platform.html"){
formData: {},
batchVisible: false,
batchForm: {level: "", money: undefined, tf_sign: "", tf_opinion: ""},
batchForm: {money: undefined, tf_sign: "", tf_opinion: ""},
showApprovalForm: false,
selectData: [],
}
@@ -201,7 +191,7 @@ layout("/layouts/platform.html"){
this.$message.warning('请勾选待审核数据!');
return;
}
this.batchForm = {level: "", money: undefined, tf_sign: "", tf_opinion: ""}
this.batchForm = {money: undefined, tf_sign: "", tf_opinion: ""}
this.batchVisible = true
},
async submitBatch() {
@@ -288,7 +278,6 @@ layout("/layouts/platform.html"){
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName,
level: "",
money: undefined,
tf_sign: "", tf_opinion: ""
}
@@ -0,0 +1,17 @@
<el-descriptions-item label="慰问类型" :span="2">
<el-form-item label="慰问类型" prop="type">
<el-select v-model="formData.type" filterable @change="typeChange" style="width:100%" placeholder="请选择慰问类型">
<el-option v-for="t in typeOptions" :key="t.id" :value="t.id" :label="t.name"></el-option>
</el-select>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="慰问金额" :span="2">
<el-form-item label="慰问金额" prop="perMoney" :rules="recipientRules.money">
<el-input-number v-model="formData.perMoney" :precision="2" :min="0" :max="10000" :controls="false" style="width:100%" placeholder="每人慰问金额(元)"></el-input-number>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="慰问时间" :span="2">
<el-form-item label="慰问时间" prop="occurTime" :rules="recipientRules.occurTime">
<el-date-picker v-model="formData.occurTime" type="date" value-format="yyyy-MM-dd" style="width:100%" placeholder="请选择慰问时间"></el-date-picker>
</el-form-item>
</el-descriptions-item>
@@ -0,0 +1,313 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style nonce="${cspNonce!}">
<!--#include('recipients.css'){}#-->
</style>
<div id="app" class="special-condolence-apply" v-cloak>
<el-card shadow="never">
<snaker-start slot="header" label="特殊慰问" define_key="TSWW"></snaker-start>
<el-form size="small" :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":"
class="flow-task-form">
<el-tabs v-model="activeApplicationTab">
<el-tab-pane label="基础信息" name="form">
<el-descriptions :column="6" border size="small">
<el-descriptions-item label="事项" :span="2"><el-form-item prop="subject" label="事项"><el-input v-model="formData.subject" maxlength="200" show-word-limit placeholder="请输入特殊慰问事项"></el-input></el-form-item></el-descriptions-item>
<el-descriptions-item label="申请人姓名" :span="2">{{formData.applyUserName}}</el-descriptions-item>
<el-descriptions-item label="申请人工号" :span="2">{{formData.applyLoginName}}</el-descriptions-item>
<!--#include('config.html'){}#-->
<el-descriptions-item label="申请人单位" :span="2">{{formData.applyUnitName}}</el-descriptions-item>
<el-descriptions-item label="申请人工会" :span="2">{{formData.applyUnionName}}</el-descriptions-item>
<el-descriptions-item label="" :span="2"><span></span></el-descriptions-item>
<el-descriptions-item label="申请事由" :span="6">
<el-form-item prop="remark" label="申请事由">
<el-input maxlength="500" v-model="formData.remark" placeholder="请填写申请事由"
type="textarea" :rows="3" resize="none"></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item :span="6" v-if="chooseType.isUploadFile === true">
<template slot="label">
附件
<el-tooltip class="item" effect="dark" :content="'上传附件说明:' + chooseType.uploadFileDesc"
placement="top-start"
v-if="chooseType.isUploadFile && chooseType.uploadFileDesc">
<i class="el-icon-question"></i>
</el-tooltip>
</template>
<el-form-item prop="files" label="附件">
<file-upload
:value.sync="formData.files"
:upload_number="5"
upload_result_category="array"
complete_result
upload_mode="drag"
></file-upload>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="签字" :span="6">
<el-form-item prop="signature" label="签字">
<pc-signature v-model="formData.signature"></pc-signature>
</el-form-item>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="慰问对象" name="recipients" :lazy="false">
<!--#include('recipients.html'){}#-->
</el-tab-pane>
</el-tabs>
</el-form>
<el-row type="flex" justify="end" class="special-condolence-actions">
<el-button type="primary" plain @click="onSave">保存申请</el-button>
<el-button type="primary" @click="onSubmit" v-if="!taskId">提交申请</el-button>
<el-button type="primary" @click="onFinishTask" v-else>提交申请</el-button>
</el-row>
</el-card>
</div>
<script nonce="${cspNonce!}">
<!--#include('recipients.js'){}#-->
new Vue({
el: "#app",
store,
mixins: [specialCondolenceRecipients],
components: {
ElDatePicker: {
extends: ELEMENT.DatePicker,
mounted() {
// 手动输入触发 change 前初始化日期面板,避免 picker 尚未创建。
if (!this.picker) this.mountPicker()
}
}
},
data() {
return {
bizId: GetQueryString("bizId"),
taskId: GetQueryString("taskId"),
formData: { recipients: [] },
formRules: {
subject: [{ required: true, whitespace: true, message: "请填写事项", trigger: ["blur", "change"] }, { max: 200, message: "事项最多200字", trigger: "blur" }],
recipients: [{ validator: (rule, value, callback) => callback(Array.isArray(value) && value.length > 0 ? undefined : new Error("请至少添加一位慰问对象")), trigger: "change" }],
helpUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
payUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
type: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
way: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
money: [
{ required: true, message: "请填写慰问金额", trigger: ["change", "blur"] },
{ validator: (rule, value, callback) => callback(this.validateMoney(value) ? undefined : new Error("慰问金额须大于0且不超过10000元,最多两位小数")), trigger: ["change", "blur"] }
],
occurTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
child: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
hospital: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
hospitalHouseNum: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
hospitalHouseBedNum: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
hospitalBy: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
deadImmediateFamily: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
remark: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
files: [{ required: true, type: "array", min: 1, max: 5, message: "请上传1至5个附件", trigger: ["change", "blur"] }],
signature: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
handlerUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
// certifierUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
bankCardNumber: [
{ required: true, message: "请填写银行卡号或对公账户号", trigger: ["change", "blur"] },
{ pattern: /^[0-9]{6,30}$/, message: "收款账户须为6至30位数字(银行卡号或对公账户号)", trigger: ["change", "blur"] }
],
bankUserName: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
bankOfDeposit: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
thisYearHospitalizationNum: [
{ required: true, message: "必填", trigger: ["change", "blur"] },
{ type: "integer", min: 0, max: 10000, message: "住院次数须为0至10000的整数", trigger: ["change", "blur"] }
],
leaveHospitalTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
hospitalizationTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
},
chooseType: {},
payUserOptions: [],
helpUserOptions: [],
typeOptions: [],
handlerUserOptions: [],
certifierUserOptions: []
}
},
methods: {
validateMoney(value) {
return /^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000
},
createRemoteMethod(options) {
return (keyword) => {
this.selectQueryUser(keyword, options)
}
},
selectQueryUser(keyword, options) {
options.length = 0
this.$axios.post("/platform/special_condolence/apply/listUser", { keyword: keyword }).then((res) => {
if (res.code === 0) {
options.push(...res.data)
}
})
},
helpUserChange(val) {
const user = this.helpUserOptions.find(o => o.id === val)
if (user) {
const { userName, loginName, unitId, unitName, unionId, unionName } = user
this.$set(this.formData, "helpUserName", userName)
this.$set(this.formData, "helpLoginName", loginName)
this.$set(this.formData, "helpUnitId", unitId)
this.$set(this.formData, "helpUnitName", unitName)
this.$set(this.formData, "helpUnionId", unionId)
this.$set(this.formData, "helpUnionName", unionName)
}
},
handlerUserChange(val) {
const user = this.handlerUserOptions.find(o => o.id === val)
if (user) {
const { userName, loginName, unitId, unitName, unionId, unionName } = user
this.$set(this.formData, "handlerUserName", userName)
this.$set(this.formData, "handlerLoginName", loginName)
this.$set(this.formData, "handlerUnitId", unitId)
this.$set(this.formData, "handlerUnitName", unitName)
this.$set(this.formData, "handlerUnionId", unionId)
this.$set(this.formData, "handlerUnionName", unionName)
}
},
certifierUserChange(val) {
const user = this.certifierUserOptions.find(o => o.id === val)
if (user) {
const { userName, loginName, unitId, unitName, unionId, unionName } = user
this.$set(this.formData, "certifierUserName", userName)
this.$set(this.formData, "certifierLoginName", loginName)
this.$set(this.formData, "certifierUnitId", unitId)
this.$set(this.formData, "certifierUnitName", unitName)
this.$set(this.formData, "certifierUnionId", unionId)
this.$set(this.formData, "certifierUnionName", unionName)
}
},
payUserChange(val) {
const user = this.payUserOptions.find(o => o.id === val)
if (user) {
const { userName, loginName } = user
this.$set(this.formData, "payUserName", userName)
this.$set(this.formData, "payLoginName", loginName)
}
},
typeChange(id) {
this.chooseType = this.typeOptions.find(o => o.id === id)
if (this.chooseType) {
this.$set(this.formData, "perMoney", this.chooseType.money)
this.$set(this.formData, "way", this.chooseType.way)
this.$set(this.formData, "typeCode", this.chooseType.code)
}
},
onSave() {
this.$refs.formRef.validate((valid, invalidFields) => {
if (!valid) { this.showInvalidApplicationTab(invalidFields); return }
this.$confirm("您确定保存吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/special_condolence/apply/save", { data: JSON.stringify(this.formData) }).then(res => {
if (res.code === 0) {
this.$message.success(res.msg)
window.location.replace("/platform/special_condolence/mine")
}
})
})
})
},
onSubmit() {
if (!this.ensureApplicationRecipients()) return
this.$refs.formRef.validate((valid, invalidFields) => {
if (!valid) { this.showInvalidApplicationTab(invalidFields); return }
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/special_condolence/apply/submit", {
data: JSON.stringify(this.formData)
}).then(res => {
if (res.code === 0) {
this.$message.success(res.msg)
window.location.replace("/platform/special_condolence/mine")
}
})
})
}
})
},
onFinishTask() {
if (!this.ensureApplicationRecipients()) return
this.$refs.formRef.validate((valid, invalidFields) => {
if (!valid) { this.showInvalidApplicationTab(invalidFields); return }
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/special_condolence/apply/submitAgain", {
data: JSON.stringify(this.formData),
taskId: GetQueryString("taskId")
}).then(res => {
if (res.code === 0) {
this.$message.success(res.msg)
window.location.replace("/platform/special_condolence/mine")
}
})
})
}
})
},
init() {
if (this.bizId) {
this.$axios.post("/platform/special_condolence/mine/info", { id: this.bizId }).then((res) => {
if (res.code === 0) {
this.formData = res.data
this.initRecipients()
this.selectQueryUser(this.formData.helpLoginName, this.helpUserOptions)
this.selectQueryUser(this.formData.certifierLoginName, this.certifierUserOptions)
this.selectQueryUser(this.formData.handlerLoginName, this.handlerUserOptions)
this.chooseType = this.typeOptions.find(o => o.id === this.formData.type)
}
})
} else {
const { username, loginname, id, unit, union, mobile } = this.$store.state.user
this.formData = {
recipients: [this.newRecipient()],
perMoney: undefined,
occurTime: this.$moment().format('YYYY-MM-DD'),
applyUserName: username,
applyLoginName: loginname,
applyUserId: id,
applyUnitId: unit?.id,
applyUnitName: unit?.name,
applyUnionId: union?.id,
applyUnionName: union?.name
}
}
},
querySpecialCondolenceType() {
this.$axios.post("/platform/special_condolence/type/querySpecialCondolenceType")
.then((resp) => {
this.typeOptions = resp.data
})
}
},
created() {
this.querySpecialCondolenceType()
this.init()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,67 @@
.special-condolence-apply .el-card__body { padding: 24px; }
.special-condolence-apply .special-condolence-section-title { display: flex; align-items: center; gap: 10px; margin: 24px 0 14px; color: #303133; font-size: 15px; font-weight: 600; line-height: 24px; }
.special-condolence-apply .el-descriptions__table { table-layout: fixed; }
.special-condolence-apply .flow-task-form > .special-condolence-section-title:first-child { margin-top: 0; }
.special-condolence-apply .el-descriptions { font-size: 14px; }
.special-condolence-apply .el-descriptions .el-descriptions-item__cell { height: 50px; padding: 8px 12px; box-sizing: border-box; font-size: 14px; line-height: 32px; vertical-align: middle; }
.special-condolence-apply .el-descriptions-item__label.is-bordered-label { width: 130px; min-width: 130px; max-width: 130px; font-weight: 400; }
.special-condolence-apply .el-descriptions-item__content { font-weight: 400; }
.special-condolence-apply .el-descriptions .el-form-item__content { line-height: 32px; }
.special-condolence-apply .el-descriptions .el-input__inner { height: 32px; line-height: 32px; font-size: 14px; }
.special-condolence-apply .el-descriptions .el-input__icon { line-height: 32px; }
.special-condolence-apply .el-descriptions .el-textarea__inner { font-size: 14px; line-height: 22px; }
.special-condolence-apply .el-descriptions-item__label { width: 130px; color: #606266; background: #f7f9fc; }
.special-condolence-apply .special-condolence-recipients { margin: 0; }
.special-condolence-apply .special-condolence-toolbar { display: flex; justify-content: flex-end; align-items: center; flex-wrap: wrap; gap: 14px; padding: 0 0 12px; }
.special-condolence-apply .special-condolence-toolbar .special-condolence-section-title { margin: 0 0 4px; }
.special-condolence-hint { color: #606266; font-size: 12px; line-height: 20px; }
.special-condolence-apply .special-condolence-toolbar-buttons { display: flex; flex-wrap: wrap; gap: 8px; }
.special-condolence-apply .special-condolence-toolbar-buttons .el-button { margin: 0; }
.special-condolence-apply .special-condolence-table-item { margin: 0; }
.special-condolence-apply .special-condolence-table-item > .el-form-item__content { line-height: normal; }
.special-condolence-apply .special-condolence-table-item .el-form-item__error { position: static; padding: 8px 20px; }
.special-condolence-apply .special-condolence-table-item .el-table th { background: #f5f7fa; color: #606266; font-weight: 600; }
.special-condolence-apply .special-condolence-table-item .el-table .cell { padding: 0 8px; }
.special-condolence-apply .special-condolence-table-item .el-select,
.special-condolence-apply .special-condolence-table-item .el-input-number,
.special-condolence-apply .special-condolence-table-item .el-date-editor { width: 100%; }
.special-condolence-apply .special-condolence-table-item .el-input-number .el-input__inner { text-align: right; }
.special-condolence-apply .special-condolence-delete { color: #d64040; }
.special-condolence-apply .special-condolence-table-footer { display: flex; justify-content: flex-end; align-items: center; flex-wrap: wrap; gap: 10px; padding: 10px 20px; background: #fafbfd; }
.special-condolence-apply .special-condolence-total { display: flex; align-items: baseline; gap: 6px; color: #606266; font-size: 13px; font-variant-numeric: tabular-nums; }
.special-condolence-apply .special-condolence-total > span { margin-left: 18px; }
.special-condolence-apply .special-condolence-total-amount { color: #303133; font-size: 20px; }
.special-condolence-apply .special-condolence-actions { gap: 12px; margin-top: 24px; padding-top: 20px; border-top: 1px solid #ebeef5; }
.special-condolence-apply .special-condolence-actions .el-button { margin: 0; min-width: 108px; }
.special-condolence-dialog { max-width: calc(100vw - 32px); border-radius: 8px; }
.special-condolence-dialog .el-dialog__header { padding: 22px 24px 16px; border-bottom: 1px solid #ebeef5; }
.special-condolence-dialog .el-dialog__body { padding: 24px; }
.special-condolence-dialog .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #ebeef5; }
.special-condolence-dialog .special-condolence-template { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 16px; border: 1px solid #dce6f3; border-radius: 6px; background: #f6f9fe; }
.special-condolence-dialog .special-condolence-template > i { color: #409eff; font-size: 28px; }
.special-condolence-dialog .special-condolence-template > div { flex: 1; min-width: 180px; }
.special-condolence-dialog .special-condolence-template p { margin: 5px 0 0; color: #606266; font-size: 12px; line-height: 18px; }
.special-condolence-dialog .special-condolence-upload-title { margin: 22px 0 12px; font-weight: 600; }
.special-condolence-dialog .special-condolence-upload .el-upload,
.special-condolence-dialog .special-condolence-upload .el-upload-dragger { width: 100%; }
.special-condolence-dialog .special-condolence-upload .el-upload-dragger { height: auto; min-height: 150px; padding: 22px 12px; }
.special-condolence-dialog .special-condolence-upload .el-icon-upload,
.special-condolence-dialog .special-condolence-upload .el-icon-loading { display: block; margin: 0 0 16px; color: #909399; font-size: 44px; line-height: 48px; }
.special-condolence-dialog .el-upload__tip { margin-bottom: 16px; line-height: 20px; }
.special-condolence-dialog .special-condolence-batch-note { margin-bottom: 24px; padding: 12px 16px; background: #f5f7fa; border-radius: 4px; line-height: 22px; }
.special-condolence-dialog .special-condolence-batch-form .el-select,
.special-condolence-dialog .special-condolence-batch-form .el-input-number,
.special-condolence-dialog .special-condolence-batch-form .el-date-editor { width: 100%; }
@media (max-width: 768px) {
.special-condolence-apply .el-card__body { padding: 16px; }
.special-condolence-apply .special-condolence-toolbar { padding: 14px; }
.special-condolence-apply .special-condolence-total > span { margin-left: 8px; }
}
.special-condolence-apply .special-condolence-table-item .el-table .el-form-item { margin: 8px 0 20px; }
.special-condolence-apply .special-condolence-table-item .el-table .el-form-item__error { position: absolute; padding: 4px 0 0; font-size: 11px; line-height: 14px; white-space: normal; }
.special-condolence-apply .flow-task-form .el-form-item__error { color: #f56c6c !important; }
.special-condolence-apply .el-form-item.is-error .el-input__inner,
.special-condolence-apply .el-form-item.is-error .el-textarea__inner { border-color: #f56c6c; }
@@ -0,0 +1,38 @@
<section class="special-condolence-recipients">
<div class="special-condolence-toolbar">
<div class="special-condolence-toolbar-buttons">
<el-button size="small" icon="el-icon-upload2" @click="importVisible = true">Excel 导入</el-button>
<el-button size="small" type="primary" icon="el-icon-plus" @click="formData.recipients.push(newRecipient())">新增慰问对象</el-button>
</div>
</div>
<el-form-item prop="recipients" label-width="0" class="special-condolence-table-item">
<el-table ref="recipientTableRef" :data="formData.recipients" border stripe size="small" max-height="520" empty-text="暂无慰问对象,点击新增或从 Excel 导入">
<el-table-column type="index" label="序号" width="55" align="center" fixed="left"></el-table-column>
<el-table-column label="慰问对象" width="200" fixed="left"><template slot-scope="s"><el-form-item :prop="'recipients.' + s.$index + '.helpUserId'" :rules="recipientRules.helpUserId"><user-select size="small" v-model="s.row.helpUserId" api="/platform/special_condolence/apply/listUser" api_input_key_name="keyword" :option_list="recipientOptions(s.row)" :option_label_func="item => item.userName + '(' + (item.loginName || '') + ')'" @selected="(user, id) => recipientUserChange(s.row, s.$index, id, user)"></user-select></el-form-item></template></el-table-column>
<el-table-column label="工号" min-width="150"><template slot-scope="s"><el-form-item :prop="'recipients.' + s.$index + '.helpLoginName'" :rules="[{ validator: (rule, value, cb) => cb(s.row.personType === '劳务派遣人员' || value ? undefined : new Error('普通人员工号不能为空')), trigger: 'change' }]"><el-input size="small" v-model="s.row.helpLoginName" readonly placeholder="选人后带出,劳务派遣可空"></el-input></el-form-item></template></el-table-column>
<el-table-column label="身份证号码" min-width="205"><template slot-scope="s"><el-form-item :prop="'recipients.' + s.$index + '.idCard'" :rules="recipientRules.idCard"><el-input size="small" v-model="s.row.idCard" maxlength="18" placeholder="请输入18位身份证号"></el-input></el-form-item></template></el-table-column>
<el-table-column label="开户行" min-width="190"><template slot-scope="s"><el-form-item :prop="'recipients.' + s.$index + '.bankOfDeposit'" :rules="recipientRules.bankOfDeposit"><el-input size="small" v-model="s.row.bankOfDeposit" maxlength="100" placeholder="请输入开户银行及支行"></el-input></el-form-item></template></el-table-column>
<el-table-column label="户名" min-width="140"><template slot-scope="s"><el-form-item :prop="'recipients.' + s.$index + '.bankUserName'" :rules="recipientRules.bankUserName"><el-input size="small" v-model="s.row.bankUserName" maxlength="100" placeholder="请输入账户户名"></el-input></el-form-item></template></el-table-column>
<el-table-column label="收款账户" min-width="215"><template slot-scope="s"><el-form-item :prop="'recipients.' + s.$index + '.bankCardNumber'" :rules="recipientRules.bankCardNumber"><el-input size="small" v-model="s.row.bankCardNumber" maxlength="30" placeholder="银行卡号或对公账户号"></el-input></el-form-item></template></el-table-column>
<el-table-column label="操作" fixed="right" width="75" align="center"><template slot-scope="s"><el-button size="small" type="text" class="special-condolence-delete" @click="formData.recipients.splice(s.$index, 1)">删除</el-button></template></el-table-column>
</el-table>
</el-form-item>
<div class="special-condolence-table-footer">
<div class="special-condolence-total">共 <strong>{{ recipientCount }}</strong> 人<span>慰问金额合计</span><strong class="special-condolence-total-amount">¥ {{ recipientTotal }}</strong></div>
</div>
</section>
<el-dialog title="Excel 导入慰问对象" :visible.sync="importVisible" width="560px" custom-class="special-condolence-dialog" :close-on-click-modal="!importLoading" :close-on-press-escape="!importLoading" :show-close="!importLoading" append-to-body>
<div class="special-condolence-template">
<i class="el-icon-document"></i>
<div><strong>第一步:下载并填写模板</strong><p>按模板表头填写,一行对应一位慰问对象。</p></div>
<el-button size="small" type="primary" plain icon="el-icon-download" @click="$downLoad('/platform/special_condolence/apply/importTemplate')">下载模板</el-button>
</div>
<div class="special-condolence-upload-title">第二步:上传填写好的文件</div>
<el-upload class="special-condolence-upload" drag action="" accept=".xlsx,.xls" :http-request="importRecipients" :show-file-list="false" :disabled="importLoading">
<i :class="importLoading ? 'el-icon-loading' : 'el-icon-upload'"></i>
<div class="el-upload__text">{{ importLoading ? '正在导入,请稍候…' : '将 Excel 文件拖到此处,或点击选择文件' }}</div>
<div slot="tip" class="el-upload__tip">支持 .xlsx、.xls 格式,单次最多 1000 行。导入的数据将追加到当前表格。</div>
</el-upload>
<el-alert title="工号、身份证号码和收款账户请保持文本格式。普通人员须填写正确工号;劳务派遣人员免校验工号,均须属于申请人本工会。" type="info" :closable="false" show-icon></el-alert>
<span slot="footer"><el-button :disabled="importLoading" @click="importVisible = false">关闭</el-button></span>
</el-dialog>
@@ -0,0 +1,112 @@
const specialCondolenceRecipients = {
data() {
return { activeApplicationTab: 'form', recipientRules: this.createRecipientRules(), importVisible: false, importLoading: false }
},
computed: {
recipientCount() {
return (this.formData.recipients || []).length
},
recipientTotal() {
const money = Number(this.formData.perMoney)
const cents = (Number.isFinite(money) && money > 0 ? Math.round(money * 100) : 0) * this.recipientCount
return (cents / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
},
watch: {
activeApplicationTab() {
this.$nextTick(() => {
if (this.$refs.recipientTableRef) this.$refs.recipientTableRef.doLayout()
})
},
typeOptions() { this.chooseType = this.typeOptions.find(t => t.id === this.formData.type) || {} },
'formData.type'(value) { this.chooseType = this.typeOptions.find(t => t.id === value) || {} }
},
methods: {
showInvalidApplicationTab(errors) {
const fields = Array.isArray(errors) ? errors.map(error => error.name || '') : Object.keys(errors || {})
if (!fields.length) return
this.activeApplicationTab = fields.some(field => !field.startsWith('recipients')) ? 'form' : 'recipients'
if (this.activeApplicationTab === 'recipients') this.notifyRecipientError('请完善慰问对象的必填信息')
},
notifyRecipientError(message) {
if (this.$toast && this.$toast.fail) this.$toast.fail(message)
else this.$message.error(message)
},
ensureApplicationRecipients() {
const rows = this.formData.recipients
if (!Array.isArray(rows) || rows.length === 0 || rows.some(row => !row || !String(row.helpUserId || '').trim())) {
this.activeApplicationTab = 'recipients'
this.notifyRecipientError('请先选择慰问对象,并填写完整信息')
return false
}
return true
},
createRecipientRules() {
const required = label => ({ required: true, whitespace: true, message: '请填写' + label, trigger: ['blur', 'change'] })
return {
helpUserId: [{ required: true, message: '请选择慰问对象', trigger: 'change' }],
type: [{ required: true, message: '请选择慰问类型', trigger: 'change' }],
idCard: [required('身份证号码'), { validator: (rule, value, cb) => cb(this.validRecipientIdCard(value) ? undefined : new Error('身份证号码不正确')), trigger: ['blur', 'change'] }],
bankOfDeposit: [required('开户行'), { max: 100, message: '开户行最多100字', trigger: 'blur' }],
bankUserName: [required('户名'), { max: 100, message: '户名最多100字', trigger: 'blur' }],
bankCardNumber: [required('收款账户'), { pattern: /^[0-9]{6,30}$/, message: '请输入6至30位数字账户', trigger: ['blur', 'change'] }],
money: [{ validator: (rule, value, cb) => cb(/^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000 ? undefined : new Error('金额须大于0且不超过10000元,最多两位小数')), trigger: ['blur', 'change'] }],
occurTime: [{ validator: (rule, value, cb) => cb(this.$moment(value, 'YYYY-MM-DD', true).isValid() ? undefined : new Error('请选择有效慰问时间')), trigger: ['blur', 'change'] }]
}
},
validRecipientIdCard(value) {
const card = String(value || '').toUpperCase()
if (!/^[1-9][0-9]{16}[0-9X]$/.test(card) || !this.$moment(card.substring(6, 14), 'YYYYMMDD', true).isValid()) return false
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
return '10X98765432'[weights.reduce((sum, weight, index) => sum + weight * Number(card[index]), 0) % 11] === card[17]
},
recipientOptions(row) {
return row.helpUserId ? [{ id: row.helpUserId, userName: row.helpUserName, loginName: row.helpLoginName, personType: row.personType, idCard: row.idCard, bankOfDeposit: row.bankOfDeposit, bankUserName: row.bankUserName, bankCardNumber: row.bankCardNumber }] : []
},
recipientUserChange(row, index, id, user) {
this.$set(row, 'helpUserId', user ? user.id : '')
this.$set(row, 'helpUserName', user ? user.userName : '')
this.$set(row, 'helpLoginName', user ? user.loginName : '')
this.$set(row, 'personType', user ? user.personType : '')
this.$set(row, 'idCard', user ? user.idCard || '' : '')
this.$set(row, 'bankOfDeposit', user ? user.bankOfDeposit || '' : '')
this.$set(row, 'bankUserName', user ? user.bankUserName || user.userName : '')
this.$set(row, 'bankCardNumber', user ? user.bankCardNumber || '' : '')
this.$nextTick(() => {
const fields = ['helpUserId', 'helpLoginName', 'idCard', 'bankOfDeposit', 'bankUserName', 'bankCardNumber']
const names = fields.map(field => 'recipients.' + index + '.' + field)
const form = this.$refs.formRef
if (typeof form.validateField === 'function') form.validateField(names)
else if (typeof form.validate === 'function') form.validate(names).catch(() => {})
})
},
newRecipient() {
return { helpUserId: '', helpLoginName: '', personType: '', helpUserName: '', idCard: '', bankOfDeposit: '', bankUserName: '', bankCardNumber: '' }
},
initRecipients() {
if (this.formData.perMoney == null && this.formData.recipients && this.formData.recipients.length) this.$set(this.formData, 'perMoney', Number(this.formData.recipients[0].money))
if (!this.formData.recipients || !this.formData.recipients.length) {
this.$set(this.formData, 'recipients', this.formData.helpUserName ? [Object.assign(this.newRecipient(), this.formData, { recipients: undefined })] : [this.newRecipient()])
}
},
async importRecipients(request) {
this.importLoading = true
try {
const data = new FormData()
data.append('file', request.file)
const result = await this.$axios.post('/platform/special_condolence/apply/importRecipients', data)
if (result.code === 0) {
const rows = this.formData.recipients.filter(r => r.helpUserName || r.type || r.idCard || r.bankCardNumber || r.bankUserName || r.bankOfDeposit || r.money)
this.$set(this.formData, 'recipients', rows.concat(result.data))
this.importVisible = false
this.$message.success('导入成功,共' + result.data.length + '人')
} else this.$message.error(result.msg || '导入失败')
} catch (error) {
this.$message.error('导入失败,请检查文件内容后重试')
} finally {
this.importLoading = false
}
}
}
}
@@ -0,0 +1,181 @@
const specialcondolenceInfo = {
template: /*language=HTML*/ `
<div>
<el-tabs v-model="activeInfoTab">
<el-tab-pane label="基础信息" name="form">
<div class="process-title">
申请信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="6" border size="small" :label-style="{ width: '130px', minWidth: '130px', maxWidth: '130px', fontSize: '14px', fontWeight: 400, lineHeight: '32px', padding: '8px 12px' }" :content-style="{ fontSize: '14px', fontWeight: 400, lineHeight: '32px', padding: '8px 12px' }">
<el-descriptions-item label="事项" :span="2">{{ viewData.subject || '--' }}</el-descriptions-item>
<el-descriptions-item label="申请人姓名" :span="2">{{ viewData.applyUserName }}</el-descriptions-item>
<el-descriptions-item label="申请人工号" :span="2">{{ viewData.applyLoginName }}</el-descriptions-item>
<template v-if="viewData.recipients && viewData.recipients.length">
<el-descriptions-item label="慰问类型" :span="2">{{ viewData.typeName }}</el-descriptions-item>
<el-descriptions-item label="每人慰问金额" :span="2">{{ viewData.perMoney }} 元</el-descriptions-item>
<el-descriptions-item label="慰问时间" :span="2">{{ viewData.occurTime }}</el-descriptions-item>
</template>
<el-descriptions-item label="申请人单位" :span="2">{{ viewData.applyUnitName }}</el-descriptions-item>
<el-descriptions-item label="申请人工会" :span="2">{{ viewData.applyUnionName }}</el-descriptions-item>
<el-descriptions-item label="" :span="2"><span></span></el-descriptions-item>
<el-descriptions-item label="申请事由" :span="6">
<div style="white-space: pre-line">{{ viewData.remark }}</div>
</el-descriptions-item>
<el-descriptions-item label="附件" :span="6">
<file-preview v-if="viewData.files && viewData.files.length > 0" :files="viewData.files"
complete_result></file-preview>
<span v-else>暂无附件</span>
</el-descriptions-item>
<el-descriptions-item label="签字" :span="6">
<el-image :src="viewData.signature"
v-if="viewData.signature"
class="signature-image"></el-image>
<span v-else>暂无签字</span>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="慰问对象" name="recipients">
<el-descriptions v-if="!viewData.recipients || !viewData.recipients.length" :column="3" border>
<template v-if="!viewData.recipients || !viewData.recipients.length">
<el-descriptions-item label="慰问对象">{{ viewData.helpUserName }}</el-descriptions-item>
<!-- <el-descriptions-item label="经办人">{{ viewData.handlerUserName }}</el-descriptions-item>-->
<el-descriptions-item label="证明人">{{ viewData.certifierUserName }}</el-descriptions-item>
<el-descriptions-item label="慰问类型">{{ viewData.typeName }}</el-descriptions-item>
<el-descriptions-item label="慰问金额">{{ viewData.money }}</el-descriptions-item>
<el-descriptions-item label="慰问时间">{{ viewData.occurTime }}</el-descriptions-item>
<el-descriptions-item label="收款账户">{{ viewData.bankCardNumber }}</el-descriptions-item>
<el-descriptions-item label="户名">{{ viewData.bankUserName }}</el-descriptions-item>
<el-descriptions-item label="开户行">{{ viewData.bankOfDeposit }}</el-descriptions-item>
<template v-if="['2'].includes(viewData.typeCode)">
<el-descriptions-item label="入院时间">{{ viewData.hospitalizationTime }}</el-descriptions-item>
<el-descriptions-item label="出院时间">{{ viewData.leaveHospitalTime }}</el-descriptions-item>
<el-descriptions-item label="当年第几次住院">{{ viewData.thisYearHospitalizationNum }}
</el-descriptions-item>
</template>
</template>
</el-descriptions>
<div v-if="viewData.recipients && viewData.recipients.length" class="process-title" style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-top:20px">
<span>慰问对象明细</span>
<span style="font-size:14px;font-weight:normal">慰问金额合计:<strong>¥ {{ detailTotal }}</strong></span>
</div>
<el-table ref="recipientInfoTableRef" v-if="viewData.recipients && viewData.recipients.length" :data="viewData.recipients" border stripe size="small" max-height="480" class="mt10" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
<el-table-column type="index" label="序号" width="55" align="center" fixed="left"></el-table-column>
<el-table-column prop="helpUserName" label="慰问对象" width="130" fixed="left"></el-table-column>
<el-table-column prop="helpLoginName" label="工号" min-width="130"></el-table-column>
<el-table-column prop="idCard" label="身份证号码" min-width="190"></el-table-column>
<el-table-column prop="bankOfDeposit" label="开户行" min-width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="bankUserName" label="户名" min-width="150"></el-table-column>
<el-table-column prop="bankCardNumber" label="收款账户" min-width="210"></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="审核记录" name="records">
<div v-if="!doneTasks.length" style="padding:32px;text-align:center;color:#909399">暂无审核记录</div>
<template v-for="task in doneTasks">
<div class="mt10">
<div class="process-title">{{ task.displayName }}</div>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id"
v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName
}}({{task.ext.initiatorAccount}})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName
}}({{task.taskFormData.loginName}})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="审批签字" :span="3" v-if="task.taskFormData && task.taskFormData.sign">
<el-image :src="task.taskFormData.sign" style="height: 80px" fit="contain"></el-image>
</el-descriptions-item>
<el-descriptions-item label="办理意见" v-if="!task.ext.isFirstTaskNode">{{
task.taskFormData.opinion }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
</el-tab-pane>
</el-tabs>
<slot></slot>
<snaker-chart ref="snakerChartRef"></snaker-chart>
</div>
`,
store,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
activeInfoTab: 'form',
visible: false,
viewData: {},
doneTasks: [],
row: null
}
},
watch: {
activeInfoTab() {
this.$nextTick(() => {
if (this.$refs.recipientInfoTableRef) this.$refs.recipientInfoTableRef.doLayout()
})
}
},
computed: {
detailTotal() {
const cents = (this.viewData.recipients || []).reduce((sum, row) => {
const money = Number(row.money)
return sum + (Number.isFinite(money) ? Math.round(money * 100) : 0)
}, 0)
return (cents / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
},
methods: {
onOpen(row) {
this.activeInfoTab = 'form'
this.row = row
this.visible = true
this.getInfo()
this.getDoneTasks()
},
getInfo() {
this.$axios.post('/platform/special_condolence/mine/info', {id: this.row.id}).then((res) => {
if (res.code === 0) {
this.viewData = res.data
}
})
},
// 获取已办任务审批记录
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
// 查看流程图
openChart(){
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
}
},
style: /*language=CSS*/ `
.el-descriptions-item__label {
width: 200px;
min-width: 200px;
max-width: 200px;
}
.el-tabs__header {
margin: 0;
}
`
}
@@ -0,0 +1,229 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
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">
</el-input>
</search-item>
<search-item label="慰问类型">
<el-select v-model="pageForm.type" @change="doSearch" style="width: 100%"
placeholder="请选择慰问类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" placeholder="请选择所属工会" filterable clearable
@change="doSearch" style="width: 100%">
<el-option
v-for="item in unionList"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
<search-item label="所属单位">
<el-select @change="doSearch"
clearable
filterable
placeholder="请选择单位" style="width: 100%" v-model="pageForm.unitId">
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in unitList"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-radio-group v-model="pageForm.approval" size="small" @change="doSearch">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300px">
<template slot-scope="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.taskState === 10" @click="onAudit(row)" size="mini" type="primary">审核</el-button>
<el-button v-if="row.canRevoke" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<specialcondolence-info ref="specialcondolenceInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{formData.taskName}}
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":"
class="flow-task-form">
<el-form-item label="审批意见" prop="tf_opinion"
: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_sign" :rules="[{required:true,message:'请完成审批签字',trigger:['change','blur']}]">
<pc-signature v-model="formData.tf_sign" :key="formData.processTaskId"></pc-signature>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意申请</el-button>
<el-button @click="handleTaskAction(2)" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到发起人</el-button>
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
</el-row>
</div>
</specialcondolence-info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
components: {
"specialcondolence-info": specialcondolenceInfo
},
data() {
return {
pageForm: {
approval: false
},
typeOptions: [],
tableColumns: [
{prop: 'subject', label: '事项'},
{prop: 'applyUserName', label: '申请人'},
{prop: 'applyUnionName', label: '所属工会'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'typeName', label: '慰问类型'},
{prop: 'recipientCount', label: '慰问人数', width: 100},
{prop: 'curTaskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
unionList: [],
unitList: [],
formData: {},
showApprovalForm: false
}
},
methods: {
onView(row) {
this.$refs.guava.edit(()=>{
this.showApprovalForm = false
this.$refs.specialcondolenceInfoRef.onOpen(row)
})
},
onAudit(row) {
this.$refs.guava.edit(()=>{
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName,
tf_sign: "",
tf_opinion: ""
}
this.$refs.specialcondolenceInfoRef.onOpen(row)
})
},
async handleTaskAction(val) {
try {
const valid = await this.$refs.formRef.validate()
if (!valid) return
} catch (error) {
return
}
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/special_condolence/approval/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
onRevoke(row) {
this.$confirm("您确定要撤回吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
querySpecialCondolenceType() {
this.$axios.post('/platform/special_condolence/type/querySpecialCondolenceType')
.then((resp) => {
this.typeOptions = resp.data
})
},
},
async created() {
this.unionList = await this.$businessTool.listUnion()
this.unitList = await this.$businessTool.listUnit()
this.querySpecialCondolenceType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,165 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
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">
</el-input>
</search-item>
<search-item label="慰问类型">
<el-select v-model="pageForm.type" @change="doSearch" style="width: 100%"
placeholder="请选择慰问类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增申请
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<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.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.createdBy === $store.state.user.id && row.instanceState == 20" @click="doExportApply(row)" size="mini" type="primary">导出</el-button>
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" @click="onDelete(row.id)" size="mini" type="danger">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #view>
<specialcondolence-info ref="specialcondolenceInfoRef"></specialcondolence-info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
components: {
"specialcondolence-info": specialcondolenceInfo
},
data() {
return {
typeOptions: [],
pageDataUrl: "/platform/special_condolence/mine/pageData",
tableColumns: [
{prop: 'subject', label: '事项'},
{prop: 'applyUserName', label: '申请人'},
{prop: 'applyUnionName', label: '所属工会'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'typeName', label: '慰问类型'},
{prop: 'recipientCount', label: '慰问人数', width: 100},
{prop: 'taskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
}
},
methods: {
doExportApply(row) {
this.$downLoad('/platform/special_condolence/mine/doExportApply?id=' + row.id + '&print=false')
},
onAdd() {
commonUtil.pjaxPush('/platform/special_condolence/apply')
},
onView(row) {
this.$refs.guava.view(()=>{
this.$refs.specialcondolenceInfoRef.onOpen(row)
})
},
onEdit(row) {
commonUtil.pjaxPush('/platform/special_condolence/apply?taskId=' + (row.startTaskId || '') + "&bizId=" + row.id)
},
onRevoke(row) {
this.$confirm("您确定要撤回吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", { taskId: row.startTaskId }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
onDelete(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/special_condolence/mine/delete", { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
querySpecialCondolenceType() {
this.$axios.post('/platform/special_condolence/type/querySpecialCondolenceType')
.then((resp) => {
this.typeOptions = resp.data
})
},
},
created() {
this.querySpecialCondolenceType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,161 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
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">
</el-input>
</search-item>
<search-item label="慰问类型">
<el-select v-model="pageForm.type" @change="doSearch" style="width: 100%"
placeholder="请选择慰问类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" placeholder="请选择所属工会" filterable clearable
@change="doSearch" style="width: 100%">
<el-option
v-for="item in unionList"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
<search-item label="所属单位">
<el-select @change="doSearch"
clearable
filterable
placeholder="请选择单位" style="width: 100%" v-model="pageForm.unitId">
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in unitList"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-button @click="onExport" icon="el-icon-s-promotion" type="primary" size="small">导出</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template slot-scope="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="$auth.hasRoleOr('SYSADMIN, SCHOOL_UNION_ADMIN')" @click="onDelete(row.id)" size="mini" type="danger">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<specialcondolence-info ref="specialcondolenceInfoRef"></specialcondolence-info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
components: {
"specialcondolence-info": specialcondolenceInfo
},
data() {
return {
typeOptions: [],
tableColumns: [
{prop: 'subject', label: '事项'},
{prop: 'applyUserName', label: '申请人'},
{prop: 'applyUnionName', label: '所属工会'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'typeName', label: '慰问类型'},
{prop: 'taskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
unionList: [],
unitList: [],
}
},
methods: {
onDelete(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/special_condolence/mine/delete", { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
onExport() {
this.$downLoad(loc() + '/onExport', this.pageForm)
},
onView(row) {
this.$refs.guava.edit(()=>{
this.$refs.specialcondolenceInfoRef.onOpen(row)
})
},
querySpecialCondolenceType() {
this.$axios.post('/platform/special_condolence/type/querySpecialCondolenceType')
.then((resp) => {
this.typeOptions = resp.data
})
},
},
async created() {
this.unionList = await this.$businessTool.listUnion()
this.unitList = await this.$businessTool.listUnit()
this.querySpecialCondolenceType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,229 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
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">
</el-input>
</search-item>
<search-item label="慰问类型">
<el-select v-model="pageForm.type" @change="doSearch" style="width: 100%"
placeholder="请选择慰问类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" placeholder="请选择所属工会" filterable clearable
@change="doSearch" style="width: 100%">
<el-option
v-for="item in unionList"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
<search-item label="所属单位">
<el-select @change="doSearch"
clearable
filterable
placeholder="请选择单位" style="width: 100%" v-model="pageForm.unitId">
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in unitList"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-radio-group v-model="pageForm.approval" size="small" @change="doSearch">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300px">
<template slot-scope="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.taskState === 10" @click="onAudit(row)" size="mini" type="primary">审核</el-button>
<el-button v-if="row.canRevoke" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<specialcondolence-info ref="specialcondolenceInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{formData.taskName}}
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":"
class="flow-task-form">
<el-form-item label="审批意见" prop="tf_opinion"
: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_sign" :rules="[{required:true,message:'请完成审批签字',trigger:['change','blur']}]">
<pc-signature v-model="formData.tf_sign" :key="formData.processTaskId"></pc-signature>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意申请</el-button>
<el-button @click="handleTaskAction(2)" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到发起人</el-button>
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
</el-row>
</div>
</specialcondolence-info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
components: {
"specialcondolence-info": specialcondolenceInfo
},
data() {
return {
pageForm: {
approval: false
},
typeOptions: [],
tableColumns: [
{prop: 'subject', label: '事项'},
{prop: 'applyUserName', label: '申请人'},
{prop: 'applyUnionName', label: '所属工会'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'typeName', label: '慰问类型'},
{prop: 'recipientCount', label: '慰问人数', width: 100},
{prop: 'curTaskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
unionList: [],
unitList: [],
formData: {},
showApprovalForm: false
}
},
methods: {
onView(row) {
this.$refs.guava.edit(()=>{
this.showApprovalForm = false
this.$refs.specialcondolenceInfoRef.onOpen(row)
})
},
onAudit(row) {
this.$refs.guava.edit(()=>{
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName,
tf_sign: "",
tf_opinion: ""
}
this.$refs.specialcondolenceInfoRef.onOpen(row)
})
},
async handleTaskAction(val) {
try {
const valid = await this.$refs.formRef.validate()
if (!valid) return
} catch (error) {
return
}
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/special_condolence/approval/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
onRevoke(row) {
this.$confirm("您确定要撤回吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
querySpecialCondolenceType() {
this.$axios.post('/platform/special_condolence/type/querySpecialCondolenceType')
.then((resp) => {
this.typeOptions = resp.data
})
},
},
async created() {
this.unionList = await this.$businessTool.listUnion()
this.unitList = await this.$businessTool.listUnit()
this.querySpecialCondolenceType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,229 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
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">
</el-input>
</search-item>
<search-item label="慰问类型">
<el-select v-model="pageForm.type" @change="doSearch" style="width: 100%"
placeholder="请选择慰问类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" placeholder="请选择所属工会" filterable clearable
@change="doSearch" style="width: 100%">
<el-option
v-for="item in unionList"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</search-item>
<search-item label="所属单位">
<el-select @change="doSearch"
clearable
filterable
placeholder="请选择单位" style="width: 100%" v-model="pageForm.unitId">
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in unitList"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-radio-group v-model="pageForm.approval" size="small" @change="doSearch">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300px">
<template slot-scope="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.taskState === 10" @click="onAudit(row)" size="mini" type="primary">审核</el-button>
<el-button v-if="row.canRevoke" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<specialcondolence-info ref="specialcondolenceInfoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{formData.taskName}}
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":"
class="flow-task-form">
<el-form-item label="审批意见" prop="tf_opinion"
: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_sign" :rules="[{required:true,message:'请完成审批签字',trigger:['change','blur']}]">
<pc-signature v-model="formData.tf_sign" :key="formData.processTaskId"></pc-signature>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意申请</el-button>
<el-button @click="handleTaskAction(2)" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到发起人</el-button>
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
</el-row>
</div>
</specialcondolence-info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/info.js'){}#-->
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
components: {
"specialcondolence-info": specialcondolenceInfo
},
data() {
return {
pageForm: {
approval: false
},
typeOptions: [],
tableColumns: [
{prop: 'subject', label: '事项'},
{prop: 'applyUserName', label: '申请人'},
{prop: 'applyUnionName', label: '所属工会'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'typeName', label: '慰问类型'},
{prop: 'recipientCount', label: '慰问人数', width: 100},
{prop: 'curTaskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
unionList: [],
unitList: [],
formData: {},
showApprovalForm: false
}
},
methods: {
onView(row) {
this.$refs.guava.edit(()=>{
this.showApprovalForm = false
this.$refs.specialcondolenceInfoRef.onOpen(row)
})
},
onAudit(row) {
this.$refs.guava.edit(()=>{
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName,
tf_sign: "",
tf_opinion: ""
}
this.$refs.specialcondolenceInfoRef.onOpen(row)
})
},
async handleTaskAction(val) {
try {
const valid = await this.$refs.formRef.validate()
if (!valid) return
} catch (error) {
return
}
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/special_condolence/approval/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
onRevoke(row) {
this.$confirm("您确定要撤回吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", {taskId: row.taskId}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
querySpecialCondolenceType() {
this.$axios.post('/platform/special_condolence/type/querySpecialCondolenceType')
.then((resp) => {
this.typeOptions = resp.data
})
},
},
async created() {
this.unionList = await this.$businessTool.listUnion()
this.unitList = await this.$businessTool.listUnit()
this.querySpecialCondolenceType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,101 @@
const basicForm = {
template: /*language=HTML*/ `
<div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left">
<el-form-item label="类型编码" prop="code">
<el-input type="text" v-model="formData.code" maxlength="50"
placeholder="请填写类型编码"></el-input>
</el-form-item>
<el-form-item label="类型名称" prop="name">
<el-input type="text" v-model="formData.name" maxlength="50"
placeholder="请填写类型名称"></el-input>
</el-form-item>
<el-form-item label="慰问费用" prop="money">
<el-input-number style="width: 100%" v-model="formData.money"
placeholder="请填写慰问费用" controls-position="right" :controls="false"
precision="2"
:min="0"
:max="10000"></el-input-number>
</el-form-item>
<el-form-item label="慰问方式" prop="way">
<el-radio-group v-model="formData.way">
<el-radio-button label="慰问金 (A)" border>慰问金 (A)</el-radio-button>
<el-radio-button label="提货券 (B)" border>提货券 (B)</el-radio-button>
<el-radio-button label="慰问品 (C)" border>慰问品 (C)</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="是否启用" prop="enable">
<el-switch
v-model="formData.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</el-form-item>
<!--<el-form-item label="上传会议纪要" prop="uploadMeetingRecord">
<el-radio-group v-model="formData.uploadMeetingRecord">
<el-radio-button :label="true">上传</el-radio-button>
<el-radio-button :label="false">不上传</el-radio-button>
</el-radio-group>
</el-form-item>-->
<el-form-item label="是否上传附件" prop="isUploadFile">
<el-radio-group v-model="formData.isUploadFile">
<el-radio-button :label="true">是</el-radio-button>
<el-radio-button :label="false">否</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="上传附件说明" prop="uploadFileDesc" v-if="formData.isUploadFile">
<el-input v-model="formData.uploadFileDesc" maxlength="50" clearable placeholder="请填写上传附件说明"></el-input>
</el-form-item>
</el-form>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onSubmit" type="primary">提交</el-button>
</el-row>
</div>
`,
data() {
return {
formData: { isUploadFile: false },
formRules: {
name: [{required: true, message: '必填', trigger: ['blur', 'change']}],
money: [{required: true, message: '必填', trigger: ['blur', 'change']}],
way: [{required: true, message: '必填', trigger: ['blur', 'change']}],
code: [{required: true, message: '必填', trigger: ['blur', 'change']}],
isUploadFile: [{required: true, message: '必填', trigger: ['blur', 'change']}],
},
}
},
methods: {
onOpen(row) {
if(row && row.id) {
this.formData = clone(row)
} else {
this.formData = { isUploadFile: false }
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post("/platform/special_condolence/type/onSubmit", this.formData)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.warning(resp.msg)
}
})
}
})
},
},
style: /*language=CSS*/ `
.el-input-number .el-input__inner {
text-align: left;
}
`
}
@@ -0,0 +1,148 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/编码">
<el-input placeholder="请输入名称或编码" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="类型列表">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增类型
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'isUploadFile'">
<span v-if="row.isUploadFile" class="text-success">是</span>
<span v-else class="text-danger">否</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'enable'">
<el-switch
@change="switchChange(row)"
v-model="row.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="230">
<template v-slot="{ row }">
<el-button @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<basic-form ref="basicFormRef" @refresh="refresh"></basic-form>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('basicForm.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
},
data() {
return {
tableColumns: [
{prop: 'code', label: '类型编码'},
{prop: 'name', label: '类型名称'},
{prop: 'money', label: '金额'},
{prop: 'way', label: '慰问方式'},
// {prop: 'isUploadFile', label: '是否上传附件'},
{prop: 'enable', label: '是否启用'},
],
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onAdd() {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen()
})
},
onEdit(row) {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen(row)
})
},
onDelete(row) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/special_condolence/type/onDelete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
switchChange(row) {
this.$axios.post("/platform/special_condolence/type/onSubmit", row).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -62,7 +62,7 @@ const filterUser = {
</search-item>
<search-item label="所属单位">
<el-select @change="unitChange" clearable filterable
<el-select clearable filterable
placeholder="请选择所属单位" style="width: 100%"
v-model="pageForm.unitId">
<el-option
@@ -74,17 +74,6 @@ const filterUser = {
</el-select>
</search-item>
<search-item label="三级单位">
<el-select clearable filterable placeholder="请先选择所属单位" style="width: 100%"
v-model="pageForm.threeUnitId">
<el-option
:key="item.id"
:label="item.name"
:value="item.id"
v-for="item in threeUnitOptions">
</el-option>
</el-select>
</search-item>
<search-item label="人员类型:">
<dict-select clearable code="USER_PERSON_TYPE"
@@ -94,13 +83,6 @@ const filterUser = {
v-model="pageForm.personTypes"></dict-select>
</search-item>
<search-item label="人员属性:">
<dict-select clearable code="USER_ATTRIBUTE"
multiple
collapse-tags
placeholder="请选择人员属性"
v-model="pageForm.userAttributes"></dict-select>
</search-item>
<search-item label="在职状态:">
<dict-select clearable code="USER_STATE"
@@ -169,14 +151,11 @@ const filterUser = {
userStates: [],
personTypes: [],
preparedBys: [],
userAttributes: [],
unitId: null,
threeUnitId: null
unitId: null
},
sexOptions: ["男", "女"],
unionOptions: [],
unitOptions: [],
threeUnitOptions: [],
tableColumns: [
{ prop: "loginname", label: "工号" },
{ prop: "username", label: "姓名" },
@@ -185,8 +164,7 @@ const filterUser = {
{ prop: "personType", label: "人员类型", sortable: true },
{ prop: "userState", label: "在职状态", sortable: true },
{ prop: "unionName", label: "所属工会", sortable: true },
{ prop: "unitName", label: "所属单位", sortable: true },
{ prop: "threeUnitName", label: "三级单位", sortable: true }
{ prop: "unitName", label: "所属单位", sortable: true }
]
}
},
@@ -208,35 +186,12 @@ const filterUser = {
this.$set(this.pageForm, "birthMonths", [])
this.$set(this.pageForm, "unionId", null)
this.$set(this.pageForm, "unitId", null)
this.$set(this.pageForm, "threeUnitId", null)
this.threeUnitOptions = []
this.$set(this.pageForm, "personTypes", [])
this.$set(this.pageForm, "preparedBys", [])
this.$set(this.pageForm, "userAttributes", [])
this.$set(this.pageForm, "userStates", [])
this.$set(this.pageForm, "isMember", null)
},
// 所属单位改变后,仅加载该单位直属的三级单位,避免跨单位筛选。
unitChange(unitId) {
this.$set(this.pageForm, "threeUnitId", null)
this.threeUnitOptions = []
if (!unitId) {
return
}
this.$axios.post("/platform/sys/unit/child", { pid: unitId })
.then((res) => {
if (res.code === 0) {
this.threeUnitOptions = res.data || []
} else {
this.$message.error(res.msg || "三级单位查询失败")
}
})
.catch(() => {
this.$message.error("三级单位查询失败,请稍后重试")
})
},
pageData() {
this.tableLoading = true
this.$axios
@@ -65,10 +65,6 @@ layout("/layouts/platform.html"){
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttribute" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE"></dict-select>
</search-item>
</search>
</el-card>
@@ -63,7 +63,7 @@ const addUser = {
</search-item>
<search-item label="所属单位">
<el-select @change="unitChange" clearable
<el-select clearable
filterable
placeholder="请选择所属单位" style="width: 100%"
v-model="pageForm.unitId">
@@ -76,17 +76,6 @@ const addUser = {
</el-select>
</search-item>
<search-item label="三级单位">
<el-select clearable filterable placeholder="请先选择所属单位" style="width: 100%"
v-model="pageForm.threeUnitId">
<el-option
:key="item.id"
:label="item.name"
:value="item.id"
v-for="item in threeUnitOptions">
</el-option>
</el-select>
</search-item>
<search-item label="人员类型:">
<dict-select clearable code="USER_PERSON_TYPE"
@@ -96,13 +85,6 @@ const addUser = {
v-model="pageForm.personTypes"></dict-select>
</search-item>
<search-item label="人员属性:">
<dict-select clearable code="USER_ATTRIBUTE"
multiple
collapse-tags
placeholder="请选择人员属性"
v-model="pageForm.userAttributes"></dict-select>
</search-item>
<search-item label="在职状态:">
<dict-select clearable code="USER_STATE"
@@ -171,9 +153,7 @@ const addUser = {
birthMonths: [],
unionId: null,
unitId: null,
threeUnitId: null,
personTypes: [],
userAttributes: [],
preparedBys: [],
userStates: [],
isMember: null
@@ -181,7 +161,6 @@ const addUser = {
sexOptions: ["男", "女"],
unionOptions: [],
unitOptions: [],
threeUnitOptions: [],
tableColumns: [
{ prop: "loginname", label: "工号" },
{ prop: "username", label: "姓名" },
@@ -190,8 +169,7 @@ const addUser = {
{ prop: "personType", label: "人员类型", sortable: true },
{ prop: "userState", label: "在职状态", sortable: true },
{ prop: "unionName", label: "所属工会", sortable: true },
{ prop: "unitName", label: "所属单位", sortable: true },
{ prop: "threeUnitName", label: "三级单位", sortable: true }
{ prop: "unitName", label: "所属单位", sortable: true }
]
}
},
@@ -213,10 +191,7 @@ const addUser = {
this.$set(this.pageForm, "birthMonths", [])
this.$set(this.pageForm, "unionId", null)
this.$set(this.pageForm, "unitId", null)
this.$set(this.pageForm, "threeUnitId", null)
this.$set(this, "threeUnitOptions", [])
this.$set(this.pageForm, "personTypes", [])
this.$set(this.pageForm, "userAttributes", [])
this.$set(this.pageForm, "preparedBys", [])
this.$set(this.pageForm, "userStates", [])
this.$set(this.pageForm, "isMember", null)
@@ -224,7 +199,6 @@ const addUser = {
async unionIdChange(val) {
this.$set(this.pageForm, "unitId", null)
this.unitChange(null)
if (val) {
this.$set(this, "unitOptions", await this.$businessTool.listUnit(val))
} else {
@@ -232,26 +206,6 @@ const addUser = {
}
},
// 所属单位改变后,仅加载该单位直属的三级单位,避免跨单位筛选。
unitChange(unitId) {
this.$set(this.pageForm, "threeUnitId", null)
this.$set(this, "threeUnitOptions", [])
if (!unitId) {
return
}
this.$axios.post("/platform/sys/unit/child", { pid: unitId })
.then((res) => {
if (res.code === 0) {
this.$set(this, "threeUnitOptions", res.data || [])
} else {
this.$message.error(res.msg || "三级单位查询失败")
}
})
.catch(() => {
this.$message.error("三级单位查询失败,请稍后重试")
})
},
pageData() {
this.tableLoading = true
this.$axios
@@ -81,26 +81,12 @@ layout("/layouts/platform.html"){
collapse-tags
placeholder="请选择所属单位"
style="width: 100%"
@change="unitIdsChange"
v-model="pageForm.unitIds"
>
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in unitOptions"></el-option>
</el-select>
</search-item>
<search-item label="三级单位">
<el-select
clearable
filterable
multiple
collapse-tags
placeholder="请先选择所属单位"
style="width: 100%"
v-model="pageForm.threeUnitIds"
>
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in threeUnitOptions"></el-option>
</el-select>
</search-item>
<search-item label="人员类型:">
<dict-select
@@ -128,10 +114,6 @@ layout("/layouts/platform.html"){
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" clearable multiple collapse-tags></dict-select>
</search-item>
<!-- <search-item label="所选福利:">-->
<!-- <el-select clearable placeholder="请选择所选福利" style="width: 100%" v-model="pageForm.optionId">-->
@@ -283,7 +265,6 @@ layout("/layouts/platform.html"){
},
unionOptions: [],
unitOptions: [],
threeUnitOptions: [],
sexOptions: ["男", "女"],
projectSelectOptions: [],
tableColumns: [
@@ -295,7 +276,6 @@ layout("/layouts/platform.html"){
{ prop: "userState", label: "在职状态", sortable: true },
{ prop: "welfareUnionName", label: "所属工会", sortable: true },
{ prop: "welfareUnitName", label: "所属单位", sortable: true },
{ prop: "threeUnitName", label: "三级单位", sortable: true },
{ prop: "remark", label: "备注", sortable: true }
],
@@ -423,32 +403,6 @@ layout("/layouts/platform.html"){
this.doSearch()
})
},
// 根据已选所属单位查询直属三级单位,筛选项只保留当前所属单位范围内的数据。
async unitIdsChange(unitIds) {
this.$set(this.pageForm, "threeUnitIds", [])
this.$set(this, "threeUnitOptions", [])
if (!unitIds || unitIds.length === 0) {
return
}
try {
const responses = await Promise.all(unitIds.map((unitId) => this.$axios.post("/platform/sys/unit/child", { pid: unitId })))
const failedResponse = responses.find((res) => res.code !== 0)
if (failedResponse) {
this.$message.error(failedResponse.msg || "三级单位查询失败")
return
}
const optionMap = {}
responses.forEach((res) => {
const options = res.data || []
options.forEach((item) => {
optionMap[item.id] = item
})
})
this.$set(this, "threeUnitOptions", Object.keys(optionMap).map((id) => optionMap[id]))
} catch (e) {
this.$message.error("三级单位查询失败,请稍后重试")
}
},
pageData() {
this.tableLoading = true
this.$axios
@@ -77,7 +77,7 @@ layout("/layouts/platform_h5.html"){
<van-cell title="会长">{{ clubDetail.clubLeader || '--' }}</van-cell>
<van-cell title="秘书长">{{ clubDetail.clubSecretary || '--' }}</van-cell>
<van-cell title="当前人数">{{ clubDetail.currentPeopleNum || 0 }} 人</van-cell>
<van-cell title="会费标准">{{ clubDetail.due || '--' }}</van-cell>
<van-cell title="会费标准">{{ clubDetail.due === null || clubDetail.due === undefined ? '--' : clubDetail.due + ' 元/年' }}</van-cell>
<van-cell title="社团介绍" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.introduce)" v-html="clubDetail.introduce"></div><span v-else>暂无</span></van-cell>
<van-cell title="社团宗旨" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.purpose)" v-html="clubDetail.purpose"></div><span v-else>暂无</span></van-cell>
<van-cell title="社团章程" class="club-user-join-info-long-cell"><file-preview v-if="clubDetail.rulesFile && clubDetail.rulesFile.length" :files="clubDetail.rulesFile" complete_result></file-preview><span v-else>暂无</span></van-cell>
@@ -16,6 +16,11 @@ layout("/layouts/platform_h5.html"){
</div>
<div class="quiz-card-scroll" v-if="!answerRecord.isFinish">
<van-collapse v-if="activity.description" v-model="descriptionPanels" class="quiz-description" @change="adaptDescriptionToMobile">
<van-collapse-item title="说明" name="description" :value="descriptionPanels.length ? '收起' : '展开'">
<div ref="descriptionContent" class="quiz-description-content" v-html="activity.description"></div>
</van-collapse-item>
</van-collapse>
<div v-for="(subject, index) in subjects" :key="index" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<div class="tag">
@@ -65,6 +70,7 @@ layout("/layouts/platform_h5.html"){
H5JumpTo: GetQueryString("H5JumpTo"),
subjects: [],
activity: {},
descriptionPanels: [],
isFinished: false,
answerRecord: {},
answerRecordId: null,
@@ -100,6 +106,28 @@ layout("/layouts/platform_h5.html"){
}
},
methods: {
// 按原有字号层次缩小大字,不覆盖富文本颜色、字重、对齐等样式。
adaptDescriptionToMobile() {
this.$nextTick(() => {
const content = this.$refs.descriptionContent
if (!content || !this.descriptionPanels.length || content.dataset.mobileAdapted) return
// 先读取全部字号,避免父元素缩放后影响子元素的继承字号计算。
const sizes = [content, ...content.querySelectorAll('*')].map(element => ({
element,
size: parseFloat(window.getComputedStyle(element).fontSize)
}))
const maxSize = sizes.reduce((max, item) => Math.max(max, item.size || 0), 0)
if (maxSize > 22) {
const ratio = 8 / (maxSize - 14)
sizes.forEach(({ element, size }) => {
if (!Number.isFinite(size)) return
const mobileSize = size > 14 ? 14 + (size - 14) * ratio : size
element.style.setProperty('font-size', mobileSize + 'px', 'important')
})
}
content.dataset.mobileAdapted = 'true'
})
},
getCategoryPageTitle(category, defaultTitle) {
const titleMap = {
QUIZ: "在线答题",
@@ -412,6 +440,25 @@ layout("/layouts/platform_h5.html"){
box-sizing: border-box;
}
.quiz-description {
margin-bottom: 14px;
border-radius: 8px;
overflow: hidden;
}
.quiz-description-content {
color: #303133;
line-height: 1.8;
overflow-wrap: break-word;
overflow-x: auto;
}
/deep/ .quiz-description-content img {
max-width: 100%;
height: auto;
}
.subject {
background: #fff;
padding: 15px;
@@ -276,8 +276,22 @@ layout("/layouts/platform_h5.html"){
font-weight: 600;
}
/* 导航栏参与弹层布局,正文独立滚动,避免固定导航遮住首张卡片。 */
.tour-record-popup {
display: flex;
flex-direction: column;
overflow: hidden;
padding-top: env(safe-area-inset-top);
box-sizing: border-box;
}
.tour-record-popup > .van-nav-bar {
flex-shrink: 0;
}
.tour-popup-body {
height: calc(100vh - 46px);
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 10px 12px 76px;
background: #f4f6f8;
@@ -594,7 +608,7 @@ layout("/layouts/platform_h5.html"){
}
.tour-popup-body {
padding: 12px 12px 82px;
padding: 12px 12px calc(82px + env(safe-area-inset-bottom));
background-color: #f3f7fd;
}
@@ -616,6 +630,12 @@ layout("/layouts/platform_h5.html"){
box-shadow: 0 -6px 18px rgba(38, 66, 103, .08);
}
.tour-bottom-bar .tour-cancel-button {
border: 0;
color: #65748a;
background-color: #f0f2f5;
}
/* 列表页结构与提案校工会预审核保持一致,业务字段及操作条件继续沿用疗休养逻辑。 */
.tour-my-page .van-nav-bar {
background-color: #fff;
@@ -917,8 +937,8 @@ layout("/layouts/platform_h5.html"){
</div>
</van-popup>
<van-popup v-model="detailVisible" position="right" :style="{ width: '100%', height: '100%' }" :close-on-click-overlay="false">
<van-nav-bar title="报名详情" left-text="返回" left-arrow @click-left="closeHistoryPopup('detail')" fixed placeholder></van-nav-bar>
<van-popup v-model="detailVisible" class="tour-record-popup" position="right" :style="{ width: '100%', height: '100%' }" :close-on-click-overlay="false">
<van-nav-bar title="报名详情" left-text="返回" left-arrow @click-left="closeHistoryPopup('detail')"></van-nav-bar>
<div class="tour-popup-body">
<div class="tour-section">
<div class="tour-section-title">教职工信息</div>
@@ -926,6 +946,7 @@ layout("/layouts/platform_h5.html"){
<van-cell title="姓名" :value="detail.userName || ''"></van-cell>
<van-cell title="性别" :value="detail.gender || ''"></van-cell>
<van-cell title="身份证号" :value="detail.idCard || ''"></van-cell>
<van-cell title="手机号" :value="detail.mobile || ''"></van-cell>
<van-cell title="所在单位" :value="detail.unitName || ''"></van-cell>
<van-cell title="所在工会" :value="detail.unionName || ''"></van-cell>
</div>
@@ -972,14 +993,15 @@ layout("/layouts/platform_h5.html"){
</div>
</van-popup>
<van-popup v-model="signupVisible" position="right" :style="{ width: '100%', height: '100%' }" :close-on-click-overlay="false" @closed="destroySignup">
<van-nav-bar title="修改报名" left-text="返回" left-arrow @click-left="closeHistoryPopup('signup')" fixed placeholder></van-nav-bar>
<van-popup v-model="signupVisible" class="tour-record-popup" position="right" :style="{ width: '100%', height: '100%' }" :close-on-click-overlay="false" @closed="destroySignup">
<van-nav-bar title="修改报名" left-text="返回" left-arrow @click-left="closeHistoryPopup('signup')"></van-nav-bar>
<div class="tour-popup-body">
<div class="tour-section">
<div class="tour-section-title">教职工信息</div>
<van-field label="姓名" readonly v-model="signupForm.userName"></van-field>
<van-field label="工号" readonly v-model="signupForm.jobNo"></van-field>
<van-field label="身份证号" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号"></van-field>
<van-field required label="身份证号" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号"></van-field>
<van-field required label="手机号" v-model="signupForm.mobile" type="tel" maxlength="11" placeholder="请填写手机号"></van-field>
<van-field label="所在工会" readonly v-model="signupForm.unionName"></van-field>
</div>
@@ -1007,12 +1029,12 @@ layout("/layouts/platform_h5.html"){
<div v-if="isDirectFamilyLine(signupForm)" class="tour-section">
<div class="tour-section-title">直系亲属线路</div>
<van-field label="亲属姓名" v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></van-field>
<van-field label="所在单位" v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></van-field>
<van-field readonly clickable is-link label="亲属关系" v-model="directRelativeForm.relationshipName" placeholder="请选择亲属关系" @click="openDirectRelativePicker"></van-field>
<van-field label="线路名称" v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></van-field>
<van-field label="出行开始" v-model="directRelativeForm.travelStartTime" type="date" placeholder="请选择出行开始日期"></van-field>
<van-field label="出行结束" v-model="directRelativeForm.travelEndTime" type="date" placeholder="请选择出行结束日期"></van-field>
<van-field required label="亲属姓名" v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></van-field>
<van-field required label="所在单位" v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></van-field>
<van-field required readonly clickable is-link label="亲属关系" v-model="directRelativeForm.relationshipName" placeholder="请选择亲属关系" @click="openDirectRelativePicker"></van-field>
<van-field required label="线路名称" v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></van-field>
<van-field required label="出行开始" v-model="directRelativeForm.travelStartTime" type="date" placeholder="请选择出行开始日期"></van-field>
<van-field required label="出行结束" v-model="directRelativeForm.travelEndTime" type="date" placeholder="请选择出行结束日期"></van-field>
</div>
<div v-if="allowFamily && !isDirectFamilyLine(signupForm)" class="tour-section">
@@ -1054,7 +1076,7 @@ layout("/layouts/platform_h5.html"){
:title="familyTabTitle(family, familyIndex)">
<div class="tour-family-form">
<van-field required label="姓名" v-model="family.familyName" maxlength="100" placeholder="请填写姓名"></van-field>
<van-field required label="年龄" v-model.number="family.age" type="digit" placeholder="请填写年龄"></van-field>
<van-field required label="年龄" v-model.number="family.age" type="number" :formatter="formatFamilyAge" placeholder="请填写年龄"></van-field>
<van-field required label="性别">
<template #input>
<van-radio-group v-model="family.gender" direction="horizontal">
@@ -1065,7 +1087,7 @@ layout("/layouts/platform_h5.html"){
</van-field>
<van-field :required="familyContactRequired(family)" label="身份证号码" v-model="family.idCard" maxlength="18" placeholder="请填写身份证号码" @blur="normalizeFamilyIdCard(family)"></van-field>
<van-field :required="familyContactRequired(family)" label="手机号" v-model="family.mobile" type="tel" maxlength="30" placeholder="请填写手机号"></van-field>
<van-field required readonly clickable is-link label="与本人关系" v-model="family.relationship" placeholder="请选择关系" @click="openRelationshipPicker"></van-field>
<van-field required readonly clickable is-link label="与本人关系" v-model="family.relationship" placeholder="请选择关系" @click="openRelationshipPicker(familyIndex)"></van-field>
<van-field v-if="fillBedInfo" readonly clickable is-link label="床型" v-model="family.bedType" placeholder="请选择房型" @click="openBedPicker('family')"></van-field>
<van-field v-if="fillBedInfo" label="床位" v-model="family.bedInfo" maxlength="100" placeholder="请输入床位数量"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="family.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
@@ -1087,8 +1109,8 @@ layout("/layouts/platform_h5.html"){
<div v-else class="tour-family-empty">暂无亲属,请按需添加</div>
</div>
</div>
<div class="tour-bottom-bar">
<van-button v-if="signupForm.id" type="danger" plain block round :loading="cancelLoading" @click="cancelSignup">取消报名</van-button>
<div class="tour-bottom-bar tour-h5-fixed-bar">
<van-button v-if="signupForm.id" class="tour-cancel-button" type="default" block round :loading="cancelLoading" @click="cancelSignup">取消报名</van-button>
<van-button type="info" block round :loading="signupLoading" @click="submitSignup">提交</van-button>
</div>
</van-popup>
@@ -1442,6 +1464,7 @@ layout("/layouts/platform_h5.html"){
userName: "",
gender: "",
idCard: "",
mobile: "",
unitId: "",
unitName: "",
unionId: "",
@@ -1601,6 +1624,11 @@ layout("/layouts/platform_h5.html"){
isBlankValue(value) {
return value === undefined || value === null || !String(value).trim()
},
// 与 Web 端一致,亲属年龄限定为 0~120 岁整数,清空时保留必填状态。
formatFamilyAge(value) {
const age = parseInt(value, 10)
return Number.isNaN(age) ? "" : String(Math.min(120, Math.max(0, age)))
},
// 年满16周岁的亲属才强制填写身份证号码和手机号。
familyContactRequired(family) {
return family && !this.isBlankValue(family.age) && Number(family.age) >= 16
@@ -307,9 +307,9 @@ layout("/layouts/platform_tour_signup_h5.html"){
<div v-if="isDirectFamilyLine(signupForm)" class="tour-apply-card">
<div class="tour-apply-title">直系亲属线路</div>
<van-field label="亲属姓名" v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></van-field>
<van-field label="所在单位" v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></van-field>
<van-field
<van-field required label="亲属姓名" v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></van-field>
<van-field required label="所在单位" v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></van-field>
<van-field required
label="亲属关系"
readonly
clickable
@@ -318,9 +318,9 @@ layout("/layouts/platform_tour_signup_h5.html"){
v-model="directRelativeForm.relationshipName"
@click="openDirectRelativePicker">
</van-field>
<van-field label="线路名称" v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></van-field>
<van-field label="出行开始" v-model="directRelativeForm.travelStartTime" type="date" placeholder="请选择出行开始日期"></van-field>
<van-field label="出行结束" v-model="directRelativeForm.travelEndTime" type="date" placeholder="请选择出行结束日期"></van-field>
<van-field required label="线路名称" v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></van-field>
<van-field required label="出行开始" v-model="directRelativeForm.travelStartTime" type="date" placeholder="请选择出行开始日期"></van-field>
<van-field required label="出行结束" v-model="directRelativeForm.travelEndTime" type="date" placeholder="请选择出行结束日期"></van-field>
</div>
<div class="tour-apply-card">
@@ -365,7 +365,7 @@ layout("/layouts/platform_tour_signup_h5.html"){
:title="familyTabTitle(family, familyIndex)">
<div class="tour-family-form">
<van-field required label="姓名" v-model="family.familyName" maxlength="100" placeholder="请填写姓名"></van-field>
<van-field required label="年龄" v-model.number="family.age" type="digit" placeholder="请填写年龄"></van-field>
<van-field required label="年龄" v-model.number="family.age" type="number" :formatter="formatFamilyAge" placeholder="请填写年龄"></van-field>
<van-field required label="性别">
<template #input>
<van-radio-group v-model="family.gender" direction="horizontal">
@@ -753,6 +753,11 @@ layout("/layouts/platform_tour_signup_h5.html"){
isBlankValue(value) {
return value === undefined || value === null || !String(value).trim()
},
// 与 Web 端一致,亲属年龄限定为 0~120 岁整数,清空时保留必填状态。
formatFamilyAge(value) {
const age = parseInt(value, 10)
return Number.isNaN(age) ? "" : String(Math.min(120, Math.max(0, age)))
},
// 年满16周岁的亲属才强制填写身份证号码和手机号。
familyContactRequired(family) {
return family && !this.isBlankValue(family.age) && Number(family.age) >= 16
@@ -114,33 +114,6 @@ layout("/layouts/platform_h5.html"){
审批意见<span class="difficult-help-audit-card__required">*</span>
</h2>
<van-form ref="formRef">
<section class="difficult-help-info-card">
<h2 class="difficult-help-info-card__title">审核信息</h2>
<div class="difficult-help-audit-form">
<van-field
v-model="formData.level"
class="difficult-help-audit-select"
name="level"
label="级别"
placeholder="请选择级别"
readonly
is-link
required
@click="openLevelPicker"
:rules="[{ required: true, message: '请选择级别' }]"
></van-field>
<van-field
v-model="formData.money"
class="difficult-help-audit-money"
name="money"
label="帮扶金额(元)"
placeholder="请输入帮扶金额(元)"
type="number"
required
:rules="[{ required: true, message: '请输入帮扶金额(元)' }]"
></van-field>
</div>
</section>
<van-field
v-model="formData.tf_opinion"
class="difficult-help-audit-textarea"
@@ -167,14 +140,6 @@ layout("/layouts/platform_h5.html"){
</div>
</div>
</info>
<van-popup :value="showLevelPicker" position="bottom" :close-on-click-overlay="false">
<van-picker
show-toolbar
:columns="levelColumns"
@confirm="onLevelConfirm"
@cancel="closeLevelPicker">
</van-picker>
</van-popup>
<van-dialog
:value="confirmVisible"
title="提示"
@@ -214,8 +179,6 @@ layout("/layouts/platform_h5.html"){
skeletonLoading: true,
formData: {},
levelColumns: [],
showLevelPicker: false,
showApprovalForm: false,
formLoading: false,
confirmVisible: false,
@@ -280,42 +243,11 @@ layout("/layouts/platform_h5.html"){
this.$set(this, "formData", {
processTaskId: row.taskId,
taskName: row.curTaskName,
level: "",
money: undefined,
tf_sign: "", tf_opinion: ""
})
this.$refs.infoRef.onOpen(row, row.curTaskName || "帮扶审核")
},
// 加载困难级别字典并转换为 Vant Picker 所需的 text/value 结构。
initLevelDict() {
return this.$axios.post("/open/common/dictOptions", {code: "DIFFICULT_LEVEL"}, {h5SkeletonLoading: true}).then((res) => {
const levelData = res && res.code === 0 ? res.data : []
this.$set(this, "levelColumns", (levelData || []).map((item) => ({
text: item.name,
value: item.code
})))
})
},
// 级别选择器作为详情上层弹框,打开时同步增加浏览器历史。
openLevelPicker() {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.open("difficult-help-level-picker")) return
this.$set(this, "showLevelPicker", true)
},
closeLevelPicker(afterClose = null) {
if (window.h5HistoryLayerManager && window.h5HistoryLayerManager.close("difficult-help-level-picker", afterClose)) return
this.$set(this, "showLevelPicker", false)
if (typeof afterClose === "function") afterClose()
},
onLevelConfirm(option) {
this.closeLevelPicker(() => {
this.$set(this.formData, "level", option.value)
})
},
// val 为流程提交类型:1 同意、2 拒绝、6 退回;校验通过后打开历史栈确认框。
handleTaskAction(val) {
this.$refs.formRef.validate()
@@ -427,14 +359,12 @@ layout("/layouts/platform_h5.html"){
},
},
created() {
// 先订阅弹框状态,工会加载异常也不能阻断等级选择器与确认框的打开、返回。
// 先订阅弹框状态,工会加载异常也不能阻断确认框的打开、返回。
if (window.h5HistoryLayerManager) {
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
this.$set(this, "confirmVisible", stack.includes("difficult-help-action-confirm"))
this.$set(this, "showLevelPicker", stack.includes("difficult-help-level-picker"))
}))
}
this.initLevelDict()
// 缓存已就绪时立即加载,否则等待用户接口回填;只初始化一次,避免重置已选筛选条件。
// $watch 由当前 Vue 实例管理,页面销毁时自动注销。
let filtersInitialized = false

Some files were not shown because too many files have changed in this diff Show More