51 changed files with 1149 additions and 1103 deletions
@@ -19,6 +19,7 @@ RoleConstant {
SCHOOL_UNION_ADMIN("校工会管理员"),
SCHOOL_UNION_CHAIRMAN("校工会主席"),
SCHOOL_UNION_VICE_CHAIRMAN("校工会副主席"),
SCHOOL_UNION_FINANCE("校工会财务"),
SCHOOL_OFFICE_DIRECTOR("校工会办公室主任"),
SCHOOL_UNION_WELFARE_ADMIN("校工会福利管理员"),
SCHOOL_UNION_MEMBER_ADMIN("校工会会员管理员"),
@@ -6,6 +6,8 @@ import com.budwk.app.base.exception.BaseException;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.models.Sys_office_template;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.ByteArrayPictureRenderData;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
@@ -18,6 +20,12 @@ import org.nutz.ioc.loader.annotation.IocBean;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -66,6 +74,88 @@ public class SysOfficeTemplateUtil {
return new ByteArrayInputStream(fileBytes);
}
/**
* 统一渲染 DOC/DOCX 模板,保留调用方的循环表格等策略;模板输入流由调用方关闭。
*/
public byte[] renderTemplate(InputStream input, Object data, Configure config, boolean pdf, String context) throws Exception {
if (input == null) throw new BaseException("模板文件不存在");
byte[] templateBytes = input.readAllBytes();
try (ByteArrayInputStream formatInput = new ByteArrayInputStream(templateBytes)) {
if (com.aspose.words.FileFormatUtil.detectFileFormat(formatInput).getLoadFormat() != com.aspose.words.LoadFormat.DOCX) {
try (ByteArrayInputStream source = new ByteArrayInputStream(templateBytes);
ByteArrayOutputStream converted = new ByteArrayOutputStream()) {
new com.aspose.words.Document(source).save(converted, com.aspose.words.SaveFormat.DOCX);
templateBytes = converted.toByteArray();
}
}
}
byte[] wordBytes;
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateBytes);
ByteArrayOutputStream output = new ByteArrayOutputStream();
XWPFTemplate template = XWPFTemplate.compile(templateInput, config == null ? Configure.builder().build() : config)) {
template.render(data).write(output);
wordBytes = output.toByteArray();
} catch (Exception e) {
log.error("[Office模板渲染] Word模板渲染失败,context={}", context, e);
throw e;
}
log.info("[Office模板渲染] Word渲染完成,context={}, templateBytes={}, outputBytes={}", context, templateBytes.length, wordBytes.length);
if (!pdf) return wordBytes;
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(wordBytes);
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
com.aspose.words.Document document = new com.aspose.words.Document(wordInput);
configureDocumentFonts(document, context);
com.aspose.words.PdfSaveOptions options = new com.aspose.words.PdfSaveOptions();
options.setFontEmbeddingMode(com.aspose.words.PdfFontEmbeddingMode.EMBED_ALL);
document.save(output, options);
log.info("[Office模板渲染] PDF转换完成,context={}, outputBytes={}", context, output.size());
return output.toByteArray();
} catch (Exception e) {
log.error("[Office模板渲染] PDF转换失败,context={}", context, e);
throw e;
}
}
private void configureDocumentFonts(com.aspose.words.Document document, String context) throws Exception {
com.aspose.words.FontSettings settings = new com.aspose.words.FontSettings();
List<com.aspose.words.FontSourceBase> sources = new ArrayList<>(List.of(settings.getFontsSources()));
Set<String> folders = new LinkedHashSet<>();
String windowsDir = System.getenv("WINDIR");
if (StrUtil.isNotBlank(windowsDir)) folders.add(new File(windowsDir, "Fonts").getPath());
String localAppData = System.getenv("LOCALAPPDATA");
if (StrUtil.isNotBlank(localAppData)) folders.add(new File(localAppData, "Microsoft/Windows/Fonts").getPath());
folders.add(new File(System.getProperty("user.dir"), "fonts").getPath());
for (String folder : folders) {
File directory = new File(folder);
log.info("[Office模板渲染] 字体目录,context={}, path={}, readable={}", context, folder, directory.isDirectory() && directory.canRead());
if (directory.isDirectory() && directory.canRead()) sources.add(new com.aspose.words.FolderFontSource(folder, true));
}
settings.setFontsSources(sources.toArray(new com.aspose.words.FontSourceBase[0]));
Set<String> availableFonts = new LinkedHashSet<>();
for (com.aspose.words.FontSourceBase source : sources) {
for (com.aspose.words.PhysicalFontInfo font : source.getAvailableFonts()) availableFonts.add(font.getFontFamilyName());
}
String chineseFont = null;
for (String candidate : List.of("SimSun", "宋体", "Microsoft YaHei", "微软雅黑", "DengXian", "等线", "SimHei", "黑体", "FangSong", "仿宋", "Noto Serif CJK SC", "Noto Sans CJK SC")) {
if (availableFonts.contains(candidate)) {
chineseFont = candidate;
break;
}
}
log.info("[Office模板渲染] 中文字体选择,context={}, fontCount={}, chineseFont={}", context, availableFonts.size(), chineseFont);
if (chineseFont != null) {
settings.getSubstitutionSettings().getDefaultFontSubstitution().setDefaultFontName(chineseFont);
for (String name : List.of("宋体", "SimSun", "新宋体", "NSimSun", "黑体", "SimHei", "仿宋", "FangSong", "楷体", "KaiTi", "微软雅黑", "Microsoft YaHei")) {
if (!availableFonts.contains(name)) settings.getSubstitutionSettings().getTableSubstitution().setSubstitutes(name, chineseFont);
}
} else {
log.warn("[Office模板渲染] 未发现常用中文字体,context={}, availableFonts={}", context, availableFonts);
}
settings.getFallbackSettings().loadMsOfficeFallbackSettings();
document.setFontSettings(settings);
document.setWarningCallback(warning -> log.warn("[Office模板渲染] 字体或排版警告,context={}, type={}, detail={}", context, warning.getWarningType(), warning.getDescription()));
}
/**
* 签字url转换为poi的图片对象
*
@@ -77,13 +167,7 @@ public class SysOfficeTemplateUtil {
public PictureRenderData createPictureRenderData(int width, int height, String url) {
try {
byte[] bytes = getFileBytesByUrl(url);
// if (StrUtil.isBlank(url)) {
// return null;
// }
// int startIndex = url.indexOf("id=") + "id=".length();
// String id = url.substring(startIndex);
// Sys_file sysFile = dao.fetch(Sys_file.class, id);
// byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
log.info("[Office图片读取] 创建图片渲染数据,referenceLength={}, byteCount={}, width={}, height={}", StrUtil.nullToEmpty(url).length(), bytes == null ? null : bytes.length, width, height);
ByteArrayPictureRenderData pictureRenderData = new ByteArrayPictureRenderData(bytes, PictureType.PNG);
PictureStyle pictureStyle = new PictureStyle();
pictureStyle.setWidth(width);
@@ -91,7 +175,7 @@ public class SysOfficeTemplateUtil {
pictureRenderData.setPictureStyle(pictureStyle);
return pictureRenderData;
} catch (Exception e) {
log.error("文件转换PictureRenderData错误,文件路径:{},{}", url, e.getMessage());
log.error("[Office图片读取] 创建图片渲染数据失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
return null;
}
}
@@ -140,13 +224,20 @@ public class SysOfficeTemplateUtil {
public byte[] getFileBytesByUrl(String url) {
try {
if (StrUtil.isBlank(url)) {
log.warn("[Office图片读取] 图片引用为空");
return null;
}
int startIndex = url.indexOf("id=") + "id=".length();
String id = url.substring(startIndex);
String logFileId = url.contains("id=") ? id : "未识别id参数";
log.info("[Office图片读取] 解析文件引用,fileId={}, hasIdParameter={}", logFileId, url.contains("id="));
Sys_file sysFile = dao.fetch(Sys_file.class, id);
return SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
log.info("[Office图片读取] 查询文件记录,fileId={}, found={}, bucket={}, storagePathPresent={}", logFileId, sysFile != null, sysFile == null ? null : sysFile.getBucket(), sysFile != null && StrUtil.isNotBlank(sysFile.getStoragePath()));
byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
log.info("[Office图片读取] 读取存储文件完成,fileId={}, byteCount={}", logFileId, bytes == null ? null : bytes.length);
return bytes;
} catch (Exception e) {
log.error("[Office图片读取] 读取图片失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
return null;
}
}
@@ -0,0 +1,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 "获取校工会财务";
}
}
@@ -1,27 +1,80 @@
package com.budwk.app.flow.listenter;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.event.ProcessEvent;
import com.budwk.app.flow.engine.event.ProcessEventListener;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.flow.service.FlowTaskMessageService;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
/** 普通节点、首次会签及转办任务统一从开始事件进入待办消息 service。 */
import java.util.List;
/**
* 流程任务开始监听器
*/
@IocBean
@Slf4j
public class ProcessTaskStartEventListener implements ProcessEventListener {
@Inject
private Dao dao;
@Inject
private FlowTaskMessageService flowTaskMessageService;
private GlobalMessageSendService globalMessageSendService;
@Inject
private DifficultHelpCommonService difficultHelpCommonService;
@Override
public void onEvent(ProcessEvent event) {
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) {
ProcessTask task = dao.fetch(ProcessTask.class, event.getSourceId());
flowTaskMessageService.notifyTask(task, false);
Long sourceId = event.getSourceId();
ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
if (task == null) return;
sendMessage(task);
// 节点及接收人判断集中在业务 service;通知失败不向流程提交接口抛出异常。
try {
difficultHelpCommonService.notifyOfficeApproval(task);
} catch (Exception e) {
log.error("困难帮扶企业微信待办通知异常,taskId={}", sourceId, e);
}
}
}
private void sendMessage(ProcessTask task) {
// 说明是流程开始 用户首次完成任务
if (task.getTaskParentId() == 0 && task.getTaskName().equals("startTask")) {
return;
}
// 节点名称
String taskDisplayName = task.getDisplayName();
// 实例名称
Long instanceId = task.getProcessInstanceId();
ProcessInstance instance = dao.fetch(ProcessInstance.class, instanceId);
String instanceName = Json.fromJson(NutMap.class, instance.getVariable()).getString(FlowConst.PROCESS_INSTANCE_NAME);
// 任务接收人
List<ProcessTaskActor> taskActors = dao.query(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
for (ProcessTaskActor taskActor : taskActors) {
// 模拟发送消息
String message = StrUtil.format("您有一条待办任务,实例:{},处理环节:{}", taskActor.getActorName(), instanceName, taskDisplayName);
log.info(message);
globalMessageSendService.sendMessage(taskDisplayName, message, 2, List.of(taskActor.getActorId()), null);
}
}
}
@@ -1,132 +0,0 @@
package com.budwk.app.flow.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/** WF 待办统一消息入口;站内消息、短信和企业微信沿用全局消息渠道。 */
@IocBean
@Slf4j
public class FlowTaskMessageService {
private static final String HOME = "/platform/home";
private static final Set<String> TASK_PARAMS = Set.of("taskId", "bizId", "taskKey", "tab");
@Inject
private Dao dao;
@Inject
private GlobalMessageSendService globalMessageSendService;
/**
* task 为参与人已落库的 WF 任务;includeStartTask 仅驳回重建时传 true,
* 使退回申请人的 startTask 也收到通知,首次提交的申请节点不通知本人。
* 无返回值:仅通知进行中的任务,异常记录 taskId,不中断业务审核。
* config 的 wfTaskId 标识流程通知,wfLoginNames 为办理人的 loginname,
* wxWorkLink 为卡片地址;微信策略据此切换到官方企业微信接口。
*/
@Aop(TransAop.READ_COMMITTED)
public void notifyTask(ProcessTask task, boolean includeStartTask) {
if (task == null || task.getId() == null || task.getProcessInstanceId() == null
|| !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) return;
if (!includeStartTask && "startTask".equals(task.getTaskName())
&& (task.getTaskParentId() == null || task.getTaskParentId() == 0L)) return;
try {
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null) {
log.warn("WF 待办通知跳过:实例不存在,taskId={}", task.getId());
return;
}
ProcessDefine define = instance.getProcessDefineId() == null ? null
: dao.fetch(ProcessDefine.class, instance.getProcessDefineId());
List<ProcessTaskActor> actors = dao.query(ProcessTaskActor.class,
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
List<String> receiverIds = actors.stream().map(ProcessTaskActor::getActorId)
.filter(StrUtil::isNotBlank).distinct().toList();
if (receiverIds.isEmpty()) {
log.warn("WF 待办通知跳过:任务没有实际办理人,taskId={}", task.getId());
return;
}
// 只用实际参与人的账号,不按角色扩大范围,不以内部用户 ID 代替企业微信 userid。
List<String> loginNames = actors.stream().filter(actor -> StrUtil.isNotBlank(actor.getActorId()))
.map(ProcessTaskActor::getActorAccount).filter(StrUtil::isNotBlank)
.map(String::trim).distinct().toList();
if (actors.stream().anyMatch(actor -> StrUtil.isBlank(actor.getActorAccount()))) {
log.warn("WF 待办存在未绑定 loginname 的办理人,taskId={}", task.getId());
}
String instanceName = define == null ? "流程审批" : StrUtil.blankToDefault(define.getDisplayName(), "流程审批");
if (StrUtil.isNotBlank(instance.getVariable())) {
try {
instanceName = StrUtil.blankToDefault(JSONUtil.parseObj(instance.getVariable())
.getStr(FlowConst.PROCESS_INSTANCE_NAME), instanceName);
} catch (Exception e) {
log.warn("WF 实例名称解析失败,使用流程定义名称,taskId={}", task.getId());
}
}
String nodeName = StrUtil.blankToDefault(task.getDisplayName(), "待办审核");
String content = "您有一条待办任务,实例:" + instanceName + ",处理环节:" + nodeName;
JSONObject config = JSONUtil.createObj().set("wfTaskId", task.getId())
.set("wfLoginNames", loginNames).set("wxWorkLink", buildLink(task, instance, define));
// 系统发送身份兼容定时推进等没有当前登录会话的流程入口。
globalMessageSendService.sendSystemMessage(nodeName, content, 2, receiverIds, config);
} catch (Exception e) {
log.error("WF 待办通知异常,taskId={}", task.getId(), e);
}
}
/** 优先节点手机表单;缺失或非法时回首页。保留原查询参数和锚点,替换过期任务参数。 */
private String buildLink(ProcessTask task, ProcessInstance instance, ProcessDefine define) {
String form = task.getH5FormKey();
// KNBF 历史定义中的办公室地址过时,沿用已经验证的实际审核入口。
if (define != null && "KNBF".equals(define.getName())
&& "36ebafda-44c2-4bd3-bd7f-10839a575a0d".equals(task.getTaskName())) {
form = "/platform/difficultHelp/branchUnionApproval/h5";
}
if (StrUtil.isBlank(form) || "null".equalsIgnoreCase(form.trim())
|| StrUtil.isBlank(instance.getBusinessNo()) || StrUtil.isBlank(task.getTaskName())) return HOME;
try {
form = form.trim();
URI uri = URI.create(form);
if (uri.getUserInfo() != null || (uri.isAbsolute()
? !("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme()))
|| uri.getHost() == null
: !form.startsWith("/") || uri.getRawAuthority() != null)) return HOME;
List<String> params = new ArrayList<>();
if (StrUtil.isNotBlank(uri.getRawQuery())) {
for (String param : uri.getRawQuery().split("&")) {
String key = URLDecoder.decode(param.split("=", 2)[0], StandardCharsets.UTF_8);
if (!TASK_PARAMS.contains(key)) params.add(param);
}
}
params.add("taskId=" + task.getId());
params.add("bizId=" + URLEncoder.encode(instance.getBusinessNo(), StandardCharsets.UTF_8));
params.add("taskKey=" + URLEncoder.encode(task.getTaskName(), StandardCharsets.UTF_8));
params.add("tab=todo");
String base = form.split("[?#]", 2)[0];
return base + "?" + String.join("&", params)
+ (uri.getRawFragment() == null ? "" : "#" + uri.getRawFragment());
} catch (IllegalArgumentException e) {
log.warn("WF 手机表单地址无效,卡片回退首页,taskId={}", task.getId());
return HOME;
}
}
}
@@ -26,7 +26,6 @@ import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.*;
import com.budwk.app.flow.service.FlowTaskMessageService;
import com.budwk.app.flow.service.ProcessDefineService;
import com.budwk.app.flow.service.ProcessInstanceService;
import com.budwk.app.flow.service.ProcessTaskService;
@@ -61,9 +60,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
@Inject
private SchoolOaTodoService schoolOaTodoService;
@Inject
private FlowTaskMessageService flowTaskMessageService;
@Override
public boolean save(ProcessTask param) {
param.setId(null);
@@ -296,8 +292,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor));
// 并行加签绕过普通节点创建入口,需要为新增的个人任务同步待办。
schoolOaTodoService.createTodo(clone);
// 并行加签不经过 CreateTaskHandler,只为本次新增任务通知一次。
flowTaskMessageService.notifyTask(clone, false);
});
}
@@ -368,8 +362,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
addTaskActor(task.getId(), CollectionUtil.newArrayList(task.getOperator()));
// 驳回创建了新的任务 ID,按新任务推送,避免复用历史待办。
schoolOaTodoService.createTodo(task);
// 驳回重建不发布开始事件;退回申请节点时也通知申请人重新办理。
flowTaskMessageService.notifyTask(task, true);
return task;
}
@@ -449,12 +441,6 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class);
// 更新会签变量到流程实例参数中
processInstanceService.updateCountersignVariable(taskModel, execution, taskActors);
// 串行会签后续办理人由 CountersignHandler 直接创建,不经过开始事件;首次会签仍由监听器通知。
String counterPrefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + taskModel.getName() + "_";
if (CountersignTypeEnum.SEQUENTIAL.equals(taskModel.getCountersignType())
&& execution.getArgs().get(counterPrefix + FlowConst.LOOP_COUNTER, -1) >= 0) {
processTaskList.forEach(task -> flowTaskMessageService.notifyTask(task, false));
}
return processTaskList;
}
@@ -84,12 +84,10 @@ public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
task.getId(), task.getProcessInstanceId(), task.getTaskState());
}
/**
* OA 待办接口联调尚未完成,暂时在代码中固定关闭,避免远端异常阻断内部 WF 流转。
* 不受 AppSchoolOa 或 enabled 配置影响;恢复对接时需显式恢复配置开关逻辑。
*/
/** 系统参数优先于文件配置,只有明确为 true 才启用;未配置不访问数据库或 OA。 */
private boolean isEnabled() {
return false;
Object enabled = Globals.MyConfig.get("AppSchoolOa");
return Boolean.parseBoolean(enabled == null ? conf.get(PREFIX + "enabled", "false") : enabled.toString());
}
private void requireTask(ProcessTask task) {
@@ -193,8 +193,7 @@ public class GlobalMessageSendService {
try {
strategy.send(request.getTitle(), request.getContent(), request.getType(),
request.getReceiverIds(), request.getConfig());
// 策略可能因开关关闭而跳过,实际发送结果由策略日志记录。
log.info("异步消息策略执行完成,渠道:{}", strategy.getChannel().getValue());
log.info("异步发送消息成功,渠道:{}", strategy.getChannel().getValue());
} catch (Exception e) {
log.error("异步发送消息失败,渠道:{},错误:{}", strategy.getChannel().getValue(), e.getMessage(), e);
}
@@ -1,23 +1,16 @@
package com.budwk.app.zhgh.dayofficework.message.strategy.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.wxwork.WxWorkMessageService;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.dayofficework.message.enums.GlobalMessageChannel;
import com.budwk.app.zhgh.dayofficework.message.strategy.GlobalMessageSendStrategy;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
@@ -31,8 +24,6 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
private Dao dao;
@Inject
private SmsService smsService;
@Inject
private WxWorkMessageService wxWorkMessageService;
@Override
public GlobalMessageChannel getChannel() {
@@ -45,13 +36,7 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void send(String title, String content, Integer type, List<String> receiverIds, JSONObject config) {
// WF 的接收账号已在任务事务内取得,异步线程不再反查尚未提交的任务数据。
if (config != null && config.getLong("wfTaskId") != null) {
sendFlowCard(title, content, config);
return;
}
try {
log.info("开始发送微信消息,标题:{},接收人数量:{}", title, receiverIds.size());
@@ -67,12 +52,7 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void sendByLoginName(String title, String content, Integer type, List<String> receiverLoginNames, JSONObject config) {
if (config != null && config.getLong("wfTaskId") != null) {
sendFlowCard(title, content, config);
return;
}
try {
log.info("开始发送微信消息(按登录名),标题:{},接收人数量:{}", title, receiverLoginNames.size());
@@ -87,58 +67,6 @@ public class WechatMessageSendStrategy implements GlobalMessageSendStrategy {
}
}
/**
* WF 专用分支:wfTaskId 为任务ID,wfLoginNames 为实际办理人的 loginname 列表,
* wxWorkLink 为非空手机入口。无返回值;关闭开关或无账号时跳过,失败抛给全局异步日志处理,
* 不回退旧微信通道,避免双发。非流程消息继续沿用原有通讯平台。
*/
private void sendFlowCard(String title, String content, JSONObject config) {
Long taskId = config.getLong("wfTaskId");
List<String> loginNames = config.getJSONArray("wfLoginNames") == null ? List.of()
: config.getJSONArray("wfLoginNames").toList(String.class);
if (loginNames.isEmpty()) {
log.warn("WF 企业微信卡片跳过:办理人 loginname 为空,taskId={}", taskId);
return;
}
String link = StrUtil.blankToDefault(config.getStr("wxWorkLink"), "/platform/home");
NutMap result;
try {
result = wxWorkMessageService.massSend(loginNames, cardText(title, 128, false),
cardText(content, 512, true), link);
} catch (RuntimeException e) {
// 异步线程已脱离任务调用栈,保留任务 ID 便于从错误日志定位具体待办。
throw new IllegalStateException("WF 企业微信卡片接口异常,taskId=" + taskId, e);
}
if (result != null && result.getBoolean("skipped", false)) {
log.info("WF 企业微信卡片跳过:消息开关关闭,taskId={}", taskId);
} else if (result != null && result.getBoolean("success", false)) {
log.info("WF 企业微信卡片发送成功,taskId={},接收账号数={}", taskId, loginNames.size());
} else {
log.error("WF 企业微信卡片发送失败或部分失败,taskId={},result={}", taskId, result);
throw new IllegalStateException("WF 企业微信卡片发送失败或部分失败,taskId=" + taskId);
}
}
/** 卡片有 UTF-8 字节上限;按 Unicode 码点缩短摘要,并计入正文 HTML 转义后的长度。 */
private String cardText(String value, int limit, boolean description) {
String text = HtmlUtil.unescape(HtmlUtil.cleanHtmlTag(StrUtil.blankToDefault(value, "流程待办")))
.replace('\r', ' ').replace('\n', ' ');
if (text.isBlank()) text = "流程待办";
StringBuilder result = new StringBuilder();
int bytes = 0;
for (int offset = 0; offset < text.length();) {
int codePoint = text.codePointAt(offset);
String character = new String(Character.toChars(codePoint));
int size = (description ? HtmlUtil.escape(character) : character).getBytes(StandardCharsets.UTF_8).length;
// 留出省略号的三个 UTF-8 字节,避免长流程名称使整条通知发送失败。
if (bytes + size > limit - 3) return result.append("…").toString();
result.append(character);
bytes += size;
offset += Character.charCount(codePoint);
}
return result.toString();
}
/**
* 发送微信文本消息。
* 接收人来自全局消息服务传入的用户ID或登录名,方法内部转换为 Sys_user 后取工号发送;
@@ -229,14 +229,18 @@ public class ProposalMineController {
}
/**
* 删除当前用户可删除的提案。
*
* @param id 提案 ID;仅允许删除本人未发起流程或退回发起节点的提案
* @return Result,成功时返回成功结果,不满足删除条件时返回原因
*/
@At
@SaCheckPermission("proposal.mine")
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "提案管理系统-我的提案", msg = "删除提案")
public Result delete(@Valid String id) {
dao.delete(ProposalInfo.class, id);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
return Result.success();
return proposalCommonService.deleteMine(id, SecurityUtil.getUserId());
}
@At
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.democratic.proposal.service.common;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
@@ -19,6 +20,15 @@ import java.util.Set;
public interface ProposalCommonService extends BaseService<ProposalInfo> {
/**
* 删除当前用户尚未发起流程或已退回发起节点的提案。
*
* @param id 待删除的提案 ID
* @param userId 当前登录用户 ID,用于核对提案创建人
* @return Result,删除成功返回 success;提案不存在、非本人或流程不允许删除时返回错误信息
*/
Result deleteMine(String id, String userId);
/**
* 按页面允许的字段白名单添加排序条件,避免前端字段直接进入SQL。
*
@@ -17,6 +17,7 @@ import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
@@ -52,6 +53,7 @@ import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import lombok.extern.slf4j.Slf4j;
import org.ddr.poi.html.HtmlRenderPolicy;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.FieldFilter;
@@ -62,6 +64,7 @@ import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.ioc.aop.Aop;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
@@ -184,6 +187,45 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
super(dao);
}
/**
* 仅允许创建人删除草稿或退回发起节点的提案,并清理其 WF 流程。
*
* @param id 待删除的提案 ID
* @param userId 当前登录用户 ID,用于核对提案创建人
* @return Result,删除成功返回 success;提案不存在、非本人或流程不允许删除时返回错误信息
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public Result deleteMine(String id, String userId) {
if (StrUtil.isBlank(id) || StrUtil.isBlank(userId)) {
return Result.error("提案不存在或无权删除");
}
ProposalInfo proposal = dao().fetch(ProposalInfo.class, id);
if (proposal == null || !Objects.equals(proposal.getCreatedBy(), userId)) {
return Result.error("提案不存在或无权删除");
}
List<ProcessInstance> instances = dao().query(ProcessInstance.class,
Cnd.where(ProcessInstance::getBusinessNo, "=", id));
// 一个提案出现多个流程实例时无法安全判断当前节点,拒绝删除并保留数据。
if (instances.size() > 1) {
return Result.error("提案流程数据异常,无法删除");
}
if (!instances.isEmpty()) {
ProcessInstance instance = instances.get(0);
List<ProcessTask> tasks = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null);
// 存在并行任务时,只有所有活动任务均位于发起节点才允许删除。
if (!Objects.equals(instance.getState(), ProcessInstanceStateEnum.DOING.getCode())
|| tasks.isEmpty() || tasks.stream().anyMatch(task -> !Objects.equals(task.getTaskName(), "startTask"))) {
return Result.error("仅未提交或退回发起节点的提案可以删除");
}
}
dao().delete(ProposalInfo.class, id);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
return Result.success();
}
/**
* 分页查询提案综合列表。
* 普通查询不关联历史任务表;仅在 origins 包含 answer 且 commonKeyword 非空时,
@@ -101,7 +101,7 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
if (identity.equals("主任")) {
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);
}
if (sysRole == null) {
@@ -148,7 +148,7 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
if (institutionUser.getIdentity().equals("主任")) {
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);
}
if (sysRole == null) {
@@ -28,10 +28,10 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.PictureRenderData;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
@@ -47,8 +47,6 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.List;
@@ -59,6 +57,7 @@ import java.util.List;
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
@At("/platform/condolence/mine")
@Api("职工慰问我的")
@@ -157,6 +156,7 @@ public class CondolenceMineController {
@ApiOperation("导出职工慰问申请表")
@SaCheckPermission("condolence.mine")
public void doExportApply(String id, HttpServletResponse response, Boolean print) throws Exception {
log.info("[职工慰问导出] 开始,applyId={}, userId={}, format={}", id, SecurityUtil.getUserId(), Boolean.TRUE.equals(print) ? "PDF" : "Word");
Condolence condolence = condolenceService.fetch(id);
if (condolence == null) throw new BaseException("该条记录不存在");
if (!SecurityUtil.getUserId().equals(condolence.getCreatedBy())) {
@@ -167,36 +167,32 @@ public class CondolenceMineController {
if (instance == null || !ProcessInstanceStateEnum.FINISHED.getCode().equals(instance.getState())) {
throw new BaseException("只能导出已完成的申请");
}
log.info("[职工慰问导出] 流程已完成,applyId={}, instanceId={}, state={}, applicantSignPresent={}", id, instance.getId(), instance.getState(), StrUtil.isNotBlank(condolence.getSignature()));
NutMap docData = Lang.obj2nutmap(condolence);
docData.put("payUserName", StrUtil.nullToEmpty(condolence.getBankUserName()));
CondolenceType type = condolenceService.dao().fetch(CondolenceType.class, condolence.getType());
docData.put("typeName", type == null ? "" : type.getName());
docData.put("signature", "");
docData.put("signature", null);
if (StrUtil.isNotBlank(condolence.getSignature())) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, condolence.getSignature());
if (signature == null) throw new BaseException("申请人签字图片读取失败");
docData.put("signature", signature);
}
fillApplyApprovals(docData, instance);
for (String field : List.of("signature", "firstApprovalSign", "secondApprovalSign", "thirdApprovalSign", "fourthApprovalSign")) {
Object value = docData.get(field);
log.info("[职工慰问导出] 渲染前图片字段,applyId={}, instanceId={}, field={}, valueType={}", id, instance.getId(), field, value == null ? "null" : value.getClass().getName());
}
String fileName = "重庆工贸职业技术学院职工慰问申请表_" + StrUtil.nullToEmpty(condolence.getHelpUserName());
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc");
ByteArrayOutputStream templateOutput = new ByteArrayOutputStream();
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc")) {
if (input == null) throw new BaseException("职工慰问申请表模板不存在");
com.aspose.words.Document document = new com.aspose.words.Document(input);
document.save(templateOutput, com.aspose.words.SaveFormat.DOCX);
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateOutput.toByteArray())) {
XWPFTemplate.compile(templateInput).render(docData).writeAndClose(output);
}
if (Boolean.TRUE.equals(print)) {
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(output.toByteArray());
ByteArrayOutputStream pdfOutput = new ByteArrayOutputStream()) {
new com.aspose.words.Document(wordInput).save(pdfOutput, com.aspose.words.SaveFormat.PDF);
CommonDownloadUtil.download(fileName + ".pdf", pdfOutput.toByteArray(), response);
}
} else {
CommonDownloadUtil.download(fileName + ".docx", output.toByteArray(), response);
}
boolean pdf = Boolean.TRUE.equals(print);
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, null, pdf, "condolence:" + id);
log.info("[职工慰问导出] 渲染完成,applyId={}, format={}, outputBytes={}", id, pdf ? "PDF" : "Word", bytes.length);
CommonDownloadUtil.download(fileName + (pdf ? ".pdf" : ".docx"), bytes, response);
} catch (Exception e) {
log.error("[职工慰问导出] 模板渲染或文件输出失败,applyId={}, instanceId={}, format={}", id, instance.getId(), Boolean.TRUE.equals(print) ? "PDF" : "Word", e);
throw e;
}
}
@@ -206,29 +202,39 @@ public class CondolenceMineController {
String[] fields = {"firstApproval", "secondApproval", "thirdApproval", "fourthApproval"};
List<String> suffixes = List.of("Opinion", "Sign", "Year", "Month", "Day");
for (String field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, "");
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
List<ProcessTask> tasks = condolenceService.dao().query(ProcessTask.class,
Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
log.info("[职工慰问导出] 查询审批记录,applyId={}, instanceId={}, taskCount={}, expectedTaskKeys={}", instance.getBusinessNo(), instance.getId(), tasks.size(), List.of(taskKeys));
for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
if ("startTask".equals(task.getTaskName()) || (taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
log.info("[职工慰问导出] 审批记录,instanceId={}, taskId={}, taskKey={}, displayName={}, state={}, finishTime={}, isFirstTaskNode={}, matchedApproval={}", instance.getId(), task.getId(), task.getTaskName(), task.getDisplayName(), task.getTaskState(), task.getFinishTime(), taskVO.getExt() == null ? null : taskVO.getExt().get("isFirstTaskNode"), List.of(taskKeys).contains(task.getTaskName()));
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
log.info("[职工慰问导出] 申请节点重置本轮审批字段,instanceId={}, taskId={}, taskKey={}", instance.getId(), task.getId(), task.getTaskName());
// 重新申请后,仅导出本轮审批记录。
for (String field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, "");
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
continue;
}
for (int i = 0; i < taskKeys.length; i++) {
if (!taskKeys[i].equals(task.getTaskName())) continue;
for (String suffix : suffixes) docData.put(fields[i] + suffix, "");
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) continue;
for (String suffix : suffixes) docData.put(fields[i] + suffix, "Sign".equals(suffix) ? null : "");
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) {
log.info("[职工慰问导出] 跳过未完成审批,instanceId={}, taskId={}, field={}, state={}", instance.getId(), task.getId(), fields[i], task.getTaskState());
continue;
}
Dict data = taskVO.getExt() == null ? Dict.create() : taskVO.getTaskFormData();
log.info("[职工慰问导出] 读取审批表单,instanceId={}, taskId={}, field={}, formKeys={}, signPresent={}, signReferenceLength={}", instance.getId(), task.getId(), fields[i], data.keySet(), StrUtil.isNotBlank(data.getStr("sign")), StrUtil.nullToEmpty(data.getStr("sign")).length());
if (StrUtil.isBlank(data.getStr("sign"))) log.warn("[职工慰问导出] 已完成审批缺少签名字段,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i]);
docData.put(fields[i] + "Opinion", StrUtil.nullToEmpty(data.getStr("opinion")));
if (StrUtil.isNotBlank(data.getStr("sign"))) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign"));
if (signature == null) throw new BaseException("审批签字图片读取失败");
docData.put(fields[i] + "Sign", signature);
log.info("[职工慰问导出] 审批签名填充完成,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i] + "Sign");
}
docData.put(fields[i] + "Year", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy"));
docData.put(fields[i] + "Month", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "M"));
@@ -9,7 +9,6 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
@@ -18,7 +17,6 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.MoneyUtil;
import com.budwk.app.base.utils.OfficePlusUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
@@ -39,7 +37,6 @@ import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommon
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo;
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
@@ -74,8 +71,6 @@ import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@@ -319,9 +314,9 @@ public class DifficultHelpReadingController {
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder().bind("familyList", policy).build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("difficulty_apply"), config)
.render(docData)
.write(zipOutputStream);
try (InputStream input = sysOfficeTemplateUtil.getTemplate("difficulty_apply")) {
zipOutputStream.write(sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:batch:" + record.getString("id")));
}
zipOutputStream.closeEntry();
}
zipOutputStream.flush();
@@ -379,17 +374,18 @@ public class DifficultHelpReadingController {
String[] taskKeys = {"36ebafda-44c2-4bd3-bd7f-10839a575a0d", "715ece82-4ae3-433c-ad18-ed875f45d1e3", "e405f96c-5fc0-4f63-b323-e69dfe853253"};
String[] fields = {"firstApproval", "secondApproval", "thirdApproval"};
for (String field : fields) {
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, Cnd.where("businessNo", "=", businessId).desc("id"));
if (instance == null) return;
List<ProcessTask> tasks = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
if ("startTask".equals(task.getTaskName()) || (taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
// 重新申请后,仅导出本轮审批,避免混入上一次退回或撤销前的意见。
for (String field : fields) {
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "");
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
continue;
}
@@ -453,22 +449,9 @@ public class DifficultHelpReadingController {
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
XWPFTemplate.compile(getApplyTemplate(templateName),config)
.render(docData)
.writeAndClose(byteArrayOutputStream);
byte[] bytes = byteArrayOutputStream.toByteArray();
File sourceFile = File.createTempFile("file_convert_origin", ".docx");
Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE);
File pdfFile = File.createTempFile("file_convert", ".pdf");
OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath());
CommonDownloadUtil.download(fileName, IoUtil.readBytes(FileUtil.getInputStream(pdfFile)), response);
// 删除临时文件
FileUtil.del(sourceFile);
FileUtil.del(pdfFile.toPath());
} catch (IOException e) {
log.error("困难慰问申请表导出PDF失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
try (InputStream input = getApplyTemplate(templateName)) {
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, true, "difficulthelp:" + difficultHelp.getId());
CommonDownloadUtil.download(fileName, bytes, response);
}
}
@@ -478,13 +461,9 @@ public class DifficultHelpReadingController {
String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
XWPFTemplate.compile(getApplyTemplate(templateName),config)
.render(docData)
.writeAndClose(byteArrayOutputStream);
CommonDownloadUtil.download(fileName, byteArrayOutputStream.toByteArray(), response);
} catch (IOException e) {
log.error("困难慰问申请表导出Word失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
try (InputStream input = getApplyTemplate(templateName)) {
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:" + difficultHelp.getId());
CommonDownloadUtil.download(fileName, bytes, response);
}
}
@@ -562,15 +541,13 @@ public class DifficultHelpReadingController {
throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage());
}
} else if ("docx".equals(flag)) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
try (InputStream input = sysOfficeTemplateUtil.getTemplate(resource)) {
Configure config = Configure.builder()
.bind("da", new LoopRowTableRenderPolicy())
.build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(resource), config)
.render(result)
.writeAndClose(out);
CommonDownloadUtil.download(fileName + ".docx", out.toByteArray(), response);
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, result, config, false, "difficulthelp:summary:" + resource);
CommonDownloadUtil.download(fileName + ".docx", bytes, response);
}
}
}
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.staffbenefit.difficulthelp.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
@@ -21,6 +22,14 @@ public interface DifficultHelpCommonService extends BaseService<DifficultHelpInf
void validateApplyUser(DifficultHelpInfo difficultHelpInfo);
/**
* 困难帮扶进入校工会办公室主任审批时,通知本次任务的实际参与人。
* @param task WF 已创建且参与人已落库的任务;仅 KNBF 办公室审批待办触发,
* 接收人使用任务参与人的 actorAccount(系统 loginname)
* @return 无返回值;开关关闭或非目标节点不发送,发送失败记录日志而不阻断流程
*/
void notifyOfficeApproval(ProcessTask task);
/**
* 导入实际金额数据处理
* @param file 文件
@@ -6,6 +6,12 @@ import cn.hutool.json.JSONUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.wxwork.WxWorkMessageService;
import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.base.utils.ManyAddOrRenewUtil;
@@ -31,6 +37,8 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.upload.TempFile;
import java.util.*;
@@ -46,6 +54,8 @@ import java.util.stream.IntStream;
*/
@IocBean(args = {"refer:dao"})
public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHelpInfo> implements DifficultHelpCommonService {
private static final Log log = Logs.get();
private static final String OFFICE_APPROVAL_TASK = "36ebafda-44c2-4bd3-bd7f-10839a575a0d";
public DifficultHelpCommonServiceImpl(Dao dao) {
super(dao);
@@ -53,6 +63,55 @@ public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHel
@Inject
private ManyAddOrRenewUtil manyAddOrRenewUtil;
@Inject
private WxWorkMessageService wxWorkMessageService;
/**
* 只通知 KNBF 办公室待办的实际审核人;重新提交生成的新任务沿用同一入口。
* task 为任务开始事件对应的 WF 任务,actorAccount 为学校企业微信 userid。
* 无返回值;服务返回 skipped 表示开关关闭,success=false 表示接口失败或部分接收人无效。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void notifyOfficeApproval(ProcessTask task) {
if (task == null || task.getId() == null || task.getProcessInstanceId() == null
|| !OFFICE_APPROVAL_TASK.equals(task.getTaskName())
|| !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) {
return;
}
try {
ProcessInstance instance = dao().fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null || instance.getProcessDefineId() == null) return;
ProcessDefine define = dao().fetch(ProcessDefine.class, instance.getProcessDefineId());
// 使用实例所属定义核验流程 KEY,避免其他流程复用同一个节点 ID 时误发。
if (define == null || !"KNBF".equals(define.getName())) return;
DifficultHelpInfo info = StrUtil.isBlank(instance.getBusinessNo()) ? null : fetch(instance.getBusinessNo());
if (info == null) {
log.warnf("困难帮扶企业微信通知跳过:申请不存在,taskId=%s", task.getId());
return;
}
List<ProcessTaskActor> actors = dao().query(ProcessTaskActor.class,
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
List<String> loginNames = actors.stream().map(ProcessTaskActor::getActorAccount)
.filter(StrUtil::isNotBlank).map(String::trim).distinct().toList();
// 不按角色扩大接收范围,未绑定工号时不以内部用户 ID 代替企业微信 userid。
if (loginNames.isEmpty()) {
log.warnf("困难帮扶企业微信通知跳过:审核人 loginname 为空,taskId=%s", task.getId());
return;
}
String content = "您有一条困难帮扶申请待审核,受助人:"
+ StrUtil.blankToDefault(info.getUserName(), "未填写") + ",请进入校工会办公室审批页面办理。";
// 流程定义中仍保留旧的 unionLeaderApproval 链接,本节点实际办理入口为 branchUnionApproval。
NutMap result = wxWorkMessageService.massSend(loginNames, "困难帮扶待办提醒", content,
"/platform/difficultHelp/branchUnionApproval/h5");
if (!result.getBoolean("skipped", false) && !result.getBoolean("success", false)) {
log.warnf("困难帮扶企业微信通知发送失败或部分失败,taskId=%s,result=%s", task.getId(), result);
}
} catch (Exception e) {
// 外部消息不改变审批结果;日志保留任务 ID,便于定位接口、账号或配置问题。
log.error("困难帮扶企业微信通知异常,taskId=" + task.getId(), e);
}
}
@Override
public void validateApplyUser(DifficultHelpInfo difficultHelpInfo) {
+3 -3
View File
@@ -5,7 +5,7 @@ nutz:
ignore: ^(.+[.])(jsp|png|gif|jpg|js|css|jspx|jpeg|html|mp3|mp4|ico|svg|vue)$
exclusions: /favicon/*,/assets/*,/druid/*,/upload/*,/components/*
server:
port: 8081
port: 80
host: 0.0.0.0
jetty:
@@ -21,7 +21,7 @@ jetty:
#结合ftp使用,或用nginx代理ftp路径
#staticPath: /Users/wizzer/temp/files
#开发模式静态资源
staticPathLocal: D:/myProject/zhgh_cqgmy/src/main/resources/static
staticPathLocal: E:/projects/zhgh_cqgmy/src/main/resources/static
security:
tokenName: saToken
@@ -87,7 +87,7 @@ xsssql:
beetl:
RESOURCE:
#本地路径,开发时设置本地路径,便于调试
rootLocal: D:/myProject/zhgh_cqgmy/src/main/resources/views/
rootLocal: E:/projects/zhgh_cqgmy/src/main/resources/views/
root: views/
DELIMITER_STATEMENT_START: "<!--#"
DELIMITER_STATEMENT_END: "#-->"
@@ -1,207 +0,0 @@
/* 首页服务门户布局单独限定作用域,避免影响应用中心和其他业务页。 */
#v4-home-app.home-portal {
width: 100%;
max-width: none;
margin: 0;
color: #20344d;
background: #f2f6fb;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
font-size: 14px;
}
#v4-home-app.home-portal * { box-sizing: border-box; }
#v4-home-app .portal-top { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 18px; max-width: 1720px; margin: auto; padding: 18px 24px 0; }
/* 使用完整不透明背景铺满横幅,会员信息独立成卡,不再覆盖校园景观。 */
#v4-home-app .portal-hero { position: relative; min-width: 0; min-height: 260px; overflow: hidden; border-radius: 12px; background: #e8f4ff; }
#v4-home-app .portal-hero::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, #e8f4ff 0%, #e8f4ff 28%, rgba(232,244,255,.92) 39%, rgba(232,244,255,.35) 54%, rgba(232,244,255,0) 68%); }
#v4-home-app .portal-hero-inner { position: relative; z-index: 1; display: flex; align-items: center; min-height: 260px; height: 100%; padding: 32px 40px; }
#v4-home-app .portal-hero-copy { max-width: 80%; }
#v4-home-app .portal-hero-eyebrow { margin-bottom: 12px; color: #527ca1; font-size: 14px; line-height: 22px; letter-spacing: 2px; }
#v4-home-app .portal-hero-copy h1 { margin: 0 0 10px; font-size: 36px; line-height: 1.3; font-weight: 700; color: #083765; }
#v4-home-app .portal-hero-copy p { margin: 0; color: #335675; font-size: 17px; line-height: 1.8; }
#v4-home-app .portal-values { display: flex; flex-wrap: wrap; gap: 22px 36px; margin-top: 24px; }
#v4-home-app .portal-values span { display: inline-flex; align-items: center; gap: 12px; }
#v4-home-app .portal-values i { color: #3599f2; font-size: 23px; }
/* 首次渲染时分层淡入文字,只改变透明度和位移,不影响卡片布局。 */
@keyframes portal-text-enter {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
#v4-home-app .portal-hero-eyebrow,
#v4-home-app .portal-hero-copy h1,
#v4-home-app .portal-hero-copy p,
#v4-home-app .portal-values > span,
#v4-home-app .portal-member-date,
#v4-home-app .portal-member-heading,
#v4-home-app .portal-member .meta-row { animation: portal-text-enter .6s cubic-bezier(.22, 1, .36, 1) backwards; }
#v4-home-app .portal-hero-copy h1 { animation-delay: .08s; }
#v4-home-app .portal-hero-copy p { animation-delay: .16s; }
#v4-home-app .portal-values > span:nth-child(1) { animation-delay: .24s; }
#v4-home-app .portal-values > span:nth-child(2) { animation-delay: .30s; }
#v4-home-app .portal-values > span:nth-child(3) { animation-delay: .36s; }
#v4-home-app .portal-values > span:nth-child(4) { animation-delay: .42s; }
#v4-home-app .portal-member-date { animation-delay: .10s; }
#v4-home-app .portal-member-heading { animation-delay: .18s; }
#v4-home-app .portal-member .meta-row:nth-child(1) { animation-delay: .26s; }
#v4-home-app .portal-member .meta-row:nth-child(2) { animation-delay: .34s; }
#v4-home-app .portal-member .meta-row:nth-child(3) { animation-delay: .42s; }
@media (prefers-reduced-motion: reduce) {
#v4-home-app .portal-hero-eyebrow,
#v4-home-app .portal-hero-copy h1,
#v4-home-app .portal-hero-copy p,
#v4-home-app .portal-values > span,
#v4-home-app .portal-member-date,
#v4-home-app .portal-member-heading,
#v4-home-app .portal-member .meta-row { animation: none; }
}
#v4-home-app .portal-content { max-width: 1720px; margin: auto; padding: 18px 24px 28px; }
/* 同行共用网格行高,统一横纵间距;待办使用紧凑两行信息,避免撑出大块留白。 */
#v4-home-app .portal-columns { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 18px; margin-top: 18px; align-items: stretch; }
#v4-home-app .portal-columns > * { min-width: 0; }
#v4-home-app .portal-news-wide { grid-column: 1 / -1; }
#v4-home-app .portal-panel,
#v4-home-app .entry-wrapper,
#v4-home-app .todo-center,
#v4-home-app .work-template-wrapper,
#v4-home-app .jcdt-container { background: #fff; border: 0; border-radius: 12px; padding: 20px; box-shadow: 0 3px 14px rgba(30, 66, 108, .035); }
#v4-home-app .home-section-header,
#v4-home-app .portal-todo-heading,
#v4-home-app .work-template-header,
#v4-home-app .portal-news-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; min-height: 28px; margin: 0 0 16px; }
#v4-home-app .home-section-title,
#v4-home-app .work-template-title,
#v4-home-app .portal-news-title,
#v4-home-app .portal-todo-heading h3 { display: inline-flex; align-items: center; gap: 9px; margin: 0; padding: 0; border: 0; font-size: 20px; line-height: 1.4; font-weight: 600; color: #123356; }
#v4-home-app .home-section-title i,
#v4-home-app .work-template-title i,
#v4-home-app .portal-news-title > i:first-child,
#v4-home-app .portal-todo-heading i { color: #1687f5; font-size: 19px; }
#v4-home-app .portal-more { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; border: 0; padding: 4px 0; background: transparent; color: #1687f5; font: inherit; font-size: 13px; cursor: pointer; text-decoration: none; }
#v4-home-app .portal-more:hover { color: #0565c6; }
#v4-home-app button:focus-visible,
#v4-home-app a:focus-visible,
#v4-home-app [tabindex]:focus-visible { outline: 2px solid #1687f5; outline-offset: 3px; }
#v4-home-app .entry-card { min-width: 0; }
#v4-home-app .entry-wrapper { padding-bottom: 0; }
#v4-home-app .portal-entry-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid #e9eff6; }
#v4-home-app .portal-entry-tabs { display: flex; gap: 34px; }
#v4-home-app .portal-entry-tabs button { border: 0; border-bottom: 3px solid transparent; padding: 0 0 13px; background: transparent; font: inherit; font-size: 19px; color: #465d78; font-weight: 600; cursor: pointer; }
#v4-home-app .portal-entry-tabs button.active { color: #0784fa; border-bottom-color: #0784fa; }
#v4-home-app .entry-section + .entry-section { margin-top: 0; }
#v4-home-app .app-grid { display: flex; justify-content: flex-start; align-items: center; gap: 12px; min-height: 0; overflow-x: auto; padding: 20px 0; border: 0; border-radius: 0; box-shadow: none; }
#v4-home-app .app-item { flex: 0 0 96px; border: 0; font: inherit; padding: 0 4px; max-width: none; }
#v4-home-app .app-item:hover { background: #f4f9ff; box-shadow: none; transform: translateY(-2px); }
#v4-home-app .app-icon { background: transparent; }
#v4-home-app .app-name { display: block; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; line-height: 20px; min-height: 0; margin-top: 2px; }
#v4-home-app .app-empty { padding: 10px 0; }
#v4-home-app .act-card { padding: 0; border: 0; border-radius: 0; box-shadow: none; }
#v4-home-app .activity-swiper-container { padding: 0 0 4px; }
#v4-home-app .activity-swiper .swiper-slide .item { display: flex; flex-direction: column; height: 100%; }
/* 加高封面并统一裁剪容器,图片、加载态和占位图都保持相同圆角。 */
#v4-home-app .activity-swiper .swiper-slide .item .img-box { flex-shrink: 0; height: auto; aspect-ratio: 1.5; border-radius: 10px; overflow: hidden; clip-path: inset(0 round 10px); background: #1687f5; isolation: isolate; }
#v4-home-app .img-box .el-image { display: block; width: 100%; height: 100%; position: absolute; inset: 0; border-radius: inherit; overflow: hidden; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box img { height: 100% !important; max-height: none !important; object-fit: cover !important; border-radius: 10px; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status { top: 8px; left: 8px; border-radius: 5px; padding: 4px 7px; background: #ed4859; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-ended { background: #8b97a7; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-upcoming { background: #1687f5; }
#v4-home-app .activity-swiper .swiper-slide .item h4 { flex: 1; margin: 10px 0 8px; height: auto; min-height: 22px; max-height: 44px; font-size: 15px; color: #20344d; line-height: 22px; }
#v4-home-app .activity-swiper .swiper-slide .item .time p { white-space: normal; font-size: 12px; color: #7b899d; line-height: 1.6; }
#v4-home-app .activity-swiper-container .swiper-button-next,
#v4-home-app .activity-swiper-container .swiper-button-prev { width: 44px; height: 44px; margin-top: 0; padding: 0; transform: translateY(-50%); border: 1px solid #dbeaff; background: rgba(255,255,255,.96); color: #1687f5; box-shadow: 0 3px 12px rgba(30, 66, 108, .18); }
#v4-home-app .activity-swiper-container .swiper-button-next { right: 8px; }
#v4-home-app .activity-swiper-container .swiper-button-prev { left: 8px; }
#v4-home-app .activity-swiper-container .swiper-button-next:hover,
#v4-home-app .activity-swiper-container .swiper-button-prev:hover { transform: translateY(-50%) scale(1.05); background: #1687f5; color: #fff; }
#v4-home-app .activity-swiper-container .swiper-button-next::after,
#v4-home-app .activity-swiper-container .swiper-button-prev::after { font-size: 18px; font-weight: 600; }
#v4-home-app .portal-cover-empty { height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 9px; color: #94a7bd; background: #f0f5fb; }
#v4-home-app .portal-cover-empty i { font-size: 28px; }
#v4-home-app .user-wrapper { background: transparent; }
#v4-home-app .user-container { display: block; width: 100%; height: 100%; margin: 0; }
#v4-home-app .todo-center { display: flex; flex-direction: column; min-width: 0; height: 100%; }
#v4-home-app .todo-center .header { display: block; margin: 0; }
#v4-home-app .todo-center .el-tabs { margin: 0 0 8px; }
#v4-home-app .todo-center .el-tabs__header { margin: 0; }
#v4-home-app .todo-center .el-tabs__nav { display: flex; width: 100%; border-color: #e9eff6; border-radius: 6px 6px 0 0; }
#v4-home-app .todo-center .el-tabs__item { flex: 1; min-width: 0; padding: 0 7px; font-size: 12px; height: 36px; line-height: 36px; text-align: center; }
#v4-home-app .todo-center .el-tabs__item.is-active { border-bottom: 2px solid #1687f5; color: #1687f5; }
#v4-home-app .portal-task-list { display: flex; flex-direction: column; justify-content: space-evenly; flex: 1; min-height: 180px; }
#v4-home-app .portal-task-row { display: flex; gap: 10px; align-items: center; padding: 8px 6px; border-bottom: 1px solid #eef3f8; }
#v4-home-app .portal-task-row:last-child { border-bottom: 0; }
#v4-home-app .portal-task-row:hover { background: #f7fbff; }
#v4-home-app .portal-task-icon { width: 34px; height: 40px; display: flex; flex: 0 0 34px; align-items: center; justify-content: center; border-radius: 8px; background: #eef7ff; color: #1687f5; font-size: 22px; }
#v4-home-app .portal-task-content { min-width: 0; flex: 1; }
#v4-home-app .portal-task-title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #263548; font-weight: 600; font-size: 14px; line-height: 22px; }
#v4-home-app .portal-task-meta { display: flex; align-items: baseline; gap: 10px; font-size: 12px; color: #8390a2; line-height: 21px; }
#v4-home-app .portal-task-meta span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#v4-home-app .portal-task-meta > span:first-child { max-width: 45%; flex-shrink: 0; }
#v4-home-app .portal-task-date { flex-shrink: 0; font-size: 12px; color: #7b899d; }
#v4-home-app .portal-task-node { flex: 1; min-width: 0; color: #8b98a9; font-size: 11px; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#v4-home-app .portal-task-row .el-button { flex-shrink: 0; padding: 8px 14px; }
#v4-home-app .portal-task-pagination { text-align: right; margin-top: 8px; }
#v4-home-app .portal-empty { min-height: 132px; display: flex; align-items: center; justify-content: center; gap: 10px; color: #8a9aac; }
#v4-home-app .portal-empty i { font-size: 26px; color: #83b9ee; }
#v4-home-app .work-template-wrapper { width: 100%; margin: 0; }
#v4-home-app .work-template-card { min-height: 98px; padding: 0; border: 0; border-radius: 0; box-shadow: none; }
#v4-home-app .work-template-grid { display: flex; align-items: flex-start; gap: 14px; overflow-x: auto; padding: 4px 0 10px; scroll-snap-type: x proximity; }
#v4-home-app .work-template-item { flex: 0 0 88px; width: 88px; padding: 4px; border: 0; font: inherit; scroll-snap-align: start; }
#v4-home-app .work-template-item:hover { background: #f4f9ff; box-shadow: none; }
#v4-home-app .work-template-icon,
#v4-home-app .work-template-icon .el-image { width: 48px; height: 48px; }
#v4-home-app .work-template-icon img { width: 48px !important; height: 48px !important; object-fit: contain; }
#v4-home-app .work-template-icon i { font-size: 30px; line-height: 48px; color: #83b9ee; }
#v4-home-app .work-template-name { font-size: 13px !important; line-height: 20px; min-height: 40px; max-height: 40px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
#v4-home-app .work-template-wrapper .no-activity-placeholder { padding: 12px; }
#v4-home-app .jcdt-container { width: 100%; max-width: none; height: 100%; margin: 0; }
#v4-home-app .portal-news-title { background: transparent; cursor: pointer; font-family: inherit; }
#v4-home-app .portal-news-title > i:last-child:not(:first-child) { color: #8092a6; font-size: 14px; }
#v4-home-app .portal-news-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 9px 0; border: 0; background: transparent; color: #42536a; font: inherit; text-align: left; cursor: pointer; }
#v4-home-app .portal-news-row time { flex-shrink: 0; padding: 3px 7px; background: #eff5fc; border-radius: 4px; color: #45698f; font-size: 13px; }
#v4-home-app .portal-news-row span { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#v4-home-app .portal-news-row:hover span { color: #1687f5; }
@media (min-width: 1600px) {
#v4-home-app .portal-hero-copy h1 { font-size: 42px; }
#v4-home-app .app-item { flex-basis: 104px; }
}
@media (max-width: 1199px) {
#v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr) 300px; padding: 18px 18px 0; }
#v4-home-app .portal-hero-inner { padding-left: 32px; }
#v4-home-app .portal-content { padding: 18px 18px 24px; }
#v4-home-app .portal-columns { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); }
#v4-home-app .portal-values { gap: 16px; }
#v4-home-app .portal-panel, #v4-home-app .todo-center { padding: 16px; }
#v4-home-app .portal-task-row { gap: 8px; padding: 8px 0; }
#v4-home-app .portal-task-icon { display: none; }
}
@media (max-width: 900px) {
#v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr); }
#v4-home-app .portal-columns { grid-template-columns: minmax(0, 1fr); }
#v4-home-app .portal-activities { order: 1; }
#v4-home-app .portal-todo { order: 2; }
#v4-home-app .portal-templates { order: 3; }
#v4-home-app .portal-news { order: 4; }
#v4-home-app .portal-hero-copy { max-width: 68%; }
#v4-home-app .portal-hero-copy h1 { font-size: 30px; }
#v4-home-app .portal-hero-copy p { font-size: 14px; }
#v4-home-app .portal-task-icon { display: flex; }
#v4-home-app .portal-task-meta { flex-wrap: nowrap; }
}
@media (max-width: 600px) {
#v4-home-app .portal-top { padding: 10px 10px 8px; }
#v4-home-app .portal-hero-inner { padding: 24px 20px; }
#v4-home-app .portal-hero-copy { max-width: 100%; }
#v4-home-app .portal-hero-copy h1 { font-size: 28px; }
#v4-home-app .portal-values { gap: 12px 18px; font-size: 12px; }
#v4-home-app .portal-values i { font-size: 17px; }
#v4-home-app .portal-content { padding: 10px; }
#v4-home-app .entry-wrapper { padding: 16px 12px 0; }
#v4-home-app .portal-entry-tabs { gap: 20px; }
#v4-home-app .portal-entry-tabs button { font-size: 16px; }
#v4-home-app .app-item { flex-basis: 84px; }
#v4-home-app .home-section-title, #v4-home-app .portal-todo-heading h3, #v4-home-app .work-template-title, #v4-home-app .portal-news-title { font-size: 18px; }
#v4-home-app .portal-task-row .el-button { padding: 8px 10px; }
}
/* 使用参考项目本地横幅图片,文字遮罩保持首页的可读性。 */
#v4-home-app .portal-hero-background { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: right bottom; }
/* 已展示全部活动时明确禁用入口,避免出现可点击但没有内容切换的按钮。 */
#v4-home-app .portal-more:disabled { color: #8a9aac; cursor: default; }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

@@ -10,7 +10,7 @@
:finished="tableFinished"
finished-text="没有更多了"
@load="onLoad">
<div class="table-list-container">
<div class="table-list-container" @click="showOverflowText">
<div v-for="(row, index) in tableData" :key="index" class="table-list-item"
:data-row-id="row && row.id ? row.id : ''">
<slot name="header" :index="index" :row="row">
@@ -36,6 +36,12 @@
</div>
</div>
</van-list>
<van-popover ref="overflowPopover" :value="overflowVisible" :style="overflowAnchorStyle"
:placement="overflowPlacement" get-container="body" @input="onOverflowInput"
@opened="positionOverflow">
<div class="table-list-overflow-content">{{ overflowText }}</div>
<template #reference><span></span></template>
</van-popover>
</van-pull-refresh>
</template>
<script>
@@ -90,10 +96,138 @@ module.exports = {
tableLoading: false,
tableFinished: false,
tableRefreshing: false,
localPageForm: {...this.page_form}
localPageForm: {...this.page_form},
overflowVisible: false,
overflowText: "",
overflowTarget: null,
overflowPlacement: "top",
overflowAnchorStyle: {}
}
},
methods: {
// 只响应真正被截断的纯文字,保留卡片内按钮和业务插槽原有的点击行为。
showOverflowText(event) {
const target = event.target
if (!target || !target.closest) return
const card = target.closest(".table-list-item")
if (!card || target.closest(".item-actions, a, button, input, textarea, select, [role=button], [tabindex], [onclick]")) return
if (target.children.length || !target.textContent.trim() || this.hasOverflowClickHandler(target, card)) return
const style = window.getComputedStyle(target)
if (style.textOverflow !== "ellipsis" || (target.scrollWidth <= target.clientWidth && target.scrollHeight <= target.clientHeight)) return
const rect = target.getBoundingClientRect()
this.overflowTarget = target
this.overflowText = target.textContent.trim()
this.overflowPlacement = rect.top < 180 ? "bottom" : "top"
this.overflowAnchorStyle = {
position: "fixed",
display: "block",
pointerEvents: "none",
left: rect.left + "px",
top: rect.top + "px",
width: rect.width + "px",
height: rect.height + "px"
}
if (!this.overflowVisible) this.bindOverflowListeners()
this.overflowVisible = true
this.$nextTick(() => {
const instance = this.$refs.overflowPopover
if (instance) instance.updateLocation()
setTimeout(() => this.positionOverflow(), 50)
setTimeout(() => this.positionOverflow(), 350)
})
},
// 检查文字及其卡片内的祖先节点,避免干扰已有 Vue 点击事件。
hasOverflowClickHandler(target, card) {
const ancestors = []
let element = target
while (element && element !== card.parentElement) {
ancestors.push(element)
element = element.parentElement
}
const visit = (node) => {
if (!node) return false
const events = node.data ? [node.data.on, node.data.nativeOn] : []
if (ancestors.includes(node.elm) && events.some((handlers) => handlers &&
Object.keys(handlers).some((key) => /^[~!&]*(click|touchstart|pointerdown|mousedown)$/.test(key)))) return true
if (node.componentInstance && visit(node.componentInstance._vnode)) return true
return Array.isArray(node.children) && node.children.some((child) => visit(child))
}
return visit(this._vnode)
},
// Vant 的弹层挂在 body;校正窗口边缘的位置,避免完整文字被屏幕裁掉。
positionOverflow() {
const instance = this.$refs.overflowPopover
const popup = instance && instance.$refs.popover && instance.$refs.popover.$el
if (!this.overflowVisible || !this.overflowTarget || !popup) return
const rect = popup.getBoundingClientRect()
const anchor = this.overflowTarget.getBoundingClientRect()
if (!rect.width || !rect.height) return
const margin = 8
const left = Math.max(margin, Math.min(rect.left, window.innerWidth - rect.width - margin))
let top = rect.top
if (top < margin) top = anchor.bottom + 8
if (top + rect.height > window.innerHeight - margin) top = anchor.top - rect.height - 8
top = Math.max(margin, Math.min(top, window.innerHeight - rect.height - margin))
popup.style.left = (parseFloat(popup.style.left) || 0) + left - rect.left + "px"
popup.style.top = (parseFloat(popup.style.top) || 0) + top - rect.top + "px"
const arrow = popup.querySelector(".van-popover__arrow")
if (arrow) {
arrow.style.left = Math.max(12, Math.min(anchor.left + anchor.width / 2 - left, rect.width - 12)) + "px"
}
},
onOverflowInput(visible) {
this.overflowVisible = visible
if (!visible) {
this.overflowTarget = null
this.unbindOverflowListeners()
}
},
// 监听只在气泡打开时存在,PJAX 替换页面前也会关闭气泡并注销。
bindOverflowListeners() {
document.addEventListener("click", this.closeOverflowOnClick)
window.addEventListener("scroll", this.closeOverflowOnMove, true)
window.addEventListener("resize", this.closeOverflowOnMove)
window.addEventListener("transitionend", this.onOverflowAnimationEnd)
window.addEventListener("animationend", this.onOverflowAnimationEnd)
$(document).on("pjax:send", this.closeOverflowForNavigation)
},
unbindOverflowListeners() {
document.removeEventListener("click", this.closeOverflowOnClick)
window.removeEventListener("scroll", this.closeOverflowOnMove, true)
window.removeEventListener("resize", this.closeOverflowOnMove)
window.removeEventListener("transitionend", this.onOverflowAnimationEnd)
window.removeEventListener("animationend", this.onOverflowAnimationEnd)
$(document).off("pjax:send", this.closeOverflowForNavigation)
},
closeOverflowForNavigation() {
this.onOverflowInput(false)
},
// Vant 会在动画结束时重新计算定位,随后再次限制气泡的屏幕边界。
onOverflowAnimationEnd() {
if (this.overflowVisible) setTimeout(() => this.positionOverflow(), 0)
},
// 滚动或改变屏幕尺寸时关闭气泡,避免气泡脱离原文字位置。
closeOverflowOnMove(event) {
if (event && event.target && event.target.closest && event.target.closest(".table-list-overflow-content")) return
this.onOverflowInput(false)
},
closeOverflowOnClick(event) {
if (!this.overflowVisible) return
if (this.overflowTarget && this.overflowTarget.contains(event.target)) return
if (event.target.closest && event.target.closest(".table-list-overflow-content")) return
this.onOverflowInput(false)
},
/**
* 获取当前列表快照。
* 返回值包含已加载记录、分页参数和加载完成状态,供业务页面在进入详情前暂存列表现场。
@@ -186,10 +320,28 @@ module.exports = {
},
mounted() {
this.$emit("ready")
},
beforeDestroy() {
this.unbindOverflowListeners()
}
}
</script>
<style>
/* Vant 将气泡挂到 body,控制宽高并允许长文本在气泡内滚动。 */
.table-list-overflow-content {
max-width: min(240px, calc(100vw - 48px));
max-height: 160px;
padding: 10px 12px;
overflow-y: auto;
color: #253247;
font-size: 13px;
line-height: 1.55;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
</style>
<style scoped>
.table-list-container {
padding: 0;
@@ -52,8 +52,6 @@ module.exports = {
*/
init() {
this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id
// 打印当前二维码的签字入口,便于从控制台复制地址进行签字测试。
console.info("signatureAddress:", this.signatureAddress)
return this.$axios.post("/platform/signature/get")
.then((res) => {
if (this._isBeingDestroyed || this._isDestroyed) {
@@ -9,6 +9,7 @@
progress
:accept="accept"
:max-count="upload_number"
:upload-text="upload_text"
title=""
description=""
tip=""
@@ -50,6 +51,11 @@ module.exports = {
default: 1,
required: false
},
upload_text: {
type: String,
default: "",
required: false
},
// 上传返回id或url
upload_result_type: {
type: String,
@@ -101,6 +107,7 @@ module.exports = {
this.fileList = val.map((v) => {
return {
...v,
file: v.file || { name: v.name },
status: null,
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase())
}
@@ -118,6 +125,7 @@ module.exports = {
this.fileList = val.map((v) => {
return {
...v,
file: v.file || { name: v.name },
url: v.url ? v.url : v.response?.data,
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()),
status: null
@@ -171,8 +179,8 @@ module.exports = {
f.url = resp.data
f.response = resp
f.percentage = 100
f.isImage = true
delete f.file
f.isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(f.name.split(".").pop().toLowerCase())
f.file = { name: f.name }
delete f.content
} else {
f.status = "fail"
@@ -706,7 +706,7 @@
Vue.component("year-van-dropdown-item", httpVueLoader("/components/plugins/vantMore/yearVanDropDownItem.vue?v=" + h5ComponentVersion))
Vue.component("van-text-dialog", httpVueLoader("/components/plugins/vantMore/vantTextDialog.vue?v=" + h5ComponentVersion))
Vue.component("enum-tag", httpVueLoader("/components/plugins/sysEnum/EnumTag.vue?v=" + h5ComponentVersion))
Vue.component("table-list", httpVueLoader("/components/plugins/h5/TableList.vue?v=" + h5ComponentVersion))
Vue.component("table-list", httpVueLoader("/components/plugins/h5/TableList.vue?v=" + h5ComponentVersion + "_overflow1"))
Vue.component("table-column", httpVueLoader("/components/plugins/h5/TableColumn.vue?v=" + h5ComponentVersion))
Vue.component("file-preview", httpVueLoader("/components/plugins/sysFilePreview/H5Index.vue?v=" + h5ComponentVersion))
</script>
+20 -68
View File
@@ -10,23 +10,22 @@ const act = {
<div ref="activitySwiper" class="swiper activity-swiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
<div class="item" role="button" tabindex="0" @click="go(activity)" @keydown.enter="go(activity)" @keydown.space.prevent="go(activity)">
<div class="item" @click="go(activity)">
<div class="img-box">
<el-image :src="activity.cover" fit="cover" :alt="activity.name">
<div slot="error" class="portal-cover-empty"><i class="fa fa-picture-o" aria-hidden="true"></i><span>暂无图片</span></div>
</el-image>
<span class="status" :class="{'status-ended': getStatusText(activity) === '已结束', 'status-upcoming': getStatusText(activity) === '未开始'}">
<img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
alt="">
<span class="status">
{{getStatusText(activity)}}
</span>
</div>
<h4 class="title" :title="activity.name">
<h4 class="title">
{{ activity.name}}
</h4>
<!--报名时间-->
<div class="time">
<p :title="[activity.startDate, activity.endDate].filter(Boolean).join(' 至 ')">
<p>
<i class="fa fa-clock-o"></i>
{{ formatActivityDate(activity.startDate) }} 至 {{ formatActivityDate(activity.endDate) }}
时间:{{ activity.startDate }}至{{ activity.endDate }}
</p>
</div>
</div>
@@ -35,21 +34,20 @@ const act = {
</div>
<!-- 导航按钮 -->
<button type="button" ref="nextButton" class="swiper-button-next" aria-label="下一组活动"></button>
<button type="button" ref="prevButton" class="swiper-button-prev" aria-label="上一组活动"></button>
<div ref="nextButton" class="swiper-button-next"></div>
<div ref="prevButton" class="swiper-button-prev"></div>
<!-- 进度条 -->
<div ref="pagination" class="swiper-pagination"></div>
</div>
<div v-else class="no-activity-placeholder">
<div class="icon"><i class="fa fa-calendar" aria-hidden="true"></i></div>
<div class="icon">📅</div>
<p>暂无近期活动</p>
<p class="subtext">敬请关注后续通知</p>
</div>
</div>
<slot></slot>
</div>
`,
data() {
@@ -59,57 +57,19 @@ const act = {
}
},
methods: {
// 轮播初始化或尺寸变化后,将左右箭头对齐当前封面的垂直中心。
updateNavigationPosition() {
this.syncBrowseState()
const swiperElement = this.$refs.activitySwiper
const cover = swiperElement && swiperElement.querySelector('.img-box')
if (!cover) return
// 按实际封面高度定位,避免标题换行或响应式列数变化导致箭头偏移。
const container = swiperElement.parentElement
const top = cover.getBoundingClientRect().top - container.getBoundingClientRect().top + cover.offsetHeight / 2
;[this.$refs.prevButton, this.$refs.nextButton].forEach(button => {
if (button) button.style.top = top + 'px'
})
},
// value 为接口返回的活动日期;无日期时显示待定,避免展示 Invalid date。
formatActivityDate(value) {
return value ? this.$moment(value).format('YYYY-MM-DD') : '待定'
},
// 按轮播实际停靠点判断是否还有内容,尺寸改变后同步首页按钮文案和可用状态。
syncBrowseState() {
const enabled = !!(this.swiper && !this.swiper.destroyed && !this.swiper.isLocked && this.swiper.snapGrid.length > 1)
this.$emit('browse-state', {
enabled: enabled,
label: enabled ? '浏览更多' : (this.actList.length ? '已展示全部活动' : '暂无活动')
})
},
// 点击前刷新尺寸,避免异步布局导致轮播状态过期;到末组后返回首组。
browseNext() {
if (!this.swiper || this.swiper.destroyed) this.initSwiper()
if (!this.swiper) return
this.swiper.update()
this.syncBrowseState()
if (this.swiper.isLocked || this.swiper.snapGrid.length <= 1) return
this.swiper.isEnd ? this.swiper.slideTo(0) : this.swiper.slideNext()
},
// 无请求参数;data 为当前用户可见且尚未结束的活动数组,保留本校筛选规则。
/*查询活动*/
listAct() {
this.$axios.post("/platform/home/listHomeActivity").then((res) => {
if (res.code === 0) {
// for (let i = 0; i < 10; i++) {
// this.actList = this.actList.concat(res.data)
// }
this.actList = res.data || []
this.actList = res.data
// 数据加载完成后初始化Swiper
this.$nextTick(() => {
this.initSwiper()
})
} else {
this.$emit('browse-state', {enabled: false, label: '活动加载失败'})
}
}).catch(() => {
this.$emit('browse-state', {enabled: false, label: '活动加载失败'})
})
},
/*初始化Swiper*/
@@ -120,7 +80,6 @@ const act = {
}
const swiperElement = this.$refs.activitySwiper
if (!swiperElement) {
this.syncBrowseState()
return
}
// 仅操作当前组件的轮播节点,避免页面其他轮播或尺寸观察器影响卡片宽度。
@@ -130,22 +89,13 @@ const act = {
centeredSlides: false,
loop: false,
watchOverflow: true,
// 双栏中的活动按容器宽度排布,避免用整个窗口宽度挤出过多卡片。
breakpointsBase: 'container',
observer: false,
observeParents: false,
// 页面内容加载后可能出现滚动条,需重新计算列宽,避免末张封面及圆角被裁切。
resizeObserver: true,
roundLengths: true,
on: {
init: () => this.$nextTick(() => this.updateNavigationPosition()),
resize: () => this.$nextTick(() => this.updateNavigationPosition()),
slidesUpdated: () => this.$nextTick(() => this.updateNavigationPosition()),
},
resizeObserver: false,
// 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。
mousewheel: {
enabled: true,
forceToAxis: true,
forceToAxis: false,
releaseOnEdges: true,
sensitivity: 0.8,
},
@@ -158,15 +108,17 @@ const act = {
type: 'progressbar',
},
breakpoints: {
480: {
768: {
slidesPerView: 2,
},
740: {
slidesPerView: 3,
1024: {
slidesPerView: 4,
},
1200: {
slidesPerView: 5,
}
}
})
this.syncBrowseState()
},
// 活动状态
@@ -1,19 +1,18 @@
const entry = {
template: /*language=HTML*/ `
<div class="entry-wrapper">
<div class="portal-entry-header">
<div class="portal-entry-tabs" role="tablist" aria-label="推荐入口">
<button v-for="section in entrySections" :key="section.key"
type="button" role="tab" :aria-selected="activeSection === section.key"
:class="{active: activeSection === section.key}" @click="$set(this, 'activeSection', section.key)">{{ section.title }}</button>
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key">
<div class="entry-section-header">
<div class="entry-section-title">
<i :class="section.icon"></i>
<span>{{ section.title }}</span>
<em>/Applications</em>
</div>
</div>
<a class="portal-more" href="/platform/v4/apps">全部应用 <i class="fa fa-arrow-right" aria-hidden="true"></i></a>
</div>
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key" v-show="activeSection === section.key" role="tabpanel">
<div class="apps-container">
<div class="app-grid" tabindex="0" :aria-label="section.title + ',可横向滚动浏览'">
<button type="button" class="app-item"
<div class="app-grid">
<div class="app-item"
v-for="(item, index) in section.list"
:key="item.id || index"
@click="openService(item, section.key)">
@@ -21,8 +20,8 @@ const entry = {
<img v-if="item.picIcon" class="app-icon-img" :src="item.picIcon" alt="" />
<i v-else class="fa fa-cube app-icon-fallback"></i>
</div>
<div class="app-name" :title="item.name">{{ item.name }}</div>
</button>
<div class="app-name">{{ item.name }}</div>
</div>
<el-empty class="app-empty"
v-if="!section.list || section.list.length === 0"
description="暂无数据"
@@ -31,12 +30,10 @@ const entry = {
</div>
</div>
</div>
<slot></slot>
</div>
`,
data() {
return {
activeSection: 'serv',
hasMore: true,
entries: {
serv: [],
@@ -60,8 +57,8 @@ const entry = {
this.getFavorite()
},
methods: {
// platform 固定传 PC;Result.data 为当前用户有权限的推荐服务数组。
getRecommendService() {
// 查询推荐服务
async getRecommendService() {
this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => {
if (res.code === 0) {
this.entries.serv = res.data
@@ -69,8 +66,8 @@ const entry = {
})
},
// platform 固定传 PC;Result.data 为当前用户可进入的推荐应用数组。
getRecommendApp() {
// 获取推荐应用
async getRecommendApp() {
this.$axios.post('/platform/home/listRecommendApp', {platform: "PC"}).then(res => {
if (res.code === 0) {
this.entries.app = res.data
@@ -78,8 +75,8 @@ const entry = {
})
},
// platform 固定传 PC;Result.data 为当前用户收藏且有权限的菜单数组。
getFavorite() {
// 获取我的收藏
async getFavorite() {
this.$axios.post('/platform/home/listFavorite', {platform: "PC"}).then(res => {
if (res.code === 0) {
this.entries.fav = res.data
@@ -130,6 +127,11 @@ const entry = {
margin-top: 22px;
}
/* 临时屏蔽“我的收藏”分组,保留原节点和数据逻辑便于恢复 */
.entry-section-fav {
display: none;
}
.entry-section-header {
display: flex;
align-items: center;
@@ -118,53 +118,42 @@ layout("/layouts/v4/baseLayout.html"){
</style>
<link rel="stylesheet" href="${base!}/assets/platform/css/home-portal.css?v=20261028-2" />
<div class="v4-container home-portal" id="v4-home-app">
<div class="v4-container" id="v4-home-app">
<!-- 首页banner -->
<div class="portal-top">
<div class="portal-hero">
<!-- 使用参考项目的本地背景资源,保留横幅文字及渐变遮罩。 -->
<img class="portal-hero-background" src="${base!}/assets/platform/img/home/service-portal-banner-campus-v4.png" alt="" />
<div class="portal-hero-inner">
<div class="portal-hero-copy">
<div class="portal-hero-eyebrow">教职工一站式服务平台</div>
<h1>智慧工会系统</h1>
<p>情系教职工,服务零距离</p>
<div class="portal-values">
<span><i class="fa fa-heart" aria-hidden="true"></i>服务职工</span>
<span><i class="fa fa-users" aria-hidden="true"></i>凝聚职工</span>
<span><i class="fa fa-leaf" aria-hidden="true"></i>关爱职工</span>
<span><i class="fa fa-file-text" aria-hidden="true"></i>助力发展</span>
</div>
</div>
<div class="section-banner">
<div class="banner-img">
<img :src="homeBannerList[0] || ''" alt=""/>
<stats></stats>
</div>
</div>
<user class="portal-member" :profile-only="true"></user>
</div>
<div class="portal-content">
<div class="" style="padding-bottom: 20px">
<user></user>
<div class="home-grid">
<div class="entry-card">
<entry></entry>
</div>
</div>
<div class="portal-columns">
<div class="home-section portal-panel portal-activities">
<div class="home-grid">
<div class="home-section">
<div class="home-section-header">
<div class="home-section-title">
<i class="fa fa-calendar"></i>
<span>最新活动</span>
</div>
<button type="button" class="portal-more" :disabled="!activityBrowse.enabled" @click="$refs.homeActivities.browseNext()">{{ activityBrowse.label }} <i v-if="activityBrowse.enabled" class="fa fa-arrow-right" aria-hidden="true"></i></button>
<em>/Activities</em>
</div>
</div>
<div class="act-card">
<act ref="homeActivities" @browse-state="updateActivityBrowse"></act>
<act></act>
</div>
</div>
<user ref="homeTodo" class="portal-todo"></user>
<work-template class="portal-templates" v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
<jcdt class="portal-news" :class="{'portal-news-wide': !$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])}" :list="websiteNews"></jcdt>
</div>
<work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
<jcdt :list="websiteNews"></jcdt>
</div>
</div>
@@ -183,7 +172,7 @@ layout("/layouts/v4/baseLayout.html"){
data(){
return{
websiteNews: [],
activityBrowse: {enabled: false, label: '活动加载中'}
homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item)
}
},
components: {
@@ -198,14 +187,10 @@ layout("/layouts/v4/baseLayout.html"){
this.getWebSiteNews()
},
methods: {
// state 由活动组件发布,enabled 表示可以翻页,label 说明加载或全部展示状态。
updateActivityBrowse(state) {
this.$set(this, 'activityBrowse', state)
},
getWebSiteNews(){
this.$axios.post('/platform/home/getNews').then(res => {
if (res.code === 0) {
this.$set(this, 'websiteNews', res.data || [])
this.websiteNews = res.data
}
})
}
@@ -2,72 +2,44 @@ const jcdt = {
template: /*language=HTML*/ `
<div class="jcdt-wrapper">
<div class="jcdt-container">
<div class="portal-news-header">
<el-dropdown @command="selectSection" trigger="click">
<button type="button" class="portal-news-title"><i class="fa fa-bullhorn" aria-hidden="true"></i> {{ currentNews.label }} <i v-if="list.length > 1" class="fa fa-angle-down" aria-hidden="true"></i></button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(section, index) in list" :key="index" :command="index">{{ section.label }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div v-if="pageCount > 1" class="portal-news-pager" aria-label="动态分页">
<span class="portal-news-count">共 {{ currentNews.value.length }} 条</span>
<button type="button" :disabled="pageNumber === 1" title="上一页动态" aria-label="上一页动态" @click="changePage(-1)"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
<span class="portal-news-page" aria-live="polite">{{ pageNumber }} / {{ pageCount }}</span>
<button type="button" :disabled="pageNumber === pageCount" title="下一页动态" aria-label="下一页动态" @click="changePage(1)"><i class="fa fa-angle-right" aria-hidden="true"></i></button>
</div>
</div>
<div class="portal-news-list">
<button type="button" class="portal-news-row" v-for="(item, index) in visibleNews" :key="index" @click="onLink(item)">
<time v-if="item.time" :title="item.time">{{ item.time.slice(5) }}</time>
<span :title="item.text">{{ item.text }}</span>
</button>
<div v-if="!currentNews.value.length" class="portal-empty">暂无动态</div>
</div>
<el-carousel height="360px" :interval="10000" indicator-position="none">
<el-carousel-item v-for="item,index in list" :key="index">
<div class="jcdt-title">
<span>{{ item.label }}</span>
</div>
<div class="jcdt-content">
<div class="jcdt-list">
<div class="jcdt-item" v-for="o,i in item.value" :key="i" @click="onLink(o)">
<div class="date" v-if="o.time">
<i class="el-icon-alarm-clock"></i>
<span>{{o.time}}</span>
</div>
<div style="display: flex; align-items: center">
<div>
</div>
<div class="title">{{o.text}}</div>
</div>
</div>
</div>
</div>
</el-carousel-item>
</el-carousel>
</div>
<slot></slot>
</div>
`,
data() {
return {activeIndex: 0, pageNumber: 1, pageSize: 3}
return {}
},
props: {
list: {
type: Array,
default: () => []
}
},
computed: {
currentNews() {
// 默认遵循接口栏目顺序,不再按栏目名称优先选择。
const section = this.list[this.activeIndex] || this.list[0]
return section ? {label: section.label, value: section.value || []} : {label: '最新动态', value: []}
},
visibleNews() {
const start = (this.pageNumber - 1) * this.pageSize
return this.currentNews.value.slice(start, start + this.pageSize)
},
pageCount() {
return Math.max(1, Math.ceil(this.currentNews.value.length / this.pageSize))
}
},
watch: {
list() {
this.selectSection(0)
default: []
}
},
methods: {
// step 为翻页方向(-1 上一页、1 下一页);在组件方法中更新页码,避免事件中的 this 指向错误。
changePage(step) {
const page = Math.max(1, Math.min(this.pageCount, this.pageNumber + step))
this.$set(this, 'pageNumber', page)
},
// index 为接口栏目数组下标;切换栏目时回到第一页,避免沿用上一栏的末页。
selectSection(index) {
this.activeIndex = index
this.pageNumber = 1
},
onLink(item) {
// 完整新闻地址直接打开,相对地址仍以重庆工贸工会网站为基准。
// 接口返回文章完整地址;相对地址按当前学校工会域名解析。
const url = this.isHttpOrHttps(item.href) ? item.href : new URL(item.href, "https://gh.cqgmy.edu.cn/").href
window.open(url)
},
@@ -76,15 +48,6 @@ const jcdt = {
}
},
style: /*language=CSS*/ `
/* 每页固定三行,不因展开或末页条数变化撑高相邻的模板卡片。 */
#v4-home-app .portal-news-list { min-height: 132px; }
#v4-home-app .portal-news-list .portal-news-row { height: 44px; padding: 9px 0; }
#v4-home-app .portal-news-pager { display: flex; flex-shrink: 0; align-items: center; gap: 8px; }
#v4-home-app .portal-news-count { margin-right: 4px; color: #8a9aac; font-size: 12px; }
#v4-home-app .portal-news-page { min-width: 32px; color: #637b96; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }
#v4-home-app .portal-news-pager button { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; border: 1px solid #e4edf7; border-radius: 6px; background: #fff; color: #1687f5; font-size: 18px; cursor: pointer; }
#v4-home-app .portal-news-pager button:hover:not(:disabled) { border-color: #b9d8fb; background: #f0f7ff; }
#v4-home-app .portal-news-pager button:disabled { border-color: #eef2f7; background: #f8fafc; color: #c3cedb; cursor: default; }
.jcdt-wrapper {
width: 100%;
background-size: cover;
@@ -5,22 +5,21 @@ const workTemplate = {
<div class="work-template-title">
<i class="fa fa-clipboard"></i>
<span>工作模板</span>
<em>/Templates</em>
</div>
<button type="button" class="portal-more" @click="browseTemplates">更多模板 <i class="fa fa-arrow-right" aria-hidden="true"></i></button>
</div>
<div class="work-template-card">
<div ref="templateGrid" v-if="templateList && templateList.length > 0" class="work-template-grid">
<button type="button" class="work-template-item"
<div v-if="templateList && templateList.length > 0" class="work-template-grid">
<div class="work-template-item"
v-for="template in templateList"
:key="template.id"
@click="go(template)">
<div class="work-template-icon">
<el-image :src="template.templateIcon || template.cover" fit="contain">
<i slot="error" class="fa fa-clipboard" aria-hidden="true"></i>
</el-image>
<img :src="template.templateIcon || template.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
alt="">
</div>
<div class="work-template-name" :title="template.templateName || template.name">{{ template.templateName || template.name }}</div>
</button>
<div class="work-template-name">{{ template.templateName || template.name }}</div>
</div>
</div>
<div v-else class="no-activity-placeholder">
@@ -29,7 +28,6 @@ const workTemplate = {
<p class="subtext">敬请关注后续更新</p>
</div>
</div>
<slot></slot>
</div>
`,
data() {
@@ -38,15 +36,6 @@ const workTemplate = {
}
},
methods: {
// 每次滚动一屏模板,到末尾后回到开头;无模板时不执行。
browseTemplates() {
const grid = this.$refs.templateGrid
if (!grid) return
// 模板仍使用全部真实数据,横向翻页不截断管理员可用的入口。
const atEnd = grid.scrollLeft + grid.clientWidth >= grid.scrollWidth - 2
grid.scrollTo({left: atEnd ? 0 : grid.scrollLeft + grid.clientWidth, behavior: 'smooth'})
},
// 无请求参数;Result.data 为当前管理员可用的工作模板数组。
listTemplate() {
this.$axios.post("/platform/home/listHomeTemplate").then((res) => {
if (res.code === 0) {
+125 -124
View File
@@ -4,17 +4,13 @@ const user = {
<!-- 用户容器:左右布局 -->
<div class="user-container">
<!-- 左侧:用户信息卡片 -->
<div v-if="profileOnly" class="user-info-card">
<div class="portal-member-date"><i class="fa fa-calendar-o" aria-hidden="true"></i>{{ $moment().format('YYYY年MM月DD日') }} {{ ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'][$moment().day()] }}</div>
<div class="user-info-card">
<div class="user-details">
<div class="portal-member-heading">
<span class="portal-member-avatar"><i class="fa fa-user" aria-hidden="true"></i></span>
<div><div class="portal-member-welcome">欢迎回来</div><div class="user-name">${@auth.getPrincipalProperty('username')}</div></div>
</div>
<div class="user-name">${@auth.getPrincipalProperty('username')}</div>
<div class="user-meta-grid">
<div class="meta-row">
<span class="meta-icon"><i class="fa fa-id-card-o" aria-hidden="true"></i></span>
<span class="meta-icon">🆔</span>
<div>
<span class="meta-label">工号</span>
<span class="meta-value">${@auth.getPrincipalProperty('loginname')}</span>
@@ -22,7 +18,7 @@ const user = {
</div>
<div class="meta-row">
<span class="meta-icon"><i class="fa fa-building-o" aria-hidden="true"></i></span>
<span class="meta-icon">🏢</span>
<div>
<span class="meta-label">单位信息</span>
<span class="meta-value">
@@ -37,11 +33,11 @@ const user = {
</span>
</div>
</div>
<div class="meta-row meta-row-roles">
<span class="meta-icon"><i class="fa fa-shield" aria-hidden="true"></i></span>
<div class="meta-row">
<span class="meta-icon">🎭</span>
<div>
<span class="meta-label">角色</span>
<div class="portal-member-roles" tabindex="0" role="region" aria-label="角色列表,可滚动查看全部角色"><span v-for="(role, index) in roleTags" :key="index" class="portal-member-role">{{ role }}</span><span v-if="!roleTags.length" class="meta-value">—</span></div>
<span class="meta-value">{{ roleNames || '—' }}</span>
</div>
</div>
</div>
@@ -67,13 +63,10 @@ const user = {
</div>
<!-- 右侧:待办中心 -->
<div v-if="!profileOnly" class="todo-center">
<div class="todo-center">
<!-- 标题和标签 -->
<div class="header">
<div class="portal-todo-heading">
<h3><i class="fa fa-list-ul" aria-hidden="true"></i> 待办中心</h3>
<a class="portal-more" :href="'/flow/todoCenter?status=' + activeTab" target="_blank">全部待办 <i class="fa fa-arrow-right" aria-hidden="true"></i></a>
</div>
<h3>待办中心</h3>
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
<el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane>
<el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane>
@@ -82,39 +75,55 @@ const user = {
</el-tabs>
</div>
<div class="portal-task-list" v-loading="tableLoading">
<div class="portal-task-row" v-for="(task, index) in tableData" :key="task.taskId || task.instanceId || index">
<i class="fa fa-file-text-o portal-task-icon" aria-hidden="true"></i>
<div class="portal-task-content">
<div class="portal-task-title" :title="task.instanceName">{{ task.instanceName }}</div>
<div class="portal-task-meta">
<span :title="task.variable && task.variable.initiatorName">申请人:{{ (task.variable && task.variable.initiatorName) || '—' }}</span>
<span class="portal-task-node" :title="[task.categoryName, task.taskName].filter(Boolean).join(' · ')">{{ [task.categoryName, task.taskName].filter(Boolean).join(' · ') }}</span>
</div>
</div>
<time class="portal-task-date">{{ task.createdAt ? $moment(task.createdAt).format('YYYY-MM-DD') : '—' }}</time>
<el-button type="primary" size="mini" @click="openView(task)">查看</el-button>
</div>
<div v-if="!tableLoading && !tableData.length" class="portal-empty"><i class="fa fa-check-circle-o" aria-hidden="true"></i><span>暂无{{ activeTab === 'todo' ? '待办事项' : '相关记录' }}</span></div>
<slot></slot>
</div>
<el-pagination v-if="pageForm.totalCount > pageForm.pageSize" class="portal-task-pagination"
@current-change="pageNumberChange" :current-page="pageForm.pageNumber" :page-size="pageForm.pageSize"
layout="total, prev, next" :total="pageForm.totalCount"></el-pagination>
<!-- 表格 -->
<el-table
:data="todoList"
style="width: 100%"
border
stripe
v-loading="loading"
:cell-style="{ padding: '8px' }"
:header-cell-style="{ background: '#f8f9fa', color: '#444', fontWeight: '500' }"
>
<el-table-column label="序号" type="index" :index="indexMethod" width="100px"></el-table-column>
<el-table-column prop="categoryName" label="流程分类" width="100"></el-table-column>
<el-table-column prop="title" label="流程名称" min-width="300">
<template slot-scope="scope">
<el-tooltip effect="dark" :content="scope.row.instanceName" placement="top">
<span class="table-title">{{ scope.row.instanceName }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="sender" label="申请人" width="120">
<template slot-scope="{row}">{{row.variable.initiatorName}}</template>
</el-table-column>
<el-table-column prop="taskName" label="当前流程节点" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="date" label="发起日期" width="120">
<template slot-scope="{row}">
{{$moment(row.createdAt).format('YYYY-MM-DD')}}
</template>
</el-table-column>
<el-table-column label="操作" width="100px" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="openView(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</div>
</div>
</div>
`,
mixins: [initTableMixins],
// 同一组件分别承载会员卡和待办卡,各自只加载需要的数据。
props: {
profileOnly: { type: Boolean, default: false }
},
data() {
return {
activeTab: 'todo',
todoList: [],
loading: false,
pageForm: {
pageSize: 3
pageNum: 1,
pageSize: 5,
totalCount: 0
},
taskStats: {
todoCount: 0,
@@ -131,12 +140,6 @@ const user = {
};
},
computed: {
roleTags() {
// 兼容角色接口已有的中英文分隔符,仅改变展示,不改变角色权限。
return String(this.roleNames || '').split(/[,,、;;]/).map(role => role.trim()).filter(Boolean)
}
},
methods: {
getWeatherInfo(iconCode, temp) {
const info = {
@@ -186,60 +189,91 @@ const user = {
return info;
},
// key 为系统配置名称;返回 Promise,值为 Result.data 中的经度或纬度。
getConfigKey(key) {
return this.$axios.post("/open/common/getConfigKey", {key}).then(res => res.data)
async getConfigKey(key) {
const resp = await this.$axios.post("/open/common/getConfigKey", { key })
return resp.data
},
// 使用本校配置经纬度查询城市及天气,缺少配置时不请求外部天气接口。
fetchWeather() {
Promise.all([this.getConfigKey("AppMapCenterPointX"), this.getConfigKey("AppMapCenterPointY")])
.then(coordinates => {
if (!coordinates[0] || !coordinates[1]) return
return this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/geo/v2/city/lookup?location=" + coordinates.join(",") + "&key=" + this.QWEATHER_KEY)
.then(geoRes => {
if (geoRes.code !== '200' || !geoRes.location || !geoRes.location.length) {
throw new Error('城市未找到')
}
const city = geoRes.location[0]
return this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/v7/weather/now?location=" + city.id + "&key=" + this.QWEATHER_KEY)
.then(weatherRes => {
if (weatherRes.code !== '200' || !weatherRes.now) throw new Error('天气数据异常')
this.weatherData = Object.assign({}, this.getWeatherInfo(weatherRes.now.icon, weatherRes.now.temp), {
temp: weatherRes.now.temp, text: weatherRes.now.text, name: city.name
})
this.weatherError = false
})
})
})
.catch(() => {
this.weatherError = true
this.weatherData = null
// 🔆 获取天气
async fetchWeather() {
try {
// 存localStorage 112.13,32.01 广州 113.27,23.14
const AppMapCenterPointX = await this.getConfigKey("AppMapCenterPointX")
const AppMapCenterPointY = await this.getConfigKey("AppMapCenterPointY")
if (!AppMapCenterPointX || !AppMapCenterPointY){
return
}
// 1. 先通过城市名获取 locationId
this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/geo/v2/city/lookup?location=" + AppMapCenterPointX + "," + AppMapCenterPointY + "&key=" + this.QWEATHER_KEY).then((geoRes) => {
if (geoRes.code !== '200' || !geoRes.location?.[0]) {
console.log('城市未找到')
}
const locationId = geoRes.location[0].id;
// 2. 获取当前天气
this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/v7/weather/now?location=" + locationId + "&key=" + this.QWEATHER_KEY).then((weatherRes) => {
if (weatherRes.code !== '200') {
console.warn('天气数据异常:', weatherRes);
}
const weatherInfo = this.getWeatherInfo(weatherRes.now.icon, weatherRes.now.temp);
this.weatherData = {
...weatherInfo,
temp: weatherRes.now.temp,
text: weatherRes.now.text,
name: geoRes.location[0].name
};
this.weatherError = false;
})
})
} catch (err) {
console.warn('天气加载失败:', err);
this.weatherError = true;
this.weatherData = null;
}
},
// 获取任务列表
tabLabel(title, count) {
return title + "(" + (count || 0) + ")"
},
// 无请求参数;data 的四个 Count 字段分别为当前用户各状态流程数量。
getStatistics() {
this.$axios.post("/flow/todoCenter/statistics").then(res => {
if (res.code === 0) this.taskStats = Object.assign({}, this.taskStats, res.data)
}).catch(() => {
this.$message.error("获取待办统计失败")
})
async getStatistics() {
try {
const res = await $.post("/flow/todoCenter/statistics")
if (res.code === 0) {
this.taskStats = Object.assign({}, this.taskStats, res.data)
}
} catch (error) {
console.error("获取待办统计失败:", error)
}
},
async getTasks() {
this.loading = true
try {
const res = await $.post("/flow/todoCenter/" + this.activeTab, this.pageForm)
if (res.code === 0) {
this.todoList = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
} catch (error) {
this.$message.error("获取任务列表失败")
console.error("获取任务列表失败:", error)
} finally {
this.loading = false
}
},
// WF 状态由 activeTab 决定;pageForm 传分页参数,data.list/totalCount 为记录及总数。
// 列表加载和翻页由 initTableMixins 统一处理,首页只补充状态地址和数量统计。
pageData() {
this.pageDataUrl = "/flow/todoCenter/" + this.activeTab
initTableMixins.methods.pageData.call(this)
this.getTasks()
this.getStatistics()
},
handleTabClick() {
this.doSearch()
handleTabClick(tab) {
this.pageData()
console.log('切换到:', tab.name);
},
// 处理任务
openView(task) {
async openView(task) {
console.log(task)
const {taskId, taskKey, taskState, instanceId, businessNo, formKey} = task
if (!formKey) {
this.$message.warning("当前流程没有配置地址")
@@ -247,9 +281,8 @@ const user = {
}
window.open(formKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
},
// 无请求参数;Result.data.roleNames 为当前登录人的角色名称字符串。
getRoleNames() {
this.$axios.post("/platform/sys/role/getRoleNames").then(res => {
$.post("/platform/sys/role/getRoleNames").then(res => {
if (res.code === 0) {
this.roleNames = res.data.roleNames
}
@@ -258,45 +291,13 @@ const user = {
}
},
created() {
if (this.profileOnly) {
this.getRoleNames()
} else {
this.pageData()
}
this.pageData()
this.getRoleNames()
},
mounted() {
if (this.profileOnly) this.fetchWeather()
this.fetchWeather()
},
style: /*language=CSS*/ `
/* 会员卡样式随组件注入,确保与卡片结构同步加载,避免旧外链样式造成排版缺失。 */
#v4-home-app .portal-member { min-width: 0; }
#v4-home-app .portal-member .user-info-card { height: 100%; padding: 20px; gap: 14px; border: 1px solid #e3edf8; border-radius: 12px; background: linear-gradient(180deg, #f1f7ff 0, #fff 110px); box-shadow: 0 3px 14px rgba(30,66,108,.035); }
#v4-home-app .portal-member-date { display: flex; align-items: center; gap: 8px; color: #71839b; font-size: 12px; line-height: 20px; }
#v4-home-app .portal-member-heading { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid #eaf0f7; }
#v4-home-app .portal-member-heading > div { min-width: 0; }
#v4-home-app .portal-member-avatar { display: flex; align-items: center; justify-content: center; flex: 0 0 46px; height: 46px; border-radius: 50%; background: #e1eeff; color: #1687f5; font-size: 25px; }
#v4-home-app .portal-member-welcome { margin-bottom: 3px; color: #8797a9; font-size: 12px; line-height: 18px; }
#v4-home-app .portal-member .user-name { margin: 0; color: #163c61; text-align: left; font-size: 18px; line-height: 24px; overflow-wrap: anywhere; }
#v4-home-app .portal-member .user-meta-grid { gap: 12px; margin: 0; }
#v4-home-app .portal-member .meta-row { gap: 8px; }
#v4-home-app .portal-member .meta-icon { min-width: 18px; color: #81a4c9; font-size: 14px; }
#v4-home-app .portal-member .meta-row > div { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 8px; min-width: 0; }
#v4-home-app .portal-member .meta-label { margin: 0; color: #8995a5; font-size: 12px; line-height: 20px; }
#v4-home-app .portal-member .meta-value { color: #283c54; font-size: 12px; font-weight: 400; line-height: 20px; overflow-wrap: anywhere; }
#v4-home-app .portal-member .meta-row-roles > div { display: block; }
/* 角色最多占用三行标签的高度,超出时内部滚动,避免撑高首页横幅。 */
#v4-home-app .portal-member-roles { display: flex; flex-wrap: wrap; align-content: flex-start; gap: 5px; margin-top: 5px; max-height: 88px; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; scrollbar-color: #c5d9ee transparent; }
#v4-home-app .portal-member-roles::-webkit-scrollbar { width: 4px; }
#v4-home-app .portal-member-roles::-webkit-scrollbar-thumb { background: #c5d9ee; border-radius: 4px; }
#v4-home-app .portal-member-role { max-width: 100%; display: inline-block; padding: 3px 8px; border: 1px solid #e1ebf7; border-radius: 5px; background: #f3f7fc; color: #46617f; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
#v4-home-app .portal-member .weather-section { margin-top: auto; padding: 8px 10px; border-radius: 8px; background: #f0f7ff; color: #527494; }
#v4-home-app .portal-member .weather-header { gap: 6px; }
#v4-home-app .portal-member .weather-location,
#v4-home-app .portal-member .weather-text { font-size: 12px; }
#v4-home-app .portal-member .weather-emoji { font-size: 20px; }
#v4-home-app .portal-member .weather-temp { font-size: 18px; }
#v4-home-app .portal-member .weather-tip { margin-top: 4px; font-size: 11px; line-height: 18px; }
#v4-home-app .portal-member .weather-error { margin-top: auto; padding: 8px; font-size: 12px; }
.user-wrapper {
width: 100%;
background-color: #f5f7fa;
@@ -100,7 +100,7 @@ layout("/layouts/platform.html"){
size="mini" type="danger">删除
</el-button>
<el-button @click="doExportDeclare(row)" size="mini" type="primary">申请表导出</el-button>
<el-button v-if="row.instanceState === 20" @click="doExportDeclare(row)" size="mini" type="primary">申请表导出</el-button>
<el-button v-if="row.instanceState === 20" @click="doExportReimbursement(row)" size="mini"
type="primary">报销凭证导出
</el-button>
@@ -21,14 +21,29 @@ const PROPOSAL_INFO = {
</el-descriptions-item>
<el-descriptions-item label="提案类别">
{{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="建议承办单位">
{{viewData.suggestUnits}}
</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>
<span v-else>--</span>
</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">
<div class="text-left">{{viewData.researchFindings}}</div>
</el-descriptions-item>-->
@@ -82,18 +97,22 @@ const PROPOSAL_INFO = {
{{row.commissionerName}}({{row.commissionerLoginName}})
</template>
</el-table-column>
<el-table-column label="立案结果" prop="caseFilingType">
<!-- 委员意见分别展示立案结果和类型,未填写时显示双横线。 -->
<el-table-column label="立案结果" prop="caseFilingResult">
<template slot-scope="{row}">
<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>
<!-- 普通提案/重点提案暂不展示,历史数据仍保留。 -->
<template v-if="false">
(
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="row.caseFilingType"></dict-tag>
)
</template>
<span v-else>--</span>
</div>
</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>
</template>
</el-table-column>
@@ -124,117 +143,125 @@ const PROPOSAL_INFO = {
</span>
</div>
</template>
<!--同一提案可能存在多个承办单位答复任务,查看时合并为一个流程节点,节点内保留每条答复。-->
<template v-if="task.replyTasks">
<el-descriptions v-for="replyTask in task.replyTasks" border class="flow-task-form"
:column="3" :key="replyTask.id">
<el-descriptions-item label="承办单位" :span="3">
{{replyTask.ext.underTakeName}}
<!--同一提案可能存在多个承办单位答复任务,查看时合并为一个流程节点,节点内保留每条答复。-->
<template v-if="task.replyTasks">
<el-descriptions v-for="replyTask in task.replyTasks" border class="flow-task-form"
:column="3" :key="replyTask.id">
<el-descriptions-item label="承办单位" :span="3">
{{replyTask.ext.underTakeName}}
</el-descriptions-item>
<el-descriptions-item label="办理用户">
{{replyTask.taskFormData.userName}}({{replyTask.taskFormData.loginName}})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{replyTask.finishTime}}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="replyTask.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
<div v-html="replyTask.taskFormData.tf_opinion"></div>
</el-descriptions-item>
</el-descriptions>
</template>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id"
v-else-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName
}}({{task.ext.initiatorAccount}})
</el-descriptions-item>
<el-descriptions-item label="办理用户">
{{replyTask.taskFormData.userName}}({{replyTask.taskFormData.loginName}})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{replyTask.finishTime}}</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="replyTask.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
<div v-html="replyTask.taskFormData.tf_opinion"></div>
:value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
</template>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id"
v-else-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName
}}({{task.ext.initiatorAccount}})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<!--邀请附议人-->
<el-descriptions border class="flow-task-form" :column="3" :key="task.id"
v-else-if="task.taskName === 'invite'">
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName
}}({{task.taskFormData.loginName}})
</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-table v-loading="tableLoading" :data="task?.taskFormData?.seconder">
<el-table-column label="姓名" prop="userName"></el-table-column>
<el-table-column label="工号" prop="loginName"></el-table-column>
<el-table-column label="性别" prop="sex"></el-table-column>
<el-table-column label="联系方式" prop="mobile"></el-table-column>
<el-table-column label="单位" prop="unitName"></el-table-column>
<el-table-column label="分工会" prop="unionName"></el-table-column>
<el-table-column label="代表团" prop="delegationName"></el-table-column>
</el-table>
</el-descriptions-item>
</el-descriptions>
<!--提案委员会立案-->
<el-descriptions border class="flow-task-form" :column="3" :key="task.id"
v-else-if="task.taskName === 'committee'||task.taskName === 'committeeFilingUnit'">
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName
}}({{task.taskFormData.loginName}})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="立案结果">
<div style="display: flex;justify-content: center">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
:value="task.ext.tf_caseFilingResult"></dict-tag>
<!-- 普通提案/重点提案暂不展示,历史流程变量仍保留。 -->
<template v-if="false">
(
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="task.ext.tf_caseFilingType"></dict-tag>
)
</template>
</div>
</el-descriptions-item>
<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 border class="flow-task-form" :column="3" :key="task.id"
v-else-if="task.taskName === 'invite'">
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName
}}({{task.taskFormData.loginName}})
</el-descriptions-item>
<template v-else>
<el-descriptions-item label="主办单位" :span="3">{{task?.ext?.tf_masterUnitNameStr}}
</el-descriptions-item>
<el-descriptions-item label="协办单位" :span="3">{{task?.ext?.tf_slaveUnitNameStr}}
<el-descriptions-item label="办理时间" :span="2">{{ task.finishTime }}
</el-descriptions-item>
<el-descriptions-item label="附议人" :span="3" v-if="task.taskName === 'invite'">
<el-table v-loading="tableLoading" :data="task?.taskFormData?.seconder">
<el-table-column label="姓名" prop="userName"></el-table-column>
<el-table-column label="工号" prop="loginName"></el-table-column>
<el-table-column label="性别" prop="sex"></el-table-column>
<el-table-column label="联系方式" prop="mobile"></el-table-column>
<el-table-column label="单位" prop="unitName"></el-table-column>
<el-table-column label="分工会" prop="unionName"></el-table-column>
<el-table-column label="代表团" prop="delegationName"></el-table-column>
</el-table>
</el-descriptions-item>
</el-descriptions>
<!--提案委员会立案-->
<el-descriptions border class="flow-task-form" :column="3" :key="task.id"
v-else-if="task.taskName === 'committee'||task.taskName === 'committeeFilingUnit'">
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName
}}({{task.taskFormData.loginName}})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<!-- 立案节点分别展示流程记录中的结果与类型,空值显示双横线。 -->
<el-descriptions-item label="立案结果">
<div style="display: flex;justify-content: center">
<dict-tag v-if="task.ext.tf_caseFilingResult"
:options="dict.type.PROPOSAL_CASE_FILING_RESULT"
:value="task.ext.tf_caseFilingResult"></dict-tag>
<span v-else>--</span>
</div>
</el-descriptions-item>
<el-descriptions-item label="立案类型">
<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>
<span v-else>--</span>
</div>
</el-descriptions-item>
<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>
<template v-else>
<el-descriptions-item label="主办单位" :span="3">{{task?.ext?.tf_masterUnitNameStr}}
</el-descriptions-item>
<el-descriptions-item label="协办单位" :span="3">{{task?.ext?.tf_slaveUnitNameStr}}
</el-descriptions-item>
</template>
</template>
</template>
<el-descriptions-item label="办理意见" :span="3">
<div v-html="task.taskFormData.tf_opinion"></div>
</el-descriptions-item>
</el-descriptions>
<el-descriptions-item label="办理意见" :span="3">
<div v-html="task.taskFormData.tf_opinion"></div>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<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">
{{task.ext.underTakeName}}
</el-descriptions-item>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<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">
{{task.ext.underTakeName}}
</el-descriptions-item>
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName
}}({{task.taskFormData.loginName}})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName
}}({{task.taskFormData.loginName}})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理评价" v-if="['feedback'].includes(task.taskName)" :span="3">
<dict-tag :options="dict.type.PROPOSAL_FEEDBACK"
:value="task.ext.tf_feedback"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理结果" v-else>
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3" v-if="!task.ext.isFirstTaskNode">
<div v-html="task.taskFormData.tf_opinion"></div>
</el-descriptions-item>
</el-descriptions>
<el-descriptions-item label="办理评价" v-if="['feedback'].includes(task.taskName)"
:span="3">
<dict-tag :options="dict.type.PROPOSAL_FEEDBACK"
:value="task.ext.tf_feedback"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理结果" v-else>
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3" v-if="!task.ext.isFirstTaskNode">
<div v-html="task.taskFormData.tf_opinion"></div>
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
</template>
@@ -243,7 +270,7 @@ const PROPOSAL_INFO = {
<snaker-chart ref="snakerChartRef"></snaker-chart>
</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: {
hide_slave: false
},
@@ -60,7 +60,10 @@ const USER_COMPONENT = {
},
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 shadow="never">
<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">导入提案
</el-button>
</el-button>-->
</table-tool>
<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>
@@ -76,8 +76,7 @@ layout("/layouts/platform.html"){
<el-button v-if="row.canRevoke" size="mini" type="danger" @click="openRevoke(row)">
撤回
</el-button>
<!-- v-if="row.taskKey === 'startTask' || !row.instanceId"-->
<el-button size="mini" type="danger" @click="del(row.id)">
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" size="mini" type="danger" @click="del(row.id)">
删除
</el-button>
</template>
@@ -8,7 +8,7 @@ layout("/layouts/platform.html"){
<custom-card>
<snaker-start slot="header" label="撰写提案" define_key="JDHTA_NC">
<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>
</snaker-start>
<el-form :model="formData" :rules="formRules" label-width="120px" ref="addForm">
@@ -55,7 +55,7 @@ layout("/layouts/platform.html"){
</el-select>
</el-form-item>
</el-descriptions-item>-->
<el-descriptions-item label="证明人">
<!-- <el-descriptions-item label="证明人">
<el-form-item prop="certifierUserId" label="证明人">
<el-select
style="width: 100%"
@@ -75,7 +75,7 @@ layout("/layouts/platform.html"){
</el-option>
</el-select>
</el-form-item>
</el-descriptions-item>
</el-descriptions-item> -->
<el-descriptions-item label="慰问类型">
<el-form-item prop="type" label="慰问类型">
<el-select v-model="formData.type" @change="typeChange" style="width: 100%"
@@ -112,14 +112,14 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="收款账户">
<el-descriptions-item label="收款账户" v-if="!['A', 'D'].includes(formData.typeCode)">
<el-form-item prop="bankCardNumber" label="收款账户">
<el-input maxlength="20" v-model="formData.bankCardNumber" placeholder="请填写收款账户"
<el-input maxlength="30" v-model="formData.bankCardNumber" placeholder="请填写银行卡号或对公账户号"
></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="户名">
<el-descriptions-item label="户名" v-if="!['A', 'D'].includes(formData.typeCode)">
<el-form-item prop="bankUserName" label="户名">
<el-input maxlength="20" v-model="formData.bankUserName"
placeholder="请填写户名"
@@ -127,7 +127,7 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="开户行" >
<el-descriptions-item label="开户行" v-if="!['A', 'D'].includes(formData.typeCode)">
<el-form-item prop="bankOfDeposit" label="开户行">
<el-input maxlength="20" v-model="formData.bankOfDeposit"
placeholder="请填写开户行"
@@ -173,10 +173,12 @@ layout("/layouts/platform.html"){
</template>
</el-descriptions>
<el-descriptions :column="3" border>
<el-descriptions-item label="申请事由" :span="3">
<el-form-item prop="remark" label="申请事由">
<el-input maxlength="500" v-model="formData.remark" placeholder="请填写申请事由"
type="textarea" :autosize="{ minRows: 4, maxRows: 8}"></el-input>
type="textarea" :rows="3" resize="none"></el-input>
</el-form-item>
</el-descriptions-item>
@@ -238,7 +240,10 @@ layout("/layouts/platform.html"){
payUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
type: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
way: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
money: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
money: [
{ required: true, message: "请填写慰问金额", trigger: ["change", "blur"] },
{ validator: (rule, value, callback) => callback(this.validateMoney(value) ? undefined : new Error("慰问金额须大于0且不超过10000元,最多两位小数")), trigger: ["change", "blur"] }
],
occurTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
child: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
hospital: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
@@ -247,14 +252,20 @@ layout("/layouts/platform.html"){
hospitalBy: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
deadImmediateFamily: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
remark: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
files: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
files: [{ required: true, type: "array", min: 1, max: 5, message: "请上传1至5个附件", trigger: ["change", "blur"] }],
signature: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
handlerUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
certifierUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
bankCardNumber: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
// certifierUserId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
bankCardNumber: [
{ required: true, message: "请填写银行卡号或对公账户号", trigger: ["change", "blur"] },
{ pattern: /^[0-9]{6,30}$/, message: "收款账户须为6至30位数字(银行卡号或对公账户号)", trigger: ["change", "blur"] }
],
bankUserName: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
bankOfDeposit: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
thisYearHospitalizationNum: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
thisYearHospitalizationNum: [
{ required: true, message: "必填", trigger: ["change", "blur"] },
{ type: "integer", min: 0, max: 10000, message: "住院次数须为0至10000的整数", trigger: ["change", "blur"] }
],
leaveHospitalTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
hospitalizationTime: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
},
@@ -268,6 +279,9 @@ layout("/layouts/platform.html"){
}
},
methods: {
validateMoney(value) {
return /^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000
},
createRemoteMethod(options) {
return (keyword) => {
this.selectQueryUser(keyword, options)
@@ -37,12 +37,12 @@ const basicForm = {
<el-radio-button :label="false">不上传</el-radio-button>
</el-radio-group>
</el-form-item>-->
<!-- <el-form-item label="是否上传附件" prop="isUploadFile">-->
<!-- <el-radio-group v-model="formData.isUploadFile">-->
<!-- <el-radio-button :label="true">是</el-radio-button>-->
<!-- <el-radio-button :label="false">否</el-radio-button>-->
<!-- </el-radio-group>-->
<!-- </el-form-item>-->
<el-form-item label="是否上传附件" prop="isUploadFile">
<el-radio-group v-model="formData.isUploadFile">
<el-radio-button :label="true">是</el-radio-button>
<el-radio-button :label="false">否</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="上传附件说明" prop="uploadFileDesc" v-if="formData.isUploadFile">
<el-input v-model="formData.uploadFileDesc" maxlength="50" clearable placeholder="请填写上传附件说明"></el-input>
</el-form-item>
@@ -26,7 +26,7 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="受补助人">
<el-form-item prop="userName">
<el-form-item prop="userId">
<template v-if="formData.mode == 2">
<el-select :remote-method="queryRecipients"
@change="userChange" clearable filterable
@@ -51,7 +51,7 @@ layout("/layouts/platform.html"){
</el-descriptions-item>
<el-descriptions-item label="身份证号" :span="2">
<el-form-item prop="idCard">
<el-input maxlength="20" clearable placeholder="请填写身份证号"
<el-input clearable placeholder="请填写身份证号"
v-model="formData.idCard" maxlength="18"></el-input>
</el-form-item>
</el-descriptions-item>
@@ -84,7 +84,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="手机号码">
<el-form-item prop="mobile">
<el-input maxlength="11" clearable placeholder="请填写手机号码"
v-model="formData.mobile" maxlength="11"></el-input>
v-model="formData.mobile"></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="家庭年总收入(万元)">
@@ -103,6 +103,7 @@ layout("/layouts/platform.html"){
<el-input-number
v-model="formData.homeNumber"
:min="1"
:precision="0"
:step="1"
style="width: 100%"
placeholder="请填写家庭人数">
@@ -113,7 +114,7 @@ layout("/layouts/platform.html"){
<el-form-item prop="homeAddress">
<el-input clearable
style="width: 100%"
v-model="formData.homeAddress"
v-model="formData.homeAddress" maxlength="200"
placeholder="请填写家庭住址">
</el-input>
</el-form-item>
@@ -132,7 +133,7 @@ layout("/layouts/platform.html"){
<!-- </el-descriptions-item>-->
<el-descriptions-item label="银行卡号" >
<el-form-item prop="bankCardNum">
<el-input maxlength="20" clearable placeholder="请填写银行卡号"
<el-input maxlength="19" clearable placeholder="请填写银行卡号"
v-model="formData.bankCardNum"></el-input>
</el-form-item>
</el-descriptions-item>
@@ -152,7 +153,7 @@ layout("/layouts/platform.html"){
</el-descriptions-item>
<el-descriptions-item label="申请金额(元)">
<el-form-item prop="applyMoney" :rules="[{required:true,type:'number',min:0.01,message:'请填写大于零的申请金额',trigger:['change','blur']}]">
<el-form-item prop="applyMoney">
<el-input-number v-model="formData.applyMoney" :precision="2" :min="0.01" placeholder="请输入申请金额"></el-input-number>
</el-form-item>
</el-descriptions-item>
@@ -198,8 +199,8 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}">
<el-form-item label-width="0"
:prop="'familyList.'+$index+'.relation'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<el-input v-model="row.relation" placeholder="请填写与本人关系" clearable></el-input>
:rules="textRules('与本人关系', 50)">
<el-input v-model="row.relation" maxlength="50" placeholder="请填写与本人关系" clearable></el-input>
</el-form-item>
</template>
</el-table-column>
@@ -207,8 +208,8 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}">
<el-form-item label-width="0"
:prop="'familyList.'+$index+'.name'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<el-input v-model="row.name" placeholder="请填写姓名" clearable></el-input>
:rules="textRules('姓名', 100)">
<el-input v-model="row.name" maxlength="100" placeholder="请填写姓名" clearable></el-input>
</el-form-item>
</template>
</el-table-column>
@@ -216,9 +217,9 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}">
<el-form-item label-width="0"
:prop="'familyList.'+$index+'.age'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
:rules="[{required:true,type:'integer',min:1,max:100,message:'年龄须为1至100的整数',trigger:['change','blur']}]">
<el-input-number v-model="row.age" placeholder="年龄" :min="1"
:max="100" :step="1" style="width: 100%" clearable></el-input-number>
:max="100" :precision="0" :step="1" style="width: 100%" clearable></el-input-number>
</el-form-item>
</template>
</el-table-column>
@@ -226,8 +227,8 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}">
<el-form-item label-width="0"
:prop="'familyList.'+$index+'.homeUnit'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<el-input v-model="row.homeUnit" placeholder="请填写工作单位" clearable></el-input>
:rules="textRules('工作单位', 200)">
<el-input v-model="row.homeUnit" maxlength="200" placeholder="请填写工作单位" clearable></el-input>
</el-form-item>
</template>
</el-table-column>
@@ -235,9 +236,9 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}">
<el-form-item label-width="0"
:prop="'familyList.'+$index+'.yearIncome'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
:rules="[{validator:validateIncome,required:true,trigger:['change','blur']}]">
<el-input-number v-model="row.yearIncome" placeholder="年收入"
:min="0" :step="1" style="width: 100%" clearable></el-input-number>
:min="0" :precision="2" :step="1" style="width: 100%" clearable></el-input-number>
</el-form-item>
</template>
</el-table-column>
@@ -245,8 +246,8 @@ layout("/layouts/platform.html"){
<template slot-scope="{row,$index}">
<el-form-item label-width="0"
:prop="'familyList.'+$index+'.remarks'"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<el-input v-model="row.remarks" placeholder="请填写备注" clearable></el-input>
:rules="textRules('备注', 500)">
<el-input v-model="row.remarks" maxlength="500" placeholder="请填写备注" clearable></el-input>
</el-form-item>
</template>
</el-table-column>
@@ -326,22 +327,37 @@ layout("/layouts/platform.html"){
applyCount: '1',
},
formRules: {
proxyUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
proxyLoginName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
sex: [{required: true, message: '请填写性别', trigger: ['blur', 'change']}],
userName: [{required: true, message: '请填写受补助人', trigger: ['blur', 'change']}],
unitName: [{required: true, message: '请填写所属单位', trigger: ['blur', 'change']}],
proxyUserName: this.textRules('填写人姓名', 100),
proxyLoginName: this.textRules('填写人工号', 100),
mode: [{required: true, type: 'enum', enum: ['1', '2'], message: '请选择申请模式', trigger: ['blur', 'change']}],
sex: [{required: true, type: 'enum', enum: ['男', '女'], message: '请选择性别', trigger: ['blur', 'change']}],
userId: [{required: true, message: '请选择受补助人', trigger: ['blur', 'change']}],
unitName: this.textRules('所属单位', 500),
unionName: [{required: true, message: '请填写所属工会', trigger: ['blur', 'change']}],
homeAddress: [{required: true, message: '请填写家庭住址', trigger: ['blur', 'change']}],
mobile: [{required: true, message: '请填写手机号码', trigger: ['blur', 'change']}],
idCard: [{required: true, message: '请填写身份证号', trigger: ['blur', 'change']}],
bankCardNum: [{required: true, message: '请填写收款账户卡号', trigger: ['blur', 'change']}],
homeAddress: this.textRules('家庭住址', 200),
mobile: [
{required: true, message: '请填写手机号码', trigger: ['blur', 'change']},
{pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: ['blur', 'change']}
],
idCard: [{validator: (rule, value, callback) => {
const result = this.isValidIdCard(value || '')
callback(result.status === 0 ? new Error(result.msg) : undefined)
}, required: true, trigger: ['blur', 'change']}],
bankCardNum: [{validator: (rule, value, callback) => {
const result = this.validateCard(value || '')
callback(result.status === 0 ? new Error(result.msg) : undefined)
}, required: true, trigger: ['blur', 'change']}],
bankUserName: [{required: true, message: '请填写户名', trigger: ['blur', 'change']}],
bankOfDeposit: [{required: true, message: '请填写开户行', trigger: ['blur', 'change']}],
position: [{required: true, message: '请填写职务', trigger: ['blur', 'change']}],
officialCapacity: [{required: true, message: '请填写职称', trigger: ['blur', 'change']}],
subsidyStandards: [{required: true, message: '请选择困难类型', trigger: ['blur', 'change']}],
reason: [{required: true, message: '请填写申请原因', trigger: ['blur', 'change']}],
bankOfDeposit: this.textRules('开户行', 30),
position: this.textRules('职务', 20),
officialCapacity: this.textRules('职称', 20),
homeIncome: [{validator: this.validateIncome, required: true, trigger: ['blur', 'change']}],
homeNumber: [{required: true, type: 'integer', min: 1, message: '家庭人数须为大于零的整数', trigger: ['blur', 'change']}],
subsidy: this.textRules('补助类型', 500),
subsidyStandards: this.textRules('困难类型', 500),
applyMoney: [{validator: this.validateMoney, required: true, trigger: ['blur', 'change']}],
reason: this.textRules('生活致困原因', 500),
sign: [{required: true, whitespace: true, message: '请完成签字', trigger: ['blur', 'change']}],
},
// 替他人申请相关
@@ -349,8 +365,21 @@ layout("/layouts/platform.html"){
}
},
methods: {
textRules(label, max) {
return [{required: true, whitespace: true, max, message: '请填写' + label + ',且不超过' + max + '个字符', trigger: ['blur', 'change']}]
},
validateIncome(rule, value, callback) {
const valid = typeof value === 'number' && Number.isFinite(value) && value >= 0 && /^\d+(\.\d{1,2})?$/.test(String(value))
callback(valid ? undefined : new Error('收入须为不小于零的数字,最多保留两位小数'))
},
validateMoney(rule, value, callback) {
const valid = typeof value === 'number' && Number.isFinite(value) && value > 0 && /^\d+(\.\d{1,2})?$/.test(String(value))
callback(valid ? undefined : new Error('申请金额须大于零,最多保留两位小数'))
},
// 保存
onSave() {
async onSave() {
const valid = await this.$refs.formRef.validate().catch(() => false)
if (!valid) return
this.$confirm("您确定保存吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
@@ -367,24 +396,13 @@ layout("/layouts/platform.html"){
// 提交
async onSubmit() {
this.$refs.formRef.validate().then((valid) => {
this.$refs.formRef.validate().catch(() => false).then((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const result = this.isValidIdCard(this.formData.idCard)
if (result.status === 0) {
this.$message.warning(result.msg)
return
}
const bankRes = this.validateCard(this.formData.bankCardNum)
if (bankRes.status === 0) {
this.$message.warning(bankRes.msg)
return;
}
this.$axios.post('/platform/difficultHelp/apply/submit', {
data: JSON.stringify(this.formData)
}).then(res => {
@@ -399,7 +417,7 @@ layout("/layouts/platform.html"){
},
async onFinishTask() {
this.$refs.formRef.validate().then((valid) => {
this.$refs.formRef.validate().catch(() => false).then((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
@@ -407,17 +425,6 @@ layout("/layouts/platform.html"){
type: "warning"
}).then(() => {
const result = this.isValidIdCard(this.formData.idCard)
if (result.status === 0) {
this.$message.warning(result.msg)
return
}
const bankRes = this.validateCard(this.formData.bankCardNum)
if (bankRes.status === 0) {
this.$message.warning(bankRes.msg)
return;
}
this.$axios.post('/platform/difficultHelp/apply/submitAgain', {
data: JSON.stringify(this.formData),
taskId: GetQueryString("taskId")
@@ -9,26 +9,30 @@ const PROPOSAL_INFO = {
<van-cell-group :border="false">
<van-cell title="提案编号">{{ viewData.code || '--' }}</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"
: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="提案类别">{{ viewData.typeName || '--' }}</van-cell>
<van-cell title="提案方式">{{ getDictText(dict.type.PROPOSAL_SOURCE, viewData.source) }}
</van-cell>
<van-cell title="代表姓名">{{ viewData.createUserName || '--' }}</van-cell>
<van-cell title="所属教代会">{{ viewData.sessionName || viewData.fullName || '--' }}</van-cell>
<van-cell :title="viewData.committeeName ? '所属委员会' : '所属代表团'">
<van-cell title="提案人">{{ viewData.createUserName || '--' }}</van-cell>
<van-cell title="教代会届次">{{ viewData.sessionName || viewData.fullName || '--' }}</van-cell>
<van-cell :title="viewData.committeeName ? '委员会名称' : '代表团名称'">
{{ viewData.committeeName || viewData.delegationName || '--' }}
</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.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>
</van-cell>
<van-cell title="建议措施" class="proposal-view-long-cell">
@@ -48,7 +52,7 @@ const PROPOSAL_INFO = {
:key="merge.id"
class="proposal-view-merge-cell"
: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
@click="openView(merge)">
</van-cell>
@@ -66,13 +70,16 @@ const PROPOSAL_INFO = {
<template v-if="opinion.commissionerLoginName">({{ opinion.commissionerLoginName }})
</template>
</van-cell>
<!-- 委员意见分别展示立案结果和类型,未填写时显示双横线。 -->
<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>
<span v-else>--</span>
</van-cell>
<van-cell title="立案类型" v-if="opinion.caseFilingType">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
<van-cell title="立案类型">
<dict-tag v-if="opinion.caseFilingType" :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="opinion.caseFilingType"></dict-tag>
<span v-else>--</span>
</van-cell>
<van-cell title="意见" v-if="opinion.opinionText" class="proposal-view-long-cell">
<div class="proposal-view-rich-value">{{ opinion.opinionText }}</div>
@@ -158,13 +165,18 @@ const PROPOSAL_INFO = {
|| '--' }})
</van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<!-- 立案节点分别展示流程记录中的结果与类型,空值显示双横线。 -->
<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>
<span v-else>--</span>
</van-cell>
<van-cell title="立案类型" v-if="selectedTask.ext.tf_caseFilingType">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
<van-cell title="立案类型">
<dict-tag v-if="selectedTask.ext.tf_caseFilingType"
:options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="selectedTask.ext.tf_caseFilingType"></dict-tag>
<span v-else>--</span>
</van-cell>
<template
v-if="['CONFIRM_FILING', 'SUGGESTION'].includes(selectedTask.ext.tf_caseFilingResult)">
@@ -187,7 +199,7 @@ const PROPOSAL_INFO = {
</template>
<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)">
{{ selectedTask.ext.underTakeName || '--' }}
</van-cell>
@@ -255,7 +255,7 @@ layout("/layouts/platform_h5.html"){
<van-empty description="暂无数据" v-else></van-empty>
</van-action-sheet>-->
<van-field
<!-- <van-field
v-model="formData.certifierUserName"
name="certifierUserName"
label="证明人"
@@ -288,7 +288,7 @@ layout("/layouts/platform_h5.html"){
</template>
</div>
<van-empty description="暂无数据" v-else></van-empty>
</van-action-sheet>
</van-action-sheet> -->
<van-field
v-model="formData.typeName"
@@ -322,7 +322,7 @@ layout("/layouts/platform_h5.html"){
name="money"
label="慰问金额"
required
:rules="[{ required: true }]"
:rules="[{ required: true, message: '请填写慰问金额' }, { validator: validateMoney, message: '慰问金额须大于0且不超过10000元,最多两位小数' }]"
placeholder="选择慰问类型自动匹配金额"
clickable
type="number"
@@ -353,17 +353,19 @@ layout("/layouts/platform_h5.html"){
</van-popup>
<van-field
v-if="!['A', 'D'].includes(formData.typeCode)"
v-model="formData.bankCardNumber"
label="收款账户"
maxlength="20"
maxlength="30"
clearable
name="bankCardNumber"
placeholder="请填写收款账户"
:rules="[{ required: true }]"
placeholder="请填写银行卡号或对公账户号"
:rules="[{ required: true, message: '请填写银行卡号或对公账户号' }, { pattern: /^[0-9]{6,30}$/, message: '收款账户须为6至30位数字(银行卡号或对公账户号)' }]"
required
></van-field>
<van-field
v-if="!['A', 'D'].includes(formData.typeCode)"
v-model="formData.bankUserName"
label="户名"
placeholder="请填写户名"
@@ -373,9 +375,10 @@ layout("/layouts/platform_h5.html"){
name="bankUserName"
></van-field>
<van-field
v-if="!['A', 'D'].includes(formData.typeCode)"
v-model="formData.bankOfDeposit"
label="开户行"
maxlength="30"
maxlength="20"
type="textarea"
rows="4"
autosize
@@ -433,8 +436,8 @@ layout("/layouts/platform_h5.html"){
></van-datetime-picker>
</van-popup>
<van-field type="digit" label="当年第几次住院"
:rules="[{ required: true }]"
<van-field type="digit" label="当年第几次住院" name="thisYearHospitalizationNum"
:rules="[{ required: true }, { validator: value => /^[0-9]+$/.test(String(value)) && Number(value) <= 10000, message: '住院次数须为0至10000的整数' }]"
required
placeholder="请输入当年第几次住院"
v-model="formData.thisYearHospitalizationNum"></van-field>
@@ -443,6 +446,7 @@ layout("/layouts/platform_h5.html"){
<van-field
v-model="formData.remark"
name="remark"
maxlength="500"
label="申请事由"
type="textarea"
rows="4"
@@ -460,15 +464,18 @@ layout("/layouts/platform_h5.html"){
{{ '附件说明:' + chooseType.uploadFileDesc }}
</div>
<van-cell-group :border="false">
<van-field class="direction-column-field" name="avatar" label="">
<van-field class="direction-column-field" name="files" label="附件" required
:rules="[{ validator: () => Array.isArray(formData.files) && formData.files.length > 0 && formData.files.length <= 5, message: '请上传1至5个附件' }]">
<template #input>
<h5-file-upload
slot="input"
:value.sync="formData.files"
:upload_number="5"
upload_text="上传附件"
upload_result_category="array"
complete_result
></h5-file-upload>
<div class="condolence-card__desc">最多上传 5 个文件</div>
</template>
</van-field>
</van-cell-group>
@@ -477,7 +484,8 @@ layout("/layouts/platform_h5.html"){
<section class="form-section">
<h2 class="condolence-card__title">签字确认</h2>
<van-cell-group :border="false">
<van-field class="direction-column-field" name="signature" label="">
<van-field class="direction-column-field" name="signature" label="" :value="formData.signature"
:rules="[{ required: true, message: '请填写签字' }]">
<template #input>
<h5-signature v-model="formData.signature" slot="input"></h5-signature>
</template>
@@ -532,6 +540,9 @@ layout("/layouts/platform_h5.html"){
}
},
methods: {
validateMoney(value) {
return /^(0|[1-9][0-9]*)(\.[0-9]{1,2})?$/.test(String(value)) && Number(value) > 0 && Number(value) <= 10000
},
// 选择出院时间
showLeaveHospitalTimePickerClick() {
if (this.formData.leaveHospitalTime) {
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
<h2 class="condolence-info-card__title">
审批签字<span class="condolence-audit-card__required">*</span>
</h2>
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
</van-field>
</van-form>
@@ -158,19 +158,44 @@
}
.condolence-info-long-cell {
display: flex;
align-items: flex-start;
}
.condolence-info-card .condolence-attachments .empty-container {
height: auto;
justify-content: flex-start;
color: inherit;
}
.condolence-info-card .condolence-attachments .empty-text {
font-size: inherit;
line-height: inherit;
}
.condolence-info-opinion > :first-child {
margin-top: 0;
}
.condolence-info-opinion > :last-child {
margin-bottom: 0;
}
.condolence-signature-image,
.condolence-approval-signature-image {
display: block;
}
.condolence-info-long-cell .van-cell__title {
width: auto;
width: 100%;
max-width: 100%;
height: 80px;
}
.condolence-info-long-cell .van-cell__value {
margin-top: 7px;
.condolence-signature-image .van-image__img,
.condolence-approval-signature-image {
object-fit: contain;
object-position: left center;
}
.condolence-signature-image {
display: block;
.condolence-info-opinion img {
max-width: 100%;
height: auto;
}
@@ -25,13 +25,13 @@ const condolenceInfo = {
<van-cell title="申请人单位">{{ viewData.applyUnitName || '--' }}</van-cell>
<van-cell title="申请人工会">{{ viewData.applyUnionName || '--' }}</van-cell>
<van-cell title="慰问对象">{{ viewData.helpUserName || '--' }}</van-cell>
<van-cell title="证明人">{{ viewData.certifierUserName || '--' }}</van-cell>
<!-- <van-cell title="证明人">{{ viewData.certifierUserName || '--' }}</van-cell> -->
<van-cell title="慰问类型">{{ viewData.typeName || '--' }}</van-cell>
<van-cell title="慰问金额">{{ viewData.money || '--' }}</van-cell>
<van-cell title="慰问时间">{{ viewData.occurTime || '--' }}</van-cell>
<van-cell title="收款账户">{{ viewData.bankCardNumber || '--' }}</van-cell>
<van-cell title="户名">{{ viewData.bankUserName || '--' }}</van-cell>
<van-cell class="condolence-info-long-cell" title="开户行">
<van-cell title="开户行">
{{ viewData.bankOfDeposit || '暂无' }}
</van-cell>
<template v-if="['2'].includes(viewData.typeCode)">
@@ -39,17 +39,18 @@ const condolenceInfo = {
<van-cell title="出院时间">{{ viewData.leaveHospitalTime || '--' }}</van-cell>
<van-cell title="当年第几次住院(次)">{{ viewData.thisYearHospitalizationNum || '--' }}</van-cell>
</template>
<van-cell class="condolence-info-long-cell" title="申请事由">
<van-cell title="申请事由">
{{ viewData.remark || '暂无' }}
</van-cell>
<van-cell class="condolence-info-long-cell" title="附件">
<file-preview :files="viewData.files" complete_result></file-preview>
<file-preview class="condolence-attachments" :files="viewData.files" complete_result></file-preview>
</van-cell>
<van-cell class="condolence-info-long-cell" title="签字">
<van-image
v-if="viewData.signature"
:src="viewData.signature"
class="condolence-signature-image"
fit="contain"
></van-image>
<span v-else>暂无签字</span>
</van-cell>
@@ -86,10 +87,10 @@ const condolenceInfo = {
></dict-tag>
</van-cell>
<van-cell v-if="task.taskFormData && task.taskFormData.sign" title="审批签字">
<img :src="task.taskFormData.sign" style="max-width: 100%; height: 80px; object-fit: contain">
<img :src="task.taskFormData.sign" class="condolence-approval-signature-image">
</van-cell>
<van-cell class="condolence-info-long-cell" title="办理意见">
<div v-html="task.taskFormData && task.taskFormData.tf_opinion || '暂无'"></div>
<van-cell title="办理意见">
<div class="condolence-info-opinion" v-html="task.taskFormData && task.taskFormData.tf_opinion || '暂无'"></div>
</van-cell>
</van-cell-group>
</section>
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
<h2 class="condolence-info-card__title">
审批签字<span class="condolence-audit-card__required">*</span>
</h2>
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
</van-field>
</van-form>
@@ -223,6 +223,23 @@ layout("/layouts/platform_h5.html"){
},
onReady() {
this.queryCondolenceType()
this.openTodoTask()
},
openTodoTask() {
const taskId = GetQueryString("taskId")
const bizId = GetQueryString("bizId")
if (!taskId || !bizId) return
this.$axios.post("/flow/common/taskInfo", {taskId}).then((res) => {
if (res.code !== 0 || !res.data) return
const task = res.data
if (task.taskName !== "cbd41337-3c87-4959-9027-809df5c7ef5e") return
const row = {id: bizId, taskId: task.id, curTaskName: task.displayName}
if (task.taskState === 10 && task.executable && GetQueryString("tab") === "todo") {
this.onApproval(row)
} else {
this.onView(row)
}
})
},
onView(row) {
this.$set(this, "showApprovalForm", false)
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
<h2 class="condolence-info-card__title">
审批签字<span class="condolence-audit-card__required">*</span>
</h2>
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
</van-field>
</van-form>
@@ -151,7 +151,7 @@ layout("/layouts/platform_h5.html"){
<h2 class="condolence-info-card__title">
审批签字<span class="condolence-audit-card__required">*</span>
</h2>
<van-field name="tf_sign" :value="formData.tf_sign" required :rules="[{required:true,message:'请完成审批签字'}]">
<van-field name="tf_sign" :value="formData.tf_sign" :rules="[{required:true,message:'请完成审批签字'}]">
<template #input><h5-signature v-model="formData.tf_sign" :key="formData.processTaskId"></h5-signature></template>
</van-field>
</van-form>
@@ -281,11 +281,13 @@ const todo = {
// 处理任务
onView(task) {
const {taskId, taskKey, instanceId, businessNo, formKey, h5FormKey} = task;
if (!h5FormKey) {
const mobileFormKey = h5FormKey || (taskKey === "cbd41337-3c87-4959-9027-809df5c7ef5e"
? "/platform/condolence/fourthApproval/h5" : "");
if (!mobileFormKey) {
this.$toast("当前流程暂不支持手机端审核,请前往PC端页面审核!");
return;
}
this.$pjaxReplace(h5FormKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
this.$pjaxReplace(mobileFormKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
},
// 根据当前列表类型统一展示流程状态,发起列表保留流程实例的实际状态。