Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceec8eb095 | ||
|
|
2a305a3f23 | ||
|
|
29260423d5 |
@@ -1,10 +1,16 @@
|
||||
package com.budwk.app.base.utils;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import com.aspose.cells.FontConfigs;
|
||||
import com.aspose.cells.PdfSaveOptions;
|
||||
import com.aspose.cells.Workbook;
|
||||
import com.aspose.slides.FontsLoader;
|
||||
import com.aspose.slides.LoadOptions;
|
||||
import com.aspose.slides.Presentation;
|
||||
import com.aspose.words.Document;
|
||||
import com.aspose.words.FontSettings;
|
||||
import com.aspose.words.FontSourceBase;
|
||||
import com.aspose.words.MemoryFontSource;
|
||||
import com.aspose.words.SaveFormat;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -12,6 +18,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 文档转换
|
||||
@@ -19,6 +27,81 @@ import java.io.IOException;
|
||||
@Slf4j
|
||||
public class OfficePlusUtil {
|
||||
|
||||
private static final String DEFAULT_FONT_NAME = "Noto Sans SC";
|
||||
private static final String DEFAULT_FONT_RESOURCE = "/fonts/NotoSansSC-VF.ttf";
|
||||
private static final Object FONT_CONFIG_LOCK = new Object();
|
||||
private static volatile boolean fontConfigured;
|
||||
|
||||
/**
|
||||
* 注册系统内置字体,并将其作为 Office 文档缺失字体或缺失字形时的默认回退字体。
|
||||
* 原文档字体在服务器可用时仍优先使用,只有找不到原字体时才使用内置字体。
|
||||
*/
|
||||
private static void configureDefaultFont() {
|
||||
if (fontConfigured) {
|
||||
return;
|
||||
}
|
||||
synchronized (FONT_CONFIG_LOCK) {
|
||||
if (fontConfigured) {
|
||||
return;
|
||||
}
|
||||
byte[] fontData = loadDefaultFontData();
|
||||
configureWordFont(fontData);
|
||||
configureExcelFont(fontData);
|
||||
FontsLoader.loadExternalFont(fontData);
|
||||
fontConfigured = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 classpath 读取随应用打包的字体,避免依赖线上服务器安装字体。
|
||||
*
|
||||
* @return 默认字体的二进制数据
|
||||
*/
|
||||
private static byte[] loadDefaultFontData() {
|
||||
try (InputStream inputStream = OfficePlusUtil.class.getResourceAsStream(DEFAULT_FONT_RESOURCE)) {
|
||||
if (inputStream == null) {
|
||||
throw new BaseException("系统内置默认字体不存在");
|
||||
}
|
||||
return inputStream.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
log.error("读取系统内置默认字体失败", e);
|
||||
throw new BaseException("读取系统内置默认字体失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 Word 转换保留已有字体源并追加内置字体,同时配置缺失字体和缺失字形回退。
|
||||
*
|
||||
* @param fontData 默认字体的二进制数据
|
||||
*/
|
||||
private static void configureWordFont(byte[] fontData) {
|
||||
FontSettings fontSettings = FontSettings.getDefaultInstance();
|
||||
FontSourceBase[] currentSources = fontSettings.getFontsSources();
|
||||
FontSourceBase[] fontSources = Arrays.copyOf(currentSources, currentSources.length + 1);
|
||||
fontSources[currentSources.length] = new MemoryFontSource(fontData);
|
||||
fontSettings.setFontsSources(fontSources);
|
||||
fontSettings.getSubstitutionSettings().getDefaultFontSubstitution().setDefaultFontName(DEFAULT_FONT_NAME);
|
||||
try {
|
||||
fontSettings.getFallbackSettings().loadNotoFallbackSettings();
|
||||
} catch (Exception e) {
|
||||
log.error("配置 Word 默认字体回退规则失败", e);
|
||||
throw new BaseException("配置 Word 默认字体失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 Excel 转换追加内置字体,并将其设置为找不到原字体时的默认字体。
|
||||
*
|
||||
* @param fontData 默认字体的二进制数据
|
||||
*/
|
||||
private static void configureExcelFont(byte[] fontData) {
|
||||
com.aspose.cells.FontSourceBase[] currentSources = FontConfigs.getFontSources();
|
||||
com.aspose.cells.FontSourceBase[] fontSources = Arrays.copyOf(currentSources, currentSources.length + 1);
|
||||
fontSources[currentSources.length] = new com.aspose.cells.MemoryFontSource(fontData);
|
||||
FontConfigs.setFontSources(fontSources);
|
||||
FontConfigs.setDefaultFontName(DEFAULT_FONT_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换
|
||||
*
|
||||
@@ -47,7 +130,11 @@ public class OfficePlusUtil {
|
||||
public static void presentationConvertPdf(String sourcePath, String targetPath) {
|
||||
Presentation presentation = null;
|
||||
try {
|
||||
presentation = new Presentation(sourcePath);
|
||||
configureDefaultFont();
|
||||
LoadOptions loadOptions = new LoadOptions();
|
||||
loadOptions.setDefaultRegularFont(DEFAULT_FONT_NAME);
|
||||
loadOptions.setDefaultAsianFont(DEFAULT_FONT_NAME);
|
||||
presentation = new Presentation(sourcePath, loadOptions);
|
||||
presentation.save(targetPath, com.aspose.slides.SaveFormat.Pdf);
|
||||
} catch (Exception e) {
|
||||
log.info("转换pdf报错", e);
|
||||
@@ -68,10 +155,12 @@ public class OfficePlusUtil {
|
||||
public static void excelConvertPdf(String sourcePath, String targetPath) {
|
||||
FileOutputStream fileOS = null;
|
||||
try {
|
||||
configureDefaultFont();
|
||||
Workbook wb = new Workbook(sourcePath);
|
||||
fileOS = new FileOutputStream(targetPath);
|
||||
PdfSaveOptions pdfSaveOptions = new PdfSaveOptions();
|
||||
pdfSaveOptions.setOnePagePerSheet(true);
|
||||
pdfSaveOptions.setDefaultFont(DEFAULT_FONT_NAME);
|
||||
// for (int i = 0; i < 3; i++) {
|
||||
// wb.getWorksheets().get(i).getHorizontalPageBreaks().clear();
|
||||
// wb.getWorksheets().get(i).getVerticalPageBreaks().clear();
|
||||
@@ -105,6 +194,7 @@ public class OfficePlusUtil {
|
||||
public static void wordConvertPdf(String sourcePath, String targetPath) {
|
||||
FileOutputStream os = null;
|
||||
try {
|
||||
configureDefaultFont();
|
||||
File file = new File(targetPath);
|
||||
os = new FileOutputStream(file);
|
||||
Document doc = new Document(sourcePath);
|
||||
|
||||
@@ -6,6 +6,8 @@ import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.sys.models.Sys_file;
|
||||
import com.budwk.app.sys.models.Sys_office_template;
|
||||
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
import com.deepoove.poi.config.Configure;
|
||||
import com.deepoove.poi.data.ByteArrayPictureRenderData;
|
||||
import com.deepoove.poi.data.PictureRenderData;
|
||||
import com.deepoove.poi.data.PictureType;
|
||||
@@ -18,6 +20,12 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -66,6 +74,88 @@ public class SysOfficeTemplateUtil {
|
||||
return new ByteArrayInputStream(fileBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一渲染 DOC/DOCX 模板,保留调用方的循环表格等策略;模板输入流由调用方关闭。
|
||||
*/
|
||||
public byte[] renderTemplate(InputStream input, Object data, Configure config, boolean pdf, String context) throws Exception {
|
||||
if (input == null) throw new BaseException("模板文件不存在");
|
||||
byte[] templateBytes = input.readAllBytes();
|
||||
try (ByteArrayInputStream formatInput = new ByteArrayInputStream(templateBytes)) {
|
||||
if (com.aspose.words.FileFormatUtil.detectFileFormat(formatInput).getLoadFormat() != com.aspose.words.LoadFormat.DOCX) {
|
||||
try (ByteArrayInputStream source = new ByteArrayInputStream(templateBytes);
|
||||
ByteArrayOutputStream converted = new ByteArrayOutputStream()) {
|
||||
new com.aspose.words.Document(source).save(converted, com.aspose.words.SaveFormat.DOCX);
|
||||
templateBytes = converted.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
byte[] wordBytes;
|
||||
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateBytes);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
XWPFTemplate template = XWPFTemplate.compile(templateInput, config == null ? Configure.builder().build() : config)) {
|
||||
template.render(data).write(output);
|
||||
wordBytes = output.toByteArray();
|
||||
} catch (Exception e) {
|
||||
log.error("[Office模板渲染] Word模板渲染失败,context={}", context, e);
|
||||
throw e;
|
||||
}
|
||||
log.info("[Office模板渲染] Word渲染完成,context={}, templateBytes={}, outputBytes={}", context, templateBytes.length, wordBytes.length);
|
||||
if (!pdf) return wordBytes;
|
||||
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(wordBytes);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
com.aspose.words.Document document = new com.aspose.words.Document(wordInput);
|
||||
configureDocumentFonts(document, context);
|
||||
com.aspose.words.PdfSaveOptions options = new com.aspose.words.PdfSaveOptions();
|
||||
options.setFontEmbeddingMode(com.aspose.words.PdfFontEmbeddingMode.EMBED_ALL);
|
||||
document.save(output, options);
|
||||
log.info("[Office模板渲染] PDF转换完成,context={}, outputBytes={}", context, output.size());
|
||||
return output.toByteArray();
|
||||
} catch (Exception e) {
|
||||
log.error("[Office模板渲染] PDF转换失败,context={}", context, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void configureDocumentFonts(com.aspose.words.Document document, String context) throws Exception {
|
||||
com.aspose.words.FontSettings settings = new com.aspose.words.FontSettings();
|
||||
List<com.aspose.words.FontSourceBase> sources = new ArrayList<>(List.of(settings.getFontsSources()));
|
||||
Set<String> folders = new LinkedHashSet<>();
|
||||
String windowsDir = System.getenv("WINDIR");
|
||||
if (StrUtil.isNotBlank(windowsDir)) folders.add(new File(windowsDir, "Fonts").getPath());
|
||||
String localAppData = System.getenv("LOCALAPPDATA");
|
||||
if (StrUtil.isNotBlank(localAppData)) folders.add(new File(localAppData, "Microsoft/Windows/Fonts").getPath());
|
||||
folders.add(new File(System.getProperty("user.dir"), "fonts").getPath());
|
||||
for (String folder : folders) {
|
||||
File directory = new File(folder);
|
||||
log.info("[Office模板渲染] 字体目录,context={}, path={}, readable={}", context, folder, directory.isDirectory() && directory.canRead());
|
||||
if (directory.isDirectory() && directory.canRead()) sources.add(new com.aspose.words.FolderFontSource(folder, true));
|
||||
}
|
||||
settings.setFontsSources(sources.toArray(new com.aspose.words.FontSourceBase[0]));
|
||||
Set<String> availableFonts = new LinkedHashSet<>();
|
||||
for (com.aspose.words.FontSourceBase source : sources) {
|
||||
for (com.aspose.words.PhysicalFontInfo font : source.getAvailableFonts()) availableFonts.add(font.getFontFamilyName());
|
||||
}
|
||||
String chineseFont = null;
|
||||
for (String candidate : List.of("SimSun", "宋体", "Microsoft YaHei", "微软雅黑", "DengXian", "等线", "SimHei", "黑体", "FangSong", "仿宋", "Noto Serif CJK SC", "Noto Sans CJK SC")) {
|
||||
if (availableFonts.contains(candidate)) {
|
||||
chineseFont = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
log.info("[Office模板渲染] 中文字体选择,context={}, fontCount={}, chineseFont={}", context, availableFonts.size(), chineseFont);
|
||||
if (chineseFont != null) {
|
||||
settings.getSubstitutionSettings().getDefaultFontSubstitution().setDefaultFontName(chineseFont);
|
||||
for (String name : List.of("宋体", "SimSun", "新宋体", "NSimSun", "黑体", "SimHei", "仿宋", "FangSong", "楷体", "KaiTi", "微软雅黑", "Microsoft YaHei")) {
|
||||
if (!availableFonts.contains(name)) settings.getSubstitutionSettings().getTableSubstitution().setSubstitutes(name, chineseFont);
|
||||
}
|
||||
} else {
|
||||
log.warn("[Office模板渲染] 未发现常用中文字体,context={}, availableFonts={}", context, availableFonts);
|
||||
}
|
||||
settings.getFallbackSettings().loadMsOfficeFallbackSettings();
|
||||
document.setFontSettings(settings);
|
||||
document.setWarningCallback(warning -> log.warn("[Office模板渲染] 字体或排版警告,context={}, type={}, detail={}", context, warning.getWarningType(), warning.getDescription()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 签字url转换为poi的图片对象
|
||||
*
|
||||
|
||||
@@ -1,27 +1,80 @@
|
||||
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.ProcessEventListener;
|
||||
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.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.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.json.Json;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
/** 普通节点、首次会签及转办任务统一从开始事件进入待办消息 service。 */
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流程任务开始监听器
|
||||
*/
|
||||
@IocBean
|
||||
@Slf4j
|
||||
public class ProcessTaskStartEventListener implements ProcessEventListener {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private FlowTaskMessageService flowTaskMessageService;
|
||||
private GlobalMessageSendService globalMessageSendService;
|
||||
@Inject
|
||||
private DifficultHelpCommonService difficultHelpCommonService;
|
||||
|
||||
@Override
|
||||
public void onEvent(ProcessEvent event) {
|
||||
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) {
|
||||
ProcessTask task = dao.fetch(ProcessTask.class, event.getSourceId());
|
||||
flowTaskMessageService.notifyTask(task, false);
|
||||
Long sourceId = event.getSourceId();
|
||||
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.ProcessTaskActor;
|
||||
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.ProcessInstanceService;
|
||||
import com.budwk.app.flow.service.ProcessTaskService;
|
||||
@@ -61,9 +60,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
@Inject
|
||||
private SchoolOaTodoService schoolOaTodoService;
|
||||
|
||||
@Inject
|
||||
private FlowTaskMessageService flowTaskMessageService;
|
||||
|
||||
@Override
|
||||
public boolean save(ProcessTask param) {
|
||||
param.setId(null);
|
||||
@@ -296,8 +292,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor));
|
||||
// 并行加签绕过普通节点创建入口,需要为新增的个人任务同步待办。
|
||||
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()));
|
||||
// 驳回创建了新的任务 ID,按新任务推送,避免复用历史待办。
|
||||
schoolOaTodoService.createTodo(task);
|
||||
// 驳回重建不发布开始事件;退回申请节点时也通知申请人重新办理。
|
||||
flowTaskMessageService.notifyTask(task, true);
|
||||
return task;
|
||||
}
|
||||
|
||||
@@ -449,12 +441,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class);
|
||||
// 更新会签变量到流程实例参数中
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,12 +84,10 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
|
||||
task.getId(), task.getProcessInstanceId(), task.getTaskState());
|
||||
}
|
||||
|
||||
/**
|
||||
* OA 待办接口联调尚未完成,暂时在代码中固定关闭,避免远端异常阻断内部 WF 流转。
|
||||
* 不受 AppSchoolOa 或 enabled 配置影响;恢复对接时需显式恢复配置开关逻辑。
|
||||
*/
|
||||
/** 系统参数优先于文件配置,只有明确为 true 才启用;未配置不访问数据库或 OA。 */
|
||||
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) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.budwk.app.sys.services.impl;
|
||||
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.sys.services.SysHomeNewsService;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
@@ -34,7 +35,7 @@ public class SysHomeNewsServiceImpl implements SysHomeNewsService {
|
||||
/**
|
||||
* 无需参数,一次抓取首页中的三个栏目,并保留原有一天有效期的缓存写入。
|
||||
*
|
||||
* @return label/value 栏目列表,文章包含 text/time/href;无文章的栏目保留空列表,抓取失败返回空栏目列表。
|
||||
* @return label/value 栏目列表,文章包含 text/time/href;正常空栏目返回空列表。
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@@ -50,8 +51,7 @@ public class SysHomeNewsServiceImpl implements SysHomeNewsService {
|
||||
return news;
|
||||
} catch (IOException e) {
|
||||
log.warn("获取重庆工贸职业技术学院工会首页失败", e);
|
||||
// 学校网站连接失败或超时时返回空列表,让首页展示空状态,不向用户报错,也不覆盖已有缓存。
|
||||
return new ArrayList<>();
|
||||
throw new BaseException("学校工会网站暂时无法访问,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+94
-21
@@ -4,12 +4,14 @@ package com.budwk.app.zhgh.activity.sports.service.impl;
|
||||
import cn.afterturn.easypoi.excel.ExcelExportUtil;
|
||||
import cn.afterturn.easypoi.excel.entity.ExportParams;
|
||||
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||
import com.budwk.app.base.utils.DateUtil;
|
||||
import com.budwk.app.sys.models.Sys_home_activity;
|
||||
import com.budwk.app.sys.models.Sys_home_template;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.activity.basic.models.ActivityEvent;
|
||||
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
|
||||
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
|
||||
import com.budwk.app.zhgh.activity.culture.models.ActivityTissue;
|
||||
@@ -28,6 +30,7 @@ import org.nutz.dao.entity.Record;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.json.Json;
|
||||
import org.nutz.lang.Lang;
|
||||
import org.nutz.lang.Strings;
|
||||
|
||||
@@ -55,6 +58,7 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public String doAdd(ActivitySchool activitySchool, ActivitySchoolEvent[] events) {
|
||||
events = validateAndApplySingleEventLimits(events);
|
||||
ActivityBasicUnit activityBasicUnit = dao().fetch(ActivityBasicUnit.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
|
||||
|
||||
if (activitySchool.getActivityLevel() == 40002) {
|
||||
@@ -101,6 +105,7 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void doEdit(ActivitySchool activitySchool, ActivitySchoolEvent[] events) {
|
||||
events = validateAndApplySingleEventLimits(events);
|
||||
ActivitySchool school = dao().fetch(ActivitySchool.class, activitySchool.getId());
|
||||
if (school.getActivityLevel() == 40002) {
|
||||
school.setBelongUnionId(SecurityUtil.getUnionId());
|
||||
@@ -161,6 +166,56 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
|
||||
dao().insertOrUpdate(sysHomeActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单项人数限制,并在具体单项未设置限制时应用“所有单项”的默认值。
|
||||
*
|
||||
* @param events 页面提交的项目配置,允许包含 eventId 为 allItems 的默认配置
|
||||
* @return 已移除 allItems 临时配置、可直接持久化的项目配置数组
|
||||
*/
|
||||
private ActivitySchoolEvent[] validateAndApplySingleEventLimits(ActivitySchoolEvent[] events) {
|
||||
if (events == null || events.length == 0) {
|
||||
return new ActivitySchoolEvent[0];
|
||||
}
|
||||
|
||||
ActivitySchoolEvent allItems = Arrays.stream(events)
|
||||
.filter(event -> event != null && "allItems".equals(event.getEventId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
List<ActivitySchoolEvent> result = new ArrayList<>();
|
||||
for (ActivitySchoolEvent event : events) {
|
||||
if (event == null || "allItems".equals(event.getEventId())) {
|
||||
continue;
|
||||
}
|
||||
ActivityEvent activityEvent = dao().fetch(ActivityEvent.class, event.getEventId());
|
||||
if (activityEvent != null && "1".equals(activityEvent.getProjectType())) {
|
||||
applySingleEventDefaultLimit(event, allItems);
|
||||
}
|
||||
result.add(event);
|
||||
}
|
||||
return result.toArray(new ActivitySchoolEvent[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单项的两个限制必须同时设置;均未设置或均为0时继承“所有单项”的对应值。
|
||||
*/
|
||||
private void applySingleEventDefaultLimit(ActivitySchoolEvent event, ActivitySchoolEvent allItems) {
|
||||
Integer athletesMaxNum = event.getAthletesMaxNum();
|
||||
Integer restrictRegNumber = event.getRestrictRegNumber();
|
||||
if ((athletesMaxNum == null) != (restrictRegNumber == null)) {
|
||||
throw new BaseException("每队(项)运动员最多人数和每人限报项目必须同时设置");
|
||||
}
|
||||
|
||||
boolean useDefault = athletesMaxNum == null || (athletesMaxNum == 0 && restrictRegNumber == 0);
|
||||
if (!useDefault) {
|
||||
return;
|
||||
}
|
||||
if (allItems == null || allItems.getAthletesMaxNum() == null || allItems.getRestrictRegNumber() == null) {
|
||||
throw new BaseException("请先设置所有单项的运动员最多人数和每人限报项目");
|
||||
}
|
||||
event.setAthletesMaxNum(allItems.getAthletesMaxNum());
|
||||
event.setRestrictRegNumber(allItems.getRestrictRegNumber());
|
||||
}
|
||||
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
private void addTeam(String schoolId, ActivitySchoolEvent[] events) {
|
||||
if (events.length > 0) {
|
||||
@@ -271,33 +326,19 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
|
||||
WHERE
|
||||
app.activityId = @id
|
||||
and app.status = 2
|
||||
$cnd
|
||||
and (@filterUnion = 0 or app.activityUnionId = @unionId)
|
||||
ORDER BY
|
||||
ev.allName DESC,
|
||||
u.unionname DESC,
|
||||
app.`team` asc
|
||||
""").setParam("id", id)
|
||||
.setVar("cnd", "");
|
||||
if (Strings.isNotBlank(unionId)) {
|
||||
sql.setVar("cnd", "and u.unionid = @unionId");
|
||||
sql.setParam("unionId", unionId);
|
||||
}
|
||||
""").setParam("id", id);
|
||||
// 参数必须声明在原始SQL中;setVar追加的字符串不会再次解析其中的@参数。
|
||||
// 未选择分工会时导出整个活动,选择时按报名记录中的活动分工会过滤。
|
||||
sql.setParam("filterUnion", Strings.isNotBlank(unionId) ? 1 : 0);
|
||||
sql.setParam("unionId", Strings.isNotBlank(unionId) ? unionId : "");
|
||||
List<Record> list = list(sql);
|
||||
String activityName;
|
||||
if (Lang.isNotEmpty(list)) {
|
||||
activityName = list.get(0).getString("activityName");
|
||||
} else {
|
||||
activityName = "暂无人员";
|
||||
}
|
||||
|
||||
List<ActivitySchoolApply> excels = new ArrayList<>();
|
||||
|
||||
list.forEach(z -> {
|
||||
ActivitySchoolApply schoolApply = z.toPojo(ActivitySchoolApply.class);
|
||||
String s = schoolApply.getIdentity().stream().map(personType::getName).toList().toString();
|
||||
schoolApply.setSf(s);
|
||||
excels.add(schoolApply);
|
||||
});
|
||||
list.forEach(record -> excels.add(toActivitySchoolApplyExport(record)));
|
||||
|
||||
ExportParams exportParams = new ExportParams();
|
||||
exportParams.setType(ExcelType.XSSF);
|
||||
@@ -305,10 +346,42 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
|
||||
CommonDownloadUtil.download(getUnionApplyExportFileName(unionId), workbook, response);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new BaseException("导出报名信息失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将原生SQL查询结果转换为报名信息导出对象。
|
||||
* identity 是数据库JSON文本,需要显式转换为身份编码集合,避免 Record.toPojo 自动转换失败导致整表无数据。
|
||||
*
|
||||
* @param record 导出SQL返回的一行报名数据
|
||||
* @return EasyPoi 使用的 ActivitySchoolApply 导出对象
|
||||
*/
|
||||
private ActivitySchoolApply toActivitySchoolApplyExport(Record record) {
|
||||
ActivitySchoolApply schoolApply = new ActivitySchoolApply();
|
||||
schoolApply.setUnionname(record.getString("unionname"));
|
||||
schoolApply.setTeam(record.getString("team"));
|
||||
schoolApply.setAllName(record.getString("allName"));
|
||||
schoolApply.setLoginname(record.getString("loginname"));
|
||||
schoolApply.setUsername(record.getString("username"));
|
||||
schoolApply.setMobile(record.getString("mobile"));
|
||||
schoolApply.setIdCard(record.getString("idCard"));
|
||||
schoolApply.setProfessionalLevel(record.getString("professionalLevel"));
|
||||
schoolApply.setSex(record.getString("sex"));
|
||||
|
||||
String identityJson = record.getString("identity");
|
||||
List<String> identities = Strings.isBlank(identityJson)
|
||||
? new ArrayList<>()
|
||||
: Json.fromJsonAsList(String.class, identityJson);
|
||||
String identityName = identities.stream()
|
||||
.map(personType::getName)
|
||||
.filter(Strings::isNotBlank)
|
||||
.collect(Collectors.joining("、"));
|
||||
schoolApply.setSf(identityName);
|
||||
return schoolApply;
|
||||
}
|
||||
|
||||
private String getUnionApplyExportFileName(String unionId) {
|
||||
String unionName = "分工会";
|
||||
if (Strings.isNotBlank(unionId)) {
|
||||
|
||||
+13
-5
@@ -17,7 +17,6 @@ import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
|
||||
import com.budwk.app.flow.service.FlowCommonService;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||
import com.budwk.app.zhgh.club.model.SysClub;
|
||||
import com.budwk.app.zhgh.club.model.SysClubExamineRegister;
|
||||
import com.budwk.app.zhgh.club.service.SysClubExamineService;
|
||||
@@ -119,10 +118,7 @@ public class ClubExamineApplyController {
|
||||
if(StrUtil.isBlank(clubId)) {
|
||||
return Result.error("社团信息为空,请核查");
|
||||
}
|
||||
int count = dao.count(ClubUser.class, Cnd.where("clubId", "=", clubId));
|
||||
SysClub club = dao.fetch(SysClub.class, clubId);
|
||||
int money = count * (club.getDue() != null ? Integer.parseInt(club.getDue()) : 0);
|
||||
return Result.success(money);
|
||||
return Result.success(sysClubExamineService.getClubMemberMoney(clubId));
|
||||
}
|
||||
|
||||
@At
|
||||
@@ -175,6 +171,10 @@ public class ClubExamineApplyController {
|
||||
public Result save(@Param("data") SysClubExamineRegister examineRegister,
|
||||
@Param("incomeDetailed") String incomeDetailed,
|
||||
@Param("incomeCensus") String incomeCensus) {
|
||||
String validateMessage = sysClubExamineService.validateActivityRequired(examineRegister);
|
||||
if (validateMessage != null) {
|
||||
return Result.error(validateMessage);
|
||||
}
|
||||
if (StrUtil.isBlank(examineRegister.getId())) {
|
||||
sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus);
|
||||
} else {
|
||||
@@ -190,6 +190,10 @@ public class ClubExamineApplyController {
|
||||
public Result submit(@Param("data") SysClubExamineRegister examineRegister,
|
||||
@Param("incomeDetailed") String incomeDetailed,
|
||||
@Param("incomeCensus") String incomeCensus) {
|
||||
String validateMessage = sysClubExamineService.validateActivityRequired(examineRegister);
|
||||
if (validateMessage != null) {
|
||||
return Result.error(validateMessage);
|
||||
}
|
||||
SysClubExamineRegister reg;
|
||||
if (StrUtil.isBlank(examineRegister.getId())) {
|
||||
reg = sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus);
|
||||
@@ -221,6 +225,10 @@ public class ClubExamineApplyController {
|
||||
@Param("incomeDetailed") String incomeDetailed,
|
||||
@Param("incomeCensus") String incomeCensus,
|
||||
@Param("taskId") Long taskId) {
|
||||
String validateMessage = sysClubExamineService.validateActivityRequired(examineRegister);
|
||||
if (validateMessage != null) {
|
||||
return Result.error(validateMessage);
|
||||
}
|
||||
if (StrUtil.isBlank(examineRegister.getId())) {
|
||||
sysClubExamineService.doAdd(examineRegister, incomeDetailed, incomeCensus);
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ public class ClubExamineMineController {
|
||||
//nutMap.setv("schoolName", Globals.MyConfig.getString("AppName"));
|
||||
nutMap.setv("clubName", register.getClubName());
|
||||
nutMap.setv("create_time", register.getFoundTime());
|
||||
nutMap.setv("dues_standard", register.getDue());
|
||||
nutMap.setv("dues_standard", register.getDue() == null ? "--" : register.getDue() + "元/年");
|
||||
// 生成材料中的年度与列表展示的登记年度保持一致。
|
||||
nutMap.setv("year", register.getYear());
|
||||
nutMap.setv("registerDate", register.getRegisterDate());
|
||||
|
||||
@@ -91,8 +91,8 @@ public class SysClub extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Comment("会费标准")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||
private String due;
|
||||
@ColDefine(type = ColType.DOUBLE, width = 10, precision = 2)
|
||||
private Double due;
|
||||
|
||||
@Column
|
||||
@Comment("附件")
|
||||
|
||||
@@ -57,8 +57,8 @@ public class SysClubExamineRegister extends BaseModel {
|
||||
|
||||
@Column
|
||||
@Comment("会费标准")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String due;
|
||||
@ColDefine(type = ColType.DOUBLE, width = 10, precision = 2)
|
||||
private Double due;
|
||||
|
||||
@Column
|
||||
@Comment("年度活动情况")
|
||||
|
||||
@@ -24,6 +24,21 @@ public interface SysClubExamineService extends BaseService<SysClubExamineRegiste
|
||||
|
||||
List<NutMap> getJgUser(@Valid String clubId);
|
||||
|
||||
/**
|
||||
* 计算指定社团的年度会费收入。
|
||||
*
|
||||
* @param clubId 社团ID,用于查询当前会员数量和社团会费标准
|
||||
* @return 会员数量乘以会费标准后的金额;社团或会费标准为空时返回0
|
||||
*/
|
||||
Double getClubMemberMoney(@Valid String clubId);
|
||||
|
||||
/**
|
||||
* 校验考核登记的两组活动明细。
|
||||
* @param examineRegister 页面data参数对应的登记实体,yearActivityList为本年度活动,plans为下一年度计划
|
||||
* @return String类型;通过返回null,否则返回缺失的明细行号及字段提示
|
||||
*/
|
||||
String validateActivityRequired(SysClubExamineRegister examineRegister);
|
||||
|
||||
SysClubExamineRegister doAdd(SysClubExamineRegister examineRegister, String incomeDetailed, String incomeCensus);
|
||||
|
||||
SysClubExamineRegister doEdit(SysClubExamineRegister examineRegister, String incomeDetailed, String incomeCensus);
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||
import com.budwk.app.zhgh.club.model.SysClub;
|
||||
import com.budwk.app.zhgh.club.model.SysClubExamineRegister;
|
||||
import com.budwk.app.zhgh.club.model.SysClubExamineRegisterDetailed;
|
||||
@@ -50,6 +51,15 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineReg
|
||||
super(dao);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getClubMemberMoney(String clubId) {
|
||||
int memberCount = dao().count(ClubUser.class, Cnd.where(ClubUser::getClubId, "=", clubId));
|
||||
SysClub club = dao().fetch(SysClub.class, clubId);
|
||||
// 社团或会费标准为空时按0计算,保证考核登记初始化能够正常完成。
|
||||
Double due = club == null || club.getDue() == null ? 0D : club.getDue();
|
||||
return memberCount * due;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NutMap> getClubUserNum(String clubId, Integer year) {
|
||||
Date yearStart = DateUtil.parse(year + "-01-01 00:00:00");
|
||||
@@ -345,6 +355,40 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineReg
|
||||
return listMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 两组明细均至少填写一条;本年度主办单位和获奖情况保持选填。
|
||||
* @param examineRegister data参数绑定的登记实体,包含yearActivityList和plans两组JSON明细
|
||||
* @return 校验通过返回null,否则返回首个缺失字段的行号和名称
|
||||
*/
|
||||
@Override
|
||||
public String validateActivityRequired(SysClubExamineRegister examineRegister) {
|
||||
if (examineRegister == null) {
|
||||
return "考核登记信息不能为空";
|
||||
}
|
||||
String message = validateActivityRows(examineRegister.getYearActivityList(), "本年度活动开展情况",
|
||||
new String[]{"activityTime", "activityName", "activityAddress", "joinNum"},
|
||||
new String[]{"活动时间", "活动主题/赛事名称", "活动地点", "参加人数"});
|
||||
return message != null ? message : validateActivityRows(examineRegister.getPlans(), "下一年度活动计划",
|
||||
new String[]{"activityName", "activityUnit", "activityAddress"},
|
||||
new String[]{"活动主题/赛事名称", "活动/赛事主办单位", "活动地点"});
|
||||
}
|
||||
|
||||
/** 检查空列表、空行及纯空白字段;数值0按已填写处理,不扩大为人数范围校验。 */
|
||||
private String validateActivityRows(List<JSONObject> rows, String label, String[] fields, String[] names) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return label + "至少填写一条";
|
||||
}
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
JSONObject row = rows.get(i);
|
||||
for (int j = 0; j < fields.length; j++) {
|
||||
if (row == null || StrUtil.isBlank(row.getStr(fields[j]))) {
|
||||
return label + "第" + (i + 1) + "条:" + names[j] + "不能为空";
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysClubExamineRegister doAdd(SysClubExamineRegister examineRegister, String incomeDetailed, String incomeCensus) {
|
||||
// JSON转换为实体列表(空值判断避免转换异常)
|
||||
|
||||
+1
-2
@@ -193,8 +193,7 @@ public class GlobalMessageSendService {
|
||||
try {
|
||||
strategy.send(request.getTitle(), request.getContent(), request.getType(),
|
||||
request.getReceiverIds(), request.getConfig());
|
||||
// 策略可能因开关关闭而跳过,实际发送结果由策略日志记录。
|
||||
log.info("异步消息策略执行完成,渠道:{}", strategy.getChannel().getValue());
|
||||
log.info("异步发送消息成功,渠道:{}", strategy.getChannel().getValue());
|
||||
} catch (Exception e) {
|
||||
log.error("异步发送消息失败,渠道:{},错误:{}", strategy.getChannel().getValue(), e.getMessage(), e);
|
||||
}
|
||||
|
||||
-72
@@ -1,23 +1,16 @@
|
||||
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 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.zhgh.dayofficework.message.enums.GlobalMessageChannel;
|
||||
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
|
||||
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 org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -31,8 +24,6 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private SmsService smsService;
|
||||
@Inject
|
||||
private WxWorkMessageService wxWorkMessageService;
|
||||
|
||||
@Override
|
||||
public GlobalMessageChannel getChannel() {
|
||||
@@ -45,13 +36,7 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
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 {
|
||||
log.info("开始发送微信消息,标题:{},接收人数量:{}", title, receiverIds.size());
|
||||
|
||||
@@ -67,12 +52,7 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
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 {
|
||||
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 后取工号发送;
|
||||
|
||||
+5
-23
@@ -28,7 +28,6 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
|
||||
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
||||
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
import com.deepoove.poi.data.PictureRenderData;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
@@ -48,8 +47,6 @@ import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
@@ -187,27 +184,12 @@ public class CondolenceMineController {
|
||||
log.info("[职工慰问导出] 渲染前图片字段,applyId={}, instanceId={}, field={}, valueType={}", id, instance.getId(), field, value == null ? "null" : value.getClass().getName());
|
||||
}
|
||||
String fileName = "重庆工贸职业技术学院职工慰问申请表_" + StrUtil.nullToEmpty(condolence.getHelpUserName());
|
||||
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc");
|
||||
ByteArrayOutputStream templateOutput = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc")) {
|
||||
if (input == null) throw new BaseException("职工慰问申请表模板不存在");
|
||||
com.aspose.words.Document document = new com.aspose.words.Document(input);
|
||||
document.save(templateOutput, com.aspose.words.SaveFormat.DOCX);
|
||||
log.info("[职工慰问导出] 模板转换完成,applyId={}, templateBytes={}", id, templateOutput.size());
|
||||
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateOutput.toByteArray())) {
|
||||
XWPFTemplate.compile(templateInput).render(docData).writeAndClose(output);
|
||||
}
|
||||
log.info("[职工慰问导出] Word渲染完成,applyId={}, outputBytes={}", id, output.size());
|
||||
if (Boolean.TRUE.equals(print)) {
|
||||
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(output.toByteArray());
|
||||
ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream()) {
|
||||
new com.aspose.words.Document(wordInput).save(pdfOutput, com.aspose.words.SaveFormat.PDF);
|
||||
log.info("[职工慰问导出] PDF转换完成,applyId={}, outputBytes={}", id, pdfOutput.size());
|
||||
CommonDownloadUtil.download(fileName + ".pdf", pdfOutput.toByteArray(), response);
|
||||
}
|
||||
} else {
|
||||
CommonDownloadUtil.download(fileName + ".docx", output.toByteArray(), response);
|
||||
}
|
||||
boolean pdf = Boolean.TRUE.equals(print);
|
||||
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, null, pdf, "condolence:" + id);
|
||||
log.info("[职工慰问导出] 渲染完成,applyId={}, format={}, outputBytes={}", id, pdf ? "PDF" : "Word", bytes.length);
|
||||
CommonDownloadUtil.download(fileName + (pdf ? ".pdf" : ".docx"), bytes, response);
|
||||
} catch (Exception e) {
|
||||
log.error("[职工慰问导出] 模板渲染或文件输出失败,applyId={}, instanceId={}, format={}", id, instance.getId(), Boolean.TRUE.equals(print) ? "PDF" : "Word", e);
|
||||
throw e;
|
||||
|
||||
+16
-39
@@ -9,7 +9,6 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
@@ -18,7 +17,6 @@ import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||
import com.budwk.app.base.utils.MoneyUtil;
|
||||
import com.budwk.app.base.utils.OfficePlusUtil;
|
||||
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
|
||||
import com.budwk.app.flow.engine.FlowEngine;
|
||||
import com.budwk.app.flow.entity.ProcessInstance;
|
||||
@@ -39,7 +37,6 @@ import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommon
|
||||
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
|
||||
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo;
|
||||
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
import com.deepoove.poi.config.Configure;
|
||||
import com.deepoove.poi.data.PictureRenderData;
|
||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||
@@ -74,8 +71,6 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@@ -319,9 +314,9 @@ public class DifficultHelpReadingController {
|
||||
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||
Configure config = Configure.builder().bind("familyList", policy).build();
|
||||
|
||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("difficulty_apply"), config)
|
||||
.render(docData)
|
||||
.write(zipOutputStream);
|
||||
try (InputStream input = sysOfficeTemplateUtil.getTemplate("difficulty_apply")) {
|
||||
zipOutputStream.write(sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:batch:" + record.getString("id")));
|
||||
}
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
zipOutputStream.flush();
|
||||
@@ -379,17 +374,18 @@ public class DifficultHelpReadingController {
|
||||
String[] taskKeys = {"36ebafda-44c2-4bd3-bd7f-10839a575a0d", "715ece82-4ae3-433c-ad18-ed875f45d1e3", "e405f96c-5fc0-4f63-b323-e69dfe853253"};
|
||||
String[] fields = {"firstApproval", "secondApproval", "thirdApproval"};
|
||||
for (String field : fields) {
|
||||
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
|
||||
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
|
||||
}
|
||||
ProcessInstance instance = dao.fetch(ProcessInstance.class, Cnd.where("businessNo", "=", businessId).desc("id"));
|
||||
if (instance == null) return;
|
||||
List<ProcessTask> tasks = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
|
||||
for (ProcessTask task : tasks) {
|
||||
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
|
||||
if ("startTask".equals(task.getTaskName()) || (taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
|
||||
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
|
||||
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
|
||||
// 重新申请后,仅导出本轮审批,避免混入上一次退回或撤销前的意见。
|
||||
for (String field : fields) {
|
||||
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
|
||||
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -453,22 +449,9 @@ public class DifficultHelpReadingController {
|
||||
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
|
||||
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf";
|
||||
|
||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
XWPFTemplate.compile(getApplyTemplate(templateName),config)
|
||||
.render(docData)
|
||||
.writeAndClose(byteArrayOutputStream);
|
||||
byte[] bytes = byteArrayOutputStream.toByteArray();
|
||||
File sourceFile = File.createTempFile("file_convert_origin", ".docx");
|
||||
Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE);
|
||||
|
||||
File pdfFile = File.createTempFile("file_convert", ".pdf");
|
||||
OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath());
|
||||
CommonDownloadUtil.download(fileName, IoUtil.readBytes(FileUtil.getInputStream(pdfFile)), response);
|
||||
// 删除临时文件
|
||||
FileUtil.del(sourceFile);
|
||||
FileUtil.del(pdfFile.toPath());
|
||||
} catch (IOException e) {
|
||||
log.error("困难慰问申请表导出PDF失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
|
||||
try (InputStream input = getApplyTemplate(templateName)) {
|
||||
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, true, "difficulthelp:" + difficultHelp.getId());
|
||||
CommonDownloadUtil.download(fileName, bytes, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,13 +461,9 @@ public class DifficultHelpReadingController {
|
||||
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
|
||||
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx";
|
||||
|
||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
XWPFTemplate.compile(getApplyTemplate(templateName),config)
|
||||
.render(docData)
|
||||
.writeAndClose(byteArrayOutputStream);
|
||||
CommonDownloadUtil.download(fileName, byteArrayOutputStream.toByteArray(), response);
|
||||
} catch (IOException e) {
|
||||
log.error("困难慰问申请表导出Word失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
|
||||
try (InputStream input = getApplyTemplate(templateName)) {
|
||||
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:" + difficultHelp.getId());
|
||||
CommonDownloadUtil.download(fileName, bytes, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,15 +541,13 @@ public class DifficultHelpReadingController {
|
||||
throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage());
|
||||
}
|
||||
} else if ("docx".equals(flag)) {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
try (InputStream input = sysOfficeTemplateUtil.getTemplate(resource)) {
|
||||
Configure config = Configure.builder()
|
||||
.bind("da", new LoopRowTableRenderPolicy())
|
||||
.build();
|
||||
|
||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(resource), config)
|
||||
.render(result)
|
||||
.writeAndClose(out);
|
||||
CommonDownloadUtil.download(fileName + ".docx", out.toByteArray(), response);
|
||||
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, result, config, false, "difficulthelp:summary:" + resource);
|
||||
CommonDownloadUtil.download(fileName + ".docx", bytes, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -1,6 +1,7 @@
|
||||
package com.budwk.app.zhgh.staffbenefit.difficulthelp.service;
|
||||
|
||||
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.vo.DifficultHelpPageParam;
|
||||
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
|
||||
@@ -21,6 +22,14 @@ public interface DifficultHelpCommonService extends BaseService<DifficultHelpInf
|
||||
|
||||
void validateApplyUser(DifficultHelpInfo difficultHelpInfo);
|
||||
|
||||
/**
|
||||
* 困难帮扶进入校工会办公室主任审批时,通知本次任务的实际参与人。
|
||||
* @param task WF 已创建且参与人已落库的任务;仅 KNBF 办公室审批待办触发,
|
||||
* 接收人使用任务参与人的 actorAccount(系统 loginname)
|
||||
* @return 无返回值;开关关闭或非目标节点不发送,发送失败记录日志而不阻断流程
|
||||
*/
|
||||
void notifyOfficeApproval(ProcessTask task);
|
||||
|
||||
/**
|
||||
* 导入实际金额数据处理
|
||||
* @param file 文件
|
||||
|
||||
+59
@@ -6,6 +6,12 @@ import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
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.SecurityUtil;
|
||||
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.random.R;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.log.Log;
|
||||
import org.nutz.log.Logs;
|
||||
import org.nutz.mvc.upload.TempFile;
|
||||
|
||||
import java.util.*;
|
||||
@@ -46,6 +54,8 @@ import java.util.stream.IntStream;
|
||||
*/
|
||||
@IocBean(args = {"refer:dao"})
|
||||
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) {
|
||||
super(dao);
|
||||
@@ -53,6 +63,55 @@ public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHel
|
||||
|
||||
@Inject
|
||||
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
|
||||
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; }
|
||||
BIN
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>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="setDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="doSetActivityUser" v-loading="settingLoading">确定</el-button>
|
||||
<el-button type="primary" :disabled="settingLoading" @click="doSetActivityUser">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
@@ -646,7 +646,7 @@ module.exports = {
|
||||
doSearch(showFullLoading = false) {
|
||||
this.tableKey = new Date().getTime()
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageData(showFullLoading === true)
|
||||
return this.pageData(showFullLoading === true)
|
||||
},
|
||||
async pageData(showFullLoading = false) {
|
||||
this.tableLoading = true
|
||||
@@ -679,43 +679,74 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
},
|
||||
async doSetActivityUser() {
|
||||
const valid = await this.$refs.setForm.validate()
|
||||
if (!valid) {
|
||||
doSetActivityUser() {
|
||||
// 方法级状态保护与整页遮罩共同阻止慢请求期间的重复确认。
|
||||
if (this.settingLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.formData.groupType !== 2 && this.tableData.length === 0) {
|
||||
this.$message.warning("请先指定筛选条件!")
|
||||
return
|
||||
}
|
||||
|
||||
const confirm = await this.$confirm("是否将符合搜索条件的用户设为活动人员?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
})
|
||||
|
||||
if (confirm === "confirm") {
|
||||
this.settingLoading = true
|
||||
const pageForm = this.normalizePageForm(this.pageForm)
|
||||
Object.assign(pageForm, this.formData)
|
||||
const resp = await $.post("/platform/activity/basic/scope/doSetActivityUser", {
|
||||
data: JSON.stringify(pageForm)
|
||||
})
|
||||
if (resp.code === 0) {
|
||||
this.setDialogVisible = false
|
||||
await this.getActivityGroup()
|
||||
this.doSearch()
|
||||
this.$message.success(resp.msg)
|
||||
this.formData = this.getDefaultFormData()
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
this.$refs.setForm.validate((valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
this.settingLoading = false
|
||||
this.$emit("update:group_id", resp.data)
|
||||
this.$emit("group_change")
|
||||
}
|
||||
|
||||
if (this.formData.groupType !== 2 && this.tableData.length === 0) {
|
||||
this.$message.warning("请先指定筛选条件!")
|
||||
return
|
||||
}
|
||||
|
||||
this.$confirm("是否将符合搜索条件的用户设为活动人员?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
if (this.settingLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
this.settingLoading = true
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: "正在设置活动人员,请稍候...",
|
||||
spinner: "el-icon-loading",
|
||||
background: "rgba(255, 255, 255, 0.65)"
|
||||
})
|
||||
const pageForm = this.normalizePageForm(this.pageForm)
|
||||
Object.assign(pageForm, this.formData)
|
||||
|
||||
$.post("/platform/activity/basic/scope/doSetActivityUser", {
|
||||
data: JSON.stringify(pageForm)
|
||||
}).then((resp) => {
|
||||
if (resp.code !== 0) {
|
||||
// 将后端业务错误转入统一失败分支,直接展示接口返回的真实原因。
|
||||
throw new Error(resp.msg || "设置活动人员失败")
|
||||
}
|
||||
|
||||
this.setDialogVisible = false
|
||||
// 设置成功后等待分组和人员列表刷新完成,再解除整页操作锁定。
|
||||
return this.getActivityGroup()
|
||||
.then(() => this.doSearch())
|
||||
.then(() => {
|
||||
this.$message.success(resp.msg)
|
||||
this.formData = this.getDefaultFormData()
|
||||
this.$emit("update:group_id", resp.data)
|
||||
this.$emit("group_change")
|
||||
})
|
||||
}).then(null, (error) => {
|
||||
// 兼容业务错误、权限异常和网络异常,避免请求失败后页面持续加载。
|
||||
const response = error && error.responseJSON ? error.responseJSON : null
|
||||
const errorMessage = response && response.msg
|
||||
? response.msg
|
||||
: error && error.message
|
||||
? error.message
|
||||
: "设置活动人员失败"
|
||||
this.$message.error(errorMessage)
|
||||
}).always(() => {
|
||||
this.settingLoading = false
|
||||
loading.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
async getActivityGroup() {
|
||||
const resp = await $.get("/platform/activity/basic/scope/getActivityUserScopeGroup")
|
||||
|
||||
@@ -52,8 +52,7 @@ module.exports = {
|
||||
*/
|
||||
init() {
|
||||
this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id
|
||||
// 打印当前二维码的签字入口,便于从控制台复制地址进行签字测试。
|
||||
console.info("signatureAddress:", this.signatureAddress)
|
||||
console.log(this.signatureAddress)
|
||||
return this.$axios.post("/platform/signature/get")
|
||||
.then((res) => {
|
||||
if (this._isBeingDestroyed || this._isDestroyed) {
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
<!-- 引入 form-create 和 designer -->
|
||||
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/form-create/index.umd.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/form-create/index.umd.js?v=20261030"></script>
|
||||
|
||||
<!-- swiper -->
|
||||
<script src="${base!}/assets/platform/plugins/swiper/swiper-bundle.js"></script>
|
||||
|
||||
@@ -10,23 +10,22 @@ const act = {
|
||||
<div ref="activitySwiper" class="swiper activity-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<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">
|
||||
<el-image :src="activity.cover" fit="cover" :alt="activity.name">
|
||||
<div slot="error" class="portal-cover-empty"><i class="fa fa-picture-o" aria-hidden="true"></i><span>暂无图片</span></div>
|
||||
</el-image>
|
||||
<span class="status" :class="{'status-ended': getStatusText(activity) === '已结束', 'status-upcoming': getStatusText(activity) === '未开始'}">
|
||||
<img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
|
||||
alt="">
|
||||
<span class="status">
|
||||
{{getStatusText(activity)}}
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="title" :title="activity.name">
|
||||
<h4 class="title">
|
||||
{{ activity.name}}
|
||||
</h4>
|
||||
<!--报名时间-->
|
||||
<div class="time">
|
||||
<p :title="[activity.startDate, activity.endDate].filter(Boolean).join(' 至 ')">
|
||||
<p>
|
||||
<i class="fa fa-clock-o"></i>
|
||||
{{ formatActivityDate(activity.startDate) }} 至 {{ formatActivityDate(activity.endDate) }}
|
||||
时间:{{ activity.startDate }}至{{ activity.endDate }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,21 +34,20 @@ const act = {
|
||||
</div>
|
||||
|
||||
<!-- 导航按钮 -->
|
||||
<button type="button" ref="nextButton" class="swiper-button-next" aria-label="下一组活动"></button>
|
||||
<button type="button" ref="prevButton" class="swiper-button-prev" aria-label="上一组活动"></button>
|
||||
<div ref="nextButton" class="swiper-button-next"></div>
|
||||
<div ref="prevButton" class="swiper-button-prev"></div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div ref="pagination" class="swiper-pagination"></div>
|
||||
</div>
|
||||
|
||||
<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 class="subtext">敬请关注后续通知</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
@@ -59,57 +57,19 @@ const act = {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.$axios.post("/platform/home/listHomeActivity").then((res) => {
|
||||
if (res.code === 0) {
|
||||
// for (let i = 0; i < 10; i++) {
|
||||
// this.actList = this.actList.concat(res.data)
|
||||
// }
|
||||
this.actList = res.data || []
|
||||
this.actList = res.data
|
||||
// 数据加载完成后初始化Swiper
|
||||
this.$nextTick(() => {
|
||||
this.initSwiper()
|
||||
})
|
||||
} else {
|
||||
this.$emit('browse-state', {enabled: false, label: '活动加载失败'})
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$emit('browse-state', {enabled: false, label: '活动加载失败'})
|
||||
})
|
||||
},
|
||||
/*初始化Swiper*/
|
||||
@@ -120,7 +80,6 @@ const act = {
|
||||
}
|
||||
const swiperElement = this.$refs.activitySwiper
|
||||
if (!swiperElement) {
|
||||
this.syncBrowseState()
|
||||
return
|
||||
}
|
||||
// 仅操作当前组件的轮播节点,避免页面其他轮播或尺寸观察器影响卡片宽度。
|
||||
@@ -130,22 +89,13 @@ const act = {
|
||||
centeredSlides: false,
|
||||
loop: false,
|
||||
watchOverflow: true,
|
||||
// 双栏中的活动按容器宽度排布,避免用整个窗口宽度挤出过多卡片。
|
||||
breakpointsBase: 'container',
|
||||
observer: false,
|
||||
observeParents: false,
|
||||
// 页面内容加载后可能出现滚动条,需重新计算列宽,避免末张封面及圆角被裁切。
|
||||
resizeObserver: true,
|
||||
roundLengths: true,
|
||||
on: {
|
||||
init: () => this.$nextTick(() => this.updateNavigationPosition()),
|
||||
resize: () => this.$nextTick(() => this.updateNavigationPosition()),
|
||||
slidesUpdated: () => this.$nextTick(() => this.updateNavigationPosition()),
|
||||
},
|
||||
resizeObserver: false,
|
||||
// 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。
|
||||
mousewheel: {
|
||||
enabled: true,
|
||||
forceToAxis: true,
|
||||
forceToAxis: false,
|
||||
releaseOnEdges: true,
|
||||
sensitivity: 0.8,
|
||||
},
|
||||
@@ -158,15 +108,17 @@ const act = {
|
||||
type: 'progressbar',
|
||||
},
|
||||
breakpoints: {
|
||||
480: {
|
||||
768: {
|
||||
slidesPerView: 2,
|
||||
},
|
||||
740: {
|
||||
slidesPerView: 3,
|
||||
1024: {
|
||||
slidesPerView: 4,
|
||||
},
|
||||
1200: {
|
||||
slidesPerView: 5,
|
||||
}
|
||||
}
|
||||
})
|
||||
this.syncBrowseState()
|
||||
},
|
||||
|
||||
// 活动状态
|
||||
@@ -187,7 +139,7 @@ const act = {
|
||||
}
|
||||
if (act.url === "/platform/activity/apply"
|
||||
&& !this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN", "BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_WENTI_SPORTS"])) {
|
||||
return this.$message.warning("PC端暂未开放报名,请前往移动端报名!")
|
||||
return this.$message.warning("温馨提示:您没有参加该活动的权限!")
|
||||
|
||||
}
|
||||
window.open(act.url)
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
const entry = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="entry-wrapper">
|
||||
<div class="portal-entry-header">
|
||||
<div class="portal-entry-tabs" role="tablist" aria-label="推荐入口">
|
||||
<button v-for="section in entrySections" :key="section.key"
|
||||
type="button" role="tab" :aria-selected="activeSection === section.key"
|
||||
:class="{active: activeSection === section.key}" @click="$set(this, 'activeSection', section.key)">{{ section.title }}</button>
|
||||
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key">
|
||||
<div class="entry-section-header">
|
||||
<div class="entry-section-title">
|
||||
<i :class="section.icon"></i>
|
||||
<span>{{ section.title }}</span>
|
||||
<em>/Applications</em>
|
||||
</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="app-grid" tabindex="0" :aria-label="section.title + ',可横向滚动浏览'">
|
||||
<button type="button" class="app-item"
|
||||
<div class="app-grid">
|
||||
<div class="app-item"
|
||||
v-for="(item, index) in section.list"
|
||||
:key="item.id || index"
|
||||
@click="openService(item, section.key)">
|
||||
@@ -21,8 +20,8 @@ const entry = {
|
||||
<img v-if="item.picIcon" class="app-icon-img" :src="item.picIcon" alt="" />
|
||||
<i v-else class="fa fa-cube app-icon-fallback"></i>
|
||||
</div>
|
||||
<div class="app-name" :title="item.name">{{ item.name }}</div>
|
||||
</button>
|
||||
<div class="app-name">{{ item.name }}</div>
|
||||
</div>
|
||||
<el-empty class="app-empty"
|
||||
v-if="!section.list || section.list.length === 0"
|
||||
description="暂无数据"
|
||||
@@ -31,12 +30,10 @@ const entry = {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
activeSection: 'serv',
|
||||
hasMore: true,
|
||||
entries: {
|
||||
serv: [],
|
||||
@@ -60,8 +57,8 @@ const entry = {
|
||||
this.getFavorite()
|
||||
},
|
||||
methods: {
|
||||
// platform 固定传 PC;Result.data 为当前用户有权限的推荐服务数组。
|
||||
getRecommendService() {
|
||||
// 查询推荐服务
|
||||
async getRecommendService() {
|
||||
this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => {
|
||||
if (res.code === 0) {
|
||||
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 => {
|
||||
if (res.code === 0) {
|
||||
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 => {
|
||||
if (res.code === 0) {
|
||||
this.entries.fav = res.data
|
||||
@@ -130,6 +127,11 @@ const entry = {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
/* 临时屏蔽“我的收藏”分组,保留原节点和数据逻辑便于恢复 */
|
||||
.entry-section-fav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.entry-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -116,67 +116,107 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 最新活动的作用域样式会在组件挂载后注入,首屏先固定布局和图片尺寸,避免刷新时放大闪烁。 */
|
||||
#v4-home-app .act-card .activity-section-wrapper,
|
||||
#v4-home-app .act-card .section-act,
|
||||
#v4-home-app .act-card .activity-swiper {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#v4-home-app .act-card .activity-swiper-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 54px 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#v4-home-app .act-card .activity-swiper .swiper-slide {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
#v4-home-app .act-card .activity-swiper .img-box {
|
||||
width: 100%;
|
||||
height: 128px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#v4-home-app .act-card .activity-swiper .img-box img {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: 100% !important;
|
||||
max-height: 128px !important;
|
||||
object-fit: fill !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#v4-home-app .act-card .activity-swiper .swiper-slide {
|
||||
width: calc((100% - 18px) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
#v4-home-app .act-card .activity-swiper .swiper-slide {
|
||||
width: calc((100% - 54px) / 4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
#v4-home-app .act-card .activity-swiper .swiper-slide {
|
||||
width: calc((100% - 72px) / 5);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/css/home-portal.css?v=20261028-2" />
|
||||
|
||||
<!-- 登录资料仅在 HTML 属性中输出并转义;放在 Vue 挂载区域外,避免名称被当作模板表达式解析。 -->
|
||||
<div id="v4-home-profile-data" hidden
|
||||
data-username="${@auth.getPrincipalProperty('username'),escape}"
|
||||
data-loginname="${@auth.getPrincipalProperty('loginname'),escape}"
|
||||
<!--#if(!isEmpty(@auth.getPrincipalProperty('unit'))){#-->
|
||||
data-unit-name="${@auth.getPrincipalProperty('unit').getName(),escape}"
|
||||
<!--#}#-->
|
||||
<!--#if(!isEmpty(@auth.getPrincipalProperty('union'))){#-->
|
||||
data-union-name="${@auth.getPrincipalProperty('union').getName(),escape}"
|
||||
<!--#}#-->
|
||||
></div>
|
||||
|
||||
<div class="v4-container home-portal" id="v4-home-app">
|
||||
<div class="v4-container" id="v4-home-app">
|
||||
<!-- 首页banner -->
|
||||
<div class="portal-top">
|
||||
<div class="portal-hero">
|
||||
<!-- 使用参考项目的本地背景资源,保留横幅文字及渐变遮罩。 -->
|
||||
<img class="portal-hero-background" src="${base!}/assets/platform/img/home/service-portal-banner-campus-v4.png" alt="" />
|
||||
<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 class="section-banner">
|
||||
<div class="banner-img">
|
||||
<img :src="homeBannerList[0] || ''" alt=""/>
|
||||
<stats></stats>
|
||||
</div>
|
||||
</div>
|
||||
<user class="portal-member" :profile-only="true" :profile="memberProfile"></user>
|
||||
</div>
|
||||
|
||||
<div class="portal-content">
|
||||
<div class="" style="padding-bottom: 20px">
|
||||
<user></user>
|
||||
|
||||
<div class="home-grid">
|
||||
<div class="entry-card">
|
||||
<entry></entry>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="portal-columns">
|
||||
<div class="home-section portal-panel portal-activities">
|
||||
<div class="home-grid">
|
||||
<div class="home-section">
|
||||
<div class="home-section-header">
|
||||
<div class="home-section-title">
|
||||
<i class="fa fa-calendar"></i>
|
||||
<span>最新活动</span>
|
||||
</div>
|
||||
<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>
|
||||
<em>/Activities</em>
|
||||
</div>
|
||||
</div>
|
||||
<div class="act-card">
|
||||
<act ref="homeActivities" @browse-state="updateActivityBrowse"></act>
|
||||
<act></act>
|
||||
</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>
|
||||
|
||||
<work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
|
||||
|
||||
<jcdt :list="websiteNews"></jcdt>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -193,17 +233,9 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
new Vue({
|
||||
el: '#v4-home-app',
|
||||
data(){
|
||||
// 浏览器读取属性时还原 HTML 转义;未配置单位或工会时使用空字符串,不访问空对象。
|
||||
const profileData = document.getElementById('v4-home-profile-data').dataset
|
||||
return{
|
||||
memberProfile: {
|
||||
username: profileData.username || '',
|
||||
loginname: profileData.loginname || '',
|
||||
unitName: profileData.unitName || '',
|
||||
unionName: profileData.unionName || ''
|
||||
},
|
||||
websiteNews: [],
|
||||
activityBrowse: {enabled: false, label: '活动加载中'}
|
||||
homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -218,14 +250,10 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
this.getWebSiteNews()
|
||||
},
|
||||
methods: {
|
||||
// state 由活动组件发布,enabled 表示可以翻页,label 说明加载或全部展示状态。
|
||||
updateActivityBrowse(state) {
|
||||
this.$set(this, 'activityBrowse', state)
|
||||
},
|
||||
getWebSiteNews(){
|
||||
this.$axios.post('/platform/home/getNews').then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'websiteNews', res.data || [])
|
||||
this.websiteNews = res.data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,72 +2,44 @@ const jcdt = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="jcdt-wrapper">
|
||||
<div class="jcdt-container">
|
||||
<div class="portal-news-header">
|
||||
<el-dropdown @command="selectSection" trigger="click">
|
||||
<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>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-for="(section, index) in list" :key="index" :command="index">{{ section.label }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<div v-if="pageCount > 1" class="portal-news-pager" aria-label="动态分页">
|
||||
<span class="portal-news-count">共 {{ currentNews.value.length }} 条</span>
|
||||
<button type="button" :disabled="pageNumber === 1" title="上一页动态" aria-label="上一页动态" @click="changePage(-1)"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
|
||||
<span class="portal-news-page" aria-live="polite">{{ pageNumber }} / {{ pageCount }}</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 class="portal-news-list">
|
||||
<button type="button" class="portal-news-row" v-for="(item, index) in visibleNews" :key="index" @click="onLink(item)">
|
||||
<time v-if="item.time" :title="item.time">{{ item.time.slice(5) }}</time>
|
||||
<span :title="item.text">{{ item.text }}</span>
|
||||
</button>
|
||||
<div v-if="!currentNews.value.length" class="portal-empty">暂无动态</div>
|
||||
</div>
|
||||
<el-carousel height="360px" :interval="10000" indicator-position="none">
|
||||
<el-carousel-item v-for="item,index in list" :key="index">
|
||||
<div class="jcdt-title">
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
<div class="jcdt-content">
|
||||
<div class="jcdt-list">
|
||||
<div class="jcdt-item" v-for="o,i in item.value" :key="i" @click="onLink(o)">
|
||||
<div class="date" v-if="o.time">
|
||||
<i class="el-icon-alarm-clock"></i>
|
||||
<span>{{o.time}}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div class="title">{{o.text}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {activeIndex: 0, pageNumber: 1, pageSize: 3}
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
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)
|
||||
default: []
|
||||
}
|
||||
},
|
||||
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) {
|
||||
// 完整新闻地址直接打开,相对地址仍以重庆工贸工会网站为基准。
|
||||
// 接口返回文章完整地址;相对地址按当前学校工会域名解析。
|
||||
const url = this.isHttpOrHttps(item.href) ? item.href : new URL(item.href, "https://gh.cqgmy.edu.cn/").href
|
||||
window.open(url)
|
||||
},
|
||||
@@ -76,15 +48,6 @@ const jcdt = {
|
||||
}
|
||||
},
|
||||
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 {
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
|
||||
@@ -5,22 +5,21 @@ const workTemplate = {
|
||||
<div class="work-template-title">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
<span>工作模板</span>
|
||||
<em>/Templates</em>
|
||||
</div>
|
||||
<button type="button" class="portal-more" @click="browseTemplates">更多模板 <i class="fa fa-arrow-right" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="work-template-card">
|
||||
<div ref="templateGrid" v-if="templateList && templateList.length > 0" class="work-template-grid">
|
||||
<button type="button" class="work-template-item"
|
||||
<div v-if="templateList && templateList.length > 0" class="work-template-grid">
|
||||
<div class="work-template-item"
|
||||
v-for="template in templateList"
|
||||
:key="template.id"
|
||||
@click="go(template)">
|
||||
<div class="work-template-icon">
|
||||
<el-image :src="template.templateIcon || template.cover" fit="contain">
|
||||
<i slot="error" class="fa fa-clipboard" aria-hidden="true"></i>
|
||||
</el-image>
|
||||
<img :src="template.templateIcon || template.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
|
||||
alt="">
|
||||
</div>
|
||||
<div class="work-template-name" :title="template.templateName || template.name">{{ template.templateName || template.name }}</div>
|
||||
</button>
|
||||
<div class="work-template-name">{{ template.templateName || template.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-activity-placeholder">
|
||||
@@ -29,7 +28,6 @@ const workTemplate = {
|
||||
<p class="subtext">敬请关注后续更新</p>
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
@@ -38,15 +36,6 @@ const workTemplate = {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.$axios.post("/platform/home/listHomeTemplate").then((res) => {
|
||||
if (res.code === 0) {
|
||||
|
||||
@@ -4,37 +4,40 @@ const user = {
|
||||
<!-- 用户容器:左右布局 -->
|
||||
<div class="user-container">
|
||||
<!-- 左侧:用户信息卡片 -->
|
||||
<div v-if="profileOnly" 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-info-card">
|
||||
<div class="user-details">
|
||||
<div class="portal-member-heading">
|
||||
<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">{{ profile.username }}</div></div>
|
||||
</div>
|
||||
<div class="user-name">${@auth.getPrincipalProperty('username')}</div>
|
||||
|
||||
<div class="user-meta-grid">
|
||||
<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>
|
||||
<span class="meta-label">工号</span>
|
||||
<span class="meta-value">{{ profile.loginname }}</span>
|
||||
<span class="meta-value">${@auth.getPrincipalProperty('loginname')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<span class="meta-label">单位信息</span>
|
||||
<span class="meta-value">
|
||||
{{ profile.unitName }} ({{ profile.unionName }})
|
||||
<!--#if(!isEmpty(@auth.getPrincipalProperty('unit'))){#-->
|
||||
${@auth.getPrincipalProperty('unit').getName()}
|
||||
<!--#}#-->
|
||||
(
|
||||
<!--#if(!isEmpty(@auth.getPrincipalProperty('union'))){#-->
|
||||
${@auth.getPrincipalProperty('union').getName()}
|
||||
<!--#}#-->
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-row meta-row-roles">
|
||||
<span class="meta-icon"><i class="fa fa-shield" aria-hidden="true"></i></span>
|
||||
<div class="meta-row">
|
||||
<span class="meta-icon">🎭</span>
|
||||
<div>
|
||||
<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>
|
||||
@@ -60,13 +63,10 @@ const user = {
|
||||
</div>
|
||||
|
||||
<!-- 右侧:待办中心 -->
|
||||
<div v-if="!profileOnly" class="todo-center">
|
||||
<div class="todo-center">
|
||||
<!-- 标题和标签 -->
|
||||
<div class="header">
|
||||
<div class="portal-todo-heading">
|
||||
<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>
|
||||
<h3>待办中心</h3>
|
||||
<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.doneCount)" name="done"></el-tab-pane>
|
||||
@@ -75,44 +75,55 @@ const user = {
|
||||
</el-tabs>
|
||||
</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">
|
||||
<i class="fa fa-file-text-o portal-task-icon" aria-hidden="true"></i>
|
||||
<div class="portal-task-content">
|
||||
<div class="portal-task-title" :title="task.instanceName">{{ task.instanceName }}</div>
|
||||
<div class="portal-task-meta">
|
||||
<span :title="task.variable && task.variable.initiatorName">申请人:{{ (task.variable && task.variable.initiatorName) || '—' }}</span>
|
||||
<span class="portal-task-node" :title="[task.categoryName, task.taskName].filter(Boolean).join(' · ')">{{ [task.categoryName, task.taskName].filter(Boolean).join(' · ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<time class="portal-task-date">{{ task.createdAt ? $moment(task.createdAt).format('YYYY-MM-DD') : '—' }}</time>
|
||||
<el-button type="primary" size="mini" @click="openView(task)">查看</el-button>
|
||||
</div>
|
||||
<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>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<el-pagination v-if="pageForm.totalCount > pageForm.pageSize" class="portal-task-pagination"
|
||||
@current-change="pageNumberChange" :current-page="pageForm.pageNumber" :page-size="pageForm.pageSize"
|
||||
layout="total, prev, next" :total="pageForm.totalCount"></el-pagination>
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
:data="todoList"
|
||||
style="width: 100%"
|
||||
border
|
||||
stripe
|
||||
v-loading="loading"
|
||||
:cell-style="{ padding: '8px' }"
|
||||
:header-cell-style="{ background: '#f8f9fa', color: '#444', fontWeight: '500' }"
|
||||
>
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="100px"></el-table-column>
|
||||
<el-table-column prop="categoryName" label="流程分类" width="100"></el-table-column>
|
||||
<el-table-column prop="title" label="流程名称" min-width="300">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip effect="dark" :content="scope.row.instanceName" placement="top">
|
||||
<span class="table-title">{{ scope.row.instanceName }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</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>
|
||||
`,
|
||||
mixins: [initTableMixins],
|
||||
// 同一组件分别承载会员卡和待办卡,各自只加载需要的数据。
|
||||
props: {
|
||||
profileOnly: { type: Boolean, default: false },
|
||||
// 首页传入显示资料:username 姓名、loginname 工号、unitName 单位、unionName 工会;缺项显示空文本。
|
||||
profile: {
|
||||
type: Object,
|
||||
default: () => ({username: '', loginname: '', unitName: '', unionName: ''})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'todo',
|
||||
todoList: [],
|
||||
loading: false,
|
||||
pageForm: {
|
||||
pageSize: 3
|
||||
pageNum: 1,
|
||||
pageSize: 5,
|
||||
totalCount: 0
|
||||
},
|
||||
taskStats: {
|
||||
todoCount: 0,
|
||||
@@ -129,12 +140,6 @@ const user = {
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
roleTags() {
|
||||
// 兼容角色接口已有的中英文分隔符,仅改变展示,不改变角色权限。
|
||||
return String(this.roleNames || '').split(/[,,、;;]/).map(role => role.trim()).filter(Boolean)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWeatherInfo(iconCode, temp) {
|
||||
const info = {
|
||||
@@ -184,60 +189,91 @@ const user = {
|
||||
|
||||
return info;
|
||||
},
|
||||
// key 为系统配置名称;返回 Promise,值为 Result.data 中的经度或纬度。
|
||||
getConfigKey(key) {
|
||||
return this.$axios.post("/open/common/getConfigKey", {key}).then(res => res.data)
|
||||
async getConfigKey(key) {
|
||||
const resp = await this.$axios.post("/open/common/getConfigKey", { key })
|
||||
return resp.data
|
||||
},
|
||||
// 使用本校配置经纬度查询城市及天气,缺少配置时不请求外部天气接口。
|
||||
fetchWeather() {
|
||||
Promise.all([this.getConfigKey("AppMapCenterPointX"), this.getConfigKey("AppMapCenterPointY")])
|
||||
.then(coordinates => {
|
||||
if (!coordinates[0] || !coordinates[1]) return
|
||||
return this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/geo/v2/city/lookup?location=" + coordinates.join(",") + "&key=" + this.QWEATHER_KEY)
|
||||
.then(geoRes => {
|
||||
if (geoRes.code !== '200' || !geoRes.location || !geoRes.location.length) {
|
||||
throw new Error('城市未找到')
|
||||
}
|
||||
const city = geoRes.location[0]
|
||||
return this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/v7/weather/now?location=" + city.id + "&key=" + this.QWEATHER_KEY)
|
||||
.then(weatherRes => {
|
||||
if (weatherRes.code !== '200' || !weatherRes.now) throw new Error('天气数据异常')
|
||||
this.weatherData = Object.assign({}, this.getWeatherInfo(weatherRes.now.icon, weatherRes.now.temp), {
|
||||
temp: weatherRes.now.temp, text: weatherRes.now.text, name: city.name
|
||||
})
|
||||
this.weatherError = false
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
this.weatherError = true
|
||||
this.weatherData = null
|
||||
// 🔆 获取天气
|
||||
async fetchWeather() {
|
||||
try {
|
||||
// 存localStorage 112.13,32.01 广州 113.27,23.14
|
||||
const AppMapCenterPointX = await this.getConfigKey("AppMapCenterPointX")
|
||||
const AppMapCenterPointY = await this.getConfigKey("AppMapCenterPointY")
|
||||
if (!AppMapCenterPointX || !AppMapCenterPointY){
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 先通过城市名获取 locationId
|
||||
this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/geo/v2/city/lookup?location=" + AppMapCenterPointX + "," + AppMapCenterPointY + "&key=" + this.QWEATHER_KEY).then((geoRes) => {
|
||||
if (geoRes.code !== '200' || !geoRes.location?.[0]) {
|
||||
console.log('城市未找到')
|
||||
}
|
||||
const locationId = geoRes.location[0].id;
|
||||
|
||||
// 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);
|
||||
}
|
||||
const weatherInfo = this.getWeatherInfo(weatherRes.now.icon, weatherRes.now.temp);
|
||||
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) {
|
||||
return title + "(" + (count || 0) + ")"
|
||||
},
|
||||
// 无请求参数;data 的四个 Count 字段分别为当前用户各状态流程数量。
|
||||
getStatistics() {
|
||||
this.$axios.post("/flow/todoCenter/statistics").then(res => {
|
||||
if (res.code === 0) this.taskStats = Object.assign({}, this.taskStats, res.data)
|
||||
}).catch(() => {
|
||||
this.$message.error("获取待办统计失败")
|
||||
})
|
||||
async getStatistics() {
|
||||
try {
|
||||
const res = await $.post("/flow/todoCenter/statistics")
|
||||
if (res.code === 0) {
|
||||
this.taskStats = Object.assign({}, this.taskStats, res.data)
|
||||
}
|
||||
} 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() {
|
||||
this.pageDataUrl = "/flow/todoCenter/" + this.activeTab
|
||||
initTableMixins.methods.pageData.call(this)
|
||||
this.getTasks()
|
||||
this.getStatistics()
|
||||
},
|
||||
handleTabClick() {
|
||||
this.doSearch()
|
||||
handleTabClick(tab) {
|
||||
this.pageData()
|
||||
console.log('切换到:', tab.name);
|
||||
},
|
||||
// 处理任务
|
||||
openView(task) {
|
||||
async openView(task) {
|
||||
console.log(task)
|
||||
const {taskId, taskKey, taskState, instanceId, businessNo, formKey} = task
|
||||
if (!formKey) {
|
||||
this.$message.warning("当前流程没有配置地址")
|
||||
@@ -245,9 +281,8 @@ const user = {
|
||||
}
|
||||
window.open(formKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
|
||||
},
|
||||
// 无请求参数;Result.data.roleNames 为当前登录人的角色名称字符串。
|
||||
getRoleNames() {
|
||||
this.$axios.post("/platform/sys/role/getRoleNames").then(res => {
|
||||
$.post("/platform/sys/role/getRoleNames").then(res => {
|
||||
if (res.code === 0) {
|
||||
this.roleNames = res.data.roleNames
|
||||
}
|
||||
@@ -256,45 +291,13 @@ const user = {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.profileOnly) {
|
||||
this.getRoleNames()
|
||||
} else {
|
||||
this.pageData()
|
||||
}
|
||||
this.pageData()
|
||||
this.getRoleNames()
|
||||
},
|
||||
mounted() {
|
||||
if (this.profileOnly) this.fetchWeather()
|
||||
this.fetchWeather()
|
||||
},
|
||||
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 {
|
||||
width: 100%;
|
||||
background-color: #f5f7fa;
|
||||
|
||||
@@ -221,8 +221,7 @@ layout("/layouts/platform.html"){
|
||||
placeholder="请输入姓名" filterable clearable
|
||||
style="width: 100%" size="small" v-if="awardsMode==1">
|
||||
<el-option
|
||||
v-for="item in userList"
|
||||
:disabled="item.disabled"
|
||||
v-for="item in getSelectableUserList(row)"
|
||||
:key="item.id"
|
||||
:label="item.username+'('+item.loginname+')'+item.sex"
|
||||
:value="item.id">
|
||||
@@ -721,10 +720,15 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
return score;
|
||||
},
|
||||
getSelectableUserList(row) {
|
||||
// 其他行已经选择的人员不再显示,当前行人员保留用于姓名正常回显。
|
||||
const selectedUserIds = this.userData
|
||||
.map(item => item.userId)
|
||||
.filter(userId => userId)
|
||||
return this.userList.filter(item => item.id === row.userId || !selectedUserIds.includes(item.id))
|
||||
},
|
||||
userDetailsChange(val, row) {
|
||||
const useridArr = this.userData.map(v => v.userId)
|
||||
this.userList.forEach(v => {
|
||||
v.disabled = useridArr.includes(v.id)
|
||||
if (row && row.userId === v.id) {
|
||||
const o = {
|
||||
unionId: v.unionId,
|
||||
|
||||
@@ -601,6 +601,8 @@
|
||||
eventForm: {
|
||||
isLeader: false
|
||||
},
|
||||
// 独立记录当前编辑项目,避免下拉框切换后丢失切换前项目标识。
|
||||
currentEventId: "",
|
||||
eventsIds: [],
|
||||
rightChecked: [],
|
||||
checkedEventInfo: [],
|
||||
@@ -778,8 +780,10 @@
|
||||
this.checkedEventInfo = this.checkedEvents.map((v) => {
|
||||
v.eventId = v.id
|
||||
if (v.projectType === "1") {
|
||||
v.athletesMaxNum = 3
|
||||
v.restrictRegNumber = 2
|
||||
// “所有单项”保存默认值,具体单项以0表示尚未个性化设置。
|
||||
const isAllItems = v.eventId === "allItems"
|
||||
v.athletesMaxNum = isAllItems ? 3 : 0
|
||||
v.restrictRegNumber = isAllItems ? 2 : 0
|
||||
v.leanderNum = 0
|
||||
v.coachNum = 0
|
||||
v.applyUserModel = 1
|
||||
@@ -791,7 +795,23 @@
|
||||
return v
|
||||
})
|
||||
},
|
||||
cacheCurrentEventForm() {
|
||||
if (!this.currentEventId || !this.eventForm) {
|
||||
return
|
||||
}
|
||||
const current = clone(this.eventForm)
|
||||
current.checkedEventId = this.currentEventId
|
||||
current.eventId = this.currentEventId
|
||||
|
||||
const index = this.checkedEventInfo.findIndex((item) => item.eventId === this.currentEventId)
|
||||
if (index >= 0) {
|
||||
this.checkedEventInfo.splice(index, 1, current)
|
||||
} else {
|
||||
this.checkedEventInfo.push(current)
|
||||
}
|
||||
},
|
||||
async eventChange(val) {
|
||||
this.cacheCurrentEventForm()
|
||||
const data = this.checkedEventInfo.find((v) => v.eventId === val)
|
||||
const resp = await $.get("/platform/activity/sports/common/getEventData", { id: val })
|
||||
await this.$nextTick()
|
||||
@@ -804,10 +824,10 @@
|
||||
projectType: resp.data.projectType,
|
||||
leanderNum: 0,
|
||||
coachNum: 0,
|
||||
athletesMaxNum: resp.data.projectType === 1 ? 3 : null,
|
||||
athletesMaxNum: resp.data.projectType === 1 ? 0 : null,
|
||||
restrictTotalTeam: 1,
|
||||
substituteNum: 0,
|
||||
restrictRegNumber: resp.data.projectType === 1 ? 2 : null,
|
||||
restrictRegNumber: resp.data.projectType === 1 ? 0 : null,
|
||||
isLeader: false,
|
||||
applyUserModel: 1
|
||||
}
|
||||
@@ -815,33 +835,27 @@
|
||||
this.projectType = resp.data.projectType
|
||||
}
|
||||
this.$set(this.eventForm, "checkedEventId", val)
|
||||
this.currentEventId = val
|
||||
},
|
||||
async saveEvent() {
|
||||
saveEvent() {
|
||||
const data = clone(this.eventForm)
|
||||
if (data.checkedEventId === "allItems") {
|
||||
this.checkedEventInfo.forEach((v) => {
|
||||
if (v.projectType === "1") {
|
||||
v.applyUserModel = this.eventForm.applyUserModel
|
||||
v.athletesMaxNum = data.athletesMaxNum
|
||||
v.restrictRegNumber = data.restrictRegNumber
|
||||
v.unionLimit = data.unionLimit
|
||||
}
|
||||
})
|
||||
const allItemsIndex = this.checkedEventInfo.findIndex((v) => v.eventId === "allItems")
|
||||
if (allItemsIndex >= 0) {
|
||||
this.checkedEventInfo.splice(allItemsIndex, 1, data)
|
||||
}
|
||||
} else {
|
||||
const info = this.checkedEventInfo.find((v) => v.eventId === data.checkedEventId)
|
||||
if (info != null) {
|
||||
const index = this.checkedEventInfo.findIndex((v) => v.eventId === data.checkedEventId)
|
||||
this.checkedEventInfo.splice(index, 1)
|
||||
if (info.projectType === "1") {
|
||||
this.$set(this.eventForm, "athletesMaxNum", data.athletesMaxNum)
|
||||
this.$set(this.eventForm, "restrictRegNumber", data.restrictRegNumber)
|
||||
}
|
||||
this.$set(this.eventForm, "checkedEventId", info.eventId)
|
||||
}
|
||||
data.eventId = data.checkedEventId
|
||||
this.checkedEventInfo.push(data)
|
||||
}
|
||||
await this.doSave()
|
||||
this.eventForm = clone(data)
|
||||
this.currentEventId = data.checkedEventId
|
||||
return this.doSave()
|
||||
},
|
||||
prev() {
|
||||
this.active--
|
||||
@@ -949,6 +963,7 @@
|
||||
this.copyTemplateMode = false
|
||||
this.copyTemplateName = ""
|
||||
this.pushTodoFlag = false
|
||||
this.currentEventId = ""
|
||||
this.events = await this.getEvents(2)
|
||||
await this.getActivityGroup()
|
||||
this.active = 0
|
||||
@@ -1028,6 +1043,7 @@
|
||||
async openEdit(row, flag, copyTemplateMode = false) {
|
||||
this.copyTemplateMode = !!copyTemplateMode
|
||||
this.pushTodoFlag = false
|
||||
this.currentEventId = ""
|
||||
this.active = flag ? 0 : this.active
|
||||
await this.getActivityGroup()
|
||||
const { id } = row
|
||||
@@ -1184,7 +1200,8 @@
|
||||
}
|
||||
|
||||
if (this.eventForm.checkedEventId == "allItems") {
|
||||
events = events.filter((v) => v.projectType == 1 && v.id != "allItems")
|
||||
// “所有单项”配置随具体单项提交,由后端统一校验并补齐继承值。
|
||||
events = events.filter((v) => v.projectType == 1)
|
||||
} else {
|
||||
this.$set(this.eventForm, "activityId", this.formData.id)
|
||||
this.$set(this.eventForm, "eventId", this.eventForm.checkedEventId)
|
||||
@@ -1192,9 +1209,10 @@
|
||||
this.$set(this.eventForm, "startAgeDate", this.eventForm.ageDate[0])
|
||||
this.$set(this.eventForm, "endAgeDate", this.eventForm.ageDate[1])
|
||||
}
|
||||
events = this.eventForm
|
||||
const allItems = this.checkedEventInfo.find((v) => v.eventId === "allItems")
|
||||
events = allItems ? [clone(allItems), this.eventForm] : [this.eventForm]
|
||||
if (this.copyTemplateMode) {
|
||||
this.cleanCopyTemplateData(null, [events])
|
||||
this.cleanCopyTemplateData(null, events)
|
||||
}
|
||||
}
|
||||
const loading = this.$loading({
|
||||
@@ -1215,6 +1233,7 @@
|
||||
this.copyTemplateMode = false
|
||||
}
|
||||
await this.openEdit(formData, false)
|
||||
this.currentEventId = this.eventForm.checkedEventId || ""
|
||||
this.$message.success(resp.msg)
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
const REGISTER_INFO_COMPONENT = {
|
||||
// 默认展示换届报告,仅由不需要查看该字段的业务页面显式关闭。
|
||||
props: {
|
||||
hideReplaceReport: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<el-tabs tab-position="top" v-model="activeName">
|
||||
@@ -36,7 +43,7 @@ const REGISTER_INFO_COMPONENT = {
|
||||
{{viewData.createTime}}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会费标准" :span="4">
|
||||
{{viewData.due}}
|
||||
{{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="4">
|
||||
<template slot="label">社团介绍</template>
|
||||
@@ -66,7 +73,7 @@ const REGISTER_INFO_COMPONENT = {
|
||||
<template slot="label">其他附件</template>
|
||||
<file-preview :files="viewData.files" complete_result></file-preview>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="2">
|
||||
<el-descriptions-item :span="2" v-if="!hideReplaceReport">
|
||||
<template slot="label">换届报告</template>
|
||||
<file-preview :files="viewData.replaceReport" complete_result></file-preview>
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -68,10 +68,12 @@ layout("/layouts/platform.html"){
|
||||
</el-steps>
|
||||
|
||||
<!-- 第一步:基础信息表单 -->
|
||||
<!-- 本表单自行提示首个必填错误,关闭全局校验弹窗增强以免重复提示。 -->
|
||||
<el-form
|
||||
v-show="step === 0"
|
||||
:model="formData"
|
||||
ref="baseForm"
|
||||
no-auto-enhance
|
||||
:rules="baseRules"
|
||||
label-width="150px"
|
||||
class="main-form"
|
||||
@@ -135,7 +137,9 @@ layout("/layouts/platform.html"){
|
||||
v-model="formData.due"
|
||||
placeholder="请输入会费标准"
|
||||
readonly
|
||||
></el-input>
|
||||
>
|
||||
<template slot="append">元/年</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -146,7 +150,7 @@ layout("/layouts/platform.html"){
|
||||
<el-form-item
|
||||
class="is-required"
|
||||
label="本年度活动开展情况"
|
||||
prop="yearActivity"
|
||||
prop="yearActivityList"
|
||||
>
|
||||
<span class="tip-text">说明:非赛事活动无需填写赛事举办单位、获奖情况。</span>
|
||||
<el-table
|
||||
@@ -638,6 +642,7 @@ layout("/layouts/platform.html"){
|
||||
id: GetQueryString("bizId"),
|
||||
taskId: GetQueryString("taskId"),
|
||||
step: 0, // 当前步骤(0-基础信息,1-财务收支,2-成员名单)
|
||||
formLoading: false,
|
||||
isRegistered: false, // 是否已登记
|
||||
currentClubId: "", // 当前选中社团ID
|
||||
|
||||
@@ -656,7 +661,7 @@ layout("/layouts/platform.html"){
|
||||
year: new Date().getFullYear() + "", // 默认当前年度
|
||||
clubId: "",
|
||||
foundTime: "",
|
||||
due: "",
|
||||
due: null,
|
||||
clubName: "",
|
||||
yearActivityList: [{}], // 本年度活动列表
|
||||
plans: [{}], // 下年度计划列表
|
||||
@@ -671,7 +676,10 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
|
||||
// 表单校验规则
|
||||
baseRules: {},
|
||||
baseRules: {
|
||||
yearActivityList: [{validator: this.validateActivityRows, trigger: ['change', 'blur']}],
|
||||
plans: [{validator: this.validateActivityRows, trigger: ['change', 'blur']}]
|
||||
},
|
||||
financeRules: {},
|
||||
memberRules: {}
|
||||
};
|
||||
@@ -724,6 +732,28 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 在列表级逐行校验,避免嵌套表单项未注册、空数组或纯空白文本绕过必填。
|
||||
validateActivityRows(rule, value, callback) {
|
||||
const currentYear = rule.field === 'yearActivityList';
|
||||
const label = currentYear ? '本年度活动开展情况' : '下一年度活动计划';
|
||||
const fields = currentYear
|
||||
? [['activityTime', '活动时间'], ['activityName', '活动主题/赛事名称'], ['activityAddress', '活动地点'], ['joinNum', '参加人数']]
|
||||
: [['activityName', '活动主题/赛事名称'], ['activityUnit', '活动/赛事主办单位'], ['activityAddress', '活动地点']];
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
callback(new Error(label + '至少填写一条'));
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
for (const field of fields) {
|
||||
const content = value[i] && value[i][field[0]];
|
||||
if (content === null || content === undefined || String(content).trim() === '') {
|
||||
callback(new Error(label + '第' + (i + 1) + '条:' + field[1] + '不能为空'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
callback();
|
||||
},
|
||||
/************************ 通用工具方法 ************************/
|
||||
// 格式化数字(保留2位小数,空值显示0)
|
||||
formatNumber(num) {
|
||||
@@ -1110,7 +1140,7 @@ layout("/layouts/platform.html"){
|
||||
if (!val) return;
|
||||
const club = this.clubOptions.find(item => item.id === val);
|
||||
// 更新社团基础信息
|
||||
this.formData.due = club?.due || "";
|
||||
this.formData.due = club && club.due !== null && club.due !== undefined ? club.due : null;
|
||||
this.formData.foundTime = club?.foundTime
|
||||
? this.$moment(club.foundTime).format("YYYY-MM-DD")
|
||||
: this.$moment().format("YYYY-MM-DD");
|
||||
@@ -1161,73 +1191,63 @@ layout("/layouts/platform.html"){
|
||||
// 步骤前进
|
||||
this.step++;
|
||||
},
|
||||
// 保存
|
||||
async handleSave() {
|
||||
try {
|
||||
this.recalculateCensus();
|
||||
const resp = await this.$axios.post("/platform/club/examine/apply/save", {
|
||||
data: JSON.stringify(this.formData),
|
||||
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed),
|
||||
incomeCensus: JSON.stringify(this.financeData.incomeCensus)
|
||||
});
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg);
|
||||
commonUtil.pjaxPush('/platform/club/examine/mine');
|
||||
// 三个操作共用基础信息校验,失败时返回第一步展示行号和必填字段。
|
||||
validateBaseBeforeAction(action) {
|
||||
if (this.formLoading) return;
|
||||
this.$refs.baseForm.validate((valid, fields) => {
|
||||
if (!valid) {
|
||||
this.step = 0;
|
||||
const firstError = Object.values(fields || {}).flat()[0];
|
||||
this.$message.warning('温馨提示:' + (firstError ? firstError.message : '请补全基础信息必填项'));
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleError(error, "保存失败");
|
||||
}
|
||||
action();
|
||||
});
|
||||
},
|
||||
// 提交(新增)
|
||||
async handleSubmit() {
|
||||
try {
|
||||
this.recalculateCensus();
|
||||
await this.$confirm("确定要提交此申请吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "info"
|
||||
// 仅接收本页三个固定操作的接口后缀;data为登记实体JSON,另两项为财务数组JSON。
|
||||
// 接口返回{code,msg,data},code为0时保存成功,否则展示后端校验提示。
|
||||
requestExamineAction(action) {
|
||||
if (this.formLoading) return;
|
||||
this.recalculateCensus();
|
||||
const params = {
|
||||
data: JSON.stringify(this.formData),
|
||||
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed),
|
||||
incomeCensus: JSON.stringify(this.financeData.incomeCensus)
|
||||
};
|
||||
if (action === 'submitAgain') params.taskId = this.taskId;
|
||||
this.formLoading = true;
|
||||
return this.$axios.post('/platform/club/examine/apply/' + action, params)
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg);
|
||||
commonUtil.pjaxPush('/platform/club/examine/mine');
|
||||
} else {
|
||||
this.$message.warning(resp.msg);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 后端校验和网络异常已由全局axios拦截器提示,此处仅结束失败请求,避免再次弹窗。
|
||||
})
|
||||
.finally(() => {
|
||||
this.formLoading = false;
|
||||
});
|
||||
const resp = await this.$axios.post("/platform/club/examine/apply/submit", {
|
||||
data: JSON.stringify(this.formData),
|
||||
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed),
|
||||
incomeCensus: JSON.stringify(this.financeData.incomeCensus)
|
||||
});
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg);
|
||||
commonUtil.pjaxPush('/platform/club/examine/mine');
|
||||
} else {
|
||||
this.$message.warning(resp.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
this.handleError(error, "提交失败");
|
||||
}
|
||||
}
|
||||
},
|
||||
// 提交(任务处理)
|
||||
async handleFinishTask() {
|
||||
try {
|
||||
this.recalculateCensus();
|
||||
await this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
});
|
||||
const resp = await this.$axios.post('/platform/club/examine/apply/submitAgain', {
|
||||
data: JSON.stringify(this.formData),
|
||||
incomeDetailed: JSON.stringify(this.financeData.incomeDetailed),
|
||||
incomeCensus: JSON.stringify(this.financeData.incomeCensus),
|
||||
taskId: this.taskId
|
||||
});
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg);
|
||||
commonUtil.pjaxPush('/platform/club/examine/mine');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
this.handleError(error, "提交失败");
|
||||
}
|
||||
}
|
||||
handleSave() {
|
||||
this.validateBaseBeforeAction(() => this.requestExamineAction('save'));
|
||||
},
|
||||
handleSubmit() {
|
||||
this.validateBaseBeforeAction(() => {
|
||||
this.$confirm('确定要提交此申请吗?', '提示', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'info'
|
||||
}).then(() => this.requestExamineAction('submit')).catch(() => {});
|
||||
});
|
||||
},
|
||||
handleFinishTask() {
|
||||
this.validateBaseBeforeAction(() => {
|
||||
this.$confirm('您确定要提交吗?', '提示', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(() => this.requestExamineAction('submitAgain')).catch(() => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<el-descriptions v-if="viewData" :column="3" border class="table_fixed">
|
||||
<el-descriptions-item label="社团名称">{{ viewData.clubName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="成立时间">{{ viewData.foundTime }} </el-descriptions-item>
|
||||
<el-descriptions-item label="会费标准">{{ viewData.due }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会费标准">{{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}</el-descriptions-item>
|
||||
<el-descriptions-item span="3">
|
||||
<template slot="label">工作总结</template>
|
||||
<file-preview :files="viewData.summaryFiles" complete_result></file-preview>
|
||||
|
||||
@@ -73,7 +73,7 @@ layout("/layouts/platform.html"){
|
||||
<el-descriptions-item label="联系人邮箱">{{viewData.concatPersonEmail}}</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template slot="label">会费标准</template>
|
||||
{{viewData.due}}
|
||||
{{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="2">
|
||||
<template slot="label">当前人数</template>
|
||||
|
||||
@@ -63,7 +63,7 @@ layout("/layouts/platform.html"){
|
||||
<el-descriptions-item label="社团编码">{{viewData.clubCode}}</el-descriptions-item>
|
||||
<el-descriptions-item label="社团类型">{{viewData.typeName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="发起人">{{viewData.sponsorName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="会费标准">{{viewData.due}}</el-descriptions-item>
|
||||
<el-descriptions-item label="会费标准">{{ viewData.due === null || viewData.due === undefined ? "--" : viewData.due + " 元/年" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前人数">{{viewData.currentPeopleNum}}</el-descriptions-item>
|
||||
<el-descriptions-item span="2">
|
||||
<template slot="label">社团介绍</template>
|
||||
|
||||
@@ -24,7 +24,13 @@ const CLUB_FORM_TEMPLATE = {
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="会费标准" prop="due">
|
||||
<el-input placeholder="请填写会费标准" v-model="formData.due">
|
||||
<el-input
|
||||
min="0"
|
||||
placeholder="请填写会费标准"
|
||||
step="0.01"
|
||||
type="number"
|
||||
v-model.number="formData.due"
|
||||
>
|
||||
<template slot="append">元/年</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@@ -156,12 +162,15 @@ const CLUB_FORM_TEMPLATE = {
|
||||
return {
|
||||
formData: {
|
||||
clubName: "",
|
||||
clubType: ""
|
||||
clubType: "",
|
||||
due: null
|
||||
},
|
||||
formRules: {
|
||||
clubName: [{ required: true, message: "请填写社团名称", trigger: ["blur", "change"] }],
|
||||
clubCode: [{ required: false, message: "请填写社团编码", trigger: ["blur", "change"] }],
|
||||
clubType: [{ required: true, message: "请选择社团类型", trigger: ["blur", "change"] }],
|
||||
// 会费标准只保存数字,单位“元/年”由页面统一展示。
|
||||
due: [{ type: "number", min: 0, message: "会费标准必须为不小于0的数字", trigger: ["blur", "change"] }],
|
||||
//concatPerson: [{ required: true, message: "请选择社团联系人", trigger: ["blur", "change"] }],
|
||||
foundTime: [{ required: true, message: "请选择成立时间", trigger: ["blur", "change"] }]
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ layout("/layouts/platform.html"){
|
||||
</el-card>
|
||||
|
||||
<template #public>
|
||||
<register-info ref="registerInfoRef">
|
||||
<register-info ref="registerInfoRef" :hide-replace-report="true">
|
||||
<div v-if="showApprovalForm">
|
||||
<div class="process-title">
|
||||
{{formData.taskName}}
|
||||
|
||||
@@ -403,12 +403,6 @@ const PROPOSAL_INFO = {
|
||||
},
|
||||
|
||||
openChart(){
|
||||
// 仅保存的提案尚未创建 WF 实例,按提案流程 KEY 预览流程定义图。
|
||||
if (!this.row.instanceId) {
|
||||
this.$refs.snakerChartRef.onOpen("JDHTA_NC")
|
||||
return
|
||||
}
|
||||
// 已提交提案使用实例对应的流程版本,保留实际办理节点和审批进度。
|
||||
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
|
||||
}
|
||||
},
|
||||
|
||||
+1
-2
@@ -40,8 +40,7 @@
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"
|
||||
style="width: 100%"></dict-select>
|
||||
</search-item>-->
|
||||
<!-- 会员模块屏蔽人员属性查询入口。 -->
|
||||
<search-item label="人员属性" v-if="false">
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-if="userAttributeMultiple" v-model="pageForm.userAttributes" code="USER_ATTRIBUTE"
|
||||
multiple collapse-tags clearable style="width: 100%"></dict-select>
|
||||
<dict-select v-else v-model="pageForm.userAttribute" code="USER_ATTRIBUTE"
|
||||
|
||||
+1
-3
@@ -32,9 +32,7 @@ const COMMON_QUERY = {
|
||||
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
|
||||
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
|
||||
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
this.$emit('search', pageForm)
|
||||
},
|
||||
async initData(){
|
||||
|
||||
+1
-2
@@ -29,8 +29,7 @@
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
|
||||
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>-->
|
||||
<!-- 会员模块屏蔽人员属性查询入口。 -->
|
||||
<search-item label="人员属性" v-if="false">
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-if="userAttributeMultiple" v-model="pageForm.userAttributes"
|
||||
placeholder="请选择人员属性" @change="doSearch" code="USER_ATTRIBUTE"
|
||||
multiple collapse-tags clearable></dict-select>
|
||||
|
||||
+2
-4
@@ -32,10 +32,8 @@ const COMMON_QUERY = {
|
||||
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
|
||||
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
|
||||
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
|
||||
// 保持其他业务筛选条件,排除已屏蔽字段。
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
// 多选人员属性沿用现有数组查询参数的JSON提交方式。
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
this.$emit('search', pageForm)
|
||||
},
|
||||
async initData(){
|
||||
|
||||
+1
-2
@@ -149,8 +149,7 @@ const MEMBER_CHANGE = {
|
||||
:disabled="allowFields('personType')" style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<!-- 屏蔽人员属性展示和编辑,保留回填值以免保存时覆盖历史数据。 -->
|
||||
<el-descriptions-item label="人员属性" v-if="false">
|
||||
<el-descriptions-item label="人员属性">
|
||||
<el-form-item prop="userAttribute">
|
||||
<dict-select v-model="formData.userAttribute" code="USER_ATTRIBUTE"
|
||||
style="width: 100%"></dict-select>
|
||||
|
||||
@@ -50,8 +50,7 @@ layout("/layouts/platform.html"){
|
||||
<!-- <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"-->
|
||||
<!-- code="AIDFUND_MEMBER_USER_TYPE"></dict-select>-->
|
||||
<!-- </search-item>-->
|
||||
<!-- 会员模块屏蔽人员属性查询入口。 -->
|
||||
<search-item label="人员属性" v-if="false">
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
|
||||
</search-item>
|
||||
@@ -77,7 +76,7 @@ layout("/layouts/platform.html"){
|
||||
设置人员分类
|
||||
</el-button>
|
||||
|
||||
<el-button v-if="false" :disabled="!checkUsers.length" @click="openBatchEditUserAttribute" class="ml5" icon="el-icon-edit"
|
||||
<el-button :disabled="!checkUsers.length" @click="openBatchEditUserAttribute" class="ml5" icon="el-icon-edit"
|
||||
size="small" type="primary">
|
||||
设置人员属性
|
||||
</el-button>
|
||||
@@ -241,7 +240,6 @@ layout("/layouts/platform.html"){
|
||||
|
||||
<el-drawer
|
||||
:before-close="handleClose"
|
||||
v-if="false"
|
||||
:visible.sync="batchEditUserAttributeVisible"
|
||||
size="1000px" title="批量设置人员属性">
|
||||
<el-timeline style="margin-right: 40px">
|
||||
@@ -571,8 +569,7 @@ layout("/layouts/platform.html"){
|
||||
{ prop: "personType", label: "人员类型", width: 120, sortable: true },
|
||||
// 临时屏蔽“人员分类”列表字段,不删除配置,后续需要可恢复。
|
||||
// { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
|
||||
// 屏蔽人员属性列,同时从导出可选列中排除。
|
||||
// { prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
||||
{ prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
||||
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
|
||||
@@ -600,15 +597,13 @@ layout("/layouts/platform.html"){
|
||||
"member-all-change": MEMBER_ALL_CHANGE_INFO,
|
||||
},
|
||||
methods: {
|
||||
// 分页和导出共用查询参数,排除已屏蔽的人员属性条件。
|
||||
// 分页和导出共用该参数构建方法,保证人员属性多选值始终按 JSON 数组提交。
|
||||
buildQueryPageForm(source = this.pageForm) {
|
||||
const pageForm = clone(source)
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
pageForm.userAttributes = JSON.stringify(source.userAttributes || [])
|
||||
return pageForm
|
||||
},
|
||||
// 复用公共分页方法,提交已清理的业务查询参数。
|
||||
// 覆盖公共分页方法,使后端按人员属性集合执行查询。
|
||||
pageData(data = null) {
|
||||
return initTableMixins.methods.pageData.call(this, this.buildQueryPageForm(data || this.pageForm))
|
||||
},
|
||||
|
||||
@@ -42,8 +42,7 @@ layout("/layouts/platform.html"){
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
|
||||
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>-->
|
||||
<!-- 会员模块屏蔽人员属性查询入口。 -->
|
||||
<search-item label="人员属性" v-if="false">
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
|
||||
</search-item>
|
||||
@@ -199,10 +198,8 @@ layout("/layouts/platform.html"){
|
||||
pageForm.personTypes = JSON.stringify(pageForm.personTypes)
|
||||
pageForm.changeTypes = JSON.stringify(pageForm.changeTypes)
|
||||
pageForm.changeOrigins = JSON.stringify(pageForm.changeOrigins)
|
||||
// 人员属性已停用,不参与变更记录筛选。
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
// 人员属性为多选条件,提交前转换为后端可解析的 JSON 数组。
|
||||
pageForm.userAttributes = JSON.stringify(pageForm.userAttributes)
|
||||
|
||||
this.$axios.post(loc() + '/pageData', pageForm)
|
||||
.then(res => {
|
||||
|
||||
@@ -73,8 +73,7 @@ const MEMBER_INFO = {
|
||||
<el-descriptions-item label="人员类型">{{ viewData.personType }}</el-descriptions-item>
|
||||
|
||||
<!-- 第 4 行 -->
|
||||
<!-- 屏蔽人员属性展示和编辑,保留回填值以免保存时覆盖历史数据。 -->
|
||||
<el-descriptions-item label="人员属性" v-if="false">{{ viewData.userAttribute }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人员属性">{{ viewData.userAttribute }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职称">{{ viewData.professionalTitle }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职级">{{ viewData.professionalLevel }}</el-descriptions-item>
|
||||
|
||||
@@ -86,7 +85,6 @@ const MEMBER_INFO = {
|
||||
<el-descriptions-item label="行政岗级">{{ viewData.administrativePositionLevel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人员分类" v-if="false">{{ viewData.aidFundMemberUserType }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
store,
|
||||
|
||||
+1
-2
@@ -43,8 +43,7 @@
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
|
||||
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>-->
|
||||
<!-- 会员模块屏蔽人员属性查询入口。 -->
|
||||
<search-item label="人员属性" v-if="false">
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
|
||||
</search-item>
|
||||
|
||||
+2
-4
@@ -37,10 +37,8 @@ const COMMON_QUERY = {
|
||||
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
|
||||
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
|
||||
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
|
||||
// 保持其他业务筛选条件,排除已屏蔽字段。
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
// 人员属性多选值按现有数组查询参数口径提交。
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
this.$emit('search', pageForm)
|
||||
},
|
||||
handleUnionChange() {
|
||||
|
||||
+1
-2
@@ -102,8 +102,7 @@
|
||||
style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<!-- 屏蔽人员属性展示和编辑,保留回填值以免保存时覆盖历史数据。 -->
|
||||
<el-descriptions-item label="人员属性" v-if="false">
|
||||
<el-descriptions-item label="人员属性">
|
||||
<el-form-item prop="userAttribute">
|
||||
<dict-select v-model="formData.userAttribute" code="USER_ATTRIBUTE"
|
||||
style="width: 100%"></dict-select>
|
||||
|
||||
@@ -49,8 +49,7 @@ layout("/layouts/platform.html"){
|
||||
<search-item label="人员分类" v-if="false">
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<!-- 会员模块屏蔽人员属性查询入口。 -->
|
||||
<search-item label="人员属性" v-if="false">
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
|
||||
</search-item>
|
||||
@@ -134,15 +133,13 @@ layout("/layouts/platform.html"){
|
||||
"member-info": MEMBER_INFO
|
||||
},
|
||||
methods: {
|
||||
// 分页和导出共用查询参数,排除已屏蔽的人员属性条件。
|
||||
// 分页和导出共用该参数构建方法,保证人员属性多选值始终按 JSON 数组提交。
|
||||
buildQueryPageForm() {
|
||||
const pageForm = clone(this.pageForm)
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes || [])
|
||||
return pageForm
|
||||
},
|
||||
// 复用公共分页方法,提交已清理的业务查询参数。
|
||||
// 覆盖公共分页方法,使后端按人员属性集合执行查询。
|
||||
pageData() {
|
||||
return initTableMixins.methods.pageData.call(this, this.buildQueryPageForm())
|
||||
},
|
||||
|
||||
@@ -49,8 +49,7 @@ layout("/layouts/platform.html"){
|
||||
<search-item label="人员分类" v-if="false">
|
||||
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
|
||||
</search-item>
|
||||
<!-- 会员模块屏蔽人员属性查询入口。 -->
|
||||
<search-item label="人员属性" v-if="false">
|
||||
<search-item label="人员属性">
|
||||
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
|
||||
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
|
||||
</search-item>
|
||||
@@ -142,12 +141,10 @@ layout("/layouts/platform.html"){
|
||||
"member-add-form": MEMBER_ADD_FORM
|
||||
},
|
||||
methods: {
|
||||
// 分页查询前排除已屏蔽的人员属性条件。
|
||||
// 人员属性为多选条件,分页查询前统一转换为后端可接收的 JSON 数组。
|
||||
pageData(data = null) {
|
||||
const pageForm = clone(data || this.pageForm)
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes || [])
|
||||
return initTableMixins.methods.pageData.call(this, pageForm)
|
||||
},
|
||||
openAdd() {
|
||||
|
||||
+2
-4
@@ -129,7 +129,7 @@ layout("/layouts/platform.html"){
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag" v-if="false">
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag">
|
||||
<el-col class="query-row-title">人员属性:</el-col>
|
||||
<el-col class="query-row-content query-row-content-tag">
|
||||
<el-tag class="query-tag-item"
|
||||
@@ -280,9 +280,7 @@ layout("/layouts/platform.html"){
|
||||
const pageForm = clone(this.pageForm)
|
||||
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
|
||||
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data
|
||||
|
||||
+3
-8
@@ -165,8 +165,7 @@ layout("/layouts/platform.html"){
|
||||
{ prop: "unitName", label: "所属单位", width: 120, sortable: true },
|
||||
// 临时屏蔽“人员分类”列表字段,不删除配置,后续需要可恢复。
|
||||
// { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
|
||||
// 屏蔽人员属性列,同时从导出可选列中排除。
|
||||
// { prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
||||
{ prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
||||
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
|
||||
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
|
||||
@@ -206,9 +205,7 @@ layout("/layouts/platform.html"){
|
||||
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
|
||||
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
|
||||
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
|
||||
const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => {
|
||||
return {prop: item.prop, label: item.label}
|
||||
@@ -233,9 +230,7 @@ layout("/layouts/platform.html"){
|
||||
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
|
||||
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
|
||||
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
|
||||
pageForm.memberStatus = JSON.stringify(this.pageForm.memberStatus)
|
||||
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag" v-if="false">
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag">
|
||||
<el-col class="query-row-title">人员属性:</el-col>
|
||||
<el-col class="query-row-content query-row-content-tag">
|
||||
<el-tag
|
||||
|
||||
+2
-4
@@ -129,7 +129,7 @@ layout("/layouts/platform.html"){
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag" v-if="false">
|
||||
<el-row type="flex" align="middle" class="query-row query-row-tag">
|
||||
<el-col class="query-row-title">人员属性:</el-col>
|
||||
<el-col class="query-row-content query-row-content-tag">
|
||||
<el-tag class="query-tag-item"
|
||||
@@ -265,9 +265,7 @@ layout("/layouts/platform.html"){
|
||||
// 统计接口按 JSON 字符串接收多选条件,这里把人员分类和人员属性统一成同一提交口径。
|
||||
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
|
||||
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
|
||||
// 人员属性已屏蔽,查询和导出均不再按该字段过滤。
|
||||
pageForm.userAttribute = ""
|
||||
pageForm.userAttributes = "[]"
|
||||
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
|
||||
this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data
|
||||
|
||||
@@ -77,7 +77,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<van-cell title="会长">{{ clubDetail.clubLeader || '--' }}</van-cell>
|
||||
<van-cell title="秘书长">{{ clubDetail.clubSecretary || '--' }}</van-cell>
|
||||
<van-cell title="当前人数">{{ clubDetail.currentPeopleNum || 0 }} 人</van-cell>
|
||||
<van-cell title="会费标准">{{ clubDetail.due || '--' }}</van-cell>
|
||||
<van-cell title="会费标准">{{ clubDetail.due === null || clubDetail.due === undefined ? '--' : clubDetail.due + ' 元/年' }}</van-cell>
|
||||
<van-cell title="社团介绍" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.introduce)" v-html="clubDetail.introduce"></div><span v-else>暂无</span></van-cell>
|
||||
<van-cell title="社团宗旨" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.purpose)" v-html="clubDetail.purpose"></div><span v-else>暂无</span></van-cell>
|
||||
<van-cell title="社团章程" class="club-user-join-info-long-cell"><file-preview v-if="clubDetail.rulesFile && clubDetail.rulesFile.length" :files="clubDetail.rulesFile" complete_result></file-preview><span v-else>暂无</span></van-cell>
|
||||
|
||||
Reference in New Issue
Block a user