Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
637fe6f838 | ||
|
|
4f85bb08e2 | ||
|
|
29260423d5 | ||
|
|
d8e7adfe9a | ||
|
|
bc1b3bf7fe | ||
|
|
59fd83ba24 | ||
|
|
3059603186 | ||
|
|
f17f6153b5 | ||
|
|
e87014fa72 | ||
|
|
0bde359113 |
@@ -19,6 +19,7 @@ RoleConstant {
|
|||||||
SCHOOL_UNION_ADMIN("校工会管理员"),
|
SCHOOL_UNION_ADMIN("校工会管理员"),
|
||||||
SCHOOL_UNION_CHAIRMAN("校工会主席"),
|
SCHOOL_UNION_CHAIRMAN("校工会主席"),
|
||||||
SCHOOL_UNION_VICE_CHAIRMAN("校工会副主席"),
|
SCHOOL_UNION_VICE_CHAIRMAN("校工会副主席"),
|
||||||
|
SCHOOL_UNION_FINANCE("校工会财务"),
|
||||||
SCHOOL_OFFICE_DIRECTOR("校工会办公室主任"),
|
SCHOOL_OFFICE_DIRECTOR("校工会办公室主任"),
|
||||||
SCHOOL_UNION_WELFARE_ADMIN("校工会福利管理员"),
|
SCHOOL_UNION_WELFARE_ADMIN("校工会福利管理员"),
|
||||||
SCHOOL_UNION_MEMBER_ADMIN("校工会会员管理员"),
|
SCHOOL_UNION_MEMBER_ADMIN("校工会会员管理员"),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import com.budwk.app.base.exception.BaseException;
|
|||||||
import com.budwk.app.sys.models.Sys_file;
|
import com.budwk.app.sys.models.Sys_file;
|
||||||
import com.budwk.app.sys.models.Sys_office_template;
|
import com.budwk.app.sys.models.Sys_office_template;
|
||||||
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
||||||
|
import com.deepoove.poi.XWPFTemplate;
|
||||||
|
import com.deepoove.poi.config.Configure;
|
||||||
import com.deepoove.poi.data.ByteArrayPictureRenderData;
|
import com.deepoove.poi.data.ByteArrayPictureRenderData;
|
||||||
import com.deepoove.poi.data.PictureRenderData;
|
import com.deepoove.poi.data.PictureRenderData;
|
||||||
import com.deepoove.poi.data.PictureType;
|
import com.deepoove.poi.data.PictureType;
|
||||||
@@ -18,6 +20,12 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
|||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -66,6 +74,88 @@ public class SysOfficeTemplateUtil {
|
|||||||
return new ByteArrayInputStream(fileBytes);
|
return new ByteArrayInputStream(fileBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一渲染 DOC/DOCX 模板,保留调用方的循环表格等策略;模板输入流由调用方关闭。
|
||||||
|
*/
|
||||||
|
public byte[] renderTemplate(InputStream input, Object data, Configure config, boolean pdf, String context) throws Exception {
|
||||||
|
if (input == null) throw new BaseException("模板文件不存在");
|
||||||
|
byte[] templateBytes = input.readAllBytes();
|
||||||
|
try (ByteArrayInputStream formatInput = new ByteArrayInputStream(templateBytes)) {
|
||||||
|
if (com.aspose.words.FileFormatUtil.detectFileFormat(formatInput).getLoadFormat() != com.aspose.words.LoadFormat.DOCX) {
|
||||||
|
try (ByteArrayInputStream source = new ByteArrayInputStream(templateBytes);
|
||||||
|
ByteArrayOutputStream converted = new ByteArrayOutputStream()) {
|
||||||
|
new com.aspose.words.Document(source).save(converted, com.aspose.words.SaveFormat.DOCX);
|
||||||
|
templateBytes = converted.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] wordBytes;
|
||||||
|
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateBytes);
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
XWPFTemplate template = XWPFTemplate.compile(templateInput, config == null ? Configure.builder().build() : config)) {
|
||||||
|
template.render(data).write(output);
|
||||||
|
wordBytes = output.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[Office模板渲染] Word模板渲染失败,context={}", context, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
log.info("[Office模板渲染] Word渲染完成,context={}, templateBytes={}, outputBytes={}", context, templateBytes.length, wordBytes.length);
|
||||||
|
if (!pdf) return wordBytes;
|
||||||
|
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(wordBytes);
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||||
|
com.aspose.words.Document document = new com.aspose.words.Document(wordInput);
|
||||||
|
configureDocumentFonts(document, context);
|
||||||
|
com.aspose.words.PdfSaveOptions options = new com.aspose.words.PdfSaveOptions();
|
||||||
|
options.setFontEmbeddingMode(com.aspose.words.PdfFontEmbeddingMode.EMBED_ALL);
|
||||||
|
document.save(output, options);
|
||||||
|
log.info("[Office模板渲染] PDF转换完成,context={}, outputBytes={}", context, output.size());
|
||||||
|
return output.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[Office模板渲染] PDF转换失败,context={}", context, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureDocumentFonts(com.aspose.words.Document document, String context) throws Exception {
|
||||||
|
com.aspose.words.FontSettings settings = new com.aspose.words.FontSettings();
|
||||||
|
List<com.aspose.words.FontSourceBase> sources = new ArrayList<>(List.of(settings.getFontsSources()));
|
||||||
|
Set<String> folders = new LinkedHashSet<>();
|
||||||
|
String windowsDir = System.getenv("WINDIR");
|
||||||
|
if (StrUtil.isNotBlank(windowsDir)) folders.add(new File(windowsDir, "Fonts").getPath());
|
||||||
|
String localAppData = System.getenv("LOCALAPPDATA");
|
||||||
|
if (StrUtil.isNotBlank(localAppData)) folders.add(new File(localAppData, "Microsoft/Windows/Fonts").getPath());
|
||||||
|
folders.add(new File(System.getProperty("user.dir"), "fonts").getPath());
|
||||||
|
for (String folder : folders) {
|
||||||
|
File directory = new File(folder);
|
||||||
|
log.info("[Office模板渲染] 字体目录,context={}, path={}, readable={}", context, folder, directory.isDirectory() && directory.canRead());
|
||||||
|
if (directory.isDirectory() && directory.canRead()) sources.add(new com.aspose.words.FolderFontSource(folder, true));
|
||||||
|
}
|
||||||
|
settings.setFontsSources(sources.toArray(new com.aspose.words.FontSourceBase[0]));
|
||||||
|
Set<String> availableFonts = new LinkedHashSet<>();
|
||||||
|
for (com.aspose.words.FontSourceBase source : sources) {
|
||||||
|
for (com.aspose.words.PhysicalFontInfo font : source.getAvailableFonts()) availableFonts.add(font.getFontFamilyName());
|
||||||
|
}
|
||||||
|
String chineseFont = null;
|
||||||
|
for (String candidate : List.of("SimSun", "宋体", "Microsoft YaHei", "微软雅黑", "DengXian", "等线", "SimHei", "黑体", "FangSong", "仿宋", "Noto Serif CJK SC", "Noto Sans CJK SC")) {
|
||||||
|
if (availableFonts.contains(candidate)) {
|
||||||
|
chineseFont = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("[Office模板渲染] 中文字体选择,context={}, fontCount={}, chineseFont={}", context, availableFonts.size(), chineseFont);
|
||||||
|
if (chineseFont != null) {
|
||||||
|
settings.getSubstitutionSettings().getDefaultFontSubstitution().setDefaultFontName(chineseFont);
|
||||||
|
for (String name : List.of("宋体", "SimSun", "新宋体", "NSimSun", "黑体", "SimHei", "仿宋", "FangSong", "楷体", "KaiTi", "微软雅黑", "Microsoft YaHei")) {
|
||||||
|
if (!availableFonts.contains(name)) settings.getSubstitutionSettings().getTableSubstitution().setSubstitutes(name, chineseFont);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("[Office模板渲染] 未发现常用中文字体,context={}, availableFonts={}", context, availableFonts);
|
||||||
|
}
|
||||||
|
settings.getFallbackSettings().loadMsOfficeFallbackSettings();
|
||||||
|
document.setFontSettings(settings);
|
||||||
|
document.setWarningCallback(warning -> log.warn("[Office模板渲染] 字体或排版警告,context={}, type={}, detail={}", context, warning.getWarningType(), warning.getDescription()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签字url转换为poi的图片对象
|
* 签字url转换为poi的图片对象
|
||||||
*
|
*
|
||||||
@@ -77,13 +167,7 @@ public class SysOfficeTemplateUtil {
|
|||||||
public PictureRenderData createPictureRenderData(int width, int height, String url) {
|
public PictureRenderData createPictureRenderData(int width, int height, String url) {
|
||||||
try {
|
try {
|
||||||
byte[] bytes = getFileBytesByUrl(url);
|
byte[] bytes = getFileBytesByUrl(url);
|
||||||
// if (StrUtil.isBlank(url)) {
|
log.info("[Office图片读取] 创建图片渲染数据,referenceLength={}, byteCount={}, width={}, height={}", StrUtil.nullToEmpty(url).length(), bytes == null ? null : bytes.length, width, height);
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// int startIndex = url.indexOf("id=") + "id=".length();
|
|
||||||
// String id = url.substring(startIndex);
|
|
||||||
// Sys_file sysFile = dao.fetch(Sys_file.class, id);
|
|
||||||
// byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
|
|
||||||
ByteArrayPictureRenderData pictureRenderData = new ByteArrayPictureRenderData(bytes, PictureType.PNG);
|
ByteArrayPictureRenderData pictureRenderData = new ByteArrayPictureRenderData(bytes, PictureType.PNG);
|
||||||
PictureStyle pictureStyle = new PictureStyle();
|
PictureStyle pictureStyle = new PictureStyle();
|
||||||
pictureStyle.setWidth(width);
|
pictureStyle.setWidth(width);
|
||||||
@@ -91,7 +175,7 @@ public class SysOfficeTemplateUtil {
|
|||||||
pictureRenderData.setPictureStyle(pictureStyle);
|
pictureRenderData.setPictureStyle(pictureStyle);
|
||||||
return pictureRenderData;
|
return pictureRenderData;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("文件转换PictureRenderData错误,文件路径:{},{}", url, e.getMessage());
|
log.error("[Office图片读取] 创建图片渲染数据失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,13 +224,20 @@ public class SysOfficeTemplateUtil {
|
|||||||
public byte[] getFileBytesByUrl(String url) {
|
public byte[] getFileBytesByUrl(String url) {
|
||||||
try {
|
try {
|
||||||
if (StrUtil.isBlank(url)) {
|
if (StrUtil.isBlank(url)) {
|
||||||
|
log.warn("[Office图片读取] 图片引用为空");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int startIndex = url.indexOf("id=") + "id=".length();
|
int startIndex = url.indexOf("id=") + "id=".length();
|
||||||
String id = url.substring(startIndex);
|
String id = url.substring(startIndex);
|
||||||
|
String logFileId = url.contains("id=") ? id : "未识别id参数";
|
||||||
|
log.info("[Office图片读取] 解析文件引用,fileId={}, hasIdParameter={}", logFileId, url.contains("id="));
|
||||||
Sys_file sysFile = dao.fetch(Sys_file.class, id);
|
Sys_file sysFile = dao.fetch(Sys_file.class, id);
|
||||||
return SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
|
log.info("[Office图片读取] 查询文件记录,fileId={}, found={}, bucket={}, storagePathPresent={}", logFileId, sysFile != null, sysFile == null ? null : sysFile.getBucket(), sysFile != null && StrUtil.isNotBlank(sysFile.getStoragePath()));
|
||||||
|
byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
|
||||||
|
log.info("[Office图片读取] 读取存储文件完成,fileId={}, byteCount={}", logFileId, bytes == null ? null : bytes.length);
|
||||||
|
return bytes;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.error("[Office图片读取] 读取图片失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.budwk.app.flow.handler;
|
||||||
|
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.flow.engine.AssignmentHandler;
|
||||||
|
import com.budwk.app.flow.engine.core.Execution;
|
||||||
|
import com.budwk.app.flow.engine.core.ServiceContext;
|
||||||
|
import com.budwk.app.flow.engine.model.TaskModel;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
|
import org.nutz.dao.sql.Sql;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为流程任务分配拥有校工会财务角色的用户。
|
||||||
|
*/
|
||||||
|
public class FlowSchoolUnionFinanceHandler implements AssignmentHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按校工会财务角色查询参与者,并去除重复的用户 ID。
|
||||||
|
*
|
||||||
|
* @param model 当前流程任务模型,无需额外传入人员参数
|
||||||
|
* @param execution 当前流程执行上下文,无需额外传入业务参数
|
||||||
|
* @return 用户 ID 列表;未配置角色或角色用户时返回空列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<String> assign(TaskModel model, Execution execution) {
|
||||||
|
Dao dao = ServiceContext.find(Dao.class);
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
userRole.userId
|
||||||
|
FROM
|
||||||
|
`sys_user_role` userRole
|
||||||
|
LEFT JOIN sys_role role ON role.id = userRole.roleId
|
||||||
|
WHERE
|
||||||
|
role.`code` = @roleCode
|
||||||
|
GROUP BY
|
||||||
|
userRole.userId
|
||||||
|
""").setParam("roleCode", RoleConstant.SCHOOL_UNION_FINANCE.name());
|
||||||
|
sql.setCallback(Sqls.callback.strList());
|
||||||
|
dao.execute(sql);
|
||||||
|
return sql.getList(String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回流程设计器中展示的参与者处理类名称。
|
||||||
|
*
|
||||||
|
* @return 流程设计器下拉选项名称
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "获取校工会财务";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.budwk.app.flow.engine.event.ProcessEvent;
|
|||||||
import com.budwk.app.flow.engine.event.ProcessEventListener;
|
import com.budwk.app.flow.engine.event.ProcessEventListener;
|
||||||
import com.budwk.app.flow.entity.ProcessTask;
|
import com.budwk.app.flow.entity.ProcessTask;
|
||||||
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
|
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
|
||||||
|
import com.budwk.app.flow.service.SchoolOaTodoService;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
@@ -17,11 +18,16 @@ public class ProcessTaskEndEventListener implements ProcessEventListener {
|
|||||||
@Inject
|
@Inject
|
||||||
private Dao dao;
|
private Dao dao;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SchoolOaTodoService schoolOaTodoService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEvent(ProcessEvent event) {
|
public void onEvent(ProcessEvent event) {
|
||||||
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_END) {
|
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_END) {
|
||||||
Long sourceId = event.getSourceId();
|
Long sourceId = event.getSourceId();
|
||||||
ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
|
ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
|
||||||
|
// service 区分正常完成与废弃,只有正常完成才发送通达 status=2。
|
||||||
|
schoolOaTodoService.completeTodo(task);
|
||||||
System.out.println("流程任务完成事件:" + task.getDisplayName());
|
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.ProcessInstance;
|
||||||
import com.budwk.app.flow.entity.ProcessTask;
|
import com.budwk.app.flow.entity.ProcessTask;
|
||||||
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
|
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
|
||||||
|
import com.budwk.app.flow.service.SchoolOaTodoService;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
@@ -22,11 +23,16 @@ public class ProcessTaskRevokeEventListener implements ProcessEventListener {
|
|||||||
@Inject
|
@Inject
|
||||||
private Dao dao;
|
private Dao dao;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SchoolOaTodoService schoolOaTodoService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEvent(ProcessEvent event) {
|
public void onEvent(ProcessEvent event) {
|
||||||
if(event.getEventType()== ProcessEventTypeEnum.PROCESS_TASK_REVOKE){
|
if(event.getEventType()== ProcessEventTypeEnum.PROCESS_TASK_REVOKE){
|
||||||
Long taskId = event.getSourceId();
|
Long taskId = event.getSourceId();
|
||||||
ProcessTask task = dao.fetch(ProcessTask.class, taskId);
|
ProcessTask task = dao.fetch(ProcessTask.class, taskId);
|
||||||
|
// 公开通达协议未定义取消,集中记录未同步事项,不把撤回冒充正常已办。
|
||||||
|
schoolOaTodoService.cancelTodo(task);
|
||||||
|
|
||||||
String taskDisplayName = task.getDisplayName();
|
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.entity.ProcessTaskActor;
|
||||||
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
|
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
|
||||||
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
|
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
|
||||||
|
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
@@ -30,13 +31,22 @@ public class ProcessTaskStartEventListener implements ProcessEventListener {
|
|||||||
private Dao dao;
|
private Dao dao;
|
||||||
@Inject
|
@Inject
|
||||||
private GlobalMessageSendService globalMessageSendService;
|
private GlobalMessageSendService globalMessageSendService;
|
||||||
|
@Inject
|
||||||
|
private DifficultHelpCommonService difficultHelpCommonService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEvent(ProcessEvent event) {
|
public void onEvent(ProcessEvent event) {
|
||||||
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) {
|
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) {
|
||||||
Long sourceId = event.getSourceId();
|
Long sourceId = event.getSourceId();
|
||||||
ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
|
ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
|
||||||
|
if (task == null) return;
|
||||||
sendMessage(task);
|
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.ProcessDefineService;
|
||||||
import com.budwk.app.flow.service.ProcessInstanceService;
|
import com.budwk.app.flow.service.ProcessInstanceService;
|
||||||
import com.budwk.app.flow.service.ProcessTaskService;
|
import com.budwk.app.flow.service.ProcessTaskService;
|
||||||
|
import com.budwk.app.flow.service.SchoolOaTodoService;
|
||||||
import com.budwk.app.flow.vo.ProcessTaskVO;
|
import com.budwk.app.flow.vo.ProcessTaskVO;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
@@ -56,6 +57,9 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
|||||||
@Inject
|
@Inject
|
||||||
private ProcessDefineService processDefineService;
|
private ProcessDefineService processDefineService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SchoolOaTodoService schoolOaTodoService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean save(ProcessTask param) {
|
public boolean save(ProcessTask param) {
|
||||||
param.setId(null);
|
param.setId(null);
|
||||||
@@ -213,6 +217,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
|||||||
System.out.println("创建任务:" + processTask.getTaskName() + "," + processTask.getDisplayName());
|
System.out.println("创建任务:" + processTask.getTaskName() + "," + processTask.getDisplayName());
|
||||||
processTaskList.add(processTask);
|
processTaskList.add(processTask);
|
||||||
addTaskActor(processTask.getId(), getTaskActors(taskModel, execution));
|
addTaskActor(processTask.getId(), getTaskActors(taskModel, execution));
|
||||||
|
// 参与人落库后统一推送通达待办,开始监听器不再重复推送。
|
||||||
|
schoolOaTodoService.createTodo(processTask);
|
||||||
|
|
||||||
return processTaskList;
|
return processTaskList;
|
||||||
}
|
}
|
||||||
@@ -284,6 +290,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
|||||||
clone.setUpdatedBy(null);
|
clone.setUpdatedBy(null);
|
||||||
dao().insert(clone);
|
dao().insert(clone);
|
||||||
addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor));
|
addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor));
|
||||||
|
// 并行加签绕过普通节点创建入口,需要为新增的个人任务同步待办。
|
||||||
|
schoolOaTodoService.createTodo(clone);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -318,6 +326,7 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public ProcessTask rejectTask(ProcessModel model, ProcessTask currentTask) {
|
public ProcessTask rejectTask(ProcessModel model, ProcessTask currentTask) {
|
||||||
Long taskParentId = currentTask.getTaskParentId();
|
Long taskParentId = currentTask.getTaskParentId();
|
||||||
if (ObjectUtil.isEmpty(taskParentId) || taskParentId.equals(0L)) {
|
if (ObjectUtil.isEmpty(taskParentId) || taskParentId.equals(0L)) {
|
||||||
@@ -351,6 +360,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
|||||||
}
|
}
|
||||||
saveProcessTask(task);
|
saveProcessTask(task);
|
||||||
addTaskActor(task.getId(), CollectionUtil.newArrayList(task.getOperator()));
|
addTaskActor(task.getId(), CollectionUtil.newArrayList(task.getOperator()));
|
||||||
|
// 驳回创建了新的任务 ID,按新任务推送,避免复用历史待办。
|
||||||
|
schoolOaTodoService.createTodo(task);
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,6 +435,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
|||||||
System.out.println("创建会签任务:" + processTask.getTaskName() + "," + processTask.getDisplayName());
|
System.out.println("创建会签任务:" + processTask.getTaskName() + "," + processTask.getDisplayName());
|
||||||
processTaskList.add(processTask);
|
processTaskList.add(processTask);
|
||||||
addTaskActor(processTask.getId(), CollectionUtil.newArrayList(taskActor));
|
addTaskActor(processTask.getId(), CollectionUtil.newArrayList(taskActor));
|
||||||
|
// 串行和并行会签均按实际生成的个人任务发送待办。
|
||||||
|
schoolOaTodoService.createTodo(processTask);
|
||||||
});
|
});
|
||||||
ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class);
|
ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class);
|
||||||
// 更新会签变量到流程实例参数中
|
// 更新会签变量到流程实例参数中
|
||||||
@@ -496,9 +509,12 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
|
|||||||
|
|
||||||
// 创建转办任务参与者
|
// 创建转办任务参与者
|
||||||
addTaskActor(newTask.getId(), List.of(transferUserId));
|
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 -> {
|
processTaskList.forEach(processTask -> {
|
||||||
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_START).sourceId(processTask.getId()).build());
|
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.constant.RedisConstant;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.base.sms.SmsService;
|
import com.budwk.app.base.sms.SmsService;
|
||||||
|
import com.budwk.app.base.wxwork.WxWorkMessageService;
|
||||||
import com.budwk.app.base.sms.model.MsgPlatformWechatRequest;
|
import com.budwk.app.base.sms.model.MsgPlatformWechatRequest;
|
||||||
import com.budwk.app.base.utils.ManyAddOrRenewUtil;
|
import com.budwk.app.base.utils.ManyAddOrRenewUtil;
|
||||||
import com.budwk.app.sys.models.Sys_msg;
|
import com.budwk.app.sys.models.Sys_msg;
|
||||||
@@ -62,6 +63,8 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
|||||||
private ManyAddOrRenewUtil manyAddOrRenewUtil;
|
private ManyAddOrRenewUtil manyAddOrRenewUtil;
|
||||||
@Inject
|
@Inject
|
||||||
private SmsService smsService;
|
private SmsService smsService;
|
||||||
|
@Inject
|
||||||
|
private WxWorkMessageService wxWorkMessageService;
|
||||||
|
|
||||||
private static NutMap typeMap = NutMap.NEW().addv("system", "系统消息").addv("user", "用户消息");
|
private static NutMap typeMap = NutMap.NEW().addv("system", "系统消息").addv("user", "用户消息");
|
||||||
|
|
||||||
@@ -102,14 +105,29 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//发送学校平台消息
|
// isExternal=false 为仅站内通知;外部通道分别处理异常,避免短信失败阻断企业微信。
|
||||||
|
if (isExternal) {
|
||||||
ThreadUtil.execute(() -> {
|
ThreadUtil.execute(() -> {
|
||||||
for (String loginName : loginNames) {
|
for (String loginName : loginNames) {
|
||||||
smsService.sendSmsByAccount(loginName,sysMsg.getNote());
|
try {
|
||||||
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
|
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;
|
return sysMsg;
|
||||||
}
|
}
|
||||||
@@ -292,11 +310,13 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void sendMsg(String loginname, String title, String body, String sender) {
|
public void sendMsg(String loginname, String title, String body, String sender) {
|
||||||
sendMsg(List.of(loginname), title, body, SecurityUtil.getUserId());
|
sendMsg(List.of(loginname), title, body, SecurityUtil.getUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void sendMsg(List<String> loginNames, String title, String body, String sender) {
|
public void sendMsg(List<String> loginNames, String title, String body, String sender) {
|
||||||
Sys_msg sysMsg = new Sys_msg();
|
Sys_msg sysMsg = new Sys_msg();
|
||||||
sysMsg.setTitle(title);
|
sysMsg.setTitle(title);
|
||||||
@@ -309,6 +329,7 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void sendMsgInSys(List<String> loginNames, String title, String body, String sender) {
|
public void sendMsgInSys(List<String> loginNames, String title, String body, String sender) {
|
||||||
Sys_msg sysMsg = new Sys_msg();
|
Sys_msg sysMsg = new Sys_msg();
|
||||||
sysMsg.setTitle(title);
|
sysMsg.setTitle(title);
|
||||||
|
|||||||
@@ -189,25 +189,16 @@ public class WkFailProcessor extends ViewProcessor {
|
|||||||
if (Globals.sso) {
|
if (Globals.sso) {
|
||||||
//1.判断是否为企业微信浏览器并且对接了企业微信
|
//1.判断是否为企业微信浏览器并且对接了企业微信
|
||||||
if (browserPlatform == BrowserPlatform.QI_YE_WEI_XIN && propertiesProxy.getBoolean("wxwork.enable", false)) {
|
if (browserPlatform == BrowserPlatform.QI_YE_WEI_XIN && propertiesProxy.getBoolean("wxwork.enable", false)) {
|
||||||
//进行企业微信授权
|
// 专用企业微信认证配置缺失时整组回退到已接通的 CAS,保留卡片原始页面。
|
||||||
// String token = getWxWorkToken();
|
try {
|
||||||
// if (StrUtil.isNotBlank(token)) {
|
String authUrl = buildEnterpriseCasUrl(redirect);
|
||||||
// //跳转到企业微信授权地址
|
log.info("auth:" + authUrl);
|
||||||
// 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);
|
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)) {
|
} 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) 地址");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断浏览器平台
|
* 判断浏览器平台
|
||||||
*
|
*
|
||||||
|
|||||||
+7
-3
@@ -229,14 +229,18 @@ public class ProposalMineController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除当前用户可删除的提案。
|
||||||
|
*
|
||||||
|
* @param id 提案 ID;仅允许删除本人未发起流程或退回发起节点的提案
|
||||||
|
* @return Result,成功时返回成功结果,不满足删除条件时返回原因
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("proposal.mine")
|
@SaCheckPermission("proposal.mine")
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@SLog(tag = "提案管理系统-我的提案", msg = "删除提案")
|
@SLog(tag = "提案管理系统-我的提案", msg = "删除提案")
|
||||||
public Result delete(@Valid String id) {
|
public Result delete(@Valid String id) {
|
||||||
dao.delete(ProposalInfo.class, id);
|
return proposalCommonService.deleteMine(id, SecurityUtil.getUserId());
|
||||||
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
|
|
||||||
return Result.success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
|
|||||||
+10
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.democratic.proposal.service.common;
|
|||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.param.ExportTableColumns;
|
import com.budwk.app.base.param.ExportTableColumns;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
|
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
|
||||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
|
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
|
||||||
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
|
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
|
||||||
@@ -19,6 +20,15 @@ import java.util.Set;
|
|||||||
|
|
||||||
public interface ProposalCommonService extends BaseService<ProposalInfo> {
|
public interface ProposalCommonService extends BaseService<ProposalInfo> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除当前用户尚未发起流程或已退回发起节点的提案。
|
||||||
|
*
|
||||||
|
* @param id 待删除的提案 ID
|
||||||
|
* @param userId 当前登录用户 ID,用于核对提案创建人
|
||||||
|
* @return Result,删除成功返回 success;提案不存在、非本人或流程不允许删除时返回错误信息
|
||||||
|
*/
|
||||||
|
Result deleteMine(String id, String userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按页面允许的字段白名单添加排序条件,避免前端字段直接进入SQL。
|
* 按页面允许的字段白名单添加排序条件,避免前端字段直接进入SQL。
|
||||||
*
|
*
|
||||||
|
|||||||
+71
-27
@@ -17,6 +17,7 @@ import com.budwk.app.base.exception.BaseException;
|
|||||||
import com.budwk.app.base.param.ExportTableColumns;
|
import com.budwk.app.base.param.ExportTableColumns;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
import com.budwk.app.base.utils.PageUtil;
|
import com.budwk.app.base.utils.PageUtil;
|
||||||
@@ -52,6 +53,7 @@ import com.deepoove.poi.config.Configure;
|
|||||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ddr.poi.html.HtmlRenderPolicy;
|
import org.ddr.poi.html.HtmlRenderPolicy;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.FieldFilter;
|
import org.nutz.dao.FieldFilter;
|
||||||
@@ -62,6 +64,7 @@ import org.nutz.dao.util.cri.SqlExpressionGroup;
|
|||||||
import org.nutz.dao.util.cri.Static;
|
import org.nutz.dao.util.cri.Static;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.json.Json;
|
import org.nutz.json.Json;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
@@ -82,16 +85,7 @@ import java.util.stream.Collectors;
|
|||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalCommonService {
|
public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalCommonService {
|
||||||
|
|
||||||
/** 确定立案提案的 Word 模板编码。 */
|
/** 所有立案结果统一使用重庆工贸提案征集表模板。 */
|
||||||
private static final String PROPOSAL_CONFIRM_FILING_TEMPLATE_CODE = "proposal_confirm_filing";
|
|
||||||
|
|
||||||
/** 工作建议提案的 Word 模板编码。 */
|
|
||||||
private static final String PROPOSAL_SUGGESTION_TEMPLATE_CODE = "proposal_suggestion";
|
|
||||||
|
|
||||||
/** 不予立案提案的 Word 模板编码。 */
|
|
||||||
private static final String PROPOSAL_NOT_TEMPLATE_CODE = "proposal_not";
|
|
||||||
|
|
||||||
/** 历史提案立案结果为空或无法识别时使用的原 Word 模板编码。 */
|
|
||||||
private static final String PROPOSAL_DEFAULT_TEMPLATE_CODE = "proposal";
|
private static final String PROPOSAL_DEFAULT_TEMPLATE_CODE = "proposal";
|
||||||
|
|
||||||
/** PC端提案列表页面的排序字段白名单,页面代码由对应Controller固定传入。 */
|
/** PC端提案列表页面的排序字段白名单,页面代码由对应Controller固定传入。 */
|
||||||
@@ -193,6 +187,45 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅允许创建人删除草稿或退回发起节点的提案,并清理其 WF 流程。
|
||||||
|
*
|
||||||
|
* @param id 待删除的提案 ID
|
||||||
|
* @param userId 当前登录用户 ID,用于核对提案创建人
|
||||||
|
* @return Result,删除成功返回 success;提案不存在、非本人或流程不允许删除时返回错误信息
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result deleteMine(String id, String userId) {
|
||||||
|
if (StrUtil.isBlank(id) || StrUtil.isBlank(userId)) {
|
||||||
|
return Result.error("提案不存在或无权删除");
|
||||||
|
}
|
||||||
|
ProposalInfo proposal = dao().fetch(ProposalInfo.class, id);
|
||||||
|
if (proposal == null || !Objects.equals(proposal.getCreatedBy(), userId)) {
|
||||||
|
return Result.error("提案不存在或无权删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ProcessInstance> instances = dao().query(ProcessInstance.class,
|
||||||
|
Cnd.where(ProcessInstance::getBusinessNo, "=", id));
|
||||||
|
// 一个提案出现多个流程实例时无法安全判断当前节点,拒绝删除并保留数据。
|
||||||
|
if (instances.size() > 1) {
|
||||||
|
return Result.error("提案流程数据异常,无法删除");
|
||||||
|
}
|
||||||
|
if (!instances.isEmpty()) {
|
||||||
|
ProcessInstance instance = instances.get(0);
|
||||||
|
List<ProcessTask> tasks = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null);
|
||||||
|
// 存在并行任务时,只有所有活动任务均位于发起节点才允许删除。
|
||||||
|
if (!Objects.equals(instance.getState(), ProcessInstanceStateEnum.DOING.getCode())
|
||||||
|
|| tasks.isEmpty() || tasks.stream().anyMatch(task -> !Objects.equals(task.getTaskName(), "startTask"))) {
|
||||||
|
return Result.error("仅未提交或退回发起节点的提案可以删除");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dao().delete(ProposalInfo.class, id);
|
||||||
|
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询提案综合列表。
|
* 分页查询提案综合列表。
|
||||||
* 普通查询不关联历史任务表;仅在 origins 包含 answer 且 commonKeyword 非空时,
|
* 普通查询不关联历史任务表;仅在 origins 包含 answer 且 commonKeyword 非空时,
|
||||||
@@ -1367,6 +1400,11 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
|||||||
execute(sql);
|
execute(sql);
|
||||||
NutMap docData = (NutMap) sql.getResult();
|
NutMap docData = (NutMap) sql.getResult();
|
||||||
|
|
||||||
|
// 提案可能已被删除,避免在读取模板字段时产生空指针。
|
||||||
|
if (docData == null) {
|
||||||
|
throw new BaseException("提案信息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
//处理下富文本
|
//处理下富文本
|
||||||
String brief = docData.getString("brief");
|
String brief = docData.getString("brief");
|
||||||
String measures = docData.getString("measures");
|
String measures = docData.getString("measures");
|
||||||
@@ -1381,11 +1419,22 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
|||||||
ProcessInstance processInstance = dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", id));
|
ProcessInstance processInstance = dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", id));
|
||||||
|
|
||||||
List<ProcessTaskVO> doneTaskVos = new ArrayList<>();
|
List<ProcessTaskVO> doneTaskVos = new ArrayList<>();
|
||||||
|
// 尚未启动或缺少 WF 实例的提案仍导出基础信息,附议和审批栏保持为空。
|
||||||
|
if (processInstance != null) {
|
||||||
List<ProcessTask> doneTaskList = flowEngine.processTaskService().getDoneTaskList(processInstance.getId(), null);
|
List<ProcessTask> doneTaskList = flowEngine.processTaskService().getDoneTaskList(processInstance.getId(), null);
|
||||||
for (ProcessTask doneTask : doneTaskList) {
|
for (ProcessTask doneTask : doneTaskList) {
|
||||||
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(doneTask.getId());
|
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(doneTask.getId());
|
||||||
|
if (taskVO != null && taskVO.getExt() != null) {
|
||||||
doneTaskVos.add(taskVO);
|
doneTaskVos.add(taskVO);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 附议人单元格仅展示已完成附议任务中的姓名,多人以顿号分隔并去重。
|
||||||
|
docData.put("secondNames", doneTaskVos.stream()
|
||||||
|
.filter(task -> "second".equals(task.getTaskName()))
|
||||||
|
.map(task -> task.getTaskFormData().getStr("userName"))
|
||||||
|
.filter(StrUtil::isNotBlank).distinct().collect(Collectors.joining("、")));
|
||||||
|
|
||||||
// 按任务节点分组
|
// 按任务节点分组
|
||||||
Map<String, List<ProcessTaskVO>> taskGroups = doneTaskVos.stream()
|
Map<String, List<ProcessTaskVO>> taskGroups = doneTaskVos.stream()
|
||||||
@@ -1394,6 +1443,17 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
|||||||
docData.putAll(taskGroups);
|
docData.putAll(taskGroups);
|
||||||
putProposalExportTaskAliases(id, taskGroups, docData);
|
putProposalExportTaskAliases(id, taskGroups, docData);
|
||||||
|
|
||||||
|
// 中文区块名称保持与现有模板一致,实际审核记录按 WF 节点编码匹配。
|
||||||
|
// 区块内继续读取 taskFormData 的意见、姓名和日期;无记录时写入空列表,避免同名节点误填。
|
||||||
|
Map<String, String> approvalTemplateNodes = Map.of(
|
||||||
|
"分工会主席审批", "delegation",
|
||||||
|
"校工会主席审批", "schoolUnionChairApproval",
|
||||||
|
"分管校领导审批", "school_leader");
|
||||||
|
approvalTemplateNodes.forEach((templateKey, taskName) -> docData.put(templateKey, doneTaskVos.stream()
|
||||||
|
.filter(task -> taskName.equals(task.getTaskName()))
|
||||||
|
.sorted(Comparator.comparing(ProcessTaskVO::getFinishTime, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||||
|
.collect(Collectors.toList())));
|
||||||
|
|
||||||
// 提案附议
|
// 提案附议
|
||||||
List<NutMap> secondInfos = new ArrayList<>();
|
List<NutMap> secondInfos = new ArrayList<>();
|
||||||
List<ProcessTaskVO> secondTaskVos = taskGroups.get("提案附议");
|
List<ProcessTaskVO> secondTaskVos = taskGroups.get("提案附议");
|
||||||
@@ -1475,29 +1535,13 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
|||||||
docData.put("secondedList", secondInfos);
|
docData.put("secondedList", secondInfos);
|
||||||
docData.put("delegationAuditList", delegationAuditList);
|
docData.put("delegationAuditList", delegationAuditList);
|
||||||
|
|
||||||
String templateCode = getProposalTemplateCode(docData.getString("caseFilingResult"));
|
|
||||||
try {
|
try {
|
||||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(templateCode), config).render(docData).writeAndClose(byteArrayOutputStream);
|
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(PROPOSAL_DEFAULT_TEMPLATE_CODE), config).render(docData).writeAndClose(byteArrayOutputStream);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("提案导出失败{},提案id:{}", e.getMessage(), id);
|
log.error("提案导出失败{},提案id:{}", e.getMessage(), id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据立案结果选择数据库办公模板编码。
|
|
||||||
*
|
|
||||||
* @param caseFilingResult 立案结果字典编码:CONFIRM_FILING、SUGGESTION 或 NOT
|
|
||||||
* @return 对应的 Word 模板编码;历史空值或未知编码返回 proposal
|
|
||||||
*/
|
|
||||||
private String getProposalTemplateCode(String caseFilingResult) {
|
|
||||||
return switch (StrUtil.nullToEmpty(caseFilingResult)) {
|
|
||||||
case "CONFIRM_FILING" -> PROPOSAL_CONFIRM_FILING_TEMPLATE_CODE;
|
|
||||||
case "SUGGESTION" -> PROPOSAL_SUGGESTION_TEMPLATE_CODE;
|
|
||||||
case "NOT" -> PROPOSAL_NOT_TEMPLATE_CODE;
|
|
||||||
default -> PROPOSAL_DEFAULT_TEMPLATE_CODE;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将现行工作流节点映射到历史提案表模板使用的区块名称,并按主协办单位拆分办理答复。
|
* 将现行工作流节点映射到历史提案表模板使用的区块名称,并按主协办单位拆分办理答复。
|
||||||
* 模板仍使用旧节点名称时,避免已完成的意见和答复因键名不一致而无法渲染。
|
* 模板仍使用旧节点名称时,避免已完成的意见和答复因键名不一致而无法渲染。
|
||||||
|
|||||||
+2
-2
@@ -101,7 +101,7 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
|
|||||||
if (identity.equals("主任")) {
|
if (identity.equals("主任")) {
|
||||||
sysRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_DIRECTOR);
|
sysRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_DIRECTOR);
|
||||||
|
|
||||||
} else if (identity.equals("成员")) {
|
} else if (identity.equals("委员")) {
|
||||||
sysRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_MEMBER);
|
sysRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_MEMBER);
|
||||||
}
|
}
|
||||||
if (sysRole == null) {
|
if (sysRole == null) {
|
||||||
@@ -148,7 +148,7 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
|
|||||||
if (institutionUser.getIdentity().equals("主任")) {
|
if (institutionUser.getIdentity().equals("主任")) {
|
||||||
sysRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_DIRECTOR);
|
sysRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_DIRECTOR);
|
||||||
|
|
||||||
} else if (institutionUser.getIdentity().equals("成员")) {
|
} else if (institutionUser.getIdentity().equals("委员")) {
|
||||||
sysRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_MEMBER);
|
sysRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_MEMBER);
|
||||||
}
|
}
|
||||||
if (sysRole == null) {
|
if (sysRole == null) {
|
||||||
|
|||||||
+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.Condolence;
|
||||||
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
||||||
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
|
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
|
||||||
import com.deepoove.poi.XWPFTemplate;
|
|
||||||
import com.deepoove.poi.data.PictureRenderData;
|
import com.deepoove.poi.data.PictureRenderData;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
@@ -47,8 +47,6 @@ import org.nutz.mvc.annotation.Ok;
|
|||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -59,6 +57,7 @@ import java.util.List;
|
|||||||
* @Version 1.0
|
* @Version 1.0
|
||||||
* @Description TODO
|
* @Description TODO
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@IocBean
|
@IocBean
|
||||||
@At("/platform/condolence/mine")
|
@At("/platform/condolence/mine")
|
||||||
@Api("职工慰问我的")
|
@Api("职工慰问我的")
|
||||||
@@ -157,6 +156,7 @@ public class CondolenceMineController {
|
|||||||
@ApiOperation("导出职工慰问申请表")
|
@ApiOperation("导出职工慰问申请表")
|
||||||
@SaCheckPermission("condolence.mine")
|
@SaCheckPermission("condolence.mine")
|
||||||
public void doExportApply(String id, HttpServletResponse response, Boolean print) throws Exception {
|
public void doExportApply(String id, HttpServletResponse response, Boolean print) throws Exception {
|
||||||
|
log.info("[职工慰问导出] 开始,applyId={}, userId={}, format={}", id, SecurityUtil.getUserId(), Boolean.TRUE.equals(print) ? "PDF" : "Word");
|
||||||
Condolence condolence = condolenceService.fetch(id);
|
Condolence condolence = condolenceService.fetch(id);
|
||||||
if (condolence == null) throw new BaseException("该条记录不存在");
|
if (condolence == null) throw new BaseException("该条记录不存在");
|
||||||
if (!SecurityUtil.getUserId().equals(condolence.getCreatedBy())) {
|
if (!SecurityUtil.getUserId().equals(condolence.getCreatedBy())) {
|
||||||
@@ -167,36 +167,32 @@ public class CondolenceMineController {
|
|||||||
if (instance == null || !ProcessInstanceStateEnum.FINISHED.getCode().equals(instance.getState())) {
|
if (instance == null || !ProcessInstanceStateEnum.FINISHED.getCode().equals(instance.getState())) {
|
||||||
throw new BaseException("只能导出已完成的申请");
|
throw new BaseException("只能导出已完成的申请");
|
||||||
}
|
}
|
||||||
|
log.info("[职工慰问导出] 流程已完成,applyId={}, instanceId={}, state={}, applicantSignPresent={}", id, instance.getId(), instance.getState(), StrUtil.isNotBlank(condolence.getSignature()));
|
||||||
NutMap docData = Lang.obj2nutmap(condolence);
|
NutMap docData = Lang.obj2nutmap(condolence);
|
||||||
docData.put("payUserName", StrUtil.nullToEmpty(condolence.getBankUserName()));
|
docData.put("payUserName", StrUtil.nullToEmpty(condolence.getBankUserName()));
|
||||||
CondolenceType type = condolenceService.dao().fetch(CondolenceType.class, condolence.getType());
|
CondolenceType type = condolenceService.dao().fetch(CondolenceType.class, condolence.getType());
|
||||||
docData.put("typeName", type == null ? "" : type.getName());
|
docData.put("typeName", type == null ? "" : type.getName());
|
||||||
docData.put("signature", "");
|
docData.put("signature", null);
|
||||||
if (StrUtil.isNotBlank(condolence.getSignature())) {
|
if (StrUtil.isNotBlank(condolence.getSignature())) {
|
||||||
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, condolence.getSignature());
|
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, condolence.getSignature());
|
||||||
if (signature == null) throw new BaseException("申请人签字图片读取失败");
|
if (signature == null) throw new BaseException("申请人签字图片读取失败");
|
||||||
docData.put("signature", signature);
|
docData.put("signature", signature);
|
||||||
}
|
}
|
||||||
fillApplyApprovals(docData, instance);
|
fillApplyApprovals(docData, instance);
|
||||||
|
for (String field : List.of("signature", "firstApprovalSign", "secondApprovalSign", "thirdApprovalSign", "fourthApprovalSign")) {
|
||||||
|
Object value = docData.get(field);
|
||||||
|
log.info("[职工慰问导出] 渲染前图片字段,applyId={}, instanceId={}, field={}, valueType={}", id, instance.getId(), field, value == null ? "null" : value.getClass().getName());
|
||||||
|
}
|
||||||
String fileName = "重庆工贸职业技术学院职工慰问申请表_" + StrUtil.nullToEmpty(condolence.getHelpUserName());
|
String fileName = "重庆工贸职业技术学院职工慰问申请表_" + StrUtil.nullToEmpty(condolence.getHelpUserName());
|
||||||
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc");
|
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc")) {
|
||||||
ByteArrayOutputStream templateOutput = new ByteArrayOutputStream();
|
|
||||||
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
|
||||||
if (input == null) throw new BaseException("职工慰问申请表模板不存在");
|
if (input == null) throw new BaseException("职工慰问申请表模板不存在");
|
||||||
com.aspose.words.Document document = new com.aspose.words.Document(input);
|
boolean pdf = Boolean.TRUE.equals(print);
|
||||||
document.save(templateOutput, com.aspose.words.SaveFormat.DOCX);
|
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, null, pdf, "condolence:" + id);
|
||||||
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateOutput.toByteArray())) {
|
log.info("[职工慰问导出] 渲染完成,applyId={}, format={}, outputBytes={}", id, pdf ? "PDF" : "Word", bytes.length);
|
||||||
XWPFTemplate.compile(templateInput).render(docData).writeAndClose(output);
|
CommonDownloadUtil.download(fileName + (pdf ? ".pdf" : ".docx"), bytes, response);
|
||||||
}
|
} catch (Exception e) {
|
||||||
if (Boolean.TRUE.equals(print)) {
|
log.error("[职工慰问导出] 模板渲染或文件输出失败,applyId={}, instanceId={}, format={}", id, instance.getId(), Boolean.TRUE.equals(print) ? "PDF" : "Word", e);
|
||||||
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(output.toByteArray());
|
throw e;
|
||||||
ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream()) {
|
|
||||||
new com.aspose.words.Document(wordInput).save(pdfOutput, com.aspose.words.SaveFormat.PDF);
|
|
||||||
CommonDownloadUtil.download(fileName + ".pdf", pdfOutput.toByteArray(), response);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CommonDownloadUtil.download(fileName + ".docx", output.toByteArray(), response);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,29 +202,39 @@ public class CondolenceMineController {
|
|||||||
String[] fields = {"firstApproval", "secondApproval", "thirdApproval", "fourthApproval"};
|
String[] fields = {"firstApproval", "secondApproval", "thirdApproval", "fourthApproval"};
|
||||||
List<String> suffixes = List.of("Opinion", "Sign", "Year", "Month", "Day");
|
List<String> suffixes = List.of("Opinion", "Sign", "Year", "Month", "Day");
|
||||||
for (String field : fields) {
|
for (String field : fields) {
|
||||||
for (String suffix : suffixes) docData.put(field + suffix, "");
|
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
|
||||||
}
|
}
|
||||||
List<ProcessTask> tasks = condolenceService.dao().query(ProcessTask.class,
|
List<ProcessTask> tasks = condolenceService.dao().query(ProcessTask.class,
|
||||||
Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
|
Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
|
||||||
|
log.info("[职工慰问导出] 查询审批记录,applyId={}, instanceId={}, taskCount={}, expectedTaskKeys={}", instance.getBusinessNo(), instance.getId(), tasks.size(), List.of(taskKeys));
|
||||||
for (ProcessTask task : tasks) {
|
for (ProcessTask task : tasks) {
|
||||||
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
|
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
|
||||||
if ("startTask".equals(task.getTaskName()) || (taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
|
log.info("[职工慰问导出] 审批记录,instanceId={}, taskId={}, taskKey={}, displayName={}, state={}, finishTime={}, isFirstTaskNode={}, matchedApproval={}", instance.getId(), task.getId(), task.getTaskName(), task.getDisplayName(), task.getTaskState(), task.getFinishTime(), taskVO.getExt() == null ? null : taskVO.getExt().get("isFirstTaskNode"), List.of(taskKeys).contains(task.getTaskName()));
|
||||||
|
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
|
||||||
|
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
|
||||||
|
log.info("[职工慰问导出] 申请节点重置本轮审批字段,instanceId={}, taskId={}, taskKey={}", instance.getId(), task.getId(), task.getTaskName());
|
||||||
// 重新申请后,仅导出本轮审批记录。
|
// 重新申请后,仅导出本轮审批记录。
|
||||||
for (String field : fields) {
|
for (String field : fields) {
|
||||||
for (String suffix : suffixes) docData.put(field + suffix, "");
|
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < taskKeys.length; i++) {
|
for (int i = 0; i < taskKeys.length; i++) {
|
||||||
if (!taskKeys[i].equals(task.getTaskName())) continue;
|
if (!taskKeys[i].equals(task.getTaskName())) continue;
|
||||||
for (String suffix : suffixes) docData.put(fields[i] + suffix, "");
|
for (String suffix : suffixes) docData.put(fields[i] + suffix, "Sign".equals(suffix) ? null : "");
|
||||||
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) continue;
|
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) {
|
||||||
|
log.info("[职工慰问导出] 跳过未完成审批,instanceId={}, taskId={}, field={}, state={}", instance.getId(), task.getId(), fields[i], task.getTaskState());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Dict data = taskVO.getExt() == null ? Dict.create() : taskVO.getTaskFormData();
|
Dict data = taskVO.getExt() == null ? Dict.create() : taskVO.getTaskFormData();
|
||||||
|
log.info("[职工慰问导出] 读取审批表单,instanceId={}, taskId={}, field={}, formKeys={}, signPresent={}, signReferenceLength={}", instance.getId(), task.getId(), fields[i], data.keySet(), StrUtil.isNotBlank(data.getStr("sign")), StrUtil.nullToEmpty(data.getStr("sign")).length());
|
||||||
|
if (StrUtil.isBlank(data.getStr("sign"))) log.warn("[职工慰问导出] 已完成审批缺少签名字段,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i]);
|
||||||
docData.put(fields[i] + "Opinion", StrUtil.nullToEmpty(data.getStr("opinion")));
|
docData.put(fields[i] + "Opinion", StrUtil.nullToEmpty(data.getStr("opinion")));
|
||||||
if (StrUtil.isNotBlank(data.getStr("sign"))) {
|
if (StrUtil.isNotBlank(data.getStr("sign"))) {
|
||||||
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign"));
|
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign"));
|
||||||
if (signature == null) throw new BaseException("审批签字图片读取失败");
|
if (signature == null) throw new BaseException("审批签字图片读取失败");
|
||||||
docData.put(fields[i] + "Sign", signature);
|
docData.put(fields[i] + "Sign", signature);
|
||||||
|
log.info("[职工慰问导出] 审批签名填充完成,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i] + "Sign");
|
||||||
}
|
}
|
||||||
docData.put(fields[i] + "Year", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy"));
|
docData.put(fields[i] + "Year", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy"));
|
||||||
docData.put(fields[i] + "Month", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "M"));
|
docData.put(fields[i] + "Month", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "M"));
|
||||||
|
|||||||
+16
-39
@@ -9,7 +9,6 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
|
|||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.io.IoUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.alibaba.excel.EasyExcel;
|
import com.alibaba.excel.EasyExcel;
|
||||||
@@ -18,7 +17,6 @@ import com.budwk.app.base.page.Pagination;
|
|||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
import com.budwk.app.base.utils.MoneyUtil;
|
import com.budwk.app.base.utils.MoneyUtil;
|
||||||
import com.budwk.app.base.utils.OfficePlusUtil;
|
|
||||||
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
|
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
|
||||||
import com.budwk.app.flow.engine.FlowEngine;
|
import com.budwk.app.flow.engine.FlowEngine;
|
||||||
import com.budwk.app.flow.entity.ProcessInstance;
|
import com.budwk.app.flow.entity.ProcessInstance;
|
||||||
@@ -39,7 +37,6 @@ import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommon
|
|||||||
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
|
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
|
||||||
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo;
|
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo;
|
||||||
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
|
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
|
||||||
import com.deepoove.poi.XWPFTemplate;
|
|
||||||
import com.deepoove.poi.config.Configure;
|
import com.deepoove.poi.config.Configure;
|
||||||
import com.deepoove.poi.data.PictureRenderData;
|
import com.deepoove.poi.data.PictureRenderData;
|
||||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||||
@@ -74,8 +71,6 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.StandardOpenOption;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -319,9 +314,9 @@ public class DifficultHelpReadingController {
|
|||||||
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
|
||||||
Configure config = Configure.builder().bind("familyList", policy).build();
|
Configure config = Configure.builder().bind("familyList", policy).build();
|
||||||
|
|
||||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("difficulty_apply"), config)
|
try (InputStream input = sysOfficeTemplateUtil.getTemplate("difficulty_apply")) {
|
||||||
.render(docData)
|
zipOutputStream.write(sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:batch:" + record.getString("id")));
|
||||||
.write(zipOutputStream);
|
}
|
||||||
zipOutputStream.closeEntry();
|
zipOutputStream.closeEntry();
|
||||||
}
|
}
|
||||||
zipOutputStream.flush();
|
zipOutputStream.flush();
|
||||||
@@ -379,17 +374,18 @@ public class DifficultHelpReadingController {
|
|||||||
String[] taskKeys = {"36ebafda-44c2-4bd3-bd7f-10839a575a0d", "715ece82-4ae3-433c-ad18-ed875f45d1e3", "e405f96c-5fc0-4f63-b323-e69dfe853253"};
|
String[] taskKeys = {"36ebafda-44c2-4bd3-bd7f-10839a575a0d", "715ece82-4ae3-433c-ad18-ed875f45d1e3", "e405f96c-5fc0-4f63-b323-e69dfe853253"};
|
||||||
String[] fields = {"firstApproval", "secondApproval", "thirdApproval"};
|
String[] fields = {"firstApproval", "secondApproval", "thirdApproval"};
|
||||||
for (String field : fields) {
|
for (String field : fields) {
|
||||||
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
|
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
|
||||||
}
|
}
|
||||||
ProcessInstance instance = dao.fetch(ProcessInstance.class, Cnd.where("businessNo", "=", businessId).desc("id"));
|
ProcessInstance instance = dao.fetch(ProcessInstance.class, Cnd.where("businessNo", "=", businessId).desc("id"));
|
||||||
if (instance == null) return;
|
if (instance == null) return;
|
||||||
List<ProcessTask> tasks = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
|
List<ProcessTask> tasks = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
|
||||||
for (ProcessTask task : tasks) {
|
for (ProcessTask task : tasks) {
|
||||||
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
|
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
|
||||||
if ("startTask".equals(task.getTaskName()) || (taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
|
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
|
||||||
|
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
|
||||||
// 重新申请后,仅导出本轮审批,避免混入上一次退回或撤销前的意见。
|
// 重新申请后,仅导出本轮审批,避免混入上一次退回或撤销前的意见。
|
||||||
for (String field : fields) {
|
for (String field : fields) {
|
||||||
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
|
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -453,22 +449,9 @@ public class DifficultHelpReadingController {
|
|||||||
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
|
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
|
||||||
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf";
|
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf";
|
||||||
|
|
||||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
try (InputStream input = getApplyTemplate(templateName)) {
|
||||||
XWPFTemplate.compile(getApplyTemplate(templateName),config)
|
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, true, "difficulthelp:" + difficultHelp.getId());
|
||||||
.render(docData)
|
CommonDownloadUtil.download(fileName, bytes, response);
|
||||||
.writeAndClose(byteArrayOutputStream);
|
|
||||||
byte[] bytes = byteArrayOutputStream.toByteArray();
|
|
||||||
File sourceFile = File.createTempFile("file_convert_origin", ".docx");
|
|
||||||
Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE);
|
|
||||||
|
|
||||||
File pdfFile = File.createTempFile("file_convert", ".pdf");
|
|
||||||
OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath());
|
|
||||||
CommonDownloadUtil.download(fileName, IoUtil.readBytes(FileUtil.getInputStream(pdfFile)), response);
|
|
||||||
// 删除临时文件
|
|
||||||
FileUtil.del(sourceFile);
|
|
||||||
FileUtil.del(pdfFile.toPath());
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("困难慰问申请表导出PDF失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,13 +461,9 @@ public class DifficultHelpReadingController {
|
|||||||
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
|
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
|
||||||
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx";
|
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx";
|
||||||
|
|
||||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
try (InputStream input = getApplyTemplate(templateName)) {
|
||||||
XWPFTemplate.compile(getApplyTemplate(templateName),config)
|
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:" + difficultHelp.getId());
|
||||||
.render(docData)
|
CommonDownloadUtil.download(fileName, bytes, response);
|
||||||
.writeAndClose(byteArrayOutputStream);
|
|
||||||
CommonDownloadUtil.download(fileName, byteArrayOutputStream.toByteArray(), response);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("困难慰问申请表导出Word失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,15 +541,13 @@ public class DifficultHelpReadingController {
|
|||||||
throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage());
|
throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
} else if ("docx".equals(flag)) {
|
} else if ("docx".equals(flag)) {
|
||||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
try (InputStream input = sysOfficeTemplateUtil.getTemplate(resource)) {
|
||||||
Configure config = Configure.builder()
|
Configure config = Configure.builder()
|
||||||
.bind("da", new LoopRowTableRenderPolicy())
|
.bind("da", new LoopRowTableRenderPolicy())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(resource), config)
|
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, result, config, false, "difficulthelp:summary:" + resource);
|
||||||
.render(result)
|
CommonDownloadUtil.download(fileName + ".docx", bytes, response);
|
||||||
.writeAndClose(out);
|
|
||||||
CommonDownloadUtil.download(fileName + ".docx", out.toByteArray(), response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -1,6 +1,7 @@
|
|||||||
package com.budwk.app.zhgh.staffbenefit.difficulthelp.service;
|
package com.budwk.app.zhgh.staffbenefit.difficulthelp.service;
|
||||||
|
|
||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
|
import com.budwk.app.flow.entity.ProcessTask;
|
||||||
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
|
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
|
||||||
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
|
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
|
||||||
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
|
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
|
||||||
@@ -21,6 +22,14 @@ public interface DifficultHelpCommonService extends BaseService<DifficultHelpInf
|
|||||||
|
|
||||||
void validateApplyUser(DifficultHelpInfo difficultHelpInfo);
|
void validateApplyUser(DifficultHelpInfo difficultHelpInfo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 困难帮扶进入校工会办公室主任审批时,通知本次任务的实际参与人。
|
||||||
|
* @param task WF 已创建且参与人已落库的任务;仅 KNBF 办公室审批待办触发,
|
||||||
|
* 接收人使用任务参与人的 actorAccount(系统 loginname)
|
||||||
|
* @return 无返回值;开关关闭或非目标节点不发送,发送失败记录日志而不阻断流程
|
||||||
|
*/
|
||||||
|
void notifyOfficeApproval(ProcessTask task);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入实际金额数据处理
|
* 导入实际金额数据处理
|
||||||
* @param file 文件
|
* @param file 文件
|
||||||
|
|||||||
+59
@@ -6,6 +6,12 @@ import cn.hutool.json.JSONUtil;
|
|||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
import com.budwk.app.base.exception.BaseException;
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.base.wxwork.WxWorkMessageService;
|
||||||
|
import com.budwk.app.flow.entity.ProcessDefine;
|
||||||
|
import com.budwk.app.flow.entity.ProcessInstance;
|
||||||
|
import com.budwk.app.flow.entity.ProcessTask;
|
||||||
|
import com.budwk.app.flow.entity.ProcessTaskActor;
|
||||||
|
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.base.utils.ManyAddOrRenewUtil;
|
import com.budwk.app.base.utils.ManyAddOrRenewUtil;
|
||||||
@@ -31,6 +37,8 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
|||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
import org.nutz.lang.random.R;
|
import org.nutz.lang.random.R;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import org.nutz.log.Log;
|
||||||
|
import org.nutz.log.Logs;
|
||||||
import org.nutz.mvc.upload.TempFile;
|
import org.nutz.mvc.upload.TempFile;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -46,6 +54,8 @@ import java.util.stream.IntStream;
|
|||||||
*/
|
*/
|
||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHelpInfo> implements DifficultHelpCommonService {
|
public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHelpInfo> implements DifficultHelpCommonService {
|
||||||
|
private static final Log log = Logs.get();
|
||||||
|
private static final String OFFICE_APPROVAL_TASK = "36ebafda-44c2-4bd3-bd7f-10839a575a0d";
|
||||||
|
|
||||||
public DifficultHelpCommonServiceImpl(Dao dao) {
|
public DifficultHelpCommonServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
@@ -53,6 +63,55 @@ public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHel
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private ManyAddOrRenewUtil manyAddOrRenewUtil;
|
private ManyAddOrRenewUtil manyAddOrRenewUtil;
|
||||||
|
@Inject
|
||||||
|
private WxWorkMessageService wxWorkMessageService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只通知 KNBF 办公室待办的实际审核人;重新提交生成的新任务沿用同一入口。
|
||||||
|
* task 为任务开始事件对应的 WF 任务,actorAccount 为学校企业微信 userid。
|
||||||
|
* 无返回值;服务返回 skipped 表示开关关闭,success=false 表示接口失败或部分接收人无效。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void notifyOfficeApproval(ProcessTask task) {
|
||||||
|
if (task == null || task.getId() == null || task.getProcessInstanceId() == null
|
||||||
|
|| !OFFICE_APPROVAL_TASK.equals(task.getTaskName())
|
||||||
|
|| !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ProcessInstance instance = dao().fetch(ProcessInstance.class, task.getProcessInstanceId());
|
||||||
|
if (instance == null || instance.getProcessDefineId() == null) return;
|
||||||
|
ProcessDefine define = dao().fetch(ProcessDefine.class, instance.getProcessDefineId());
|
||||||
|
// 使用实例所属定义核验流程 KEY,避免其他流程复用同一个节点 ID 时误发。
|
||||||
|
if (define == null || !"KNBF".equals(define.getName())) return;
|
||||||
|
DifficultHelpInfo info = StrUtil.isBlank(instance.getBusinessNo()) ? null : fetch(instance.getBusinessNo());
|
||||||
|
if (info == null) {
|
||||||
|
log.warnf("困难帮扶企业微信通知跳过:申请不存在,taskId=%s", task.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<ProcessTaskActor> actors = dao().query(ProcessTaskActor.class,
|
||||||
|
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
|
||||||
|
List<String> loginNames = actors.stream().map(ProcessTaskActor::getActorAccount)
|
||||||
|
.filter(StrUtil::isNotBlank).map(String::trim).distinct().toList();
|
||||||
|
// 不按角色扩大接收范围,未绑定工号时不以内部用户 ID 代替企业微信 userid。
|
||||||
|
if (loginNames.isEmpty()) {
|
||||||
|
log.warnf("困难帮扶企业微信通知跳过:审核人 loginname 为空,taskId=%s", task.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String content = "您有一条困难帮扶申请待审核,受助人:"
|
||||||
|
+ StrUtil.blankToDefault(info.getUserName(), "未填写") + ",请进入校工会办公室审批页面办理。";
|
||||||
|
// 流程定义中仍保留旧的 unionLeaderApproval 链接,本节点实际办理入口为 branchUnionApproval。
|
||||||
|
NutMap result = wxWorkMessageService.massSend(loginNames, "困难帮扶待办提醒", content,
|
||||||
|
"/platform/difficultHelp/branchUnionApproval/h5");
|
||||||
|
if (!result.getBoolean("skipped", false) && !result.getBoolean("success", false)) {
|
||||||
|
log.warnf("困难帮扶企业微信通知发送失败或部分失败,taskId=%s,result=%s", task.getId(), result);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 外部消息不改变审批结果;日志保留任务 ID,便于定位接口、账号或配置问题。
|
||||||
|
log.error("困难帮扶企业微信通知异常,taskId=" + task.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validateApplyUser(DifficultHelpInfo difficultHelpInfo) {
|
public void validateApplyUser(DifficultHelpInfo difficultHelpInfo) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ nutz:
|
|||||||
ignore: ^(.+[.])(jsp|png|gif|jpg|js|css|jspx|jpeg|html|mp3|mp4|ico|svg|vue)$
|
ignore: ^(.+[.])(jsp|png|gif|jpg|js|css|jspx|jpeg|html|mp3|mp4|ico|svg|vue)$
|
||||||
exclusions: /favicon/*,/assets/*,/druid/*,/upload/*,/components/*
|
exclusions: /favicon/*,/assets/*,/druid/*,/upload/*,/components/*
|
||||||
server:
|
server:
|
||||||
port: 8081
|
port: 80
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
|
|
||||||
jetty:
|
jetty:
|
||||||
@@ -21,7 +21,7 @@ jetty:
|
|||||||
#结合ftp使用,或用nginx代理ftp路径
|
#结合ftp使用,或用nginx代理ftp路径
|
||||||
#staticPath: /Users/wizzer/temp/files
|
#staticPath: /Users/wizzer/temp/files
|
||||||
#开发模式静态资源
|
#开发模式静态资源
|
||||||
staticPathLocal: D:/myProject/zhgh_cqgmy/src/main/resources/static
|
staticPathLocal: E:/projects/zhgh_cqgmy/src/main/resources/static
|
||||||
|
|
||||||
security:
|
security:
|
||||||
tokenName: saToken
|
tokenName: saToken
|
||||||
@@ -87,7 +87,7 @@ xsssql:
|
|||||||
beetl:
|
beetl:
|
||||||
RESOURCE:
|
RESOURCE:
|
||||||
#本地路径,开发时设置本地路径,便于调试
|
#本地路径,开发时设置本地路径,便于调试
|
||||||
rootLocal: D:/myProject/zhgh_cqgmy/src/main/resources/views/
|
rootLocal: E:/projects/zhgh_cqgmy/src/main/resources/views/
|
||||||
root: views/
|
root: views/
|
||||||
DELIMITER_STATEMENT_START: "<!--#"
|
DELIMITER_STATEMENT_START: "<!--#"
|
||||||
DELIMITER_STATEMENT_END: "#-->"
|
DELIMITER_STATEMENT_END: "#-->"
|
||||||
|
|||||||
@@ -1,17 +1,85 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<configuration scan="false" scanPeriod="60000" debug="false">
|
<configuration scan="false" scanPeriod="60000" debug="false">
|
||||||
|
|
||||||
<!-- 定义日志文件的存储路径 -->
|
<!-- 日志存储路径 -->
|
||||||
<property name="LOG_HOME" value="./logs"/>
|
<property name="LOG_HOME" value="./logs"/>
|
||||||
|
|
||||||
<!-- 控制台输出配置 -->
|
<!-- 控制台输出 -->
|
||||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<!-- INFO级别日志文件配置 -->
|
|
||||||
|
<!--
|
||||||
|
#################################################################################
|
||||||
|
# #
|
||||||
|
# 全文输出日志(一个文件存储)start #
|
||||||
|
# #
|
||||||
|
#################################################################################
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- 所有级别日志写入同一个文件 -->
|
||||||
|
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_HOME}/app.log</file>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_HOME}/app-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<maxHistory>300</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- 框架日志级别(按需调整) -->
|
||||||
|
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||||
|
<logger name="org.quartz" level="INFO"/>
|
||||||
|
<logger name="org.nutz" level="DEBUG"/>
|
||||||
|
|
||||||
|
<!-- root logger:DEBUG 级别,输出到控制台和全量文件 -->
|
||||||
|
<root level="DEBUG">
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
<appender-ref ref="ALL_FILE"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
#################################################################################
|
||||||
|
# #
|
||||||
|
# 全文输出日志(一个文件存储)end #
|
||||||
|
# #
|
||||||
|
#################################################################################
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
#################################################################################
|
||||||
|
# #
|
||||||
|
# debug、info、error分文件输出 start #
|
||||||
|
# #
|
||||||
|
#################################################################################
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- DEBUG级别日志文件配置 -->
|
||||||
|
<!--<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_HOME}/debug.log</file>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<level>DEBUG</level>
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_HOME}/debug-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<maxHistory>300</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!– INFO级别日志文件配置 –>
|
||||||
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<file>${LOG_HOME}/info.log</file>
|
<file>${LOG_HOME}/info.log</file>
|
||||||
<encoder>
|
<encoder>
|
||||||
@@ -24,11 +92,11 @@
|
|||||||
</filter>
|
</filter>
|
||||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
<fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
|
<fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
<maxHistory>30</maxHistory>
|
<maxHistory>300</maxHistory>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<!-- ERROR级别日志文件配置 -->
|
<!– ERROR级别日志文件配置 –>
|
||||||
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<file>${LOG_HOME}/error.log</file>
|
<file>${LOG_HOME}/error.log</file>
|
||||||
<encoder>
|
<encoder>
|
||||||
@@ -41,19 +109,31 @@
|
|||||||
</filter>
|
</filter>
|
||||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
<fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
|
<fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
<maxHistory>30</maxHistory>
|
<maxHistory>300</maxHistory>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="java" additivity="false" />
|
<!– 框架日志级别 –>
|
||||||
|
<!– <logger name="java" additivity="false" />–>
|
||||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||||
<logger name="org.quartz" level="INFO"/>
|
<logger name="org.quartz" level="INFO"/>
|
||||||
<logger name="org.nutz" level="DEBUG"/>
|
<logger name="org.nutz" level="DEBUG"/>
|
||||||
|
|
||||||
<!-- 日志级别和appender的关联 -->
|
<!– 日志级别和appender的关联 –>
|
||||||
<root level="DEBUG">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
|
<appender-ref ref="DEBUG_FILE"/>
|
||||||
<appender-ref ref="INFO_FILE"/>
|
<appender-ref ref="INFO_FILE"/>
|
||||||
<appender-ref ref="ERROR_FILE"/>
|
<appender-ref ref="ERROR_FILE"/>
|
||||||
</root>
|
</root>-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
#################################################################################
|
||||||
|
# #
|
||||||
|
# debug、info、error分文件输出 end #
|
||||||
|
# #
|
||||||
|
#################################################################################
|
||||||
|
-->
|
||||||
|
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
7pKr23u2tZHcvXud
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
:finished="tableFinished"
|
:finished="tableFinished"
|
||||||
finished-text="没有更多了"
|
finished-text="没有更多了"
|
||||||
@load="onLoad">
|
@load="onLoad">
|
||||||
<div class="table-list-container">
|
<div class="table-list-container" @click="showOverflowText">
|
||||||
<div v-for="(row, index) in tableData" :key="index" class="table-list-item"
|
<div v-for="(row, index) in tableData" :key="index" class="table-list-item"
|
||||||
:data-row-id="row && row.id ? row.id : ''">
|
:data-row-id="row && row.id ? row.id : ''">
|
||||||
<slot name="header" :index="index" :row="row">
|
<slot name="header" :index="index" :row="row">
|
||||||
@@ -36,6 +36,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</van-list>
|
</van-list>
|
||||||
|
<van-popover ref="overflowPopover" :value="overflowVisible" :style="overflowAnchorStyle"
|
||||||
|
:placement="overflowPlacement" get-container="body" @input="onOverflowInput"
|
||||||
|
@opened="positionOverflow">
|
||||||
|
<div class="table-list-overflow-content">{{ overflowText }}</div>
|
||||||
|
<template #reference><span></span></template>
|
||||||
|
</van-popover>
|
||||||
</van-pull-refresh>
|
</van-pull-refresh>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@@ -90,10 +96,138 @@ module.exports = {
|
|||||||
tableLoading: false,
|
tableLoading: false,
|
||||||
tableFinished: false,
|
tableFinished: false,
|
||||||
tableRefreshing: false,
|
tableRefreshing: false,
|
||||||
localPageForm: {...this.page_form}
|
localPageForm: {...this.page_form},
|
||||||
|
overflowVisible: false,
|
||||||
|
overflowText: "",
|
||||||
|
overflowTarget: null,
|
||||||
|
overflowPlacement: "top",
|
||||||
|
overflowAnchorStyle: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 只响应真正被截断的纯文字,保留卡片内按钮和业务插槽原有的点击行为。
|
||||||
|
showOverflowText(event) {
|
||||||
|
const target = event.target
|
||||||
|
if (!target || !target.closest) return
|
||||||
|
const card = target.closest(".table-list-item")
|
||||||
|
if (!card || target.closest(".item-actions, a, button, input, textarea, select, [role=button], [tabindex], [onclick]")) return
|
||||||
|
if (target.children.length || !target.textContent.trim() || this.hasOverflowClickHandler(target, card)) return
|
||||||
|
const style = window.getComputedStyle(target)
|
||||||
|
if (style.textOverflow !== "ellipsis" || (target.scrollWidth <= target.clientWidth && target.scrollHeight <= target.clientHeight)) return
|
||||||
|
|
||||||
|
const rect = target.getBoundingClientRect()
|
||||||
|
this.overflowTarget = target
|
||||||
|
this.overflowText = target.textContent.trim()
|
||||||
|
this.overflowPlacement = rect.top < 180 ? "bottom" : "top"
|
||||||
|
this.overflowAnchorStyle = {
|
||||||
|
position: "fixed",
|
||||||
|
display: "block",
|
||||||
|
pointerEvents: "none",
|
||||||
|
left: rect.left + "px",
|
||||||
|
top: rect.top + "px",
|
||||||
|
width: rect.width + "px",
|
||||||
|
height: rect.height + "px"
|
||||||
|
}
|
||||||
|
if (!this.overflowVisible) this.bindOverflowListeners()
|
||||||
|
this.overflowVisible = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const instance = this.$refs.overflowPopover
|
||||||
|
if (instance) instance.updateLocation()
|
||||||
|
setTimeout(() => this.positionOverflow(), 50)
|
||||||
|
setTimeout(() => this.positionOverflow(), 350)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查文字及其卡片内的祖先节点,避免干扰已有 Vue 点击事件。
|
||||||
|
hasOverflowClickHandler(target, card) {
|
||||||
|
const ancestors = []
|
||||||
|
let element = target
|
||||||
|
while (element && element !== card.parentElement) {
|
||||||
|
ancestors.push(element)
|
||||||
|
element = element.parentElement
|
||||||
|
}
|
||||||
|
const visit = (node) => {
|
||||||
|
if (!node) return false
|
||||||
|
const events = node.data ? [node.data.on, node.data.nativeOn] : []
|
||||||
|
if (ancestors.includes(node.elm) && events.some((handlers) => handlers &&
|
||||||
|
Object.keys(handlers).some((key) => /^[~!&]*(click|touchstart|pointerdown|mousedown)$/.test(key)))) return true
|
||||||
|
if (node.componentInstance && visit(node.componentInstance._vnode)) return true
|
||||||
|
return Array.isArray(node.children) && node.children.some((child) => visit(child))
|
||||||
|
}
|
||||||
|
return visit(this._vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Vant 的弹层挂在 body;校正窗口边缘的位置,避免完整文字被屏幕裁掉。
|
||||||
|
positionOverflow() {
|
||||||
|
const instance = this.$refs.overflowPopover
|
||||||
|
const popup = instance && instance.$refs.popover && instance.$refs.popover.$el
|
||||||
|
if (!this.overflowVisible || !this.overflowTarget || !popup) return
|
||||||
|
const rect = popup.getBoundingClientRect()
|
||||||
|
const anchor = this.overflowTarget.getBoundingClientRect()
|
||||||
|
if (!rect.width || !rect.height) return
|
||||||
|
const margin = 8
|
||||||
|
const left = Math.max(margin, Math.min(rect.left, window.innerWidth - rect.width - margin))
|
||||||
|
let top = rect.top
|
||||||
|
if (top < margin) top = anchor.bottom + 8
|
||||||
|
if (top + rect.height > window.innerHeight - margin) top = anchor.top - rect.height - 8
|
||||||
|
top = Math.max(margin, Math.min(top, window.innerHeight - rect.height - margin))
|
||||||
|
popup.style.left = (parseFloat(popup.style.left) || 0) + left - rect.left + "px"
|
||||||
|
popup.style.top = (parseFloat(popup.style.top) || 0) + top - rect.top + "px"
|
||||||
|
const arrow = popup.querySelector(".van-popover__arrow")
|
||||||
|
if (arrow) {
|
||||||
|
arrow.style.left = Math.max(12, Math.min(anchor.left + anchor.width / 2 - left, rect.width - 12)) + "px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onOverflowInput(visible) {
|
||||||
|
this.overflowVisible = visible
|
||||||
|
if (!visible) {
|
||||||
|
this.overflowTarget = null
|
||||||
|
this.unbindOverflowListeners()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 监听只在气泡打开时存在,PJAX 替换页面前也会关闭气泡并注销。
|
||||||
|
bindOverflowListeners() {
|
||||||
|
document.addEventListener("click", this.closeOverflowOnClick)
|
||||||
|
window.addEventListener("scroll", this.closeOverflowOnMove, true)
|
||||||
|
window.addEventListener("resize", this.closeOverflowOnMove)
|
||||||
|
window.addEventListener("transitionend", this.onOverflowAnimationEnd)
|
||||||
|
window.addEventListener("animationend", this.onOverflowAnimationEnd)
|
||||||
|
$(document).on("pjax:send", this.closeOverflowForNavigation)
|
||||||
|
},
|
||||||
|
|
||||||
|
unbindOverflowListeners() {
|
||||||
|
document.removeEventListener("click", this.closeOverflowOnClick)
|
||||||
|
window.removeEventListener("scroll", this.closeOverflowOnMove, true)
|
||||||
|
window.removeEventListener("resize", this.closeOverflowOnMove)
|
||||||
|
window.removeEventListener("transitionend", this.onOverflowAnimationEnd)
|
||||||
|
window.removeEventListener("animationend", this.onOverflowAnimationEnd)
|
||||||
|
$(document).off("pjax:send", this.closeOverflowForNavigation)
|
||||||
|
},
|
||||||
|
|
||||||
|
closeOverflowForNavigation() {
|
||||||
|
this.onOverflowInput(false)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Vant 会在动画结束时重新计算定位,随后再次限制气泡的屏幕边界。
|
||||||
|
onOverflowAnimationEnd() {
|
||||||
|
if (this.overflowVisible) setTimeout(() => this.positionOverflow(), 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 滚动或改变屏幕尺寸时关闭气泡,避免气泡脱离原文字位置。
|
||||||
|
closeOverflowOnMove(event) {
|
||||||
|
if (event && event.target && event.target.closest && event.target.closest(".table-list-overflow-content")) return
|
||||||
|
this.onOverflowInput(false)
|
||||||
|
},
|
||||||
|
|
||||||
|
closeOverflowOnClick(event) {
|
||||||
|
if (!this.overflowVisible) return
|
||||||
|
if (this.overflowTarget && this.overflowTarget.contains(event.target)) return
|
||||||
|
if (event.target.closest && event.target.closest(".table-list-overflow-content")) return
|
||||||
|
this.onOverflowInput(false)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前列表快照。
|
* 获取当前列表快照。
|
||||||
* 返回值包含已加载记录、分页参数和加载完成状态,供业务页面在进入详情前暂存列表现场。
|
* 返回值包含已加载记录、分页参数和加载完成状态,供业务页面在进入详情前暂存列表现场。
|
||||||
@@ -186,10 +320,28 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$emit("ready")
|
this.$emit("ready")
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.unbindOverflowListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Vant 将气泡挂到 body,控制宽高并允许长文本在气泡内滚动。 */
|
||||||
|
.table-list-overflow-content {
|
||||||
|
max-width: min(240px, calc(100vw - 48px));
|
||||||
|
max-height: 160px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: #253247;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.table-list-container {
|
.table-list-container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -35,22 +35,58 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
//前端唯一标识 当然后端还有用户id作为唯一标识
|
// 二维码携带本次组件实例的标识,与服务端当前登录用户共同确定签字记录。
|
||||||
id: new Date().getTime() + Math.floor(Math.random() * 10000) + 1,
|
id: new Date().getTime() + Math.floor(Math.random() * 10000) + 1,
|
||||||
signatureAddress: null,
|
signatureAddress: null,
|
||||||
interval: null,
|
interval: null,
|
||||||
|
connectionWaitResolve: null,
|
||||||
|
unsubscribeSignature: null,
|
||||||
signatureContent: null,
|
signatureContent: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async init() {
|
/**
|
||||||
const {code, data} = await this.$axios.post("/platform/signature/get")
|
* 无请求参数,返回 Promise;响应 code 为0表示成功,data.signature 为已有签字图片地址。
|
||||||
if (code === 0) {
|
* 请求结束后由 mounted 的 finally 统一关闭 loading。
|
||||||
this.signatureContent = data && data.signature
|
*/
|
||||||
this.$emit("input", this.signatureContent)
|
init() {
|
||||||
}
|
|
||||||
this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id
|
this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id
|
||||||
|
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() {
|
signatureAgain() {
|
||||||
const val = sessionStorage.getItem("h5-scan-code-signature-" + this.id)
|
const val = sessionStorage.getItem("h5-scan-code-signature-" + this.id)
|
||||||
@@ -58,25 +94,42 @@ module.exports = {
|
|||||||
this.$emit("input", val)
|
this.$emit("input", val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
mounted() {
|
||||||
await this.init()
|
this.loading = true
|
||||||
webSocketPubSub.subscribe("h5-scan-code-signature", (data) => {
|
// 先登记消息回调,连接成功后由公共连接加入房间,避免请求期间遗漏订阅。
|
||||||
console.info("ws:收到签字成功消息:", data)
|
this.unsubscribeSignature = webSocketPubSub.subscribe("h5-scan-code-signature", (data) => {
|
||||||
this.signatureContent = data.value
|
this.signatureContent = data.value
|
||||||
this.$emit("input", data.value)
|
this.$emit("input", data.value)
|
||||||
})
|
})
|
||||||
|
this.init()
|
||||||
// 可选:轮询或 watch ready 状态
|
.then(() => {
|
||||||
const checkReady = () => {
|
if (!this._isBeingDestroyed && !this._isDestroyed) {
|
||||||
if (webSocketPubSub.ws && webSocketPubSub.ws.readyState === WebSocket.OPEN) {
|
return this.waitForConnection()
|
||||||
console.info("ws.readyState is open");
|
}
|
||||||
this.loading = false;
|
})
|
||||||
console.log(this.signatureAddress)
|
.catch(() => {
|
||||||
} else {
|
if (!this._isBeingDestroyed && !this._isDestroyed) {
|
||||||
setTimeout(checkReady, 100);
|
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
|
||||||
}
|
}
|
||||||
};
|
|
||||||
checkReady();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
progress
|
progress
|
||||||
:accept="accept"
|
:accept="accept"
|
||||||
:max-count="upload_number"
|
:max-count="upload_number"
|
||||||
|
:upload-text="upload_text"
|
||||||
title=""
|
title=""
|
||||||
description=""
|
description=""
|
||||||
tip=""
|
tip=""
|
||||||
@@ -50,6 +51,11 @@ module.exports = {
|
|||||||
default: 1,
|
default: 1,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
upload_text: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
required: false
|
||||||
|
},
|
||||||
// 上传返回id或url
|
// 上传返回id或url
|
||||||
upload_result_type: {
|
upload_result_type: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -101,6 +107,7 @@ module.exports = {
|
|||||||
this.fileList = val.map((v) => {
|
this.fileList = val.map((v) => {
|
||||||
return {
|
return {
|
||||||
...v,
|
...v,
|
||||||
|
file: v.file || { name: v.name },
|
||||||
status: null,
|
status: null,
|
||||||
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase())
|
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase())
|
||||||
}
|
}
|
||||||
@@ -118,6 +125,7 @@ module.exports = {
|
|||||||
this.fileList = val.map((v) => {
|
this.fileList = val.map((v) => {
|
||||||
return {
|
return {
|
||||||
...v,
|
...v,
|
||||||
|
file: v.file || { name: v.name },
|
||||||
url: v.url ? v.url : v.response?.data,
|
url: v.url ? v.url : v.response?.data,
|
||||||
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()),
|
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()),
|
||||||
status: null
|
status: null
|
||||||
@@ -171,8 +179,8 @@ module.exports = {
|
|||||||
f.url = resp.data
|
f.url = resp.data
|
||||||
f.response = resp
|
f.response = resp
|
||||||
f.percentage = 100
|
f.percentage = 100
|
||||||
f.isImage = true
|
f.isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(f.name.split(".").pop().toLowerCase())
|
||||||
delete f.file
|
f.file = { name: f.name }
|
||||||
delete f.content
|
delete f.content
|
||||||
} else {
|
} else {
|
||||||
f.status = "fail"
|
f.status = "fail"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -443,50 +443,76 @@
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.ws = null
|
this.ws = null
|
||||||
this.subscribers = new Map()
|
this.subscribers = new Map()
|
||||||
this.isSubscribed = false // 添加订阅状态标记
|
this.isSubscribed = false
|
||||||
|
this.connectTimer = null
|
||||||
|
this.reconnectTimer = null
|
||||||
|
this.pingTimer = null
|
||||||
|
this.isDisposed = false
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页面只创建一个心跳任务;每次连接自行绑定事件及握手超时。
|
||||||
init() {
|
init() {
|
||||||
try {
|
|
||||||
this.connect()
|
|
||||||
this.setupEventListeners()
|
|
||||||
this.setupPing()
|
|
||||||
this.setupWindowEvents()
|
this.setupWindowEvents()
|
||||||
} catch (ex) {
|
this.setupPing()
|
||||||
console.info(ex)
|
this.connect()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 正在连接或关闭时等待对应事件,避免同时创建多个连接。
|
||||||
connect() {
|
connect() {
|
||||||
|
if (this.isDisposed || this.reconnectTimer !== null ||
|
||||||
|
(this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
const WS_URL = window.location.host + "${base}/websocket"
|
const WS_URL = window.location.host + "${base}/websocket"
|
||||||
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
|
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
|
||||||
this.ws = new WebSocket(protocol + WS_URL)
|
this.ws = new WebSocket(protocol + WS_URL)
|
||||||
|
this.isSubscribed = false
|
||||||
|
this.setupEventListeners()
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//订阅
|
/**
|
||||||
|
* type 为消息 action 名称,callback 接收服务端消息对象;返回取消本次订阅的方法。
|
||||||
|
* 连接尚未建立时先保留回调,在 onopen 中统一加入当前用户的消息房间。
|
||||||
|
*/
|
||||||
subscribe(type, callback) {
|
subscribe(type, callback) {
|
||||||
if (!this.subscribers.has(type)) {
|
if (!this.subscribers.has(type)) {
|
||||||
this.subscribers.set(type, new Set())
|
this.subscribers.set(type, new Set())
|
||||||
}
|
}
|
||||||
this.subscribers.get(type).add(callback)
|
const callbacks = this.subscribers.get(type)
|
||||||
|
callbacks.add(callback)
|
||||||
setTimeout(() => {
|
|
||||||
// 所有订阅完成后,发送 join 消息
|
|
||||||
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
|
|
||||||
this.isSubscribed = true
|
|
||||||
this.sendJoinMessage()
|
this.sendJoinMessage()
|
||||||
|
return () => {
|
||||||
|
callbacks.delete(callback)
|
||||||
|
if (callbacks.size === 0 && this.subscribers.get(type) === callbacks) {
|
||||||
|
this.subscribers.delete(type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 2000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
// 事件绑定到本次连接,旧连接的延迟事件不得影响新连接。
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// 消息处理
|
const socket = this.ws
|
||||||
this.ws.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
|
if (this.isDisposed || this.ws !== socket) {
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
// 触发订阅者的回调
|
|
||||||
if (this.subscribers.has(data.action)) {
|
if (this.subscribers.has(data.action)) {
|
||||||
this.subscribers.get(data.action).forEach((callback) => {
|
this.subscribers.get(data.action).forEach((callback) => {
|
||||||
try {
|
try {
|
||||||
@@ -500,82 +526,112 @@
|
|||||||
console.error("消息解析错误:", error)
|
console.error("消息解析错误:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
socket.onopen = () => {
|
||||||
// 连接建立
|
if (this.isDisposed || this.ws !== socket) {
|
||||||
this.ws.onopen = () => {
|
return
|
||||||
// this.ws.send(
|
}
|
||||||
// JSON.stringify({
|
clearTimeout(this.connectTimer)
|
||||||
// room: store.state.room,
|
this.connectTimer = null
|
||||||
// action: "join"
|
this.sendJoinMessage()
|
||||||
// })
|
|
||||||
// )
|
|
||||||
// 连接建立时不立即发送 join,等待订阅完成
|
|
||||||
this.ping()
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增发送 join 消息的方法
|
// 每个已建立的连接只加入一次房间,首次连接及断线恢复都补发已有订阅。
|
||||||
sendJoinMessage() {
|
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({
|
this.send({
|
||||||
room: store.state.room,
|
room: store.state.room,
|
||||||
action: "join"
|
action: "join"
|
||||||
})
|
})
|
||||||
|
this.isSubscribed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 心跳检测
|
// 沿用原心跳间隔,仅对已建立的连接发送心跳。
|
||||||
setupPing() {
|
setupPing() {
|
||||||
setInterval(() => this.ping(), (11624317 / 1000) * 2)
|
this.pingTimer = setInterval(() => this.ping(), (11624317 / 1000) * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CONNECTING/CLOSING 状态分别由握手超时和关闭事件处理。
|
||||||
ping() {
|
ping() {
|
||||||
|
if (this.isDisposed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
try {
|
try {
|
||||||
this.ws.send("{}")
|
this.ws.send("{}")
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
this.reconnect()
|
console.warn("WebSocket 心跳发送失败,等待关闭后重连", error)
|
||||||
|
this.ws.close()
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
||||||
this.reconnect()
|
this.reconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭事件和心跳共用一个重连任务,避免重复定时器创建并行连接。
|
||||||
reconnect() {
|
reconnect() {
|
||||||
if (this.ws) {
|
if (this.isDisposed || this.reconnectTimer !== null ||
|
||||||
this.ws.close() // 确保旧连接已关闭
|
(this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectTimer = null
|
||||||
this.connect()
|
this.connect()
|
||||||
this.setupEventListeners()
|
}, 3000)
|
||||||
// 如果之前已订阅,重新发送join消息
|
|
||||||
if (this.isSubscribed) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.ws.readyState === WebSocket.OPEN) {
|
|
||||||
this.sendJoinMessage()
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
}, 3000) // 延迟3秒重连
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 窗口事件处理
|
// 真正离开页面时释放连接和定时器;进入往返缓存时保留页面状态。
|
||||||
setupWindowEvents() {
|
setupWindowEvents() {
|
||||||
let _beforeUnload_time = 0
|
window.addEventListener("pagehide", (event) => {
|
||||||
window.onbeforeunload = () => {
|
if (event.persisted) {
|
||||||
_beforeUnload_time = new Date().getTime()
|
return
|
||||||
}
|
}
|
||||||
window.onunload = () => {
|
this.isDisposed = true
|
||||||
const _gap_time = new Date().getTime() - _beforeUnload_time
|
clearTimeout(this.connectTimer)
|
||||||
if (_gap_time <= 5) {
|
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({
|
this.send({
|
||||||
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
|
room: store.state.room,
|
||||||
action: "left"
|
action: "left"
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("WebSocket 离开房间消息发送失败", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
|
||||||
|
this.ws.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// message 为消息对象或 JSON 字符串;连接未建立时不发送,返回值为 undefined。
|
||||||
send(message) {
|
send(message) {
|
||||||
if (this.ws) {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
const messageStr = typeof message === "string" ? message : JSON.stringify(message)
|
const messageStr = typeof message === "string" ? message : JSON.stringify(message)
|
||||||
this.ws.send(messageStr)
|
this.ws.send(messageStr)
|
||||||
}
|
}
|
||||||
@@ -650,7 +706,7 @@
|
|||||||
Vue.component("year-van-dropdown-item", httpVueLoader("/components/plugins/vantMore/yearVanDropDownItem.vue?v=" + h5ComponentVersion))
|
Vue.component("year-van-dropdown-item", httpVueLoader("/components/plugins/vantMore/yearVanDropDownItem.vue?v=" + h5ComponentVersion))
|
||||||
Vue.component("van-text-dialog", httpVueLoader("/components/plugins/vantMore/vantTextDialog.vue?v=" + h5ComponentVersion))
|
Vue.component("van-text-dialog", httpVueLoader("/components/plugins/vantMore/vantTextDialog.vue?v=" + h5ComponentVersion))
|
||||||
Vue.component("enum-tag", httpVueLoader("/components/plugins/sysEnum/EnumTag.vue?v=" + h5ComponentVersion))
|
Vue.component("enum-tag", httpVueLoader("/components/plugins/sysEnum/EnumTag.vue?v=" + h5ComponentVersion))
|
||||||
Vue.component("table-list", httpVueLoader("/components/plugins/h5/TableList.vue?v=" + h5ComponentVersion))
|
Vue.component("table-list", httpVueLoader("/components/plugins/h5/TableList.vue?v=" + h5ComponentVersion + "_overflow1"))
|
||||||
Vue.component("table-column", httpVueLoader("/components/plugins/h5/TableColumn.vue?v=" + h5ComponentVersion))
|
Vue.component("table-column", httpVueLoader("/components/plugins/h5/TableColumn.vue?v=" + h5ComponentVersion))
|
||||||
Vue.component("file-preview", httpVueLoader("/components/plugins/sysFilePreview/H5Index.vue?v=" + h5ComponentVersion))
|
Vue.component("file-preview", httpVueLoader("/components/plugins/sysFilePreview/H5Index.vue?v=" + h5ComponentVersion))
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -68,9 +68,10 @@
|
|||||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.css" />
|
<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" />
|
<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>
|
<!-- 使用随应用打包的本地资源,按工具库、UI、表格的依赖顺序加载,避免外链失效影响页面。 -->
|
||||||
<script src="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/index.umd.min.js"></script>
|
<script src="${base!}/assets/platform/plugins/vxe/xe-utils.js"></script>
|
||||||
<script src="https://vxeui.com/umd/vxe-table@3.9.0/lib/index.umd.min.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 -->
|
<!-- 引入 form-create 和 designer -->
|
||||||
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
|
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
|
||||||
@@ -153,51 +154,76 @@
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.ws = null
|
this.ws = null
|
||||||
this.subscribers = new Map()
|
this.subscribers = new Map()
|
||||||
this.isSubscribed = false // 添加订阅状态标记
|
this.isSubscribed = false
|
||||||
|
this.connectTimer = null
|
||||||
|
this.reconnectTimer = null
|
||||||
|
this.pingTimer = null
|
||||||
|
this.isDisposed = false
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页面只创建一个心跳任务;每次连接自行绑定事件及握手超时。
|
||||||
init() {
|
init() {
|
||||||
try {
|
|
||||||
this.connect()
|
|
||||||
this.setupEventListeners()
|
|
||||||
this.setupPing()
|
|
||||||
this.setupWindowEvents()
|
this.setupWindowEvents()
|
||||||
} catch (ex) {
|
this.setupPing()
|
||||||
console.info(ex)
|
this.connect()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 正在连接或关闭时等待对应事件,避免同时创建多个连接。
|
||||||
connect() {
|
connect() {
|
||||||
|
if (this.isDisposed || this.reconnectTimer !== null ||
|
||||||
|
(this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
const WS_URL = window.location.host + "${base}/websocket"
|
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://"
|
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
|
||||||
this.ws = new WebSocket(protocol + WS_URL)
|
this.ws = new WebSocket(protocol + WS_URL)
|
||||||
|
this.isSubscribed = false
|
||||||
|
this.setupEventListeners()
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//订阅
|
/**
|
||||||
|
* type 为消息 action 名称,callback 接收服务端消息对象;返回取消本次订阅的方法。
|
||||||
|
* 连接尚未建立时先保留回调,在 onopen 中统一加入当前用户的消息房间。
|
||||||
|
*/
|
||||||
subscribe(type, callback) {
|
subscribe(type, callback) {
|
||||||
if (!this.subscribers.has(type)) {
|
if (!this.subscribers.has(type)) {
|
||||||
this.subscribers.set(type, new Set())
|
this.subscribers.set(type, new Set())
|
||||||
}
|
}
|
||||||
this.subscribers.get(type).add(callback)
|
const callbacks = this.subscribers.get(type)
|
||||||
|
callbacks.add(callback)
|
||||||
setTimeout(() => {
|
|
||||||
// 所有订阅完成后,发送 join 消息
|
|
||||||
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
|
|
||||||
this.isSubscribed = true
|
|
||||||
this.sendJoinMessage()
|
this.sendJoinMessage()
|
||||||
|
return () => {
|
||||||
|
callbacks.delete(callback)
|
||||||
|
if (callbacks.size === 0 && this.subscribers.get(type) === callbacks) {
|
||||||
|
this.subscribers.delete(type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
// 事件绑定到本次连接,旧连接的延迟事件不得影响新连接。
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// 消息处理
|
const socket = this.ws
|
||||||
this.ws.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
|
if (this.isDisposed || this.ws !== socket) {
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
// 触发订阅者的回调
|
|
||||||
if (this.subscribers.has(data.action)) {
|
if (this.subscribers.has(data.action)) {
|
||||||
this.subscribers.get(data.action).forEach((callback) => {
|
this.subscribers.get(data.action).forEach((callback) => {
|
||||||
try {
|
try {
|
||||||
@@ -211,83 +237,112 @@
|
|||||||
console.error("消息解析错误:", error)
|
console.error("消息解析错误:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
socket.onopen = () => {
|
||||||
// 连接建立
|
if (this.isDisposed || this.ws !== socket) {
|
||||||
this.ws.onopen = () => {
|
return
|
||||||
// this.ws.send(
|
}
|
||||||
// JSON.stringify({
|
clearTimeout(this.connectTimer)
|
||||||
// room: store.state.room,
|
this.connectTimer = null
|
||||||
// action: "join"
|
this.sendJoinMessage()
|
||||||
// })
|
|
||||||
// )
|
|
||||||
// 连接建立时不立即发送 join,等待订阅完成
|
|
||||||
this.ping()
|
this.ping()
|
||||||
console.info('websocket open success')
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增发送 join 消息的方法
|
// 每个已建立的连接只加入一次房间,首次连接及断线恢复都补发已有订阅。
|
||||||
sendJoinMessage() {
|
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({
|
this.send({
|
||||||
room: store.state.room,
|
room: store.state.room,
|
||||||
action: "join"
|
action: "join"
|
||||||
})
|
})
|
||||||
|
this.isSubscribed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 心跳检测
|
// 沿用原心跳间隔,仅对已建立的连接发送心跳。
|
||||||
setupPing() {
|
setupPing() {
|
||||||
setInterval(() => this.ping(), (11624317 / 1000) * 2)
|
this.pingTimer = setInterval(() => this.ping(), (11624317 / 1000) * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CONNECTING/CLOSING 状态分别由握手超时和关闭事件处理。
|
||||||
ping() {
|
ping() {
|
||||||
|
if (this.isDisposed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
try {
|
try {
|
||||||
this.ws.send("{}")
|
this.ws.send("{}")
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
this.reconnect()
|
console.warn("WebSocket 心跳发送失败,等待关闭后重连", error)
|
||||||
|
this.ws.close()
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
||||||
this.reconnect()
|
this.reconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭事件和心跳共用一个重连任务,避免重复定时器创建并行连接。
|
||||||
reconnect() {
|
reconnect() {
|
||||||
if (this.ws) {
|
if (this.isDisposed || this.reconnectTimer !== null ||
|
||||||
this.ws.close() // 确保旧连接已关闭
|
(this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectTimer = null
|
||||||
this.connect()
|
this.connect()
|
||||||
this.setupEventListeners()
|
}, 1000)
|
||||||
// 如果之前已订阅,重新发送join消息
|
|
||||||
if (this.isSubscribed) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.ws.readyState === WebSocket.OPEN) {
|
|
||||||
this.sendJoinMessage()
|
|
||||||
}
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
}, 1000) // 延迟2秒重连
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 窗口事件处理
|
// 真正离开页面时释放连接和定时器;进入往返缓存时保留页面状态。
|
||||||
setupWindowEvents() {
|
setupWindowEvents() {
|
||||||
let _beforeUnload_time = 0
|
window.addEventListener("pagehide", (event) => {
|
||||||
window.onbeforeunload = () => {
|
if (event.persisted) {
|
||||||
_beforeUnload_time = new Date().getTime()
|
return
|
||||||
}
|
}
|
||||||
window.onunload = () => {
|
this.isDisposed = true
|
||||||
const _gap_time = new Date().getTime() - _beforeUnload_time
|
clearTimeout(this.connectTimer)
|
||||||
if (_gap_time <= 5) {
|
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({
|
this.send({
|
||||||
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
|
room: store.state.room,
|
||||||
action: "left"
|
action: "left"
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("WebSocket 离开房间消息发送失败", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
|
||||||
|
this.ws.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// message 为消息对象或 JSON 字符串;连接未建立时不发送,返回值为 undefined。
|
||||||
send(message) {
|
send(message) {
|
||||||
if (this.ws) {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
const messageStr = typeof message === "string" ? message : JSON.stringify(message)
|
const messageStr = typeof message === "string" ? message : JSON.stringify(message)
|
||||||
this.ws.send(messageStr)
|
this.ws.send(messageStr)
|
||||||
}
|
}
|
||||||
@@ -481,7 +536,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 310px;
|
/* 用户区按完整姓名、账号及操作按钮的实际宽度布局,避免退出按钮溢出后被顶栏裁切。 */
|
||||||
|
width: max-content;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,6 +673,26 @@
|
|||||||
height: 100%;
|
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) {
|
@media (max-width: 1024px) {
|
||||||
.v4-nav {
|
.v4-nav {
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ layout("/layouts/platform.html"){
|
|||||||
size="mini" type="danger">删除
|
size="mini" type="danger">删除
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button @click="doExportDeclare(row)" size="mini" type="primary">申请表导出</el-button>
|
<el-button v-if="row.instanceState === 20" @click="doExportDeclare(row)" size="mini" type="primary">申请表导出</el-button>
|
||||||
<el-button v-if="row.instanceState === 20" @click="doExportReimbursement(row)" size="mini"
|
<el-button v-if="row.instanceState === 20" @click="doExportReimbursement(row)" size="mini"
|
||||||
type="primary">报销凭证导出
|
type="primary">报销凭证导出
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|||||||
@@ -21,14 +21,29 @@ const PROPOSAL_INFO = {
|
|||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="提案类别">
|
<el-descriptions-item label="提案类别">
|
||||||
{{viewData.typeName}}
|
{{viewData.typeName}}
|
||||||
</el-descriptions-item><!--
|
</el-descriptions-item>
|
||||||
|
<!-- 提案方式和联系电话来自提案详情,未填写时显示占位符。 -->
|
||||||
|
<el-descriptions-item label="提案方式">
|
||||||
|
<dict-tag v-if="viewData.source" :options="dict.type.PROPOSAL_SOURCE"
|
||||||
|
:value="viewData.source"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系电话">{{ viewData.mobile || '--' }}</el-descriptions-item><!--
|
||||||
<el-descriptions-item label="建议承办单位">
|
<el-descriptions-item label="建议承办单位">
|
||||||
{{viewData.suggestUnits}}
|
{{viewData.suggestUnits}}
|
||||||
</el-descriptions-item>-->
|
</el-descriptions-item>-->
|
||||||
<el-descriptions-item label="立案结果" :span="2">
|
<!-- 立案结果和类型尚未填写时仍显示字段,并以双横线表示空值。 -->
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
<el-descriptions-item label="立案结果">
|
||||||
|
<dict-tag v-if="viewData.caseFilingResult" :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
||||||
:value="viewData.caseFilingResult"></dict-tag>
|
:value="viewData.caseFilingResult"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="立案类型">
|
||||||
|
<dict-tag v-if="viewData.caseFilingType" :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
||||||
|
:value="viewData.caseFilingType"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item></el-descriptions-item>
|
||||||
<!--<el-descriptions-item label="调研情况" :span="3">
|
<!--<el-descriptions-item label="调研情况" :span="3">
|
||||||
<div class="text-left">{{viewData.researchFindings}}</div>
|
<div class="text-left">{{viewData.researchFindings}}</div>
|
||||||
</el-descriptions-item>-->
|
</el-descriptions-item>-->
|
||||||
@@ -82,18 +97,22 @@ const PROPOSAL_INFO = {
|
|||||||
{{row.commissionerName}}({{row.commissionerLoginName}})
|
{{row.commissionerName}}({{row.commissionerLoginName}})
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="立案结果" prop="caseFilingType">
|
<!-- 委员意见分别展示立案结果和类型,未填写时显示双横线。 -->
|
||||||
|
<el-table-column label="立案结果" prop="caseFilingResult">
|
||||||
<template slot-scope="{row}">
|
<template slot-scope="{row}">
|
||||||
<div style="display: flex;justify-content: center">
|
<div style="display: flex;justify-content: center">
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
<dict-tag v-if="row.caseFilingResult" :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
||||||
:value="row.caseFilingResult"></dict-tag>
|
:value="row.caseFilingResult"></dict-tag>
|
||||||
<!-- 普通提案/重点提案暂不展示,历史数据仍保留。 -->
|
<span v-else>--</span>
|
||||||
<template v-if="false">
|
</div>
|
||||||
(
|
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
|
||||||
:value="row.caseFilingType"></dict-tag>
|
|
||||||
)
|
|
||||||
</template>
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="立案类型" prop="caseFilingType">
|
||||||
|
<template slot-scope="{row}">
|
||||||
|
<div style="display: flex;justify-content: center">
|
||||||
|
<dict-tag v-if="row.caseFilingType" :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
||||||
|
:value="row.caseFilingType"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -162,7 +181,8 @@ const PROPOSAL_INFO = {
|
|||||||
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName
|
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName
|
||||||
}}({{task.taskFormData.loginName}})
|
}}({{task.taskFormData.loginName}})
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="办理时间" :span="2">{{ task.finishTime }}</el-descriptions-item>
|
<el-descriptions-item label="办理时间" :span="2">{{ task.finishTime }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="附议人" :span="3" v-if="task.taskName === 'invite'">
|
<el-descriptions-item label="附议人" :span="3" v-if="task.taskName === 'invite'">
|
||||||
<el-table v-loading="tableLoading" :data="task?.taskFormData?.seconder">
|
<el-table v-loading="tableLoading" :data="task?.taskFormData?.seconder">
|
||||||
<el-table-column label="姓名" prop="userName"></el-table-column>
|
<el-table-column label="姓名" prop="userName"></el-table-column>
|
||||||
@@ -183,22 +203,27 @@ const PROPOSAL_INFO = {
|
|||||||
}}({{task.taskFormData.loginName}})
|
}}({{task.taskFormData.loginName}})
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
||||||
|
<!-- 立案节点分别展示流程记录中的结果与类型,空值显示双横线。 -->
|
||||||
<el-descriptions-item label="立案结果">
|
<el-descriptions-item label="立案结果">
|
||||||
<div style="display: flex;justify-content: center">
|
<div style="display: flex;justify-content: center">
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
<dict-tag v-if="task.ext.tf_caseFilingResult"
|
||||||
|
:options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
||||||
:value="task.ext.tf_caseFilingResult"></dict-tag>
|
:value="task.ext.tf_caseFilingResult"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
<!-- 普通提案/重点提案暂不展示,历史流程变量仍保留。 -->
|
</div>
|
||||||
<template v-if="false">
|
</el-descriptions-item>
|
||||||
(
|
<el-descriptions-item label="立案类型">
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
<div style="display: flex;justify-content: center">
|
||||||
|
<dict-tag v-if="task.ext.tf_caseFilingType"
|
||||||
|
:options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
||||||
:value="task.ext.tf_caseFilingType"></dict-tag>
|
:value="task.ext.tf_caseFilingType"></dict-tag>
|
||||||
)
|
<span v-else>--</span>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<template v-if="['CONFIRM_FILING','SUGGESTION'].includes(task.ext.tf_caseFilingResult)">
|
<template v-if="['CONFIRM_FILING','SUGGESTION'].includes(task.ext.tf_caseFilingResult)">
|
||||||
<el-descriptions-item v-if="task.ext.tf_caseFilingResult === 'SUGGESTION'" label="承办单位" :span="3">{{task?.ext?.tf_masterUnitNameStr || task?.ext?.tf_slaveUnitNameStr}}
|
<el-descriptions-item v-if="task.ext.tf_caseFilingResult === 'SUGGESTION'"
|
||||||
|
label="承办单位" :span="3">{{task?.ext?.tf_masterUnitNameStr ||
|
||||||
|
task?.ext?.tf_slaveUnitNameStr}}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-descriptions-item label="主办单位" :span="3">{{task?.ext?.tf_masterUnitNameStr}}
|
<el-descriptions-item label="主办单位" :span="3">{{task?.ext?.tf_masterUnitNameStr}}
|
||||||
@@ -214,7 +239,8 @@ const PROPOSAL_INFO = {
|
|||||||
|
|
||||||
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
|
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
|
||||||
<el-descriptions-item label="承办单位"
|
<el-descriptions-item label="承办单位"
|
||||||
v-if="['opinion_unit_reply','unit_reply','two_unit_reply','opinion_master_reply','master_reply','slave_reply'].includes(task.taskName)" :span="3">
|
v-if="['opinion_unit_reply','unit_reply','two_unit_reply','opinion_master_reply','master_reply','slave_reply'].includes(task.taskName)"
|
||||||
|
:span="3">
|
||||||
{{task.ext.underTakeName}}
|
{{task.ext.underTakeName}}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
@@ -223,7 +249,8 @@ const PROPOSAL_INFO = {
|
|||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="办理评价" v-if="['feedback'].includes(task.taskName)" :span="3">
|
<el-descriptions-item label="办理评价" v-if="['feedback'].includes(task.taskName)"
|
||||||
|
:span="3">
|
||||||
<dict-tag :options="dict.type.PROPOSAL_FEEDBACK"
|
<dict-tag :options="dict.type.PROPOSAL_FEEDBACK"
|
||||||
:value="task.ext.tf_feedback"></dict-tag>
|
:value="task.ext.tf_feedback"></dict-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -243,7 +270,7 @@ const PROPOSAL_INFO = {
|
|||||||
<snaker-chart ref="snakerChartRef"></snaker-chart>
|
<snaker-chart ref="snakerChartRef"></snaker-chart>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
dicts: ["PROPOSAL_TYPE", "PROPOSAL_CASE_FILING_RESULT", "PROPOSAL_CASE_FILING_TYPE", "PROCESS_TASK_SUBMIT_TYPE", "PROPOSAL_FEEDBACK"],
|
dicts: ["PROPOSAL_TYPE", "PROPOSAL_SOURCE", "PROPOSAL_CASE_FILING_RESULT", "PROPOSAL_CASE_FILING_TYPE", "PROCESS_TASK_SUBMIT_TYPE", "PROPOSAL_FEEDBACK"],
|
||||||
props: {
|
props: {
|
||||||
hide_slave: false
|
hide_slave: false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ const USER_COMPONENT = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
filterUnit(query, item) {
|
filterUnit(query, item) {
|
||||||
return item.username.indexOf(query) > -1
|
// 按姓名或工号筛选人员,字段为空时按空字符串处理。
|
||||||
|
const username = item.username || ""
|
||||||
|
const loginname = item.loginname || ""
|
||||||
|
return username.indexOf(query) > -1 || loginname.indexOf(query) > -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ layout("/layouts/platform.html"){
|
|||||||
</el-card>
|
</el-card>
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<table-tool :columns.sync="tableColumns">
|
<table-tool :columns.sync="tableColumns">
|
||||||
<el-button class="ml5" icon="el-icon-upload2" type="primary" size="small"
|
<!-- <el-button class="ml5" icon="el-icon-upload2" type="primary" size="small"
|
||||||
@click="openImport">导入提案
|
@click="openImport">导入提案
|
||||||
</el-button>
|
</el-button>-->
|
||||||
</table-tool>
|
</table-tool>
|
||||||
<el-table v-loading="tableLoading" :data="tableData" @sort-change="pageOrder" header-align="center" style="width: 100%" row-key="id">
|
<el-table v-loading="tableLoading" :data="tableData" @sort-change="pageOrder" header-align="center" style="width: 100%" row-key="id">
|
||||||
<el-table-column label="序号" width="50" type="index" :index="indexMethod" fixed="left"></el-table-column>
|
<el-table-column label="序号" width="50" type="index" :index="indexMethod" fixed="left"></el-table-column>
|
||||||
@@ -76,8 +76,7 @@ layout("/layouts/platform.html"){
|
|||||||
<el-button v-if="row.canRevoke" size="mini" type="danger" @click="openRevoke(row)">
|
<el-button v-if="row.canRevoke" size="mini" type="danger" @click="openRevoke(row)">
|
||||||
撤回
|
撤回
|
||||||
</el-button>
|
</el-button>
|
||||||
<!-- v-if="row.taskKey === 'startTask' || !row.instanceId"-->
|
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" size="mini" type="danger" @click="del(row.id)">
|
||||||
<el-button size="mini" type="danger" @click="del(row.id)">
|
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ layout("/layouts/platform.html"){
|
|||||||
<custom-card>
|
<custom-card>
|
||||||
<snaker-start slot="header" label="撰写提案" define_key="JDHTA_NC">
|
<snaker-start slot="header" label="撰写提案" define_key="JDHTA_NC">
|
||||||
<template slot="header-right-label">
|
<template slot="header-right-label">
|
||||||
<el-link type="primary" @click="openImport" v-if="!formData.id" style="margin-right: 15px">导入提案</el-link>
|
<!-- <el-link type="primary" @click="openImport" v-if="!formData.id" style="margin-right: 15px">导入提案</el-link>-->
|
||||||
</template>
|
</template>
|
||||||
</snaker-start>
|
</snaker-start>
|
||||||
<el-form :model="formData" :rules="formRules" label-width="120px" ref="addForm">
|
<el-form :model="formData" :rules="formRules" label-width="120px" ref="addForm">
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ layout("/layouts/platform.html"){
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>-->
|
</el-descriptions-item>-->
|
||||||
<el-descriptions-item label="证明人">
|
<!-- <el-descriptions-item label="证明人">
|
||||||
<el-form-item prop="certifierUserId" label="证明人">
|
<el-form-item prop="certifierUserId" label="证明人">
|
||||||
<el-select
|
<el-select
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@@ -75,7 +75,7 @@ layout("/layouts/platform.html"){
|
|||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item> -->
|
||||||
<el-descriptions-item label="慰问类型">
|
<el-descriptions-item label="慰问类型">
|
||||||
<el-form-item prop="type" label="慰问类型">
|
<el-form-item prop="type" label="慰问类型">
|
||||||
<el-select v-model="formData.type" @change="typeChange" style="width: 100%"
|
<el-select v-model="formData.type" @change="typeChange" style="width: 100%"
|
||||||
@@ -112,14 +112,14 @@ layout("/layouts/platform.html"){
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="收款账户">
|
<el-descriptions-item label="收款账户" v-if="!['A', 'D'].includes(formData.typeCode)">
|
||||||
<el-form-item prop="bankCardNumber" label="收款账户">
|
<el-form-item prop="bankCardNumber" label="收款账户">
|
||||||
<el-input maxlength="20" v-model="formData.bankCardNumber" placeholder="请填写收款账户"
|
<el-input maxlength="30" v-model="formData.bankCardNumber" placeholder="请填写银行卡号或对公账户号"
|
||||||
></el-input>
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="户名">
|
<el-descriptions-item label="户名" v-if="!['A', 'D'].includes(formData.typeCode)">
|
||||||
<el-form-item prop="bankUserName" label="户名">
|
<el-form-item prop="bankUserName" label="户名">
|
||||||
<el-input maxlength="20" v-model="formData.bankUserName"
|
<el-input maxlength="20" v-model="formData.bankUserName"
|
||||||
placeholder="请填写户名"
|
placeholder="请填写户名"
|
||||||
@@ -127,7 +127,7 @@ layout("/layouts/platform.html"){
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="开户行" >
|
<el-descriptions-item label="开户行" v-if="!['A', 'D'].includes(formData.typeCode)">
|
||||||
<el-form-item prop="bankOfDeposit" label="开户行">
|
<el-form-item prop="bankOfDeposit" label="开户行">
|
||||||
<el-input maxlength="20" v-model="formData.bankOfDeposit"
|
<el-input maxlength="20" v-model="formData.bankOfDeposit"
|
||||||
placeholder="请填写开户行"
|
placeholder="请填写开户行"
|
||||||
@@ -173,10 +173,12 @@ layout("/layouts/platform.html"){
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
</el-descriptions>
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
<el-descriptions-item label="申请事由" :span="3">
|
<el-descriptions-item label="申请事由" :span="3">
|
||||||
<el-form-item prop="remark" label="申请事由">
|
<el-form-item prop="remark" label="申请事由">
|
||||||
<el-input maxlength="500" v-model="formData.remark" placeholder="请填写申请事由"
|
<el-input maxlength="500" v-model="formData.remark" placeholder="请填写申请事由"
|
||||||
type="textarea" :autosize="{ minRows: 4, maxRows: 8}"></el-input>
|
type="textarea" :rows="3" resize="none"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
@@ -238,7 +240,10 @@ layout("/layouts/platform.html"){
|
|||||||
payUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
payUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
type: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
type: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
way: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
way: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
money: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
money: [
|
||||||
|
{ required: true, message: "请填写慰问金额", trigger: ["change", "blur"] },
|
||||||
|
{ validator: (rule, value, callback) => callback(this.validateMoney(value) ? undefined : new Error("慰问金额须大于0且不超过10000元,最多两位小数")), trigger: ["change", "blur"] }
|
||||||
|
],
|
||||||
occurTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
occurTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
child: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
child: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
hospital: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
hospital: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
@@ -247,14 +252,20 @@ layout("/layouts/platform.html"){
|
|||||||
hospitalBy: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
hospitalBy: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
deadImmediateFamily: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
deadImmediateFamily: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
remark: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
remark: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
files: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
files: [{ required: true, type: "array", min: 1, max: 5, message: "请上传1至5个附件", trigger: ["change", "blur"] }],
|
||||||
signature: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
signature: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
handlerUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
handlerUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
certifierUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
// certifierUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
bankCardNumber: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
bankCardNumber: [
|
||||||
|
{ required: true, message: "请填写银行卡号或对公账户号", trigger: ["change", "blur"] },
|
||||||
|
{ pattern: /^[0-9]{6,30}$/, message: "收款账户须为6至30位数字(银行卡号或对公账户号)", trigger: ["change", "blur"] }
|
||||||
|
],
|
||||||
bankUserName: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
bankUserName: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
bankOfDeposit: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
bankOfDeposit: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
thisYearHospitalizationNum: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
thisYearHospitalizationNum: [
|
||||||
|
{ required: true, message: "必填", trigger: ["change", "blur"] },
|
||||||
|
{ type: "integer", min: 0, max: 10000, message: "住院次数须为0至10000的整数", trigger: ["change", "blur"] }
|
||||||
|
],
|
||||||
leaveHospitalTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
leaveHospitalTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
hospitalizationTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
hospitalizationTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||||
},
|
},
|
||||||
@@ -268,6 +279,9 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
validateMoney(value) {
|
||||||
|
return /^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000
|
||||||
|
},
|
||||||
createRemoteMethod(options) {
|
createRemoteMethod(options) {
|
||||||
return (keyword) => {
|
return (keyword) => {
|
||||||
this.selectQueryUser(keyword, options)
|
this.selectQueryUser(keyword, options)
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ const basicForm = {
|
|||||||
<el-radio-button :label="false">不上传</el-radio-button>
|
<el-radio-button :label="false">不上传</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>-->
|
</el-form-item>-->
|
||||||
<!-- <el-form-item label="是否上传附件" prop="isUploadFile">-->
|
<el-form-item label="是否上传附件" prop="isUploadFile">
|
||||||
<!-- <el-radio-group v-model="formData.isUploadFile">-->
|
<el-radio-group v-model="formData.isUploadFile">
|
||||||
<!-- <el-radio-button :label="true">是</el-radio-button>-->
|
<el-radio-button :label="true">是</el-radio-button>
|
||||||
<!-- <el-radio-button :label="false">否</el-radio-button>-->
|
<el-radio-button :label="false">否</el-radio-button>
|
||||||
<!-- </el-radio-group>-->
|
</el-radio-group>
|
||||||
<!-- </el-form-item>-->
|
</el-form-item>
|
||||||
<el-form-item label="上传附件说明" prop="uploadFileDesc" v-if="formData.isUploadFile">
|
<el-form-item label="上传附件说明" prop="uploadFileDesc" v-if="formData.isUploadFile">
|
||||||
<el-input v-model="formData.uploadFileDesc" maxlength="50" clearable placeholder="请填写上传附件说明"></el-input>
|
<el-input v-model="formData.uploadFileDesc" maxlength="50" clearable placeholder="请填写上传附件说明"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|||||||
+64
-58
@@ -26,7 +26,7 @@ layout("/layouts/platform.html"){
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="受补助人">
|
<el-descriptions-item label="受补助人">
|
||||||
<el-form-item prop="userName">
|
<el-form-item prop="userId">
|
||||||
<template v-if="formData.mode == 2">
|
<template v-if="formData.mode == 2">
|
||||||
<el-select :remote-method="queryRecipients"
|
<el-select :remote-method="queryRecipients"
|
||||||
@change="userChange" clearable filterable
|
@change="userChange" clearable filterable
|
||||||
@@ -51,7 +51,7 @@ layout("/layouts/platform.html"){
|
|||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="身份证号" :span="2">
|
<el-descriptions-item label="身份证号" :span="2">
|
||||||
<el-form-item prop="idCard">
|
<el-form-item prop="idCard">
|
||||||
<el-input maxlength="20" clearable placeholder="请填写身份证号"
|
<el-input clearable placeholder="请填写身份证号"
|
||||||
v-model="formData.idCard" maxlength="18"></el-input>
|
v-model="formData.idCard" maxlength="18"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -84,7 +84,7 @@ layout("/layouts/platform.html"){
|
|||||||
<el-descriptions-item label="手机号码">
|
<el-descriptions-item label="手机号码">
|
||||||
<el-form-item prop="mobile">
|
<el-form-item prop="mobile">
|
||||||
<el-input maxlength="11" clearable placeholder="请填写手机号码"
|
<el-input maxlength="11" clearable placeholder="请填写手机号码"
|
||||||
v-model="formData.mobile" maxlength="11"></el-input>
|
v-model="formData.mobile"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="家庭年总收入(万元)">
|
<el-descriptions-item label="家庭年总收入(万元)">
|
||||||
@@ -103,6 +103,7 @@ layout("/layouts/platform.html"){
|
|||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.homeNumber"
|
v-model="formData.homeNumber"
|
||||||
:min="1"
|
:min="1"
|
||||||
|
:precision="0"
|
||||||
:step="1"
|
:step="1"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
placeholder="请填写家庭人数">
|
placeholder="请填写家庭人数">
|
||||||
@@ -113,7 +114,7 @@ layout("/layouts/platform.html"){
|
|||||||
<el-form-item prop="homeAddress">
|
<el-form-item prop="homeAddress">
|
||||||
<el-input clearable
|
<el-input clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
v-model="formData.homeAddress"
|
v-model="formData.homeAddress" maxlength="200"
|
||||||
placeholder="请填写家庭住址">
|
placeholder="请填写家庭住址">
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -132,7 +133,7 @@ layout("/layouts/platform.html"){
|
|||||||
<!-- </el-descriptions-item>-->
|
<!-- </el-descriptions-item>-->
|
||||||
<el-descriptions-item label="银行卡号" >
|
<el-descriptions-item label="银行卡号" >
|
||||||
<el-form-item prop="bankCardNum">
|
<el-form-item prop="bankCardNum">
|
||||||
<el-input maxlength="20" clearable placeholder="请填写银行卡号"
|
<el-input maxlength="19" clearable placeholder="请填写银行卡号"
|
||||||
v-model="formData.bankCardNum"></el-input>
|
v-model="formData.bankCardNum"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -152,7 +153,7 @@ layout("/layouts/platform.html"){
|
|||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="申请金额(元)">
|
<el-descriptions-item label="申请金额(元)">
|
||||||
<el-form-item prop="applyMoney" :rules="[{required:true,type:'number',min:0.01,message:'请填写大于零的申请金额',trigger:['change','blur']}]">
|
<el-form-item prop="applyMoney">
|
||||||
<el-input-number v-model="formData.applyMoney" :precision="2" :min="0.01" placeholder="请输入申请金额"></el-input-number>
|
<el-input-number v-model="formData.applyMoney" :precision="2" :min="0.01" placeholder="请输入申请金额"></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -198,8 +199,8 @@ layout("/layouts/platform.html"){
|
|||||||
<template slot-scope="{row,$index}">
|
<template slot-scope="{row,$index}">
|
||||||
<el-form-item label-width="0"
|
<el-form-item label-width="0"
|
||||||
:prop="'familyList.'+$index+'.relation'"
|
:prop="'familyList.'+$index+'.relation'"
|
||||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
:rules="textRules('与本人关系', 50)">
|
||||||
<el-input v-model="row.relation" placeholder="请填写与本人关系" clearable></el-input>
|
<el-input v-model="row.relation" maxlength="50" placeholder="请填写与本人关系" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -207,8 +208,8 @@ layout("/layouts/platform.html"){
|
|||||||
<template slot-scope="{row,$index}">
|
<template slot-scope="{row,$index}">
|
||||||
<el-form-item label-width="0"
|
<el-form-item label-width="0"
|
||||||
:prop="'familyList.'+$index+'.name'"
|
:prop="'familyList.'+$index+'.name'"
|
||||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
:rules="textRules('姓名', 100)">
|
||||||
<el-input v-model="row.name" placeholder="请填写姓名" clearable></el-input>
|
<el-input v-model="row.name" maxlength="100" placeholder="请填写姓名" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -216,9 +217,9 @@ layout("/layouts/platform.html"){
|
|||||||
<template slot-scope="{row,$index}">
|
<template slot-scope="{row,$index}">
|
||||||
<el-form-item label-width="0"
|
<el-form-item label-width="0"
|
||||||
:prop="'familyList.'+$index+'.age'"
|
:prop="'familyList.'+$index+'.age'"
|
||||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
:rules="[{required:true,type:'integer',min:1,max:100,message:'年龄须为1至100的整数',trigger:['change','blur']}]">
|
||||||
<el-input-number v-model="row.age" placeholder="年龄" :min="1"
|
<el-input-number v-model="row.age" placeholder="年龄" :min="1"
|
||||||
:max="100" :step="1" style="width: 100%" clearable></el-input-number>
|
:max="100" :precision="0" :step="1" style="width: 100%" clearable></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -226,8 +227,8 @@ layout("/layouts/platform.html"){
|
|||||||
<template slot-scope="{row,$index}">
|
<template slot-scope="{row,$index}">
|
||||||
<el-form-item label-width="0"
|
<el-form-item label-width="0"
|
||||||
:prop="'familyList.'+$index+'.homeUnit'"
|
:prop="'familyList.'+$index+'.homeUnit'"
|
||||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
:rules="textRules('工作单位', 200)">
|
||||||
<el-input v-model="row.homeUnit" placeholder="请填写工作单位" clearable></el-input>
|
<el-input v-model="row.homeUnit" maxlength="200" placeholder="请填写工作单位" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -235,9 +236,9 @@ layout("/layouts/platform.html"){
|
|||||||
<template slot-scope="{row,$index}">
|
<template slot-scope="{row,$index}">
|
||||||
<el-form-item label-width="0"
|
<el-form-item label-width="0"
|
||||||
:prop="'familyList.'+$index+'.yearIncome'"
|
:prop="'familyList.'+$index+'.yearIncome'"
|
||||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
:rules="[{validator:validateIncome,required:true,trigger:['change','blur']}]">
|
||||||
<el-input-number v-model="row.yearIncome" placeholder="年收入"
|
<el-input-number v-model="row.yearIncome" placeholder="年收入"
|
||||||
:min="0" :step="1" style="width: 100%" clearable></el-input-number>
|
:min="0" :precision="2" :step="1" style="width: 100%" clearable></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -245,8 +246,8 @@ layout("/layouts/platform.html"){
|
|||||||
<template slot-scope="{row,$index}">
|
<template slot-scope="{row,$index}">
|
||||||
<el-form-item label-width="0"
|
<el-form-item label-width="0"
|
||||||
:prop="'familyList.'+$index+'.remarks'"
|
:prop="'familyList.'+$index+'.remarks'"
|
||||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
:rules="textRules('备注', 500)">
|
||||||
<el-input v-model="row.remarks" placeholder="请填写备注" clearable></el-input>
|
<el-input v-model="row.remarks" maxlength="500" placeholder="请填写备注" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -326,23 +327,37 @@ layout("/layouts/platform.html"){
|
|||||||
applyCount: '1',
|
applyCount: '1',
|
||||||
},
|
},
|
||||||
formRules: {
|
formRules: {
|
||||||
proxyUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
|
proxyUserName: this.textRules('填写人姓名', 100),
|
||||||
proxyLoginName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
|
proxyLoginName: this.textRules('填写人工号', 100),
|
||||||
sex: [{required: true, message: '请填写性别', trigger: ['blur', 'change']}],
|
mode: [{required: true, type: 'enum', enum: ['1', '2'], message: '请选择申请模式', trigger: ['blur', 'change']}],
|
||||||
userName: [{required: true, message: '请填写受补助人', trigger: ['blur', 'change']}],
|
sex: [{required: true, type: 'enum', enum: ['男', '女'], message: '请选择性别', trigger: ['blur', 'change']}],
|
||||||
unitName: [{required: true, message: '请填写所属单位', trigger: ['blur', 'change']}],
|
userId: [{required: true, message: '请选择受补助人', trigger: ['blur', 'change']}],
|
||||||
|
unitName: this.textRules('所属单位', 500),
|
||||||
unionName: [{required: true, message: '请填写所属工会', trigger: ['blur', 'change']}],
|
unionName: [{required: true, message: '请填写所属工会', trigger: ['blur', 'change']}],
|
||||||
homeAddress: [{required: true, message: '请填写家庭住址', trigger: ['blur', 'change']}],
|
homeAddress: this.textRules('家庭住址', 200),
|
||||||
mobile: [{required: true, message: '请填写手机号码', trigger: ['blur', 'change']}],
|
mobile: [
|
||||||
idCard: [{required: true, message: '请填写身份证号', trigger: ['blur', 'change']}],
|
{required: true, message: '请填写手机号码', trigger: ['blur', 'change']},
|
||||||
bankCardNum: [{required: true, message: '请填写收款账户卡号', trigger: ['blur', 'change']}],
|
{pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: ['blur', 'change']}
|
||||||
|
],
|
||||||
|
idCard: [{validator: (rule, value, callback) => {
|
||||||
|
const result = this.isValidIdCard(value || '')
|
||||||
|
callback(result.status === 0 ? new Error(result.msg) : undefined)
|
||||||
|
}, required: true, trigger: ['blur', 'change']}],
|
||||||
|
bankCardNum: [{validator: (rule, value, callback) => {
|
||||||
|
const result = this.validateCard(value || '')
|
||||||
|
callback(result.status === 0 ? new Error(result.msg) : undefined)
|
||||||
|
}, required: true, trigger: ['blur', 'change']}],
|
||||||
bankUserName: [{required: true, message: '请填写户名', trigger: ['blur', 'change']}],
|
bankUserName: [{required: true, message: '请填写户名', trigger: ['blur', 'change']}],
|
||||||
bankOfDeposit: [{required: true, message: '请填写开户行', trigger: ['blur', 'change']}],
|
bankOfDeposit: this.textRules('开户行', 30),
|
||||||
position: [{required: true, message: '请填写职务', trigger: ['blur', 'change']}],
|
position: this.textRules('职务', 20),
|
||||||
officialCapacity: [{required: true, message: '请填写职称', trigger: ['blur', 'change']}],
|
officialCapacity: this.textRules('职称', 20),
|
||||||
subsidyStandards: [{required: true, message: '请选择困难类型', trigger: ['blur', 'change']}],
|
homeIncome: [{validator: this.validateIncome, required: true, trigger: ['blur', 'change']}],
|
||||||
reason: [{required: true, message: '请填写申请原因', trigger: ['blur', 'change']}],
|
homeNumber: [{required: true, type: 'integer', min: 1, message: '家庭人数须为大于零的整数', trigger: ['blur', 'change']}],
|
||||||
sign: [{required: true, 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: {
|
methods: {
|
||||||
|
textRules(label, max) {
|
||||||
|
return [{required: true, whitespace: true, max, message: '请填写' + label + ',且不超过' + max + '个字符', trigger: ['blur', 'change']}]
|
||||||
|
},
|
||||||
|
validateIncome(rule, value, callback) {
|
||||||
|
const valid = typeof value === 'number' && Number.isFinite(value) && value >= 0 && /^\d+(\.\d{1,2})?$/.test(String(value))
|
||||||
|
callback(valid ? undefined : new Error('收入须为不小于零的数字,最多保留两位小数'))
|
||||||
|
},
|
||||||
|
validateMoney(rule, value, callback) {
|
||||||
|
const valid = typeof value === 'number' && Number.isFinite(value) && value > 0 && /^\d+(\.\d{1,2})?$/.test(String(value))
|
||||||
|
callback(valid ? undefined : new Error('申请金额须大于零,最多保留两位小数'))
|
||||||
|
},
|
||||||
// 保存
|
// 保存
|
||||||
onSave() {
|
async onSave() {
|
||||||
|
const valid = await this.$refs.formRef.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
this.$confirm("您确定保存吗?", "提示", {
|
this.$confirm("您确定保存吗?", "提示", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
@@ -368,24 +396,13 @@ layout("/layouts/platform.html"){
|
|||||||
|
|
||||||
// 提交
|
// 提交
|
||||||
async onSubmit() {
|
async onSubmit() {
|
||||||
this.$refs.formRef.validate().then((valid) => {
|
this.$refs.formRef.validate().catch(() => false).then((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.$confirm("您确定要提交吗?", "提示", {
|
this.$confirm("您确定要提交吗?", "提示", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
type: "warning"
|
type: "warning"
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const result = this.isValidIdCard(this.formData.idCard)
|
|
||||||
if (result.status === 0) {
|
|
||||||
this.$message.warning(result.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const bankRes = this.validateCard(this.formData.bankCardNum)
|
|
||||||
if (bankRes.status === 0) {
|
|
||||||
this.$message.warning(bankRes.msg)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$axios.post('/platform/difficultHelp/apply/submit', {
|
this.$axios.post('/platform/difficultHelp/apply/submit', {
|
||||||
data: JSON.stringify(this.formData)
|
data: JSON.stringify(this.formData)
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
@@ -400,7 +417,7 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
|
|
||||||
async onFinishTask() {
|
async onFinishTask() {
|
||||||
this.$refs.formRef.validate().then((valid) => {
|
this.$refs.formRef.validate().catch(() => false).then((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.$confirm("您确定要提交吗?", "提示", {
|
this.$confirm("您确定要提交吗?", "提示", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
@@ -408,17 +425,6 @@ layout("/layouts/platform.html"){
|
|||||||
type: "warning"
|
type: "warning"
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|
||||||
const result = this.isValidIdCard(this.formData.idCard)
|
|
||||||
if (result.status === 0) {
|
|
||||||
this.$message.warning(result.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const bankRes = this.validateCard(this.formData.bankCardNum)
|
|
||||||
if (bankRes.status === 0) {
|
|
||||||
this.$message.warning(bankRes.msg)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$axios.post('/platform/difficultHelp/apply/submitAgain', {
|
this.$axios.post('/platform/difficultHelp/apply/submitAgain', {
|
||||||
data: JSON.stringify(this.formData),
|
data: JSON.stringify(this.formData),
|
||||||
taskId: GetQueryString("taskId")
|
taskId: GetQueryString("taskId")
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ layout("/layouts/platform.html"){
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #edit>
|
<template #edit>
|
||||||
<el-form :model="formData" :rules="welfareFormRules" label-width="150px" ref="form" v-loading="formLoading">
|
<el-form :model="formData" :rules="welfareFormRules" :validate-on-rule-change="false"
|
||||||
|
@submit.native.prevent label-width="150px" ref="form" v-loading="formLoading">
|
||||||
<el-form-item label="沿用往期福利" v-if="!formData.id">
|
<el-form-item label="沿用往期福利" v-if="!formData.id">
|
||||||
<el-select clearable filterable placeholder="请选择往期福利" style="width: 100%"
|
<el-select clearable filterable placeholder="请选择往期福利" style="width: 100%"
|
||||||
v-model="reuseProjectId" @change="reuseHistoryProject">
|
v-model="reuseProjectId" @change="reuseHistoryProject">
|
||||||
@@ -294,10 +295,12 @@ layout("/layouts/platform.html"){
|
|||||||
welfareFormRules() {
|
welfareFormRules() {
|
||||||
// 慰问金只要求项目名称和单个发放时间,其他发放形式沿用原有校验。
|
// 慰问金只要求项目名称和单个发放时间,其他发放形式沿用原有校验。
|
||||||
const requiredForOtherMode = this.formData.provideMode !== 99
|
const requiredForOtherMode = this.formData.provideMode !== 99
|
||||||
|
// 慰问金在提交时统一校验,输入项目名称或切换表单项时不提前提示。
|
||||||
|
const requiredTrigger = requiredForOtherMode ? ["blur", "change"] : "submit"
|
||||||
return {
|
return {
|
||||||
name: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
name: [{ required: true, message: "必填", trigger: requiredTrigger }],
|
||||||
festival: [{ required: requiredForOtherMode, message: "必选", trigger: ["blur", "change"] }],
|
festival: [{ required: requiredForOtherMode, message: "必选", trigger: ["blur", "change"] }],
|
||||||
provideTime: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
|
provideTime: [{ required: true, message: "必选", trigger: requiredTrigger }],
|
||||||
choiceTime: [{ required: requiredForOtherMode, message: "必选", trigger: ["blur", "change"] }],
|
choiceTime: [{ required: requiredForOtherMode, message: "必选", trigger: ["blur", "change"] }],
|
||||||
provideAddress: [{ required: requiredForOtherMode, message: "必选", trigger: ["blur", "change"] }],
|
provideAddress: [{ required: requiredForOtherMode, message: "必选", trigger: ["blur", "change"] }],
|
||||||
provideMode: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
|
provideMode: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
|
||||||
@@ -410,6 +413,9 @@ layout("/layouts/platform.html"){
|
|||||||
this.$set(this, "reuseProjectId", null)
|
this.$set(this, "reuseProjectId", null)
|
||||||
if (this.$refs.form) this.$refs.form.resetFields()
|
if (this.$refs.form) this.$refs.form.resetFields()
|
||||||
this.$set(this, "formData", {
|
this.$set(this, "formData", {
|
||||||
|
name: "",
|
||||||
|
choiceTime: [],
|
||||||
|
provideTime: null,
|
||||||
provideMode: 99,
|
provideMode: 99,
|
||||||
isPushHome: false,
|
isPushHome: false,
|
||||||
signMode: 4,
|
signMode: 4,
|
||||||
|
|||||||
@@ -9,26 +9,30 @@ const PROPOSAL_INFO = {
|
|||||||
<van-cell-group :border="false">
|
<van-cell-group :border="false">
|
||||||
<van-cell title="提案编号">{{ viewData.code || '--' }}</van-cell>
|
<van-cell title="提案编号">{{ viewData.code || '--' }}</van-cell>
|
||||||
<van-cell title="提案名称">{{ viewData.name || '--' }}</van-cell>
|
<van-cell title="提案名称">{{ viewData.name || '--' }}</van-cell>
|
||||||
<van-cell title="立案结果" v-if="viewData.caseFilingResult">
|
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
<van-cell title="提案类别">{{ viewData.typeName || '--' }}</van-cell>
|
||||||
:value="viewData.caseFilingResult"></dict-tag>
|
|
||||||
</van-cell>
|
|
||||||
<van-cell title="立案类型" v-if="viewData.caseFilingType">
|
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
|
||||||
:value="viewData.caseFilingType"></dict-tag>
|
|
||||||
</van-cell>
|
|
||||||
<van-cell title="提案类型">{{ viewData.typeName || '--' }}</van-cell>
|
|
||||||
<van-cell title="提案方式">{{ getDictText(dict.type.PROPOSAL_SOURCE, viewData.source) }}
|
<van-cell title="提案方式">{{ getDictText(dict.type.PROPOSAL_SOURCE, viewData.source) }}
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell title="代表姓名">{{ viewData.createUserName || '--' }}</van-cell>
|
<van-cell title="提案人">{{ viewData.createUserName || '--' }}</van-cell>
|
||||||
<van-cell title="所属教代会">{{ viewData.sessionName || viewData.fullName || '--' }}</van-cell>
|
<van-cell title="教代会届次">{{ viewData.sessionName || viewData.fullName || '--' }}</van-cell>
|
||||||
<van-cell :title="viewData.committeeName ? '所属委员会' : '所属代表团'">
|
<van-cell :title="viewData.committeeName ? '委员会名称' : '代表团名称'">
|
||||||
{{ viewData.committeeName || viewData.delegationName || '--' }}
|
{{ viewData.committeeName || viewData.delegationName || '--' }}
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell title="单位">{{ viewData.unitName || '--' }}</van-cell>
|
<van-cell title="提案人单位">{{ viewData.unitName || '--' }}</van-cell>
|
||||||
<van-cell title="联系电话">{{ viewData.mobile || '--' }}</van-cell>
|
<van-cell title="联系电话">{{ viewData.mobile || '--' }}</van-cell>
|
||||||
<van-cell title="提案时间">{{ viewData.createTime || '--' }}</van-cell>
|
<van-cell title="提案时间">{{ viewData.createTime || '--' }}</van-cell>
|
||||||
<van-cell title="案由" class="proposal-view-long-cell">
|
<!-- 立案结果和类型尚未填写时仍显示字段,并以双横线表示空值。 -->
|
||||||
|
<van-cell title="立案结果">
|
||||||
|
<dict-tag v-if="viewData.caseFilingResult" :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
||||||
|
:value="viewData.caseFilingResult"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
|
</van-cell>
|
||||||
|
<van-cell title="立案类型">
|
||||||
|
<dict-tag v-if="viewData.caseFilingType" :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
||||||
|
:value="viewData.caseFilingType"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
|
</van-cell>
|
||||||
|
<van-cell title="提案案由" class="proposal-view-long-cell">
|
||||||
<div class="proposal-view-rich-value" v-html="viewData.brief || '--'"></div>
|
<div class="proposal-view-rich-value" v-html="viewData.brief || '--'"></div>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell title="建议措施" class="proposal-view-long-cell">
|
<van-cell title="建议措施" class="proposal-view-long-cell">
|
||||||
@@ -48,7 +52,7 @@ const PROPOSAL_INFO = {
|
|||||||
:key="merge.id"
|
:key="merge.id"
|
||||||
class="proposal-view-merge-cell"
|
class="proposal-view-merge-cell"
|
||||||
:title="(index + 1) + '. ' + merge.name"
|
:title="(index + 1) + '. ' + merge.name"
|
||||||
:label="'提案编号:' + merge.code + '|提案人:' + merge.createUserName + '|类别:' + merge.typeName + '|代表团:' + merge.delegationName"
|
:label="'提案编号:' + merge.code + '|提案人:' + merge.createUserName + '|提案类别:' + merge.typeName + '|代表团:' + merge.delegationName"
|
||||||
is-link
|
is-link
|
||||||
@click="openView(merge)">
|
@click="openView(merge)">
|
||||||
</van-cell>
|
</van-cell>
|
||||||
@@ -66,13 +70,16 @@ const PROPOSAL_INFO = {
|
|||||||
<template v-if="opinion.commissionerLoginName">({{ opinion.commissionerLoginName }})
|
<template v-if="opinion.commissionerLoginName">({{ opinion.commissionerLoginName }})
|
||||||
</template>
|
</template>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
|
<!-- 委员意见分别展示立案结果和类型,未填写时显示双横线。 -->
|
||||||
<van-cell title="立案结果">
|
<van-cell title="立案结果">
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
<dict-tag v-if="opinion.caseFilingResult" :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
||||||
:value="opinion.caseFilingResult"></dict-tag>
|
:value="opinion.caseFilingResult"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell title="立案类型" v-if="opinion.caseFilingType">
|
<van-cell title="立案类型">
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
<dict-tag v-if="opinion.caseFilingType" :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
||||||
:value="opinion.caseFilingType"></dict-tag>
|
:value="opinion.caseFilingType"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell title="意见" v-if="opinion.opinionText" class="proposal-view-long-cell">
|
<van-cell title="意见" v-if="opinion.opinionText" class="proposal-view-long-cell">
|
||||||
<div class="proposal-view-rich-value">{{ opinion.opinionText }}</div>
|
<div class="proposal-view-rich-value">{{ opinion.opinionText }}</div>
|
||||||
@@ -158,13 +165,18 @@ const PROPOSAL_INFO = {
|
|||||||
|| '--' }})
|
|| '--' }})
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
|
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
|
||||||
|
<!-- 立案节点分别展示流程记录中的结果与类型,空值显示双横线。 -->
|
||||||
<van-cell title="立案结果">
|
<van-cell title="立案结果">
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
<dict-tag v-if="selectedTask.ext.tf_caseFilingResult"
|
||||||
|
:options="dict.type.PROPOSAL_CASE_FILING_RESULT"
|
||||||
:value="selectedTask.ext.tf_caseFilingResult"></dict-tag>
|
:value="selectedTask.ext.tf_caseFilingResult"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell title="立案类型" v-if="selectedTask.ext.tf_caseFilingType">
|
<van-cell title="立案类型">
|
||||||
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
<dict-tag v-if="selectedTask.ext.tf_caseFilingType"
|
||||||
|
:options="dict.type.PROPOSAL_CASE_FILING_TYPE"
|
||||||
:value="selectedTask.ext.tf_caseFilingType"></dict-tag>
|
:value="selectedTask.ext.tf_caseFilingType"></dict-tag>
|
||||||
|
<span v-else>--</span>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<template
|
<template
|
||||||
v-if="['CONFIRM_FILING', 'SUGGESTION'].includes(selectedTask.ext.tf_caseFilingResult)">
|
v-if="['CONFIRM_FILING', 'SUGGESTION'].includes(selectedTask.ext.tf_caseFilingResult)">
|
||||||
@@ -187,7 +199,7 @@ const PROPOSAL_INFO = {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<van-cell title="办理单位"
|
<van-cell title="承办单位"
|
||||||
v-if="['opinion_unit_reply', 'unit_reply', 'two_unit_reply', 'opinion_master_reply', 'master_reply', 'slave_reply'].includes(selectedTask.taskName)">
|
v-if="['opinion_unit_reply', 'unit_reply', 'two_unit_reply', 'opinion_master_reply', 'master_reply', 'slave_reply'].includes(selectedTask.taskName)">
|
||||||
{{ selectedTask.ext.underTakeName || '--' }}
|
{{ selectedTask.ext.underTakeName || '--' }}
|
||||||
</van-cell>
|
</van-cell>
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
<van-empty description="暂无数据" v-else></van-empty>
|
<van-empty description="暂无数据" v-else></van-empty>
|
||||||
</van-action-sheet>-->
|
</van-action-sheet>-->
|
||||||
|
|
||||||
<van-field
|
<!-- <van-field
|
||||||
v-model="formData.certifierUserName"
|
v-model="formData.certifierUserName"
|
||||||
name="certifierUserName"
|
name="certifierUserName"
|
||||||
label="证明人"
|
label="证明人"
|
||||||
@@ -288,7 +288,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<van-empty description="暂无数据" v-else></van-empty>
|
<van-empty description="暂无数据" v-else></van-empty>
|
||||||
</van-action-sheet>
|
</van-action-sheet> -->
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
v-model="formData.typeName"
|
v-model="formData.typeName"
|
||||||
@@ -322,7 +322,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
name="money"
|
name="money"
|
||||||
label="慰问金额"
|
label="慰问金额"
|
||||||
required
|
required
|
||||||
:rules="[{ required: true }]"
|
:rules="[{ required: true, message: '请填写慰问金额' }, { validator: validateMoney, message: '慰问金额须大于0且不超过10000元,最多两位小数' }]"
|
||||||
placeholder="选择慰问类型自动匹配金额"
|
placeholder="选择慰问类型自动匹配金额"
|
||||||
clickable
|
clickable
|
||||||
type="number"
|
type="number"
|
||||||
@@ -353,17 +353,19 @@ layout("/layouts/platform_h5.html"){
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
|
v-if="!['A', 'D'].includes(formData.typeCode)"
|
||||||
v-model="formData.bankCardNumber"
|
v-model="formData.bankCardNumber"
|
||||||
label="收款账户"
|
label="收款账户"
|
||||||
maxlength="20"
|
maxlength="30"
|
||||||
clearable
|
clearable
|
||||||
name="bankCardNumber"
|
name="bankCardNumber"
|
||||||
placeholder="请填写收款账户"
|
placeholder="请填写银行卡号或对公账户号"
|
||||||
:rules="[{ required: true }]"
|
:rules="[{ required: true, message: '请填写银行卡号或对公账户号' }, { pattern: /^[0-9]{6,30}$/, message: '收款账户须为6至30位数字(银行卡号或对公账户号)' }]"
|
||||||
required
|
required
|
||||||
></van-field>
|
></van-field>
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
|
v-if="!['A', 'D'].includes(formData.typeCode)"
|
||||||
v-model="formData.bankUserName"
|
v-model="formData.bankUserName"
|
||||||
label="户名"
|
label="户名"
|
||||||
placeholder="请填写户名"
|
placeholder="请填写户名"
|
||||||
@@ -373,9 +375,10 @@ layout("/layouts/platform_h5.html"){
|
|||||||
name="bankUserName"
|
name="bankUserName"
|
||||||
></van-field>
|
></van-field>
|
||||||
<van-field
|
<van-field
|
||||||
|
v-if="!['A', 'D'].includes(formData.typeCode)"
|
||||||
v-model="formData.bankOfDeposit"
|
v-model="formData.bankOfDeposit"
|
||||||
label="开户行"
|
label="开户行"
|
||||||
maxlength="30"
|
maxlength="20"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
rows="4"
|
rows="4"
|
||||||
autosize
|
autosize
|
||||||
@@ -433,8 +436,8 @@ layout("/layouts/platform_h5.html"){
|
|||||||
></van-datetime-picker>
|
></van-datetime-picker>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
|
|
||||||
<van-field type="digit" label="当年第几次住院"
|
<van-field type="digit" label="当年第几次住院" name="thisYearHospitalizationNum"
|
||||||
:rules="[{ required: true }]"
|
:rules="[{ required: true }, { validator: value => /^[0-9]+$/.test(String(value)) && Number(value) <= 10000, message: '住院次数须为0至10000的整数' }]"
|
||||||
required
|
required
|
||||||
placeholder="请输入当年第几次住院"
|
placeholder="请输入当年第几次住院"
|
||||||
v-model="formData.thisYearHospitalizationNum"></van-field>
|
v-model="formData.thisYearHospitalizationNum"></van-field>
|
||||||
@@ -443,6 +446,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
<van-field
|
<van-field
|
||||||
v-model="formData.remark"
|
v-model="formData.remark"
|
||||||
name="remark"
|
name="remark"
|
||||||
|
maxlength="500"
|
||||||
label="申请事由"
|
label="申请事由"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
rows="4"
|
rows="4"
|
||||||
@@ -460,15 +464,18 @@ layout("/layouts/platform_h5.html"){
|
|||||||
{{ '附件说明:' + chooseType.uploadFileDesc }}
|
{{ '附件说明:' + chooseType.uploadFileDesc }}
|
||||||
</div>
|
</div>
|
||||||
<van-cell-group :border="false">
|
<van-cell-group :border="false">
|
||||||
<van-field class="direction-column-field" name="avatar" label="">
|
<van-field class="direction-column-field" name="files" label="附件" required
|
||||||
|
:rules="[{ validator: () => Array.isArray(formData.files) && formData.files.length > 0 && formData.files.length <= 5, message: '请上传1至5个附件' }]">
|
||||||
<template #input>
|
<template #input>
|
||||||
<h5-file-upload
|
<h5-file-upload
|
||||||
slot="input"
|
slot="input"
|
||||||
:value.sync="formData.files"
|
:value.sync="formData.files"
|
||||||
:upload_number="5"
|
:upload_number="5"
|
||||||
|
upload_text="上传附件"
|
||||||
upload_result_category="array"
|
upload_result_category="array"
|
||||||
complete_result
|
complete_result
|
||||||
></h5-file-upload>
|
></h5-file-upload>
|
||||||
|
<div class="condolence-card__desc">最多上传 5 个文件</div>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
@@ -477,7 +484,8 @@ layout("/layouts/platform_h5.html"){
|
|||||||
<section class="form-section">
|
<section class="form-section">
|
||||||
<h2 class="condolence-card__title">签字确认</h2>
|
<h2 class="condolence-card__title">签字确认</h2>
|
||||||
<van-cell-group :border="false">
|
<van-cell-group :border="false">
|
||||||
<van-field class="direction-column-field" name="signature" label="">
|
<van-field class="direction-column-field" name="signature" label="" :value="formData.signature"
|
||||||
|
:rules="[{ required: true, message: '请填写签字' }]">
|
||||||
<template #input>
|
<template #input>
|
||||||
<h5-signature v-model="formData.signature" slot="input"></h5-signature>
|
<h5-signature v-model="formData.signature" slot="input"></h5-signature>
|
||||||
</template>
|
</template>
|
||||||
@@ -532,6 +540,9 @@ layout("/layouts/platform_h5.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
validateMoney(value) {
|
||||||
|
return /^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000
|
||||||
|
},
|
||||||
// 选择出院时间
|
// 选择出院时间
|
||||||
showLeaveHospitalTimePickerClick() {
|
showLeaveHospitalTimePickerClick() {
|
||||||
if (this.formData.leaveHospitalTime) {
|
if (this.formData.leaveHospitalTime) {
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
<h2 class="condolence-info-card__title">
|
<h2 class="condolence-info-card__title">
|
||||||
审批签字<span class="condolence-audit-card__required">*</span>
|
审批签字<span class="condolence-audit-card__required">*</span>
|
||||||
</h2>
|
</h2>
|
||||||
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
|
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
|
||||||
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
||||||
</van-field>
|
</van-field>
|
||||||
</van-form>
|
</van-form>
|
||||||
|
|||||||
@@ -158,19 +158,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.condolence-info-long-cell {
|
.condolence-info-long-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condolence-info-card .condolence-attachments .empty-container {
|
||||||
|
height: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condolence-info-card .condolence-attachments .empty-text {
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condolence-info-opinion > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condolence-info-opinion > :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condolence-signature-image,
|
||||||
|
.condolence-approval-signature-image {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
width: 100%;
|
||||||
|
|
||||||
.condolence-info-long-cell .van-cell__title {
|
|
||||||
width: auto;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.condolence-info-long-cell .van-cell__value {
|
.condolence-signature-image .van-image__img,
|
||||||
margin-top: 7px;
|
.condolence-approval-signature-image {
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: left center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.condolence-signature-image {
|
.condolence-info-opinion img {
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ const condolenceInfo = {
|
|||||||
<van-cell title="申请人单位">{{ viewData.applyUnitName || '--' }}</van-cell>
|
<van-cell title="申请人单位">{{ viewData.applyUnitName || '--' }}</van-cell>
|
||||||
<van-cell title="申请人工会">{{ viewData.applyUnionName || '--' }}</van-cell>
|
<van-cell title="申请人工会">{{ viewData.applyUnionName || '--' }}</van-cell>
|
||||||
<van-cell title="慰问对象">{{ viewData.helpUserName || '--' }}</van-cell>
|
<van-cell title="慰问对象">{{ viewData.helpUserName || '--' }}</van-cell>
|
||||||
<van-cell title="证明人">{{ viewData.certifierUserName || '--' }}</van-cell>
|
<!-- <van-cell title="证明人">{{ viewData.certifierUserName || '--' }}</van-cell> -->
|
||||||
<van-cell title="慰问类型">{{ viewData.typeName || '--' }}</van-cell>
|
<van-cell title="慰问类型">{{ viewData.typeName || '--' }}</van-cell>
|
||||||
<van-cell title="慰问金额">{{ viewData.money || '--' }}</van-cell>
|
<van-cell title="慰问金额">{{ viewData.money || '--' }}</van-cell>
|
||||||
<van-cell title="慰问时间">{{ viewData.occurTime || '--' }}</van-cell>
|
<van-cell title="慰问时间">{{ viewData.occurTime || '--' }}</van-cell>
|
||||||
<van-cell title="收款账户">{{ viewData.bankCardNumber || '--' }}</van-cell>
|
<van-cell title="收款账户">{{ viewData.bankCardNumber || '--' }}</van-cell>
|
||||||
<van-cell title="户名">{{ viewData.bankUserName || '--' }}</van-cell>
|
<van-cell title="户名">{{ viewData.bankUserName || '--' }}</van-cell>
|
||||||
<van-cell class="condolence-info-long-cell" title="开户行">
|
<van-cell title="开户行">
|
||||||
{{ viewData.bankOfDeposit || '暂无' }}
|
{{ viewData.bankOfDeposit || '暂无' }}
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<template v-if="['2'].includes(viewData.typeCode)">
|
<template v-if="['2'].includes(viewData.typeCode)">
|
||||||
@@ -39,17 +39,18 @@ const condolenceInfo = {
|
|||||||
<van-cell title="出院时间">{{ viewData.leaveHospitalTime || '--' }}</van-cell>
|
<van-cell title="出院时间">{{ viewData.leaveHospitalTime || '--' }}</van-cell>
|
||||||
<van-cell title="当年第几次住院(次)">{{ viewData.thisYearHospitalizationNum || '--' }}</van-cell>
|
<van-cell title="当年第几次住院(次)">{{ viewData.thisYearHospitalizationNum || '--' }}</van-cell>
|
||||||
</template>
|
</template>
|
||||||
<van-cell class="condolence-info-long-cell" title="申请事由">
|
<van-cell title="申请事由">
|
||||||
{{ viewData.remark || '暂无' }}
|
{{ viewData.remark || '暂无' }}
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell class="condolence-info-long-cell" title="附件">
|
<van-cell class="condolence-info-long-cell" title="附件">
|
||||||
<file-preview :files="viewData.files" complete_result></file-preview>
|
<file-preview class="condolence-attachments" :files="viewData.files" complete_result></file-preview>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell class="condolence-info-long-cell" title="签字">
|
<van-cell class="condolence-info-long-cell" title="签字">
|
||||||
<van-image
|
<van-image
|
||||||
v-if="viewData.signature"
|
v-if="viewData.signature"
|
||||||
:src="viewData.signature"
|
:src="viewData.signature"
|
||||||
class="condolence-signature-image"
|
class="condolence-signature-image"
|
||||||
|
fit="contain"
|
||||||
></van-image>
|
></van-image>
|
||||||
<span v-else>暂无签字</span>
|
<span v-else>暂无签字</span>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
@@ -86,10 +87,10 @@ const condolenceInfo = {
|
|||||||
></dict-tag>
|
></dict-tag>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell v-if="task.taskFormData && task.taskFormData.sign" title="审批签字">
|
<van-cell v-if="task.taskFormData && task.taskFormData.sign" title="审批签字">
|
||||||
<img :src="task.taskFormData.sign" style="max-width: 100%; height: 80px; object-fit: contain">
|
<img :src="task.taskFormData.sign" class="condolence-approval-signature-image">
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell class="condolence-info-long-cell" title="办理意见">
|
<van-cell title="办理意见">
|
||||||
<div v-html="task.taskFormData && task.taskFormData.tf_opinion || '暂无'"></div>
|
<div class="condolence-info-opinion" v-html="task.taskFormData && task.taskFormData.tf_opinion || '暂无'"></div>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+18
-1
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
<h2 class="condolence-info-card__title">
|
<h2 class="condolence-info-card__title">
|
||||||
审批签字<span class="condolence-audit-card__required">*</span>
|
审批签字<span class="condolence-audit-card__required">*</span>
|
||||||
</h2>
|
</h2>
|
||||||
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
|
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
|
||||||
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
||||||
</van-field>
|
</van-field>
|
||||||
</van-form>
|
</van-form>
|
||||||
@@ -223,6 +223,23 @@ layout("/layouts/platform_h5.html"){
|
|||||||
},
|
},
|
||||||
onReady() {
|
onReady() {
|
||||||
this.queryCondolenceType()
|
this.queryCondolenceType()
|
||||||
|
this.openTodoTask()
|
||||||
|
},
|
||||||
|
openTodoTask() {
|
||||||
|
const taskId = GetQueryString("taskId")
|
||||||
|
const bizId = GetQueryString("bizId")
|
||||||
|
if (!taskId || !bizId) return
|
||||||
|
this.$axios.post("/flow/common/taskInfo", {taskId}).then((res) => {
|
||||||
|
if (res.code !== 0 || !res.data) return
|
||||||
|
const task = res.data
|
||||||
|
if (task.taskName !== "cbd41337-3c87-4959-9027-809df5c7ef5e") return
|
||||||
|
const row = {id: bizId, taskId: task.id, curTaskName: task.displayName}
|
||||||
|
if (task.taskState === 10 && task.executable && GetQueryString("tab") === "todo") {
|
||||||
|
this.onApproval(row)
|
||||||
|
} else {
|
||||||
|
this.onView(row)
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onView(row) {
|
onView(row) {
|
||||||
this.$set(this, "showApprovalForm", false)
|
this.$set(this, "showApprovalForm", false)
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
<h2 class="condolence-info-card__title">
|
<h2 class="condolence-info-card__title">
|
||||||
审批签字<span class="condolence-audit-card__required">*</span>
|
审批签字<span class="condolence-audit-card__required">*</span>
|
||||||
</h2>
|
</h2>
|
||||||
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
|
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
|
||||||
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
||||||
</van-field>
|
</van-field>
|
||||||
</van-form>
|
</van-form>
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="condolence-list-page">
|
<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">
|
<van-sticky offset-top="46px">
|
||||||
<div class="condolence-list-sticky">
|
<div class="condolence-list-sticky">
|
||||||
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
<h2 class="condolence-info-card__title">
|
<h2 class="condolence-info-card__title">
|
||||||
审批签字<span class="condolence-audit-card__required">*</span>
|
审批签字<span class="condolence-audit-card__required">*</span>
|
||||||
</h2>
|
</h2>
|
||||||
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
|
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
|
||||||
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
|
||||||
</van-field>
|
</van-field>
|
||||||
</van-form>
|
</van-form>
|
||||||
|
|||||||
+23
-6
@@ -9,7 +9,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
</style>
|
</style>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="difficult-help-approval-page">
|
<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">
|
<van-sticky offset-top="46px">
|
||||||
<div class="difficult-help-approval-sticky">
|
<div class="difficult-help-approval-sticky">
|
||||||
<van-search
|
<van-search
|
||||||
@@ -29,7 +29,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
:close-on-click-overlay="false"
|
:close-on-click-overlay="false"
|
||||||
>
|
>
|
||||||
<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="unionList"
|
:options="unionList"
|
||||||
@change="unionChange"
|
@change="unionChange"
|
||||||
@open="onFilterOpen"
|
@open="onFilterOpen"
|
||||||
@@ -37,7 +37,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
v-model="pageForm.unionId">
|
v-model="pageForm.unionId">
|
||||||
</van-dropdown-item>
|
</van-dropdown-item>
|
||||||
<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"
|
:options="unitList"
|
||||||
@change="doSearch"
|
@change="doSearch"
|
||||||
@open="onFilterOpen"
|
@open="onFilterOpen"
|
||||||
@@ -228,6 +228,14 @@ layout("/layouts/platform_h5.html"){
|
|||||||
components: {
|
components: {
|
||||||
"info": INFO
|
"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: {
|
methods: {
|
||||||
historyBack,
|
historyBack,
|
||||||
|
|
||||||
@@ -419,13 +427,22 @@ layout("/layouts/platform_h5.html"){
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.initLevelDict()
|
// 先订阅弹框状态,工会加载异常也不能阻断等级选择器与确认框的打开、返回。
|
||||||
this.initUnion().then(() => this.flushUnits())
|
if (window.h5HistoryLayerManager) {
|
||||||
if (!window.h5HistoryLayerManager) return
|
|
||||||
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
|
this.$set(this, "historyLayerUnsubscribe", window.h5HistoryLayerManager.subscribe((stack) => {
|
||||||
this.$set(this, "confirmVisible", stack.includes("difficult-help-action-confirm"))
|
this.$set(this, "confirmVisible", stack.includes("difficult-help-action-confirm"))
|
||||||
this.$set(this, "showLevelPicker", stack.includes("difficult-help-level-picker"))
|
this.$set(this, "showLevelPicker", stack.includes("difficult-help-level-picker"))
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
this.initLevelDict()
|
||||||
|
// 缓存已就绪时立即加载,否则等待用户接口回填;只初始化一次,避免重置已选筛选条件。
|
||||||
|
// $watch 由当前 Vue 实例管理,页面销毁时自动注销。
|
||||||
|
let filtersInitialized = false
|
||||||
|
this.$watch("filterUserReady", (ready) => {
|
||||||
|
if (!ready || filtersInitialized) return
|
||||||
|
filtersInitialized = true
|
||||||
|
this.initUnion().then(() => this.flushUnits())
|
||||||
|
}, {immediate: true})
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (typeof this.historyLayerUnsubscribe === "function") this.historyLayerUnsubscribe()
|
if (typeof this.historyLayerUnsubscribe === "function") this.historyLayerUnsubscribe()
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
</style>
|
</style>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="difficult-help-approval-page">
|
<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">
|
<van-sticky offset-top="46px">
|
||||||
<div class="difficult-help-approval-sticky">
|
<div class="difficult-help-approval-sticky">
|
||||||
<van-search
|
<van-search
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ layout("/layouts/platform_h5.html"){
|
|||||||
</style>
|
</style>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="difficult-help-approval-page">
|
<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">
|
<van-sticky offset-top="46px">
|
||||||
<div class="difficult-help-approval-sticky">
|
<div class="difficult-help-approval-sticky">
|
||||||
<van-search
|
<van-search
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
const apps = {
|
const apps = {
|
||||||
|
// 从“我的工具”进入时指定分类名称;空值表示恢复应用中心原有浏览状态。
|
||||||
|
props: {
|
||||||
|
initialCategoryName: {type: String, default: ""}
|
||||||
|
},
|
||||||
template: /*language=HTML*/ `
|
template: /*language=HTML*/ `
|
||||||
<div class="apps-container">
|
<div class="apps-container">
|
||||||
<van-nav-bar title="应用中心" placeholder fixed></van-nav-bar>
|
<van-nav-bar title="应用中心" placeholder fixed></van-nav-bar>
|
||||||
@@ -201,7 +205,8 @@ const apps = {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// 动态分类加载完成后再恢复分类,避免把缓存的动态分类误判为“全部应用”
|
// 动态分类加载完成后再恢复分类,避免把缓存的动态分类误判为“全部应用”
|
||||||
this.restoreAppsState()
|
// 指定入口不恢复旧搜索词、滚动位置及子菜单弹框,避免目标分类被旧状态遮挡。
|
||||||
|
if (!this.initialCategoryName) this.restoreAppsState()
|
||||||
this.loadCategories().finally(() => {
|
this.loadCategories().finally(() => {
|
||||||
this.restoreActiveCategory()
|
this.restoreActiveCategory()
|
||||||
this.loadApps()
|
this.loadApps()
|
||||||
@@ -249,6 +254,13 @@ const apps = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
restoreActiveCategory() {
|
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))
|
const index = this.allCategories.findIndex(item => String(item.id) === String(this.cachedCategoryId))
|
||||||
this.activeTab = index >= 0 ? index : 0
|
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>
|
<todo v-if="homeTabbarActive === 'todo'" @todo-count-change="updateTodoCount"></todo>
|
||||||
<work v-if="homeTabbarActive === 'work'"></work>
|
<work v-if="homeTabbarActive === 'work'"></work>
|
||||||
<mine v-if="homeTabbarActive === 'mine'" @switch-tab="switchHomeTab"></mine>
|
<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>
|
<msg v-if="homeTabbarActive === 'msg'" @unread-count-change="updateUnreadMessageCount"></msg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -111,19 +111,25 @@ layout("/layouts/platform_h5.html"){
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
homeTabbarActive: "home",
|
homeTabbarActive: "home",
|
||||||
|
appsCategoryName: "",
|
||||||
todoCount: 0,
|
todoCount: 0,
|
||||||
unreadMessageCount: 0
|
unreadMessageCount: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
homeTabbarChange(val) {
|
homeTabbarChange(val) {
|
||||||
|
// 普通页签切换沿用应用页缓存,不保留工具入口指定的分类。
|
||||||
|
this.$set(this, "appsCategoryName", "")
|
||||||
this.$store.commit("setActiveTarBar", val)
|
this.$store.commit("setActiveTarBar", val)
|
||||||
},
|
},
|
||||||
switchHomeTab(val) {
|
// categoryName 来自“我的工具”,先传分类再挂载应用页,避免读取上一次的分类缓存。
|
||||||
this.homeTabbarActive = val
|
switchHomeTab(val, categoryName = "") {
|
||||||
|
this.$set(this, "appsCategoryName", val === "apps" ? categoryName : "")
|
||||||
|
this.$set(this, "homeTabbarActive", val)
|
||||||
this.$store.commit("setActiveTarBar", val)
|
this.$store.commit("setActiveTarBar", val)
|
||||||
},
|
},
|
||||||
moduleClick(val) {
|
moduleClick(val) {
|
||||||
|
this.$set(this, "appsCategoryName", "")
|
||||||
this.homeTabbarActive = "apps"
|
this.homeTabbarActive = "apps"
|
||||||
this.$store.commit("setActiveTarBar", "apps")
|
this.$store.commit("setActiveTarBar", "apps")
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,22 +74,23 @@ const mine = {
|
|||||||
<van-icon name="edit"></van-icon>
|
<van-icon name="edit"></van-icon>
|
||||||
<span>我的签名</span>
|
<span>我的签名</span>
|
||||||
</button>
|
</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>
|
<van-icon name="star-o"></van-icon>
|
||||||
<span>我的收藏</span>
|
<span>我的收藏</span>
|
||||||
</button>
|
</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>
|
<van-icon name="gift-o"></van-icon>
|
||||||
<span>福利中心</span>
|
<span>福利中心</span>
|
||||||
</button>
|
</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>
|
<van-icon name="calendar-o"></van-icon>
|
||||||
<span>活动报名</span>
|
<span>活动报名</span>
|
||||||
</button>
|
</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>
|
<van-icon name="completed-o"></van-icon>
|
||||||
<span>问卷调查</span>
|
<span>问卷调查</span>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- 暂不开放设置中心和帮助中心,保留入口模板便于恢复。
|
||||||
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
<button type="button" class="mine-grid-item" @click="switchTab('apps')">
|
||||||
<van-icon name="setting-o"></van-icon>
|
<van-icon name="setting-o"></van-icon>
|
||||||
<span>设置中心</span>
|
<span>设置中心</span>
|
||||||
@@ -98,6 +99,7 @@ const mine = {
|
|||||||
<van-icon name="question-o"></van-icon>
|
<van-icon name="question-o"></van-icon>
|
||||||
<span>帮助中心</span>
|
<span>帮助中心</span>
|
||||||
</button>
|
</button>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -150,9 +152,10 @@ const mine = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
switchTab(tab) {
|
// tab 为底部页签,categoryName 为可选应用分类名称;通过事件交给首页切换并定位分类。
|
||||||
|
switchTab(tab, categoryName = "") {
|
||||||
this.$store.commit("setActiveTarBar", tab)
|
this.$store.commit("setActiveTarBar", tab)
|
||||||
this.$emit("switch-tab", tab)
|
this.$emit("switch-tab", tab, categoryName)
|
||||||
},
|
},
|
||||||
logout() {
|
logout() {
|
||||||
this.$dialog
|
this.$dialog
|
||||||
|
|||||||
@@ -281,11 +281,13 @@ const todo = {
|
|||||||
// 处理任务
|
// 处理任务
|
||||||
onView(task) {
|
onView(task) {
|
||||||
const {taskId, taskKey, instanceId, businessNo, formKey, h5FormKey} = task;
|
const {taskId, taskKey, instanceId, businessNo, formKey, h5FormKey} = task;
|
||||||
if (!h5FormKey) {
|
const mobileFormKey = h5FormKey || (taskKey === "cbd41337-3c87-4959-9027-809df5c7ef5e"
|
||||||
|
? "/platform/condolence/fourthApproval/h5" : "");
|
||||||
|
if (!mobileFormKey) {
|
||||||
this.$toast("当前流程暂不支持手机端审核,请前往PC端页面审核!");
|
this.$toast("当前流程暂不支持手机端审核,请前往PC端页面审核!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$pjaxReplace(h5FormKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
|
this.$pjaxReplace(mobileFormKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 根据当前列表类型统一展示流程状态,发起列表保留流程实例的实际状态。
|
// 根据当前列表类型统一展示流程状态,发起列表保留流程实例的实际状态。
|
||||||
|
|||||||
Reference in New Issue
Block a user