Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceec8eb095 | ||
|
|
2a305a3f23 | ||
|
|
29260423d5 | ||
|
|
d8e7adfe9a | ||
|
|
bc1b3bf7fe | ||
|
|
59fd83ba24 | ||
|
|
3059603186 | ||
|
|
f17f6153b5 | ||
|
|
e87014fa72 |
@@ -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的图片对象
|
||||
*
|
||||
@@ -77,13 +167,7 @@ public class SysOfficeTemplateUtil {
|
||||
public PictureRenderData createPictureRenderData(int width, int height, String url) {
|
||||
try {
|
||||
byte[] bytes = getFileBytesByUrl(url);
|
||||
// if (StrUtil.isBlank(url)) {
|
||||
// return null;
|
||||
// }
|
||||
// int startIndex = url.indexOf("id=") + "id=".length();
|
||||
// String id = url.substring(startIndex);
|
||||
// Sys_file sysFile = dao.fetch(Sys_file.class, id);
|
||||
// byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
|
||||
log.info("[Office图片读取] 创建图片渲染数据,referenceLength={}, byteCount={}, width={}, height={}", StrUtil.nullToEmpty(url).length(), bytes == null ? null : bytes.length, width, height);
|
||||
ByteArrayPictureRenderData pictureRenderData = new ByteArrayPictureRenderData(bytes, PictureType.PNG);
|
||||
PictureStyle pictureStyle = new PictureStyle();
|
||||
pictureStyle.setWidth(width);
|
||||
@@ -91,7 +175,7 @@ public class SysOfficeTemplateUtil {
|
||||
pictureRenderData.setPictureStyle(pictureStyle);
|
||||
return pictureRenderData;
|
||||
} catch (Exception e) {
|
||||
log.error("文件转换PictureRenderData错误,文件路径:{},{}", url, e.getMessage());
|
||||
log.error("[Office图片读取] 创建图片渲染数据失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -140,13 +224,20 @@ public class SysOfficeTemplateUtil {
|
||||
public byte[] getFileBytesByUrl(String url) {
|
||||
try {
|
||||
if (StrUtil.isBlank(url)) {
|
||||
log.warn("[Office图片读取] 图片引用为空");
|
||||
return null;
|
||||
}
|
||||
int startIndex = url.indexOf("id=") + "id=".length();
|
||||
String id = url.substring(startIndex);
|
||||
String logFileId = url.contains("id=") ? id : "未识别id参数";
|
||||
log.info("[Office图片读取] 解析文件引用,fileId={}, hasIdParameter={}", logFileId, url.contains("id="));
|
||||
Sys_file sysFile = dao.fetch(Sys_file.class, id);
|
||||
return SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
|
||||
log.info("[Office图片读取] 查询文件记录,fileId={}, found={}, bucket={}, storagePathPresent={}", logFileId, sysFile != null, sysFile == null ? null : sysFile.getBucket(), sysFile != null && StrUtil.isNotBlank(sysFile.getStoragePath()));
|
||||
byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
|
||||
log.info("[Office图片读取] 读取存储文件完成,fileId={}, byteCount={}", logFileId, bytes == null ? null : bytes.length);
|
||||
return bytes;
|
||||
} catch (Exception e) {
|
||||
log.error("[Office图片读取] 读取图片失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.budwk.app.base.wxwork;
|
||||
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import java.util.List;
|
||||
|
||||
/** 企业微信自建应用消息服务,接收人的 loginname 必须对应学校企业微信 userid。 */
|
||||
public interface WxWorkMessageService {
|
||||
/**
|
||||
* 单人发送;link 为空发送文本,否则发送文本卡片,相对链接使用配置域名补全。
|
||||
* @param loginname 接收人的企业微信 userid,当前学校直接使用系统 loginname
|
||||
* @param title 消息标题,为空使用配置的默认标题
|
||||
* @param content 消息正文,可含富文本标签,发送前转为纯文本
|
||||
* @param link 业务详情链接,可为空、站内相对路径或完整 HTTP(S) 地址
|
||||
* @return 与 massSend 相同的结果结构;skipped 表示关闭开关而未发送
|
||||
*/
|
||||
NutMap send(String loginname, String title, String content, String link);
|
||||
|
||||
/**
|
||||
* 多人发送相同消息,去重并分批调用企业微信接口。
|
||||
* @param loginNames 接收人 loginname 列表,每项须为企业微信 userid,不接受 @all
|
||||
* @param title 消息标题,为空使用默认标题
|
||||
* @param content 消息正文,清理标签后不能为空
|
||||
* @param link 详情链接;空值为文本消息,非空为文本卡片
|
||||
* @return NutMap:success 为全部批次被接口接受且无无效接收人;skipped 为未发送;
|
||||
* batches 为批次结果列表,每项包含 errcode、errmsg、msgid、invaliduser 等接口字段。
|
||||
* 接口接受不代表用户已读;网络或配置错误抛出业务异常,不自动重发已接受批次。
|
||||
*/
|
||||
NutMap massSend(List<String> loginNames, String title, String content, String link);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.budwk.app.base.wxwork.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HtmlUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.constant.RedisConstant;
|
||||
import com.budwk.app.base.wxwork.WxWorkMessageService;
|
||||
import com.budwk.app.web.commons.base.Globals;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.integration.jedis.RedisService;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.impl.PropertiesProxy;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** 企业微信官方消息接口;参考 ypi 的单发、群发及 token 缓存方式,配置与业务调用分离。 */
|
||||
@Slf4j
|
||||
@IocBean
|
||||
public class WxWorkMessageServiceImpl implements WxWorkMessageService {
|
||||
private static final String PREFIX = "wxwork.message.";
|
||||
private static final String API = "https://qyapi.weixin.qq.com/cgi-bin/";
|
||||
@Inject
|
||||
private PropertiesProxy conf;
|
||||
@Inject
|
||||
private RedisService redisService;
|
||||
|
||||
/** 参数和返回结构见接口;发送是外部操作,数据库事务无法撤回已发送的消息。 */
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public NutMap send(String loginname, String title, String content, String link) {
|
||||
return massSend(loginname == null ? null : List.of(loginname), title, content, link);
|
||||
}
|
||||
|
||||
/** 先判断独立消息开关,关闭时不读取凭据、不取 token,也不调用外部接口。 */
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public NutMap massSend(List<String> loginNames, String title, String content, String link) {
|
||||
Object switchValue = Globals.MyConfig.get("AppWxWorkMessage");
|
||||
boolean enabled = Boolean.parseBoolean(switchValue == null
|
||||
? conf.get(PREFIX + "enabled", "false") : switchValue.toString());
|
||||
if (!enabled) {
|
||||
return NutMap.NEW().addv("success", false).addv("skipped", true).addv("batches", List.of());
|
||||
}
|
||||
if (loginNames == null || loginNames.isEmpty()
|
||||
|| loginNames.stream().anyMatch(name -> StrUtil.isBlank(name) || name.contains("|") || name.trim().startsWith("@"))) {
|
||||
throw new BaseException("企业微信接收人须为非空 loginname,不允许分隔符或群发标记");
|
||||
}
|
||||
List<String> receivers = loginNames.stream().map(String::trim).distinct().toList();
|
||||
String body = plainText(content);
|
||||
String heading = plainText(StrUtil.blankToDefault(title, conf.get(PREFIX + "default-title", "智慧工会")));
|
||||
if (StrUtil.isBlank(body)) throw new BaseException("企业微信消息正文不能为空");
|
||||
String url = resolveLink(link);
|
||||
long agentId;
|
||||
try {
|
||||
agentId = Long.parseLong(required("wxwork.agentId"));
|
||||
if (agentId <= 0) throw new NumberFormatException();
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BaseException("企业微信 agentId 必须为正整数");
|
||||
}
|
||||
JSONObject payload = JSONUtil.createObj().set("agentid", agentId);
|
||||
if (url == null) {
|
||||
String text = heading + "\n" + body;
|
||||
requireLength(text, 2048, "文本消息");
|
||||
payload.set("msgtype", "text").set("text", JSONUtil.createObj().set("content", text));
|
||||
} else {
|
||||
requireLength(heading, 128, "卡片标题");
|
||||
String description = HtmlUtil.escape(body).replace("\n", "<br>");
|
||||
requireLength(description, 512, "卡片正文");
|
||||
requireLength(url, 2048, "卡片链接");
|
||||
payload.set("msgtype", "textcard").set("textcard", JSONUtil.createObj()
|
||||
.set("title", heading).set("description", description).set("url", url).set("btntxt", "查看详情"));
|
||||
}
|
||||
List<JSONObject> batches = new ArrayList<>();
|
||||
boolean success = true;
|
||||
// 企业微信 touser 单次最多 1000 人;按去重后的接收人分批,禁止自动重复整个批次列表。
|
||||
for (int offset = 0; offset < receivers.size(); offset += 1000) {
|
||||
payload.set("touser", String.join("|", receivers.subList(offset, Math.min(offset + 1000, receivers.size()))));
|
||||
String token = getAccessToken(false);
|
||||
JSONObject result = sendPayload(payload, token);
|
||||
int code = result.getInt("errcode", -1);
|
||||
// 仅在明确的 token 无效/过期返回时刷新一次;超时的发送结果不确定,不能盲目重发。
|
||||
if (code == 40014 || code == 42001 || code == 40001) {
|
||||
result = sendPayload(payload, getAccessToken(true));
|
||||
}
|
||||
batches.add(result);
|
||||
boolean accepted = result.getInt("errcode", -1) == 0
|
||||
&& StrUtil.isBlank(result.getStr("invaliduser"))
|
||||
&& StrUtil.isBlank(result.getStr("unlicenseduser"));
|
||||
success &= accepted;
|
||||
log.info("企业微信消息批次完成:offset={},success={},errcode={},msgid={}",
|
||||
offset, accepted, result.getInt("errcode"), result.getStr("msgid"));
|
||||
}
|
||||
return NutMap.NEW().addv("success", success).addv("skipped", false).addv("batches", batches);
|
||||
}
|
||||
|
||||
/** 标题和富文本正文统一转换为可读纯文本,保留常用段落换行。 */
|
||||
private String plainText(String value) {
|
||||
return HtmlUtil.unescape(HtmlUtil.cleanHtmlTag(StrUtil.blankToDefault(value, "")
|
||||
.replaceAll("(?i)<br\\s*/?>|</p>|</div>", "\n"))).trim();
|
||||
}
|
||||
|
||||
/** 使用 UTF-8 字节数校验接口上限,避免中文按字符计数造成发送失败。 */
|
||||
private void requireLength(String value, int limit, String label) {
|
||||
if (value.getBytes(StandardCharsets.UTF_8).length > limit) {
|
||||
throw new BaseException(label + "超过 " + limit + " 字节,请精简内容");
|
||||
}
|
||||
}
|
||||
|
||||
/** 相对路径按配置的系统域名补全;限制 HTTP(S),避免生成不可打开的卡片链接。 */
|
||||
private String resolveLink(String link) {
|
||||
if (StrUtil.isBlank(link)) return null;
|
||||
try {
|
||||
URI uri = URI.create(link.trim());
|
||||
if (!uri.isAbsolute()) {
|
||||
if (uri.getRawAuthority() != null) throw new IllegalArgumentException();
|
||||
String base = required(PREFIX + "link-base-url").replaceAll("/+$", "") + "/";
|
||||
uri = URI.create(base).resolve(uri);
|
||||
}
|
||||
if (!("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme()))
|
||||
|| uri.getHost() == null || uri.getUserInfo() != null) throw new IllegalArgumentException();
|
||||
return uri.toASCIIString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BaseException("企业微信详情链接或系统域名配置无效");
|
||||
}
|
||||
}
|
||||
|
||||
/** 按企业及应用隔离缓存,与登录 token 缓存分开;提前过期以避开有效期临界点。 */
|
||||
private synchronized String getAccessToken(boolean refresh) {
|
||||
String corpId = required("wxwork.corpId");
|
||||
String agentId = required("wxwork.agentId");
|
||||
String secret = required("wxwork.corpSecret");
|
||||
String key = RedisConstant.REDIS_KEY_WX_WORK_TOKEN + "message:" + corpId + ":" + agentId;
|
||||
String cached = refresh ? null : redisService.get(key);
|
||||
if (StrUtil.isNotBlank(cached)) return cached;
|
||||
JSONObject response = execute(HttpRequest.get(API + "gettoken?corpid=" + encode(corpId)
|
||||
+ "&corpsecret=" + encode(secret)));
|
||||
String token = response.getStr("access_token");
|
||||
if (response.getInt("errcode", -1) != 0 || StrUtil.isBlank(token)) {
|
||||
throw new BaseException("企业微信获取 token 失败,errcode=" + response.getInt("errcode", -1));
|
||||
}
|
||||
int expires = response.getInt("expires_in", 7200);
|
||||
redisService.setex(key, Math.max(1, expires - 120), token);
|
||||
return token;
|
||||
}
|
||||
|
||||
/** 提交已校验的消息 JSON;返回企业微信原始业务字段供调用方判断部分失败。 */
|
||||
private JSONObject sendPayload(JSONObject payload, String token) {
|
||||
return execute(HttpRequest.post(API + "message/send?access_token=" + encode(token))
|
||||
.header("Content-Type", "application/json; charset=UTF-8").body(payload.toString()));
|
||||
}
|
||||
|
||||
/** 关闭响应资源并校验 HTTP/JSON;异常不输出带 Secret 或 token 的请求 URL。 */
|
||||
protected JSONObject execute(HttpRequest request) {
|
||||
request.setConnectionTimeout(conf.getInt(PREFIX + "connect-timeout", 5000));
|
||||
request.setReadTimeout(conf.getInt(PREFIX + "read-timeout", 10000));
|
||||
try (HttpResponse response = request.execute()) {
|
||||
if (!response.isOk()) throw new BaseException("企业微信接口 HTTP 状态异常:" + response.getStatus());
|
||||
if (!JSONUtil.isTypeJSONObject(response.body())) throw new BaseException("企业微信接口返回非 JSON 对象");
|
||||
return JSONUtil.parseObj(response.body());
|
||||
} catch (BaseException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new BaseException("企业微信请求失败,请检查网络及接口配置;异常类型:" + e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/** 必填参数不允许为空或使用原有占位符 1,防止错误配置请求外部接口。 */
|
||||
private String required(String key) {
|
||||
String value = conf.get(key, "").trim();
|
||||
if (StrUtil.isBlank(value) || "1".equals(value)) throw new BaseException("请配置有效的 " + key);
|
||||
return value;
|
||||
}
|
||||
|
||||
private String encode(String value) {
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.budwk.app.flow.engine.event.ProcessEvent;
|
||||
import com.budwk.app.flow.engine.event.ProcessEventListener;
|
||||
import com.budwk.app.flow.entity.ProcessTask;
|
||||
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
|
||||
import com.budwk.app.flow.service.SchoolOaTodoService;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
@@ -17,11 +18,16 @@ public class ProcessTaskEndEventListener implements ProcessEventListener {
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@Inject
|
||||
private SchoolOaTodoService schoolOaTodoService;
|
||||
|
||||
@Override
|
||||
public void onEvent(ProcessEvent event) {
|
||||
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_END) {
|
||||
Long sourceId = event.getSourceId();
|
||||
ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
|
||||
// service 区分正常完成与废弃,只有正常完成才发送通达 status=2。
|
||||
schoolOaTodoService.completeTodo(task);
|
||||
System.out.println("流程任务完成事件:" + task.getDisplayName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.enums.ProcessEventTypeEnum;
|
||||
import com.budwk.app.flow.service.SchoolOaTodoService;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
@@ -22,11 +23,16 @@ public class ProcessTaskRevokeEventListener implements ProcessEventListener {
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@Inject
|
||||
private SchoolOaTodoService schoolOaTodoService;
|
||||
|
||||
@Override
|
||||
public void onEvent(ProcessEvent event) {
|
||||
if(event.getEventType()== ProcessEventTypeEnum.PROCESS_TASK_REVOKE){
|
||||
Long taskId = event.getSourceId();
|
||||
ProcessTask task = dao.fetch(ProcessTask.class, taskId);
|
||||
// 公开通达协议未定义取消,集中记录未同步事项,不把撤回冒充正常已办。
|
||||
schoolOaTodoService.cancelTodo(task);
|
||||
|
||||
String taskDisplayName = task.getDisplayName();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.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;
|
||||
@@ -30,13 +31,22 @@ public class ProcessTaskStartEventListener implements ProcessEventListener {
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private GlobalMessageSendService globalMessageSendService;
|
||||
@Inject
|
||||
private DifficultHelpCommonService difficultHelpCommonService;
|
||||
|
||||
@Override
|
||||
public void onEvent(ProcessEvent event) {
|
||||
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.budwk.app.flow.service;
|
||||
|
||||
import com.budwk.app.flow.entity.ProcessTask;
|
||||
|
||||
/** 通达 OA 待办同步入口,WF 调用方无需处理鉴权、人员映射或接口参数。 */
|
||||
public interface SchoolOaTodoService {
|
||||
/**
|
||||
* 推送已创建且已分配参与人的 WF 任务;首次自动提交的 startTask 不发送。
|
||||
* @param task 包含任务 ID、实例 ID、状态和表单地址的完整任务
|
||||
* @return 无返回值;开关关闭时跳过,开启后请求失败抛出业务异常
|
||||
*/
|
||||
void createTodo(ProcessTask task);
|
||||
|
||||
/**
|
||||
* 将已完成任务对应的接收人待办同步为 status=2;非完成状态不冒充已办。
|
||||
* @param task 数据库中的完整任务,使用原发起人、接收人、标题和地址组装请求
|
||||
* @return 无返回值;errcode=0 表示接口接受请求,原待办匹配规则仍需学校联调确认
|
||||
*/
|
||||
void completeTodo(ProcessTask task);
|
||||
|
||||
/**
|
||||
* 记录需取消的 OA 待办。通达公开文档未提供取消接口,本方法不发送虚构的删除请求。
|
||||
* @param task 撤回、废弃或转办的原任务,用于记录任务 ID 和状态
|
||||
* @return 无返回值;仅在开关开启时输出待人工处理提示,不代表 OA 已取消
|
||||
*/
|
||||
void cancelTodo(ProcessTask task);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import com.budwk.app.flow.enums.*;
|
||||
import com.budwk.app.flow.service.ProcessDefineService;
|
||||
import com.budwk.app.flow.service.ProcessInstanceService;
|
||||
import com.budwk.app.flow.service.ProcessTaskService;
|
||||
import com.budwk.app.flow.service.SchoolOaTodoService;
|
||||
import com.budwk.app.flow.vo.ProcessTaskVO;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
@@ -56,6 +57,9 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
@Inject
|
||||
private ProcessDefineService processDefineService;
|
||||
|
||||
@Inject
|
||||
private SchoolOaTodoService schoolOaTodoService;
|
||||
|
||||
@Override
|
||||
public boolean save(ProcessTask param) {
|
||||
param.setId(null);
|
||||
@@ -213,6 +217,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
System.out.println("创建任务:" + processTask.getTaskName() + "," + processTask.getDisplayName());
|
||||
processTaskList.add(processTask);
|
||||
addTaskActor(processTask.getId(), getTaskActors(taskModel, execution));
|
||||
// 参与人落库后统一推送通达待办,开始监听器不再重复推送。
|
||||
schoolOaTodoService.createTodo(processTask);
|
||||
|
||||
return processTaskList;
|
||||
}
|
||||
@@ -284,6 +290,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
clone.setUpdatedBy(null);
|
||||
dao().insert(clone);
|
||||
addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor));
|
||||
// 并行加签绕过普通节点创建入口,需要为新增的个人任务同步待办。
|
||||
schoolOaTodoService.createTodo(clone);
|
||||
});
|
||||
|
||||
}
|
||||
@@ -318,6 +326,7 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public ProcessTask rejectTask(ProcessModel model, ProcessTask currentTask) {
|
||||
Long taskParentId = currentTask.getTaskParentId();
|
||||
if (ObjectUtil.isEmpty(taskParentId) || taskParentId.equals(0L)) {
|
||||
@@ -351,6 +360,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
}
|
||||
saveProcessTask(task);
|
||||
addTaskActor(task.getId(), CollectionUtil.newArrayList(task.getOperator()));
|
||||
// 驳回创建了新的任务 ID,按新任务推送,避免复用历史待办。
|
||||
schoolOaTodoService.createTodo(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
@@ -424,6 +435,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
System.out.println("创建会签任务:" + processTask.getTaskName() + "," + processTask.getDisplayName());
|
||||
processTaskList.add(processTask);
|
||||
addTaskActor(processTask.getId(), CollectionUtil.newArrayList(taskActor));
|
||||
// 串行和并行会签均按实际生成的个人任务发送待办。
|
||||
schoolOaTodoService.createTodo(processTask);
|
||||
});
|
||||
ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class);
|
||||
// 更新会签变量到流程实例参数中
|
||||
@@ -496,9 +509,12 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
||||
|
||||
// 创建转办任务参与者
|
||||
addTaskActor(newTask.getId(), List.of(transferUserId));
|
||||
// 通达取消协议待学校确认;先明确记录原待办未清理,再向新办理人推送。
|
||||
schoolOaTodoService.cancelTodo(task);
|
||||
schoolOaTodoService.createTodo(newTask);
|
||||
|
||||
// 发布流程任务开始事件
|
||||
List<ProcessTask> processTaskList = List.of(task);
|
||||
List<ProcessTask> processTaskList = List.of(newTask);
|
||||
processTaskList.forEach(processTask -> {
|
||||
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_START).sourceId(processTask.getId()).build());
|
||||
});
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
package com.budwk.app.flow.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.flow.constant.FlowConst;
|
||||
import com.budwk.app.flow.entity.ProcessInstance;
|
||||
import com.budwk.app.flow.entity.ProcessTask;
|
||||
import com.budwk.app.flow.entity.ProcessTaskActor;
|
||||
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||
import com.budwk.app.flow.service.SchoolOaTodoService;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.web.commons.base.Globals;
|
||||
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.impl.PropertiesProxy;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 通达 V13 流程中心接口适配。所有配置、token 和 HTTP 请求集中在本服务。
|
||||
* 接口依据:https://open.tongda2000.com/oa/v13/interface.html
|
||||
* 同步请求失败向调用方抛出异常;本地事务不能撤销已经成功的远端请求。
|
||||
*/
|
||||
@Slf4j
|
||||
@IocBean
|
||||
public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
|
||||
private static final String PREFIX = "school-oa.todo.";
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private PropertiesProxy conf;
|
||||
|
||||
// token 由同步方法保护;实例内缓存有效期,避免每位接收人重复申请凭据。
|
||||
private String accessToken;
|
||||
private long tokenExpiresAt;
|
||||
private String tokenConfig;
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void createTodo(ProcessTask task) {
|
||||
if (!isEnabled()) return;
|
||||
requireTask(task);
|
||||
if (isInitialTask(task) || !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) return;
|
||||
sendTask(task, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void completeTodo(ProcessTask task) {
|
||||
if (!isEnabled()) return;
|
||||
requireTask(task);
|
||||
if (isInitialTask(task)) return;
|
||||
// 废弃任务也会发布结束事件;尤其提案附议仍可能可办理,不能直接改为 OA 已办。
|
||||
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) {
|
||||
cancelTodo(task);
|
||||
return;
|
||||
}
|
||||
sendTask(task, 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelTodo(ProcessTask task) {
|
||||
if (!isEnabled()) return;
|
||||
requireTask(task);
|
||||
if (isInitialTask(task)) return;
|
||||
log.warn("通达OA取消规则尚未提供,本次未发送取消请求,请核对OA原待办:taskId={},instanceId={},state={}",
|
||||
task.getId(), task.getProcessInstanceId(), task.getTaskState());
|
||||
}
|
||||
|
||||
/** 系统参数优先于文件配置,只有明确为 true 才启用;未配置不访问数据库或 OA。 */
|
||||
private boolean isEnabled() {
|
||||
Object enabled = Globals.MyConfig.get("AppSchoolOa");
|
||||
return Boolean.parseBoolean(enabled == null ? conf.get(PREFIX + "enabled", "false") : enabled.toString());
|
||||
}
|
||||
|
||||
private void requireTask(ProcessTask task) {
|
||||
if (task == null || task.getId() == null || task.getProcessInstanceId() == null) {
|
||||
throw new BaseException("通达OA同步失败:任务或流程实例ID为空");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInitialTask(ProcessTask task) {
|
||||
return Long.valueOf(0L).equals(task.getTaskParentId()) && "startTask".equals(task.getTaskName());
|
||||
}
|
||||
|
||||
/**
|
||||
* task 为完整 WF 任务;status 只允许本服务传入 1(待办)或 2(已办)。
|
||||
* sender/recipient 使用学校绑定的第三方用户标识;content 为 JSON 字符串,url 为 UTF-8 Base64。
|
||||
* 每个接收人单独请求,响应 JSON 中必须有 errcode=0;不采用参考项目的 uniqueId/state 协议。
|
||||
*/
|
||||
private void sendTask(ProcessTask task, int status) {
|
||||
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
|
||||
if (instance == null) throw new BaseException("通达OA同步失败:流程实例不存在");
|
||||
Sys_user sender = dao.fetch(Sys_user.class, instance.getOperator());
|
||||
if (sender == null) throw new BaseException("通达OA同步失败:流程发起人不存在");
|
||||
List<ProcessTaskActor> actors = dao.query(ProcessTaskActor.class,
|
||||
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
|
||||
if (actors.isEmpty()) throw new BaseException("通达OA同步失败:任务参与人为空");
|
||||
|
||||
String senderId = userIdentifier(sender.getId(), sender.getLoginname());
|
||||
String title = StrUtil.blankToDefault(JSONUtil.parseObj(StrUtil.blankToDefault(instance.getVariable(), "{}"))
|
||||
.getStr(FlowConst.PROCESS_INSTANCE_NAME), "流程申请") + " - " + task.getDisplayName();
|
||||
String targetUrl = buildTaskUrl(task, instance);
|
||||
String systemParam = conf.get(PREFIX + "system-id-param", "clientid");
|
||||
if (!"clientid".equals(systemParam) && !"exsId".equals(systemParam)) {
|
||||
throw new BaseException("通达OA配置 system-id-param 仅支持 clientid 或 exsId");
|
||||
}
|
||||
Map<String, Object> params = new LinkedHashMap<>();
|
||||
params.put(systemParam, requireConfig("system-id"));
|
||||
params.put("status", status);
|
||||
params.put("sender", senderId);
|
||||
params.put("content", JSONUtil.quote(title));
|
||||
params.put("url", Base64.getEncoder().encodeToString(targetUrl.getBytes(StandardCharsets.UTF_8)));
|
||||
// 先校验全部人员,避免处理到中途才发现缺少账号;不假设本地用户 ID 就是 OA 用户 ID。
|
||||
List<String> recipients = actors.stream()
|
||||
.filter(actor -> !FlowConst.ADMIN_ID.equals(actor.getActorId())
|
||||
&& !"superadmin".equalsIgnoreCase(actor.getActorAccount()))
|
||||
.map(actor -> userIdentifier(actor.getActorId(), actor.getActorAccount())).distinct().toList();
|
||||
for (String recipient : recipients) {
|
||||
params.put("recipient", recipient);
|
||||
String endpoint = baseUrl() + "/restapi/1_0/appcenter/affairprocessing?" + encodeParams(params);
|
||||
JSONObject result = execute(HttpRequest.get(endpoint).header("Authorization", "Bearer " + getAccessToken()), "待办同步");
|
||||
Integer errorCode = result.getInt("errcode");
|
||||
if (errorCode == null || errorCode != 0) {
|
||||
String detail = responseError(result);
|
||||
log.error("通达OA待办业务失败,taskId={},instanceId={},status={},errcode={},detail={}",
|
||||
task.getId(), task.getProcessInstanceId(), status, errorCode, detail);
|
||||
throw new BaseException("通达OA待办同步失败:errcode={},taskId={},{}", errorCode, task.getId(), detail);
|
||||
}
|
||||
log.info("通达OA接受待办同步请求,taskId={},status={}", task.getId(), status);
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户映射按学校绑定选择 loginname 或 id,发送人与接收人使用同一规则。 */
|
||||
private String userIdentifier(String id, String account) {
|
||||
String mode = conf.get(PREFIX + "user-id-mode", "loginname");
|
||||
if (!"loginname".equals(mode) && !"id".equals(mode)) {
|
||||
throw new BaseException("通达OA配置 user-id-mode 仅支持 loginname 或 id");
|
||||
}
|
||||
String value = "id".equals(mode) ? id : account;
|
||||
if (StrUtil.isBlank(value) || value.contains(",")) {
|
||||
throw new BaseException("通达OA同步失败:绑定用户标识为空或包含逗号");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 通达仅有一个 url 字段,沿用当前 PC 审核页并携带任务、实例及业务参数。 */
|
||||
private String buildTaskUrl(ProcessTask task, ProcessInstance instance) {
|
||||
String form = task.getFormKey();
|
||||
if (StrUtil.isBlank(form)) throw new BaseException("通达OA同步失败:任务PC表单地址为空");
|
||||
String url = form.startsWith("http://") || form.startsWith("https://") ? form
|
||||
: StrUtil.removeSuffix(Globals.AppDomain, "/") + "/" + StrUtil.removePrefix(form, "/");
|
||||
// 原表单带锚点时,查询参数必须放在 # 前,否则服务端收不到 taskId。
|
||||
int fragmentIndex = url.indexOf('#');
|
||||
String fragment = fragmentIndex < 0 ? "" : url.substring(fragmentIndex);
|
||||
if (fragmentIndex >= 0) url = url.substring(0, fragmentIndex);
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("taskId", task.getId());
|
||||
query.put("instanceId", instance.getId());
|
||||
query.put("bizId", StrUtil.nullToEmpty(instance.getBusinessNo()));
|
||||
query.put("taskKey", StrUtil.nullToEmpty(task.getTaskName()));
|
||||
return url + (url.contains("?") ? "&" : "?") + encodeParams(query) + fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /oauth2/token:client_secret 必须是学校提供的 AES 加密结果,不是明文密钥。
|
||||
* 返回 access_token 和 expires_in(秒);缓存提前过期,配置变化立即重新申请。
|
||||
*/
|
||||
private synchronized String getAccessToken() {
|
||||
String base = baseUrl();
|
||||
String clientId = requireConfig("client-id");
|
||||
String secret = requireConfig("client-secret-encrypted");
|
||||
String username = conf.get(PREFIX + "token-username", "username");
|
||||
String password = conf.get(PREFIX + "token-password", "password");
|
||||
String currentConfig = JSONUtil.toJsonStr(List.of(base, clientId, secret, username, password));
|
||||
long now = System.currentTimeMillis();
|
||||
if (currentConfig.equals(tokenConfig) && StrUtil.isNotBlank(accessToken) && now < tokenExpiresAt) {
|
||||
// 只记录缓存剩余有效期,便于区分复用与重新获取,不输出凭据。
|
||||
log.info("通达OA复用token缓存,remainingSeconds={}", (tokenExpiresAt - now) / 1000L);
|
||||
return accessToken;
|
||||
}
|
||||
Map<String, Object> form = new LinkedHashMap<>();
|
||||
form.put("grant_type", "password");
|
||||
form.put("client_id", clientId);
|
||||
form.put("client_secret", secret);
|
||||
form.put("username", username);
|
||||
form.put("password", password);
|
||||
long startedAt = System.nanoTime();
|
||||
JSONObject result = execute(HttpRequest.post(base + "/oauth2/token")
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(encodeParams(form)), "获取凭据");
|
||||
String token = result.getStr("access_token");
|
||||
Long expires = result.getLong("expires_in");
|
||||
if (StrUtil.isBlank(token) || expires == null || expires <= 0 || expires > Integer.MAX_VALUE) {
|
||||
String detail = responseError(result);
|
||||
log.error("通达OA获取凭据业务失败,缺少有效的 access_token 或 expires_in,detail={}", detail);
|
||||
throw new BaseException("通达OA获取凭据失败:缺少有效的 access_token 或 expires_in,{}", detail);
|
||||
}
|
||||
accessToken = token;
|
||||
tokenConfig = currentConfig;
|
||||
tokenExpiresAt = now + (expires - Math.min(60, expires / 10)) * 1000L;
|
||||
log.info("通达OA获取token成功,elapsedMs={},expiresInSeconds={}",
|
||||
(System.nanoTime() - startedAt) / 1_000_000, expires);
|
||||
return token;
|
||||
}
|
||||
|
||||
/** HTTP 成功不代表业务成功;本方法只验证状态码和 JSON,业务码由调用处校验。 */
|
||||
private JSONObject execute(HttpRequest request, String action) {
|
||||
long startedAt = System.nanoTime();
|
||||
int timeout = conf.getInt(PREFIX + "timeout-ms", 10000);
|
||||
// 只记录接口地址,不记录 GET 查询参数、请求头或 token 请求正文。
|
||||
String endpoint = request.getUrl().split("[?#]", 2)[0];
|
||||
try (HttpResponse response = request.timeout(timeout).execute()) {
|
||||
String body = response.body();
|
||||
boolean jsonObject = JSONUtil.isTypeJSONObject(body);
|
||||
// 非 JSON 错误常为 OA 或网关的 HTML 页面;仅在日志保留脱敏摘要,不把页面返回给前端。
|
||||
if (!jsonObject) {
|
||||
log.error("通达OA非JSON响应,action={},method={},endpoint={},httpStatus={},contentType={},bodySummary={}",
|
||||
action, request.getMethod(), endpoint, response.getStatus(),
|
||||
redact(response.header("Content-Type")), responseBodySummary(body));
|
||||
}
|
||||
if (!response.isOk()) {
|
||||
if (response.getStatus() == 401) invalidateToken();
|
||||
String detail = jsonObject
|
||||
? responseError(JSONUtil.parseObj(body)) : "响应不是JSON对象,脱敏响应摘要请查看服务端日志";
|
||||
throw new BaseException("通达OA{}失败:HTTP {},{}", action, response.getStatus(), detail);
|
||||
}
|
||||
if (!jsonObject) throw new BaseException("通达OA{}失败:响应不是JSON对象,脱敏响应摘要请查看服务端日志", action);
|
||||
return JSONUtil.parseObj(body);
|
||||
} catch (Exception e) {
|
||||
long elapsed = (System.nanoTime() - startedAt) / 1_000_000;
|
||||
Throwable root = e;
|
||||
Map<Throwable, Boolean> visited = new IdentityHashMap<>();
|
||||
while (root.getCause() != null && visited.put(root, Boolean.TRUE) == null
|
||||
&& !visited.containsKey(root.getCause())) root = root.getCause();
|
||||
String detail = root.getClass().getName() + ": " + redact(root.getMessage());
|
||||
// 保留各层原因、原始栈帧及 suppressed 异常;先复制脱敏,避免全局异常处理器泄露凭证。
|
||||
Throwable safeCause = diagnosticCause(e, new IdentityHashMap<>());
|
||||
log.error("通达OA请求失败,action={},method={},endpoint={},elapsedMs={},timeoutMs={},rootCause={}",
|
||||
action, request.getMethod(), endpoint, elapsed, timeout, detail, safeCause);
|
||||
// 不自动重放写请求,超时可能发生在远端已成功之后;避免重复待办。
|
||||
BaseException failure = new BaseException("通达OA{}失败,耗时{}ms:{};请核对远端结果后再重试", action, elapsed, detail);
|
||||
failure.initCause(safeCause);
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
/** body 为远端非 JSON 正文;先脱敏再截取最多 2000 字符,避免截断凭据后无法匹配脱敏。 */
|
||||
private String responseBodySummary(String body) {
|
||||
if (StrUtil.isBlank(body)) return "[空响应正文]";
|
||||
String safe = redact(body);
|
||||
return safe.length() > 2000 ? safe.substring(0, 2000) + "[已截断]" : safe;
|
||||
}
|
||||
|
||||
/** 只提取接口错误字段,不输出含 access_token、refresh_token 等内容的整个响应。 */
|
||||
private String responseError(JSONObject response) {
|
||||
Map<String, Object> errors = new LinkedHashMap<>();
|
||||
for (String key : List.of("error", "error_description", "errcode", "errmsg", "message")) {
|
||||
if (response.containsKey(key)) errors.put(key, redact(String.valueOf(response.get(key))));
|
||||
}
|
||||
return errors.isEmpty() ? "OA未返回错误说明" : JSONUtil.toJsonStr(errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制异常诊断信息,message 标注原异常类型,保留原始栈帧和原因链。
|
||||
* visited 防止第三方异常存在循环引用;原异常不修改,也不直接写入日志。
|
||||
*/
|
||||
private Throwable diagnosticCause(Throwable source, Map<Throwable, Throwable> visited) {
|
||||
if (visited.containsKey(source)) return visited.get(source);
|
||||
RuntimeException copy = new RuntimeException(source.getClass().getName() + ": " + redact(source.getMessage()));
|
||||
visited.put(source, copy);
|
||||
copy.setStackTrace(source.getStackTrace());
|
||||
if (source.getCause() != null && !visited.containsKey(source.getCause())) {
|
||||
copy.initCause(diagnosticCause(source.getCause(), visited));
|
||||
}
|
||||
for (Throwable suppressed : source.getSuppressed()) {
|
||||
if (!visited.containsKey(suppressed)) copy.addSuppressed(diagnosticCause(suppressed, visited));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** 清除已知凭证及可能被远端回显的认证字段、URL查询参数,并移除日志换行。 */
|
||||
private String redact(String message) {
|
||||
if (message == null) return "无异常说明";
|
||||
String text = message;
|
||||
for (String key : List.of("client-secret-encrypted", "token-password", "client-id")) {
|
||||
String value = conf.get(PREFIX + key);
|
||||
if (StrUtil.isNotBlank(value)) {
|
||||
text = text.replace(value, "[REDACTED]")
|
||||
.replace(URLEncoder.encode(value, StandardCharsets.UTF_8), "[REDACTED]");
|
||||
}
|
||||
}
|
||||
if (StrUtil.isNotBlank(accessToken)) text = text.replace(accessToken, "[REDACTED]");
|
||||
text = text.replaceAll("(?i)Bearer\\s+[^\\s,;\\\"']+", "Bearer [REDACTED]");
|
||||
text = text.replaceAll("(?i)((?:client_secret|access_token|refresh_token|password|authorization)[\\\"']?\\s*[:=]\\s*)[\\\"']?[^\\s&,;}\\\"']+[\\\"']?", "$1[REDACTED]");
|
||||
text = text.replaceAll("(https?://[^\\s?#]+)\\?[^\\s]+", "$1?[REDACTED]");
|
||||
return text.replace('\r', ' ').replace('\n', ' ');
|
||||
}
|
||||
|
||||
private synchronized void invalidateToken() {
|
||||
accessToken = null;
|
||||
tokenExpiresAt = 0;
|
||||
}
|
||||
|
||||
private String baseUrl() {
|
||||
String value = requireConfig("base-url");
|
||||
URI uri = URI.create(value);
|
||||
if ((!"http".equals(uri.getScheme()) && !"https".equals(uri.getScheme()))
|
||||
|| uri.getHost() == null || uri.getQuery() != null || uri.getFragment() != null || uri.getUserInfo() != null) {
|
||||
throw new BaseException("通达OA base-url 必须为不含账号、查询参数和锚点的HTTP(S)地址");
|
||||
}
|
||||
return StrUtil.removeSuffix(value, "/");
|
||||
}
|
||||
|
||||
private String requireConfig(String key) {
|
||||
String value = conf.get(PREFIX + key);
|
||||
if (StrUtil.isBlank(value)) throw new BaseException("通达OA配置 {} 未填写", PREFIX + key);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
/** 每个参数单独编码,保留标题中的中文、引号及 Base64 中的 +、/、=。 */
|
||||
private String encodeParams(Map<String, Object> params) {
|
||||
return params.entrySet().stream().map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)
|
||||
+ "=" + URLEncoder.encode(String.valueOf(entry.getValue()), StandardCharsets.UTF_8))
|
||||
.collect(Collectors.joining("&"));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import cn.hutool.http.HtmlUtil;
|
||||
import com.budwk.app.base.constant.RedisConstant;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.base.sms.SmsService;
|
||||
import com.budwk.app.base.wxwork.WxWorkMessageService;
|
||||
import com.budwk.app.base.sms.model.MsgPlatformWechatRequest;
|
||||
import com.budwk.app.base.utils.ManyAddOrRenewUtil;
|
||||
import com.budwk.app.sys.models.Sys_msg;
|
||||
@@ -62,6 +63,8 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
||||
private ManyAddOrRenewUtil manyAddOrRenewUtil;
|
||||
@Inject
|
||||
private SmsService smsService;
|
||||
@Inject
|
||||
private WxWorkMessageService wxWorkMessageService;
|
||||
|
||||
private static NutMap typeMap = NutMap.NEW().addv("system", "系统消息").addv("user", "用户消息");
|
||||
|
||||
@@ -102,14 +105,29 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
||||
}
|
||||
});
|
||||
|
||||
//发送学校平台消息
|
||||
ThreadUtil.execute(() -> {
|
||||
for (String loginName : loginNames) {
|
||||
smsService.sendSmsByAccount(loginName,sysMsg.getNote());
|
||||
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
|
||||
}
|
||||
|
||||
});
|
||||
// isExternal=false 为仅站内通知;外部通道分别处理异常,避免短信失败阻断企业微信。
|
||||
if (isExternal) {
|
||||
ThreadUtil.execute(() -> {
|
||||
for (String loginName : loginNames) {
|
||||
try {
|
||||
smsService.sendSmsByAccount(loginName, sysMsg.getNote());
|
||||
} catch (Exception e) {
|
||||
log.error("消息短信发送失败,msgId=" + sysMsg.getId(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
ThreadUtil.execute(() -> {
|
||||
try {
|
||||
// 学校企业微信 userid 直接使用 loginname;批量发送标题、正文及业务链接。
|
||||
NutMap result = wxWorkMessageService.massSend(loginNames, sysMsg.getTitle(), sysMsg.getNote(), sysMsg.getUrl());
|
||||
if (!result.getBoolean("skipped", false) && !result.getBoolean("success", false)) {
|
||||
log.warn("企业微信消息存在失败接收人,msgId=" + sysMsg.getId() + ",result=" + result);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("企业微信消息发送失败,msgId=" + sysMsg.getId(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sysMsg;
|
||||
}
|
||||
@@ -292,11 +310,13 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void sendMsg(String loginname, String title, String body, String sender) {
|
||||
sendMsg(List.of(loginname), title, body, SecurityUtil.getUserId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void sendMsg(List<String> loginNames, String title, String body, String sender) {
|
||||
Sys_msg sysMsg = new Sys_msg();
|
||||
sysMsg.setTitle(title);
|
||||
@@ -309,6 +329,7 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void sendMsgInSys(List<String> loginNames, String title, String body, String sender) {
|
||||
Sys_msg sysMsg = new Sys_msg();
|
||||
sysMsg.setTitle(title);
|
||||
|
||||
@@ -189,25 +189,16 @@ public class WkFailProcessor extends ViewProcessor {
|
||||
if (Globals.sso) {
|
||||
//1.判断是否为企业微信浏览器并且对接了企业微信
|
||||
if (browserPlatform == BrowserPlatform.QI_YE_WEI_XIN && propertiesProxy.getBoolean("wxwork.enable", false)) {
|
||||
//进行企业微信授权
|
||||
// String token = getWxWorkToken();
|
||||
// if (StrUtil.isNotBlank(token)) {
|
||||
// //跳转到企业微信授权地址
|
||||
// new ServerRedirectView(buildWxWorkOauth2Url()).render(request, response, null);
|
||||
// } else {
|
||||
// //跳转到错误页 别问我为啥错了 我也不知道 首先去检查配置文件里的参数有没有写对
|
||||
//// new ServerRedirectView(Globals.AppDomain + errorUri).render(request, response, null);
|
||||
// //跳转到cas登录页
|
||||
// String callBackUrl = propertiesProxy.get("cas.client-host-url") + propertiesProxy.get("cas.client-call-back-url") + "?redirect=" + redirect;
|
||||
// String authUrl = propertiesProxy.get("cas.qi-wx-server-login-url") + "?service=" + URLEncoder.encode(callBackUrl, StandardCharsets.UTF_8);
|
||||
// new ServerRedirectView(authUrl).render(request, response, null);
|
||||
// }
|
||||
|
||||
// 武汉城市职业学院 跳转到企业微信cas登录页
|
||||
String callBackUrl = propertiesProxy.get("wecom.wecom-client-host-url") + propertiesProxy.get("wecom.wecom-client-call-back-url") + "?redirect=" + redirect;
|
||||
String authUrl = propertiesProxy.get("wecom.wecom-server-login-url") + "?service=" + URLEncoder.encode(callBackUrl, StandardCharsets.UTF_8);
|
||||
log.info("auth:"+authUrl);
|
||||
new ServerRedirectView(authUrl).render(request, response, null);
|
||||
// 专用企业微信认证配置缺失时整组回退到已接通的 CAS,保留卡片原始页面。
|
||||
try {
|
||||
String authUrl = buildEnterpriseCasUrl(redirect);
|
||||
log.info("auth:" + authUrl);
|
||||
new ServerRedirectView(authUrl).render(request, response, null);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("企业微信登录跳转配置无效:" + e.getMessage());
|
||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
|
||||
"统一身份认证配置不完整或无效,请联系管理员检查认证地址及回调地址");
|
||||
}
|
||||
} else if (browserPlatform == BrowserPlatform.WEI_XIN && propertiesProxy.getBoolean("wx.enable", false)) {
|
||||
//进行微信公众号授权
|
||||
//跳转到微信授权页
|
||||
@@ -225,6 +216,53 @@ public class WkFailProcessor extends ViewProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造企业微信内的 CAS 登录地址。redirect 为已 URL 编码的原始站内路径及查询参数;
|
||||
* 返回携带 service 参数的完整认证 URL,认证回调通过 redirect 返回卡片页面。
|
||||
* 三个 wecom 配置必须同时存在,否则整体使用 cas 配置,避免混用两套认证平台。
|
||||
*/
|
||||
private String buildEnterpriseCasUrl(String redirect) {
|
||||
String loginUrl = propertiesProxy.get("wecom.wecom-server-login-url");
|
||||
String clientHost = propertiesProxy.get("wecom.wecom-client-host-url");
|
||||
String callbackPath = propertiesProxy.get("wecom.wecom-client-call-back-url");
|
||||
if (missingAuthConfig(loginUrl) || missingAuthConfig(clientHost) || missingAuthConfig(callbackPath)) {
|
||||
loginUrl = propertiesProxy.get("cas.server-login-url");
|
||||
clientHost = propertiesProxy.get("cas.client-host-url");
|
||||
callbackPath = propertiesProxy.get("cas.client-call-back-url");
|
||||
}
|
||||
if (missingAuthConfig(loginUrl) || missingAuthConfig(clientHost) || missingAuthConfig(callbackPath)) {
|
||||
throw new IllegalArgumentException("CAS 登录地址、系统域名或回调路径为空");
|
||||
}
|
||||
loginUrl = loginUrl.trim();
|
||||
clientHost = clientHost.trim();
|
||||
callbackPath = callbackPath.trim();
|
||||
validateAuthUrl(loginUrl);
|
||||
validateAuthUrl(clientHost);
|
||||
// 回调必须是本系统路径,不能被绝对 URL 或双斜线路径覆盖到其他主机。
|
||||
if (!callbackPath.startsWith("/") || callbackPath.startsWith("//") || callbackPath.contains("#")) {
|
||||
throw new IllegalArgumentException("认证回调必须是以单斜线开头的站内路径");
|
||||
}
|
||||
String callback = clientHost.replaceAll("/+$", "") + callbackPath
|
||||
+ (callbackPath.contains("?") ? "&" : "?") + "redirect=" + redirect;
|
||||
// 保留部署配置中的 HTTP/HTTPS 协议;工会 HTTPS 回调不会被改为 HTTP。
|
||||
return loginUrl + (loginUrl.contains("?") ? "&" : "?") + "service="
|
||||
+ URLEncoder.encode(callback, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/** 空值和字面量 null 都视为未配置,防止拼接出浏览器相对地址 null?service=...。 */
|
||||
private boolean missingAuthConfig(String value) {
|
||||
return StrUtil.isBlank(value) || "null".equalsIgnoreCase(value.trim());
|
||||
}
|
||||
|
||||
/** 认证地址和回调域名必须为完整 HTTP(S) URL,拒绝无主机名、片段或用户信息的地址。 */
|
||||
private void validateAuthUrl(String value) {
|
||||
java.net.URI uri = java.net.URI.create(value);
|
||||
if (!("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme()))
|
||||
|| uri.getHost() == null || uri.getFragment() != null || uri.getUserInfo() != null) {
|
||||
throw new IllegalArgumentException("认证配置不是有效的 HTTP(S) 地址");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断浏览器平台
|
||||
*
|
||||
|
||||
+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转换为实体列表(空值判断避免转换异常)
|
||||
|
||||
+32
-26
@@ -28,10 +28,10 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
|
||||
import com.budwk.app.zhgh.staffbenefit.condolence.model.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;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
@@ -47,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;
|
||||
|
||||
@@ -59,6 +57,7 @@ import java.util.List;
|
||||
* @Version 1.0
|
||||
* @Description TODO
|
||||
*/
|
||||
@Slf4j
|
||||
@IocBean
|
||||
@At("/platform/condolence/mine")
|
||||
@Api("职工慰问我的")
|
||||
@@ -157,6 +156,7 @@ public class CondolenceMineController {
|
||||
@ApiOperation("导出职工慰问申请表")
|
||||
@SaCheckPermission("condolence.mine")
|
||||
public void doExportApply(String id, HttpServletResponse response, Boolean print) throws Exception {
|
||||
log.info("[职工慰问导出] 开始,applyId={}, userId={}, format={}", id, SecurityUtil.getUserId(), Boolean.TRUE.equals(print) ? "PDF" : "Word");
|
||||
Condolence condolence = condolenceService.fetch(id);
|
||||
if (condolence == null) throw new BaseException("该条记录不存在");
|
||||
if (!SecurityUtil.getUserId().equals(condolence.getCreatedBy())) {
|
||||
@@ -167,36 +167,32 @@ public class CondolenceMineController {
|
||||
if (instance == null || !ProcessInstanceStateEnum.FINISHED.getCode().equals(instance.getState())) {
|
||||
throw new BaseException("只能导出已完成的申请");
|
||||
}
|
||||
log.info("[职工慰问导出] 流程已完成,applyId={}, instanceId={}, state={}, applicantSignPresent={}", id, instance.getId(), instance.getState(), StrUtil.isNotBlank(condolence.getSignature()));
|
||||
NutMap docData = Lang.obj2nutmap(condolence);
|
||||
docData.put("payUserName", StrUtil.nullToEmpty(condolence.getBankUserName()));
|
||||
CondolenceType type = condolenceService.dao().fetch(CondolenceType.class, condolence.getType());
|
||||
docData.put("typeName", type == null ? "" : type.getName());
|
||||
docData.put("signature", "");
|
||||
docData.put("signature", null);
|
||||
if (StrUtil.isNotBlank(condolence.getSignature())) {
|
||||
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, condolence.getSignature());
|
||||
if (signature == null) throw new BaseException("申请人签字图片读取失败");
|
||||
docData.put("signature", signature);
|
||||
}
|
||||
fillApplyApprovals(docData, instance);
|
||||
for (String field : List.of("signature", "firstApprovalSign", "secondApprovalSign", "thirdApprovalSign", "fourthApprovalSign")) {
|
||||
Object value = docData.get(field);
|
||||
log.info("[职工慰问导出] 渲染前图片字段,applyId={}, instanceId={}, field={}, valueType={}", id, instance.getId(), field, value == null ? "null" : value.getClass().getName());
|
||||
}
|
||||
String fileName = "重庆工贸职业技术学院职工慰问申请表_" + StrUtil.nullToEmpty(condolence.getHelpUserName());
|
||||
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);
|
||||
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateOutput.toByteArray())) {
|
||||
XWPFTemplate.compile(templateInput).render(docData).writeAndClose(output);
|
||||
}
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,29 +202,39 @@ public class CondolenceMineController {
|
||||
String[] fields = {"firstApproval", "secondApproval", "thirdApproval", "fourthApproval"};
|
||||
List<String> suffixes = List.of("Opinion", "Sign", "Year", "Month", "Day");
|
||||
for (String field : fields) {
|
||||
for (String suffix : suffixes) docData.put(field + suffix, "");
|
||||
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
|
||||
}
|
||||
List<ProcessTask> tasks = condolenceService.dao().query(ProcessTask.class,
|
||||
Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
|
||||
log.info("[职工慰问导出] 查询审批记录,applyId={}, instanceId={}, taskCount={}, expectedTaskKeys={}", instance.getBusinessNo(), instance.getId(), tasks.size(), List.of(taskKeys));
|
||||
for (ProcessTask task : tasks) {
|
||||
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
|
||||
if ("startTask".equals(task.getTaskName()) || (taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
|
||||
log.info("[职工慰问导出] 审批记录,instanceId={}, taskId={}, taskKey={}, displayName={}, state={}, finishTime={}, isFirstTaskNode={}, matchedApproval={}", instance.getId(), task.getId(), task.getTaskName(), task.getDisplayName(), task.getTaskState(), task.getFinishTime(), taskVO.getExt() == null ? null : taskVO.getExt().get("isFirstTaskNode"), List.of(taskKeys).contains(task.getTaskName()));
|
||||
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
|
||||
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
|
||||
log.info("[职工慰问导出] 申请节点重置本轮审批字段,instanceId={}, taskId={}, taskKey={}", instance.getId(), task.getId(), task.getTaskName());
|
||||
// 重新申请后,仅导出本轮审批记录。
|
||||
for (String field : fields) {
|
||||
for (String suffix : suffixes) docData.put(field + suffix, "");
|
||||
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < taskKeys.length; i++) {
|
||||
if (!taskKeys[i].equals(task.getTaskName())) continue;
|
||||
for (String suffix : suffixes) docData.put(fields[i] + suffix, "");
|
||||
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) continue;
|
||||
for (String suffix : suffixes) docData.put(fields[i] + suffix, "Sign".equals(suffix) ? null : "");
|
||||
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) {
|
||||
log.info("[职工慰问导出] 跳过未完成审批,instanceId={}, taskId={}, field={}, state={}", instance.getId(), task.getId(), fields[i], task.getTaskState());
|
||||
continue;
|
||||
}
|
||||
Dict data = taskVO.getExt() == null ? Dict.create() : taskVO.getTaskFormData();
|
||||
log.info("[职工慰问导出] 读取审批表单,instanceId={}, taskId={}, field={}, formKeys={}, signPresent={}, signReferenceLength={}", instance.getId(), task.getId(), fields[i], data.keySet(), StrUtil.isNotBlank(data.getStr("sign")), StrUtil.nullToEmpty(data.getStr("sign")).length());
|
||||
if (StrUtil.isBlank(data.getStr("sign"))) log.warn("[职工慰问导出] 已完成审批缺少签名字段,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i]);
|
||||
docData.put(fields[i] + "Opinion", StrUtil.nullToEmpty(data.getStr("opinion")));
|
||||
if (StrUtil.isNotBlank(data.getStr("sign"))) {
|
||||
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign"));
|
||||
if (signature == null) throw new BaseException("审批签字图片读取失败");
|
||||
docData.put(fields[i] + "Sign", signature);
|
||||
log.info("[职工慰问导出] 审批签名填充完成,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i] + "Sign");
|
||||
}
|
||||
docData.put(fields[i] + "Year", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy"));
|
||||
docData.put(fields[i] + "Month", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "M"));
|
||||
|
||||
+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.
@@ -0,0 +1 @@
|
||||
7pKr23u2tZHcvXud
|
||||
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")
|
||||
|
||||
@@ -35,22 +35,59 @@ module.exports = {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
//前端唯一标识 当然后端还有用户id作为唯一标识
|
||||
// 二维码携带本次组件实例的标识,与服务端当前登录用户共同确定签字记录。
|
||||
id: new Date().getTime() + Math.floor(Math.random() * 10000) + 1,
|
||||
signatureAddress: null,
|
||||
interval: null,
|
||||
connectionWaitResolve: null,
|
||||
unsubscribeSignature: null,
|
||||
signatureContent: null,
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
const {code, data} = await this.$axios.post("/platform/signature/get")
|
||||
if (code === 0) {
|
||||
this.signatureContent = data && data.signature
|
||||
this.$emit("input", this.signatureContent)
|
||||
}
|
||||
/**
|
||||
* 无请求参数,返回 Promise;响应 code 为0表示成功,data.signature 为已有签字图片地址。
|
||||
* 请求结束后由 mounted 的 finally 统一关闭 loading。
|
||||
*/
|
||||
init() {
|
||||
this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id
|
||||
console.log(this.signatureAddress)
|
||||
return this.$axios.post("/platform/signature/get")
|
||||
.then((res) => {
|
||||
if (this._isBeingDestroyed || this._isDestroyed) {
|
||||
return
|
||||
}
|
||||
if (res.code !== 0) {
|
||||
throw new Error("签字信息加载失败")
|
||||
}
|
||||
this.signatureContent = res.data && res.data.signature
|
||||
this.$emit("input", this.signatureContent)
|
||||
})
|
||||
},
|
||||
// 返回等待完成的 Promise;连接成功、等待30秒或组件销毁时结束等待。
|
||||
waitForConnection() {
|
||||
return new Promise((resolve) => {
|
||||
const startedAt = Date.now()
|
||||
this.connectionWaitResolve = resolve
|
||||
const checkReady = () => {
|
||||
this.interval = null
|
||||
if (this._isBeingDestroyed || this._isDestroyed ||
|
||||
(webSocketPubSub.ws && webSocketPubSub.ws.readyState === WebSocket.OPEN)) {
|
||||
this.connectionWaitResolve = null
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
if (Date.now() - startedAt >= 30000) {
|
||||
this.connectionWaitResolve = null
|
||||
this.$message.warning("签字消息连接超时,请刷新页面重试")
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
this.interval = setTimeout(checkReady, 100)
|
||||
}
|
||||
checkReady()
|
||||
})
|
||||
},
|
||||
signatureAgain() {
|
||||
const val = sessionStorage.getItem("h5-scan-code-signature-" + this.id)
|
||||
@@ -58,25 +95,42 @@ module.exports = {
|
||||
this.$emit("input", val)
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.init()
|
||||
webSocketPubSub.subscribe("h5-scan-code-signature", (data) => {
|
||||
console.info("ws:收到签字成功消息:", data)
|
||||
mounted() {
|
||||
this.loading = true
|
||||
// 先登记消息回调,连接成功后由公共连接加入房间,避免请求期间遗漏订阅。
|
||||
this.unsubscribeSignature = webSocketPubSub.subscribe("h5-scan-code-signature", (data) => {
|
||||
this.signatureContent = data.value
|
||||
this.$emit("input", data.value)
|
||||
})
|
||||
|
||||
// 可选:轮询或 watch ready 状态
|
||||
const checkReady = () => {
|
||||
if (webSocketPubSub.ws && webSocketPubSub.ws.readyState === WebSocket.OPEN) {
|
||||
console.info("ws.readyState is open");
|
||||
this.loading = false;
|
||||
console.log(this.signatureAddress)
|
||||
} else {
|
||||
setTimeout(checkReady, 100);
|
||||
}
|
||||
};
|
||||
checkReady();
|
||||
this.init()
|
||||
.then(() => {
|
||||
if (!this._isBeingDestroyed && !this._isDestroyed) {
|
||||
return this.waitForConnection()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!this._isBeingDestroyed && !this._isDestroyed) {
|
||||
this.$message.error("签字信息加载失败,请刷新页面重试")
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!this._isBeingDestroyed && !this._isDestroyed) {
|
||||
this.loading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
// pjax 切换页面时取消轮询和订阅,旧组件不得继续接收签字消息。
|
||||
clearTimeout(this.interval)
|
||||
this.interval = null
|
||||
if (this.connectionWaitResolve) {
|
||||
this.connectionWaitResolve()
|
||||
this.connectionWaitResolve = null
|
||||
}
|
||||
if (this.unsubscribeSignature) {
|
||||
this.unsubscribeSignature()
|
||||
this.unsubscribeSignature = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
progress
|
||||
:accept="accept"
|
||||
:max-count="upload_number"
|
||||
:upload-text="upload_text"
|
||||
title=""
|
||||
description=""
|
||||
tip=""
|
||||
@@ -50,6 +51,11 @@ module.exports = {
|
||||
default: 1,
|
||||
required: false
|
||||
},
|
||||
upload_text: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false
|
||||
},
|
||||
// 上传返回id或url
|
||||
upload_result_type: {
|
||||
type: String,
|
||||
@@ -101,6 +107,7 @@ module.exports = {
|
||||
this.fileList = val.map((v) => {
|
||||
return {
|
||||
...v,
|
||||
file: v.file || { name: v.name },
|
||||
status: null,
|
||||
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase())
|
||||
}
|
||||
@@ -118,6 +125,7 @@ module.exports = {
|
||||
this.fileList = val.map((v) => {
|
||||
return {
|
||||
...v,
|
||||
file: v.file || { name: v.name },
|
||||
url: v.url ? v.url : v.response?.data,
|
||||
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()),
|
||||
status: null
|
||||
@@ -171,8 +179,8 @@ module.exports = {
|
||||
f.url = resp.data
|
||||
f.response = resp
|
||||
f.percentage = 100
|
||||
f.isImage = true
|
||||
delete f.file
|
||||
f.isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(f.name.split(".").pop().toLowerCase())
|
||||
f.file = { name: f.name }
|
||||
delete f.content
|
||||
} else {
|
||||
f.status = "fail"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -443,50 +443,76 @@
|
||||
constructor() {
|
||||
this.ws = null
|
||||
this.subscribers = new Map()
|
||||
this.isSubscribed = false // 添加订阅状态标记
|
||||
this.isSubscribed = false
|
||||
this.connectTimer = null
|
||||
this.reconnectTimer = null
|
||||
this.pingTimer = null
|
||||
this.isDisposed = false
|
||||
this.init()
|
||||
}
|
||||
|
||||
// 页面只创建一个心跳任务;每次连接自行绑定事件及握手超时。
|
||||
init() {
|
||||
this.setupWindowEvents()
|
||||
this.setupPing()
|
||||
this.connect()
|
||||
}
|
||||
|
||||
// 正在连接或关闭时等待对应事件,避免同时创建多个连接。
|
||||
connect() {
|
||||
if (this.isDisposed || this.reconnectTimer !== null ||
|
||||
(this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.connect()
|
||||
const WS_URL = window.location.host + "${base}/websocket"
|
||||
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
|
||||
this.ws = new WebSocket(protocol + WS_URL)
|
||||
this.isSubscribed = false
|
||||
this.setupEventListeners()
|
||||
this.setupPing()
|
||||
this.setupWindowEvents()
|
||||
} catch (ex) {
|
||||
console.info(ex)
|
||||
const socket = this.ws
|
||||
// 握手最多等待30秒,心跳不得提前关闭仍处于 CONNECTING 的连接。
|
||||
this.connectTimer = setTimeout(() => {
|
||||
this.connectTimer = null
|
||||
if (this.ws === socket && socket.readyState === WebSocket.CONNECTING) {
|
||||
console.warn("WebSocket 握手超时,关闭当前连接后重试", socket.url)
|
||||
socket.close()
|
||||
}
|
||||
}, 30000)
|
||||
} catch (error) {
|
||||
console.error("WebSocket 创建连接失败", error)
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
const WS_URL = window.location.host + "${base}/websocket"
|
||||
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
|
||||
this.ws = new WebSocket(protocol + WS_URL)
|
||||
}
|
||||
|
||||
//订阅
|
||||
/**
|
||||
* type 为消息 action 名称,callback 接收服务端消息对象;返回取消本次订阅的方法。
|
||||
* 连接尚未建立时先保留回调,在 onopen 中统一加入当前用户的消息房间。
|
||||
*/
|
||||
subscribe(type, callback) {
|
||||
if (!this.subscribers.has(type)) {
|
||||
this.subscribers.set(type, new Set())
|
||||
}
|
||||
this.subscribers.get(type).add(callback)
|
||||
|
||||
setTimeout(() => {
|
||||
// 所有订阅完成后,发送 join 消息
|
||||
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.isSubscribed = true
|
||||
this.sendJoinMessage()
|
||||
const callbacks = this.subscribers.get(type)
|
||||
callbacks.add(callback)
|
||||
this.sendJoinMessage()
|
||||
return () => {
|
||||
callbacks.delete(callback)
|
||||
if (callbacks.size === 0 && this.subscribers.get(type) === callbacks) {
|
||||
this.subscribers.delete(type)
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
// 事件绑定到本次连接,旧连接的延迟事件不得影响新连接。
|
||||
setupEventListeners() {
|
||||
// 消息处理
|
||||
this.ws.onmessage = (event) => {
|
||||
const socket = this.ws
|
||||
socket.onmessage = (event) => {
|
||||
if (this.isDisposed || this.ws !== socket) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
// 触发订阅者的回调
|
||||
if (this.subscribers.has(data.action)) {
|
||||
this.subscribers.get(data.action).forEach((callback) => {
|
||||
try {
|
||||
@@ -500,82 +526,112 @@
|
||||
console.error("消息解析错误:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 连接建立
|
||||
this.ws.onopen = () => {
|
||||
// this.ws.send(
|
||||
// JSON.stringify({
|
||||
// room: store.state.room,
|
||||
// action: "join"
|
||||
// })
|
||||
// )
|
||||
// 连接建立时不立即发送 join,等待订阅完成
|
||||
this.ping()
|
||||
}
|
||||
}
|
||||
|
||||
// 新增发送 join 消息的方法
|
||||
sendJoinMessage() {
|
||||
this.send({
|
||||
room: store.state.room,
|
||||
action: "join"
|
||||
})
|
||||
}
|
||||
|
||||
// 心跳检测
|
||||
setupPing() {
|
||||
setInterval(() => this.ping(), (11624317 / 1000) * 2)
|
||||
}
|
||||
|
||||
ping() {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.send("{}")
|
||||
} catch (e) {
|
||||
this.reconnect()
|
||||
socket.onopen = () => {
|
||||
if (this.isDisposed || this.ws !== socket) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
clearTimeout(this.connectTimer)
|
||||
this.connectTimer = null
|
||||
this.sendJoinMessage()
|
||||
this.ping()
|
||||
console.info("WebSocket 连接已建立", socket.url)
|
||||
}
|
||||
socket.onerror = () => {
|
||||
if (!this.isDisposed && this.ws === socket) {
|
||||
console.warn("WebSocket 连接异常,等待关闭后重连", socket.url)
|
||||
}
|
||||
}
|
||||
socket.onclose = (event) => {
|
||||
if (this.isDisposed || this.ws !== socket) {
|
||||
return
|
||||
}
|
||||
clearTimeout(this.connectTimer)
|
||||
this.connectTimer = null
|
||||
this.isSubscribed = false
|
||||
console.warn("WebSocket 已关闭", event.code, event.reason)
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// 每个已建立的连接只加入一次房间,首次连接及断线恢复都补发已有订阅。
|
||||
sendJoinMessage() {
|
||||
if (this.isDisposed || this.isSubscribed || this.subscribers.size === 0 ||
|
||||
!window.store || !store.state.room || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return
|
||||
}
|
||||
this.send({
|
||||
room: store.state.room,
|
||||
action: "join"
|
||||
})
|
||||
this.isSubscribed = true
|
||||
}
|
||||
|
||||
// 沿用原心跳间隔,仅对已建立的连接发送心跳。
|
||||
setupPing() {
|
||||
this.pingTimer = setInterval(() => this.ping(), (11624317 / 1000) * 2)
|
||||
}
|
||||
|
||||
// CONNECTING/CLOSING 状态分别由握手超时和关闭事件处理。
|
||||
ping() {
|
||||
if (this.isDisposed) {
|
||||
return
|
||||
}
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.send("{}")
|
||||
} catch (error) {
|
||||
console.warn("WebSocket 心跳发送失败,等待关闭后重连", error)
|
||||
this.ws.close()
|
||||
}
|
||||
} else if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭事件和心跳共用一个重连任务,避免重复定时器创建并行连接。
|
||||
reconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close() // 确保旧连接已关闭
|
||||
if (this.isDisposed || this.reconnectTimer !== null ||
|
||||
(this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null
|
||||
this.connect()
|
||||
this.setupEventListeners()
|
||||
// 如果之前已订阅,重新发送join消息
|
||||
if (this.isSubscribed) {
|
||||
setTimeout(() => {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.sendJoinMessage()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}, 3000) // 延迟3秒重连
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 窗口事件处理
|
||||
// 真正离开页面时释放连接和定时器;进入往返缓存时保留页面状态。
|
||||
setupWindowEvents() {
|
||||
let _beforeUnload_time = 0
|
||||
window.onbeforeunload = () => {
|
||||
_beforeUnload_time = new Date().getTime()
|
||||
}
|
||||
window.onunload = () => {
|
||||
const _gap_time = new Date().getTime() - _beforeUnload_time
|
||||
if (_gap_time <= 5) {
|
||||
this.send({
|
||||
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
|
||||
action: "left"
|
||||
})
|
||||
window.addEventListener("pagehide", (event) => {
|
||||
if (event.persisted) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.isDisposed = true
|
||||
clearTimeout(this.connectTimer)
|
||||
clearTimeout(this.reconnectTimer)
|
||||
clearInterval(this.pingTimer)
|
||||
this.connectTimer = null
|
||||
this.reconnectTimer = null
|
||||
this.pingTimer = null
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.send({
|
||||
room: store.state.room,
|
||||
action: "left"
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn("WebSocket 离开房间消息发送失败", error)
|
||||
}
|
||||
}
|
||||
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
|
||||
this.ws.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// message 为消息对象或 JSON 字符串;连接未建立时不发送,返回值为 undefined。
|
||||
send(message) {
|
||||
if (this.ws) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
const messageStr = typeof message === "string" ? message : JSON.stringify(message)
|
||||
this.ws.send(messageStr)
|
||||
}
|
||||
|
||||
@@ -68,13 +68,14 @@
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.css" />
|
||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-table.css" />
|
||||
|
||||
<script src="https://vxeui.com/umd/xe-utils@3.5.30/dist/xe-utils.umd.min.js"></script>
|
||||
<script src="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/index.umd.min.js"></script>
|
||||
<script src="https://vxeui.com/umd/vxe-table@3.9.0/lib/index.umd.min.js"></script>
|
||||
<!-- 使用随应用打包的本地资源,按工具库、UI、表格的依赖顺序加载,避免外链失效影响页面。 -->
|
||||
<script src="${base!}/assets/platform/plugins/vxe/xe-utils.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.js"></script>
|
||||
<script src="${base!}/assets/platform/plugins/vxe/vue-table.js"></script>
|
||||
|
||||
<!-- 引入 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>
|
||||
@@ -153,51 +154,76 @@
|
||||
constructor() {
|
||||
this.ws = null
|
||||
this.subscribers = new Map()
|
||||
this.isSubscribed = false // 添加订阅状态标记
|
||||
this.isSubscribed = false
|
||||
this.connectTimer = null
|
||||
this.reconnectTimer = null
|
||||
this.pingTimer = null
|
||||
this.isDisposed = false
|
||||
this.init()
|
||||
}
|
||||
|
||||
// 页面只创建一个心跳任务;每次连接自行绑定事件及握手超时。
|
||||
init() {
|
||||
this.setupWindowEvents()
|
||||
this.setupPing()
|
||||
this.connect()
|
||||
}
|
||||
|
||||
// 正在连接或关闭时等待对应事件,避免同时创建多个连接。
|
||||
connect() {
|
||||
if (this.isDisposed || this.reconnectTimer !== null ||
|
||||
(this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.connect()
|
||||
const WS_URL = window.location.host + "${base}/websocket"
|
||||
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
|
||||
this.ws = new WebSocket(protocol + WS_URL)
|
||||
this.isSubscribed = false
|
||||
this.setupEventListeners()
|
||||
this.setupPing()
|
||||
this.setupWindowEvents()
|
||||
} catch (ex) {
|
||||
console.info(ex)
|
||||
const socket = this.ws
|
||||
// 握手最多等待30秒,心跳不得提前关闭仍处于 CONNECTING 的连接。
|
||||
this.connectTimer = setTimeout(() => {
|
||||
this.connectTimer = null
|
||||
if (this.ws === socket && socket.readyState === WebSocket.CONNECTING) {
|
||||
console.warn("WebSocket 握手超时,关闭当前连接后重试", socket.url)
|
||||
socket.close()
|
||||
}
|
||||
}, 30000)
|
||||
} catch (error) {
|
||||
console.error("WebSocket 创建连接失败", error)
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
const WS_URL = window.location.host + "${base}/websocket"
|
||||
// const WS_URL = 'zhgh.jshvc.edu.cn' + "${base}/websocket"
|
||||
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
|
||||
this.ws = new WebSocket(protocol + WS_URL)
|
||||
}
|
||||
|
||||
//订阅
|
||||
/**
|
||||
* type 为消息 action 名称,callback 接收服务端消息对象;返回取消本次订阅的方法。
|
||||
* 连接尚未建立时先保留回调,在 onopen 中统一加入当前用户的消息房间。
|
||||
*/
|
||||
subscribe(type, callback) {
|
||||
if (!this.subscribers.has(type)) {
|
||||
this.subscribers.set(type, new Set())
|
||||
}
|
||||
this.subscribers.get(type).add(callback)
|
||||
|
||||
setTimeout(() => {
|
||||
// 所有订阅完成后,发送 join 消息
|
||||
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.isSubscribed = true
|
||||
this.sendJoinMessage()
|
||||
const callbacks = this.subscribers.get(type)
|
||||
callbacks.add(callback)
|
||||
this.sendJoinMessage()
|
||||
return () => {
|
||||
callbacks.delete(callback)
|
||||
if (callbacks.size === 0 && this.subscribers.get(type) === callbacks) {
|
||||
this.subscribers.delete(type)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
// 事件绑定到本次连接,旧连接的延迟事件不得影响新连接。
|
||||
setupEventListeners() {
|
||||
// 消息处理
|
||||
this.ws.onmessage = (event) => {
|
||||
const socket = this.ws
|
||||
socket.onmessage = (event) => {
|
||||
if (this.isDisposed || this.ws !== socket) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
// 触发订阅者的回调
|
||||
if (this.subscribers.has(data.action)) {
|
||||
this.subscribers.get(data.action).forEach((callback) => {
|
||||
try {
|
||||
@@ -211,83 +237,112 @@
|
||||
console.error("消息解析错误:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 连接建立
|
||||
this.ws.onopen = () => {
|
||||
// this.ws.send(
|
||||
// JSON.stringify({
|
||||
// room: store.state.room,
|
||||
// action: "join"
|
||||
// })
|
||||
// )
|
||||
// 连接建立时不立即发送 join,等待订阅完成
|
||||
this.ping()
|
||||
console.info('websocket open success')
|
||||
}
|
||||
}
|
||||
|
||||
// 新增发送 join 消息的方法
|
||||
sendJoinMessage() {
|
||||
this.send({
|
||||
room: store.state.room,
|
||||
action: "join"
|
||||
})
|
||||
}
|
||||
|
||||
// 心跳检测
|
||||
setupPing() {
|
||||
setInterval(() => this.ping(), (11624317 / 1000) * 2)
|
||||
}
|
||||
|
||||
ping() {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.send("{}")
|
||||
} catch (e) {
|
||||
this.reconnect()
|
||||
socket.onopen = () => {
|
||||
if (this.isDisposed || this.ws !== socket) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
clearTimeout(this.connectTimer)
|
||||
this.connectTimer = null
|
||||
this.sendJoinMessage()
|
||||
this.ping()
|
||||
console.info("WebSocket 连接已建立", socket.url)
|
||||
}
|
||||
socket.onerror = () => {
|
||||
if (!this.isDisposed && this.ws === socket) {
|
||||
console.warn("WebSocket 连接异常,等待关闭后重连", socket.url)
|
||||
}
|
||||
}
|
||||
socket.onclose = (event) => {
|
||||
if (this.isDisposed || this.ws !== socket) {
|
||||
return
|
||||
}
|
||||
clearTimeout(this.connectTimer)
|
||||
this.connectTimer = null
|
||||
this.isSubscribed = false
|
||||
console.warn("WebSocket 已关闭", event.code, event.reason)
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// 每个已建立的连接只加入一次房间,首次连接及断线恢复都补发已有订阅。
|
||||
sendJoinMessage() {
|
||||
if (this.isDisposed || this.isSubscribed || this.subscribers.size === 0 ||
|
||||
!window.store || !store.state.room || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return
|
||||
}
|
||||
this.send({
|
||||
room: store.state.room,
|
||||
action: "join"
|
||||
})
|
||||
this.isSubscribed = true
|
||||
}
|
||||
|
||||
// 沿用原心跳间隔,仅对已建立的连接发送心跳。
|
||||
setupPing() {
|
||||
this.pingTimer = setInterval(() => this.ping(), (11624317 / 1000) * 2)
|
||||
}
|
||||
|
||||
// CONNECTING/CLOSING 状态分别由握手超时和关闭事件处理。
|
||||
ping() {
|
||||
if (this.isDisposed) {
|
||||
return
|
||||
}
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.send("{}")
|
||||
} catch (error) {
|
||||
console.warn("WebSocket 心跳发送失败,等待关闭后重连", error)
|
||||
this.ws.close()
|
||||
}
|
||||
} else if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭事件和心跳共用一个重连任务,避免重复定时器创建并行连接。
|
||||
reconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close() // 确保旧连接已关闭
|
||||
if (this.isDisposed || this.reconnectTimer !== null ||
|
||||
(this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null
|
||||
this.connect()
|
||||
this.setupEventListeners()
|
||||
// 如果之前已订阅,重新发送join消息
|
||||
if (this.isSubscribed) {
|
||||
setTimeout(() => {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.sendJoinMessage()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}, 1000) // 延迟2秒重连
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 窗口事件处理
|
||||
// 真正离开页面时释放连接和定时器;进入往返缓存时保留页面状态。
|
||||
setupWindowEvents() {
|
||||
let _beforeUnload_time = 0
|
||||
window.onbeforeunload = () => {
|
||||
_beforeUnload_time = new Date().getTime()
|
||||
}
|
||||
window.onunload = () => {
|
||||
const _gap_time = new Date().getTime() - _beforeUnload_time
|
||||
if (_gap_time <= 5) {
|
||||
this.send({
|
||||
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
|
||||
action: "left"
|
||||
})
|
||||
window.addEventListener("pagehide", (event) => {
|
||||
if (event.persisted) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.isDisposed = true
|
||||
clearTimeout(this.connectTimer)
|
||||
clearTimeout(this.reconnectTimer)
|
||||
clearInterval(this.pingTimer)
|
||||
this.connectTimer = null
|
||||
this.reconnectTimer = null
|
||||
this.pingTimer = null
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.send({
|
||||
room: store.state.room,
|
||||
action: "left"
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn("WebSocket 离开房间消息发送失败", error)
|
||||
}
|
||||
}
|
||||
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
|
||||
this.ws.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// message 为消息对象或 JSON 字符串;连接未建立时不发送,返回值为 undefined。
|
||||
send(message) {
|
||||
if (this.ws) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
const messageStr = typeof message === "string" ? message : JSON.stringify(message)
|
||||
this.ws.send(messageStr)
|
||||
}
|
||||
@@ -481,7 +536,8 @@
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
max-width: 310px;
|
||||
/* 用户区按完整姓名、账号及操作按钮的实际宽度布局,避免退出按钮溢出后被顶栏裁切。 */
|
||||
width: max-content;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -617,6 +673,26 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 常见笔记本宽度下压缩导航留白,为完整用户信息与退出按钮腾出空间。 */
|
||||
@media (min-width: 1025px) and (max-width: 1440px) {
|
||||
.v4-nav {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.v4-nav-item {
|
||||
padding: 0 8px;
|
||||
column-gap: 6px;
|
||||
}
|
||||
|
||||
.v4-user-section {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.v4-user-info {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.v4-nav {
|
||||
gap: 24px;
|
||||
|
||||
@@ -139,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)
|
||||
|
||||
@@ -116,6 +116,69 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 最新活动的作用域样式会在组件挂载后注入,首屏先固定布局和图片尺寸,避免刷新时放大闪烁。 */
|
||||
#v4-home-app .act-card .activity-section-wrapper,
|
||||
#v4-home-app .act-card .section-act,
|
||||
#v4-home-app .act-card .activity-swiper {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#v4-home-app .act-card .activity-swiper-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 54px 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#v4-home-app .act-card .activity-swiper .swiper-slide {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
#v4-home-app .act-card .activity-swiper .img-box {
|
||||
width: 100%;
|
||||
height: 128px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#v4-home-app .act-card .activity-swiper .img-box img {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: 100% !important;
|
||||
max-height: 128px !important;
|
||||
object-fit: fill !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#v4-home-app .act-card .activity-swiper .swiper-slide {
|
||||
width: calc((100% - 18px) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
#v4-home-app .act-card .activity-swiper .swiper-slide {
|
||||
width: calc((100% - 54px) / 4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
#v4-home-app .act-card .activity-swiper .swiper-slide {
|
||||
width: calc((100% - 72px) / 5);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="v4-container" id="v4-home-app">
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -55,7 +55,7 @@ layout("/layouts/platform.html"){
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>-->
|
||||
<el-descriptions-item label="证明人">
|
||||
<!-- <el-descriptions-item label="证明人">
|
||||
<el-form-item prop="certifierUserId" label="证明人">
|
||||
<el-select
|
||||
style="width: 100%"
|
||||
@@ -75,7 +75,7 @@ layout("/layouts/platform.html"){
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions-item> -->
|
||||
<el-descriptions-item label="慰问类型">
|
||||
<el-form-item prop="type" label="慰问类型">
|
||||
<el-select v-model="formData.type" @change="typeChange" style="width: 100%"
|
||||
@@ -112,14 +112,14 @@ layout("/layouts/platform.html"){
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="收款账户">
|
||||
<el-descriptions-item label="收款账户" v-if="!['A', 'D'].includes(formData.typeCode)">
|
||||
<el-form-item prop="bankCardNumber" label="收款账户">
|
||||
<el-input maxlength="20" v-model="formData.bankCardNumber" placeholder="请填写收款账户"
|
||||
<el-input maxlength="30" v-model="formData.bankCardNumber" placeholder="请填写银行卡号或对公账户号"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="户名">
|
||||
<el-descriptions-item label="户名" v-if="!['A', 'D'].includes(formData.typeCode)">
|
||||
<el-form-item prop="bankUserName" label="户名">
|
||||
<el-input maxlength="20" v-model="formData.bankUserName"
|
||||
placeholder="请填写户名"
|
||||
@@ -127,7 +127,7 @@ layout("/layouts/platform.html"){
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="开户行" >
|
||||
<el-descriptions-item label="开户行" v-if="!['A', 'D'].includes(formData.typeCode)">
|
||||
<el-form-item prop="bankOfDeposit" label="开户行">
|
||||
<el-input maxlength="20" v-model="formData.bankOfDeposit"
|
||||
placeholder="请填写开户行"
|
||||
@@ -173,10 +173,12 @@ layout("/layouts/platform.html"){
|
||||
</template>
|
||||
|
||||
|
||||
</el-descriptions>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="申请事由" :span="3">
|
||||
<el-form-item prop="remark" label="申请事由">
|
||||
<el-input maxlength="500" v-model="formData.remark" placeholder="请填写申请事由"
|
||||
type="textarea" :autosize="{ minRows: 4, maxRows: 8}"></el-input>
|
||||
type="textarea" :rows="3" resize="none"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -238,7 +240,10 @@ layout("/layouts/platform.html"){
|
||||
payUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
type: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
way: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
money: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
money: [
|
||||
{ required: true, message: "请填写慰问金额", trigger: ["change", "blur"] },
|
||||
{ validator: (rule, value, callback) => callback(this.validateMoney(value) ? undefined : new Error("慰问金额须大于0且不超过10000元,最多两位小数")), trigger: ["change", "blur"] }
|
||||
],
|
||||
occurTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
child: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
hospital: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
@@ -247,14 +252,20 @@ layout("/layouts/platform.html"){
|
||||
hospitalBy: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
deadImmediateFamily: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
remark: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
files: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
files: [{ required: true, type: "array", min: 1, max: 5, message: "请上传1至5个附件", trigger: ["change", "blur"] }],
|
||||
signature: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
handlerUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
certifierUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
bankCardNumber: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
// certifierUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
bankCardNumber: [
|
||||
{ required: true, message: "请填写银行卡号或对公账户号", trigger: ["change", "blur"] },
|
||||
{ pattern: /^[0-9]{6,30}$/, message: "收款账户须为6至30位数字(银行卡号或对公账户号)", trigger: ["change", "blur"] }
|
||||
],
|
||||
bankUserName: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
bankOfDeposit: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
thisYearHospitalizationNum: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
thisYearHospitalizationNum: [
|
||||
{ required: true, message: "必填", trigger: ["change", "blur"] },
|
||||
{ type: "integer", min: 0, max: 10000, message: "住院次数须为0至10000的整数", trigger: ["change", "blur"] }
|
||||
],
|
||||
leaveHospitalTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
hospitalizationTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
},
|
||||
@@ -268,6 +279,9 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateMoney(value) {
|
||||
return /^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000
|
||||
},
|
||||
createRemoteMethod(options) {
|
||||
return (keyword) => {
|
||||
this.selectQueryUser(keyword, options)
|
||||
|
||||
@@ -37,12 +37,12 @@ const basicForm = {
|
||||
<el-radio-button :label="false">不上传</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>-->
|
||||
<!-- <el-form-item label="是否上传附件" prop="isUploadFile">-->
|
||||
<!-- <el-radio-group v-model="formData.isUploadFile">-->
|
||||
<!-- <el-radio-button :label="true">是</el-radio-button>-->
|
||||
<!-- <el-radio-button :label="false">否</el-radio-button>-->
|
||||
<!-- </el-radio-group>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="是否上传附件" prop="isUploadFile">
|
||||
<el-radio-group v-model="formData.isUploadFile">
|
||||
<el-radio-button :label="true">是</el-radio-button>
|
||||
<el-radio-button :label="false">否</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传附件说明" prop="uploadFileDesc" v-if="formData.isUploadFile">
|
||||
<el-input v-model="formData.uploadFileDesc" maxlength="50" clearable placeholder="请填写上传附件说明"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
+64
-58
@@ -26,7 +26,7 @@ layout("/layouts/platform.html"){
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="受补助人">
|
||||
<el-form-item prop="userName">
|
||||
<el-form-item prop="userId">
|
||||
<template v-if="formData.mode == 2">
|
||||
<el-select :remote-method="queryRecipients"
|
||||
@change="userChange" clearable filterable
|
||||
@@ -51,7 +51,7 @@ layout("/layouts/platform.html"){
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号" :span="2">
|
||||
<el-form-item prop="idCard">
|
||||
<el-input maxlength="20" clearable placeholder="请填写身份证号"
|
||||
<el-input clearable placeholder="请填写身份证号"
|
||||
v-model="formData.idCard" maxlength="18"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
@@ -84,7 +84,7 @@ layout("/layouts/platform.html"){
|
||||
<el-descriptions-item label="手机号码">
|
||||
<el-form-item prop="mobile">
|
||||
<el-input maxlength="11" clearable placeholder="请填写手机号码"
|
||||
v-model="formData.mobile" maxlength="11"></el-input>
|
||||
v-model="formData.mobile"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="家庭年总收入(万元)">
|
||||
@@ -103,6 +103,7 @@ layout("/layouts/platform.html"){
|
||||
<el-input-number
|
||||
v-model="formData.homeNumber"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
placeholder="请填写家庭人数">
|
||||
@@ -113,7 +114,7 @@ layout("/layouts/platform.html"){
|
||||
<el-form-item prop="homeAddress">
|
||||
<el-input clearable
|
||||
style="width: 100%"
|
||||
v-model="formData.homeAddress"
|
||||
v-model="formData.homeAddress" maxlength="200"
|
||||
placeholder="请填写家庭住址">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@@ -132,7 +133,7 @@ layout("/layouts/platform.html"){
|
||||
<!-- </el-descriptions-item>-->
|
||||
<el-descriptions-item label="银行卡号" >
|
||||
<el-form-item prop="bankCardNum">
|
||||
<el-input maxlength="20" clearable placeholder="请填写银行卡号"
|
||||
<el-input maxlength="19" clearable placeholder="请填写银行卡号"
|
||||
v-model="formData.bankCardNum"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
@@ -152,7 +153,7 @@ layout("/layouts/platform.html"){
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="申请金额(元)">
|
||||
<el-form-item prop="applyMoney" :rules="[{required:true,type:'number',min:0.01,message:'请填写大于零的申请金额',trigger:['change','blur']}]">
|
||||
<el-form-item prop="applyMoney">
|
||||
<el-input-number v-model="formData.applyMoney" :precision="2" :min="0.01" placeholder="请输入申请金额"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
@@ -198,8 +199,8 @@ layout("/layouts/platform.html"){
|
||||
<template slot-scope="{row,$index}">
|
||||
<el-form-item label-width="0"
|
||||
:prop="'familyList.'+$index+'.relation'"
|
||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
<el-input v-model="row.relation" placeholder="请填写与本人关系" clearable></el-input>
|
||||
:rules="textRules('与本人关系', 50)">
|
||||
<el-input v-model="row.relation" maxlength="50" placeholder="请填写与本人关系" clearable></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -207,8 +208,8 @@ layout("/layouts/platform.html"){
|
||||
<template slot-scope="{row,$index}">
|
||||
<el-form-item label-width="0"
|
||||
:prop="'familyList.'+$index+'.name'"
|
||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
<el-input v-model="row.name" placeholder="请填写姓名" clearable></el-input>
|
||||
:rules="textRules('姓名', 100)">
|
||||
<el-input v-model="row.name" maxlength="100" placeholder="请填写姓名" clearable></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -216,9 +217,9 @@ layout("/layouts/platform.html"){
|
||||
<template slot-scope="{row,$index}">
|
||||
<el-form-item label-width="0"
|
||||
:prop="'familyList.'+$index+'.age'"
|
||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
:rules="[{required:true,type:'integer',min:1,max:100,message:'年龄须为1至100的整数',trigger:['change','blur']}]">
|
||||
<el-input-number v-model="row.age" placeholder="年龄" :min="1"
|
||||
:max="100" :step="1" style="width: 100%" clearable></el-input-number>
|
||||
:max="100" :precision="0" :step="1" style="width: 100%" clearable></el-input-number>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -226,8 +227,8 @@ layout("/layouts/platform.html"){
|
||||
<template slot-scope="{row,$index}">
|
||||
<el-form-item label-width="0"
|
||||
:prop="'familyList.'+$index+'.homeUnit'"
|
||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
<el-input v-model="row.homeUnit" placeholder="请填写工作单位" clearable></el-input>
|
||||
:rules="textRules('工作单位', 200)">
|
||||
<el-input v-model="row.homeUnit" maxlength="200" placeholder="请填写工作单位" clearable></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -235,9 +236,9 @@ layout("/layouts/platform.html"){
|
||||
<template slot-scope="{row,$index}">
|
||||
<el-form-item label-width="0"
|
||||
:prop="'familyList.'+$index+'.yearIncome'"
|
||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
:rules="[{validator:validateIncome,required:true,trigger:['change','blur']}]">
|
||||
<el-input-number v-model="row.yearIncome" placeholder="年收入"
|
||||
:min="0" :step="1" style="width: 100%" clearable></el-input-number>
|
||||
:min="0" :precision="2" :step="1" style="width: 100%" clearable></el-input-number>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -245,8 +246,8 @@ layout("/layouts/platform.html"){
|
||||
<template slot-scope="{row,$index}">
|
||||
<el-form-item label-width="0"
|
||||
:prop="'familyList.'+$index+'.remarks'"
|
||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
<el-input v-model="row.remarks" placeholder="请填写备注" clearable></el-input>
|
||||
:rules="textRules('备注', 500)">
|
||||
<el-input v-model="row.remarks" maxlength="500" placeholder="请填写备注" clearable></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -326,23 +327,37 @@ layout("/layouts/platform.html"){
|
||||
applyCount: '1',
|
||||
},
|
||||
formRules: {
|
||||
proxyUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
|
||||
proxyLoginName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
|
||||
sex: [{required: true, message: '请填写性别', trigger: ['blur', 'change']}],
|
||||
userName: [{required: true, message: '请填写受补助人', trigger: ['blur', 'change']}],
|
||||
unitName: [{required: true, message: '请填写所属单位', trigger: ['blur', 'change']}],
|
||||
proxyUserName: this.textRules('填写人姓名', 100),
|
||||
proxyLoginName: this.textRules('填写人工号', 100),
|
||||
mode: [{required: true, type: 'enum', enum: ['1', '2'], message: '请选择申请模式', trigger: ['blur', 'change']}],
|
||||
sex: [{required: true, type: 'enum', enum: ['男', '女'], message: '请选择性别', trigger: ['blur', 'change']}],
|
||||
userId: [{required: true, message: '请选择受补助人', trigger: ['blur', 'change']}],
|
||||
unitName: this.textRules('所属单位', 500),
|
||||
unionName: [{required: true, message: '请填写所属工会', trigger: ['blur', 'change']}],
|
||||
homeAddress: [{required: true, message: '请填写家庭住址', trigger: ['blur', 'change']}],
|
||||
mobile: [{required: true, message: '请填写手机号码', trigger: ['blur', 'change']}],
|
||||
idCard: [{required: true, message: '请填写身份证号', trigger: ['blur', 'change']}],
|
||||
bankCardNum: [{required: true, message: '请填写收款账户卡号', trigger: ['blur', 'change']}],
|
||||
homeAddress: this.textRules('家庭住址', 200),
|
||||
mobile: [
|
||||
{required: true, message: '请填写手机号码', trigger: ['blur', 'change']},
|
||||
{pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: ['blur', 'change']}
|
||||
],
|
||||
idCard: [{validator: (rule, value, callback) => {
|
||||
const result = this.isValidIdCard(value || '')
|
||||
callback(result.status === 0 ? new Error(result.msg) : undefined)
|
||||
}, required: true, trigger: ['blur', 'change']}],
|
||||
bankCardNum: [{validator: (rule, value, callback) => {
|
||||
const result = this.validateCard(value || '')
|
||||
callback(result.status === 0 ? new Error(result.msg) : undefined)
|
||||
}, required: true, trigger: ['blur', 'change']}],
|
||||
bankUserName: [{required: true, message: '请填写户名', trigger: ['blur', 'change']}],
|
||||
bankOfDeposit: [{required: true, message: '请填写开户行', trigger: ['blur', 'change']}],
|
||||
position: [{required: true, message: '请填写职务', trigger: ['blur', 'change']}],
|
||||
officialCapacity: [{required: true, message: '请填写职称', trigger: ['blur', 'change']}],
|
||||
subsidyStandards: [{required: true, message: '请选择困难类型', trigger: ['blur', 'change']}],
|
||||
reason: [{required: true, message: '请填写申请原因', trigger: ['blur', 'change']}],
|
||||
sign: [{required: true, message: '请签字', trigger: ['blur', 'change']}]
|
||||
bankOfDeposit: this.textRules('开户行', 30),
|
||||
position: this.textRules('职务', 20),
|
||||
officialCapacity: this.textRules('职称', 20),
|
||||
homeIncome: [{validator: this.validateIncome, required: true, trigger: ['blur', 'change']}],
|
||||
homeNumber: [{required: true, type: 'integer', min: 1, message: '家庭人数须为大于零的整数', trigger: ['blur', 'change']}],
|
||||
subsidy: this.textRules('补助类型', 500),
|
||||
subsidyStandards: this.textRules('困难类型', 500),
|
||||
applyMoney: [{validator: this.validateMoney, required: true, trigger: ['blur', 'change']}],
|
||||
reason: this.textRules('生活致困原因', 500),
|
||||
sign: [{required: true, whitespace: true, message: '请完成签字', trigger: ['blur', 'change']}],
|
||||
},
|
||||
|
||||
// 替他人申请相关
|
||||
@@ -350,8 +365,21 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
textRules(label, max) {
|
||||
return [{required: true, whitespace: true, max, message: '请填写' + label + ',且不超过' + max + '个字符', trigger: ['blur', 'change']}]
|
||||
},
|
||||
validateIncome(rule, value, callback) {
|
||||
const valid = typeof value === 'number' && Number.isFinite(value) && value >= 0 && /^\d+(\.\d{1,2})?$/.test(String(value))
|
||||
callback(valid ? undefined : new Error('收入须为不小于零的数字,最多保留两位小数'))
|
||||
},
|
||||
validateMoney(rule, value, callback) {
|
||||
const valid = typeof value === 'number' && Number.isFinite(value) && value > 0 && /^\d+(\.\d{1,2})?$/.test(String(value))
|
||||
callback(valid ? undefined : new Error('申请金额须大于零,最多保留两位小数'))
|
||||
},
|
||||
// 保存
|
||||
onSave() {
|
||||
async onSave() {
|
||||
const valid = await this.$refs.formRef.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
this.$confirm("您确定保存吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
@@ -368,24 +396,13 @@ layout("/layouts/platform.html"){
|
||||
|
||||
// 提交
|
||||
async onSubmit() {
|
||||
this.$refs.formRef.validate().then((valid) => {
|
||||
this.$refs.formRef.validate().catch(() => false).then((valid) => {
|
||||
if (valid) {
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
const result = this.isValidIdCard(this.formData.idCard)
|
||||
if (result.status === 0) {
|
||||
this.$message.warning(result.msg)
|
||||
return
|
||||
}
|
||||
const bankRes = this.validateCard(this.formData.bankCardNum)
|
||||
if (bankRes.status === 0) {
|
||||
this.$message.warning(bankRes.msg)
|
||||
return;
|
||||
}
|
||||
|
||||
this.$axios.post('/platform/difficultHelp/apply/submit', {
|
||||
data: JSON.stringify(this.formData)
|
||||
}).then(res => {
|
||||
@@ -400,7 +417,7 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
|
||||
async onFinishTask() {
|
||||
this.$refs.formRef.validate().then((valid) => {
|
||||
this.$refs.formRef.validate().catch(() => false).then((valid) => {
|
||||
if (valid) {
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
@@ -408,17 +425,6 @@ layout("/layouts/platform.html"){
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
|
||||
const result = this.isValidIdCard(this.formData.idCard)
|
||||
if (result.status === 0) {
|
||||
this.$message.warning(result.msg)
|
||||
return
|
||||
}
|
||||
const bankRes = this.validateCard(this.formData.bankCardNum)
|
||||
if (bankRes.status === 0) {
|
||||
this.$message.warning(bankRes.msg)
|
||||
return;
|
||||
}
|
||||
|
||||
this.$axios.post('/platform/difficultHelp/apply/submitAgain', {
|
||||
data: JSON.stringify(this.formData),
|
||||
taskId: GetQueryString("taskId")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -255,7 +255,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<van-empty description="暂无数据" v-else></van-empty>
|
||||
</van-action-sheet>-->
|
||||
|
||||
<van-field
|
||||
<!-- <van-field
|
||||
v-model="formData.certifierUserName"
|
||||
name="certifierUserName"
|
||||
label="证明人"
|
||||
@@ -288,7 +288,7 @@ layout("/layouts/platform_h5.html"){
|
||||
</template>
|
||||
</div>
|
||||
<van-empty description="暂无数据" v-else></van-empty>
|
||||
</van-action-sheet>
|
||||
</van-action-sheet> -->
|
||||
|
||||
<van-field
|
||||
v-model="formData.typeName"
|
||||
@@ -322,7 +322,7 @@ layout("/layouts/platform_h5.html"){
|
||||
name="money"
|
||||
label="慰问金额"
|
||||
required
|
||||
:rules="[{ required: true }]"
|
||||
:rules="[{ required: true, message: '请填写慰问金额' }, { validator: validateMoney, message: '慰问金额须大于0且不超过10000元,最多两位小数' }]"
|
||||
placeholder="选择慰问类型自动匹配金额"
|
||||
clickable
|
||||
type="number"
|
||||
@@ -353,17 +353,19 @@ layout("/layouts/platform_h5.html"){
|
||||
</van-popup>
|
||||
|
||||
<van-field
|
||||
v-if="!['A', 'D'].includes(formData.typeCode)"
|
||||
v-model="formData.bankCardNumber"
|
||||
label="收款账户"
|
||||
maxlength="20"
|
||||
maxlength="30"
|
||||
clearable
|
||||
name="bankCardNumber"
|
||||
placeholder="请填写收款账户"
|
||||
:rules="[{ required: true }]"
|
||||
placeholder="请填写银行卡号或对公账户号"
|
||||
:rules="[{ required: true, message: '请填写银行卡号或对公账户号' }, { pattern: /^[0-9]{6,30}$/, message: '收款账户须为6至30位数字(银行卡号或对公账户号)' }]"
|
||||
required
|
||||
></van-field>
|
||||
|
||||
<van-field
|
||||
v-if="!['A', 'D'].includes(formData.typeCode)"
|
||||
v-model="formData.bankUserName"
|
||||
label="户名"
|
||||
placeholder="请填写户名"
|
||||
@@ -373,9 +375,10 @@ layout("/layouts/platform_h5.html"){
|
||||
name="bankUserName"
|
||||
></van-field>
|
||||
<van-field
|
||||
v-if="!['A', 'D'].includes(formData.typeCode)"
|
||||
v-model="formData.bankOfDeposit"
|
||||
label="开户行"
|
||||
maxlength="30"
|
||||
maxlength="20"
|
||||
type="textarea"
|
||||
rows="4"
|
||||
autosize
|
||||
@@ -433,8 +436,8 @@ layout("/layouts/platform_h5.html"){
|
||||
></van-datetime-picker>
|
||||
</van-popup>
|
||||
|
||||
<van-field type="digit" label="当年第几次住院"
|
||||
:rules="[{ required: true }]"
|
||||
<van-field type="digit" label="当年第几次住院" name="thisYearHospitalizationNum"
|
||||
:rules="[{ required: true }, { validator: value => /^[0-9]+$/.test(String(value)) && Number(value) <= 10000, message: '住院次数须为0至10000的整数' }]"
|
||||
required
|
||||
placeholder="请输入当年第几次住院"
|
||||
v-model="formData.thisYearHospitalizationNum"></van-field>
|
||||
@@ -443,6 +446,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<van-field
|
||||
v-model="formData.remark"
|
||||
name="remark"
|
||||
maxlength="500"
|
||||
label="申请事由"
|
||||
type="textarea"
|
||||
rows="4"
|
||||
@@ -460,15 +464,18 @@ layout("/layouts/platform_h5.html"){
|
||||
{{ '附件说明:' + chooseType.uploadFileDesc }}
|
||||
</div>
|
||||
<van-cell-group :border="false">
|
||||
<van-field class="direction-column-field" name="avatar" label="">
|
||||
<van-field class="direction-column-field" name="files" label="附件" required
|
||||
:rules="[{ validator: () => Array.isArray(formData.files) && formData.files.length > 0 && formData.files.length <= 5, message: '请上传1至5个附件' }]">
|
||||
<template #input>
|
||||
<h5-file-upload
|
||||
slot="input"
|
||||
:value.sync="formData.files"
|
||||
:upload_number="5"
|
||||
upload_text="上传附件"
|
||||
upload_result_category="array"
|
||||
complete_result
|
||||
></h5-file-upload>
|
||||
<div class="condolence-card__desc">最多上传 5 个文件</div>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
@@ -477,7 +484,8 @@ layout("/layouts/platform_h5.html"){
|
||||
<section class="form-section">
|
||||
<h2 class="condolence-card__title">签字确认</h2>
|
||||
<van-cell-group :border="false">
|
||||
<van-field class="direction-column-field" name="signature" label="">
|
||||
<van-field class="direction-column-field" name="signature" label="" :value="formData.signature"
|
||||
:rules="[{ required: true, message: '请填写签字' }]">
|
||||
<template #input>
|
||||
<h5-signature v-model="formData.signature" slot="input"></h5-signature>
|
||||
</template>
|
||||
@@ -532,6 +540,9 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateMoney(value) {
|
||||
return /^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000
|
||||
},
|
||||
// 选择出院时间
|
||||
showLeaveHospitalTimePickerClick() {
|
||||
if (this.formData.leaveHospitalTime) {
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<h2 class="condolence-info-card__title">
|
||||
审批签字<span class="condolence-audit-card__required">*</span>
|
||||
</h2>
|
||||
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
|
||||
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
|
||||
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
||||
</van-field>
|
||||
</van-form>
|
||||
|
||||
@@ -158,19 +158,44 @@
|
||||
}
|
||||
|
||||
.condolence-info-long-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.condolence-info-card .condolence-attachments .empty-container {
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.condolence-info-card .condolence-attachments .empty-text {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.condolence-info-opinion > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.condolence-info-opinion > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.condolence-signature-image,
|
||||
.condolence-approval-signature-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.condolence-info-long-cell .van-cell__title {
|
||||
width: auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.condolence-info-long-cell .van-cell__value {
|
||||
margin-top: 7px;
|
||||
.condolence-signature-image .van-image__img,
|
||||
.condolence-approval-signature-image {
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
}
|
||||
|
||||
.condolence-signature-image {
|
||||
display: block;
|
||||
.condolence-info-opinion img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@ const condolenceInfo = {
|
||||
<van-cell title="申请人单位">{{ viewData.applyUnitName || '--' }}</van-cell>
|
||||
<van-cell title="申请人工会">{{ viewData.applyUnionName || '--' }}</van-cell>
|
||||
<van-cell title="慰问对象">{{ viewData.helpUserName || '--' }}</van-cell>
|
||||
<van-cell title="证明人">{{ viewData.certifierUserName || '--' }}</van-cell>
|
||||
<!-- <van-cell title="证明人">{{ viewData.certifierUserName || '--' }}</van-cell> -->
|
||||
<van-cell title="慰问类型">{{ viewData.typeName || '--' }}</van-cell>
|
||||
<van-cell title="慰问金额">{{ viewData.money || '--' }}</van-cell>
|
||||
<van-cell title="慰问时间">{{ viewData.occurTime || '--' }}</van-cell>
|
||||
<van-cell title="收款账户">{{ viewData.bankCardNumber || '--' }}</van-cell>
|
||||
<van-cell title="户名">{{ viewData.bankUserName || '--' }}</van-cell>
|
||||
<van-cell class="condolence-info-long-cell" title="开户行">
|
||||
<van-cell title="开户行">
|
||||
{{ viewData.bankOfDeposit || '暂无' }}
|
||||
</van-cell>
|
||||
<template v-if="['2'].includes(viewData.typeCode)">
|
||||
@@ -39,17 +39,18 @@ const condolenceInfo = {
|
||||
<van-cell title="出院时间">{{ viewData.leaveHospitalTime || '--' }}</van-cell>
|
||||
<van-cell title="当年第几次住院(次)">{{ viewData.thisYearHospitalizationNum || '--' }}</van-cell>
|
||||
</template>
|
||||
<van-cell class="condolence-info-long-cell" title="申请事由">
|
||||
<van-cell title="申请事由">
|
||||
{{ viewData.remark || '暂无' }}
|
||||
</van-cell>
|
||||
<van-cell class="condolence-info-long-cell" title="附件">
|
||||
<file-preview :files="viewData.files" complete_result></file-preview>
|
||||
<file-preview class="condolence-attachments" :files="viewData.files" complete_result></file-preview>
|
||||
</van-cell>
|
||||
<van-cell class="condolence-info-long-cell" title="签字">
|
||||
<van-image
|
||||
v-if="viewData.signature"
|
||||
:src="viewData.signature"
|
||||
class="condolence-signature-image"
|
||||
fit="contain"
|
||||
></van-image>
|
||||
<span v-else>暂无签字</span>
|
||||
</van-cell>
|
||||
@@ -86,10 +87,10 @@ const condolenceInfo = {
|
||||
></dict-tag>
|
||||
</van-cell>
|
||||
<van-cell v-if="task.taskFormData && task.taskFormData.sign" title="审批签字">
|
||||
<img :src="task.taskFormData.sign" style="max-width: 100%; height: 80px; object-fit: contain">
|
||||
<img :src="task.taskFormData.sign" class="condolence-approval-signature-image">
|
||||
</van-cell>
|
||||
<van-cell class="condolence-info-long-cell" title="办理意见">
|
||||
<div v-html="task.taskFormData && task.taskFormData.tf_opinion || '暂无'"></div>
|
||||
<van-cell title="办理意见">
|
||||
<div class="condolence-info-opinion" v-html="task.taskFormData && task.taskFormData.tf_opinion || '暂无'"></div>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</section>
|
||||
|
||||
+18
-1
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<h2 class="condolence-info-card__title">
|
||||
审批签字<span class="condolence-audit-card__required">*</span>
|
||||
</h2>
|
||||
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
|
||||
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
|
||||
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
||||
</van-field>
|
||||
</van-form>
|
||||
@@ -223,6 +223,23 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
onReady() {
|
||||
this.queryCondolenceType()
|
||||
this.openTodoTask()
|
||||
},
|
||||
openTodoTask() {
|
||||
const taskId = GetQueryString("taskId")
|
||||
const bizId = GetQueryString("bizId")
|
||||
if (!taskId || !bizId) return
|
||||
this.$axios.post("/flow/common/taskInfo", {taskId}).then((res) => {
|
||||
if (res.code !== 0 || !res.data) return
|
||||
const task = res.data
|
||||
if (task.taskName !== "cbd41337-3c87-4959-9027-809df5c7ef5e") return
|
||||
const row = {id: bizId, taskId: task.id, curTaskName: task.displayName}
|
||||
if (task.taskState === 10 && task.executable && GetQueryString("tab") === "todo") {
|
||||
this.onApproval(row)
|
||||
} else {
|
||||
this.onView(row)
|
||||
}
|
||||
})
|
||||
},
|
||||
onView(row) {
|
||||
this.$set(this, "showApprovalForm", false)
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<h2 class="condolence-info-card__title">
|
||||
审批签字<span class="condolence-audit-card__required">*</span>
|
||||
</h2>
|
||||
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
|
||||
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
|
||||
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
||||
</van-field>
|
||||
</van-form>
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
<div id="app">
|
||||
<div class="condolence-list-page">
|
||||
<van-nav-bar title="分工会主席审批" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
<van-nav-bar title="校工会主席审批" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<van-sticky offset-top="46px">
|
||||
<div class="condolence-list-sticky">
|
||||
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<h2 class="condolence-info-card__title">
|
||||
审批签字<span class="condolence-audit-card__required">*</span>
|
||||
</h2>
|
||||
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
|
||||
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
|
||||
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
||||
</van-field>
|
||||
</van-form>
|
||||
|
||||
+26
-9
@@ -9,7 +9,7 @@ layout("/layouts/platform_h5.html"){
|
||||
</style>
|
||||
<div id="app">
|
||||
<div class="difficult-help-approval-page">
|
||||
<van-nav-bar title="帮扶分工会审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-nav-bar title="校工会办公室主任审批" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
<div class="difficult-help-approval-sticky">
|
||||
<van-search
|
||||
@@ -29,7 +29,7 @@ layout("/layouts/platform_h5.html"){
|
||||
:close-on-click-overlay="false"
|
||||
>
|
||||
<van-dropdown-item
|
||||
v-if="$auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
|
||||
v-if="filterUserReady && $auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
|
||||
:options="unionList"
|
||||
@change="unionChange"
|
||||
@open="onFilterOpen"
|
||||
@@ -37,7 +37,7 @@ layout("/layouts/platform_h5.html"){
|
||||
v-model="pageForm.unionId">
|
||||
</van-dropdown-item>
|
||||
<van-dropdown-item
|
||||
v-if="$auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
|
||||
v-if="filterUserReady && $auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
|
||||
:options="unitList"
|
||||
@change="doSearch"
|
||||
@open="onFilterOpen"
|
||||
@@ -228,6 +228,14 @@ layout("/layouts/platform_h5.html"){
|
||||
components: {
|
||||
"info": INFO
|
||||
},
|
||||
computed: {
|
||||
// 首次进入时用户接口可能尚未返回;非管理员还需所属工会,避免空对象报错或请求全部工会。
|
||||
filterUserReady() {
|
||||
const user = this.$store.state.user
|
||||
if (!user || !Array.isArray(user.roles)) return false
|
||||
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"]) || !!(user.union && user.union.id)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
|
||||
@@ -419,13 +427,22 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// 先订阅弹框状态,工会加载异常也不能阻断等级选择器与确认框的打开、返回。
|
||||
if (window.h5HistoryLayerManager) {
|
||||
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
|
||||
this.$set(this, "confirmVisible", stack.includes("difficult-help-action-confirm"))
|
||||
this.$set(this, "showLevelPicker", stack.includes("difficult-help-level-picker"))
|
||||
}))
|
||||
}
|
||||
this.initLevelDict()
|
||||
this.initUnion().then(() => this.flushUnits())
|
||||
if (!window.h5HistoryLayerManager) return
|
||||
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
|
||||
this.$set(this, "confirmVisible", stack.includes("difficult-help-action-confirm"))
|
||||
this.$set(this, "showLevelPicker", stack.includes("difficult-help-level-picker"))
|
||||
}))
|
||||
// 缓存已就绪时立即加载,否则等待用户接口回填;只初始化一次,避免重置已选筛选条件。
|
||||
// $watch 由当前 Vue 实例管理,页面销毁时自动注销。
|
||||
let filtersInitialized = false
|
||||
this.$watch("filterUserReady", (ready) => {
|
||||
if (!ready || filtersInitialized) return
|
||||
filtersInitialized = true
|
||||
this.initUnion().then(() => this.flushUnits())
|
||||
}, {immediate: true})
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof this.historyLayerUnsubscribe === "function") this.historyLayerUnsubscribe()
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ layout("/layouts/platform_h5.html"){
|
||||
</style>
|
||||
<div id="app">
|
||||
<div class="difficult-help-approval-page">
|
||||
<van-nav-bar title="帮扶校工会审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-nav-bar title="分管校领导审批" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
<div class="difficult-help-approval-sticky">
|
||||
<van-search
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ layout("/layouts/platform_h5.html"){
|
||||
</style>
|
||||
<div id="app">
|
||||
<div class="difficult-help-approval-page">
|
||||
<van-nav-bar title="帮扶二级党组织审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-nav-bar title="校工会主席审批" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
<div class="difficult-help-approval-sticky">
|
||||
<van-search
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const apps = {
|
||||
// 从“我的工具”进入时指定分类名称;空值表示恢复应用中心原有浏览状态。
|
||||
props: {
|
||||
initialCategoryName: {type: String, default: ""}
|
||||
},
|
||||
template: /*language=HTML*/ `
|
||||
<div class="apps-container">
|
||||
<van-nav-bar title="应用中心" placeholder fixed></van-nav-bar>
|
||||
@@ -201,7 +205,8 @@ const apps = {
|
||||
},
|
||||
mounted() {
|
||||
// 动态分类加载完成后再恢复分类,避免把缓存的动态分类误判为“全部应用”
|
||||
this.restoreAppsState()
|
||||
// 指定入口不恢复旧搜索词、滚动位置及子菜单弹框,避免目标分类被旧状态遮挡。
|
||||
if (!this.initialCategoryName) this.restoreAppsState()
|
||||
this.loadCategories().finally(() => {
|
||||
this.restoreActiveCategory()
|
||||
this.loadApps()
|
||||
@@ -249,6 +254,13 @@ const apps = {
|
||||
},
|
||||
|
||||
restoreActiveCategory() {
|
||||
// 动态分类返回后按名称定位;未配置或无可用分类时明确提示,并展示全部应用。
|
||||
if (this.initialCategoryName) {
|
||||
const targetIndex = this.allCategories.findIndex(item => item.name === this.initialCategoryName)
|
||||
this.$set(this, "activeTab", targetIndex >= 0 ? targetIndex : 0)
|
||||
if (targetIndex < 0) this.$toast("未找到“" + this.initialCategoryName + "”分类")
|
||||
return
|
||||
}
|
||||
const index = this.allCategories.findIndex(item => String(item.id) === String(this.cachedCategoryId))
|
||||
this.activeTab = index >= 0 ? index : 0
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<todo v-if="homeTabbarActive === 'todo'" @todo-count-change="updateTodoCount"></todo>
|
||||
<work v-if="homeTabbarActive === 'work'"></work>
|
||||
<mine v-if="homeTabbarActive === 'mine'" @switch-tab="switchHomeTab"></mine>
|
||||
<apps v-if="homeTabbarActive === 'apps'"></apps>
|
||||
<apps v-if="homeTabbarActive === 'apps'" :initial-category-name="appsCategoryName"></apps>
|
||||
<msg v-if="homeTabbarActive === 'msg'" @unread-count-change="updateUnreadMessageCount"></msg>
|
||||
</div>
|
||||
|
||||
@@ -111,19 +111,25 @@ layout("/layouts/platform_h5.html"){
|
||||
data() {
|
||||
return {
|
||||
homeTabbarActive: "home",
|
||||
appsCategoryName: "",
|
||||
todoCount: 0,
|
||||
unreadMessageCount: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
homeTabbarChange(val) {
|
||||
// 普通页签切换沿用应用页缓存,不保留工具入口指定的分类。
|
||||
this.$set(this, "appsCategoryName", "")
|
||||
this.$store.commit("setActiveTarBar", val)
|
||||
},
|
||||
switchHomeTab(val) {
|
||||
this.homeTabbarActive = val
|
||||
// categoryName 来自“我的工具”,先传分类再挂载应用页,避免读取上一次的分类缓存。
|
||||
switchHomeTab(val, categoryName = "") {
|
||||
this.$set(this, "appsCategoryName", val === "apps" ? categoryName : "")
|
||||
this.$set(this, "homeTabbarActive", val)
|
||||
this.$store.commit("setActiveTarBar", val)
|
||||
},
|
||||
moduleClick(val) {
|
||||
this.$set(this, "appsCategoryName", "")
|
||||
this.homeTabbarActive = "apps"
|
||||
this.$store.commit("setActiveTarBar", "apps")
|
||||
},
|
||||
|
||||
@@ -74,22 +74,23 @@ const mine = {
|
||||
<van-icon name="edit"></van-icon>
|
||||
<span>我的签名</span>
|
||||
</button>
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps', '我的收藏')">
|
||||
<van-icon name="star-o"></van-icon>
|
||||
<span>我的收藏</span>
|
||||
</button>
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps', '职工福利')">
|
||||
<van-icon name="gift-o"></van-icon>
|
||||
<span>福利中心</span>
|
||||
</button>
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps', '活动管理')">
|
||||
<van-icon name="calendar-o"></van-icon>
|
||||
<span>活动报名</span>
|
||||
</button>
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<button type="button" class="mine-grid-item" @click="$pjaxReplace('/platform/h5/qsv')">
|
||||
<van-icon name="completed-o"></van-icon>
|
||||
<span>问卷调查</span>
|
||||
</button>
|
||||
<!-- 暂不开放设置中心和帮助中心,保留入口模板便于恢复。
|
||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||
<van-icon name="setting-o"></van-icon>
|
||||
<span>设置中心</span>
|
||||
@@ -98,6 +99,7 @@ const mine = {
|
||||
<van-icon name="question-o"></van-icon>
|
||||
<span>帮助中心</span>
|
||||
</button>
|
||||
-->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -150,9 +152,10 @@ const mine = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchTab(tab) {
|
||||
// tab 为底部页签,categoryName 为可选应用分类名称;通过事件交给首页切换并定位分类。
|
||||
switchTab(tab, categoryName = "") {
|
||||
this.$store.commit("setActiveTarBar", tab)
|
||||
this.$emit("switch-tab", tab)
|
||||
this.$emit("switch-tab", tab, categoryName)
|
||||
},
|
||||
logout() {
|
||||
this.$dialog
|
||||
|
||||
@@ -281,11 +281,13 @@ const todo = {
|
||||
// 处理任务
|
||||
onView(task) {
|
||||
const {taskId, taskKey, instanceId, businessNo, formKey, h5FormKey} = task;
|
||||
if (!h5FormKey) {
|
||||
const mobileFormKey = h5FormKey || (taskKey === "cbd41337-3c87-4959-9027-809df5c7ef5e"
|
||||
? "/platform/condolence/fourthApproval/h5" : "");
|
||||
if (!mobileFormKey) {
|
||||
this.$toast("当前流程暂不支持手机端审核,请前往PC端页面审核!");
|
||||
return;
|
||||
}
|
||||
this.$pjaxReplace(h5FormKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
|
||||
this.$pjaxReplace(mobileFormKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
|
||||
},
|
||||
|
||||
// 根据当前列表类型统一展示流程状态,发起列表保留流程实例的实际状态。
|
||||
|
||||
Reference in New Issue
Block a user