59 changed files with 1342 additions and 1124 deletions
@@ -1,10 +1,16 @@
package com.budwk.app.base.utils; package com.budwk.app.base.utils;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import com.aspose.cells.FontConfigs;
import com.aspose.cells.PdfSaveOptions; import com.aspose.cells.PdfSaveOptions;
import com.aspose.cells.Workbook; import com.aspose.cells.Workbook;
import com.aspose.slides.FontsLoader;
import com.aspose.slides.LoadOptions;
import com.aspose.slides.Presentation; import com.aspose.slides.Presentation;
import com.aspose.words.Document; 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.aspose.words.SaveFormat;
import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.exception.BaseException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -12,6 +18,8 @@ import lombok.extern.slf4j.Slf4j;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/** /**
* 文档转换 * 文档转换
@@ -19,6 +27,81 @@ import java.io.IOException;
@Slf4j @Slf4j
public class OfficePlusUtil { 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) { public static void presentationConvertPdf(String sourcePath, String targetPath) {
Presentation presentation = null; Presentation presentation = null;
try { 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); presentation.save(targetPath, com.aspose.slides.SaveFormat.Pdf);
} catch (Exception e) { } catch (Exception e) {
log.info("转换pdf报错", e); log.info("转换pdf报错", e);
@@ -68,10 +155,12 @@ public class OfficePlusUtil {
public static void excelConvertPdf(String sourcePath, String targetPath) { public static void excelConvertPdf(String sourcePath, String targetPath) {
FileOutputStream fileOS = null; FileOutputStream fileOS = null;
try { try {
configureDefaultFont();
Workbook wb = new Workbook(sourcePath); Workbook wb = new Workbook(sourcePath);
fileOS = new FileOutputStream(targetPath); fileOS = new FileOutputStream(targetPath);
PdfSaveOptions pdfSaveOptions = new PdfSaveOptions(); PdfSaveOptions pdfSaveOptions = new PdfSaveOptions();
pdfSaveOptions.setOnePagePerSheet(true); pdfSaveOptions.setOnePagePerSheet(true);
pdfSaveOptions.setDefaultFont(DEFAULT_FONT_NAME);
// for (int i = 0; i < 3; i++) { // for (int i = 0; i < 3; i++) {
// wb.getWorksheets().get(i).getHorizontalPageBreaks().clear(); // wb.getWorksheets().get(i).getHorizontalPageBreaks().clear();
// wb.getWorksheets().get(i).getVerticalPageBreaks().clear(); // wb.getWorksheets().get(i).getVerticalPageBreaks().clear();
@@ -105,6 +194,7 @@ public class OfficePlusUtil {
public static void wordConvertPdf(String sourcePath, String targetPath) { public static void wordConvertPdf(String sourcePath, String targetPath) {
FileOutputStream os = null; FileOutputStream os = null;
try { try {
configureDefaultFont();
File file = new File(targetPath); File file = new File(targetPath);
os = new FileOutputStream(file); os = new FileOutputStream(file);
Document doc = new Document(sourcePath); 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_file;
import com.budwk.app.sys.models.Sys_office_template; import com.budwk.app.sys.models.Sys_office_template;
import com.budwk.app.sys.utils.SysFileMinIoUtil; 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.ByteArrayPictureRenderData;
import com.deepoove.poi.data.PictureRenderData; import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType; import com.deepoove.poi.data.PictureType;
@@ -18,6 +20,12 @@ import org.nutz.ioc.loader.annotation.IocBean;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; 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.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -66,6 +74,88 @@ public class SysOfficeTemplateUtil {
return new ByteArrayInputStream(fileBytes); 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的图片对象 * 签字url转换为poi的图片对象
* *
@@ -77,13 +167,7 @@ public class SysOfficeTemplateUtil {
public PictureRenderData createPictureRenderData(int width, int height, String url) { public PictureRenderData createPictureRenderData(int width, int height, String url) {
try { try {
byte[] bytes = getFileBytesByUrl(url); byte[] bytes = getFileBytesByUrl(url);
// if (StrUtil.isBlank(url)) { log.info("[Office图片读取] 创建图片渲染数据,referenceLength={}, byteCount={}, width={}, height={}", StrUtil.nullToEmpty(url).length(), bytes == null ? null : bytes.length, width, height);
// return null;
// }
// int startIndex = url.indexOf("id=") + "id=".length();
// String id = url.substring(startIndex);
// Sys_file sysFile = dao.fetch(Sys_file.class, id);
// byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
ByteArrayPictureRenderData pictureRenderData = new ByteArrayPictureRenderData(bytes, PictureType.PNG); ByteArrayPictureRenderData pictureRenderData = new ByteArrayPictureRenderData(bytes, PictureType.PNG);
PictureStyle pictureStyle = new PictureStyle(); PictureStyle pictureStyle = new PictureStyle();
pictureStyle.setWidth(width); pictureStyle.setWidth(width);
@@ -91,7 +175,7 @@ public class SysOfficeTemplateUtil {
pictureRenderData.setPictureStyle(pictureStyle); pictureRenderData.setPictureStyle(pictureStyle);
return pictureRenderData; return pictureRenderData;
} catch (Exception e) { } catch (Exception e) {
log.error("文件转换PictureRenderData错误,文件路径:{},{}", url, e.getMessage()); log.error("[Office图片读取] 创建图片渲染数据失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
return null; return null;
} }
} }
@@ -140,13 +224,20 @@ public class SysOfficeTemplateUtil {
public byte[] getFileBytesByUrl(String url) { public byte[] getFileBytesByUrl(String url) {
try { try {
if (StrUtil.isBlank(url)) { if (StrUtil.isBlank(url)) {
log.warn("[Office图片读取] 图片引用为空");
return null; return null;
} }
int startIndex = url.indexOf("id=") + "id=".length(); int startIndex = url.indexOf("id=") + "id=".length();
String id = url.substring(startIndex); String id = url.substring(startIndex);
String logFileId = url.contains("id=") ? id : "未识别id参数";
log.info("[Office图片读取] 解析文件引用,fileId={}, hasIdParameter={}", logFileId, url.contains("id="));
Sys_file sysFile = dao.fetch(Sys_file.class, id); Sys_file sysFile = dao.fetch(Sys_file.class, id);
return SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath()); log.info("[Office图片读取] 查询文件记录,fileId={}, found={}, bucket={}, storagePathPresent={}", logFileId, sysFile != null, sysFile == null ? null : sysFile.getBucket(), sysFile != null && StrUtil.isNotBlank(sysFile.getStoragePath()));
byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
log.info("[Office图片读取] 读取存储文件完成,fileId={}, byteCount={}", logFileId, bytes == null ? null : bytes.length);
return bytes;
} catch (Exception e) { } catch (Exception e) {
log.error("[Office图片读取] 读取图片失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
return null; return null;
} }
} }
@@ -1,27 +1,80 @@
package com.budwk.app.flow.listenter; package com.budwk.app.flow.listenter;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.event.ProcessEvent; import com.budwk.app.flow.engine.event.ProcessEvent;
import com.budwk.app.flow.engine.event.ProcessEventListener; import com.budwk.app.flow.engine.event.ProcessEventListener;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessEventTypeEnum; import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.flow.service.FlowTaskMessageService; import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
/** 普通节点、首次会签及转办任务统一从开始事件进入待办消息 service。 */ import java.util.List;
/**
* 流程任务开始监听器
*/
@IocBean @IocBean
@Slf4j
public class ProcessTaskStartEventListener implements ProcessEventListener { public class ProcessTaskStartEventListener implements ProcessEventListener {
@Inject @Inject
private Dao dao; private Dao dao;
@Inject @Inject
private FlowTaskMessageService flowTaskMessageService; private GlobalMessageSendService globalMessageSendService;
@Inject
private DifficultHelpCommonService difficultHelpCommonService;
@Override @Override
public void onEvent(ProcessEvent event) { public void onEvent(ProcessEvent event) {
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) { if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) {
ProcessTask task = dao.fetch(ProcessTask.class, event.getSourceId()); Long sourceId = event.getSourceId();
flowTaskMessageService.notifyTask(task, false); ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
if (task == null) return;
sendMessage(task);
// 节点及接收人判断集中在业务 service;通知失败不向流程提交接口抛出异常。
try {
difficultHelpCommonService.notifyOfficeApproval(task);
} catch (Exception e) {
log.error("困难帮扶企业微信待办通知异常,taskId={}", sourceId, e);
}
} }
} }
private void sendMessage(ProcessTask task) {
// 说明是流程开始 用户首次完成任务
if (task.getTaskParentId() == 0 && task.getTaskName().equals("startTask")) {
return;
}
// 节点名称
String taskDisplayName = task.getDisplayName();
// 实例名称
Long instanceId = task.getProcessInstanceId();
ProcessInstance instance = dao.fetch(ProcessInstance.class, instanceId);
String instanceName = Json.fromJson(NutMap.class, instance.getVariable()).getString(FlowConst.PROCESS_INSTANCE_NAME);
// 任务接收人
List<ProcessTaskActor> taskActors = dao.query(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
for (ProcessTaskActor taskActor : taskActors) {
// 模拟发送消息
String message = StrUtil.format("您有一条待办任务,实例:{},处理环节:{}", taskActor.getActorName(), instanceName, taskDisplayName);
log.info(message);
globalMessageSendService.sendMessage(taskDisplayName, message, 2, List.of(taskActor.getActorId()), null);
}
}
} }
@@ -1,132 +0,0 @@
package com.budwk.app.flow.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/** WF 待办统一消息入口;站内消息、短信和企业微信沿用全局消息渠道。 */
@IocBean
@Slf4j
public class FlowTaskMessageService {
private static final String HOME = "/platform/home";
private static final Set<String> TASK_PARAMS = Set.of("taskId", "bizId", "taskKey", "tab");
@Inject
private Dao dao;
@Inject
private GlobalMessageSendService globalMessageSendService;
/**
* task 为参与人已落库的 WF 任务;includeStartTask 仅驳回重建时传 true,
* 使退回申请人的 startTask 也收到通知,首次提交的申请节点不通知本人。
* 无返回值:仅通知进行中的任务,异常记录 taskId,不中断业务审核。
* config 的 wfTaskId 标识流程通知,wfLoginNames 为办理人的 loginname,
* wxWorkLink 为卡片地址;微信策略据此切换到官方企业微信接口。
*/
@Aop(TransAop.READ_COMMITTED)
public void notifyTask(ProcessTask task, boolean includeStartTask) {
if (task == null || task.getId() == null || task.getProcessInstanceId() == null
|| !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) return;
if (!includeStartTask && "startTask".equals(task.getTaskName())
&& (task.getTaskParentId() == null || task.getTaskParentId() == 0L)) return;
try {
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null) {
log.warn("WF 待办通知跳过:实例不存在,taskId={}", task.getId());
return;
}
ProcessDefine define = instance.getProcessDefineId() == null ? null
: dao.fetch(ProcessDefine.class, instance.getProcessDefineId());
List<ProcessTaskActor> actors = dao.query(ProcessTaskActor.class,
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
List<String> receiverIds = actors.stream().map(ProcessTaskActor::getActorId)
.filter(StrUtil::isNotBlank).distinct().toList();
if (receiverIds.isEmpty()) {
log.warn("WF 待办通知跳过:任务没有实际办理人,taskId={}", task.getId());
return;
}
// 只用实际参与人的账号,不按角色扩大范围,不以内部用户 ID 代替企业微信 userid。
List<String> loginNames = actors.stream().filter(actor -> StrUtil.isNotBlank(actor.getActorId()))
.map(ProcessTaskActor::getActorAccount).filter(StrUtil::isNotBlank)
.map(String::trim).distinct().toList();
if (actors.stream().anyMatch(actor -> StrUtil.isBlank(actor.getActorAccount()))) {
log.warn("WF 待办存在未绑定 loginname 的办理人,taskId={}", task.getId());
}
String instanceName = define == null ? "流程审批" : StrUtil.blankToDefault(define.getDisplayName(), "流程审批");
if (StrUtil.isNotBlank(instance.getVariable())) {
try {
instanceName = StrUtil.blankToDefault(JSONUtil.parseObj(instance.getVariable())
.getStr(FlowConst.PROCESS_INSTANCE_NAME), instanceName);
} catch (Exception e) {
log.warn("WF 实例名称解析失败,使用流程定义名称,taskId={}", task.getId());
}
}
String nodeName = StrUtil.blankToDefault(task.getDisplayName(), "待办审核");
String content = "您有一条待办任务,实例:" + instanceName + ",处理环节:" + nodeName;
JSONObject config = JSONUtil.createObj().set("wfTaskId", task.getId())
.set("wfLoginNames", loginNames).set("wxWorkLink", buildLink(task, instance, define));
// 系统发送身份兼容定时推进等没有当前登录会话的流程入口。
globalMessageSendService.sendSystemMessage(nodeName, content, 2, receiverIds, config);
} catch (Exception e) {
log.error("WF 待办通知异常,taskId={}", task.getId(), e);
}
}
/** 优先节点手机表单;缺失或非法时回首页。保留原查询参数和锚点,替换过期任务参数。 */
private String buildLink(ProcessTask task, ProcessInstance instance, ProcessDefine define) {
String form = task.getH5FormKey();
// KNBF 历史定义中的办公室地址过时,沿用已经验证的实际审核入口。
if (define != null && "KNBF".equals(define.getName())
&& "36ebafda-44c2-4bd3-bd7f-10839a575a0d".equals(task.getTaskName())) {
form = "/platform/difficultHelp/branchUnionApproval/h5";
}
if (StrUtil.isBlank(form) || "null".equalsIgnoreCase(form.trim())
|| StrUtil.isBlank(instance.getBusinessNo()) || StrUtil.isBlank(task.getTaskName())) return HOME;
try {
form = form.trim();
URI uri = URI.create(form);
if (uri.getUserInfo() != null || (uri.isAbsolute()
? !("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme()))
|| uri.getHost() == null
: !form.startsWith("/") || uri.getRawAuthority() != null)) return HOME;
List<String> params = new ArrayList<>();
if (StrUtil.isNotBlank(uri.getRawQuery())) {
for (String param : uri.getRawQuery().split("&")) {
String key = URLDecoder.decode(param.split("=", 2)[0], StandardCharsets.UTF_8);
if (!TASK_PARAMS.contains(key)) params.add(param);
}
}
params.add("taskId=" + task.getId());
params.add("bizId=" + URLEncoder.encode(instance.getBusinessNo(), StandardCharsets.UTF_8));
params.add("taskKey=" + URLEncoder.encode(task.getTaskName(), StandardCharsets.UTF_8));
params.add("tab=todo");
String base = form.split("[?#]", 2)[0];
return base + "?" + String.join("&", params)
+ (uri.getRawFragment() == null ? "" : "#" + uri.getRawFragment());
} catch (IllegalArgumentException e) {
log.warn("WF 手机表单地址无效,卡片回退首页,taskId={}", task.getId());
return HOME;
}
}
}
@@ -26,7 +26,6 @@ import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor; import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.*; import com.budwk.app.flow.enums.*;
import com.budwk.app.flow.service.FlowTaskMessageService;
import com.budwk.app.flow.service.ProcessDefineService; import com.budwk.app.flow.service.ProcessDefineService;
import com.budwk.app.flow.service.ProcessInstanceService; import com.budwk.app.flow.service.ProcessInstanceService;
import com.budwk.app.flow.service.ProcessTaskService; import com.budwk.app.flow.service.ProcessTaskService;
@@ -61,9 +60,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
@Inject @Inject
private SchoolOaTodoService schoolOaTodoService; private SchoolOaTodoService schoolOaTodoService;
@Inject
private FlowTaskMessageService flowTaskMessageService;
@Override @Override
public boolean save(ProcessTask param) { public boolean save(ProcessTask param) {
param.setId(null); param.setId(null);
@@ -296,8 +292,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor)); addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor));
// 并行加签绕过普通节点创建入口,需要为新增的个人任务同步待办。 // 并行加签绕过普通节点创建入口,需要为新增的个人任务同步待办。
schoolOaTodoService.createTodo(clone); schoolOaTodoService.createTodo(clone);
// 并行加签不经过 CreateTaskHandler,只为本次新增任务通知一次。
flowTaskMessageService.notifyTask(clone, false);
}); });
} }
@@ -368,8 +362,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
addTaskActor(task.getId(), CollectionUtil.newArrayList(task.getOperator())); addTaskActor(task.getId(), CollectionUtil.newArrayList(task.getOperator()));
// 驳回创建了新的任务 ID,按新任务推送,避免复用历史待办。 // 驳回创建了新的任务 ID,按新任务推送,避免复用历史待办。
schoolOaTodoService.createTodo(task); schoolOaTodoService.createTodo(task);
// 驳回重建不发布开始事件;退回申请节点时也通知申请人重新办理。
flowTaskMessageService.notifyTask(task, true);
return task; return task;
} }
@@ -449,12 +441,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class); ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class);
// 更新会签变量到流程实例参数中 // 更新会签变量到流程实例参数中
processInstanceService.updateCountersignVariable(taskModel, execution, taskActors); processInstanceService.updateCountersignVariable(taskModel, execution, taskActors);
// 串行会签后续办理人由 CountersignHandler 直接创建,不经过开始事件;首次会签仍由监听器通知。
String counterPrefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + taskModel.getName() + "_";
if (CountersignTypeEnum.SEQUENTIAL.equals(taskModel.getCountersignType())
&& execution.getArgs().get(counterPrefix + FlowConst.LOOP_COUNTER, -1) >= 0) {
processTaskList.forEach(task -> flowTaskMessageService.notifyTask(task, false));
}
return processTaskList; return processTaskList;
} }
@@ -84,12 +84,10 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
task.getId(), task.getProcessInstanceId(), task.getTaskState()); task.getId(), task.getProcessInstanceId(), task.getTaskState());
} }
/** /** 系统参数优先于文件配置,只有明确为 true 才启用;未配置不访问数据库或 OA。 */
* OA 待办接口联调尚未完成,暂时在代码中固定关闭,避免远端异常阻断内部 WF 流转。
* 不受 AppSchoolOa 或 enabled 配置影响;恢复对接时需显式恢复配置开关逻辑。
*/
private boolean isEnabled() { private boolean isEnabled() {
return false; Object enabled = Globals.MyConfig.get("AppSchoolOa");
return Boolean.parseBoolean(enabled == null ? conf.get(PREFIX + "enabled", "false") : enabled.toString());
} }
private void requireTask(ProcessTask task) { private void requireTask(ProcessTask task) {
@@ -4,12 +4,14 @@ package com.budwk.app.zhgh.activity.sports.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil; import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.DateUtil; import com.budwk.app.base.utils.DateUtil;
import com.budwk.app.sys.models.Sys_home_activity; import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.models.Sys_home_template; import com.budwk.app.sys.models.Sys_home_template;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; 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.ActivityBasicUnit;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion; import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
import com.budwk.app.zhgh.activity.culture.models.ActivityTissue; 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.dao.sql.Sql;
import org.nutz.ioc.aop.Aop; import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
import org.nutz.lang.Strings; import org.nutz.lang.Strings;
@@ -55,6 +58,7 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public String doAdd(ActivitySchool activitySchool, ActivitySchoolEvent[] events) { public String doAdd(ActivitySchool activitySchool, ActivitySchoolEvent[] events) {
events = validateAndApplySingleEventLimits(events);
ActivityBasicUnit activityBasicUnit = dao().fetch(ActivityBasicUnit.class, Cnd.where("id", "=", SecurityUtil.getUserId())); ActivityBasicUnit activityBasicUnit = dao().fetch(ActivityBasicUnit.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
if (activitySchool.getActivityLevel() == 40002) { if (activitySchool.getActivityLevel() == 40002) {
@@ -101,6 +105,7 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void doEdit(ActivitySchool activitySchool, ActivitySchoolEvent[] events) { public void doEdit(ActivitySchool activitySchool, ActivitySchoolEvent[] events) {
events = validateAndApplySingleEventLimits(events);
ActivitySchool school = dao().fetch(ActivitySchool.class, activitySchool.getId()); ActivitySchool school = dao().fetch(ActivitySchool.class, activitySchool.getId());
if (school.getActivityLevel() == 40002) { if (school.getActivityLevel() == 40002) {
school.setBelongUnionId(SecurityUtil.getUnionId()); school.setBelongUnionId(SecurityUtil.getUnionId());
@@ -161,6 +166,56 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
dao().insertOrUpdate(sysHomeActivity); 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) @Aop(TransAop.READ_COMMITTED)
private void addTeam(String schoolId, ActivitySchoolEvent[] events) { private void addTeam(String schoolId, ActivitySchoolEvent[] events) {
if (events.length > 0) { if (events.length > 0) {
@@ -271,33 +326,19 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
WHERE WHERE
app.activityId = @id app.activityId = @id
and app.status = 2 and app.status = 2
$cnd and (@filterUnion = 0 or app.activityUnionId = @unionId)
ORDER BY ORDER BY
ev.allName DESC, ev.allName DESC,
u.unionname DESC, u.unionname DESC,
app.`team` asc app.`team` asc
""").setParam("id", id) """).setParam("id", id);
.setVar("cnd", ""); // 参数必须声明在原始SQL中;setVar追加的字符串不会再次解析其中的@参数。
if (Strings.isNotBlank(unionId)) { // 未选择分工会时导出整个活动,选择时按报名记录中的活动分工会过滤。
sql.setVar("cnd", "and u.unionid = @unionId"); sql.setParam("filterUnion", Strings.isNotBlank(unionId) ? 1 : 0);
sql.setParam("unionId", unionId); sql.setParam("unionId", Strings.isNotBlank(unionId) ? unionId : "");
}
List<Record> list = list(sql); 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<ActivitySchoolApply> excels = new ArrayList<>();
list.forEach(record -> excels.add(toActivitySchoolApplyExport(record)));
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);
});
ExportParams exportParams = new ExportParams(); ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF); exportParams.setType(ExcelType.XSSF);
@@ -305,10 +346,42 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
CommonDownloadUtil.download(getUnionApplyExportFileName(unionId), workbook, response); CommonDownloadUtil.download(getUnionApplyExportFileName(unionId), workbook, response);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); 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) { private String getUnionApplyExportFileName(String unionId) {
String unionName = "分工会"; String unionName = "分工会";
if (Strings.isNotBlank(unionId)) { 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.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.service.FlowCommonService; import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; 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.SysClub;
import com.budwk.app.zhgh.club.model.SysClubExamineRegister; import com.budwk.app.zhgh.club.model.SysClubExamineRegister;
import com.budwk.app.zhgh.club.service.SysClubExamineService; import com.budwk.app.zhgh.club.service.SysClubExamineService;
@@ -119,10 +118,7 @@ public class ClubExamineApplyController {
if(StrUtil.isBlank(clubId)) { if(StrUtil.isBlank(clubId)) {
return Result.error("社团信息为空,请核查"); return Result.error("社团信息为空,请核查");
} }
int count = dao.count(ClubUser.class, Cnd.where("clubId", "=", clubId)); return Result.success(sysClubExamineService.getClubMemberMoney(clubId));
SysClub club = dao.fetch(SysClub.class, clubId);
int money = count * (club.getDue() != null ? Integer.parseInt(club.getDue()) : 0);
return Result.success(money);
} }
@At @At
@@ -175,6 +171,10 @@ public class ClubExamineApplyController {
public Result save(@Param("data") SysClubExamineRegister examineRegister, public Result save(@Param("data") SysClubExamineRegister examineRegister,
@Param("incomeDetailed") String incomeDetailed, @Param("incomeDetailed") String incomeDetailed,
@Param("incomeCensus") String incomeCensus) { @Param("incomeCensus") String incomeCensus) {
String validateMessage = sysClubExamineService.validateActivityRequired(examineRegister);
if (validateMessage != null) {
return Result.error(validateMessage);
}
if (StrUtil.isBlank(examineRegister.getId())) { if (StrUtil.isBlank(examineRegister.getId())) {
sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus); sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus);
} else { } else {
@@ -190,6 +190,10 @@ public class ClubExamineApplyController {
public Result submit(@Param("data") SysClubExamineRegister examineRegister, public Result submit(@Param("data") SysClubExamineRegister examineRegister,
@Param("incomeDetailed") String incomeDetailed, @Param("incomeDetailed") String incomeDetailed,
@Param("incomeCensus") String incomeCensus) { @Param("incomeCensus") String incomeCensus) {
String validateMessage = sysClubExamineService.validateActivityRequired(examineRegister);
if (validateMessage != null) {
return Result.error(validateMessage);
}
SysClubExamineRegister reg; SysClubExamineRegister reg;
if (StrUtil.isBlank(examineRegister.getId())) { if (StrUtil.isBlank(examineRegister.getId())) {
reg = sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus); reg = sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus);
@@ -221,6 +225,10 @@ public class ClubExamineApplyController {
@Param("incomeDetailed") String incomeDetailed, @Param("incomeDetailed") String incomeDetailed,
@Param("incomeCensus") String incomeCensus, @Param("incomeCensus") String incomeCensus,
@Param("taskId") Long taskId) { @Param("taskId") Long taskId) {
String validateMessage = sysClubExamineService.validateActivityRequired(examineRegister);
if (validateMessage != null) {
return Result.error(validateMessage);
}
if (StrUtil.isBlank(examineRegister.getId())) { if (StrUtil.isBlank(examineRegister.getId())) {
sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus); sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus);
} else { } else {
@@ -86,7 +86,7 @@ public class ClubExamineMineController {
//nutMap.setv("schoolName", Globals.MyConfig.getString("AppName")); //nutMap.setv("schoolName", Globals.MyConfig.getString("AppName"));
nutMap.setv("clubName", register.getClubName()); nutMap.setv("clubName", register.getClubName());
nutMap.setv("create_time", register.getFoundTime()); 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("year", register.getYear());
nutMap.setv("registerDate", register.getRegisterDate()); nutMap.setv("registerDate", register.getRegisterDate());
@@ -91,8 +91,8 @@ public class SysClub extends BaseModel {
@Column @Column
@Comment("会费标准") @Comment("会费标准")
@ColDefine(type = ColType.VARCHAR, width = 50) @ColDefine(type = ColType.DOUBLE, width = 10, precision = 2)
private String due; private Double due;
@Column @Column
@Comment("附件") @Comment("附件")
@@ -57,8 +57,8 @@ public class SysClubExamineRegister extends BaseModel {
@Column @Column
@Comment("会费标准") @Comment("会费标准")
@ColDefine(type = ColType.VARCHAR, width = 32) @ColDefine(type = ColType.DOUBLE, width = 10, precision = 2)
private String due; private Double due;
@Column @Column
@Comment("年度活动情况") @Comment("年度活动情况")
@@ -24,6 +24,21 @@ public interface SysClubExamineService extends BaseService<SysClubExamineRegiste
List<NutMap> getJgUser(@Valid String clubId); 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 doAdd(SysClubExamineRegister examineRegister, String incomeDetailed, String incomeCensus);
SysClubExamineRegister doEdit(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.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; 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.SysClub;
import com.budwk.app.zhgh.club.model.SysClubExamineRegister; import com.budwk.app.zhgh.club.model.SysClubExamineRegister;
import com.budwk.app.zhgh.club.model.SysClubExamineRegisterDetailed; import com.budwk.app.zhgh.club.model.SysClubExamineRegisterDetailed;
@@ -50,6 +51,15 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineReg
super(dao); 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 @Override
public List<NutMap> getClubUserNum(String clubId, Integer year) { public List<NutMap> getClubUserNum(String clubId, Integer year) {
Date yearStart = DateUtil.parse(year + "-01-01 00:00:00"); Date yearStart = DateUtil.parse(year + "-01-01 00:00:00");
@@ -345,6 +355,40 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineReg
return listMap; 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 @Override
public SysClubExamineRegister doAdd(SysClubExamineRegister examineRegister, String incomeDetailed, String incomeCensus) { public SysClubExamineRegister doAdd(SysClubExamineRegister examineRegister, String incomeDetailed, String incomeCensus) {
// JSON转换为实体列表(空值判断避免转换异常) // JSON转换为实体列表(空值判断避免转换异常)
@@ -193,8 +193,7 @@ public class GlobalMessageSendService {
try { try {
strategy.send(request.getTitle(), request.getContent(), request.getType(), strategy.send(request.getTitle(), request.getContent(), request.getType(),
request.getReceiverIds(), request.getConfig()); request.getReceiverIds(), request.getConfig());
// 策略可能因开关关闭而跳过,实际发送结果由策略日志记录。 log.info("异步发送消息成功,渠道:{}", strategy.getChannel().getValue());
log.info("异步消息策略执行完成,渠道:{}", strategy.getChannel().getValue());
} catch (Exception e) { } catch (Exception e) {
log.error("异步发送消息失败,渠道:{},错误:{}", strategy.getChannel().getValue(), e.getMessage(), e); log.error("异步发送消息失败,渠道:{},错误:{}", strategy.getChannel().getValue(), e.getMessage(), e);
} }
@@ -1,23 +1,16 @@
package com.budwk.app.zhgh.dayofficework.message.strategy.impl; package com.budwk.app.zhgh.dayofficework.message.strategy.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import com.budwk.app.base.sms.SmsService; import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.wxwork.WxWorkMessageService;
import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel; import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy; import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
/** /**
@@ -31,8 +24,6 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
private Dao dao; private Dao dao;
@Inject @Inject
private SmsService smsService; private SmsService smsService;
@Inject
private WxWorkMessageService wxWorkMessageService;
@Override @Override
public GlobalMessageChannel getChannel() { public GlobalMessageChannel getChannel() {
@@ -45,13 +36,7 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config) { public void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config) {
// WF 的接收账号已在任务事务内取得,异步线程不再反查尚未提交的任务数据。
if (config != null && config.getLong("wfTaskId") != null) {
sendFlowCard(title, content, config);
return;
}
try { try {
log.info("开始发送微信消息,标题:{},接收人数量:{}", title, receiverIds.size()); log.info("开始发送微信消息,标题:{},接收人数量:{}", title, receiverIds.size());
@@ -67,12 +52,7 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void sendByLoginName(String title, String content, Integer type, List<String> receiverLoginNames, JSONObject config) { public void sendByLoginName(String title, String content, Integer type, List<String> receiverLoginNames, JSONObject config) {
if (config != null && config.getLong("wfTaskId") != null) {
sendFlowCard(title, content, config);
return;
}
try { try {
log.info("开始发送微信消息(按登录名),标题:{},接收人数量:{}", title, receiverLoginNames.size()); log.info("开始发送微信消息(按登录名),标题:{},接收人数量:{}", title, receiverLoginNames.size());
@@ -87,58 +67,6 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
} }
} }
/**
* WF 专用分支:wfTaskId 为任务ID,wfLoginNames 为实际办理人的 loginname 列表,
* wxWorkLink 为非空手机入口。无返回值;关闭开关或无账号时跳过,失败抛给全局异步日志处理,
* 不回退旧微信通道,避免双发。非流程消息继续沿用原有通讯平台。
*/
private void sendFlowCard(String title, String content, JSONObject config) {
Long taskId = config.getLong("wfTaskId");
List<String> loginNames = config.getJSONArray("wfLoginNames") == null ? List.of()
: config.getJSONArray("wfLoginNames").toList(String.class);
if (loginNames.isEmpty()) {
log.warn("WF 企业微信卡片跳过:办理人 loginname 为空,taskId={}", taskId);
return;
}
String link = StrUtil.blankToDefault(config.getStr("wxWorkLink"), "/platform/home");
NutMap result;
try {
result = wxWorkMessageService.massSend(loginNames, cardText(title, 128, false),
cardText(content, 512, true), link);
} catch (RuntimeException e) {
// 异步线程已脱离任务调用栈,保留任务 ID 便于从错误日志定位具体待办。
throw new IllegalStateException("WF 企业微信卡片接口异常,taskId=" + taskId, e);
}
if (result != null && result.getBoolean("skipped", false)) {
log.info("WF 企业微信卡片跳过:消息开关关闭,taskId={}", taskId);
} else if (result != null && result.getBoolean("success", false)) {
log.info("WF 企业微信卡片发送成功,taskId={},接收账号数={}", taskId, loginNames.size());
} else {
log.error("WF 企业微信卡片发送失败或部分失败,taskId={},result={}", taskId, result);
throw new IllegalStateException("WF 企业微信卡片发送失败或部分失败,taskId=" + taskId);
}
}
/** 卡片有 UTF-8 字节上限;按 Unicode 码点缩短摘要,并计入正文 HTML 转义后的长度。 */
private String cardText(String value, int limit, boolean description) {
String text = HtmlUtil.unescape(HtmlUtil.cleanHtmlTag(StrUtil.blankToDefault(value, "流程待办")))
.replace('\r', ' ').replace('\n', ' ');
if (text.isBlank()) text = "流程待办";
StringBuilder result = new StringBuilder();
int bytes = 0;
for (int offset = 0; offset < text.length();) {
int codePoint = text.codePointAt(offset);
String character = new String(Character.toChars(codePoint));
int size = (description ? HtmlUtil.escape(character) : character).getBytes(StandardCharsets.UTF_8).length;
// 留出省略号的三个 UTF-8 字节,避免长流程名称使整条通知发送失败。
if (bytes + size > limit - 3) return result.append("…").toString();
result.append(character);
bytes += size;
offset += Character.charCount(codePoint);
}
return result.toString();
}
/** /**
* 发送微信文本消息。 * 发送微信文本消息。
* 接收人来自全局消息服务传入的用户ID或登录名,方法内部转换为 Sys_user 后取工号发送; * 接收人来自全局消息服务传入的用户ID或登录名,方法内部转换为 Sys_user 后取工号发送;
@@ -28,10 +28,10 @@ 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.Condolence;
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType; import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService; import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.PictureRenderData; import com.deepoove.poi.data.PictureRenderData;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
@@ -47,8 +47,6 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
@@ -59,6 +57,7 @@ import java.util.List;
* @Version 1.0 * @Version 1.0
* @Description TODO * @Description TODO
*/ */
@Slf4j
@IocBean @IocBean
@At("/platform/condolence/mine") @At("/platform/condolence/mine")
@Api("职工慰问我的") @Api("职工慰问我的")
@@ -157,6 +156,7 @@ public class CondolenceMineController {
@ApiOperation("导出职工慰问申请表") @ApiOperation("导出职工慰问申请表")
@SaCheckPermission("condolence.mine") @SaCheckPermission("condolence.mine")
public void doExportApply(String id, HttpServletResponse response, Boolean print) throws Exception { 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");
Condolence condolence = condolenceService.fetch(id); Condolence condolence = condolenceService.fetch(id);
if (condolence == null) throw new BaseException("该条记录不存在"); if (condolence == null) throw new BaseException("该条记录不存在");
if (!SecurityUtil.getUserId().equals(condolence.getCreatedBy())) { if (!SecurityUtil.getUserId().equals(condolence.getCreatedBy())) {
@@ -167,36 +167,32 @@ public class CondolenceMineController {
if (instance == null || !ProcessInstanceStateEnum.FINISHED.getCode().equals(instance.getState())) { if (instance == null || !ProcessInstanceStateEnum.FINISHED.getCode().equals(instance.getState())) {
throw new BaseException("只能导出已完成的申请"); throw new BaseException("只能导出已完成的申请");
} }
log.info("[职工慰问导出] 流程已完成,applyId={}, instanceId={}, state={}, applicantSignPresent={}", id, instance.getId(), instance.getState(), StrUtil.isNotBlank(condolence.getSignature()));
NutMap docData = Lang.obj2nutmap(condolence); NutMap docData = Lang.obj2nutmap(condolence);
docData.put("payUserName", StrUtil.nullToEmpty(condolence.getBankUserName())); docData.put("payUserName", StrUtil.nullToEmpty(condolence.getBankUserName()));
CondolenceType type = condolenceService.dao().fetch(CondolenceType.class, condolence.getType()); CondolenceType type = condolenceService.dao().fetch(CondolenceType.class, condolence.getType());
docData.put("typeName", type == null ? "" : type.getName()); docData.put("typeName", type == null ? "" : type.getName());
docData.put("signature", ""); docData.put("signature", null);
if (StrUtil.isNotBlank(condolence.getSignature())) { if (StrUtil.isNotBlank(condolence.getSignature())) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, condolence.getSignature()); PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, condolence.getSignature());
if (signature == null) throw new BaseException("申请人签字图片读取失败"); if (signature == null) throw new BaseException("申请人签字图片读取失败");
docData.put("signature", signature); docData.put("signature", signature);
} }
fillApplyApprovals(docData, instance); fillApplyApprovals(docData, instance);
for (String field : List.of("signature", "firstApprovalSign", "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(condolence.getHelpUserName()); String fileName = "重庆工贸职业技术学院职工慰问申请表_" + StrUtil.nullToEmpty(condolence.getHelpUserName());
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc"); try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc")) {
ByteArrayOutputStream templateOutput = new ByteArrayOutputStream();
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
if (input == null) throw new BaseException("职工慰问申请表模板不存在"); if (input == null) throw new BaseException("职工慰问申请表模板不存在");
com.aspose.words.Document document = new com.aspose.words.Document(input); boolean pdf = Boolean.TRUE.equals(print);
document.save(templateOutput, com.aspose.words.SaveFormat.DOCX); byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, null, pdf, "condolence:" + id);
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateOutput.toByteArray())) { log.info("[职工慰问导出] 渲染完成,applyId={}, format={}, outputBytes={}", id, pdf ? "PDF" : "Word", bytes.length);
XWPFTemplate.compile(templateInput).render(docData).writeAndClose(output); CommonDownloadUtil.download(fileName + (pdf ? ".pdf" : ".docx"), bytes, response);
} } catch (Exception e) {
if (Boolean.TRUE.equals(print)) { log.error("[职工慰问导出] 模板渲染或文件输出失败,applyId={}, instanceId={}, format={}", id, instance.getId(), Boolean.TRUE.equals(print) ? "PDF" : "Word", e);
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(output.toByteArray()); throw e;
ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream()) {
new com.aspose.words.Document(wordInput).save(pdfOutput, com.aspose.words.SaveFormat.PDF);
CommonDownloadUtil.download(fileName + ".pdf", pdfOutput.toByteArray(), response);
}
} else {
CommonDownloadUtil.download(fileName + ".docx", output.toByteArray(), response);
}
} }
} }
@@ -206,29 +202,39 @@ public class CondolenceMineController {
String[] fields = {"firstApproval", "secondApproval", "thirdApproval", "fourthApproval"}; String[] fields = {"firstApproval", "secondApproval", "thirdApproval", "fourthApproval"};
List<String> suffixes = List.of("Opinion", "Sign", "Year", "Month", "Day"); List<String> suffixes = List.of("Opinion", "Sign", "Year", "Month", "Day");
for (String field : fields) { for (String field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, ""); for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
} }
List<ProcessTask> tasks = condolenceService.dao().query(ProcessTask.class, List<ProcessTask> tasks = condolenceService.dao().query(ProcessTask.class,
Cnd.where("processInstanceId", "=", instance.getId()).asc("id")); 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) { for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId()); ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
if ("startTask".equals(task.getTaskName()) || (taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) { 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 field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, ""); for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
} }
continue; continue;
} }
for (int i = 0; i < taskKeys.length; i++) { for (int i = 0; i < taskKeys.length; i++) {
if (!taskKeys[i].equals(task.getTaskName())) continue; if (!taskKeys[i].equals(task.getTaskName())) continue;
for (String suffix : suffixes) docData.put(fields[i] + suffix, ""); for (String suffix : suffixes) docData.put(fields[i] + suffix, "Sign".equals(suffix) ? null : "");
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) continue; 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(); 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"))); docData.put(fields[i] + "Opinion", StrUtil.nullToEmpty(data.getStr("opinion")));
if (StrUtil.isNotBlank(data.getStr("sign"))) { if (StrUtil.isNotBlank(data.getStr("sign"))) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign")); PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign"));
if (signature == null) throw new BaseException("审批签字图片读取失败"); if (signature == null) throw new BaseException("审批签字图片读取失败");
docData.put(fields[i] + "Sign", signature); 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] + "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] + "Month", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "M"));
@@ -9,7 +9,6 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel; 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.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil; import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.MoneyUtil; 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.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine; import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance; 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.DifficultHelpPageParam;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo; import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo;
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm; 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.config.Configure;
import com.deepoove.poi.data.PictureRenderData; import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy; import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
@@ -74,8 +71,6 @@ import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.*; import java.io.*;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@@ -319,9 +314,9 @@ public class DifficultHelpReadingController {
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder().bind("familyList", policy).build(); Configure config = Configure.builder().bind("familyList", policy).build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("difficulty_apply"), config) try (InputStream input = sysOfficeTemplateUtil.getTemplate("difficulty_apply")) {
.render(docData) zipOutputStream.write(sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:batch:" + record.getString("id")));
.write(zipOutputStream); }
zipOutputStream.closeEntry(); zipOutputStream.closeEntry();
} }
zipOutputStream.flush(); zipOutputStream.flush();
@@ -379,17 +374,18 @@ public class DifficultHelpReadingController {
String[] taskKeys = {"36ebafda-44c2-4bd3-bd7f-10839a575a0d", "715ece82-4ae3-433c-ad18-ed875f45d1e3", "e405f96c-5fc0-4f63-b323-e69dfe853253"}; String[] taskKeys = {"36ebafda-44c2-4bd3-bd7f-10839a575a0d", "715ece82-4ae3-433c-ad18-ed875f45d1e3", "e405f96c-5fc0-4f63-b323-e69dfe853253"};
String[] fields = {"firstApproval", "secondApproval", "thirdApproval"}; String[] fields = {"firstApproval", "secondApproval", "thirdApproval"};
for (String field : fields) { 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")); ProcessInstance instance = dao.fetch(ProcessInstance.class, Cnd.where("businessNo", "=", businessId).desc("id"));
if (instance == null) return; if (instance == null) return;
List<ProcessTask> tasks = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", instance.getId()).asc("id")); List<ProcessTask> tasks = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
for (ProcessTask task : tasks) { for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId()); 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 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; continue;
} }
@@ -453,22 +449,9 @@ public class DifficultHelpReadingController {
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" + String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf"; DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { try (InputStream input = getApplyTemplate(templateName)) {
XWPFTemplate.compile(getApplyTemplate(templateName),config) byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, true, "difficulthelp:" + difficultHelp.getId());
.render(docData) CommonDownloadUtil.download(fileName, bytes, response);
.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());
} }
} }
@@ -478,13 +461,9 @@ public class DifficultHelpReadingController {
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" + String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx"; DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { try (InputStream input = getApplyTemplate(templateName)) {
XWPFTemplate.compile(getApplyTemplate(templateName),config) byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:" + difficultHelp.getId());
.render(docData) CommonDownloadUtil.download(fileName, bytes, response);
.writeAndClose(byteArrayOutputStream);
CommonDownloadUtil.download(fileName, byteArrayOutputStream.toByteArray(), response);
} catch (IOException e) {
log.error("困难慰问申请表导出Word失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
} }
} }
@@ -562,15 +541,13 @@ public class DifficultHelpReadingController {
throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage()); throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage());
} }
} else if ("docx".equals(flag)) { } else if ("docx".equals(flag)) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { try (InputStream input = sysOfficeTemplateUtil.getTemplate(resource)) {
Configure config = Configure.builder() Configure config = Configure.builder()
.bind("da", new LoopRowTableRenderPolicy()) .bind("da", new LoopRowTableRenderPolicy())
.build(); .build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(resource), config) byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, result, config, false, "difficulthelp:summary:" + resource);
.render(result) CommonDownloadUtil.download(fileName + ".docx", bytes, response);
.writeAndClose(out);
CommonDownloadUtil.download(fileName + ".docx", out.toByteArray(), response);
} }
} }
} }
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.staffbenefit.difficulthelp.service; package com.budwk.app.zhgh.staffbenefit.difficulthelp.service;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo; import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam; import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm; import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
@@ -21,6 +22,14 @@ public interface DifficultHelpCommonService extends BaseService<DifficultHelpInf
void validateApplyUser(DifficultHelpInfo difficultHelpInfo); void validateApplyUser(DifficultHelpInfo difficultHelpInfo);
/**
* 困难帮扶进入校工会办公室主任审批时,通知本次任务的实际参与人。
* @param task WF 已创建且参与人已落库的任务;仅 KNBF 办公室审批待办触发,
* 接收人使用任务参与人的 actorAccount(系统 loginname)
* @return 无返回值;开关关闭或非目标节点不发送,发送失败记录日志而不阻断流程
*/
void notifyOfficeApproval(ProcessTask task);
/** /**
* 导入实际金额数据处理 * 导入实际金额数据处理
* @param file 文件 * @param file 文件
@@ -6,6 +6,12 @@ import cn.hutool.json.JSONUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.wxwork.WxWorkMessageService;
import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.base.utils.ManyAddOrRenewUtil; import com.budwk.app.base.utils.ManyAddOrRenewUtil;
@@ -31,6 +37,8 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
import org.nutz.lang.random.R; import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.upload.TempFile; import org.nutz.mvc.upload.TempFile;
import java.util.*; import java.util.*;
@@ -46,6 +54,8 @@ import java.util.stream.IntStream;
*/ */
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHelpInfo> implements DifficultHelpCommonService { public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHelpInfo> implements DifficultHelpCommonService {
private static final Log log = Logs.get();
private static final String OFFICE_APPROVAL_TASK = "36ebafda-44c2-4bd3-bd7f-10839a575a0d";
public DifficultHelpCommonServiceImpl(Dao dao) { public DifficultHelpCommonServiceImpl(Dao dao) {
super(dao); super(dao);
@@ -53,6 +63,55 @@ public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHel
@Inject @Inject
private ManyAddOrRenewUtil manyAddOrRenewUtil; private ManyAddOrRenewUtil manyAddOrRenewUtil;
@Inject
private WxWorkMessageService wxWorkMessageService;
/**
* 只通知 KNBF 办公室待办的实际审核人;重新提交生成的新任务沿用同一入口。
* task 为任务开始事件对应的 WF 任务,actorAccount 为学校企业微信 userid。
* 无返回值;服务返回 skipped 表示开关关闭,success=false 表示接口失败或部分接收人无效。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void notifyOfficeApproval(ProcessTask task) {
if (task == null || task.getId() == null || task.getProcessInstanceId() == null
|| !OFFICE_APPROVAL_TASK.equals(task.getTaskName())
|| !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) {
return;
}
try {
ProcessInstance instance = dao().fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null || instance.getProcessDefineId() == null) return;
ProcessDefine define = dao().fetch(ProcessDefine.class, instance.getProcessDefineId());
// 使用实例所属定义核验流程 KEY,避免其他流程复用同一个节点 ID 时误发。
if (define == null || !"KNBF".equals(define.getName())) return;
DifficultHelpInfo info = StrUtil.isBlank(instance.getBusinessNo()) ? null : fetch(instance.getBusinessNo());
if (info == null) {
log.warnf("困难帮扶企业微信通知跳过:申请不存在,taskId=%s", task.getId());
return;
}
List<ProcessTaskActor> actors = dao().query(ProcessTaskActor.class,
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
List<String> loginNames = actors.stream().map(ProcessTaskActor::getActorAccount)
.filter(StrUtil::isNotBlank).map(String::trim).distinct().toList();
// 不按角色扩大接收范围,未绑定工号时不以内部用户 ID 代替企业微信 userid。
if (loginNames.isEmpty()) {
log.warnf("困难帮扶企业微信通知跳过:审核人 loginname 为空,taskId=%s", task.getId());
return;
}
String content = "您有一条困难帮扶申请待审核,受助人:"
+ StrUtil.blankToDefault(info.getUserName(), "未填写") + ",请进入校工会办公室审批页面办理。";
// 流程定义中仍保留旧的 unionLeaderApproval 链接,本节点实际办理入口为 branchUnionApproval。
NutMap result = wxWorkMessageService.massSend(loginNames, "困难帮扶待办提醒", content,
"/platform/difficultHelp/branchUnionApproval/h5");
if (!result.getBoolean("skipped", false) && !result.getBoolean("success", false)) {
log.warnf("困难帮扶企业微信通知发送失败或部分失败,taskId=%s,result=%s", task.getId(), result);
}
} catch (Exception e) {
// 外部消息不改变审批结果;日志保留任务 ID,便于定位接口、账号或配置问题。
log.error("困难帮扶企业微信通知异常,taskId=" + task.getId(), e);
}
}
@Override @Override
public void validateApplyUser(DifficultHelpInfo difficultHelpInfo) { public void validateApplyUser(DifficultHelpInfo difficultHelpInfo) {
@@ -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.
@@ -1,207 +0,0 @@
/* 首页服务门户布局单独限定作用域,避免影响应用中心和其他业务页。 */
#v4-home-app.home-portal {
width: 100%;
max-width: none;
margin: 0;
color: #20344d;
background: #f2f6fb;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
font-size: 14px;
}
#v4-home-app.home-portal * { box-sizing: border-box; }
#v4-home-app .portal-top { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 18px; max-width: 1720px; margin: auto; padding: 18px 24px 0; }
/* 使用完整不透明背景铺满横幅,会员信息独立成卡,不再覆盖校园景观。 */
#v4-home-app .portal-hero { position: relative; min-width: 0; min-height: 260px; overflow: hidden; border-radius: 12px; background: #e8f4ff; }
#v4-home-app .portal-hero::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, #e8f4ff 0%, #e8f4ff 28%, rgba(232,244,255,.92) 39%, rgba(232,244,255,.35) 54%, rgba(232,244,255,0) 68%); }
#v4-home-app .portal-hero-inner { position: relative; z-index: 1; display: flex; align-items: center; min-height: 260px; height: 100%; padding: 32px 40px; }
#v4-home-app .portal-hero-copy { max-width: 80%; }
#v4-home-app .portal-hero-eyebrow { margin-bottom: 12px; color: #527ca1; font-size: 14px; line-height: 22px; letter-spacing: 2px; }
#v4-home-app .portal-hero-copy h1 { margin: 0 0 10px; font-size: 36px; line-height: 1.3; font-weight: 700; color: #083765; }
#v4-home-app .portal-hero-copy p { margin: 0; color: #335675; font-size: 17px; line-height: 1.8; }
#v4-home-app .portal-values { display: flex; flex-wrap: wrap; gap: 22px 36px; margin-top: 24px; }
#v4-home-app .portal-values span { display: inline-flex; align-items: center; gap: 12px; }
#v4-home-app .portal-values i { color: #3599f2; font-size: 23px; }
/* 首次渲染时分层淡入文字,只改变透明度和位移,不影响卡片布局。 */
@keyframes portal-text-enter {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
#v4-home-app .portal-hero-eyebrow,
#v4-home-app .portal-hero-copy h1,
#v4-home-app .portal-hero-copy p,
#v4-home-app .portal-values > span,
#v4-home-app .portal-member-date,
#v4-home-app .portal-member-heading,
#v4-home-app .portal-member .meta-row { animation: portal-text-enter .6s cubic-bezier(.22, 1, .36, 1) backwards; }
#v4-home-app .portal-hero-copy h1 { animation-delay: .08s; }
#v4-home-app .portal-hero-copy p { animation-delay: .16s; }
#v4-home-app .portal-values > span:nth-child(1) { animation-delay: .24s; }
#v4-home-app .portal-values > span:nth-child(2) { animation-delay: .30s; }
#v4-home-app .portal-values > span:nth-child(3) { animation-delay: .36s; }
#v4-home-app .portal-values > span:nth-child(4) { animation-delay: .42s; }
#v4-home-app .portal-member-date { animation-delay: .10s; }
#v4-home-app .portal-member-heading { animation-delay: .18s; }
#v4-home-app .portal-member .meta-row:nth-child(1) { animation-delay: .26s; }
#v4-home-app .portal-member .meta-row:nth-child(2) { animation-delay: .34s; }
#v4-home-app .portal-member .meta-row:nth-child(3) { animation-delay: .42s; }
@media (prefers-reduced-motion: reduce) {
#v4-home-app .portal-hero-eyebrow,
#v4-home-app .portal-hero-copy h1,
#v4-home-app .portal-hero-copy p,
#v4-home-app .portal-values > span,
#v4-home-app .portal-member-date,
#v4-home-app .portal-member-heading,
#v4-home-app .portal-member .meta-row { animation: none; }
}
#v4-home-app .portal-content { max-width: 1720px; margin: auto; padding: 18px 24px 28px; }
/* 同行共用网格行高,统一横纵间距;待办使用紧凑两行信息,避免撑出大块留白。 */
#v4-home-app .portal-columns { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 18px; margin-top: 18px; align-items: stretch; }
#v4-home-app .portal-columns > * { min-width: 0; }
#v4-home-app .portal-news-wide { grid-column: 1 / -1; }
#v4-home-app .portal-panel,
#v4-home-app .entry-wrapper,
#v4-home-app .todo-center,
#v4-home-app .work-template-wrapper,
#v4-home-app .jcdt-container { background: #fff; border: 0; border-radius: 12px; padding: 20px; box-shadow: 0 3px 14px rgba(30, 66, 108, .035); }
#v4-home-app .home-section-header,
#v4-home-app .portal-todo-heading,
#v4-home-app .work-template-header,
#v4-home-app .portal-news-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; min-height: 28px; margin: 0 0 16px; }
#v4-home-app .home-section-title,
#v4-home-app .work-template-title,
#v4-home-app .portal-news-title,
#v4-home-app .portal-todo-heading h3 { display: inline-flex; align-items: center; gap: 9px; margin: 0; padding: 0; border: 0; font-size: 20px; line-height: 1.4; font-weight: 600; color: #123356; }
#v4-home-app .home-section-title i,
#v4-home-app .work-template-title i,
#v4-home-app .portal-news-title > i:first-child,
#v4-home-app .portal-todo-heading i { color: #1687f5; font-size: 19px; }
#v4-home-app .portal-more { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; border: 0; padding: 4px 0; background: transparent; color: #1687f5; font: inherit; font-size: 13px; cursor: pointer; text-decoration: none; }
#v4-home-app .portal-more:hover { color: #0565c6; }
#v4-home-app button:focus-visible,
#v4-home-app a:focus-visible,
#v4-home-app [tabindex]:focus-visible { outline: 2px solid #1687f5; outline-offset: 3px; }
#v4-home-app .entry-card { min-width: 0; }
#v4-home-app .entry-wrapper { padding-bottom: 0; }
#v4-home-app .portal-entry-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid #e9eff6; }
#v4-home-app .portal-entry-tabs { display: flex; gap: 34px; }
#v4-home-app .portal-entry-tabs button { border: 0; border-bottom: 3px solid transparent; padding: 0 0 13px; background: transparent; font: inherit; font-size: 19px; color: #465d78; font-weight: 600; cursor: pointer; }
#v4-home-app .portal-entry-tabs button.active { color: #0784fa; border-bottom-color: #0784fa; }
#v4-home-app .entry-section + .entry-section { margin-top: 0; }
#v4-home-app .app-grid { display: flex; justify-content: flex-start; align-items: center; gap: 12px; min-height: 0; overflow-x: auto; padding: 20px 0; border: 0; border-radius: 0; box-shadow: none; }
#v4-home-app .app-item { flex: 0 0 96px; border: 0; font: inherit; padding: 0 4px; max-width: none; }
#v4-home-app .app-item:hover { background: #f4f9ff; box-shadow: none; transform: translateY(-2px); }
#v4-home-app .app-icon { background: transparent; }
#v4-home-app .app-name { display: block; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; line-height: 20px; min-height: 0; margin-top: 2px; }
#v4-home-app .app-empty { padding: 10px 0; }
#v4-home-app .act-card { padding: 0; border: 0; border-radius: 0; box-shadow: none; }
#v4-home-app .activity-swiper-container { padding: 0 0 4px; }
#v4-home-app .activity-swiper .swiper-slide .item { display: flex; flex-direction: column; height: 100%; }
/* 加高封面并统一裁剪容器,图片、加载态和占位图都保持相同圆角。 */
#v4-home-app .activity-swiper .swiper-slide .item .img-box { flex-shrink: 0; height: auto; aspect-ratio: 1.5; border-radius: 10px; overflow: hidden; clip-path: inset(0 round 10px); background: #1687f5; isolation: isolate; }
#v4-home-app .img-box .el-image { display: block; width: 100%; height: 100%; position: absolute; inset: 0; border-radius: inherit; overflow: hidden; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box img { height: 100% !important; max-height: none !important; object-fit: cover !important; border-radius: 10px; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status { top: 8px; left: 8px; border-radius: 5px; padding: 4px 7px; background: #ed4859; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-ended { background: #8b97a7; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-upcoming { background: #1687f5; }
#v4-home-app .activity-swiper .swiper-slide .item h4 { flex: 1; margin: 10px 0 8px; height: auto; min-height: 22px; max-height: 44px; font-size: 15px; color: #20344d; line-height: 22px; }
#v4-home-app .activity-swiper .swiper-slide .item .time p { white-space: normal; font-size: 12px; color: #7b899d; line-height: 1.6; }
#v4-home-app .activity-swiper-container .swiper-button-next,
#v4-home-app .activity-swiper-container .swiper-button-prev { width: 44px; height: 44px; margin-top: 0; padding: 0; transform: translateY(-50%); border: 1px solid #dbeaff; background: rgba(255,255,255,.96); color: #1687f5; box-shadow: 0 3px 12px rgba(30, 66, 108, .18); }
#v4-home-app .activity-swiper-container .swiper-button-next { right: 8px; }
#v4-home-app .activity-swiper-container .swiper-button-prev { left: 8px; }
#v4-home-app .activity-swiper-container .swiper-button-next:hover,
#v4-home-app .activity-swiper-container .swiper-button-prev:hover { transform: translateY(-50%) scale(1.05); background: #1687f5; color: #fff; }
#v4-home-app .activity-swiper-container .swiper-button-next::after,
#v4-home-app .activity-swiper-container .swiper-button-prev::after { font-size: 18px; font-weight: 600; }
#v4-home-app .portal-cover-empty { height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 9px; color: #94a7bd; background: #f0f5fb; }
#v4-home-app .portal-cover-empty i { font-size: 28px; }
#v4-home-app .user-wrapper { background: transparent; }
#v4-home-app .user-container { display: block; width: 100%; height: 100%; margin: 0; }
#v4-home-app .todo-center { display: flex; flex-direction: column; min-width: 0; height: 100%; }
#v4-home-app .todo-center .header { display: block; margin: 0; }
#v4-home-app .todo-center .el-tabs { margin: 0 0 8px; }
#v4-home-app .todo-center .el-tabs__header { margin: 0; }
#v4-home-app .todo-center .el-tabs__nav { display: flex; width: 100%; border-color: #e9eff6; border-radius: 6px 6px 0 0; }
#v4-home-app .todo-center .el-tabs__item { flex: 1; min-width: 0; padding: 0 7px; font-size: 12px; height: 36px; line-height: 36px; text-align: center; }
#v4-home-app .todo-center .el-tabs__item.is-active { border-bottom: 2px solid #1687f5; color: #1687f5; }
#v4-home-app .portal-task-list { display: flex; flex-direction: column; justify-content: space-evenly; flex: 1; min-height: 180px; }
#v4-home-app .portal-task-row { display: flex; gap: 10px; align-items: center; padding: 8px 6px; border-bottom: 1px solid #eef3f8; }
#v4-home-app .portal-task-row:last-child { border-bottom: 0; }
#v4-home-app .portal-task-row:hover { background: #f7fbff; }
#v4-home-app .portal-task-icon { width: 34px; height: 40px; display: flex; flex: 0 0 34px; align-items: center; justify-content: center; border-radius: 8px; background: #eef7ff; color: #1687f5; font-size: 22px; }
#v4-home-app .portal-task-content { min-width: 0; flex: 1; }
#v4-home-app .portal-task-title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #263548; font-weight: 600; font-size: 14px; line-height: 22px; }
#v4-home-app .portal-task-meta { display: flex; align-items: baseline; gap: 10px; font-size: 12px; color: #8390a2; line-height: 21px; }
#v4-home-app .portal-task-meta span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#v4-home-app .portal-task-meta > span:first-child { max-width: 45%; flex-shrink: 0; }
#v4-home-app .portal-task-date { flex-shrink: 0; font-size: 12px; color: #7b899d; }
#v4-home-app .portal-task-node { flex: 1; min-width: 0; color: #8b98a9; font-size: 11px; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#v4-home-app .portal-task-row .el-button { flex-shrink: 0; padding: 8px 14px; }
#v4-home-app .portal-task-pagination { text-align: right; margin-top: 8px; }
#v4-home-app .portal-empty { min-height: 132px; display: flex; align-items: center; justify-content: center; gap: 10px; color: #8a9aac; }
#v4-home-app .portal-empty i { font-size: 26px; color: #83b9ee; }
#v4-home-app .work-template-wrapper { width: 100%; margin: 0; }
#v4-home-app .work-template-card { min-height: 98px; padding: 0; border: 0; border-radius: 0; box-shadow: none; }
#v4-home-app .work-template-grid { display: flex; align-items: flex-start; gap: 14px; overflow-x: auto; padding: 4px 0 10px; scroll-snap-type: x proximity; }
#v4-home-app .work-template-item { flex: 0 0 88px; width: 88px; padding: 4px; border: 0; font: inherit; scroll-snap-align: start; }
#v4-home-app .work-template-item:hover { background: #f4f9ff; box-shadow: none; }
#v4-home-app .work-template-icon,
#v4-home-app .work-template-icon .el-image { width: 48px; height: 48px; }
#v4-home-app .work-template-icon img { width: 48px !important; height: 48px !important; object-fit: contain; }
#v4-home-app .work-template-icon i { font-size: 30px; line-height: 48px; color: #83b9ee; }
#v4-home-app .work-template-name { font-size: 13px !important; line-height: 20px; min-height: 40px; max-height: 40px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
#v4-home-app .work-template-wrapper .no-activity-placeholder { padding: 12px; }
#v4-home-app .jcdt-container { width: 100%; max-width: none; height: 100%; margin: 0; }
#v4-home-app .portal-news-title { background: transparent; cursor: pointer; font-family: inherit; }
#v4-home-app .portal-news-title > i:last-child:not(:first-child) { color: #8092a6; font-size: 14px; }
#v4-home-app .portal-news-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 9px 0; border: 0; background: transparent; color: #42536a; font: inherit; text-align: left; cursor: pointer; }
#v4-home-app .portal-news-row time { flex-shrink: 0; padding: 3px 7px; background: #eff5fc; border-radius: 4px; color: #45698f; font-size: 13px; }
#v4-home-app .portal-news-row span { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#v4-home-app .portal-news-row:hover span { color: #1687f5; }
@media (min-width: 1600px) {
#v4-home-app .portal-hero-copy h1 { font-size: 42px; }
#v4-home-app .app-item { flex-basis: 104px; }
}
@media (max-width: 1199px) {
#v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr) 300px; padding: 18px 18px 0; }
#v4-home-app .portal-hero-inner { padding-left: 32px; }
#v4-home-app .portal-content { padding: 18px 18px 24px; }
#v4-home-app .portal-columns { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); }
#v4-home-app .portal-values { gap: 16px; }
#v4-home-app .portal-panel, #v4-home-app .todo-center { padding: 16px; }
#v4-home-app .portal-task-row { gap: 8px; padding: 8px 0; }
#v4-home-app .portal-task-icon { display: none; }
}
@media (max-width: 900px) {
#v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr); }
#v4-home-app .portal-columns { grid-template-columns: minmax(0, 1fr); }
#v4-home-app .portal-activities { order: 1; }
#v4-home-app .portal-todo { order: 2; }
#v4-home-app .portal-templates { order: 3; }
#v4-home-app .portal-news { order: 4; }
#v4-home-app .portal-hero-copy { max-width: 68%; }
#v4-home-app .portal-hero-copy h1 { font-size: 30px; }
#v4-home-app .portal-hero-copy p { font-size: 14px; }
#v4-home-app .portal-task-icon { display: flex; }
#v4-home-app .portal-task-meta { flex-wrap: nowrap; }
}
@media (max-width: 600px) {
#v4-home-app .portal-top { padding: 10px 10px 8px; }
#v4-home-app .portal-hero-inner { padding: 24px 20px; }
#v4-home-app .portal-hero-copy { max-width: 100%; }
#v4-home-app .portal-hero-copy h1 { font-size: 28px; }
#v4-home-app .portal-values { gap: 12px 18px; font-size: 12px; }
#v4-home-app .portal-values i { font-size: 17px; }
#v4-home-app .portal-content { padding: 10px; }
#v4-home-app .entry-wrapper { padding: 16px 12px 0; }
#v4-home-app .portal-entry-tabs { gap: 20px; }
#v4-home-app .portal-entry-tabs button { font-size: 16px; }
#v4-home-app .app-item { flex-basis: 84px; }
#v4-home-app .home-section-title, #v4-home-app .portal-todo-heading h3, #v4-home-app .work-template-title, #v4-home-app .portal-news-title { font-size: 18px; }
#v4-home-app .portal-task-row .el-button { padding: 8px 10px; }
}
/* 使用参考项目本地横幅图片,文字遮罩保持首页的可读性。 */
#v4-home-app .portal-hero-background { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: right bottom; }
/* 已展示全部活动时明确禁用入口,避免出现可点击但没有内容切换的按钮。 */
#v4-home-app .portal-more:disabled { color: #8a9aac; cursor: default; }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

File diff suppressed because one or more lines are too long
@@ -380,7 +380,7 @@
</el-form> </el-form>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="setDialogVisible = false">取消</el-button> <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> </span>
</el-dialog> </el-dialog>
@@ -646,7 +646,7 @@ module.exports = {
doSearch(showFullLoading = false) { doSearch(showFullLoading = false) {
this.tableKey = new Date().getTime() this.tableKey = new Date().getTime()
this.pageForm.pageNumber = 1 this.pageForm.pageNumber = 1
this.pageData(showFullLoading === true) return this.pageData(showFullLoading === true)
}, },
async pageData(showFullLoading = false) { async pageData(showFullLoading = false) {
this.tableLoading = true this.tableLoading = true
@@ -679,43 +679,74 @@ module.exports = {
} }
} }
}, },
async doSetActivityUser() { doSetActivityUser() {
const valid = await this.$refs.setForm.validate() // 方法级状态保护与整页遮罩共同阻止慢请求期间的重复确认。
if (!valid) { if (this.settingLoading) {
return return
} }
if (this.formData.groupType !== 2 && this.tableData.length === 0) { this.$refs.setForm.validate((valid) => {
this.$message.warning("请先指定筛选条件!") if (!valid) {
return 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.settingLoading = false
this.$emit("update:group_id", resp.data) if (this.formData.groupType !== 2 && this.tableData.length === 0) {
this.$emit("group_change") 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() { async getActivityGroup() {
const resp = await $.get("/platform/activity/basic/scope/getActivityUserScopeGroup") const resp = await $.get("/platform/activity/basic/scope/getActivityUserScopeGroup")
@@ -52,8 +52,7 @@ module.exports = {
*/ */
init() { init() {
this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id
// 打印当前二维码的签字入口,便于从控制台复制地址进行签字测试。 console.log(this.signatureAddress)
console.info("signatureAddress:", this.signatureAddress)
return this.$axios.post("/platform/signature/get") return this.$axios.post("/platform/signature/get")
.then((res) => { .then((res) => {
if (this._isBeingDestroyed || this._isDestroyed) { if (this._isBeingDestroyed || this._isDestroyed) {
@@ -9,6 +9,7 @@
progress progress
:accept="accept" :accept="accept"
:max-count="upload_number" :max-count="upload_number"
:upload-text="upload_text"
title="" title=""
description="" description=""
tip="" tip=""
@@ -50,6 +51,11 @@ module.exports = {
default: 1, default: 1,
required: false required: false
}, },
upload_text: {
type: String,
default: "",
required: false
},
// 上传返回id或url // 上传返回id或url
upload_result_type: { upload_result_type: {
type: String, type: String,
@@ -101,6 +107,7 @@ module.exports = {
this.fileList = val.map((v) => { this.fileList = val.map((v) => {
return { return {
...v, ...v,
file: v.file || { name: v.name },
status: null, status: null,
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()) isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase())
} }
@@ -118,6 +125,7 @@ module.exports = {
this.fileList = val.map((v) => { this.fileList = val.map((v) => {
return { return {
...v, ...v,
file: v.file || { name: v.name },
url: v.url ? v.url : v.response?.data, url: v.url ? v.url : v.response?.data,
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()), isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()),
status: null status: null
@@ -171,8 +179,8 @@ module.exports = {
f.url = resp.data f.url = resp.data
f.response = resp f.response = resp
f.percentage = 100 f.percentage = 100
f.isImage = true f.isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(f.name.split(".").pop().toLowerCase())
delete f.file f.file = { name: f.name }
delete f.content delete f.content
} else { } else {
f.status = "fail" f.status = "fail"
@@ -75,7 +75,7 @@
<!-- 引入 form-create 和 designer --> <!-- 引入 form-create 和 designer -->
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script> <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 --> <!-- swiper -->
<script src="${base!}/assets/platform/plugins/swiper/swiper-bundle.js"></script> <script src="${base!}/assets/platform/plugins/swiper/swiper-bundle.js"></script>
+21 -69
View File
@@ -10,23 +10,22 @@ const act = {
<div ref="activitySwiper" class="swiper activity-swiper"> <div ref="activitySwiper" class="swiper activity-swiper">
<div class="swiper-wrapper"> <div class="swiper-wrapper">
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id"> <div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
<div class="item" role="button" tabindex="0" @click="go(activity)" @keydown.enter="go(activity)" @keydown.space.prevent="go(activity)"> <div class="item" @click="go(activity)">
<div class="img-box"> <div class="img-box">
<el-image :src="activity.cover" fit="cover" :alt="activity.name"> <img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
<div slot="error" class="portal-cover-empty"><i class="fa fa-picture-o" aria-hidden="true"></i><span>暂无图片</span></div> alt="">
</el-image> <span class="status">
<span class="status" :class="{'status-ended': getStatusText(activity) === '已结束', 'status-upcoming': getStatusText(activity) === '未开始'}">
{{getStatusText(activity)}} {{getStatusText(activity)}}
</span> </span>
</div> </div>
<h4 class="title" :title="activity.name"> <h4 class="title">
{{ activity.name}} {{ activity.name}}
</h4> </h4>
<!--报名时间--> <!--报名时间-->
<div class="time"> <div class="time">
<p :title="[activity.startDate, activity.endDate].filter(Boolean).join(' 至 ')"> <p>
<i class="fa fa-clock-o"></i> <i class="fa fa-clock-o"></i>
{{ formatActivityDate(activity.startDate) }} 至 {{ formatActivityDate(activity.endDate) }} 时间:{{ activity.startDate }}至{{ activity.endDate }}
</p> </p>
</div> </div>
</div> </div>
@@ -35,21 +34,20 @@ const act = {
</div> </div>
<!-- 导航按钮 --> <!-- 导航按钮 -->
<button type="button" ref="nextButton" class="swiper-button-next" aria-label="下一组活动"></button> <div ref="nextButton" class="swiper-button-next"></div>
<button type="button" ref="prevButton" class="swiper-button-prev" aria-label="上一组活动"></button> <div ref="prevButton" class="swiper-button-prev"></div>
<!-- 进度条 --> <!-- 进度条 -->
<div ref="pagination" class="swiper-pagination"></div> <div ref="pagination" class="swiper-pagination"></div>
</div> </div>
<div v-else class="no-activity-placeholder"> <div v-else class="no-activity-placeholder">
<div class="icon"><i class="fa fa-calendar" aria-hidden="true"></i></div> <div class="icon">📅</div>
<p>暂无近期活动</p> <p>暂无近期活动</p>
<p class="subtext">敬请关注后续通知</p> <p class="subtext">敬请关注后续通知</p>
</div> </div>
</div> </div>
<slot></slot>
</div> </div>
`, `,
data() { data() {
@@ -59,57 +57,19 @@ const act = {
} }
}, },
methods: { methods: {
// 轮播初始化或尺寸变化后,将左右箭头对齐当前封面的垂直中心。 /*查询活动*/
updateNavigationPosition() {
this.syncBrowseState()
const swiperElement = this.$refs.activitySwiper
const cover = swiperElement && swiperElement.querySelector('.img-box')
if (!cover) return
// 按实际封面高度定位,避免标题换行或响应式列数变化导致箭头偏移。
const container = swiperElement.parentElement
const top = cover.getBoundingClientRect().top - container.getBoundingClientRect().top + cover.offsetHeight / 2
;[this.$refs.prevButton, this.$refs.nextButton].forEach(button => {
if (button) button.style.top = top + 'px'
})
},
// value 为接口返回的活动日期;无日期时显示待定,避免展示 Invalid date。
formatActivityDate(value) {
return value ? this.$moment(value).format('YYYY-MM-DD') : '待定'
},
// 按轮播实际停靠点判断是否还有内容,尺寸改变后同步首页按钮文案和可用状态。
syncBrowseState() {
const enabled = !!(this.swiper && !this.swiper.destroyed && !this.swiper.isLocked && this.swiper.snapGrid.length > 1)
this.$emit('browse-state', {
enabled: enabled,
label: enabled ? '浏览更多' : (this.actList.length ? '已展示全部活动' : '暂无活动')
})
},
// 点击前刷新尺寸,避免异步布局导致轮播状态过期;到末组后返回首组。
browseNext() {
if (!this.swiper || this.swiper.destroyed) this.initSwiper()
if (!this.swiper) return
this.swiper.update()
this.syncBrowseState()
if (this.swiper.isLocked || this.swiper.snapGrid.length <= 1) return
this.swiper.isEnd ? this.swiper.slideTo(0) : this.swiper.slideNext()
},
// 无请求参数;data 为当前用户可见且尚未结束的活动数组,保留本校筛选规则。
listAct() { listAct() {
this.$axios.post("/platform/home/listHomeActivity").then((res) => { this.$axios.post("/platform/home/listHomeActivity").then((res) => {
if (res.code === 0) { if (res.code === 0) {
// for (let i = 0; i < 10; i++) { // for (let i = 0; i < 10; i++) {
// this.actList = this.actList.concat(res.data) // this.actList = this.actList.concat(res.data)
// } // }
this.actList = res.data || [] this.actList = res.data
// 数据加载完成后初始化Swiper // 数据加载完成后初始化Swiper
this.$nextTick(() => { this.$nextTick(() => {
this.initSwiper() this.initSwiper()
}) })
} else {
this.$emit('browse-state', {enabled: false, label: '活动加载失败'})
} }
}).catch(() => {
this.$emit('browse-state', {enabled: false, label: '活动加载失败'})
}) })
}, },
/*初始化Swiper*/ /*初始化Swiper*/
@@ -120,7 +80,6 @@ const act = {
} }
const swiperElement = this.$refs.activitySwiper const swiperElement = this.$refs.activitySwiper
if (!swiperElement) { if (!swiperElement) {
this.syncBrowseState()
return return
} }
// 仅操作当前组件的轮播节点,避免页面其他轮播或尺寸观察器影响卡片宽度。 // 仅操作当前组件的轮播节点,避免页面其他轮播或尺寸观察器影响卡片宽度。
@@ -130,22 +89,13 @@ const act = {
centeredSlides: false, centeredSlides: false,
loop: false, loop: false,
watchOverflow: true, watchOverflow: true,
// 双栏中的活动按容器宽度排布,避免用整个窗口宽度挤出过多卡片。
breakpointsBase: 'container',
observer: false, observer: false,
observeParents: false, observeParents: false,
// 页面内容加载后可能出现滚动条,需重新计算列宽,避免末张封面及圆角被裁切。 resizeObserver: false,
resizeObserver: true,
roundLengths: true,
on: {
init: () => this.$nextTick(() => this.updateNavigationPosition()),
resize: () => this.$nextTick(() => this.updateNavigationPosition()),
slidesUpdated: () => this.$nextTick(() => this.updateNavigationPosition()),
},
// 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。 // 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。
mousewheel: { mousewheel: {
enabled: true, enabled: true,
forceToAxis: true, forceToAxis: false,
releaseOnEdges: true, releaseOnEdges: true,
sensitivity: 0.8, sensitivity: 0.8,
}, },
@@ -158,15 +108,17 @@ const act = {
type: 'progressbar', type: 'progressbar',
}, },
breakpoints: { breakpoints: {
480: { 768: {
slidesPerView: 2, slidesPerView: 2,
}, },
740: { 1024: {
slidesPerView: 3, slidesPerView: 4,
},
1200: {
slidesPerView: 5,
} }
} }
}) })
this.syncBrowseState()
}, },
// 活动状态 // 活动状态
@@ -187,7 +139,7 @@ const act = {
} }
if (act.url === "/platform/activity/apply" if (act.url === "/platform/activity/apply"
&& !this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN", "BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_WENTI_SPORTS"])) { && !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) window.open(act.url)
@@ -1,19 +1,18 @@
const entry = { const entry = {
template: /*language=HTML*/ ` template: /*language=HTML*/ `
<div class="entry-wrapper"> <div class="entry-wrapper">
<div class="portal-entry-header"> <div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key">
<div class="portal-entry-tabs" role="tablist" aria-label="推荐入口"> <div class="entry-section-header">
<button v-for="section in entrySections" :key="section.key" <div class="entry-section-title">
type="button" role="tab" :aria-selected="activeSection === section.key" <i :class="section.icon"></i>
:class="{active: activeSection === section.key}" @click="$set(this, 'activeSection', section.key)">{{ section.title }}</button> <span>{{ section.title }}</span>
<em>/Applications</em>
</div>
</div> </div>
<a class="portal-more" href="/platform/v4/apps">全部应用 <i class="fa fa-arrow-right" aria-hidden="true"></i></a>
</div>
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key" v-show="activeSection === section.key" role="tabpanel">
<div class="apps-container"> <div class="apps-container">
<div class="app-grid" tabindex="0" :aria-label="section.title + ',可横向滚动浏览'"> <div class="app-grid">
<button type="button" class="app-item" <div class="app-item"
v-for="(item, index) in section.list" v-for="(item, index) in section.list"
:key="item.id || index" :key="item.id || index"
@click="openService(item, section.key)"> @click="openService(item, section.key)">
@@ -21,8 +20,8 @@ const entry = {
<img v-if="item.picIcon" class="app-icon-img" :src="item.picIcon" alt="" /> <img v-if="item.picIcon" class="app-icon-img" :src="item.picIcon" alt="" />
<i v-else class="fa fa-cube app-icon-fallback"></i> <i v-else class="fa fa-cube app-icon-fallback"></i>
</div> </div>
<div class="app-name" :title="item.name">{{ item.name }}</div> <div class="app-name">{{ item.name }}</div>
</button> </div>
<el-empty class="app-empty" <el-empty class="app-empty"
v-if="!section.list || section.list.length === 0" v-if="!section.list || section.list.length === 0"
description="暂无数据" description="暂无数据"
@@ -31,12 +30,10 @@ const entry = {
</div> </div>
</div> </div>
</div> </div>
<slot></slot>
</div> </div>
`, `,
data() { data() {
return { return {
activeSection: 'serv',
hasMore: true, hasMore: true,
entries: { entries: {
serv: [], serv: [],
@@ -60,8 +57,8 @@ const entry = {
this.getFavorite() this.getFavorite()
}, },
methods: { methods: {
// platform 固定传 PC;Result.data 为当前用户有权限的推荐服务数组。 // 查询推荐服务
getRecommendService() { async getRecommendService() {
this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => { this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => {
if (res.code === 0) { if (res.code === 0) {
this.entries.serv = res.data this.entries.serv = res.data
@@ -69,8 +66,8 @@ const entry = {
}) })
}, },
// platform 固定传 PC;Result.data 为当前用户可进入的推荐应用数组。 // 获取推荐应用
getRecommendApp() { async getRecommendApp() {
this.$axios.post('/platform/home/listRecommendApp', {platform: "PC"}).then(res => { this.$axios.post('/platform/home/listRecommendApp', {platform: "PC"}).then(res => {
if (res.code === 0) { if (res.code === 0) {
this.entries.app = res.data this.entries.app = res.data
@@ -78,8 +75,8 @@ const entry = {
}) })
}, },
// platform 固定传 PC;Result.data 为当前用户收藏且有权限的菜单数组。 // 获取我的收藏
getFavorite() { async getFavorite() {
this.$axios.post('/platform/home/listFavorite', {platform: "PC"}).then(res => { this.$axios.post('/platform/home/listFavorite', {platform: "PC"}).then(res => {
if (res.code === 0) { if (res.code === 0) {
this.entries.fav = res.data this.entries.fav = res.data
@@ -130,6 +127,11 @@ const entry = {
margin-top: 22px; margin-top: 22px;
} }
/* 临时屏蔽“我的收藏”分组,保留原节点和数据逻辑便于恢复 */
.entry-section-fav {
display: none;
}
.entry-section-header { .entry-section-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -116,55 +116,107 @@ layout("/layouts/v4/baseLayout.html"){
object-fit: contain; 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> </style>
<link rel="stylesheet" href="${base!}/assets/platform/css/home-portal.css?v=20261028-2" /> <div class="v4-container" id="v4-home-app">
<div class="v4-container home-portal" id="v4-home-app">
<!-- 首页banner --> <!-- 首页banner -->
<div class="portal-top"> <div class="section-banner">
<div class="portal-hero"> <div class="banner-img">
<!-- 使用参考项目的本地背景资源,保留横幅文字及渐变遮罩。 --> <img :src="homeBannerList[0] || ''" alt=""/>
<img class="portal-hero-background" src="${base!}/assets/platform/img/home/service-portal-banner-campus-v4.png" alt="" /> <stats></stats>
<div class="portal-hero-inner">
<div class="portal-hero-copy">
<div class="portal-hero-eyebrow">教职工一站式服务平台</div>
<h1>智慧工会系统</h1>
<p>情系教职工,服务零距离</p>
<div class="portal-values">
<span><i class="fa fa-heart" aria-hidden="true"></i>服务职工</span>
<span><i class="fa fa-users" aria-hidden="true"></i>凝聚职工</span>
<span><i class="fa fa-leaf" aria-hidden="true"></i>关爱职工</span>
<span><i class="fa fa-file-text" aria-hidden="true"></i>助力发展</span>
</div>
</div>
</div> </div>
</div> </div>
<user class="portal-member" :profile-only="true"></user>
</div>
<div class="portal-content"> <div class="" style="padding-bottom: 20px">
<user></user>
<div class="home-grid">
<div class="entry-card"> <div class="entry-card">
<entry></entry> <entry></entry>
</div> </div>
</div>
<div class="portal-columns"> <div class="home-grid">
<div class="home-section portal-panel portal-activities"> <div class="home-section">
<div class="home-section-header"> <div class="home-section-header">
<div class="home-section-title"> <div class="home-section-title">
<i class="fa fa-calendar"></i> <i class="fa fa-calendar"></i>
<span>最新活动</span> <span>最新活动</span>
</div> <em>/Activities</em>
<button type="button" class="portal-more" :disabled="!activityBrowse.enabled" @click="$refs.homeActivities.browseNext()">{{ activityBrowse.label }} <i v-if="activityBrowse.enabled" class="fa fa-arrow-right" aria-hidden="true"></i></button> </div>
</div> </div>
<div class="act-card"> <div class="act-card">
<act ref="homeActivities" @browse-state="updateActivityBrowse"></act> <act></act>
</div> </div>
</div> </div>
<user ref="homeTodo" class="portal-todo"></user>
<work-template class="portal-templates" v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
<jcdt class="portal-news" :class="{'portal-news-wide': !$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])}" :list="websiteNews"></jcdt>
</div> </div>
<work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
<jcdt :list="websiteNews"></jcdt>
</div> </div>
</div> </div>
@@ -183,7 +235,7 @@ layout("/layouts/v4/baseLayout.html"){
data(){ data(){
return{ return{
websiteNews: [], websiteNews: [],
activityBrowse: {enabled: false, label: '活动加载中'} homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item)
} }
}, },
components: { components: {
@@ -198,14 +250,10 @@ layout("/layouts/v4/baseLayout.html"){
this.getWebSiteNews() this.getWebSiteNews()
}, },
methods: { methods: {
// state 由活动组件发布,enabled 表示可以翻页,label 说明加载或全部展示状态。
updateActivityBrowse(state) {
this.$set(this, 'activityBrowse', state)
},
getWebSiteNews(){ getWebSiteNews(){
this.$axios.post('/platform/home/getNews').then(res => { this.$axios.post('/platform/home/getNews').then(res => {
if (res.code === 0) { if (res.code === 0) {
this.$set(this, 'websiteNews', res.data || []) this.websiteNews = res.data
} }
}) })
} }
@@ -2,72 +2,44 @@ const jcdt = {
template: /*language=HTML*/ ` template: /*language=HTML*/ `
<div class="jcdt-wrapper"> <div class="jcdt-wrapper">
<div class="jcdt-container"> <div class="jcdt-container">
<div class="portal-news-header"> <el-carousel height="360px" :interval="10000" indicator-position="none">
<el-dropdown @command="selectSection" trigger="click"> <el-carousel-item v-for="item,index in list" :key="index">
<button type="button" class="portal-news-title"><i class="fa fa-bullhorn" aria-hidden="true"></i> {{ currentNews.label }} <i v-if="list.length > 1" class="fa fa-angle-down" aria-hidden="true"></i></button> <div class="jcdt-title">
<el-dropdown-menu slot="dropdown"> <span>{{ item.label }}</span>
<el-dropdown-item v-for="(section, index) in list" :key="index" :command="index">{{ section.label }}</el-dropdown-item> </div>
</el-dropdown-menu> <div class="jcdt-content">
</el-dropdown> <div class="jcdt-list">
<div v-if="pageCount > 1" class="portal-news-pager" aria-label="动态分页"> <div class="jcdt-item" v-for="o,i in item.value" :key="i" @click="onLink(o)">
<span class="portal-news-count">共 {{ currentNews.value.length }} 条</span> <div class="date" v-if="o.time">
<button type="button" :disabled="pageNumber === 1" title="上一页动态" aria-label="上一页动态" @click="changePage(-1)"><i class="fa fa-angle-left" aria-hidden="true"></i></button> <i class="el-icon-alarm-clock"></i>
<span class="portal-news-page" aria-live="polite">{{ pageNumber }} / {{ pageCount }}</span> <span>{{o.time}}</span>
<button type="button" :disabled="pageNumber === pageCount" title="下一页动态" aria-label="下一页动态" @click="changePage(1)"><i class="fa fa-angle-right" aria-hidden="true"></i></button> </div>
</div> <div style="display: flex; align-items: center">
</div> <div>
<div class="portal-news-list">
<button type="button" class="portal-news-row" v-for="(item, index) in visibleNews" :key="index" @click="onLink(item)"> </div>
<time v-if="item.time" :title="item.time">{{ item.time.slice(5) }}</time> <div class="title">{{o.text}}</div>
<span :title="item.text">{{ item.text }}</span> </div>
</button> </div>
<div v-if="!currentNews.value.length" class="portal-empty">暂无动态</div> </div>
</div> </div>
</el-carousel-item>
</el-carousel>
</div> </div>
<slot></slot>
</div> </div>
`, `,
data() { data() {
return {activeIndex: 0, pageNumber: 1, pageSize: 3} return {}
}, },
props: { props: {
list: { list: {
type: Array, type: Array,
default: () => [] default: []
}
},
computed: {
currentNews() {
// 默认遵循接口栏目顺序,不再按栏目名称优先选择。
const section = this.list[this.activeIndex] || this.list[0]
return section ? {label: section.label, value: section.value || []} : {label: '最新动态', value: []}
},
visibleNews() {
const start = (this.pageNumber - 1) * this.pageSize
return this.currentNews.value.slice(start, start + this.pageSize)
},
pageCount() {
return Math.max(1, Math.ceil(this.currentNews.value.length / this.pageSize))
}
},
watch: {
list() {
this.selectSection(0)
} }
}, },
methods: { methods: {
// step 为翻页方向(-1 上一页、1 下一页);在组件方法中更新页码,避免事件中的 this 指向错误。
changePage(step) {
const page = Math.max(1, Math.min(this.pageCount, this.pageNumber + step))
this.$set(this, 'pageNumber', page)
},
// index 为接口栏目数组下标;切换栏目时回到第一页,避免沿用上一栏的末页。
selectSection(index) {
this.activeIndex = index
this.pageNumber = 1
},
onLink(item) { onLink(item) {
// 完整新闻地址直接打开,相对地址仍以重庆工贸工会网站为基准。 // 接口返回文章完整地址;相对地址按当前学校工会域名解析。
const url = this.isHttpOrHttps(item.href) ? item.href : new URL(item.href, "https://gh.cqgmy.edu.cn/").href const url = this.isHttpOrHttps(item.href) ? item.href : new URL(item.href, "https://gh.cqgmy.edu.cn/").href
window.open(url) window.open(url)
}, },
@@ -76,15 +48,6 @@ const jcdt = {
} }
}, },
style: /*language=CSS*/ ` style: /*language=CSS*/ `
/* 每页固定三行,不因展开或末页条数变化撑高相邻的模板卡片。 */
#v4-home-app .portal-news-list { min-height: 132px; }
#v4-home-app .portal-news-list .portal-news-row { height: 44px; padding: 9px 0; }
#v4-home-app .portal-news-pager { display: flex; flex-shrink: 0; align-items: center; gap: 8px; }
#v4-home-app .portal-news-count { margin-right: 4px; color: #8a9aac; font-size: 12px; }
#v4-home-app .portal-news-page { min-width: 32px; color: #637b96; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }
#v4-home-app .portal-news-pager button { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; border: 1px solid #e4edf7; border-radius: 6px; background: #fff; color: #1687f5; font-size: 18px; cursor: pointer; }
#v4-home-app .portal-news-pager button:hover:not(:disabled) { border-color: #b9d8fb; background: #f0f7ff; }
#v4-home-app .portal-news-pager button:disabled { border-color: #eef2f7; background: #f8fafc; color: #c3cedb; cursor: default; }
.jcdt-wrapper { .jcdt-wrapper {
width: 100%; width: 100%;
background-size: cover; background-size: cover;
@@ -5,22 +5,21 @@ const workTemplate = {
<div class="work-template-title"> <div class="work-template-title">
<i class="fa fa-clipboard"></i> <i class="fa fa-clipboard"></i>
<span>工作模板</span> <span>工作模板</span>
<em>/Templates</em>
</div> </div>
<button type="button" class="portal-more" @click="browseTemplates">更多模板 <i class="fa fa-arrow-right" aria-hidden="true"></i></button>
</div> </div>
<div class="work-template-card"> <div class="work-template-card">
<div ref="templateGrid" v-if="templateList && templateList.length > 0" class="work-template-grid"> <div v-if="templateList && templateList.length > 0" class="work-template-grid">
<button type="button" class="work-template-item" <div class="work-template-item"
v-for="template in templateList" v-for="template in templateList"
:key="template.id" :key="template.id"
@click="go(template)"> @click="go(template)">
<div class="work-template-icon"> <div class="work-template-icon">
<el-image :src="template.templateIcon || template.cover" fit="contain"> <img :src="template.templateIcon || template.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
<i slot="error" class="fa fa-clipboard" aria-hidden="true"></i> alt="">
</el-image>
</div> </div>
<div class="work-template-name" :title="template.templateName || template.name">{{ template.templateName || template.name }}</div> <div class="work-template-name">{{ template.templateName || template.name }}</div>
</button> </div>
</div> </div>
<div v-else class="no-activity-placeholder"> <div v-else class="no-activity-placeholder">
@@ -29,7 +28,6 @@ const workTemplate = {
<p class="subtext">敬请关注后续更新</p> <p class="subtext">敬请关注后续更新</p>
</div> </div>
</div> </div>
<slot></slot>
</div> </div>
`, `,
data() { data() {
@@ -38,15 +36,6 @@ const workTemplate = {
} }
}, },
methods: { methods: {
// 每次滚动一屏模板,到末尾后回到开头;无模板时不执行。
browseTemplates() {
const grid = this.$refs.templateGrid
if (!grid) return
// 模板仍使用全部真实数据,横向翻页不截断管理员可用的入口。
const atEnd = grid.scrollLeft + grid.clientWidth >= grid.scrollWidth - 2
grid.scrollTo({left: atEnd ? 0 : grid.scrollLeft + grid.clientWidth, behavior: 'smooth'})
},
// 无请求参数;Result.data 为当前管理员可用的工作模板数组。
listTemplate() { listTemplate() {
this.$axios.post("/platform/home/listHomeTemplate").then((res) => { this.$axios.post("/platform/home/listHomeTemplate").then((res) => {
if (res.code === 0) { if (res.code === 0) {
+125 -124
View File
@@ -4,17 +4,13 @@ const user = {
<!-- 用户容器:左右布局 --> <!-- 用户容器:左右布局 -->
<div class="user-container"> <div class="user-container">
<!-- 左侧:用户信息卡片 --> <!-- 左侧:用户信息卡片 -->
<div v-if="profileOnly" class="user-info-card"> <div class="user-info-card">
<div class="portal-member-date"><i class="fa fa-calendar-o" aria-hidden="true"></i>{{ $moment().format('YYYY年MM月DD日') }} {{ ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'][$moment().day()] }}</div>
<div class="user-details"> <div class="user-details">
<div class="portal-member-heading"> <div class="user-name">${@auth.getPrincipalProperty('username')}</div>
<span class="portal-member-avatar"><i class="fa fa-user" aria-hidden="true"></i></span>
<div><div class="portal-member-welcome">欢迎回来</div><div class="user-name">${@auth.getPrincipalProperty('username')}</div></div>
</div>
<div class="user-meta-grid"> <div class="user-meta-grid">
<div class="meta-row"> <div class="meta-row">
<span class="meta-icon"><i class="fa fa-id-card-o" aria-hidden="true"></i></span> <span class="meta-icon">🆔</span>
<div> <div>
<span class="meta-label">工号</span> <span class="meta-label">工号</span>
<span class="meta-value">${@auth.getPrincipalProperty('loginname')}</span> <span class="meta-value">${@auth.getPrincipalProperty('loginname')}</span>
@@ -22,7 +18,7 @@ const user = {
</div> </div>
<div class="meta-row"> <div class="meta-row">
<span class="meta-icon"><i class="fa fa-building-o" aria-hidden="true"></i></span> <span class="meta-icon">🏢</span>
<div> <div>
<span class="meta-label">单位信息</span> <span class="meta-label">单位信息</span>
<span class="meta-value"> <span class="meta-value">
@@ -37,11 +33,11 @@ const user = {
</span> </span>
</div> </div>
</div> </div>
<div class="meta-row meta-row-roles"> <div class="meta-row">
<span class="meta-icon"><i class="fa fa-shield" aria-hidden="true"></i></span> <span class="meta-icon">🎭</span>
<div> <div>
<span class="meta-label">角色</span> <span class="meta-label">角色</span>
<div class="portal-member-roles" tabindex="0" role="region" aria-label="角色列表,可滚动查看全部角色"><span v-for="(role, index) in roleTags" :key="index" class="portal-member-role">{{ role }}</span><span v-if="!roleTags.length" class="meta-value">—</span></div> <span class="meta-value">{{ roleNames || '—' }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -67,13 +63,10 @@ const user = {
</div> </div>
<!-- 右侧:待办中心 --> <!-- 右侧:待办中心 -->
<div v-if="!profileOnly" class="todo-center"> <div class="todo-center">
<!-- 标题和标签 --> <!-- 标题和标签 -->
<div class="header"> <div class="header">
<div class="portal-todo-heading"> <h3>待办中心</h3>
<h3><i class="fa fa-list-ul" aria-hidden="true"></i> 待办中心</h3>
<a class="portal-more" :href="'/flow/todoCenter?status=' + activeTab" target="_blank">全部待办 <i class="fa fa-arrow-right" aria-hidden="true"></i></a>
</div>
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick"> <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
<el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane> <el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane>
<el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane> <el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane>
@@ -82,39 +75,55 @@ const user = {
</el-tabs> </el-tabs>
</div> </div>
<div class="portal-task-list" v-loading="tableLoading"> <!-- 表格 -->
<div class="portal-task-row" v-for="(task, index) in tableData" :key="task.taskId || task.instanceId || index"> <el-table
<i class="fa fa-file-text-o portal-task-icon" aria-hidden="true"></i> :data="todoList"
<div class="portal-task-content"> style="width: 100%"
<div class="portal-task-title" :title="task.instanceName">{{ task.instanceName }}</div> border
<div class="portal-task-meta"> stripe
<span :title="task.variable && task.variable.initiatorName">申请人:{{ (task.variable && task.variable.initiatorName) || '—' }}</span> v-loading="loading"
<span class="portal-task-node" :title="[task.categoryName, task.taskName].filter(Boolean).join(' · ')">{{ [task.categoryName, task.taskName].filter(Boolean).join(' · ') }}</span> :cell-style="{ padding: '8px' }"
</div> :header-cell-style="{ background: '#f8f9fa', color: '#444', fontWeight: '500' }"
</div> >
<time class="portal-task-date">{{ task.createdAt ? $moment(task.createdAt).format('YYYY-MM-DD') : '—' }}</time> <el-table-column label="序号" type="index" :index="indexMethod" width="100px"></el-table-column>
<el-button type="primary" size="mini" @click="openView(task)">查看</el-button> <el-table-column prop="categoryName" label="流程分类" width="100"></el-table-column>
</div> <el-table-column prop="title" label="流程名称" min-width="300">
<div v-if="!tableLoading && !tableData.length" class="portal-empty"><i class="fa fa-check-circle-o" aria-hidden="true"></i><span>暂无{{ activeTab === 'todo' ? '待办事项' : '相关记录' }}</span></div> <template slot-scope="scope">
<slot></slot> <el-tooltip effect="dark" :content="scope.row.instanceName" placement="top">
</div> <span class="table-title">{{ scope.row.instanceName }}</span>
<el-pagination v-if="pageForm.totalCount > pageForm.pageSize" class="portal-task-pagination" </el-tooltip>
@current-change="pageNumberChange" :current-page="pageForm.pageNumber" :page-size="pageForm.pageSize" </template>
layout="total, prev, next" :total="pageForm.totalCount"></el-pagination> </el-table-column>
<el-table-column prop="sender" label="申请人" width="120">
<template slot-scope="{row}">{{row.variable.initiatorName}}</template>
</el-table-column>
<el-table-column prop="taskName" label="当前流程节点" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="date" label="发起日期" width="120">
<template slot-scope="{row}">
{{$moment(row.createdAt).format('YYYY-MM-DD')}}
</template>
</el-table-column>
<el-table-column label="操作" width="100px" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="openView(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</div> </div>
</div> </div>
</div> </div>
`, `,
mixins: [initTableMixins], mixins: [initTableMixins],
// 同一组件分别承载会员卡和待办卡,各自只加载需要的数据。
props: {
profileOnly: { type: Boolean, default: false }
},
data() { data() {
return { return {
activeTab: 'todo', activeTab: 'todo',
todoList: [],
loading: false,
pageForm: { pageForm: {
pageSize: 3 pageNum: 1,
pageSize: 5,
totalCount: 0
}, },
taskStats: { taskStats: {
todoCount: 0, todoCount: 0,
@@ -131,12 +140,6 @@ const user = {
}; };
}, },
computed: {
roleTags() {
// 兼容角色接口已有的中英文分隔符,仅改变展示,不改变角色权限。
return String(this.roleNames || '').split(/[,,、;;]/).map(role => role.trim()).filter(Boolean)
}
},
methods: { methods: {
getWeatherInfo(iconCode, temp) { getWeatherInfo(iconCode, temp) {
const info = { const info = {
@@ -186,60 +189,91 @@ const user = {
return info; return info;
}, },
// key 为系统配置名称;返回 Promise,值为 Result.data 中的经度或纬度。 async getConfigKey(key) {
getConfigKey(key) { const resp = await this.$axios.post("/open/common/getConfigKey", { key })
return this.$axios.post("/open/common/getConfigKey", {key}).then(res => res.data) return resp.data
}, },
// 使用本校配置经纬度查询城市及天气,缺少配置时不请求外部天气接口。 // 🔆 获取天气
fetchWeather() { async fetchWeather() {
Promise.all([this.getConfigKey("AppMapCenterPointX"), this.getConfigKey("AppMapCenterPointY")]) try {
.then(coordinates => { // 存localStorage 112.13,32.01 广州 113.27,23.14
if (!coordinates[0] || !coordinates[1]) return const AppMapCenterPointX = await this.getConfigKey("AppMapCenterPointX")
return this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/geo/v2/city/lookup?location=" + coordinates.join(",") + "&key=" + this.QWEATHER_KEY) const AppMapCenterPointY = await this.getConfigKey("AppMapCenterPointY")
.then(geoRes => { if (!AppMapCenterPointX || !AppMapCenterPointY){
if (geoRes.code !== '200' || !geoRes.location || !geoRes.location.length) { return
throw new Error('城市未找到') }
}
const city = geoRes.location[0] // 1. 先通过城市名获取 locationId
return this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/v7/weather/now?location=" + city.id + "&key=" + this.QWEATHER_KEY) this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/geo/v2/city/lookup?location=" + AppMapCenterPointX + "," + AppMapCenterPointY + "&key=" + this.QWEATHER_KEY).then((geoRes) => {
.then(weatherRes => { if (geoRes.code !== '200' || !geoRes.location?.[0]) {
if (weatherRes.code !== '200' || !weatherRes.now) throw new Error('天气数据异常') console.log('城市未找到')
this.weatherData = Object.assign({}, this.getWeatherInfo(weatherRes.now.icon, weatherRes.now.temp), { }
temp: weatherRes.now.temp, text: weatherRes.now.text, name: city.name const locationId = geoRes.location[0].id;
})
this.weatherError = false // 2. 获取当前天气
}) this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/v7/weather/now?location=" + locationId + "&key=" + this.QWEATHER_KEY).then((weatherRes) => {
}) if (weatherRes.code !== '200') {
}) console.warn('天气数据异常:', weatherRes);
.catch(() => { }
this.weatherError = true const weatherInfo = this.getWeatherInfo(weatherRes.now.icon, weatherRes.now.temp);
this.weatherData = null this.weatherData = {
...weatherInfo,
temp: weatherRes.now.temp,
text: weatherRes.now.text,
name: geoRes.location[0].name
};
this.weatherError = false;
})
}) })
} catch (err) {
console.warn('天气加载失败:', err);
this.weatherError = true;
this.weatherData = null;
}
}, },
// 获取任务列表 // 获取任务列表
tabLabel(title, count) { tabLabel(title, count) {
return title + "(" + (count || 0) + ")" return title + "(" + (count || 0) + ")"
}, },
// 无请求参数;data 的四个 Count 字段分别为当前用户各状态流程数量。 async getStatistics() {
getStatistics() { try {
this.$axios.post("/flow/todoCenter/statistics").then(res => { const res = await $.post("/flow/todoCenter/statistics")
if (res.code === 0) this.taskStats = Object.assign({}, this.taskStats, res.data) if (res.code === 0) {
}).catch(() => { this.taskStats = Object.assign({}, this.taskStats, res.data)
this.$message.error("获取待办统计失败") }
}) } catch (error) {
console.error("获取待办统计失败:", error)
}
},
async getTasks() {
this.loading = true
try {
const res = await $.post("/flow/todoCenter/" + this.activeTab, this.pageForm)
if (res.code === 0) {
this.todoList = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
} catch (error) {
this.$message.error("获取任务列表失败")
console.error("获取任务列表失败:", error)
} finally {
this.loading = false
}
}, },
// WF 状态由 activeTab 决定;pageForm 传分页参数,data.list/totalCount 为记录及总数。
// 列表加载和翻页由 initTableMixins 统一处理,首页只补充状态地址和数量统计。
pageData() { pageData() {
this.pageDataUrl = "/flow/todoCenter/" + this.activeTab this.getTasks()
initTableMixins.methods.pageData.call(this)
this.getStatistics() this.getStatistics()
}, },
handleTabClick() { handleTabClick(tab) {
this.doSearch() this.pageData()
console.log('切换到:', tab.name);
}, },
// 处理任务 // 处理任务
openView(task) { async openView(task) {
console.log(task)
const {taskId, taskKey, taskState, instanceId, businessNo, formKey} = task const {taskId, taskKey, taskState, instanceId, businessNo, formKey} = task
if (!formKey) { if (!formKey) {
this.$message.warning("当前流程没有配置地址") this.$message.warning("当前流程没有配置地址")
@@ -247,9 +281,8 @@ const user = {
} }
window.open(formKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab) window.open(formKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
}, },
// 无请求参数;Result.data.roleNames 为当前登录人的角色名称字符串。
getRoleNames() { getRoleNames() {
this.$axios.post("/platform/sys/role/getRoleNames").then(res => { $.post("/platform/sys/role/getRoleNames").then(res => {
if (res.code === 0) { if (res.code === 0) {
this.roleNames = res.data.roleNames this.roleNames = res.data.roleNames
} }
@@ -258,45 +291,13 @@ const user = {
} }
}, },
created() { created() {
if (this.profileOnly) { this.pageData()
this.getRoleNames() this.getRoleNames()
} else {
this.pageData()
}
}, },
mounted() { mounted() {
if (this.profileOnly) this.fetchWeather() this.fetchWeather()
}, },
style: /*language=CSS*/ ` style: /*language=CSS*/ `
/* 会员卡样式随组件注入,确保与卡片结构同步加载,避免旧外链样式造成排版缺失。 */
#v4-home-app .portal-member { min-width: 0; }
#v4-home-app .portal-member .user-info-card { height: 100%; padding: 20px; gap: 14px; border: 1px solid #e3edf8; border-radius: 12px; background: linear-gradient(180deg, #f1f7ff 0, #fff 110px); box-shadow: 0 3px 14px rgba(30,66,108,.035); }
#v4-home-app .portal-member-date { display: flex; align-items: center; gap: 8px; color: #71839b; font-size: 12px; line-height: 20px; }
#v4-home-app .portal-member-heading { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid #eaf0f7; }
#v4-home-app .portal-member-heading > div { min-width: 0; }
#v4-home-app .portal-member-avatar { display: flex; align-items: center; justify-content: center; flex: 0 0 46px; height: 46px; border-radius: 50%; background: #e1eeff; color: #1687f5; font-size: 25px; }
#v4-home-app .portal-member-welcome { margin-bottom: 3px; color: #8797a9; font-size: 12px; line-height: 18px; }
#v4-home-app .portal-member .user-name { margin: 0; color: #163c61; text-align: left; font-size: 18px; line-height: 24px; overflow-wrap: anywhere; }
#v4-home-app .portal-member .user-meta-grid { gap: 12px; margin: 0; }
#v4-home-app .portal-member .meta-row { gap: 8px; }
#v4-home-app .portal-member .meta-icon { min-width: 18px; color: #81a4c9; font-size: 14px; }
#v4-home-app .portal-member .meta-row > div { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 8px; min-width: 0; }
#v4-home-app .portal-member .meta-label { margin: 0; color: #8995a5; font-size: 12px; line-height: 20px; }
#v4-home-app .portal-member .meta-value { color: #283c54; font-size: 12px; font-weight: 400; line-height: 20px; overflow-wrap: anywhere; }
#v4-home-app .portal-member .meta-row-roles > div { display: block; }
/* 角色最多占用三行标签的高度,超出时内部滚动,避免撑高首页横幅。 */
#v4-home-app .portal-member-roles { display: flex; flex-wrap: wrap; align-content: flex-start; gap: 5px; margin-top: 5px; max-height: 88px; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; scrollbar-color: #c5d9ee transparent; }
#v4-home-app .portal-member-roles::-webkit-scrollbar { width: 4px; }
#v4-home-app .portal-member-roles::-webkit-scrollbar-thumb { background: #c5d9ee; border-radius: 4px; }
#v4-home-app .portal-member-role { max-width: 100%; display: inline-block; padding: 3px 8px; border: 1px solid #e1ebf7; border-radius: 5px; background: #f3f7fc; color: #46617f; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
#v4-home-app .portal-member .weather-section { margin-top: auto; padding: 8px 10px; border-radius: 8px; background: #f0f7ff; color: #527494; }
#v4-home-app .portal-member .weather-header { gap: 6px; }
#v4-home-app .portal-member .weather-location,
#v4-home-app .portal-member .weather-text { font-size: 12px; }
#v4-home-app .portal-member .weather-emoji { font-size: 20px; }
#v4-home-app .portal-member .weather-temp { font-size: 18px; }
#v4-home-app .portal-member .weather-tip { margin-top: 4px; font-size: 11px; line-height: 18px; }
#v4-home-app .portal-member .weather-error { margin-top: auto; padding: 8px; font-size: 12px; }
.user-wrapper { .user-wrapper {
width: 100%; width: 100%;
background-color: #f5f7fa; background-color: #f5f7fa;
@@ -221,8 +221,7 @@ layout("/layouts/platform.html"){
placeholder="请输入姓名" filterable clearable placeholder="请输入姓名" filterable clearable
style="width: 100%" size="small" v-if="awardsMode==1"> style="width: 100%" size="small" v-if="awardsMode==1">
<el-option <el-option
v-for="item in userList" v-for="item in getSelectableUserList(row)"
:disabled="item.disabled"
:key="item.id" :key="item.id"
:label="item.username+'('+item.loginname+')'+item.sex" :label="item.username+'('+item.loginname+')'+item.sex"
:value="item.id"> :value="item.id">
@@ -721,10 +720,15 @@ layout("/layouts/platform.html"){
} }
return score; 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) { userDetailsChange(val, row) {
const useridArr = this.userData.map(v => v.userId)
this.userList.forEach(v => { this.userList.forEach(v => {
v.disabled = useridArr.includes(v.id)
if (row && row.userId === v.id) { if (row && row.userId === v.id) {
const o = { const o = {
unionId: v.unionId, unionId: v.unionId,
@@ -601,6 +601,8 @@
eventForm: { eventForm: {
isLeader: false isLeader: false
}, },
// 独立记录当前编辑项目,避免下拉框切换后丢失切换前项目标识。
currentEventId: "",
eventsIds: [], eventsIds: [],
rightChecked: [], rightChecked: [],
checkedEventInfo: [], checkedEventInfo: [],
@@ -778,8 +780,10 @@
this.checkedEventInfo = this.checkedEvents.map((v) => { this.checkedEventInfo = this.checkedEvents.map((v) => {
v.eventId = v.id v.eventId = v.id
if (v.projectType === "1") { if (v.projectType === "1") {
v.athletesMaxNum = 3 // “所有单项”保存默认值,具体单项以0表示尚未个性化设置。
v.restrictRegNumber = 2 const isAllItems = v.eventId === "allItems"
v.athletesMaxNum = isAllItems ? 3 : 0
v.restrictRegNumber = isAllItems ? 2 : 0
v.leanderNum = 0 v.leanderNum = 0
v.coachNum = 0 v.coachNum = 0
v.applyUserModel = 1 v.applyUserModel = 1
@@ -791,7 +795,23 @@
return v 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) { async eventChange(val) {
this.cacheCurrentEventForm()
const data = this.checkedEventInfo.find((v) => v.eventId === val) const data = this.checkedEventInfo.find((v) => v.eventId === val)
const resp = await $.get("/platform/activity/sports/common/getEventData", { id: val }) const resp = await $.get("/platform/activity/sports/common/getEventData", { id: val })
await this.$nextTick() await this.$nextTick()
@@ -804,10 +824,10 @@
projectType: resp.data.projectType, projectType: resp.data.projectType,
leanderNum: 0, leanderNum: 0,
coachNum: 0, coachNum: 0,
athletesMaxNum: resp.data.projectType === 1 ? 3 : null, athletesMaxNum: resp.data.projectType === 1 ? 0 : null,
restrictTotalTeam: 1, restrictTotalTeam: 1,
substituteNum: 0, substituteNum: 0,
restrictRegNumber: resp.data.projectType === 1 ? 2 : null, restrictRegNumber: resp.data.projectType === 1 ? 0 : null,
isLeader: false, isLeader: false,
applyUserModel: 1 applyUserModel: 1
} }
@@ -815,33 +835,27 @@
this.projectType = resp.data.projectType this.projectType = resp.data.projectType
} }
this.$set(this.eventForm, "checkedEventId", val) this.$set(this.eventForm, "checkedEventId", val)
this.currentEventId = val
}, },
async saveEvent() { saveEvent() {
const data = clone(this.eventForm) const data = clone(this.eventForm)
if (data.checkedEventId === "allItems") { if (data.checkedEventId === "allItems") {
this.checkedEventInfo.forEach((v) => { const allItemsIndex = this.checkedEventInfo.findIndex((v) => v.eventId === "allItems")
if (v.projectType === "1") { if (allItemsIndex >= 0) {
v.applyUserModel = this.eventForm.applyUserModel this.checkedEventInfo.splice(allItemsIndex, 1, data)
v.athletesMaxNum = data.athletesMaxNum }
v.restrictRegNumber = data.restrictRegNumber
v.unionLimit = data.unionLimit
}
})
} else { } else {
const info = this.checkedEventInfo.find((v) => v.eventId === data.checkedEventId) const info = this.checkedEventInfo.find((v) => v.eventId === data.checkedEventId)
if (info != null) { if (info != null) {
const index = this.checkedEventInfo.findIndex((v) => v.eventId === data.checkedEventId) const index = this.checkedEventInfo.findIndex((v) => v.eventId === data.checkedEventId)
this.checkedEventInfo.splice(index, 1) 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 data.eventId = data.checkedEventId
this.checkedEventInfo.push(data) this.checkedEventInfo.push(data)
} }
await this.doSave() this.eventForm = clone(data)
this.currentEventId = data.checkedEventId
return this.doSave()
}, },
prev() { prev() {
this.active-- this.active--
@@ -949,6 +963,7 @@
this.copyTemplateMode = false this.copyTemplateMode = false
this.copyTemplateName = "" this.copyTemplateName = ""
this.pushTodoFlag = false this.pushTodoFlag = false
this.currentEventId = ""
this.events = await this.getEvents(2) this.events = await this.getEvents(2)
await this.getActivityGroup() await this.getActivityGroup()
this.active = 0 this.active = 0
@@ -1028,6 +1043,7 @@
async openEdit(row, flag, copyTemplateMode = false) { async openEdit(row, flag, copyTemplateMode = false) {
this.copyTemplateMode = !!copyTemplateMode this.copyTemplateMode = !!copyTemplateMode
this.pushTodoFlag = false this.pushTodoFlag = false
this.currentEventId = ""
this.active = flag ? 0 : this.active this.active = flag ? 0 : this.active
await this.getActivityGroup() await this.getActivityGroup()
const { id } = row const { id } = row
@@ -1184,7 +1200,8 @@
} }
if (this.eventForm.checkedEventId == "allItems") { if (this.eventForm.checkedEventId == "allItems") {
events = events.filter((v) => v.projectType == 1 && v.id != "allItems") // “所有单项”配置随具体单项提交,由后端统一校验并补齐继承值。
events = events.filter((v) => v.projectType == 1)
} else { } else {
this.$set(this.eventForm, "activityId", this.formData.id) this.$set(this.eventForm, "activityId", this.formData.id)
this.$set(this.eventForm, "eventId", this.eventForm.checkedEventId) 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, "startAgeDate", this.eventForm.ageDate[0])
this.$set(this.eventForm, "endAgeDate", this.eventForm.ageDate[1]) 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) { if (this.copyTemplateMode) {
this.cleanCopyTemplateData(null, [events]) this.cleanCopyTemplateData(null, events)
} }
} }
const loading = this.$loading({ const loading = this.$loading({
@@ -1215,6 +1233,7 @@
this.copyTemplateMode = false this.copyTemplateMode = false
} }
await this.openEdit(formData, false) await this.openEdit(formData, false)
this.currentEventId = this.eventForm.checkedEventId || ""
this.$message.success(resp.msg) this.$message.success(resp.msg)
} else { } else {
this.$message.error(resp.msg) this.$message.error(resp.msg)
@@ -1,4 +1,11 @@
const REGISTER_INFO_COMPONENT = { const REGISTER_INFO_COMPONENT = {
// 默认展示换届报告,仅由不需要查看该字段的业务页面显式关闭。
props: {
hideReplaceReport: {
type: Boolean,
default: false
}
},
template: ` template: `
<div> <div>
<el-tabs tab-position="top" v-model="activeName"> <el-tabs tab-position="top" v-model="activeName">
@@ -36,7 +43,7 @@ const REGISTER_INFO_COMPONENT = {
{{viewData.createTime}} {{viewData.createTime}}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="会费标准" :span="4"> <el-descriptions-item label="会费标准" :span="4">
{{viewData.due}} {{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="4"> <el-descriptions-item :span="4">
<template slot="label">社团介绍</template> <template slot="label">社团介绍</template>
@@ -66,7 +73,7 @@ const REGISTER_INFO_COMPONENT = {
<template slot="label">其他附件</template> <template slot="label">其他附件</template>
<file-preview :files="viewData.files" complete_result></file-preview> <file-preview :files="viewData.files" complete_result></file-preview>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="2"> <el-descriptions-item :span="2" v-if="!hideReplaceReport">
<template slot="label">换届报告</template> <template slot="label">换届报告</template>
<file-preview :files="viewData.replaceReport" complete_result></file-preview> <file-preview :files="viewData.replaceReport" complete_result></file-preview>
</el-descriptions-item> </el-descriptions-item>
@@ -68,10 +68,12 @@ layout("/layouts/platform.html"){
</el-steps> </el-steps>
<!-- 第一步:基础信息表单 --> <!-- 第一步:基础信息表单 -->
<!-- 本表单自行提示首个必填错误,关闭全局校验弹窗增强以免重复提示。 -->
<el-form <el-form
v-show="step === 0" v-show="step === 0"
:model="formData" :model="formData"
ref="baseForm" ref="baseForm"
no-auto-enhance
:rules="baseRules" :rules="baseRules"
label-width="150px" label-width="150px"
class="main-form" class="main-form"
@@ -135,7 +137,9 @@ layout("/layouts/platform.html"){
v-model="formData.due" v-model="formData.due"
placeholder="请输入会费标准" placeholder="请输入会费标准"
readonly readonly
></el-input> >
<template slot="append">元/年</template>
</el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -146,7 +150,7 @@ layout("/layouts/platform.html"){
<el-form-item <el-form-item
class="is-required" class="is-required"
label="本年度活动开展情况" label="本年度活动开展情况"
prop="yearActivity" prop="yearActivityList"
> >
<span class="tip-text">说明:非赛事活动无需填写赛事举办单位、获奖情况。</span> <span class="tip-text">说明:非赛事活动无需填写赛事举办单位、获奖情况。</span>
<el-table <el-table
@@ -638,6 +642,7 @@ layout("/layouts/platform.html"){
id: GetQueryString("bizId"), id: GetQueryString("bizId"),
taskId: GetQueryString("taskId"), taskId: GetQueryString("taskId"),
step: 0, // 当前步骤(0-基础信息,1-财务收支,2-成员名单) step: 0, // 当前步骤(0-基础信息,1-财务收支,2-成员名单)
formLoading: false,
isRegistered: false, // 是否已登记 isRegistered: false, // 是否已登记
currentClubId: "", // 当前选中社团ID currentClubId: "", // 当前选中社团ID
@@ -656,7 +661,7 @@ layout("/layouts/platform.html"){
year: new Date().getFullYear() + "", // 默认当前年度 year: new Date().getFullYear() + "", // 默认当前年度
clubId: "", clubId: "",
foundTime: "", foundTime: "",
due: "", due: null,
clubName: "", clubName: "",
yearActivityList: [{}], // 本年度活动列表 yearActivityList: [{}], // 本年度活动列表
plans: [{}], // 下年度计划列表 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: {}, financeRules: {},
memberRules: {} memberRules: {}
}; };
@@ -724,6 +732,28 @@ layout("/layouts/platform.html"){
} }
}, },
methods: { 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) // 格式化数字(保留2位小数,空值显示0)
formatNumber(num) { formatNumber(num) {
@@ -1110,7 +1140,7 @@ layout("/layouts/platform.html"){
if (!val) return; if (!val) return;
const club = this.clubOptions.find(item => item.id === val); 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.formData.foundTime = club?.foundTime
? this.$moment(club.foundTime).format("YYYY-MM-DD") ? this.$moment(club.foundTime).format("YYYY-MM-DD")
: this.$moment().format("YYYY-MM-DD"); : this.$moment().format("YYYY-MM-DD");
@@ -1161,73 +1191,63 @@ layout("/layouts/platform.html"){
// 步骤前进 // 步骤前进
this.step++; this.step++;
}, },
// 保存 // 三个操作共用基础信息校验,失败时返回第一步展示行号和必填字段。
async handleSave() { validateBaseBeforeAction(action) {
try { if (this.formLoading) return;
this.recalculateCensus(); this.$refs.baseForm.validate((valid, fields) => {
const resp = await this.$axios.post("/platform/club/examine/apply/save", { if (!valid) {
data: JSON.stringify(this.formData), this.step = 0;
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed), const firstError = Object.values(fields || {}).flat()[0];
incomeCensus: JSON.stringify(this.financeData.incomeCensus) this.$message.warning('温馨提示:' + (firstError ? firstError.message : '请补全基础信息必填项'));
}); return;
if (resp.code === 0) {
this.$message.success(resp.msg);
commonUtil.pjaxPush('/platform/club/examine/mine');
} }
} catch (error) { action();
this.handleError(error, "保存失败"); });
}
}, },
// 提交(新增) // 仅接收本页三个固定操作的接口后缀;data为登记实体JSON,另两项为财务数组JSON。
async handleSubmit() { // 接口返回{code,msg,data},code为0时保存成功,否则展示后端校验提示。
try { requestExamineAction(action) {
this.recalculateCensus(); if (this.formLoading) return;
await this.$confirm("确定要提交此申请吗?", "提示", { this.recalculateCensus();
confirmButtonText: "确定", const params = {
cancelButtonText: "取消", data: JSON.stringify(this.formData),
type: "info" 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, "提交失败");
}
}
}, },
// 提交(任务处理) handleSave() {
async handleFinishTask() { this.validateBaseBeforeAction(() => this.requestExamineAction('save'));
try { },
this.recalculateCensus(); handleSubmit() {
await this.$confirm("您确定要提交吗?", "提示", { this.validateBaseBeforeAction(() => {
confirmButtonText: "确定", this.$confirm('确定要提交此申请吗?', '提示', {
cancelButtonText: "取消", confirmButtonText: '确定', cancelButtonText: '取消', type: 'info'
type: "warning" }).then(() => this.requestExamineAction('submit')).catch(() => {});
}); });
const resp = await this.$axios.post('/platform/club/examine/apply/submitAgain', { },
data: JSON.stringify(this.formData), handleFinishTask() {
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed), this.validateBaseBeforeAction(() => {
incomeCensus: JSON.stringify(this.financeData.incomeCensus), this.$confirm('您确定要提交吗?', '提示', {
taskId: this.taskId confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}); }).then(() => this.requestExamineAction('submitAgain')).catch(() => {});
if (resp.code === 0) { });
this.$message.success(resp.msg);
commonUtil.pjaxPush('/platform/club/examine/mine');
}
} catch (error) {
if (error !== "cancel") {
this.handleError(error, "提交失败");
}
}
} }
} }
}); });
@@ -10,7 +10,7 @@
<el-descriptions v-if="viewData" :column="3" border class="table_fixed"> <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.clubName }}</el-descriptions-item>
<el-descriptions-item label="成立时间">{{ viewData.foundTime }} </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"> <el-descriptions-item span="3">
<template slot="label">工作总结</template> <template slot="label">工作总结</template>
<file-preview :files="viewData.summaryFiles" complete_result></file-preview> <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 label="联系人邮箱">{{viewData.concatPersonEmail}}</el-descriptions-item>
<el-descriptions-item> <el-descriptions-item>
<template slot="label">会费标准</template> <template slot="label">会费标准</template>
{{viewData.due}} {{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="2"> <el-descriptions-item :span="2">
<template slot="label">当前人数</template> <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.clubCode}}</el-descriptions-item>
<el-descriptions-item label="社团类型">{{viewData.typeName}}</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.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 label="当前人数">{{viewData.currentPeopleNum}}</el-descriptions-item>
<el-descriptions-item span="2"> <el-descriptions-item span="2">
<template slot="label">社团介绍</template> <template slot="label">社团介绍</template>
@@ -24,7 +24,13 @@ const CLUB_FORM_TEMPLATE = {
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="会费标准" prop="due"> <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> <template slot="append">元/年</template>
</el-input> </el-input>
</el-form-item> </el-form-item>
@@ -156,12 +162,15 @@ const CLUB_FORM_TEMPLATE = {
return { return {
formData: { formData: {
clubName: "", clubName: "",
clubType: "" clubType: "",
due: null
}, },
formRules: { formRules: {
clubName: [{ required: true, message: "请填写社团名称", trigger: ["blur", "change"] }], clubName: [{ required: true, message: "请填写社团名称", trigger: ["blur", "change"] }],
clubCode: [{ required: false, message: "请填写社团编码", trigger: ["blur", "change"] }], clubCode: [{ required: false, message: "请填写社团编码", trigger: ["blur", "change"] }],
clubType: [{ required: true, 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"] }], //concatPerson: [{ required: true, message: "请选择社团联系人", trigger: ["blur", "change"] }],
foundTime: [{ required: true, message: "请选择成立时间", trigger: ["blur", "change"] }] foundTime: [{ required: true, message: "请选择成立时间", trigger: ["blur", "change"] }]
} }
@@ -73,7 +73,7 @@ layout("/layouts/platform.html"){
</el-card> </el-card>
<template #public> <template #public>
<register-info ref="registerInfoRef"> <register-info ref="registerInfoRef" :hide-replace-report="true">
<div v-if="showApprovalForm"> <div v-if="showApprovalForm">
<div class="process-title"> <div class="process-title">
{{formData.taskName}} {{formData.taskName}}
@@ -55,7 +55,7 @@ layout("/layouts/platform.html"){
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-descriptions-item>--> </el-descriptions-item>-->
<el-descriptions-item label="证明人"> <!-- <el-descriptions-item label="证明人">
<el-form-item prop="certifierUserId" label="证明人"> <el-form-item prop="certifierUserId" label="证明人">
<el-select <el-select
style="width: 100%" style="width: 100%"
@@ -75,7 +75,7 @@ layout("/layouts/platform.html"){
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item> -->
<el-descriptions-item label="慰问类型"> <el-descriptions-item label="慰问类型">
<el-form-item prop="type" label="慰问类型"> <el-form-item prop="type" label="慰问类型">
<el-select v-model="formData.type" @change="typeChange" style="width: 100%" <el-select v-model="formData.type" @change="typeChange" style="width: 100%"
@@ -112,14 +112,14 @@ layout("/layouts/platform.html"){
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="收款账户"> <el-descriptions-item label="收款账户" v-if="!['A', 'D'].includes(formData.typeCode)">
<el-form-item prop="bankCardNumber" label="收款账户"> <el-form-item prop="bankCardNumber" label="收款账户">
<el-input maxlength="20" v-model="formData.bankCardNumber" placeholder="请填写收款账户" <el-input maxlength="30" v-model="formData.bankCardNumber" placeholder="请填写银行卡号或对公账户号"
></el-input> ></el-input>
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="户名"> <el-descriptions-item label="户名" v-if="!['A', 'D'].includes(formData.typeCode)">
<el-form-item prop="bankUserName" label="户名"> <el-form-item prop="bankUserName" label="户名">
<el-input maxlength="20" v-model="formData.bankUserName" <el-input maxlength="20" v-model="formData.bankUserName"
placeholder="请填写户名" placeholder="请填写户名"
@@ -127,7 +127,7 @@ layout("/layouts/platform.html"){
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="开户行" > <el-descriptions-item label="开户行" v-if="!['A', 'D'].includes(formData.typeCode)">
<el-form-item prop="bankOfDeposit" label="开户行"> <el-form-item prop="bankOfDeposit" label="开户行">
<el-input maxlength="20" v-model="formData.bankOfDeposit" <el-input maxlength="20" v-model="formData.bankOfDeposit"
placeholder="请填写开户行" placeholder="请填写开户行"
@@ -173,10 +173,12 @@ layout("/layouts/platform.html"){
</template> </template>
</el-descriptions>
<el-descriptions :column="3" border>
<el-descriptions-item label="申请事由" :span="3"> <el-descriptions-item label="申请事由" :span="3">
<el-form-item prop="remark" label="申请事由"> <el-form-item prop="remark" label="申请事由">
<el-input maxlength="500" v-model="formData.remark" placeholder="请填写申请事由" <el-input maxlength="500" v-model="formData.remark" placeholder="请填写申请事由"
type="textarea" :autosize="{ minRows: 4, maxRows: 8}"></el-input> type="textarea" :rows="3" resize="none"></el-input>
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
@@ -238,7 +240,10 @@ layout("/layouts/platform.html"){
payUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }], payUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
type: [{ required: true, message: "必填", trigger: ["change", "blur"] }], type: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
way: [{ required: true, message: "必填", trigger: ["change", "blur"] }], way: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
money: [{ 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"] }], occurTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
child: [{ required: true, message: "必填", trigger: ["change", "blur"] }], child: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
hospital: [{ required: true, message: "必填", trigger: ["change", "blur"] }], hospital: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
@@ -247,14 +252,20 @@ layout("/layouts/platform.html"){
hospitalBy: [{ required: true, message: "必填", trigger: ["change", "blur"] }], hospitalBy: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
deadImmediateFamily: [{ required: true, message: "必填", trigger: ["change", "blur"] }], deadImmediateFamily: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
remark: [{ required: true, message: "必填", trigger: ["change", "blur"] }], remark: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
files: [{ 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"] }], signature: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
handlerUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }], handlerUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
certifierUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }], // certifierUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
bankCardNumber: [{ 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"] }], bankUserName: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
bankOfDeposit: [{ required: true, message: "必填", trigger: ["change", "blur"] }], bankOfDeposit: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
thisYearHospitalizationNum: [{ 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"] }], leaveHospitalTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
hospitalizationTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }], hospitalizationTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
}, },
@@ -268,6 +279,9 @@ layout("/layouts/platform.html"){
} }
}, },
methods: { methods: {
validateMoney(value) {
return /^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000
},
createRemoteMethod(options) { createRemoteMethod(options) {
return (keyword) => { return (keyword) => {
this.selectQueryUser(keyword, options) this.selectQueryUser(keyword, options)
@@ -37,12 +37,12 @@ const basicForm = {
<el-radio-button :label="false">不上传</el-radio-button> <el-radio-button :label="false">不上传</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item>--> </el-form-item>-->
<!-- <el-form-item label="是否上传附件" prop="isUploadFile">--> <el-form-item label="是否上传附件" prop="isUploadFile">
<!-- <el-radio-group v-model="formData.isUploadFile">--> <el-radio-group v-model="formData.isUploadFile">
<!-- <el-radio-button :label="true">是</el-radio-button>--> <el-radio-button :label="true">是</el-radio-button>
<!-- <el-radio-button :label="false">否</el-radio-button>--> <el-radio-button :label="false">否</el-radio-button>
<!-- </el-radio-group>--> </el-radio-group>
<!-- </el-form-item>--> </el-form-item>
<el-form-item label="上传附件说明" prop="uploadFileDesc" v-if="formData.isUploadFile"> <el-form-item label="上传附件说明" prop="uploadFileDesc" v-if="formData.isUploadFile">
<el-input v-model="formData.uploadFileDesc" maxlength="50" clearable placeholder="请填写上传附件说明"></el-input> <el-input v-model="formData.uploadFileDesc" maxlength="50" clearable placeholder="请填写上传附件说明"></el-input>
</el-form-item> </el-form-item>
@@ -26,7 +26,7 @@ layout("/layouts/platform.html"){
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="受补助人"> <el-descriptions-item label="受补助人">
<el-form-item prop="userName"> <el-form-item prop="userId">
<template v-if="formData.mode == 2"> <template v-if="formData.mode == 2">
<el-select :remote-method="queryRecipients" <el-select :remote-method="queryRecipients"
@change="userChange" clearable filterable @change="userChange" clearable filterable
@@ -51,7 +51,7 @@ layout("/layouts/platform.html"){
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="身份证号" :span="2"> <el-descriptions-item label="身份证号" :span="2">
<el-form-item prop="idCard"> <el-form-item prop="idCard">
<el-input maxlength="20" clearable placeholder="请填写身份证号" <el-input clearable placeholder="请填写身份证号"
v-model="formData.idCard" maxlength="18"></el-input> v-model="formData.idCard" maxlength="18"></el-input>
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
@@ -84,7 +84,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="手机号码"> <el-descriptions-item label="手机号码">
<el-form-item prop="mobile"> <el-form-item prop="mobile">
<el-input maxlength="11" clearable placeholder="请填写手机号码" <el-input maxlength="11" clearable placeholder="请填写手机号码"
v-model="formData.mobile" maxlength="11"></el-input> v-model="formData.mobile"></el-input>
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="家庭年总收入(万元)"> <el-descriptions-item label="家庭年总收入(万元)">
@@ -103,6 +103,7 @@ layout("/layouts/platform.html"){
<el-input-number <el-input-number
v-model="formData.homeNumber" v-model="formData.homeNumber"
:min="1" :min="1"
:precision="0"
:step="1" :step="1"
style="width: 100%" style="width: 100%"
placeholder="请填写家庭人数"> placeholder="请填写家庭人数">
@@ -113,7 +114,7 @@ layout("/layouts/platform.html"){
<el-form-item prop="homeAddress"> <el-form-item prop="homeAddress">
<el-input clearable <el-input clearable
style="width: 100%" style="width: 100%"
v-model="formData.homeAddress" v-model="formData.homeAddress" maxlength="200"
placeholder="请填写家庭住址"> placeholder="请填写家庭住址">
</el-input> </el-input>
</el-form-item> </el-form-item>
@@ -132,7 +133,7 @@ layout("/layouts/platform.html"){
<!-- </el-descriptions-item>--> <!-- </el-descriptions-item>-->
<el-descriptions-item label="银行卡号" > <el-descriptions-item label="银行卡号" >
<el-form-item prop="bankCardNum"> <el-form-item prop="bankCardNum">
<el-input maxlength="20" clearable placeholder="请填写银行卡号" <el-input maxlength="19" clearable placeholder="请填写银行卡号"
v-model="formData.bankCardNum"></el-input> v-model="formData.bankCardNum"></el-input>
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
@@ -152,7 +153,7 @@ layout("/layouts/platform.html"){
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="申请金额(元)"> <el-descriptions-item label="申请金额(元)">
<el-form-item prop="applyMoney" :rules="[{required:true,type:'number',min:0.01,message:'请填写大于零的申请金额',trigger:['change','blur']}]"> <el-form-item prop="applyMoney">
<el-input-number v-model="formData.applyMoney" :precision="2" :min="0.01" placeholder="请输入申请金额"></el-input-number> <el-input-number v-model="formData.applyMoney" :precision="2" :min="0.01" placeholder="请输入申请金额"></el-input-number>
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
@@ -198,8 +199,8 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}"> <template slot-scope="{row,$index}">
<el-form-item label-width="0" <el-form-item label-width="0"
:prop="'familyList.'+$index+'.relation'" :prop="'familyList.'+$index+'.relation'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]"> :rules="textRules('与本人关系', 50)">
<el-input v-model="row.relation" placeholder="请填写与本人关系" clearable></el-input> <el-input v-model="row.relation" maxlength="50" placeholder="请填写与本人关系" clearable></el-input>
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
@@ -207,8 +208,8 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}"> <template slot-scope="{row,$index}">
<el-form-item label-width="0" <el-form-item label-width="0"
:prop="'familyList.'+$index+'.name'" :prop="'familyList.'+$index+'.name'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]"> :rules="textRules('姓名', 100)">
<el-input v-model="row.name" placeholder="请填写姓名" clearable></el-input> <el-input v-model="row.name" maxlength="100" placeholder="请填写姓名" clearable></el-input>
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
@@ -216,9 +217,9 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}"> <template slot-scope="{row,$index}">
<el-form-item label-width="0" <el-form-item label-width="0"
:prop="'familyList.'+$index+'.age'" :prop="'familyList.'+$index+'.age'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]"> :rules="[{required:true,type:'integer',min:1,max:100,message:'年龄须为1至100的整数',trigger:['change','blur']}]">
<el-input-number v-model="row.age" placeholder="年龄" :min="1" <el-input-number v-model="row.age" placeholder="年龄" :min="1"
:max="100" :step="1" style="width: 100%" clearable></el-input-number> :max="100" :precision="0" :step="1" style="width: 100%" clearable></el-input-number>
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
@@ -226,8 +227,8 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}"> <template slot-scope="{row,$index}">
<el-form-item label-width="0" <el-form-item label-width="0"
:prop="'familyList.'+$index+'.homeUnit'" :prop="'familyList.'+$index+'.homeUnit'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]"> :rules="textRules('工作单位', 200)">
<el-input v-model="row.homeUnit" placeholder="请填写工作单位" clearable></el-input> <el-input v-model="row.homeUnit" maxlength="200" placeholder="请填写工作单位" clearable></el-input>
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
@@ -235,9 +236,9 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}"> <template slot-scope="{row,$index}">
<el-form-item label-width="0" <el-form-item label-width="0"
:prop="'familyList.'+$index+'.yearIncome'" :prop="'familyList.'+$index+'.yearIncome'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]"> :rules="[{validator:validateIncome,required:true,trigger:['change','blur']}]">
<el-input-number v-model="row.yearIncome" placeholder="年收入" <el-input-number v-model="row.yearIncome" placeholder="年收入"
:min="0" :step="1" style="width: 100%" clearable></el-input-number> :min="0" :precision="2" :step="1" style="width: 100%" clearable></el-input-number>
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
@@ -245,8 +246,8 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}"> <template slot-scope="{row,$index}">
<el-form-item label-width="0" <el-form-item label-width="0"
:prop="'familyList.'+$index+'.remarks'" :prop="'familyList.'+$index+'.remarks'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]"> :rules="textRules('备注', 500)">
<el-input v-model="row.remarks" placeholder="请填写备注" clearable></el-input> <el-input v-model="row.remarks" maxlength="500" placeholder="请填写备注" clearable></el-input>
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
@@ -326,22 +327,37 @@ layout("/layouts/platform.html"){
applyCount: '1', applyCount: '1',
}, },
formRules: { formRules: {
proxyUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}], proxyUserName: this.textRules('填写人姓名', 100),
proxyLoginName: [{required: true, message: '必填', trigger: ['blur', 'change']}], proxyLoginName: this.textRules('填写人工号', 100),
sex: [{required: true, message: '请填写性别', trigger: ['blur', 'change']}], mode: [{required: true, type: 'enum', enum: ['1', '2'], message: '请选择申请模式', trigger: ['blur', 'change']}],
userName: [{required: true, message: '请填写受补助人', trigger: ['blur', 'change']}], sex: [{required: true, type: 'enum', enum: ['男', '女'], message: '请选择性别', trigger: ['blur', 'change']}],
unitName: [{required: true, message: '请填写所属单位', trigger: ['blur', 'change']}], userId: [{required: true, message: '请选择受补助人', trigger: ['blur', 'change']}],
unitName: this.textRules('所属单位', 500),
unionName: [{required: true, message: '请填写所属工会', trigger: ['blur', 'change']}], unionName: [{required: true, message: '请填写所属工会', trigger: ['blur', 'change']}],
homeAddress: [{required: true, message: '请填写家庭住址', trigger: ['blur', 'change']}], homeAddress: this.textRules('家庭住址', 200),
mobile: [{required: true, message: '请填写手机号码', trigger: ['blur', 'change']}], mobile: [
idCard: [{required: true, message: '请填写身份证号', trigger: ['blur', 'change']}], {required: true, message: '请填写手机号码', trigger: ['blur', 'change']},
bankCardNum: [{required: true, message: '请填写收款账户卡号', trigger: ['blur', 'change']}], {pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: ['blur', 'change']}
],
idCard: [{validator: (rule, value, callback) => {
const result = this.isValidIdCard(value || '')
callback(result.status === 0 ? new Error(result.msg) : undefined)
}, required: true, trigger: ['blur', 'change']}],
bankCardNum: [{validator: (rule, value, callback) => {
const result = this.validateCard(value || '')
callback(result.status === 0 ? new Error(result.msg) : undefined)
}, required: true, trigger: ['blur', 'change']}],
bankUserName: [{required: true, message: '请填写户名', trigger: ['blur', 'change']}], bankUserName: [{required: true, message: '请填写户名', trigger: ['blur', 'change']}],
bankOfDeposit: [{required: true, message: '请填写开户行', trigger: ['blur', 'change']}], bankOfDeposit: this.textRules('开户行', 30),
position: [{required: true, message: '请填写职务', trigger: ['blur', 'change']}], position: this.textRules('职务', 20),
officialCapacity: [{required: true, message: '请填写职称', trigger: ['blur', 'change']}], officialCapacity: this.textRules('职称', 20),
subsidyStandards: [{required: true, message: '请选择困难类型', trigger: ['blur', 'change']}], homeIncome: [{validator: this.validateIncome, required: true, trigger: ['blur', 'change']}],
reason: [{required: true, message: '请填写申请原因', trigger: ['blur', 'change']}], homeNumber: [{required: true, type: 'integer', min: 1, message: '家庭人数须为大于零的整数', trigger: ['blur', 'change']}],
subsidy: this.textRules('补助类型', 500),
subsidyStandards: this.textRules('困难类型', 500),
applyMoney: [{validator: this.validateMoney, required: true, trigger: ['blur', 'change']}],
reason: this.textRules('生活致困原因', 500),
sign: [{required: true, whitespace: true, message: '请完成签字', trigger: ['blur', 'change']}],
}, },
// 替他人申请相关 // 替他人申请相关
@@ -349,8 +365,21 @@ layout("/layouts/platform.html"){
} }
}, },
methods: { methods: {
textRules(label, max) {
return [{required: true, whitespace: true, max, message: '请填写' + label + ',且不超过' + max + '个字符', trigger: ['blur', 'change']}]
},
validateIncome(rule, value, callback) {
const valid = typeof value === 'number' && Number.isFinite(value) && value >= 0 && /^\d+(\.\d{1,2})?$/.test(String(value))
callback(valid ? undefined : new Error('收入须为不小于零的数字,最多保留两位小数'))
},
validateMoney(rule, value, callback) {
const valid = typeof value === 'number' && Number.isFinite(value) && value > 0 && /^\d+(\.\d{1,2})?$/.test(String(value))
callback(valid ? undefined : new Error('申请金额须大于零,最多保留两位小数'))
},
// 保存 // 保存
onSave() { async onSave() {
const valid = await this.$refs.formRef.validate().catch(() => false)
if (!valid) return
this.$confirm("您确定保存吗?", "提示", { this.$confirm("您确定保存吗?", "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消", cancelButtonText: "取消",
@@ -367,24 +396,13 @@ layout("/layouts/platform.html"){
// 提交 // 提交
async onSubmit() { async onSubmit() {
this.$refs.formRef.validate().then((valid) => { this.$refs.formRef.validate().catch(() => false).then((valid) => {
if (valid) { if (valid) {
this.$confirm("您确定要提交吗?", "提示", { this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消", cancelButtonText: "取消",
type: "warning" type: "warning"
}).then(() => { }).then(() => {
const result = this.isValidIdCard(this.formData.idCard)
if (result.status === 0) {
this.$message.warning(result.msg)
return
}
const bankRes = this.validateCard(this.formData.bankCardNum)
if (bankRes.status === 0) {
this.$message.warning(bankRes.msg)
return;
}
this.$axios.post('/platform/difficultHelp/apply/submit', { this.$axios.post('/platform/difficultHelp/apply/submit', {
data: JSON.stringify(this.formData) data: JSON.stringify(this.formData)
}).then(res => { }).then(res => {
@@ -399,7 +417,7 @@ layout("/layouts/platform.html"){
}, },
async onFinishTask() { async onFinishTask() {
this.$refs.formRef.validate().then((valid) => { this.$refs.formRef.validate().catch(() => false).then((valid) => {
if (valid) { if (valid) {
this.$confirm("您确定要提交吗?", "提示", { this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
@@ -407,17 +425,6 @@ layout("/layouts/platform.html"){
type: "warning" type: "warning"
}).then(() => { }).then(() => {
const result = this.isValidIdCard(this.formData.idCard)
if (result.status === 0) {
this.$message.warning(result.msg)
return
}
const bankRes = this.validateCard(this.formData.bankCardNum)
if (bankRes.status === 0) {
this.$message.warning(bankRes.msg)
return;
}
this.$axios.post('/platform/difficultHelp/apply/submitAgain', { this.$axios.post('/platform/difficultHelp/apply/submitAgain', {
data: JSON.stringify(this.formData), data: JSON.stringify(this.formData),
taskId: GetQueryString("taskId") taskId: GetQueryString("taskId")
@@ -77,7 +77,7 @@ layout("/layouts/platform_h5.html"){
<van-cell title="会长">{{ clubDetail.clubLeader || '--' }}</van-cell> <van-cell title="会长">{{ clubDetail.clubLeader || '--' }}</van-cell>
<van-cell title="秘书长">{{ clubDetail.clubSecretary || '--' }}</van-cell> <van-cell title="秘书长">{{ clubDetail.clubSecretary || '--' }}</van-cell>
<van-cell title="当前人数">{{ clubDetail.currentPeopleNum || 0 }} 人</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.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"><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> <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>
@@ -255,7 +255,7 @@ layout("/layouts/platform_h5.html"){
<van-empty description="暂无数据" v-else></van-empty> <van-empty description="暂无数据" v-else></van-empty>
</van-action-sheet>--> </van-action-sheet>-->
<van-field <!-- <van-field
v-model="formData.certifierUserName" v-model="formData.certifierUserName"
name="certifierUserName" name="certifierUserName"
label="证明人" label="证明人"
@@ -288,7 +288,7 @@ layout("/layouts/platform_h5.html"){
</template> </template>
</div> </div>
<van-empty description="暂无数据" v-else></van-empty> <van-empty description="暂无数据" v-else></van-empty>
</van-action-sheet> </van-action-sheet> -->
<van-field <van-field
v-model="formData.typeName" v-model="formData.typeName"
@@ -322,7 +322,7 @@ layout("/layouts/platform_h5.html"){
name="money" name="money"
label="慰问金额" label="慰问金额"
required required
:rules="[{ required: true }]" :rules="[{ required: true, message: '请填写慰问金额' }, { validator: validateMoney, message: '慰问金额须大于0且不超过10000元,最多两位小数' }]"
placeholder="选择慰问类型自动匹配金额" placeholder="选择慰问类型自动匹配金额"
clickable clickable
type="number" type="number"
@@ -353,17 +353,19 @@ layout("/layouts/platform_h5.html"){
</van-popup> </van-popup>
<van-field <van-field
v-if="!['A', 'D'].includes(formData.typeCode)"
v-model="formData.bankCardNumber" v-model="formData.bankCardNumber"
label="收款账户" label="收款账户"
maxlength="20" maxlength="30"
clearable clearable
name="bankCardNumber" name="bankCardNumber"
placeholder="请填写收款账户" placeholder="请填写银行卡号或对公账户号"
:rules="[{ required: true }]" :rules="[{ required: true, message: '请填写银行卡号或对公账户号' }, { pattern: /^[0-9]{6,30}$/, message: '收款账户须为6至30位数字(银行卡号或对公账户号)' }]"
required required
></van-field> ></van-field>
<van-field <van-field
v-if="!['A', 'D'].includes(formData.typeCode)"
v-model="formData.bankUserName" v-model="formData.bankUserName"
label="户名" label="户名"
placeholder="请填写户名" placeholder="请填写户名"
@@ -373,9 +375,10 @@ layout("/layouts/platform_h5.html"){
name="bankUserName" name="bankUserName"
></van-field> ></van-field>
<van-field <van-field
v-if="!['A', 'D'].includes(formData.typeCode)"
v-model="formData.bankOfDeposit" v-model="formData.bankOfDeposit"
label="开户行" label="开户行"
maxlength="30" maxlength="20"
type="textarea" type="textarea"
rows="4" rows="4"
autosize autosize
@@ -433,8 +436,8 @@ layout("/layouts/platform_h5.html"){
></van-datetime-picker> ></van-datetime-picker>
</van-popup> </van-popup>
<van-field type="digit" label="当年第几次住院" <van-field type="digit" label="当年第几次住院" name="thisYearHospitalizationNum"
:rules="[{ required: true }]" :rules="[{ required: true }, { validator: value => /^[0-9]+$/.test(String(value)) && Number(value) <= 10000, message: '住院次数须为0至10000的整数' }]"
required required
placeholder="请输入当年第几次住院" placeholder="请输入当年第几次住院"
v-model="formData.thisYearHospitalizationNum"></van-field> v-model="formData.thisYearHospitalizationNum"></van-field>
@@ -443,6 +446,7 @@ layout("/layouts/platform_h5.html"){
<van-field <van-field
v-model="formData.remark" v-model="formData.remark"
name="remark" name="remark"
maxlength="500"
label="申请事由" label="申请事由"
type="textarea" type="textarea"
rows="4" rows="4"
@@ -460,15 +464,18 @@ layout("/layouts/platform_h5.html"){
{{ '附件说明:' + chooseType.uploadFileDesc }} {{ '附件说明:' + chooseType.uploadFileDesc }}
</div> </div>
<van-cell-group :border="false"> <van-cell-group :border="false">
<van-field class="direction-column-field" name="avatar" label=""> <van-field class="direction-column-field" name="files" label="附件" required
:rules="[{ validator: () => Array.isArray(formData.files) && formData.files.length > 0 && formData.files.length <= 5, message: '请上传1至5个附件' }]">
<template #input> <template #input>
<h5-file-upload <h5-file-upload
slot="input" slot="input"
:value.sync="formData.files" :value.sync="formData.files"
:upload_number="5" :upload_number="5"
upload_text="上传附件"
upload_result_category="array" upload_result_category="array"
complete_result complete_result
></h5-file-upload> ></h5-file-upload>
<div class="condolence-card__desc">最多上传 5 个文件</div>
</template> </template>
</van-field> </van-field>
</van-cell-group> </van-cell-group>
@@ -477,7 +484,8 @@ layout("/layouts/platform_h5.html"){
<section class="form-section"> <section class="form-section">
<h2 class="condolence-card__title">签字确认</h2> <h2 class="condolence-card__title">签字确认</h2>
<van-cell-group :border="false"> <van-cell-group :border="false">
<van-field class="direction-column-field" name="signature" label=""> <van-field class="direction-column-field" name="signature" label="" :value="formData.signature"
:rules="[{ required: true, message: '请填写签字' }]">
<template #input> <template #input>
<h5-signature v-model="formData.signature" slot="input"></h5-signature> <h5-signature v-model="formData.signature" slot="input"></h5-signature>
</template> </template>
@@ -532,6 +540,9 @@ layout("/layouts/platform_h5.html"){
} }
}, },
methods: { methods: {
validateMoney(value) {
return /^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000
},
// 选择出院时间 // 选择出院时间
showLeaveHospitalTimePickerClick() { showLeaveHospitalTimePickerClick() {
if (this.formData.leaveHospitalTime) { if (this.formData.leaveHospitalTime) {
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
<h2 class="condolence-info-card__title"> <h2 class="condolence-info-card__title">
审批签字<span class="condolence-audit-card__required">*</span> 审批签字<span class="condolence-audit-card__required">*</span>
</h2> </h2>
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]"> <van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template> <template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
</van-field> </van-field>
</van-form> </van-form>
@@ -158,19 +158,44 @@
} }
.condolence-info-long-cell { .condolence-info-long-cell {
display: flex;
align-items: flex-start;
}
.condolence-info-card .condolence-attachments .empty-container {
height: auto;
justify-content: flex-start;
color: inherit;
}
.condolence-info-card .condolence-attachments .empty-text {
font-size: inherit;
line-height: inherit;
}
.condolence-info-opinion > :first-child {
margin-top: 0;
}
.condolence-info-opinion > :last-child {
margin-bottom: 0;
}
.condolence-signature-image,
.condolence-approval-signature-image {
display: block; display: block;
} width: 100%;
.condolence-info-long-cell .van-cell__title {
width: auto;
max-width: 100%; max-width: 100%;
height: 80px;
} }
.condolence-info-long-cell .van-cell__value { .condolence-signature-image .van-image__img,
margin-top: 7px; .condolence-approval-signature-image {
object-fit: contain;
object-position: left center;
} }
.condolence-signature-image { .condolence-info-opinion img {
display: block;
max-width: 100%; max-width: 100%;
height: auto;
} }
@@ -25,13 +25,13 @@ const condolenceInfo = {
<van-cell title="申请人单位">{{ viewData.applyUnitName || '--' }}</van-cell> <van-cell title="申请人单位">{{ viewData.applyUnitName || '--' }}</van-cell>
<van-cell title="申请人工会">{{ viewData.applyUnionName || '--' }}</van-cell> <van-cell title="申请人工会">{{ viewData.applyUnionName || '--' }}</van-cell>
<van-cell title="慰问对象">{{ viewData.helpUserName || '--' }}</van-cell> <van-cell title="慰问对象">{{ viewData.helpUserName || '--' }}</van-cell>
<van-cell title="证明人">{{ viewData.certifierUserName || '--' }}</van-cell> <!-- <van-cell title="证明人">{{ viewData.certifierUserName || '--' }}</van-cell> -->
<van-cell title="慰问类型">{{ viewData.typeName || '--' }}</van-cell> <van-cell title="慰问类型">{{ viewData.typeName || '--' }}</van-cell>
<van-cell title="慰问金额">{{ viewData.money || '--' }}</van-cell> <van-cell title="慰问金额">{{ viewData.money || '--' }}</van-cell>
<van-cell title="慰问时间">{{ viewData.occurTime || '--' }}</van-cell> <van-cell title="慰问时间">{{ viewData.occurTime || '--' }}</van-cell>
<van-cell title="收款账户">{{ viewData.bankCardNumber || '--' }}</van-cell> <van-cell title="收款账户">{{ viewData.bankCardNumber || '--' }}</van-cell>
<van-cell title="户名">{{ viewData.bankUserName || '--' }}</van-cell> <van-cell title="户名">{{ viewData.bankUserName || '--' }}</van-cell>
<van-cell class="condolence-info-long-cell" title="开户行"> <van-cell title="开户行">
{{ viewData.bankOfDeposit || '暂无' }} {{ viewData.bankOfDeposit || '暂无' }}
</van-cell> </van-cell>
<template v-if="['2'].includes(viewData.typeCode)"> <template v-if="['2'].includes(viewData.typeCode)">
@@ -39,17 +39,18 @@ const condolenceInfo = {
<van-cell title="出院时间">{{ viewData.leaveHospitalTime || '--' }}</van-cell> <van-cell title="出院时间">{{ viewData.leaveHospitalTime || '--' }}</van-cell>
<van-cell title="当年第几次住院(次)">{{ viewData.thisYearHospitalizationNum || '--' }}</van-cell> <van-cell title="当年第几次住院(次)">{{ viewData.thisYearHospitalizationNum || '--' }}</van-cell>
</template> </template>
<van-cell class="condolence-info-long-cell" title="申请事由"> <van-cell title="申请事由">
{{ viewData.remark || '暂无' }} {{ viewData.remark || '暂无' }}
</van-cell> </van-cell>
<van-cell class="condolence-info-long-cell" title="附件"> <van-cell class="condolence-info-long-cell" title="附件">
<file-preview :files="viewData.files" complete_result></file-preview> <file-preview class="condolence-attachments" :files="viewData.files" complete_result></file-preview>
</van-cell> </van-cell>
<van-cell class="condolence-info-long-cell" title="签字"> <van-cell class="condolence-info-long-cell" title="签字">
<van-image <van-image
v-if="viewData.signature" v-if="viewData.signature"
:src="viewData.signature" :src="viewData.signature"
class="condolence-signature-image" class="condolence-signature-image"
fit="contain"
></van-image> ></van-image>
<span v-else>暂无签字</span> <span v-else>暂无签字</span>
</van-cell> </van-cell>
@@ -86,10 +87,10 @@ const condolenceInfo = {
></dict-tag> ></dict-tag>
</van-cell> </van-cell>
<van-cell v-if="task.taskFormData && task.taskFormData.sign" title="审批签字"> <van-cell v-if="task.taskFormData && task.taskFormData.sign" title="审批签字">
<img :src="task.taskFormData.sign" style="max-width: 100%; height: 80px; object-fit: contain"> <img :src="task.taskFormData.sign" class="condolence-approval-signature-image">
</van-cell> </van-cell>
<van-cell class="condolence-info-long-cell" title="办理意见"> <van-cell title="办理意见">
<div v-html="task.taskFormData && task.taskFormData.tf_opinion || '暂无'"></div> <div class="condolence-info-opinion" v-html="task.taskFormData && task.taskFormData.tf_opinion || '暂无'"></div>
</van-cell> </van-cell>
</van-cell-group> </van-cell-group>
</section> </section>
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
<h2 class="condolence-info-card__title"> <h2 class="condolence-info-card__title">
审批签字<span class="condolence-audit-card__required">*</span> 审批签字<span class="condolence-audit-card__required">*</span>
</h2> </h2>
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]"> <van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template> <template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
</van-field> </van-field>
</van-form> </van-form>
@@ -223,6 +223,23 @@ layout("/layouts/platform_h5.html"){
}, },
onReady() { onReady() {
this.queryCondolenceType() this.queryCondolenceType()
this.openTodoTask()
},
openTodoTask() {
const taskId = GetQueryString("taskId")
const bizId = GetQueryString("bizId")
if (!taskId || !bizId) return
this.$axios.post("/flow/common/taskInfo", {taskId}).then((res) => {
if (res.code !== 0 || !res.data) return
const task = res.data
if (task.taskName !== "cbd41337-3c87-4959-9027-809df5c7ef5e") return
const row = {id: bizId, taskId: task.id, curTaskName: task.displayName}
if (task.taskState === 10 && task.executable && GetQueryString("tab") === "todo") {
this.onApproval(row)
} else {
this.onView(row)
}
})
}, },
onView(row) { onView(row) {
this.$set(this, "showApprovalForm", false) this.$set(this, "showApprovalForm", false)
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
<h2 class="condolence-info-card__title"> <h2 class="condolence-info-card__title">
审批签字<span class="condolence-audit-card__required">*</span> 审批签字<span class="condolence-audit-card__required">*</span>
</h2> </h2>
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]"> <van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template> <template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
</van-field> </van-field>
</van-form> </van-form>
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
<h2 class="condolence-info-card__title"> <h2 class="condolence-info-card__title">
审批签字<span class="condolence-audit-card__required">*</span> 审批签字<span class="condolence-audit-card__required">*</span>
</h2> </h2>
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]"> <van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template> <template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
</van-field> </van-field>
</van-form> </van-form>
@@ -281,11 +281,13 @@ const todo = {
// 处理任务 // 处理任务
onView(task) { onView(task) {
const {taskId, taskKey, instanceId, businessNo, formKey, h5FormKey} = task; const {taskId, taskKey, instanceId, businessNo, formKey, h5FormKey} = task;
if (!h5FormKey) { const mobileFormKey = h5FormKey || (taskKey === "cbd41337-3c87-4959-9027-809df5c7ef5e"
? "/platform/condolence/fourthApproval/h5" : "");
if (!mobileFormKey) {
this.$toast("当前流程暂不支持手机端审核,请前往PC端页面审核!"); this.$toast("当前流程暂不支持手机端审核,请前往PC端页面审核!");
return; return;
} }
this.$pjaxReplace(h5FormKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab) this.$pjaxReplace(mobileFormKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
}, },
// 根据当前列表类型统一展示流程状态,发起列表保留流程实例的实际状态。 // 根据当前列表类型统一展示流程状态,发起列表保留流程实例的实际状态。