Author SHA1 Message Date
c-chengc1 c3ea84bfad fix:20260904代码扫描漏洞问题修复 2026-09-11 16:28:57 +08:00
c-chengc1 61b8819057 fix:20260904代码扫描漏洞问题修复 2026-09-11 14:04:30 +08:00
c-chengc1 01921f7fcf fix:20260904代码扫描漏洞问题修复 2026-09-09 10:55:31 +08:00
c-chengc1 465a3257b5 fix:20260904代码扫描漏洞问题修复 2026-09-09 10:25:53 +08:00
c-chengc1 45410cb0a3 Merge remote-tracking branch 'origin/feature_工会报销UI优化' into feature_工会报销UI优化 2026-09-04 16:16:09 +08:00
c-hongqw1 6ef45e5403 fix:1、提案查看的流程节点,增加排序
2、场地预约关闭按钮无法关闭的bug修复
2026-09-04 16:15:55 +08:00
c-hongqw1 4235deede7 地大bug整改 2026-09-04 16:07:12 +08:00
c-chengc1 43ae24dceb Merge remote-tracking branch 'origin/feature_程诚_0831问题修复' into release_20260831 2026-09-02 16:27:12 +08:00
c-chengc1 cccad1a852 fix:职代会pc端修复投票纪录无法正常显示工会、代表团 2026-09-02 16:26:38 +08:00
c-chengc1 0b3fef4d71 Merge remote-tracking branch 'origin/feature_工会报销UI优化' into release_20260831 2026-09-02 11:43:11 +08:00
c-hongqw1 0b44f36c83 手机端社团、工会报销bug整改 2026-09-02 10:39:56 +08:00
c-chengc1 2de6820c1a Merge remote-tracking branch 'origin/feature_周何峰' into release_20260831 2026-09-02 09:20:52 +08:00
c-zhouhf1 478b70c2d1 整改微信小程序登录
整改测试提案UI
整改测试作品征集UI
2026-09-01 18:00:36 +08:00
c-chengc1 d305317458 Merge branch 'feature_程诚_0831问题修复' into release_20260831 2026-09-01 16:00:47 +08:00
c-chengc1 982065c424 feat:职工福利PC端系统默认选择改为复选框组件;职代会PC端投票统计结果显示不正确问题修复;体育活动h5分工会报名权限提示;品牌活动h5联系人字段超长样式适配修复; 2026-09-01 16:00:11 +08:00
c-chengc1 a92f75730d feat:职工福利ui优化,我的福利支持修改福利 2026-09-01 15:04:19 +08:00
c-zhouhf1 8a0714a8e3 Merge branch 'release_20260831' into feature_周何峰 2026-08-31 15:20:19 +08:00
c-zhouhf1 bf40155fb9 品牌活动报名统计增加校工会活动管理员看全部活动
不是超级管理员加不了角色
2026-08-31 15:18:29 +08:00
113 changed files with 3256 additions and 679 deletions
+33
View File
@@ -133,6 +133,38 @@
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
<artifactId>nutzboot-parent</artifactId> <artifactId>nutzboot-parent</artifactId>
@@ -151,6 +183,7 @@
<easypoi.version>4.5.0</easypoi.version> <easypoi.version>4.5.0</easypoi.version>
<jsoup.veision>1.15.3</jsoup.veision> <jsoup.veision>1.15.3</jsoup.veision>
<mysql-connector-java.version>8.0.28</mysql-connector-java.version> <mysql-connector-java.version>8.0.28</mysql-connector-java.version>
<tomcat.version>9.0.121</tomcat.version>
<sa-token.version>1.25.0</sa-token.version> <sa-token.version>1.25.0</sa-token.version>
<easy-captcha.version>1.6.2</easy-captcha.version> <easy-captcha.version>1.6.2</easy-captcha.version>
<emoji-java.veision>4.0.0</emoji-java.veision> <emoji-java.veision>4.0.0</emoji-java.veision>
+50 -1
View File
@@ -7,6 +7,7 @@
<properties> <properties>
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version> <nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
<nutz.version>1.r.70-SNAPSHOT</nutz.version> <nutz.version>1.r.70-SNAPSHOT</nutz.version>
<tomcat.version>9.0.121</tomcat.version>
<mysql-connector-java.version>8.0.28</mysql-connector-java.version> <mysql-connector-java.version>8.0.28</mysql-connector-java.version>
<jaxb-api.version>2.3.1</jaxb-api.version> <jaxb-api.version>2.3.1</jaxb-api.version>
<slf4j.version>2.0.8</slf4j.version> <slf4j.version>2.0.8</slf4j.version>
@@ -144,8 +145,22 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-jetty</artifactId> <artifactId>nutzboot-starter-tomcat</artifactId>
<version>${nutzboot.version}</version> <version>${nutzboot.version}</version>
<exclusions>
<exclusion>
<groupId>org.gandon.tomcat</groupId>
<artifactId>juli-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutz-plugins-websocket</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
@@ -424,6 +439,40 @@
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<!-- Servlet API 由 Tomcat 提供,防止旧 API 混入可执行 JAR。 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- 显式覆盖导入的 NutzBoot BOM,避免回退到其默认 Tomcat 8.5。 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
<artifactId>nutzboot-parent</artifactId> <artifactId>nutzboot-parent</artifactId>
@@ -11,6 +11,7 @@ import com.budwk.app.web.commons.auth.satoken.SaTokenDaoRedisImpl;
import com.budwk.app.web.commons.auth.satoken.StpInterfaceImpl; import com.budwk.app.web.commons.auth.satoken.StpInterfaceImpl;
import com.budwk.app.web.commons.base.Globals; import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.web.commons.ext.pubsub.WebPubSub; import com.budwk.app.web.commons.ext.pubsub.WebPubSub;
import com.budwk.app.web.commons.ext.handler.WkTomcatConfiguration;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService; import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.beetl.core.GroupTemplate; import org.beetl.core.GroupTemplate;
@@ -65,6 +66,7 @@ public class MainLauncher {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
NbApp nb = new NbApp().setArgs(args).setPrintProcDoc(true); NbApp nb = new NbApp().setArgs(args).setPrintProcDoc(true);
nb.getAppContext().setMainPackage("com.budwk"); nb.getAppContext().setMainPackage("com.budwk");
nb.addListener(new WkTomcatConfiguration());
nb.run(); nb.run();
} }
@@ -93,6 +93,15 @@ public class FlowTodoCenterController {
return Result.success(pagination); return Result.success(pagination);
} }
/**
* 分页查询当前用户的已办任务,仅返回列表展示及详情跳转所需的信息。
*
* @param pageNumber 查询页码
* @param pageSize 每页记录数
* @param searchKeyword 流程名称查询关键字
* @param category 流程分类
* @return 已办任务分页结果,不包含完整流程变量和任务表单数据
*/
@At @At
@SaCheckLogin @SaCheckLogin
public Result done(Integer pageNumber, Integer pageSize, String searchKeyword, String category) { public Result done(Integer pageNumber, Integer pageSize, String searchKeyword, String category) {
@@ -106,8 +115,13 @@ public class FlowTodoCenterController {
t.h5FormKey, t.h5FormKey,
t.createdAt, t.createdAt,
t.finishTime, t.finishTime,
t.variable AS taskVariable, -- 保留任务变量字段类型,但已办列表不返回其中的业务表单数据。
ins.variable, '{}' AS taskVariable,
-- 列表仅需流程名称和申请人,避免完整变量中的身份证等信息泄露。
JSON_OBJECT(
'instanceName', ins.variable -> '$.instanceName',
'initiatorName', ins.variable -> '$.initiatorName'
) AS variable,
ins.state AS instanceState, ins.state AS instanceState,
ins.id AS instanceId, ins.id AS instanceId,
ins.businessNo, ins.businessNo,
@@ -23,6 +23,7 @@ import com.budwk.app.flow.entity.ProcessCcInstance;
import com.budwk.app.flow.entity.ProcessDefine; import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum; import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
@@ -556,11 +557,18 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
public void deleteProcessInstanceById(Long processInstanceId) { public void deleteProcessInstanceById(Long processInstanceId) {
List<ProcessTask> taskList = dao().query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId)); List<ProcessTask> taskList = dao().query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId));
List<Long> taskIds = taskList.stream().map(ProcessTask::getId).toList(); List<Long> taskIds = taskList.stream().map(ProcessTask::getId).toList();
// 任务参与人依赖流程任务,删除流程时先清理参与人,避免遗留无法回查所属流程的孤立记录。
if (CollectionUtil.isNotEmpty(taskIds)) {
dao().clear(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "in", taskIds));
dao().clear(ProcessTask.class, Cnd.where(ProcessTask::getId, "in", taskIds)); dao().clear(ProcessTask.class, Cnd.where(ProcessTask::getId, "in", taskIds));
}
// 抄送记录直接依赖流程实例,需要与实例一并清理。
dao().clear(ProcessCcInstance.class, Cnd.where(ProcessCcInstance::getProcessInstanceId, "=", processInstanceId));
dao().clear(ProcessInstance.class, Cnd.where(ProcessInstance::getId, "=", processInstanceId)); dao().clear(ProcessInstance.class, Cnd.where(ProcessInstance::getId, "=", processInstanceId));
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void deleteProcessInstanceByBusinessKey(String businessKey) { public void deleteProcessInstanceByBusinessKey(String businessKey) {
ProcessInstance processInstance = fetch(Cnd.where(ProcessInstance::getBusinessNo, "=", businessKey)); ProcessInstance processInstance = fetch(Cnd.where(ProcessInstance::getBusinessNo, "=", businessKey));
if (processInstance != null) { if (processInstance != null) {
@@ -33,6 +33,10 @@ import org.nutz.mvc.upload.TempFile;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -43,6 +47,9 @@ import java.nio.file.StandardOpenOption;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Iterator;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -54,6 +61,9 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
static String IMG_BASE64_PATTERN = "<img\\s+[^>]*src\\s*=\\s*['\"](data:image/[^'\"]+;base64,[^'\"]+)['\"][^>]*>"; static String IMG_BASE64_PATTERN = "<img\\s+[^>]*src\\s*=\\s*['\"](data:image/[^'\"]+;base64,[^'\"]+)['\"][^>]*>";
private static final int DOWNLOAD_FILE_CACHE_LIMIT = 2000; private static final int DOWNLOAD_FILE_CACHE_LIMIT = 2000;
private final Map<String, Sys_file> downloadFileCache = new ConcurrentHashMap<>(); private final Map<String, Sys_file> downloadFileCache = new ConcurrentHashMap<>();
private static final Set<String> UPLOAD_SUFFIXES = Set.of("gif", "jpg", "jpeg", "png", "doc", "docx",
"xls", "xlsx", "rar", "zip", "7z", "txt", "pdf", "pptx", "ppt", "mp4");
private static final Set<String> ACTIVE_SUFFIXES = Set.of("svg", "svgz", "html", "htm", "xhtml", "xml", "js", "mjs", "swf");
public SysFileServiceImpl(Dao dao) { public SysFileServiceImpl(Dao dao) {
super(dao); super(dao);
@@ -81,11 +91,13 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
@Override @Override
public void download(String id, HttpServletRequest request, HttpServletResponse response) throws IOException { public void download(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
if (handleCachedFileRequest(id, request, response)) { response.setHeader("X-Content-Type-Options", "nosniff");
return; response.setHeader("Cache-Control", "private, no-store");
}
Sys_file sys_file = fetchDownloadFile(id); Sys_file sys_file = fetchDownloadFile(id);
if (!ObjectUtil.isEmpty(sys_file) && handleCachedImageRequest(sys_file, request, response)) { // 历史主动内容也必须在缓存协商前拦截,避免旧 SVG 继续在业务域下执行。
if (sys_file != null && (ACTIVE_SUFFIXES.contains(StrUtil.blankToDefault(sys_file.getSuffix(), "").toLowerCase(Locale.ROOT))
|| ACTIVE_SUFFIXES.contains(FileUtil.extName(sys_file.getName()).toLowerCase(Locale.ROOT)))) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "不允许访问此类型的附件");
return; return;
} }
if (ObjectUtil.isEmpty(sys_file)) { if (ObjectUtil.isEmpty(sys_file)) {
@@ -112,16 +124,6 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
} }
private boolean handleCachedFileRequest(String id, HttpServletRequest request, HttpServletResponse response) {
String etag = buildFileEtag(id);
if (etag.equals(request.getHeader("If-None-Match"))) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
setImageCacheHeaders(response, etag);
return true;
}
return false;
}
private Sys_file fetchDownloadFile(String id) { private Sys_file fetchDownloadFile(String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
return null; return null;
@@ -140,23 +142,15 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
return sysFile; return sysFile;
} }
private boolean handleCachedImageRequest(Sys_file sysFile, HttpServletRequest request, HttpServletResponse response) {
if (!isImage(sysFile)) {
return false;
}
String etag = buildFileEtag(sysFile);
if (etag.equals(request.getHeader("If-None-Match"))) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
setImageCacheHeaders(response, etag);
return true;
}
return false;
}
private boolean writeImageResponse(Sys_file sysFile, byte[] bytes, HttpServletResponse response) throws IOException { private boolean writeImageResponse(Sys_file sysFile, byte[] bytes, HttpServletResponse response) throws IOException {
if (!isImage(sysFile)) { if (!isImage(sysFile)) {
return false; return false;
} }
String suffix = StrUtil.blankToDefault(sysFile.getSuffix(), FileUtil.extName(sysFile.getName())).toLowerCase(Locale.ROOT);
if (!isValidImage(bytes, suffix)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "图片内容与文件类型不符");
return true;
}
setImageCacheHeaders(response, buildFileEtag(sysFile)); setImageCacheHeaders(response, buildFileEtag(sysFile));
response.setHeader("Content-Disposition", "inline;filename=" + java.net.URLEncoder.encode(sysFile.getName(), java.nio.charset.StandardCharsets.UTF_8)); response.setHeader("Content-Disposition", "inline;filename=" + java.net.URLEncoder.encode(sysFile.getName(), java.nio.charset.StandardCharsets.UTF_8));
response.setHeader("Content-Length", String.valueOf(bytes.length)); response.setHeader("Content-Length", String.valueOf(bytes.length));
@@ -166,9 +160,9 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
private void setImageCacheHeaders(HttpServletResponse response, String etag) { private void setImageCacheHeaders(HttpServletResponse response, String etag) {
response.setHeader("Cache-Control", "public, max-age=604800, immutable"); response.setHeader("Cache-Control", "private, no-cache");
response.setHeader("ETag", etag); response.setHeader("ETag", etag);
response.setDateHeader("Expires", System.currentTimeMillis() + 604800000L); response.setDateHeader("Expires", 0);
} }
private String buildFileEtag(Sys_file sysFile) { private String buildFileEtag(Sys_file sysFile) {
@@ -176,12 +170,12 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
private String buildFileEtag(String id) { private String buildFileEtag(String id) {
return "\"" + id + "\""; return "\"safe-" + id + "\"";
} }
private boolean isImage(Sys_file sysFile) { private boolean isImage(Sys_file sysFile) {
String suffix = StrUtil.blankToDefault(sysFile.getSuffix(), FileUtil.extName(sysFile.getName())).toLowerCase(); String suffix = StrUtil.blankToDefault(sysFile.getSuffix(), FileUtil.extName(sysFile.getName())).toLowerCase();
return "jpg".equals(suffix) || "jpeg".equals(suffix) || "png".equals(suffix) || "gif".equals(suffix) || "webp".equals(suffix) || "bmp".equals(suffix) || "svg".equals(suffix); return "jpg".equals(suffix) || "jpeg".equals(suffix) || "png".equals(suffix) || "gif".equals(suffix) || "webp".equals(suffix) || "bmp".equals(suffix);
} }
private String getImageContentType(Sys_file sysFile) { private String getImageContentType(Sys_file sysFile) {
@@ -192,7 +186,6 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
case "gif" -> "image/gif"; case "gif" -> "image/gif";
case "webp" -> "image/webp"; case "webp" -> "image/webp";
case "bmp" -> "image/bmp"; case "bmp" -> "image/bmp";
case "svg" -> "image/svg+xml";
default -> "application/octet-stream"; default -> "application/octet-stream";
}; };
} }
@@ -305,6 +298,7 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
* 存储文件 * 存储文件
**/ **/
private String storageFile(String engine, TempFile file, boolean returnFileId) { private String storageFile(String engine, TempFile file, boolean returnFileId) {
validateUpload(file);
// 如果引擎为空,默认使用本地 // 如果引擎为空,默认使用本地
if (ObjectUtil.isEmpty(engine)) { if (ObjectUtil.isEmpty(engine)) {
engine = SysFileEngineTypeEnum.LOCAL.getValue(); engine = SysFileEngineTypeEnum.LOCAL.getValue();
@@ -378,6 +372,53 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
} }
/** 所有业务上传共用校验,客户端声明的 MIME 类型不能作为图片真实性依据。 */
private void validateUpload(TempFile file) {
if (file == null || StrUtil.isBlank(file.getSubmittedFileName())) {
throw new BaseException("请选择要上传的文件");
}
String suffix = FileUtil.extName(file.getSubmittedFileName()).toLowerCase(Locale.ROOT);
if (!UPLOAD_SUFFIXES.contains(suffix)) {
throw new BaseException("不允许上传此类型的文件");
}
if (Set.of("gif", "jpg", "jpeg", "png").contains(suffix)) {
try {
if (!isValidImage(Files.readAllBytes(file.getFile().toPath()), suffix)) {
throw new BaseException("图片内容与文件类型不符");
}
} catch (IOException e) {
throw new BaseException("图片读取失败");
}
}
}
/** 解码真实图片并限制像素数量,阻止伪装为图片的 SVG 和异常大尺寸图片。 */
private boolean isValidImage(byte[] bytes, String suffix) {
try (ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes))) {
if (input == null) {
return false;
}
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (!readers.hasNext()) {
return false;
}
ImageReader reader = readers.next();
try {
reader.setInput(input, true, true);
String expected = "jpg".equals(suffix) ? "jpeg" : suffix;
if (!expected.equalsIgnoreCase(reader.getFormatName())) {
return false;
}
long pixels = (long) reader.getWidth(0) * reader.getHeight(0);
return pixels > 0 && pixels <= 40000000 && reader.read(0) != null;
} finally {
reader.dispose();
}
} catch (IOException | RuntimeException e) {
return false;
}
}
/** /**
* 存储文件 * 存储文件
*/ */
@@ -10,6 +10,9 @@ import cn.dev33.satoken.servlet.model.SaStorageForServlet;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.Mvcs; import org.nutz.mvc.Mvcs;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponseWrapper;
/** /**
* @author wizzer@qq.com * @author wizzer@qq.com
*/ */
@@ -22,7 +25,20 @@ public class SaTokenContextImpl implements SaTokenContext {
@Override @Override
public SaResponse getResponse() { public SaResponse getResponse() {
return new SaResponseForServlet(Mvcs.getResp()); // 当前 Sa-Token 版本没有 Cookie 安全属性配置,在写入登录 Cookie 时统一补齐。
return new SaResponseForServlet(new HttpServletResponseWrapper(Mvcs.getResp()) {
/**
* 限制登录 Cookie 的脚本访问,并在 HTTPS 请求中仅允许安全传输。
*
* @param cookie Sa-Token 写入或清除的 Cookie
*/
@Override
public void addCookie(Cookie cookie) {
cookie.setHttpOnly(true);
cookie.setSecure(cookie.getSecure() || Mvcs.getReq().isSecure());
super.addCookie(cookie);
}
});
} }
@Override @Override
@@ -15,6 +15,10 @@ import java.lang.reflect.Method;
public class SaCheckLoginInterceptor implements MethodInterceptor { public class SaCheckLoginInterceptor implements MethodInterceptor {
@Override @Override
public void filter(InterceptorChain chain) throws Throwable { public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod(); Method method = chain.getCallingMethod();
SaCheckLogin at = method.getAnnotation(SaCheckLogin.class); SaCheckLogin at = method.getAnnotation(SaCheckLogin.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at); SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -15,6 +15,10 @@ import java.lang.reflect.Method;
public class SaCheckPermissionInterceptor implements MethodInterceptor { public class SaCheckPermissionInterceptor implements MethodInterceptor {
@Override @Override
public void filter(InterceptorChain chain) throws Throwable { public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod(); Method method = chain.getCallingMethod();
SaCheckPermission at = method.getAnnotation(SaCheckPermission.class); SaCheckPermission at = method.getAnnotation(SaCheckPermission.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at); SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -15,6 +15,10 @@ import java.lang.reflect.Method;
public class SaCheckRoleInterceptor implements MethodInterceptor { public class SaCheckRoleInterceptor implements MethodInterceptor {
@Override @Override
public void filter(InterceptorChain chain) throws Throwable { public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod(); Method method = chain.getCallingMethod();
SaCheckRole at = method.getAnnotation(SaCheckRole.class); SaCheckRole at = method.getAnnotation(SaCheckRole.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at); SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -0,0 +1,60 @@
package com.budwk.app.web.commons.auth.satoken.aop;
import com.budwk.app.web.commons.base.Globals;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.mvc.Mvcs;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
* Sa-Token 注解认证白名单工具。
* <p>
* 用于集中维护不需要登录、权限、角色校验即可访问的 URL,匹配规则与旧 Shiro 体系中的
* authIgnoreUrlArr 保持一致:数组中的每一项都按正则表达式与当前请求 URI 完整匹配。
*/
public class SaTokenAuthIgnoreUtil {
/**
* 不需要认证授权的 URL。
* <p>
* 参数说明:数组中的字符串为请求 URI 正则表达式,匹配的是去除项目 contextPath 后的路径。
* 返回值说明:通过 {@link #isIgnore()} 返回 booleantrue 表示当前请求跳过 Sa-Token 注解校验。
*/
private static final String[] AUTH_IGNORE_URL_ARR = Lang.array("/",
"/platform/activity/basic/scope/getScopeUser",
"/platform/fitnessWalk/stepManage/getActivityDateStep",
"/platform/fitnessWalk/stepManage/winningRecord",
"/platform/fitnessWalk/stepManage/updateStepMonth",
"/platform/fitnessWalk/stepRanking/getUserStepRanking",
"/platform/fitnessWalk/stepManage/getActivityQualifyProgressBar",
"/platform/fitnessWalk/stepWining/getUserStepWining",
"/platform/fitnessWalk/common/getIssueById",
"/platform/fitnessWalk/common/saveAnswer",
"/platform/fitnessWalk/stepWining/prizeOption",
"/platform/fitnessWalk/stepWining/prizeUsers",
"/platform/fitnessWalk/punchLottery/judgeWinningToPunch",
"/platform/fitnessWalk/punchLottery/getLotteryRecord",
"/platform/fitnessWalk/punchLottery/doExchange",
"/platform/fitnessWalk/punchLottery/getPunchWining",
"/platform/fitnessWalk/punchLottery/doReadLottery",
"/platform/fitnessWalk/activityRelation/isCurrentUserRelationActivityQualified",
"/platform/fitnessWalk/welfare/addWelfareList",
"/platform/fitnessWalk/weapp/.*");
/**
* 判断当前请求是否命中匿名访问白名单。
*
* @return boolean true 表示当前 URL 不需要执行登录、权限、角色注解校验;false 表示继续走原有 Sa-Token 校验逻辑
*/
public static boolean isIgnore() {
HttpServletRequest request = Mvcs.getReq();
if (request == null) {
return false;
}
String requestURI = Strings.sNull(request.getRequestURI()).replaceFirst("^" + Pattern.quote(Strings.sNull(Globals.AppBase)), "");
return Arrays.stream(AUTH_IGNORE_URL_ARR).anyMatch(v -> Pattern.compile(v).matcher(requestURI).matches());
}
}
@@ -1,7 +1,6 @@
package com.budwk.app.web.commons.ext.handler; package com.budwk.app.web.commons.ext.handler;
import org.eclipse.jetty.server.Request; import org.nutz.boot.starter.WebServletFace;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json; import org.nutz.json.Json;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -9,28 +8,63 @@ import org.nutz.log.Log;
import org.nutz.log.Logs; import org.nutz.log.Logs;
import javax.servlet.RequestDispatcher; import javax.servlet.RequestDispatcher;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServlet;
import java.io.IOException; import java.io.IOException;
/** /**
* 错误页拦截器,登陆后台显示友好提示 * 错误页拦截器,登陆后台显示友好提示
*/ */
@IocBean @IocBean
public class WkErrorPageHandler extends ErrorPageErrorHandler { public class WkErrorPageHandler extends HttpServlet implements WebServletFace {
private static final Log log = Logs.get(); private static final Log log = Logs.get();
/**
* 返回容器错误处理 Servlet 的注册名称。
* @return 独立于业务路由的 Servlet 名称
*/
@Override @Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { public String getName() {
return "wkErrorPage";
}
/**
* 返回容器错误页的内部派发地址。
* @return 错误页映射路径
*/
@Override
public String getPathSpec() {
return "/__container_error";
}
/**
* 将当前错误处理实例交给 NutzBoot 注册。
* @return 当前 Servlet 实例
*/
@Override
public Servlet getServlet() {
return this;
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if (request.getDispatcherType() != DispatcherType.ERROR) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (response.getStatus() == 403 || response.getStatus() == 404 || response.getStatus() == 500) { if (response.getStatus() == 403 || response.getStatus() == 404 || response.getStatus() == 500) {
try { try {
if (isAjax(request)) { if (isAjax(request)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(Json.toJson(new NutMap("code", "-1").setv("msg", response.getStatus() + " error"))); response.getWriter().write(Json.toJson(new NutMap("code", "-1").setv("msg", response.getStatus() + " error")));
return; return;
} else { } else {
request.setAttribute("original_request_uri", request.getRequestURI()); request.setAttribute("original_request_uri", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI));
RequestDispatcher rd = request.getRequestDispatcher("/platform/home/" + response.getStatus()); RequestDispatcher rd = request.getRequestDispatcher("/platform/home/" + response.getStatus());
rd.forward(request, response); rd.forward(request, response);
return; return;
@@ -39,7 +73,7 @@ public class WkErrorPageHandler extends ErrorPageErrorHandler {
log.error(e); log.error(e);
} }
} }
super.handle(target, baseRequest, request, response); // 不回落到容器默认错误页,避免泄露异常堆栈和容器信息。
} }
private boolean isAjax(ServletRequest req) { private boolean isAjax(ServletRequest req) {
@@ -0,0 +1,88 @@
package com.budwk.app.web.commons.ext.handler;
import com.budwk.app.web.commons.ext.websocket.WkWebSocket;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.Executor;
import org.apache.catalina.core.StandardThreadExecutor;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.valves.ErrorReportValve;
import org.apache.tomcat.websocket.server.WsSci;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.nutz.boot.NbApp;
import org.nutz.boot.starter.tomcat.TomcatStarter;
import org.nutz.boot.tools.NbAppEventListener;
import org.nutz.ioc.impl.PropertiesProxy;
import java.util.Set;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* 在容器启动前补齐 NutzBoot Tomcat 的 WebSocket 注册和线程池绑定。
*/
public class WkTomcatConfiguration implements NbAppEventListener {
/**
* 配置官方嵌入式 Tomcat,保留项目的监听地址与消息推送能力。
*
* @param app 当前 NutzBoot 应用
* @param eventType 只在服务器启动前配置,避免重复注册端点
*/
@Override
public void whenStartServers(NbApp app, EventType eventType) {
if (eventType != EventType.before) {
return;
}
TomcatStarter starter = app.getAppContext().getIoc().get(TomcatStarter.class);
Tomcat tomcat = starter.getServer();
PropertiesProxy conf = app.getAppContext().getConf();
tomcat.getConnector().setProperty("address", starter.getHost());
// Starter 创建了共享线程池,但尚未把它交给 Connector 使用。
for (Executor executor : tomcat.getService().findExecutors()) {
if (executor instanceof StandardThreadExecutor) {
StandardThreadExecutor threads = (StandardThreadExecutor) executor;
threads.setMinSpareThreads(conf.getInt("tomcat.executor.minSpareThreads", 10));
threads.setMaxIdleTime(conf.getInt("tomcat.executor.maxIdleTime", 60000));
tomcat.getConnector().getProtocolHandler().setExecutor(threads);
break;
}
}
// NutzBoot 使用编程式 Context,必须显式运行官方 WebSocket 初始化器。
StandardContext context = (StandardContext) tomcat.getHost().findChild(starter.getContextPath());
context.setUseHttpOnly(conf.getBoolean("tomcat.session.cookie.httponly", true));
context.addServletContainerInitializer(new WsSci(), Set.of(WkWebSocket.class));
// Tomcat 的错误页再次 forward 时仍保持 ERROR 派发;让 MVC 渲染原业务错误页。
// Nutz 的过滤器在前一个监听器中注册,须等其完成后才能添加映射。
context.addApplicationLifecycleListener(new ServletContextListener() {
/**
* 在 Nutz 注册完成后补充业务错误页的 ERROR 派发映射。
* @param event 当前 Servlet 上下文初始化事件
*/
@Override
public void contextInitialized(ServletContextEvent event) {
FilterMap errorPages = new FilterMap();
errorPages.setFilterName("nutz");
errorPages.setDispatcher("ERROR");
// ERROR 再转发时 URL 匹配路径不变,按目标 Servlet 匹配才能进入 MVC。
errorPages.addServletName("default");
context.addFilterMap(errorPages);
}
});
// Host 的兜底错误页也不能重新暴露此前已关闭的版本和堆栈。
ErrorReportValve errorReport = null;
for (org.apache.catalina.Valve valve : tomcat.getHost().getPipeline().getValves()) {
if (valve instanceof ErrorReportValve) {
errorReport = (ErrorReportValve) valve;
break;
}
}
if (errorReport == null) {
errorReport = new ErrorReportValve();
tomcat.getHost().getPipeline().addValve(errorReport);
}
errorReport.setShowReport(false);
errorReport.setShowServerInfo(false);
}
}
@@ -162,27 +162,10 @@ public class ActivityWorksCollectionManageController {
@SaCheckPermission(value = {"activity.workscollection.manage", "activity.workscollection.new"}, mode = SaMode.OR) @SaCheckPermission(value = {"activity.workscollection.manage", "activity.workscollection.new"}, mode = SaMode.OR)
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Result update(@Param("data") @Valid Activity_works_collection worksCollection) { public Result update(@Param("data") @Valid Activity_works_collection worksCollection) {
String validateMessage = activityWorksCollectionService.validateReferencedTypesBeforeUpdate(worksCollection); String validateMessage = activityWorksCollectionService.updatePreservingTypeIds(worksCollection);
if (StrUtil.isNotBlank(validateMessage)) { if (StrUtil.isNotBlank(validateMessage)) {
return Result.error(validateMessage); return Result.error(validateMessage);
} }
dao.update(worksCollection);
dao.updateLinks(worksCollection, "subjectTypes");
dao.insertLinks(worksCollection, "subjectTypes");
//清除页面已经删除的
dao.clear(Activity_works_subjectType.class,
Cnd.where(Activity_works_subjectType::getActivityId, "=", worksCollection.getId())
.andEX("id", "not in", worksCollection.getSubjectTypes().stream().map(Activity_works_subjectType::getId).toList()));
worksCollection.getSubjectTypes().forEach(item -> {
dao.updateLinks(item, "worksTypes");
dao.insertLinks(item, "worksTypes");
//清除页面已经删除的
List<String> workIds = item.getWorksTypes().stream().map(Activity_works_worksType::getId).toList();
dao.clear(Activity_works_worksType.class,
Cnd.where(Activity_works_worksType::getId, "not in ", workIds)
.and(Activity_works_worksType::getSubjectId,"=",item.getId()));
});
syncHomeActivity(worksCollection); syncHomeActivity(worksCollection);
return Result.success(); return Result.success();
} }
@@ -35,6 +35,14 @@ public interface ActivityWorksCollectionService extends BaseService<Activity_wor
*/ */
void cancelTemplate(String id); void cancelTemplate(String id);
/**
* 更新作品征集活动及其主题、作品类型,并保留所有原有类型ID。
*
* @param worksCollection 前端提交的活动及其主题、作品类型配置
* @return 更新成功返回 {@code null};数据层级异常或删除了投稿引用类型时返回提示信息
*/
String updatePreservingTypeIds(Activity_works_collection worksCollection);
/** /**
* 校验编辑活动时删除的主题类型、作品类型是否已被投稿引用。 * 校验编辑活动时删除的主题类型、作品类型是否已被投稿引用。
* *
@@ -21,7 +21,9 @@ import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -116,6 +118,136 @@ public class ActivityWorksCollectionServiceImpl extends BaseServiceImpl<Activity
Cnd.where("id", "=", id)); Cnd.where("id", "=", id));
} }
/**
* 活动编辑采用显式新增、更新、删除,避免通用关联保存重新生成已有类型ID。
* 原有主题和作品类型必须携带数据库ID;只有页面新增的类型允许生成新ID。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public String updatePreservingTypeIds(Activity_works_collection worksCollection) {
if (worksCollection == null || StrUtil.isBlank(worksCollection.getId())) {
return "活动数据不存在,请刷新页面后重试";
}
Activity_works_collection persistedActivity = dao.fetch(Activity_works_collection.class, worksCollection.getId());
if (persistedActivity == null) {
return "活动不存在或已被删除";
}
List<Activity_works_subjectType> submittedSubjectTypes = CollUtil.defaultIfEmpty(
worksCollection.getSubjectTypes(), Collections.emptyList());
List<Activity_works_subjectType> persistedSubjectTypes = dao.query(Activity_works_subjectType.class,
Cnd.where(Activity_works_subjectType::getActivityId, "=", worksCollection.getId()));
Map<String, Activity_works_subjectType> persistedSubjectMap = persistedSubjectTypes.stream()
.collect(Collectors.toMap(Activity_works_subjectType::getId, item -> item));
Set<String> submittedSubjectIds = new HashSet<>();
for (Activity_works_subjectType subjectType : submittedSubjectTypes) {
if (subjectType == null) {
return "主题类型数据不能为空";
}
if (StrUtil.isNotBlank(subjectType.getId())) {
if (!persistedSubjectMap.containsKey(subjectType.getId())) {
return "主题类型数据已发生变化,请刷新页面后重新编辑";
}
if (!submittedSubjectIds.add(subjectType.getId())) {
return "主题类型数据重复,请刷新页面后重新编辑";
}
}
List<Activity_works_worksType> submittedWorksTypes = CollUtil.defaultIfEmpty(
subjectType.getWorksTypes(), Collections.emptyList());
if (StrUtil.isBlank(subjectType.getId())) {
boolean containsPersistedWorksType = submittedWorksTypes.stream()
.anyMatch(worksType -> worksType != null && StrUtil.isNotBlank(worksType.getId()));
if (containsPersistedWorksType) {
return "新增主题不能关联原有作品类型,请刷新页面后重新编辑";
}
continue;
}
List<Activity_works_worksType> persistedWorksTypes = dao.query(Activity_works_worksType.class,
Cnd.where(Activity_works_worksType::getSubjectId, "=", subjectType.getId()));
Map<String, Activity_works_worksType> persistedWorksTypeMap = persistedWorksTypes.stream()
.collect(Collectors.toMap(Activity_works_worksType::getId, item -> item));
Set<String> submittedWorksIds = new HashSet<>();
for (Activity_works_worksType worksType : submittedWorksTypes) {
if (worksType == null) {
return "作品类型数据不能为空";
}
if (StrUtil.isNotBlank(worksType.getId()) && !persistedWorksTypeMap.containsKey(worksType.getId())) {
return "作品类型数据已发生变化,请刷新页面后重新编辑";
}
if (StrUtil.isNotBlank(worksType.getId()) && !submittedWorksIds.add(worksType.getId())) {
return "作品类型数据重复,请刷新页面后重新编辑";
}
}
}
String validateMessage = validateReferencedTypesBeforeUpdate(worksCollection);
if (StrUtil.isNotBlank(validateMessage)) {
return validateMessage;
}
dao.update(worksCollection);
for (Activity_works_subjectType subjectType : submittedSubjectTypes) {
subjectType.setActivityId(worksCollection.getId());
if (StrUtil.isBlank(subjectType.getId())) {
dao.insert(subjectType);
} else {
dao.update(subjectType);
}
submittedSubjectIds.add(subjectType.getId());
List<Activity_works_worksType> submittedWorksTypes = CollUtil.defaultIfEmpty(
subjectType.getWorksTypes(), Collections.emptyList());
Set<String> retainedWorksIds = new HashSet<>();
for (Activity_works_worksType worksType : submittedWorksTypes) {
worksType.setSubjectId(subjectType.getId());
if (StrUtil.isBlank(worksType.getId())) {
dao.insert(worksType);
} else {
dao.update(worksType);
}
retainedWorksIds.add(worksType.getId());
}
clearRemovedWorksTypes(subjectType.getId(), retainedWorksIds);
}
clearRemovedSubjectTypes(worksCollection.getId(), submittedSubjectIds);
return null;
}
/**
* 删除页面已移除且未被投稿引用的作品类型;空集合表示删除当前主题下全部原有类型。
*/
private void clearRemovedWorksTypes(String subjectId, Set<String> retainedWorksIds) {
Cnd cnd = Cnd.where(Activity_works_worksType::getSubjectId, "=", subjectId);
if (CollUtil.isNotEmpty(retainedWorksIds)) {
cnd.and(Activity_works_worksType::getId, "not in", retainedWorksIds);
}
dao.clear(Activity_works_worksType.class, cnd);
}
/**
* 删除页面已移除且未被投稿引用的主题类型;空集合表示删除当前活动全部原有主题。
*/
private void clearRemovedSubjectTypes(String activityId, Set<String> retainedSubjectIds) {
Cnd cnd = Cnd.where(Activity_works_subjectType::getActivityId, "=", activityId);
if (CollUtil.isNotEmpty(retainedSubjectIds)) {
cnd.and(Activity_works_subjectType::getId, "not in", retainedSubjectIds);
}
List<String> removedSubjectIds = dao.query(Activity_works_subjectType.class, cnd).stream()
.map(Activity_works_subjectType::getId)
.toList();
if (CollUtil.isEmpty(removedSubjectIds)) {
return;
}
// 先删除未被投稿引用主题下的作品类型,避免产生作品类型孤儿数据。
dao.clear(Activity_works_worksType.class,
Cnd.where(Activity_works_worksType::getSubjectId, "in", removedSubjectIds));
dao.clear(Activity_works_subjectType.class,
Cnd.where(Activity_works_subjectType::getId, "in", removedSubjectIds));
}
@Override @Override
public String validateReferencedTypesBeforeUpdate(Activity_works_collection worksCollection) { public String validateReferencedTypesBeforeUpdate(Activity_works_collection worksCollection) {
if (worksCollection == null || StrUtil.isBlank(worksCollection.getId())) { if (worksCollection == null || StrUtil.isBlank(worksCollection.getId())) {
@@ -218,6 +218,10 @@ public class ClubUserJoinApplyController {
} }
ProcessInstance processInstance = dao.fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", userApply.getId())); ProcessInstance processInstance = dao.fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", userApply.getId()));
// 申请记录存在但没有流程实例时,说明用户仅保存了申请草稿,需提示其先到“我的申请”继续提交。
if (processInstance == null) {
return Result.error(99, "您有该社团的申请记录尚未提交,请到我的申请里查看!");
}
if (!List.of(ProcessInstanceStateEnum.REJECT.getCode(), ProcessInstanceStateEnum.FINISHED.getCode()).contains(processInstance.getState())) { if (!List.of(ProcessInstanceStateEnum.REJECT.getCode(), ProcessInstanceStateEnum.FINISHED.getCode()).contains(processInstance.getState())) {
return Result.error(99, "您有该社团的申请记录尚未完成,请到我的申请里查看!"); return Result.error(99, "您有该社团的申请记录尚未完成,请到我的申请里查看!");
} }
@@ -11,6 +11,7 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil; import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.constant.FlowConst; import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine; import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum; import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
@@ -95,6 +96,7 @@ public class ClubRefreshReportController {
sys_club_refresh info sys_club_refresh info
LEFT JOIN sys_club club ON club.id = info.clubId LEFT JOIN sys_club club ON club.id = info.clubId
LEFT JOIN `wf_process_instance` ins ON ins.businessNo = info.id LEFT JOIN `wf_process_instance` ins ON ins.businessNo = info.id
AND ins.processDefineId IN (SELECT id FROM wf_process_define WHERE name = 'XHHJBG')
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10 LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
$condition $condition
@@ -178,8 +180,21 @@ public class ClubRefreshReportController {
@SaCheckPermission("club.infoManage.refreshReport") @SaCheckPermission("club.infoManage.refreshReport")
@SLog(tag = "社团管理系统-信息管理", msg = "删除换届报告") @SLog(tag = "社团管理系统-信息管理", msg = "删除换届报告")
public Result delete(@Param("id") String id) { public Result delete(@Param("id") String id) {
// 历史换届记录可能与社团注册共用业务编号,必须按流程定义精确定位换届流程,避免误删注册流程。
List<Long> refreshDefineIds = dao.query(ProcessDefine.class,
Cnd.where(ProcessDefine::getName, "=", "XHHJBG"))
.stream()
.map(ProcessDefine::getId)
.toList();
List<ProcessInstance> refreshInstances = refreshDefineIds.isEmpty()
? List.of()
: dao.query(ProcessInstance.class,
Cnd.where(ProcessInstance::getBusinessNo, "=", id)
.and(ProcessInstance::getProcessDefineId, "in", refreshDefineIds));
clubInfoManageService.dao().delete(SysClubRefresh.class, id); clubInfoManageService.dao().delete(SysClubRefresh.class, id);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id); for (ProcessInstance refreshInstance : refreshInstances) {
flowEngine.processInstanceService().deleteProcessInstanceById(refreshInstance.getId());
}
return Result.success(); return Result.success();
} }
} }
@@ -17,6 +17,8 @@ import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
@@ -161,6 +163,26 @@ public class SiteCugManageController {
return Result.success(savedInfo == null ? info : savedInfo); return Result.success(savedInfo == null ? info : savedInfo);
} }
/**
* 切换场地开启状态。
*
* @param id 场地ID,必须传入已存在的 site_cug_info 主键
* @param state 目标状态,true 表示开启,false 表示关闭
* @return Result 响应;code 为 0 表示状态更新成功,否则 msg 为参数错误信息
*/
@At
@ApiOperation("切换场地开启状态")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("siteCug.manage")
@SLog(tag = "场馆功能管理-场地管理", msg = "切换场地开启状态")
public Result switchState(String id, Boolean state) {
if (StrUtil.isBlank(id) || state == null) {
return Result.error("场地ID或开启状态不能为空");
}
infoService.updateState(id, state);
return Result.success();
}
@At @At
@ApiOperation("delete") @ApiOperation("delete")
@SaCheckPermission("siteCug.manage") @SaCheckPermission("siteCug.manage")
@@ -4,4 +4,12 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo; import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
public interface SiteCugInfoService extends BaseService<SiteCugInfo> { public interface SiteCugInfoService extends BaseService<SiteCugInfo> {
/**
* 仅更新场地开启状态,不触发场地完整表单校验。
*
* @param id 场地ID
* @param state true 表示开启,false 表示关闭
*/
void updateState(String id, Boolean state);
} }
@@ -4,7 +4,11 @@ import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo; import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService; import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@Slf4j @Slf4j
@@ -14,4 +18,16 @@ public class SiteCugInfoServiceImpl extends BaseServiceImpl<SiteCugInfo> impleme
public SiteCugInfoServiceImpl(Dao dao) { public SiteCugInfoServiceImpl(Dao dao) {
super(dao); super(dao);
} }
/**
* 仅更新指定场地的开启状态,防止开关操作覆盖其他场地配置。
*
* @param id 场地ID
* @param state true 表示开启,false 表示关闭
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateState(String id, Boolean state) {
update(Chain.make("state", state), Cnd.where("id", "=", id));
}
} }
@@ -54,7 +54,7 @@ import java.util.List;
@IocBean @IocBean
@At("/platform/unionReimburse/apply") @At("/platform/unionReimburse/apply")
@Api("工会报销申请") @Api("工会报销申请")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Slf4j @Slf4j
public class UnionReimburseApplyController { public class UnionReimburseApplyController {
@@ -151,14 +151,11 @@ public class UnionReimburseApplyController {
username as userName, username as userName,
loginname as loginName, loginname as loginName,
sex, sex,
mobile,
technicalTitle, technicalTitle,
IFNULL(unitname, '暂无') as unitName, IFNULL(unitname, '暂无') as unitName,
unitid as unitId, unitid as unitId,
unionid as unionId, unionid as unionId,
unionname as unionName, unionname as unionName,
DATE(birthday) AS birthday,
idCard,
unionCode unionCode
from from
vw_user vw_user
@@ -173,6 +170,9 @@ public class UnionReimburseApplyController {
} }
cnd.and(View_user::getId, "!=", SecurityUtil.getUserId()); cnd.and(View_user::getId, "!=", SecurityUtil.getUserId());
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
if (StrUtil.isBlank(SecurityUtil.getUnionId())) {
return Result.success(java.util.Collections.emptyList());
}
cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId()); cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId());
} }
sql.setCondition(cnd); sql.setCondition(cnd);
@@ -34,7 +34,7 @@ import java.util.List;
*/ */
@IocBean @IocBean
@At("/platform/unionReimburse/certifierUserSign") @At("/platform/unionReimburse/certifierUserSign")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Api("证明人签字") @Api("证明人签字")
@Slf4j @Slf4j
public class UnionReimburseCertifierUserSignController { public class UnionReimburseCertifierUserSignController {
@@ -68,7 +68,7 @@ import java.util.HashMap;
@IocBean @IocBean
@At("/platform/unionReimburse/mine") @At("/platform/unionReimburse/mine")
@Api("工会报销我的") @Api("工会报销我的")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Slf4j @Slf4j
public class UnionReimburseMineController { public class UnionReimburseMineController {
@@ -29,7 +29,7 @@ import java.util.List;
@IocBean @IocBean
@At("/platform/unionReimburse/review") @At("/platform/unionReimburse/review")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Api("工会报销审核") @Api("工会报销审核")
public class UnionReimburseReviewController { public class UnionReimburseReviewController {
@@ -38,7 +38,7 @@ import java.util.List;
@IocBean @IocBean
@At("/platform/unionReimburse/statistics") @At("/platform/unionReimburse/statistics")
@Ok("json:full") @Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}")
@Api("慰问统计") @Api("慰问统计")
@Slf4j @Slf4j
public class UnionReimburseStatisticsController { public class UnionReimburseStatisticsController {
@@ -4,10 +4,12 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_file; import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.sys.services.SysFileService; import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.zhgh.club.model.ClubUser; import com.budwk.app.zhgh.club.model.ClubUser;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub;
@@ -125,6 +127,37 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (oldRecord == null) { if (oldRecord == null) {
return Result.error("未找到对应的报销记录"); return Result.error("未找到对应的报销记录");
} }
if (!isAdmin() && !Objects.equals(oldRecord.getUserId(), SecurityUtil.getUserId())) {
return Result.error("无权修改此报销申请");
}
}
// 人员身份和敏感资料由服务端补齐,避免前端删减字段导致丢失或被篡改。
if (oldRecord == null) {
View_user handler = dao().fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
if (handler == null) {
return Result.error("经办人信息不存在");
}
unionReimburse.setUserId(handler.getId());
unionReimburse.setUserName(handler.getUsername());
unionReimburse.setLoginName(handler.getLoginname());
unionReimburse.setUnitId(handler.getUnitId());
unionReimburse.setUnitName(handler.getUnitName());
unionReimburse.setUnionId(handler.getUnionId());
unionReimburse.setUnionName(handler.getUnionName());
} else {
// 修改历史单据时保留原经办组织,避免人员调动改变经费归属。
unionReimburse.setUserId(oldRecord.getUserId());
unionReimburse.setUserName(oldRecord.getUserName());
unionReimburse.setLoginName(oldRecord.getLoginName());
unionReimburse.setUnitId(oldRecord.getUnitId());
unionReimburse.setUnitName(oldRecord.getUnitName());
unionReimburse.setUnionId(oldRecord.getUnionId());
unionReimburse.setUnionName(oldRecord.getUnionName());
}
Result recipientResult = fillCondolencePersonalData(unionReimburse, oldRecord);
if (recipientResult != null) {
return recipientResult;
} }
normalizeInvoiceDetails(unionReimburse); normalizeInvoiceDetails(unionReimburse);
@@ -179,14 +212,104 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (unionReimburse == null) { if (unionReimburse == null) {
return null; return null;
} }
if (!canReadApply(unionReimburse)) {
throw new BaseException("无权查看此报销申请");
}
List<UnionReimburseInvoiceDetail> invoiceDetails = this.dao().query( List<UnionReimburseInvoiceDetail> invoiceDetails = this.dao().query(
UnionReimburseInvoiceDetail.class, UnionReimburseInvoiceDetail.class,
Cnd.where("reimburseId", "=", id).asc("createdAt") Cnd.where("reimburseId", "=", id).asc("createdAt")
); );
unionReimburse.setInvoiceDetails(invoiceDetails == null ? new ArrayList<>() : invoiceDetails); unionReimburse.setInvoiceDetails(invoiceDetails == null ? new ArrayList<>() : invoiceDetails);
// 申请页和详情页共用此接口,原始身份资料仅保留在服务端业务记录中。
unionReimburse.setCondolenceIdCard(null);
unionReimburse.setCondolenceBirthday(null);
unionReimburse.setCondolenceMobile(null);
return unionReimburse; return unionReimburse;
} }
/** 保留历史单据的身份资料快照,新选人员须符合人员查询的数据范围。 */
private Result fillCondolencePersonalData(UnionReimburse form, UnionReimburse oldRecord) {
form.setCondolenceIdCard(null);
form.setCondolenceBirthday(null);
form.setCondolenceMobile(null);
if (!"UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject())) {
return null;
}
if (StrUtil.isBlank(form.getCondolenceUserId())) {
return null;
}
if (oldRecord != null && Objects.equals(oldRecord.getCondolenceUserId(), form.getCondolenceUserId())) {
form.setCondolenceIdCard(oldRecord.getCondolenceIdCard());
form.setCondolenceBirthday(oldRecord.getCondolenceBirthday());
form.setCondolenceMobile(oldRecord.getCondolenceMobile());
form.setCondolenceUnionId(oldRecord.getCondolenceUnionId());
form.setCondolenceUnitId(oldRecord.getCondolenceUnitId());
form.setCondolenceUserName(oldRecord.getCondolenceUserName());
form.setCondolenceLoginName(oldRecord.getCondolenceLoginName());
form.setCondolenceUnitName(oldRecord.getCondolenceUnitName());
form.setCondolenceUnionName(oldRecord.getCondolenceUnionName());
form.setCondolenceUnionCode(oldRecord.getCondolenceUnionCode());
form.setCondolenceSex(oldRecord.getCondolenceSex());
return null;
}
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", form.getCondolenceUserId()));
if (user == null || Objects.equals(user.getId(), SecurityUtil.getUserId())
|| (!isAdmin() && (StrUtil.isBlank(SecurityUtil.getUnionId())
|| !Objects.equals(SecurityUtil.getUnionId(), user.getUnionId())))) {
return Result.error("无权选择此慰问对象");
}
form.setCondolenceUserName(user.getUsername());
form.setCondolenceLoginName(user.getLoginname());
form.setCondolenceUnitId(user.getUnitId());
form.setCondolenceUnitName(user.getUnitName());
form.setCondolenceUnionId(user.getUnionId());
form.setCondolenceUnionName(user.getUnionName());
form.setCondolenceUnionCode(user.getUnionCode());
form.setCondolenceSex(user.getSex());
form.setCondolenceIdCard(user.getIdCard());
form.setCondolenceBirthday(user.getBirthday() == null ? null : DateUtil.formatDate(user.getBirthday()));
form.setCondolenceMobile(user.getMobile());
return null;
}
/** 详情访问沿用经办、审核、统计和流程参与范围,不能仅凭单据 ID 查询。 */
private boolean canReadApply(UnionReimburse form) {
String userId = SecurityUtil.getUserId();
if (StrUtil.isBlank(userId)) {
return false;
}
if (isAdmin() || userId.equals(form.getUserId())) {
return true;
}
if (form.getStateId() != 1 && (AuthUtil.hasPermission("unionReimburse.review")
|| AuthUtil.hasPermission("h5.unionReimburse.review"))) {
return true;
}
if ((form.getStateId() == 2 || form.getStateId() == 3)
&& StrUtil.isNotBlank(SecurityUtil.getUnionId())
&& Objects.equals(SecurityUtil.getUnionId(), form.getCondolenceUnionId())
&& (AuthUtil.hasPermission("unionReimburse.statistics")
|| AuthUtil.hasPermission("h5.unionReimburse.statistics"))) {
return true;
}
// 旧流程单据允许任务参与人和抄送人回看,关系必须由数据库确认。
Sql sql = Sqls.create("""
SELECT COUNT(*) FROM wf_process_instance i
WHERE i.businessNo = @id AND (
EXISTS (SELECT 1 FROM wf_process_task t
INNER JOIN wf_process_task_actor a ON a.processTaskId = t.id
WHERE t.processInstanceId = i.id AND a.actorId = @userId)
OR EXISTS (SELECT 1 FROM wf_process_cc_instance c
WHERE c.processInstanceId = i.id AND c.actorId = @userId)
)
""");
sql.setParam("id", form.getId());
sql.setParam("userId", userId);
sql.setCallback(Sqls.callback.integer());
dao().execute(sql);
return sql.getInt() > 0;
}
@Override @Override
public void deleteApply(String id) { public void deleteApply(String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
@@ -242,14 +365,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
u.username AS userName, u.username AS userName,
u.loginname AS loginName, u.loginname AS loginName,
u.sex, u.sex,
u.mobile,
u.technicalTitle, u.technicalTitle,
IFNULL(u.unitname, '暂无') AS unitName, IFNULL(u.unitname, '暂无') AS unitName,
u.unitid AS unitId, u.unitid AS unitId,
u.unionid AS unionId, u.unionid AS unionId,
u.unionname AS unionName, u.unionname AS unionName,
DATE(u.birthday) AS birthday,
u.idCard,
u.unionCode u.unionCode
FROM FROM
vw_user u vw_user u
@@ -91,6 +91,7 @@ public class CongressVotingStatisticalController {
@At("/records") @At("/records")
@SaCheckPermission("congress.vote.statistical") @SaCheckPermission("congress.vote.statistical")
public Result voteRecords(@Param("issueId") String issueId) { public Result voteRecords(@Param("issueId") String issueId) {
// H5 投票使用现有职代会代表表写入 rep_id,旧表关联仅作为历史数据兼容回退。
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
r.id, r.id,
@@ -98,10 +99,17 @@ public class CongressVotingStatisticalController {
r.name, r.name,
r.vote_result AS voteResult, r.vote_result AS voteResult,
r.create_time AS createTime, r.create_time AS createTime,
COALESCE(rep.union_id, du.union_id) AS unionId, COALESCE(tc_delegate.unionId, rep.union_id, du.union_id) AS unionId,
du.union_name AS unionName, COALESCE(tc_delegate.unionName, du.union_name) AS unionName,
COALESCE(rep.delegation_name, d.name) AS delegationName COALESCE(delegate_delegation.name, rep.delegation_name, d.name) AS delegationName
FROM congress_voting_record r FROM congress_voting_record r
LEFT JOIN teacher_congress_delegate tc_delegate ON tc_delegate.id = r.rep_id
AND tc_delegate.sessionId = r.session_id
AND COALESCE(tc_delegate.delFlag, 0) = 0
LEFT JOIN teacher_congress_delegation delegate_delegation
ON delegate_delegation.id = COALESCE(r.delegation_id, tc_delegate.delegationId)
AND delegate_delegation.sessionId = r.session_id
AND COALESCE(delegate_delegation.delFlag, 0) = 0
LEFT JOIN congress_rep rep ON rep.id = r.rep_id LEFT JOIN congress_rep rep ON rep.id = r.rep_id
LEFT JOIN congress_delegation d ON d.id = r.delegation_id LEFT JOIN congress_delegation d ON d.id = r.delegation_id
LEFT JOIN ( LEFT JOIN (
@@ -173,10 +173,7 @@ public class ProposalSecondedController {
} }
ProposalSearchParam.buildSearch(cnd, pageForm); ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id"); cnd.groupBy("t.id");
// 用户未指定有效排序字段时,继续沿用原有任务创建时间倒序。
if (!proposalCommonService.applySafePageOrder(cnd, pageForm, ORDER_COLUMNS)) {
cnd.desc("t.createdAt"); cnd.desc("t.createdAt");
}
sql.setCondition(cnd); sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination); return Result.success(pagination);
@@ -244,7 +244,7 @@ public class ProposalUnderTakeReplyController {
@SaCheckPermission("proposal.unitReply") @SaCheckPermission("proposal.unitReply")
@ApiOperation("查询本单位可转办用户") @ApiOperation("查询本单位可转办用户")
public Result selectViceUser(@Param("keyword") @Valid String keyword) { public Result selectViceUser(@Param("keyword") @Valid String keyword) {
Sql sql = Sqls.create("select id,loginname,username from vw_user $condition"); Sql sql = Sqls.create("select id,loginname,username,unitName from vw_user $condition");
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.andEX("unitId", "=", SecurityUtil.getUnitId()); cnd.andEX("unitId", "=", SecurityUtil.getUnitId());
cnd.andEX("id", "!=", SecurityUtil.getUserId()); cnd.andEX("id", "!=", SecurityUtil.getUserId());
@@ -79,7 +79,7 @@ public class ProposalSearchParam extends PageForm {
if (StrUtil.isAllNotBlank(searchParam.getPageOrderName(), searchParam.getPageOrderBy())) { if (StrUtil.isAllNotBlank(searchParam.getPageOrderName(), searchParam.getPageOrderBy())) {
cnd.orderBy(searchParam.getPageOrderName(), PageUtil.getOrder(searchParam.getPageOrderBy())); cnd.orderBy(searchParam.getPageOrderName(), PageUtil.getOrder(searchParam.getPageOrderBy()));
} else { } else {
cnd.asc("info.code"); cnd.desc("info.code");
} }
} }
} }
@@ -11,7 +11,6 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.eclipse.jetty.util.StringUtil;
import org.nutz.dao.entity.annotation.*; import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert; import org.nutz.dao.interceptor.annotation.PrevInsert;
@@ -161,13 +160,13 @@ public class MallGoods extends BaseModel {
//循环sku,判定sku是否有效 //循环sku,判定sku是否有效
for (Map<String, Object> sku : goodsOperationDTO.getSkuList()) { for (Map<String, Object> sku : goodsOperationDTO.getSkuList()) {
//判定参数不能为空 //判定参数不能为空
if ((!sku.containsKey("price") || StringUtil.isEmpty(sku.get("price").toString()) || Convert.toDouble(sku.get("price")) <= 0)) { if ((!sku.containsKey("price") || CharSequenceUtil.isEmpty(sku.get("price").toString()) || Convert.toDouble(sku.get("price")) <= 0)) {
throw new BaseException("商品SKU价格不能小于等于0"); throw new BaseException("商品SKU价格不能小于等于0");
} }
// if ((!sku.containsKey("cost") || StringUtil.isEmpty(sku.get("cost").toString()) || Convert.toDouble(sku.get("cost")) <= 0)) { // if ((!sku.containsKey("cost") || StringUtil.isEmpty(sku.get("cost").toString()) || Convert.toDouble(sku.get("cost")) <= 0)) {
// throw new BaseException("商品SKU成本价不能小于等于0"); // throw new BaseException("商品SKU成本价不能小于等于0");
// } // }
if (!sku.containsKey("quantity") || StringUtil.isEmpty(sku.get("quantity").toString()) || Convert.toInt(sku.get("quantity").toString()) < 0) { if (!sku.containsKey("quantity") || CharSequenceUtil.isEmpty(sku.get("quantity").toString()) || Convert.toInt(sku.get("quantity").toString()) < 0) {
throw new BaseException("商品库存数量不能为负数"); throw new BaseException("商品库存数量不能为负数");
} }
sku.values().forEach(i -> { sku.values().forEach(i -> {
@@ -127,7 +127,6 @@ public class CondolenceApplyController {
username as userName, username as userName,
loginname as loginName, loginname as loginName,
sex, sex,
mobile,
technicalTitle, technicalTitle,
IFNULL(unitname, '暂无') as unitName, IFNULL(unitname, '暂无') as unitName,
unitid as unitId, unitid as unitId,
@@ -146,6 +145,9 @@ public class CondolenceApplyController {
} }
if(!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { if(!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
if(AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_CHAIRMAN.name())) { if(AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
if (StrUtil.isBlank(SecurityUtil.getUnionId())) {
return Result.success(java.util.Collections.emptyList());
}
cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId()); cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId());
} else { } else {
cnd.and(View_user::getId, "=", SecurityUtil.getUserId()); cnd.and(View_user::getId, "=", SecurityUtil.getUserId());
+12 -8
View File
@@ -16,16 +16,20 @@ sm4cbc:
keyHex: 00112233445566778899aabbccddeeff keyHex: 00112233445566778899aabbccddeeff
ivHex: 0102030405060708090a0b0c0d0e0f10 ivHex: 0102030405060708090a0b0c0d0e0f10
jetty: tomcat:
contextPath: / contextPath: ""
threadpool: # 容器错误响应不披露版本;Session Cookie 保持 HttpOnly。
idleTimeout: 60000 session:
minThreads: 10 cookie:
httponly: true
executor:
maxIdleTime: 60000
minSpareThreads: 10
maxThreads: 500 maxThreads: 500
page: page:
403: /error/403.html 403: /__container_error
404: /error/404.html 404: /__container_error
500: /error/500.html 500: /__container_error
#结合ftp使用,或用nginx代理ftp路径 #结合ftp使用,或用nginx代理ftp路径
#staticPath: /Users/wizzer/temp/files #staticPath: /Users/wizzer/temp/files
#开发模式静态资源 #开发模式静态资源
+2 -2
View File
@@ -14,7 +14,7 @@ var ioc={
// 单个文件最大尺寸(大约的值,单位为字节,即 2097152 为 2M) // 单个文件最大尺寸(大约的值,单位为字节,即 2097152 为 2M)
maxFileSize : 2097152, maxFileSize : 2097152,
// 正则表达式匹配可以支持的文件名 // 正则表达式匹配可以支持的文件名
nameFilter : '^(.+[.])(gif|jpg|png|svg)$' nameFilter : '(?i)^(.+[.])(gif|jpg|jpeg|png)$'
} }
}, },
imageUpload : { imageUpload : {
@@ -32,7 +32,7 @@ var ioc={
// 单个文件最大尺寸(大约的值,单位为字节,即 209715200 为 200M) // 单个文件最大尺寸(大约的值,单位为字节,即 209715200 为 200M)
maxFileSize : 209715200, maxFileSize : 209715200,
// 正则表达式匹配可以支持的文件名 // 正则表达式匹配可以支持的文件名
nameFilter : '^(.+[.])(gif||svg|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|mp4)$' nameFilter : '(?i)^(.+[.])(gif|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|mp4)$'
} }
}, },
fileUpload : { fileUpload : {
@@ -36,7 +36,7 @@ layout("/layouts/platform.html"){
新增场地 新增场地
</el-button> </el-button>
</table-tool> </table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize"> <el-table v-loading="tableLoading" :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column> <el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column <el-table-column
v-for="column in tableColumns" v-for="column in tableColumns"
@@ -152,11 +152,23 @@ layout("/layouts/platform.html"){
.catch(() => {}) .catch(() => {})
}, },
switchChange(row) { switchChange(row) {
this.$axios.post("/platform/siteCug/manage/submit", row).then((res) => { const previousState = !row.state
this.tableLoading = true
this.$axios.post("/platform/siteCug/manage/switchState", {
id: row.id,
state: row.state
}).then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.$message.success(res.msg) this.$message.success(res.msg)
this.pageData() } else {
// 状态更新失败时恢复开关,避免页面显示与数据库实际状态不一致。
this.$set(row, "state", previousState)
this.$message.warning(res.msg)
} }
}).catch(() => {
this.$set(row, "state", previousState)
}).finally(() => {
this.tableLoading = false
}) })
}, },
querySiteType() { querySiteType() {
@@ -187,31 +187,6 @@ layout("/layouts/platform.html"){
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="生日"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item prop="condolenceBirthday" label="生日">
<el-input
:value="formData.condolenceBirthday && $moment(formData.condolenceBirthday).isValid() ?$moment(formData.condolenceBirthday).format('YYYY-MM-DD') : ''"
show-word-limit placeholder="请输入生日" disabled></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="身份证号"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item prop="condolenceIdCard" label="身份证号">
<el-input v-model="formData.condolenceIdCard" show-word-limit
placeholder="请输入身份证号" disabled></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="联系方式"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item prop="condolenceMobile" label="联系方式">
<el-input v-model="formData.condolenceMobile" show-word-limit
placeholder="请输入联系方式" disabled></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="慰问类型" <el-descriptions-item label="慰问类型"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'"> v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item label="慰问类型" prop="condolenceTypeId"> <el-form-item label="慰问类型" prop="condolenceTypeId">
@@ -816,7 +791,6 @@ layout("/layouts/platform.html"){
trigger: ["change", "blur"] trigger: ["change", "blur"]
}, {pattern: /^([1-9]{1})(\d{15}|\d{18})$/, message: "请输入正确的银行卡号", trigger: "blur"}], }, {pattern: /^([1-9]{1})(\d{15}|\d{18})$/, message: "请输入正确的银行卡号", trigger: "blur"}],
bankOfDeposit: [{required: true, message: "请填写开户行", trigger: ["change", "blur"]}], bankOfDeposit: [{required: true, message: "请填写开户行", trigger: ["change", "blur"]}],
condolenceMobile: [{pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur"}],
condolenceTime: [{required: true, message: "请选择慰问时间", trigger: ["change", "blur"]}], condolenceTime: [{required: true, message: "请选择慰问时间", trigger: ["change", "blur"]}],
// invoiceNumber: [{required: true, message: "请填写发票张数", trigger: ["change", "blur"]}], // invoiceNumber: [{required: true, message: "请填写发票张数", trigger: ["change", "blur"]}],
// invoice: [{required: true, message: "请填写发票号码", trigger: ["change", "blur"]}], // invoice: [{required: true, message: "请填写发票号码", trigger: ["change", "blur"]}],
@@ -1187,10 +1161,7 @@ layout("/layouts/platform.html"){
unionId, unionId,
unionName, unionName,
unionCode, unionCode,
sex, sex
birthday,
idCard,
mobile
} = user } = user
this.$set(this.formData, "condolenceUserName", userName) this.$set(this.formData, "condolenceUserName", userName)
this.$set(this.formData, "condolenceLoginName", loginName) this.$set(this.formData, "condolenceLoginName", loginName)
@@ -1200,9 +1171,6 @@ layout("/layouts/platform.html"){
this.$set(this.formData, "condolenceUnionName", unionName) this.$set(this.formData, "condolenceUnionName", unionName)
this.$set(this.formData, "condolenceUnionCode", unionCode) this.$set(this.formData, "condolenceUnionCode", unionCode)
this.$set(this.formData, "condolenceSex", sex) this.$set(this.formData, "condolenceSex", sex)
this.$set(this.formData, "condolenceBirthday", birthday)
this.$set(this.formData, "condolenceIdCard", idCard)
this.$set(this.formData, "condolenceMobile", mobile)
} }
}, },
typeChange(val) { typeChange(val) {
@@ -86,18 +86,6 @@ const unionReimburseInfo = {
<span>{{ viewData.condolenceSex }}</span> <span>{{ viewData.condolenceSex }}</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="生日" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.condolenceBirthday && $moment(viewData.condolenceBirthday).isValid() ? $moment(viewData.condolenceBirthday).format('YYYY-MM-DD') : '' }}</span>
</el-descriptions-item>
<el-descriptions-item label="身份证号" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.condolenceIdCard }}</span>
</el-descriptions-item>
<el-descriptions-item label="联系方式" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.condolenceMobile }}</span>
</el-descriptions-item>
<el-descriptions-item label="慰问类型" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'"> <el-descriptions-item label="慰问类型" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.typeName }}</span> <span>{{ viewData.typeName }}</span>
</el-descriptions-item> </el-descriptions-item>
@@ -109,15 +109,27 @@ layout("/layouts/platform.html"){
if (!value) return [] if (!value) return []
try { try {
const data = typeof value === "string" ? JSON.parse(value) : value const data = typeof value === "string" ? JSON.parse(value) : value
return Array.isArray(data) ? data : [] if (Array.isArray(data)) return data
if (data && typeof data === "object") {
return Object.keys(data).map(item => ({item, result: data[item]}))
}
return []
} catch (e) { } catch (e) {
return [] return []
} }
}, },
voteOptionText(item) {
const value = item.result || item.vote || item.selected || item.option || item.value
if (Array.isArray(value)) return value.join("、")
if (value && typeof value === "object") {
return value.label || value.name || value.value || "-"
}
return value || "-"
},
voteResultText(value) { voteResultText(value) {
return this.parseVoteResult(value).map(item => { return this.parseVoteResult(value).map(item => {
return (item.item || item.itemName || "投票项") + "" + return (item.item || item.itemName || "投票项") + "" +
(item.selected || item.option || item.value || "-") this.voteOptionText(item)
}).join("") }).join("")
}, },
buildSummary(records) { buildSummary(records) {
@@ -125,7 +137,7 @@ layout("/layouts/platform.html"){
records.forEach(record => { records.forEach(record => {
this.parseVoteResult(record.voteResult).forEach(item => { this.parseVoteResult(record.voteResult).forEach(item => {
const itemName = item.item || item.itemName || "投票项" const itemName = item.item || item.itemName || "投票项"
const option = item.selected || item.option || item.value || "-" const option = this.voteOptionText(item)
if (!counter[itemName]) counter[itemName] = {} if (!counter[itemName]) counter[itemName] = {}
counter[itemName][option] = (counter[itemName][option] || 0) + 1 counter[itemName][option] = (counter[itemName][option] || 0) + 1
}) })
@@ -110,10 +110,10 @@ const PROPOSAL_INFO = {
</el-dialog> </el-dialog>
<!--审核信息支持按需同时展开多条记录初次加载和切换提案时仍保持全部折叠--> <!--审核信息支持按需同时展开多条记录初次加载和切换提案时仍保持全部折叠-->
<template v-if="doneTasks.length"> <template v-if="displayDoneTasks.length">
<div class="process-title">审核信息</div> <div class="process-title">审核信息</div>
<el-collapse v-model="activeTaskIds"> <el-collapse v-model="activeTaskIds">
<el-collapse-item v-for="task in doneTasks" :key="task.id" :name="task.id"> <el-collapse-item v-for="task in displayDoneTasks" :key="task.id" :name="task.id">
<template slot="title"> <template slot="title">
<!--折叠标题使用独立的单行布局避免复用 process-title 后产生外边距和高度冲突--> <!--折叠标题使用独立的单行布局避免复用 process-title 后产生外边距和高度冲突-->
<div style="display: flex;align-items: center;justify-content: space-between;width: 100%;min-width: 0;padding-left: 10px"> <div style="display: flex;align-items: center;justify-content: space-between;width: 100%;min-width: 0;padding-left: 10px">
@@ -125,8 +125,28 @@ const PROPOSAL_INFO = {
</span> </span>
</div> </div>
</template> </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}}
</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" <el-descriptions border class="flow-task-form" :column="3" :key="task.id"
v-if="task.ext.isFirstTaskNode"> v-else-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName <el-descriptions-item label="申请用户">{{ task.ext.initiatorName
}}({{task.ext.initiatorAccount}}) }}({{task.ext.initiatorAccount}})
</el-descriptions-item> </el-descriptions-item>
@@ -233,6 +253,7 @@ const PROPOSAL_INFO = {
tableLoading: false, tableLoading: false,
viewData: {}, viewData: {},
doneTasks: [], doneTasks: [],
displayDoneTasks: [],
activeTaskIds: [], activeTaskIds: [],
row: null, row: null,
viewDialogVisible: false viewDialogVisible: false
@@ -258,6 +279,7 @@ const PROPOSAL_INFO = {
this.$set(this, "tableLoading", true) this.$set(this, "tableLoading", true)
// 切换提案时先清空上一条提案的审核记录和展开状态,避免异步加载期间展示旧数据。 // 切换提案时先清空上一条提案的审核记录和展开状态,避免异步加载期间展示旧数据。
this.doneTasks = [] this.doneTasks = []
this.displayDoneTasks = []
this.activeTaskIds = [] this.activeTaskIds = []
let pendingRequestCount = 2 let pendingRequestCount = 2
const handleRequestComplete = () => { const handleRequestComplete = () => {
@@ -293,15 +315,94 @@ const PROPOSAL_INFO = {
return this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => { return this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
if (res.code === 0) { if (res.code === 0) {
const tasks = res.data || [] const tasks = res.data || []
this.doneTasks = tasks // 提案历史按业务阶段展示,避免较晚完成的附议记录被排到团长审核之后。
const sortedTasks = this.sortDoneTasks(tasks)
this.$set(this, "doneTasks", sortedTasks)
// 参考历史系统的展示方式,将多个单位的答复收纳在一个流程节点内。
this.$set(this, "displayDoneTasks", this.groupReplyTasks(sortedTasks))
// 审核记录加载完成后默认全部折叠,由用户按需展开查看。 // 审核记录加载完成后默认全部折叠,由用户按需展开查看。
this.activeTaskIds = [] this.activeTaskIds = []
// 通知外层页面已办节点数据已加载完成,便于当前环节做表单回显。 // 通知外层页面已办节点数据已加载完成,便于当前环节做表单回显。
this.$emit("done-tasks", tasks) this.$emit("done-tasks", this.doneTasks)
} }
}) })
}, },
/**
* 按提案业务阶段对审核历史排序
*
* @param tasks doneTasks 接口返回的已办任务数组
* @return 排序后的新数组同一阶段按办理时间任务 ID 保持稳定顺序
*/
sortDoneTasks(tasks) {
return (tasks || []).map((task, index) => ({task, index})).sort((left, right) => {
const stageDiff = this.getTaskStageOrder(left.task) - this.getTaskStageOrder(right.task)
if (stageDiff !== 0) {
return stageDiff
}
const leftTime = String(left.task.finishTime || "")
const rightTime = String(right.task.finishTime || "")
if (leftTime !== rightTime) {
if (!leftTime) return 1
if (!rightTime) return -1
return leftTime.localeCompare(rightTime)
}
const idDiff = Number(left.task.id || 0) - Number(right.task.id || 0)
return idDiff !== 0 ? idDiff : left.index - right.index
}).map((item) => item.task)
},
/**
* 将同一提案的承办单位答复任务合并成一个展示节点
*
* @param tasks 已按业务阶段排好顺序的任务数组
* @return 仅用于查看页面的展示数组原始任务及每条答复内容均保存在 replyTasks
*/
groupReplyTasks(tasks) {
const replyTaskNames = ["opinion_unit_reply", "unit_reply", "two_unit_reply", "master_reply", "slave_reply"]
const replyTasks = (tasks || []).filter((task) => replyTaskNames.includes(task.taskName))
if (!replyTasks.length) {
return tasks || []
}
const firstReplyTask = replyTasks[0]
const replyTaskIds = new Set(replyTasks.map((task) => task.id))
return (tasks || []).reduce((displayTasks, task) => {
if (!replyTaskIds.has(task.id)) {
displayTasks.push(task)
} else if (task.id === firstReplyTask.id) {
displayTasks.push({
id: "reply-group-" + firstReplyTask.id,
taskName: "replyGroup",
displayName: "承办单位答复",
replyTasks: replyTasks
})
}
return displayTasks
}, [])
},
/**
* 获取提案任务所属的展示阶段
* 未单列的预审核委员审议确认承办单位和复核记录统一放在委员会立案阶段
* 答复后的校领导审批通过 legacyStage 标识排在承办单位答复之后
*
* @param task 单条已办任务
* @return 数字类型的阶段顺序
*/
getTaskStageOrder(task) {
const taskName = task.taskName
const legacyStage = task.ext ? task.ext.legacyStage : ""
if (taskName === "startTask") return 10
if (taskName === "invite") return 15
if (taskName === "second") return 20
if (taskName === "delegation") return 30
if (["opinion_unit_reply", "unit_reply", "two_unit_reply", "master_reply", "slave_reply"].includes(taskName)) return 50
if (taskName === "schoolLeader" && legacyStage === "replyLeaderAudit") return 55
if (taskName === "feedback") return 60
return 40
},
openChart(){ openChart(){
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId) this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
} }
@@ -179,16 +179,17 @@ layout("/layouts/platform.html"){
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item prop="tf_attachment" label="附件"> <!-- 承办单位答复附件上传入口暂时停用,保留原配置便于后续恢复。 -->
<file-upload <!-- <el-form-item prop="tf_attachment" label="附件">-->
:value.sync="formData.tf_attachment" <!-- <file-upload-->
upload_mode="drag" <!-- :value.sync="formData.tf_attachment"-->
:upload_number="5" <!-- upload_mode="drag"-->
upload_result_category="array" <!-- :upload_number="5"-->
complete_result <!-- upload_result_category="array"-->
upload_mode="file" <!-- complete_result-->
></file-upload> <!-- upload_mode="file"-->
</el-form-item> <!-- ></file-upload>-->
<!-- </el-form-item>-->
<el-form-item label="答复内容" prop="tf_opinion" <el-form-item label="答复内容" prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]"> :rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<text-editor v-model="formData.tf_opinion"></text-editor> <text-editor v-model="formData.tf_opinion"></text-editor>
@@ -14,12 +14,11 @@ const welfareOption = {
<el-table-column align="center" header-align="center" label="系统默认选择" width="140"> <el-table-column align="center" header-align="center" label="系统默认选择" width="140">
<template slot-scope="{row}"> <template slot-scope="{row}">
<el-radio <el-checkbox
v-model="systemDefaultOption" :value="!!row.isSystemDefault"
:label="row" @change="setSystemDefault(row, $event)">
@change="setSystemDefault(row)">
&nbsp; &nbsp;
</el-radio> </el-checkbox>
</template> </template>
</el-table-column> </el-table-column>
@@ -124,9 +123,10 @@ const welfareOption = {
}, },
// 每个福利项目只允许配置一个系统默认选项,保存时会随福利选项一并提交。 // 每个福利项目只允许配置一个系统默认选项,保存时会随福利选项一并提交。
setSystemDefault(defaultOption) { setSystemDefault(defaultOption, checked = true) {
this.systemDefaultOption = checked ? defaultOption : null;
this.welfareList.forEach((item) => { this.welfareList.forEach((item) => {
this.$set(item, 'isSystemDefault', item === defaultOption); this.$set(item, 'isSystemDefault', checked && item === defaultOption);
}); });
}, },
@@ -200,7 +200,7 @@ const welfareOption = {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
/deep/ .welfare-option .el-radio__label { /deep/ .welfare-option .el-checkbox__label {
padding-left: 0; padding-left: 0;
} }
@@ -397,6 +397,19 @@ layout("/layouts/platform_h5.html"){
justify-content: flex-end; justify-content: flex-end;
} }
.sports-event-union-action-hint {
display: inline-flex;
align-items: center;
color: #ed6a0c;
font-size: 12px;
line-height: 18px;
text-align: right;
}
.sports-event-union-action-hint.is-authorized {
color: #1989fa;
}
.sports-event-page .table-card .table-card-footer .van-cell__value { .sports-event-page .table-card .table-card-footer .van-cell__value {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -700,6 +713,12 @@ layout("/layouts/platform_h5.html"){
取消报名 取消报名
</van-button> </van-button>
</div> </div>
<span
v-if="row.applyWay.includes(2) && !row.applyWay.includes(1)"
class="sports-event-union-action-hint"
:class="{'is-authorized': canUnionApplyOnPc}">
{{ canUnionApplyOnPc ? '分工会报名请在PC端进行' : '该活动为分工会报名,暂无权限' }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -731,6 +750,12 @@ layout("/layouts/platform_h5.html"){
<i class="fa fa-trash"></i>取消报名 <i class="fa fa-trash"></i>取消报名
</van-button> </van-button>
</div> </div>
<span
v-if="row.applyWay.includes(2) && !row.applyWay.includes(1)"
class="sports-event-union-action-hint"
:class="{'is-authorized': canUnionApplyOnPc}">
{{ canUnionApplyOnPc ? '分工会报名请在PC端进行' : '该活动为分工会报名,暂无权限' }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -768,6 +793,16 @@ layout("/layouts/platform_h5.html"){
} }
}, },
components: {}, components: {},
computed: {
canUnionApplyOnPc() {
return this.$auth.hasRoleOr([
"SYSADMIN",
"SCHOOL_UNION_ADMIN",
"BRANCH_UNION_CHAIRMAN",
"BRANCH_UNION_WENTI_SPORTS"
]) && this.$auth.hasPermission("activity.apply")
}
},
methods: { methods: {
toggleViewMode() { toggleViewMode() {
this.viewMode = this.viewMode === "card" ? "list" : "card" this.viewMode = this.viewMode === "card" ? "list" : "card"
@@ -324,11 +324,37 @@ layout("/layouts/platform_h5.html"){
} }
.brand-course-card__detail:nth-child(-n + 2) .brand-course-card__detail-label, .brand-course-card__detail:nth-child(-n + 2) .brand-course-card__detail-label,
.brand-course-card__detail:nth-child(-n + 2) .brand-course-card__detail-value { .brand-course-card__detail:nth-child(2) .brand-course-card__detail-value {
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: normal; overflow-wrap: normal;
white-space: nowrap; white-space: nowrap;
} }
.brand-course-card__detail:first-child .brand-course-card__detail-value {
display: block;
max-width: 100%;
overflow: hidden;
overflow-wrap: normal;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand-course-contact-popover {
display: block;
min-width: 0;
max-width: 100%;
}
.brand-course-contact-popover__content {
max-width: 240px;
padding: 8px 12px;
overflow-wrap: anywhere;
color: #33445a;
font-size: 12px;
line-height: 18px;
}
.brand-course-card__links { .brand-course-card__links {
display: flex; display: flex;
min-width: 0; min-width: 0;
@@ -913,7 +939,17 @@ layout("/layouts/platform_h5.html"){
<van-icon name="friends-o"></van-icon> <van-icon name="friends-o"></van-icon>
<div class="brand-course-card__detail-copy"> <div class="brand-course-card__detail-copy">
<div class="brand-course-card__detail-label">联系人</div> <div class="brand-course-card__detail-label">联系人</div>
<div class="brand-course-card__detail-value">{{row.courseInstructor}}</div> <van-popover
class="brand-course-contact-popover"
:value="contactPopoverIndex === index"
placement="top-start"
trigger="click"
@input="setContactPopover(index, $event)">
<div class="brand-course-contact-popover__content">{{row.courseInstructor || '暂无'}}</div>
<template #reference>
<div class="brand-course-card__detail-value">{{row.courseInstructor || '暂无'}}</div>
</template>
</van-popover>
</div> </div>
</div> </div>
<div class="brand-course-card__detail"> <div class="brand-course-card__detail">
@@ -1009,6 +1045,7 @@ layout("/layouts/platform_h5.html"){
activeAssort: null, activeAssort: null,
filterOpened: false, filterOpened: false,
viewMode: 'card', viewMode: 'card',
contactPopoverIndex: -1,
weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'}, weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'},
} }
@@ -1017,6 +1054,9 @@ layout("/layouts/platform_h5.html"){
toggleViewMode() { toggleViewMode() {
this.viewMode = this.viewMode === 'card' ? 'list' : 'card' this.viewMode = this.viewMode === 'card' ? 'list' : 'card'
}, },
setContactPopover(index, visible) {
this.contactPopoverIndex = visible ? index : -1
},
onAssortChange(name) { onAssortChange(name) {
this.pageForm.assortTypes = name ? JSON.stringify([name]) : [] this.pageForm.assortTypes = name ? JSON.stringify([name]) : []
this.doSearch() this.doSearch()
@@ -444,7 +444,7 @@ layout("/layouts/platform_h5.html"){
<button type="button" class="works-mine-card__action" @click="onView(row)"> <button type="button" class="works-mine-card__action" @click="onView(row)">
<van-icon name="eye-o"></van-icon>查看 <van-icon name="eye-o"></van-icon>查看
</button> </button>
<button type="button" class="works-mine-card__action is-primary" @click="onEdit(row)"> <button type="button" class="works-mine-card__action" @click="onEdit(row)">
<van-icon name="edit"></van-icon>编辑 <van-icon name="edit"></van-icon>编辑
</button> </button>
<button type="button" class="works-mine-card__action is-danger" @click="onDelete(row)"> <button type="button" class="works-mine-card__action is-danger" @click="onDelete(row)">
@@ -467,7 +467,7 @@ layout("/layouts/platform_h5.html"){
:close-on-click-overlay="false" show-cancel-button cancel-button-text="取消" confirm-button-text="确认删除" :close-on-click-overlay="false" show-cancel-button cancel-button-text="取消" confirm-button-text="确认删除"
@cancel="closeDeleteConfirm" @confirm="confirmDelete"></van-dialog> @cancel="closeDeleteConfirm" @confirm="confirmDelete"></van-dialog>
<apply-form ref="applyFormRef"></apply-form> <apply-form ref="applyFormRef" @submit-success="handleEditSubmitSuccess"></apply-form>
<info ref="infoRef"></info> <info ref="infoRef"></info>
</div> </div>
@@ -541,6 +541,10 @@ layout("/layouts/platform_h5.html"){
} }
this.$refs.applyFormRef.onOpenEdit(row) this.$refs.applyFormRef.onOpenEdit(row)
}, },
// 编辑提交成功后留在我的作品页面,仅刷新当前列表数据。
handleEditSubmitSuccess() {
this.loadWorks(true)
},
onDelete(row) { onDelete(row) {
this.$set(this, 'pendingDeleteRow', row) this.$set(this, 'pendingDeleteRow', row)
window.h5HistoryLayerManager.open('works-collection-mine-delete-confirm') window.h5HistoryLayerManager.open('works-collection-mine-delete-confirm')
@@ -8,7 +8,7 @@ const INFO = {
<van-sticky> <van-sticky>
<van-nav-bar title="作品详情" left-text="返回" left-arrow @click-left="closeInfo"></van-nav-bar> <van-nav-bar title="作品详情" left-text="返回" left-arrow @click-left="closeInfo"></van-nav-bar>
</van-sticky> </van-sticky>
<div class="works-detail-scroll"> <div ref="detailScrollRef" class="works-detail-scroll">
<section class="works-detail-card"> <section class="works-detail-card">
<h2 class="works-detail-card__title">基础信息</h2> <h2 class="works-detail-card__title">基础信息</h2>
<van-cell-group :border="false"> <van-cell-group :border="false">
@@ -154,6 +154,7 @@ const INFO = {
onOpen(row) { onOpen(row) {
this.$set(this, "viewData", Object.assign({}, row, {files: this.normalizeFiles(row.files)})) this.$set(this, "viewData", Object.assign({}, row, {files: this.normalizeFiles(row.files)}))
window.h5HistoryLayerManager.open("works-collection-mine-info") window.h5HistoryLayerManager.open("works-collection-mine-info")
this.resetScrollPosition()
this.$axios.post("/platform/activity/worksCollection/common/findOne", {id: row.id}).then((res) => { this.$axios.post("/platform/activity/worksCollection/common/findOne", {id: row.id}).then((res) => {
if (res.code === 0) { if (res.code === 0) {
const viewData = res.data || {} const viewData = res.data || {}
@@ -162,6 +163,12 @@ const INFO = {
} }
}) })
}, },
// 弹窗组件会被复用,每次打开后需清除上一次查看时保留的滚动位置。
resetScrollPosition() {
this.$nextTick(() => {
if (this.$refs.detailScrollRef) this.$refs.detailScrollRef.scrollTop = 0
})
},
normalizeFiles(files) { normalizeFiles(files) {
if (Array.isArray(files)) return files if (Array.isArray(files)) return files
if (!files) return [] if (!files) return []
@@ -222,24 +222,30 @@ layout("/layouts/platform_h5.html"){
white-space: nowrap; white-space: nowrap;
} }
.works-read-like-tag { .works-read-like-summary {
display: inline-flex; display: inline-flex;
min-height: 24px; min-height: 24px;
padding: 3px 9px;
align-items: center; align-items: center;
flex: none; flex: none;
border: 0; color: #7f8da1;
border-radius: 12px;
color: #397de0;
background-color: #edf5ff;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
white-space: nowrap;
box-sizing: border-box; box-sizing: border-box;
} }
.works-read-like-tag .van-icon { /* 顶部仅展示累计点赞信息,人员图标与无底色文字用于区别底部可点击的点赞按钮。 */
margin-right: 4px; .works-read-like-summary .van-icon {
font-size: 14px; display: inline-flex;
width: 20px;
height: 20px;
margin-right: 5px;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #ffffff;
background-color: #8795aa;
font-size: 12px;
} }
.works-read-files-row { .works-read-files-row {
@@ -441,8 +447,9 @@ layout("/layouts/platform_h5.html"){
<div class="works-read-card-title">{{row.name || '未命名作品'}}</div> <div class="works-read-card-title">{{row.name || '未命名作品'}}</div>
<div class="works-read-card-activity">{{row.activityName || '暂无活动信息'}}</div> <div class="works-read-card-activity">{{row.activityName || '暂无活动信息'}}</div>
</div> </div>
<div class="works-read-like-tag"> <div class="works-read-like-summary">
<van-icon name="good-job-o"></van-icon>{{row.num || 0}} <van-icon name="friends"></van-icon>
<span>已有 {{row.num || 0}} 人点赞</span>
</div> </div>
</div> </div>
</template> </template>
@@ -10,7 +10,7 @@ const applyForm = {
<van-nav-bar title="作品提交" left-text="返回" left-arrow @click-left="closeApply"></van-nav-bar> <van-nav-bar title="作品提交" left-text="返回" left-arrow @click-left="closeApply"></van-nav-bar>
</van-sticky> </van-sticky>
<van-form ref="formRef" class="works-apply-form form-container"> <van-form ref="formRef" class="works-apply-form form-container">
<div class="works-apply-scroll"> <div ref="applyScrollRef" class="works-apply-scroll">
<section class="works-apply-section"> <section class="works-apply-section">
<h2 class="works-apply-section__title">基础信息</h2> <h2 class="works-apply-section__title">基础信息</h2>
<van-cell-group :border="false"> <van-cell-group :border="false">
@@ -356,6 +356,7 @@ const applyForm = {
showOptionPicker: false, optionPopupType: "", optionPopupTitle: "", showOptionPicker: false, optionPopupType: "", optionPopupTitle: "",
optionPopupOptions: [], pendingOptionValue: "", optionPopupOptions: [], pendingOptionValue: "",
submitConfirmVisible: false, fileAccept: null, chooseWorksType: {}, submitConfirmVisible: false, fileAccept: null, chooseWorksType: {},
submitSuccessMode: "navigate",
historyLayerPageKey: "works-collection-upload-h5", historyLayerPageKey: "works-collection-upload-h5",
historyLayerUnsubscribe: null, ownsHistoryRegistration: false historyLayerUnsubscribe: null, ownsHistoryRegistration: false
} }
@@ -384,16 +385,19 @@ const applyForm = {
}, },
onOpen(row) { onOpen(row) {
this.resetFormData() this.resetFormData()
this.$set(this, "submitSuccessMode", "navigate")
this.$set(this, "row", row) this.$set(this, "row", row)
this.$set(this.formData, "activityId", row.id) this.$set(this.formData, "activityId", row.id)
this.activityChange(row.id).then(() => { this.activityChange(row.id).then(() => {
window.h5HistoryLayerManager.open("works-collection-apply") window.h5HistoryLayerManager.open("works-collection-apply")
this.resetScrollPosition()
}) })
}, },
onOpenEdit(row) { onOpenEdit(row) {
const formData = clone(row) const formData = clone(row)
formData.files = this.normalizeFiles(formData.files) formData.files = this.normalizeFiles(formData.files)
this.resetFormData() this.resetFormData()
this.$set(this, "submitSuccessMode", "close")
this.$set(this, "row", formData) this.$set(this, "row", formData)
this.$set(this, "formData", formData) this.$set(this, "formData", formData)
const user = this.$store.state.user || {} const user = this.$store.state.user || {}
@@ -408,6 +412,13 @@ const applyForm = {
}).then(() => { }).then(() => {
this.setSelectedWorksType(formData.worksId) this.setSelectedWorksType(formData.worksId)
window.h5HistoryLayerManager.open("works-collection-apply") window.h5HistoryLayerManager.open("works-collection-apply")
this.resetScrollPosition()
})
},
// 表单弹窗会被复用,每次打开后需清除上一次编辑或提交时保留的滚动位置。
resetScrollPosition() {
this.$nextTick(() => {
if (this.$refs.applyScrollRef) this.$refs.applyScrollRef.scrollTop = 0
}) })
}, },
// 附件可能由列表 SQL 返回 JSON 字符串,也可能已被序列化为数组;统一转换为上传组件需要的数组。 // 附件可能由列表 SQL 返回 JSON 字符串,也可能已被序列化为数组;统一转换为上传组件需要的数组。
@@ -505,6 +516,13 @@ const applyForm = {
}).then((res) => { }).then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.$toast.success("提交成功") this.$toast.success("提交成功")
if (this.submitSuccessMode === "close") {
// 我的作品页使用弹窗编辑,成功后只关闭当前表单并通知父页面刷新列表。
window.h5HistoryLayerManager.close("works-collection-apply", () => {
this.$emit("submit-success")
})
return
}
window.h5HistoryLayerManager.closeAllAndNavigate("/platform/activity/worksCollection/mine/h5") window.h5HistoryLayerManager.closeAllAndNavigate("/platform/activity/worksCollection/mine/h5")
} }
}).finally(() => { this.$set(this, "formLoading", false) }) }).finally(() => { this.$set(this, "formLoading", false) })
@@ -128,7 +128,7 @@ layout("/layouts/platform_h5.html"){
<van-field v-model="formData.tf_opinion" class="club-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{ required: true, message: '请填写审批意见' }]" maxlength="100" show-word-limit></van-field> <van-field v-model="formData.tf_opinion" class="club-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{ required: true, message: '请填写审批意见' }]" maxlength="100" show-word-limit></van-field>
</van-form> </van-form>
</section> </section>
<div class="club-audit-actions"><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(6)">退回</van-button><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(2)">不同意</van-button><van-button :loading="formLoading" type="primary" block @click="handleTaskAction(1)">同意</van-button></div> <div class="club-audit-actions"><van-button :loading="formLoading" type="primary" block @click="handleTaskAction(1)">同意</van-button><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(2)">拒绝</van-button><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(6)">退回</van-button></div>
</template> </template>
</info> </info>
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmPendingAction" @cancel="closeConfirm"></van-dialog> <van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmPendingAction" @cancel="closeConfirm"></van-dialog>
@@ -62,22 +62,48 @@ layout("/layouts/platform_h5.html"){
</template> </template>
</table-list> </table-list>
</main> </main>
<info ref="infoRef" page-key="club-mineclub" :merge-apply-info="true"></info> <van-popup v-model="clubDetailVisible" position="right" :style="{ width: '100%', height: '100%' }" get-container="#app" class="club-user-join-popup" :close-on-click-overlay="false">
<div class="club-user-join-detail">
<van-nav-bar title="社团详情" left-text="返回" left-arrow fixed @click-left="closeClubDetail"></van-nav-bar>
<div class="club-user-join-detail__scroll">
<main class="club-user-join-detail__content">
<section class="club-user-join-card club-user-join-info-card">
<h3 class="club-user-join-card__title">社团基础信息</h3>
<van-cell-group :border="false">
<van-cell title="社团名称">{{ clubDetail.clubName || '--' }}</van-cell>
<van-cell title="成立时间">{{ clubDetail.foundTime || '--' }}</van-cell>
<van-cell title="社团编码">{{ clubDetail.clubCode || '--' }}</van-cell>
<van-cell title="社团类型"><dict-tag :options="dict.type.CLUB_REGISTER_TYPE" :value="clubDetail.clubType"></dict-tag></van-cell>
<van-cell title="会长">{{ clubDetail.clubLeader || '--' }}</van-cell>
<van-cell title="秘书长">{{ clubDetail.clubSecretary || '--' }}</van-cell>
<van-cell title="当前人数">{{ clubDetail.currentPeopleNum || 0 }} 人</van-cell>
<van-cell title="会费标准">{{ clubDetail.due || '--' }}</van-cell>
<van-cell title="社团介绍" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.introduce)" v-html="clubDetail.introduce"></div><span v-else>暂无</span></van-cell>
<van-cell title="社团宗旨" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.purpose)" v-html="clubDetail.purpose"></div><span v-else>暂无</span></van-cell>
<van-cell title="社团章程" class="club-user-join-info-long-cell"><file-preview v-if="clubDetail.rulesFile && clubDetail.rulesFile.length" :files="clubDetail.rulesFile" complete_result></file-preview><span v-else>暂无</span></van-cell>
<van-cell title="年度活动计划" class="club-user-join-info-long-cell"><file-preview v-if="clubDetail.yearPlanFile && clubDetail.yearPlanFile.length" :files="clubDetail.yearPlanFile" complete_result></file-preview><span v-else>暂无</span></van-cell>
</van-cell-group>
</section>
</main>
</div>
</div>
</van-popup>
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmExit" @cancel="closeConfirm"></van-dialog> <van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmExit" @cancel="closeConfirm"></van-dialog>
</div> </div>
<script nonce="${cspNonce!}"> <script nonce="${cspNonce!}">
<!--#include('../common/clubUserJoin.js'){}#-->
new Vue({ new Vue({
el: "#app", store, components: {info: clubUserJoin}, el: "#app", store, dicts: ['CLUB_REGISTER_TYPE'],
data() { return {pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, clubName: ''}, listLoading: true, formLoading: false, confirmVisible: false, currentRow: null, historyLayerUnsubscribe: null} }, data() { return {pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, clubName: ''}, listLoading: true, formLoading: false, clubDetailVisible: false, clubDetail: {}, confirmVisible: false, currentRow: null, historyLayerUnsubscribe: null} },
computed: { confirmMessage() { return this.currentRow ? '您确定要申请退出“' + this.currentRow.clubName + '”吗?' : '您确定要申请退出该社团吗?'; } }, computed: { confirmMessage() { return this.currentRow ? '您确定要申请退出“' + this.currentRow.clubName + '”吗?' : '您确定要申请退出该社团吗?'; } },
created() { const manager = window.h5HistoryLayerManager; if (manager) { manager.ensureRegistered('club-mineclub'); this.historyLayerUnsubscribe = manager.subscribe((layers) => { this.$set(this, 'confirmVisible', layers.includes('club-mineclub-confirm')); }); } }, created() { const manager = window.h5HistoryLayerManager; if (manager) { manager.ensureRegistered('club-mineclub'); this.historyLayerUnsubscribe = manager.subscribe((layers) => { this.$set(this, 'clubDetailVisible', layers.includes('club-mineclub-detail')); this.$set(this, 'confirmVisible', layers.includes('club-mineclub-confirm')); }); } },
beforeDestroy() { if (this.historyLayerUnsubscribe) { this.historyLayerUnsubscribe(); } const manager = window.h5HistoryLayerManager; if (manager) { manager.unregister('club-mineclub'); } }, beforeDestroy() { if (this.historyLayerUnsubscribe) { this.historyLayerUnsubscribe(); } const manager = window.h5HistoryLayerManager; if (manager) { manager.unregister('club-mineclub'); } },
methods: { methods: {
onNavBack() { const manager = window.h5HistoryLayerManager; if (manager && manager.stack.length) { manager.close(); return; } historyBack(); }, onNavBack() { const manager = window.h5HistoryLayerManager; if (manager && manager.stack.length) { manager.close(); return; } historyBack(); },
onLoadingChange(state) { this.$set(this, 'listLoading', state.loading); }, onLoadingChange(state) { this.$set(this, 'listLoading', state.loading); },
onView(row) { this.$refs.infoRef.onOpen(Object.assign({}, row, {id: row.applyId}), '社团申请详情'); }, onView(row) { this.$set(this, 'clubDetail', Object.assign({}, row || {})); const manager = window.h5HistoryLayerManager; if (manager) { manager.open('club-mineclub-detail'); return; } this.$set(this, 'clubDetailVisible', true); },
closeClubDetail(afterClose) { const manager = window.h5HistoryLayerManager; if (manager) { manager.close('club-mineclub-detail', afterClose); return; } this.$set(this, 'clubDetailVisible', false); if (afterClose) { afterClose(); } },
hasClubContent(value) { return value !== null && value !== undefined && String(value).replace(/<[^>]*>/g, '').replace(/&nbsp;/gi, '').trim().length > 0; },
onExit(row) { this.$set(this, 'currentRow', row); const manager = window.h5HistoryLayerManager; if (manager) { manager.open('club-mineclub-confirm'); return; } this.$set(this, 'confirmVisible', true); }, onExit(row) { this.$set(this, 'currentRow', row); const manager = window.h5HistoryLayerManager; if (manager) { manager.open('club-mineclub-confirm'); return; } this.$set(this, 'confirmVisible', true); },
closeConfirm(afterClose) { const manager = window.h5HistoryLayerManager; if (manager) { manager.close('club-mineclub-confirm', afterClose); return; } this.$set(this, 'confirmVisible', false); if (afterClose) { afterClose(); } }, closeConfirm(afterClose) { const manager = window.h5HistoryLayerManager; if (manager) { manager.close('club-mineclub-confirm', afterClose); return; } this.$set(this, 'confirmVisible', false); if (afterClose) { afterClose(); } },
confirmExit() { const row = this.currentRow; this.closeConfirm(() => { this.$set(this, 'formLoading', true); this.$axios.post('/platform/club/join/apply/submit', {data: JSON.stringify(row), mode: false}).then((res) => { if (res.code === 0) { this.$toast(res.msg); const manager = window.h5HistoryLayerManager; if (manager) { manager.closeAll(() => { this.$pjaxReplace('/platform/club/join/mine/h5'); }); } else { this.$pjaxReplace('/platform/club/join/mine/h5'); } } }).finally(() => { this.$set(this, 'formLoading', false); }); }); }, confirmExit() { const row = this.currentRow; this.closeConfirm(() => { this.$set(this, 'formLoading', true); this.$axios.post('/platform/club/join/apply/submit', {data: JSON.stringify(row), mode: false}).then((res) => { if (res.code === 0) { this.$toast(res.msg); const manager = window.h5HistoryLayerManager; if (manager) { manager.closeAll(() => { this.$pjaxReplace('/platform/club/join/mine/h5'); }); } else { this.$pjaxReplace('/platform/club/join/mine/h5'); } } }).finally(() => { this.$set(this, 'formLoading', false); }); }); },
@@ -579,9 +579,6 @@ layout("/layouts/platform_h5.html"){
</van-action-sheet> </van-action-sheet>
<van-field label="性别" v-model="formData.condolenceSex" readonly placeholder="自动带出"></van-field> <van-field label="性别" v-model="formData.condolenceSex" readonly placeholder="自动带出"></van-field>
<van-field label="生日" :value="formatDateText(formData.condolenceBirthday)" readonly placeholder="自动带出"></van-field>
<van-field label="身份证号" v-model="formData.condolenceIdCard" readonly placeholder="自动带出"></van-field>
<van-field label="联系方式" v-model="formData.condolenceMobile" readonly placeholder="自动带出"></van-field>
<van-field <van-field
v-model="formData.typeName" v-model="formData.typeName"
@@ -805,8 +802,13 @@ layout("/layouts/platform_h5.html"){
</van-popup> </van-popup>
</template> </template>
<van-field class="more-text" name="files" required </van-cell-group>
label="附件" :rules="[{ required: true, message: '请上传附件' }]"> </section>
<section class="form-section">
<h2 class="form-section__title">附件信息</h2>
<van-cell-group :border="false">
<van-field class="more-text reimburse-upload-field" name="files" required
label="" :rules="[{ required: true, message: '请上传附件' }]">
<template #input> <template #input>
<h5-file-upload <h5-file-upload
slot="input" slot="input"
@@ -1003,7 +1005,7 @@ layout("/layouts/platform_h5.html"){
</div> </div>
</van-form> </van-form>
</div> </div>
<van-popup v-model:show="invoiceDialogVisible" position="bottom" round :close-on-click-overlay="true" class="invoice-popup"> <van-popup v-model:show="invoiceDialogVisible" position="bottom" round :close-on-click-overlay="true" get-container="#app" class="invoice-popup">
<van-nav-bar :title="invoiceDialogMode === 'edit' ? '编辑发票明细' : '新增发票明细'" <van-nav-bar :title="invoiceDialogMode === 'edit' ? '编辑发票明细' : '新增发票明细'"
left-text="取消" left-text="取消"
left-arrow left-arrow
@@ -1013,7 +1015,7 @@ layout("/layouts/platform_h5.html"){
<van-loading type="spinner" color="#1677ff" size="24px">发票识别中...</van-loading> <van-loading type="spinner" color="#1677ff" size="24px">发票识别中...</van-loading>
</div> </div>
<van-cell-group class="invoice-popup-card"> <van-cell-group class="invoice-popup-card">
<van-field class="more-text" label="发票文件" required> <van-field class="more-text invoice-popup-file-field" label="发票文件" required>
<template #input> <template #input>
<h5-file-upload <h5-file-upload
slot="input" slot="input"
@@ -1407,7 +1409,7 @@ layout("/layouts/platform_h5.html"){
}, },
normalizeContactFields() { normalizeContactFields() {
// 提交前裁剪联系方式空格,避免系统手机号带空格时后端格式校验失败。 // 提交前裁剪联系方式空格,避免系统手机号带空格时后端格式校验失败。
const fields = ["mobile", "condolenceMobile"] const fields = ["mobile"]
fields.forEach((fieldName) => { fields.forEach((fieldName) => {
if (this.formData[fieldName]) { if (this.formData[fieldName]) {
this.$set(this.formData, fieldName, String(this.formData[fieldName]).trim()) this.$set(this.formData, fieldName, String(this.formData[fieldName]).trim())
@@ -2204,7 +2206,6 @@ layout("/layouts/platform_h5.html"){
const selectedUser = this.condolenceOptions.find(user => user.id === o.value) const selectedUser = this.condolenceOptions.find(user => user.id === o.value)
if (selectedUser) { if (selectedUser) {
this.fillCondolenceUser(selectedUser) this.fillCondolenceUser(selectedUser)
this.$set(this.formData, "condolenceMobile", selectedUser.mobile)
} }
}, },
onCondolenceTypeConfirm(o) { onCondolenceTypeConfirm(o) {
@@ -2336,9 +2337,6 @@ layout("/layouts/platform_h5.html"){
this.$set(this.formData, "condolenceUnionName", user.unionName) this.$set(this.formData, "condolenceUnionName", user.unionName)
this.$set(this.formData, "condolenceUnionCode", user.unionCode) this.$set(this.formData, "condolenceUnionCode", user.unionCode)
this.$set(this.formData, "condolenceSex", user.sex) this.$set(this.formData, "condolenceSex", user.sex)
this.$set(this.formData, "condolenceBirthday", user.birthday)
this.$set(this.formData, "condolenceIdCard", user.idCard)
this.$set(this.formData, "condolenceMobile", user.mobile)
}, },
// 查询慰问类型 // 查询慰问类型
queryCondolenceType() { queryCondolenceType() {
@@ -143,7 +143,7 @@ layout("/layouts/platform_h5.html"){
return isNaN(amount) ? value : amount.toFixed(2) + "元" return isNaN(amount) ? value : amount.toFixed(2) + "元"
}, },
onView(row) { onView(row) {
this.$refs.unionReimburseInfoRef.onOpen(row, "报销统计详情") this.$refs.unionReimburseInfoRef.onOpen(row, "报销详情")
}, },
onEdit(row) { onEdit(row) {
this.$pjaxReplace("/platform/unionReimburse/apply/h5?bizId=" + row.id + "&taskId=" + row.startTaskId) this.$pjaxReplace("/platform/unionReimburse/apply/h5?bizId=" + row.id + "&taskId=" + row.startTaskId)
@@ -258,6 +258,15 @@
background: #f7f9fd; background: #f7f9fd;
} }
/* 附件上传区域与困难帮扶申请页保持一致,避免上传控件被文本域样式包裹。 */
.union-reimburse-form .form-section .reimburse-upload-field .van-field__control {
min-height: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.union-reimburse-form .payment-section, .union-reimburse-form .payment-section,
.union-reimburse-form .invoice-section { padding: 0 0 14px; } .union-reimburse-form .invoice-section { padding: 0 0 14px; }
.union-reimburse-form .payment-toolbar, .union-reimburse-form .payment-toolbar,
@@ -388,6 +397,15 @@
border-radius: 10px; border-radius: 10px;
background: #f7f9fd; background: #f7f9fd;
} }
/* 发票文件只保留上传控件本身,移除报销文本域的边框、背景和内边距。 */
.union-reimburse-form .invoice-popup-card .more-text.invoice-popup-file-field .van-field__control {
min-height: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.union-reimburse-form .invoice-popup-tip { .union-reimburse-form .invoice-popup-tip {
margin: 10px 0 0; margin: 10px 0 0;
padding: 9px 11px; padding: 9px 11px;
@@ -15,7 +15,7 @@ const UNION_REIMBURSE_INFO = {
</div> </div>
<div class="union-info-row"> <div class="union-info-row">
<div class="union-info-label">经办人</div> <div class="union-info-label">经办人</div>
<div class="union-info-value union-info-link">{{ viewData.userName }}</div> <div class="union-info-value">{{ viewData.userName }}</div>
</div> </div>
<div class="union-info-row"> <div class="union-info-row">
<div class="union-info-label">联系方式</div> <div class="union-info-label">联系方式</div>
@@ -100,18 +100,6 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-label">性别</div> <div class="union-info-label">性别</div>
<div class="union-info-value">{{ viewData.condolenceSex }}</div> <div class="union-info-value">{{ viewData.condolenceSex }}</div>
</div> </div>
<div class="union-info-row">
<div class="union-info-label">生日</div>
<div class="union-info-value">{{ formatDateText(viewData.condolenceBirthday) }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">身份证号</div>
<div class="union-info-value">{{ viewData.condolenceIdCard }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">联系方式</div>
<div class="union-info-value">{{ viewData.condolenceMobile }}</div>
</div>
<div class="union-info-row"> <div class="union-info-row">
<div class="union-info-label">慰问类型</div> <div class="union-info-label">慰问类型</div>
<div class="union-info-value">{{ viewData.typeName || viewData.condolenceTypeName }}</div> <div class="union-info-value">{{ viewData.typeName || viewData.condolenceTypeName }}</div>
@@ -186,7 +174,7 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-section-title">活动信息</div> <div class="union-info-section-title">活动信息</div>
<div class="union-info-row"> <div class="union-info-row">
<div class="union-info-label">活动名称</div> <div class="union-info-label">活动名称</div>
<div class="union-info-value union-info-link">{{ viewData.activityName }}</div> <div class="union-info-value">{{ viewData.activityName }}</div>
</div> </div>
<div class="union-info-row"> <div class="union-info-row">
<div class="union-info-label">活动人数</div> <div class="union-info-label">活动人数</div>
@@ -346,17 +334,6 @@ const UNION_REIMBURSE_INFO = {
return this.viewData.paymentDetails || [] return this.viewData.paymentDetails || []
} }
}, },
watch: {
visible(value) {
const manager = window.h5HistoryLayerManager
if (!manager) return
if (value) {
manager.open(this.historyLayerName)
return
}
manager.close(this.historyLayerName)
}
},
mounted() { mounted() {
this.injectInfoStyle() this.injectInfoStyle()
const manager = window.h5HistoryLayerManager const manager = window.h5HistoryLayerManager
@@ -386,12 +363,19 @@ const UNION_REIMBURSE_INFO = {
onOpen(row, title) { onOpen(row, title) {
this.$set(this, "row", row) this.$set(this, "row", row)
this.$set(this, "detailTitle", title || "报销详情") this.$set(this, "detailTitle", title || "报销详情")
this.$set(this, "visible", true)
this.$set(this, "currentPaymentIndex", 0) this.$set(this, "currentPaymentIndex", 0)
// 详情弹层的显示状态由全局历史栈统一同步,确保系统返回键与页面返回按钮关闭同一层。
const manager = window.h5HistoryLayerManager
if (manager && manager.open(this.historyLayerName)) {
this.getInfo()
this.getDoneTasks()
return
}
this.$set(this, "visible", true)
this.getInfo() this.getInfo()
this.getDoneTasks() this.getDoneTasks()
}, },
// 由全局历史栈关闭详情,保证遮罩、关闭按钮和浏览器返回键遵循同一顺序 // 只关闭当前详情对应的历史层,避免误关闭审核确认弹框等上层业务弹框
onClose() { onClose() {
const manager = window.h5HistoryLayerManager const manager = window.h5HistoryLayerManager
if (manager && manager.close(this.historyLayerName)) return if (manager && manager.close(this.historyLayerName)) return
@@ -144,7 +144,7 @@ layout("/layouts/platform_h5.html"){
return value && this.$moment(value).isValid() ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : "" return value && this.$moment(value).isValid() ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : ""
}, },
onView(row) { onView(row) {
this.$refs.unionReimburseInfoRef.onOpen(row, "报销申请详情") this.$refs.unionReimburseInfoRef.onOpen(row, "报销详情")
}, },
onEdit(row) { onEdit(row) {
this.$pjaxReplace('/platform/unionReimburse/apply/h5?taskId=' + (row.startTaskId || '') + "&bizId=" + row.id) this.$pjaxReplace('/platform/unionReimburse/apply/h5?taskId=' + (row.startTaskId || '') + "&bizId=" + row.id)
@@ -166,7 +166,7 @@ layout("/layouts/platform_h5.html"){
submitTaskAction() { const submitType = this.pendingSubmitType; this.closeSubmitConfirm(() => { this.$set(this, "formLoading", true); this.$axios.post("/platform/unionReimburse/review/reviewTask", {data: JSON.stringify(Object.assign({}, this.formData)), submitType: submitType}).then((res) => { if (res.code === 0) { this.$toast.success("提交成功"); this.doSearch(); this.$refs.unionReimburseInfoRef.onClose() } else { this.$toast.fail(res.msg) } }).finally(() => { this.$set(this, "formLoading", false) }) }) }, submitTaskAction() { const submitType = this.pendingSubmitType; this.closeSubmitConfirm(() => { this.$set(this, "formLoading", true); this.$axios.post("/platform/unionReimburse/review/reviewTask", {data: JSON.stringify(Object.assign({}, this.formData)), submitType: submitType}).then((res) => { if (res.code === 0) { this.$toast.success("提交成功"); this.doSearch(); this.$refs.unionReimburseInfoRef.onClose() } else { this.$toast.fail(res.msg) } }).finally(() => { this.$set(this, "formLoading", false) }) }) },
onView(row) { onView(row) {
this.$set(this, "showApprovalForm", false) this.$set(this, "showApprovalForm", false)
this.$refs.unionReimburseInfoRef.onOpen(row, "报销申请详情") this.$refs.unionReimburseInfoRef.onOpen(row, "报销详情")
}, },
onAudit(row) { onAudit(row) {
this.$set(this, "showApprovalForm", true) this.$set(this, "showApprovalForm", true)
@@ -82,6 +82,22 @@
background-color: #ff976a; background-color: #ff976a;
} }
/* 校工会预审核四个操作按钮与 PC 端 Element UI 的 primary、danger、info 配色保持一致。 */
.proposal-pre-audit-actions .proposal-pre-audit-button--primary {
border-color: var(--color-primary);
background-color: var(--color-primary);
}
.proposal-pre-audit-actions .proposal-pre-audit-button--danger {
border-color: #f56c6c;
background-color: #f56c6c;
}
.proposal-pre-audit-actions .proposal-pre-audit-button--info {
border-color: #909399;
background-color: #909399;
}
.proposal-soft-pagination .van-pagination__item { .proposal-soft-pagination .van-pagination__item {
color: var(--proposal-soft-primary); color: var(--proposal-soft-primary);
background-color: #f8fbff; background-color: #f8fbff;
@@ -280,11 +296,18 @@
.proposal-view-card .van-cell__title { .proposal-view-card .van-cell__title {
flex: 0 0 92px; flex: 0 0 92px;
width: 92px; width: 92px;
color: #596779; color: #354255;
font-size: 14px; font-size: 14px;
font-weight: 600;
line-height: 24px; line-height: 24px;
} }
/* 纵向字段中 flex-basis 会转为标题高度,需要取消横向字段使用的 92px 固定占位。 */
.proposal-view-card .direction-column-field .van-cell__title {
flex: none;
width: auto;
}
.proposal-view-card .van-cell__value { .proposal-view-card .van-cell__value {
min-width: 0; min-width: 0;
color: #4f5d70; color: #4f5d70;
@@ -495,7 +518,7 @@
.proposal-view-table-wrap .el-table { .proposal-view-table-wrap .el-table {
min-width: 560px; min-width: 560px;
font-size: 16px; font-size: 14px;
} }
.proposal-view-slot:empty { .proposal-view-slot:empty {
@@ -19,7 +19,8 @@ const PROPOSAL_INFO = {
:value="viewData.caseFilingType"></dict-tag> :value="viewData.caseFilingType"></dict-tag>
</van-cell> </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="提案方式">{{ getDictText(dict.type.PROPOSAL_SOURCE, viewData.source) }}
</van-cell>
<van-cell title="代表姓名">{{ viewData.createUserName || '--' }}</van-cell> <van-cell title="代表姓名">{{ viewData.createUserName || '--' }}</van-cell>
<van-cell title="所属教代会">{{ viewData.sessionName || viewData.fullName || '--' }}</van-cell> <van-cell title="所属教代会">{{ viewData.sessionName || viewData.fullName || '--' }}</van-cell>
<van-cell :title="viewData.committeeName ? '所属委员会' : '所属代表团'"> <van-cell :title="viewData.committeeName ? '所属委员会' : '所属代表团'">
@@ -28,7 +29,7 @@ const PROPOSAL_INFO = {
<van-cell title="单位">{{ viewData.unitName || '--' }}</van-cell> <van-cell title="单位">{{ viewData.unitName || '--' }}</van-cell>
<van-cell title="联系电话">{{ viewData.mobile || '--' }}</van-cell> <van-cell title="联系电话">{{ viewData.mobile || '--' }}</van-cell>
<van-cell title="提案时间">{{ viewData.createTime || '--' }}</van-cell> <van-cell title="提案时间">{{ viewData.createTime || '--' }}</van-cell>
<van-cell title="提案案由" class="proposal-view-long-cell"> <van-cell title="案由" class="proposal-view-long-cell">
<div class="proposal-view-rich-value" v-html="viewData.brief || '--'"></div> <div class="proposal-view-rich-value" v-html="viewData.brief || '--'"></div>
</van-cell> </van-cell>
<van-cell title="建议措施" class="proposal-view-long-cell"> <van-cell title="建议措施" class="proposal-view-long-cell">
@@ -62,7 +63,9 @@ const PROPOSAL_INFO = {
<h2 class="proposal-view-card__title">委员会成员意见 {{ index + 1 }}</h2> <h2 class="proposal-view-card__title">委员会成员意见 {{ index + 1 }}</h2>
<van-cell-group :border="false"> <van-cell-group :border="false">
<van-cell title="委员"> <van-cell title="委员">
{{ opinion.commissionerName || '--' }}<template v-if="opinion.commissionerLoginName">{{ opinion.commissionerLoginName }}</template> {{ opinion.commissionerName || '--' }}
<template v-if="opinion.commissionerLoginName">{{ opinion.commissionerLoginName }}
</template>
</van-cell> </van-cell>
<van-cell title="立案结果"> <van-cell title="立案结果">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT" <dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
@@ -109,12 +112,12 @@ const PROPOSAL_INFO = {
<!-- 点击时间轴节点后只在此处展示该任务原有的完整详情 --> <!-- 点击时间轴节点后只在此处展示该任务原有的完整详情 -->
<section class="proposal-view-card proposal-flow-detail" v-if="selectedTask"> <section class="proposal-view-card proposal-flow-detail" v-if="selectedTask">
<h2 class="proposal-view-card__title">节点详情</h2> <h2 class="proposal-view-card__title">{{ selectedTask.displayName || '--' }}</h2>
<h3 class="proposal-flow-detail__name">{{ selectedTask.displayName || '--' }}</h3>
<van-cell-group :border="false"> <van-cell-group :border="false">
<template v-if="selectedTask.ext && selectedTask.ext.isFirstTaskNode"> <template v-if="selectedTask.ext && selectedTask.ext.isFirstTaskNode">
<van-cell title="申请用户"> <van-cell title="申请用户">
{{ selectedTask.ext.initiatorName || '--' }}{{ selectedTask.ext.initiatorAccount || '--' }} {{ selectedTask.ext.initiatorName || '--' }}{{ selectedTask.ext.initiatorAccount ||
'--' }}
</van-cell> </van-cell>
<van-cell title="申请时间">{{ selectedTask.finishTime || '--' }}</van-cell> <van-cell title="申请时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="办理结果"> <van-cell title="办理结果">
@@ -125,26 +128,35 @@ const PROPOSAL_INFO = {
<template v-else-if="selectedTask.taskName === 'invite'"> <template v-else-if="selectedTask.taskName === 'invite'">
<van-cell title="办理用户"> <van-cell title="办理用户">
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName || '--' }} {{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName
|| '--' }}
</van-cell> </van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell> <van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="附议人" class="proposal-view-long-cell"> <van-cell title="附议人" class="proposal-view-long-cell">
<div class="proposal-view-table-wrap"> <div class="proposal-view-table-wrap">
<el-table :data="selectedTask?.taskFormData?.seconder"> <el-table :data="selectedTask?.taskFormData?.seconder">
<el-table-column label="姓名" prop="userName"></el-table-column> <el-table-column label="姓名" prop="userName" width="80px"
<el-table-column label="工号" prop="loginName"></el-table-column> header-align="center" align="center"></el-table-column>
<el-table-column label="性别" prop="sex"></el-table-column> <el-table-column label="工号" prop="loginName" width="80px"
<el-table-column label="单位" prop="unitName" show-overflow-tooltip></el-table-column> header-align="center" align="center"></el-table-column>
<el-table-column label="分工会" prop="unionName" show-overflow-tooltip></el-table-column> <el-table-column label="性别" prop="sex" width="50px" header-align="center"
<el-table-column label="代表团" prop="delegationName" show-overflow-tooltip></el-table-column> align="center"></el-table-column>
<el-table-column label="单位" prop="unitName"
show-overflow-tooltip header-align="center"
align="center"></el-table-column>
<el-table-column label="代表团" prop="delegationName"
show-overflow-tooltip header-align="center"
align="center"></el-table-column>
</el-table> </el-table>
</div> </div>
</van-cell> </van-cell>
</template> </template>
<template v-else-if="selectedTask.taskName === 'committee' || selectedTask.taskName === 'committeeFilingUnit'"> <template
v-else-if="selectedTask.taskName === 'committee' || selectedTask.taskName === 'committeeFilingUnit'">
<van-cell title="办理用户"> <van-cell title="办理用户">
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName || '--' }} {{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName
|| '--' }}
</van-cell> </van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell> <van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="立案结果"> <van-cell title="立案结果">
@@ -155,19 +167,23 @@ const PROPOSAL_INFO = {
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE" <dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="selectedTask.ext.tf_caseFilingType"></dict-tag> :value="selectedTask.ext.tf_caseFilingType"></dict-tag>
</van-cell> </van-cell>
<template v-if="['CONFIRM_FILING', 'SUGGESTION'].includes(selectedTask.ext.tf_caseFilingResult)"> <template
v-if="['CONFIRM_FILING', 'SUGGESTION'].includes(selectedTask.ext.tf_caseFilingResult)">
<van-cell v-if="selectedTask.ext.tf_caseFilingResult === 'SUGGESTION'" title="承办单位"> <van-cell v-if="selectedTask.ext.tf_caseFilingResult === 'SUGGESTION'" title="承办单位">
{{ selectedTask?.ext?.tf_masterUnitNameStr || selectedTask?.ext?.tf_slaveUnitNameStr || '--' }} {{ selectedTask?.ext?.tf_masterUnitNameStr || selectedTask?.ext?.tf_slaveUnitNameStr
|| '--' }}
</van-cell> </van-cell>
<template v-else> <template v-else>
<van-cell title="主办单位">{{ selectedTask?.ext?.tf_masterUnitNameStr || '--' }}</van-cell> <van-cell title="主办单位">{{ selectedTask?.ext?.tf_masterUnitNameStr || '--' }}
<van-cell title="协办单位">{{ selectedTask?.ext?.tf_slaveUnitNameStr || '--' }}</van-cell> </van-cell>
<van-cell title="协办单位">{{ selectedTask?.ext?.tf_slaveUnitNameStr || '--' }}
</van-cell>
</template> </template>
</template> </template>
<van-cell title="办理意见" <van-cell title="办理意见"
v-if="selectedTask.taskFormData.tf_opinion" v-if="selectedTask.taskFormData.tf_opinion">
class="proposal-view-long-cell"> <div class="proposal-view-rich-value"
<div class="proposal-view-rich-value" v-html="selectedTask.taskFormData.tf_opinion"></div> v-html="selectedTask.taskFormData.tf_opinion"></div>
</van-cell> </van-cell>
</template> </template>
@@ -177,7 +193,8 @@ const PROPOSAL_INFO = {
{{ selectedTask.ext.underTakeName || '--' }} {{ selectedTask.ext.underTakeName || '--' }}
</van-cell> </van-cell>
<van-cell title="办理用户"> <van-cell title="办理用户">
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName || '--' }} {{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName
|| '--' }}
</van-cell> </van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell> <van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="办理评价" v-if="selectedTask.taskName === 'feedback'"> <van-cell title="办理评价" v-if="selectedTask.taskName === 'feedback'">
@@ -189,9 +206,9 @@ const PROPOSAL_INFO = {
:value="selectedTask.ext.submitType"></dict-tag> :value="selectedTask.ext.submitType"></dict-tag>
</van-cell> </van-cell>
<van-cell title="办理意见" <van-cell title="办理意见"
v-if="selectedTask.taskFormData && selectedTask.taskFormData.tf_opinion" v-if="selectedTask.taskFormData && selectedTask.taskFormData.tf_opinion">
class="proposal-view-long-cell"> <div class="proposal-view-rich-value"
<div class="proposal-view-rich-value" v-html="selectedTask.taskFormData.tf_opinion"></div> v-html="selectedTask.taskFormData.tf_opinion"></div>
</van-cell> </van-cell>
</template> </template>
</van-cell-group> </van-cell-group>
@@ -1,114 +1,389 @@
const PROPOSAL_TASK_POPUP = { const PROPOSAL_TASK_POPUP = {
template: /*language=HTML*/ ` template: /*language=HTML*/ `
<van-popup v-model="visible" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app"> <van-popup v-model="visible" position="right" class="proposal-full-popup" :close-on-click-overlay="false"
get-container="#app">
<div class="proposal-full-popup__page"> <div class="proposal-full-popup__page">
<van-nav-bar @click-left="close" left-arrow left-text="返回" :title="title" placeholder fixed></van-nav-bar> <van-nav-bar @click-left="close" left-arrow left-text="返回" :title="title" placeholder
fixed></van-nav-bar>
<div class="proposal-full-popup__scroll"> <div class="proposal-full-popup__scroll">
<proposal-info v-if="bizId" :biz-id="bizId" :visible="visible"> <proposal-info v-if="bizId" :biz-id="bizId" :visible="visible">
<div v-if="isApproval"> <div v-if="isApproval">
<van-form ref="formRef"> <van-form ref="formRef">
<template v-if="kind === 'delegation' || kind === 'preAudit' || kind === 'schoolLeaderApproval'"> <template
v-if="kind === 'delegation' || kind === 'preAudit' || kind === 'schoolLeaderApproval'">
<section class="proposal-view-card"> <section class="proposal-view-card">
<h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2> <h2 class="proposal-view-card__title">审批意见<span
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{required:true,message:'请填写审批意见'}]" required maxlength="100" show-word-limit></van-field> class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入审批意见"
:rules="[{required:true,message:'请填写审批意见'}]" required
maxlength="100" show-word-limit></van-field>
</section> </section>
</template> </template>
<template v-if="kind === 'feedbackEvaluation'"> <template v-if="kind === 'feedbackEvaluation'">
<van-field v-model="formData.tf_feedback_name" label="满意度" placeholder="请选择满意度" :rules="[{required:true,message:'请选择满意度'}]" required readonly is-link @click="openLayer('feedback-picker')"></van-field>
<section class="proposal-view-card"> <section class="proposal-view-card">
<h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{required:true,message:'请填写审批意见'}]" required maxlength="100" show-word-limit></van-field> <h2 class="proposal-view-card__title">反馈评价<span
class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_feedback_name" label="满意度"
placeholder="请选择满意度"
:rules="[{required:true,message:'请选择满意度'}]" required readonly
is-link @click="openLayer('feedback-picker')"></van-field>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入反馈意见"
:rules="[{required:true,message:'请填写反馈意见'}]"
maxlength="100" show-word-limit></van-field>
</section> </section>
</template> </template>
<template v-if="kind === 'suggestion'"> <template v-if="kind === 'suggestion'">
<van-field v-model="formData.result" label="承办意向" placeholder="请选择承办意向" :rules="[{required:true,message:'请选择承办意向'}]" required readonly is-link @click="openLayer('result-picker')"></van-field>
<section class="proposal-view-card"> <section class="proposal-view-card">
<h2 class="proposal-view-card__title">反馈意见<span class="proposal-audit-card__required">*</span></h2> <van-field v-model="formData.result" label="承办意向"
<van-field v-model="formData.opinion" class="proposal-audit-field" name="opinion" type="textarea" rows="6" label="" placeholder="请输入反馈意见" :rules="[{required:true,message:'请输入反馈意见'}]" required show-word-limit></van-field> placeholder="请选择承办意向"
:rules="[{required:true,message:'请选择承办意向'}]" required readonly
is-link @click="openLayer('result-picker')"></van-field>
<h2 class="proposal-view-card__title">反馈意见<span
class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.opinion" class="proposal-audit-field"
name="opinion" type="textarea" rows="6" label=""
placeholder="请输入反馈意见"
:rules="[{required:true,message:'请输入反馈意见'}]" required
show-word-limit></van-field>
</section> </section>
</template> </template>
<template v-if="kind === 'unitReply'"> <template v-if="kind === 'unitReply'">
<section class="proposal-view-card"> <section class="proposal-view-card">
<van-field :value="formData.underTakeName" label="承办单位" readonly></van-field> <h2 class="proposal-view-card__title">承办单位答复<span
<van-field v-model="formData.tf_implementStateName" label="落实情况" placeholder="请选择落实情况" :rules="[{required:true,message:'请选择落实情况'}]" required readonly is-link @click="openLayer('implement-picker')"></van-field> class="proposal-audit-card__required">*</span></h2>
<h2 class="proposal-view-card__title">答复内容<span class="proposal-audit-card__required">*</span></h2> <van-field :value="formData.underTakeName" label="承办单位"
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入答复内容" :rules="[{required:true,message:'请填写答复内容'}]" required maxlength="100" show-word-limit></van-field> readonly></van-field>
<van-field v-model="formData.tf_implementStateName" label="落实情况"
placeholder="请选择落实情况"
:rules="[{required:true,message:'请选择落实情况'}]" required readonly
is-link @click="openLayer('implement-picker')"></van-field>
<!-- 承办单位答复附件上传入口暂时停用保留公用上传组件配置便于后续恢复 -->
<!-- <van-field label="附件" name="tf_attachment" class="direction-column-field">-->
<!-- <template #input>-->
<!-- <h5-file-upload-->
<!-- :value.sync="formData.tf_attachment"-->
<!-- :upload_number="5"-->
<!-- upload_mode="file"-->
<!-- upload_result_category="array"-->
<!-- upload_result_type="url"-->
<!-- complete_result>-->
<!-- </h5-file-upload>-->
<!-- </template>-->
<!-- </van-field>-->
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入答复内容"
:rules="[{required:true,message:'请填写答复内容'}]"
maxlength="100" show-word-limit></van-field>
</section> </section>
<van-field v-if="supportCandidate && formData.underTakeIsMaster" v-model="formData.tf_nextNodeOperatorName" label="校领导" placeholder="请选择校领导" :rules="[{required:true,message:'请选择校领导'}]" required readonly is-link @click="openLayer('candidate-picker')"></van-field> <van-field v-if="supportCandidate && formData.underTakeIsMaster"
v-model="formData.tf_nextNodeOperatorName" label="校领导"
placeholder="请选择校领导"
:rules="[{required:true,message:'请选择校领导'}]" required readonly
is-link @click="openLayer('candidate-picker')"></van-field>
</template> </template>
<template v-if="kind === 'caseCheck'"> <template v-if="kind === 'caseCheck'">
<van-field :value="formData.tf_username" label="审核人" readonly></van-field> <van-field :value="formData.tf_username" label="审核人" readonly></van-field>
<van-field :value="formData.tf_auditTime" label="审核时间" readonly></van-field> <van-field :value="formData.tf_auditTime" label="审核时间" readonly></van-field>
<van-field :value="formData.tf_caseCheckNeedSecondReply" label="二次答复" required :rules="[{validator:validateSecondReply,message:'请选择是否需要二次答复'}]"> <van-field :value="formData.tf_caseCheckNeedSecondReply" label="二次答复" required
<template #input><van-radio-group v-model="formData.tf_caseCheckNeedSecondReply" direction="horizontal" @change="onSecondReplyChange"><van-radio :name="0">无需二次答复</van-radio><van-radio :name="1"></van-radio></van-radio-group></template> :rules="[{validator:validateSecondReply,message:'请选择是否需要二次答复'}]">
<template #input>
<van-radio-group v-model="formData.tf_caseCheckNeedSecondReply"
direction="horizontal" @change="onSecondReplyChange">
<van-radio :name="0">无需二次答复</van-radio>
<van-radio :name="1">需要二次答复</van-radio>
</van-radio-group>
</template>
</van-field> </van-field>
<van-field v-if="formData.tf_caseCheckNeedSecondReply === 1" :value="selectedReplyUnitNames" label="答复单位" placeholder="请选择办理单位" readonly required is-link :rules="[{validator:validateSecondReplyUnits,message:'请选择需要二次答复的办理单位'}]" @click="openReplyUnitPicker"></van-field> <van-field v-if="formData.tf_caseCheckNeedSecondReply === 1"
:value="selectedReplyUnitNames" label="答复单位"
placeholder="请选择办理单位" readonly required is-link
:rules="[{validator:validateSecondReplyUnits,message:'请选择需要二次答复的办理单位'}]"
@click="openReplyUnitPicker"></van-field>
<section class="proposal-view-card"> <section class="proposal-view-card">
<h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2> <h2 class="proposal-view-card__title">审批意见<span
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" required maxlength="100" show-word-limit :rules="[{required:true,message:'请填写审批意见'}]"></van-field> class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入审批意见" required maxlength="100" show-word-limit
:rules="[{required:true,message:'请填写审批意见'}]"></van-field>
</section> </section>
</template> </template>
</van-form> </van-form>
<div class="proposal-popup-form-actions" v-if="kind === 'delegation'"> <div class="proposal-popup-form-actions" v-if="kind === 'delegation'">
<van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit(6)">退回</van-button><van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit(2)"></van-button><van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit(1)"></van-button> <van-button type="primary" block :loading="formLoading" class="proposal-soft-button"
@click="prepareSubmit(1)">同意
</van-button>
<van-button type="danger" block :loading="formLoading"
class="proposal-soft-button proposal-soft-button--danger"
@click="prepareSubmit(2)">不同意
</van-button>
<van-button type="danger" block :loading="formLoading"
class="proposal-soft-button proposal-soft-button--danger"
@click="prepareSubmit(6)">退回
</van-button>
</div> </div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'preAudit'" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr))"> <div class="proposal-popup-form-actions proposal-pre-audit-actions" v-else-if="kind === 'preAudit'"
<van-button type="warning" :loading="formLoading" class="proposal-soft-button proposal-soft-button--warning" @click="prepareSubmit(4)">退回不审核</van-button><van-button type="warning" :loading="formLoading" class="proposal-soft-button proposal-soft-button--warning" @click="prepareSubmit(6)">退</van-button><van-button type="danger" :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit(2)"></van-button><van-button type="primary" :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit(1)"></van-button> style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr))">
<van-button type="primary" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--primary"
@click="prepareSubmit(1)">同意
</van-button>
<van-button type="danger" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--danger"
@click="prepareSubmit(2)">不同意
</van-button>
<van-button type="info" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--info"
@click="prepareSubmit(4)">退回不审核
</van-button>
<van-button type="info" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--info"
@click="prepareSubmit(6)">退回审核
</van-button>
</div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'schoolLeaderApproval'">
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button"
@click="prepareSubmit('agree')">同意答复
</van-button>
<van-button type="danger" block :loading="formLoading"
class="proposal-soft-button proposal-soft-button--danger"
@click="prepareSubmit('back')">退回答复
</van-button>
</div>
<div class="proposal-popup-form-actions" v-else>
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button"
@click="prepareSubmit(1)">提交
</van-button>
<van-button block :disabled="formLoading" class="proposal-rounded-button"
@click="close">返回
</van-button>
</div> </div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'schoolLeaderApproval'"><van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit('back')">退回答复</van-button><van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit('agree')"></van-button></div>
<div class="proposal-popup-form-actions" v-else><van-button block :disabled="formLoading" class="proposal-rounded-button" @click="close">取消</van-button><van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit(1)"></van-button></div>
</div> </div>
</proposal-info> </proposal-info>
</div> </div>
</div> </div>
<!-- 字典通过异步请求加载初始化阶段可能为 null使用空数组保证弹层首次渲染不执行 null.map --> <!-- 字典通过异步请求加载初始化阶段可能为 null使用空数组保证弹层首次渲染不执行 null.map -->
<van-popup v-model="feedbackPickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="(dict.type.PROPOSAL_FEEDBACK || []).map((item) => ({text:item.label,value:item.code}))" @confirm="confirmFeedback" @cancel="closeLayer('feedback-picker')"></van-picker></van-popup> <van-popup v-model="feedbackPickerVisible" position="bottom" :close-on-click-overlay="false">
<van-popup v-model="resultPickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="(dict.type.UNDERTAKE_SUGGESTION || []).map((item) => item.code)" @confirm="confirmResult" @cancel="closeLayer('result-picker')"></van-picker></van-popup> <van-picker show-toolbar
<van-popup v-model="implementPickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="(dict.type.PROPOSAL_REPLY_IMPLEMENT || []).map((item) => ({text:item.label,value:item.code}))" @confirm="confirmImplement" @cancel="closeLayer('implement-picker')"></van-picker></van-popup> :columns="(dict.type.PROPOSAL_FEEDBACK || []).map((item) => ({text:item.label,value:item.code}))"
<van-popup v-model="candidatePickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="candidates.map((item) => ({text:item.userName,value:item.userId}))" @confirm="confirmCandidate" @cancel="closeLayer('candidate-picker')"></van-picker></van-popup> @confirm="confirmFeedback" @cancel="closeLayer('feedback-picker')"></van-picker>
<van-popup v-model="replyUnitPickerVisible" position="bottom" round :close-on-click-overlay="false" :style="{height:'70%'}"> </van-popup>
<div class="reply-unit-popup"><div class="reply-unit-toolbar"><button type="button" @click="closeLayer('reply-unit-picker')">取消</button><div class="reply-unit-toolbar-title"></div><button type="button" @click="confirmReplyUnits"></button></div> <van-popup v-model="resultPickerVisible" position="bottom" :close-on-click-overlay="false">
<div class="reply-unit-list" v-if="replyUnitOptions.length"><van-checkbox-group v-model="pendingReplyUnitIds"><van-cell v-for="item in replyUnitOptions" :key="item.unitId" clickable :title="item.unitName" :label="item.isMaster ? '主办单位' : '协办单位'" @click="toggleReplyUnit(item)"><template #right-icon><van-checkbox :name="item.unitId" @click.stop></van-checkbox></template></van-cell></van-checkbox-group></div> <van-picker show-toolbar :columns="(dict.type.UNDERTAKE_SUGGESTION || []).map((item) => item.code)"
@confirm="confirmResult" @cancel="closeLayer('result-picker')"></van-picker>
</van-popup>
<van-popup v-model="implementPickerVisible" position="bottom" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="(dict.type.PROPOSAL_REPLY_IMPLEMENT || []).map((item) => ({text:item.label,value:item.code}))"
@confirm="confirmImplement" @cancel="closeLayer('implement-picker')"></van-picker>
</van-popup>
<van-popup v-model="candidatePickerVisible" position="bottom" :close-on-click-overlay="false">
<van-picker show-toolbar :columns="candidates.map((item) => ({text:item.userName,value:item.userId}))"
@confirm="confirmCandidate" @cancel="closeLayer('candidate-picker')"></van-picker>
</van-popup>
<van-popup v-model="replyUnitPickerVisible" position="bottom" round :close-on-click-overlay="false"
:style="{height:'70%'}">
<div class="reply-unit-popup">
<div class="reply-unit-toolbar">
<button type="button" @click="closeLayer('reply-unit-picker')">取消</button>
<div class="reply-unit-toolbar-title">选择答复单位</div>
<button type="button" @click="confirmReplyUnits">确定</button>
</div>
<div class="reply-unit-list" v-if="replyUnitOptions.length">
<van-checkbox-group v-model="pendingReplyUnitIds">
<van-cell v-for="item in replyUnitOptions" :key="item.unitId" clickable
:title="item.unitName" :label="item.isMaster ? '主办单位' : '协办单位'"
@click="toggleReplyUnit(item)">
<template #right-icon>
<van-checkbox :name="item.unitId" @click.stop></van-checkbox>
</template>
</van-cell>
</van-checkbox-group>
</div>
<div class="reply-unit-empty" v-else>暂无可选办理单位</div> <div class="reply-unit-empty" v-else>暂无可选办理单位</div>
</div> </div>
</van-popup> </van-popup>
<van-dialog :value="confirmVisible" title="提示" message="您确定要提交吗?" show-cancel-button @confirm="submit" @cancel="closeLayer('submit-confirm')"></van-dialog> <van-dialog :value="confirmVisible" title="提示" message="您确定要提交吗?" show-cancel-button
@confirm="submit" @cancel="closeLayer('submit-confirm')"></van-dialog>
</van-popup> </van-popup>
`, `,
dicts: ["PROPOSAL_FEEDBACK", "UNDERTAKE_SUGGESTION", "PROPOSAL_REPLY_IMPLEMENT"], dicts: ["PROPOSAL_FEEDBACK", "UNDERTAKE_SUGGESTION", "PROPOSAL_REPLY_IMPLEMENT"],
components: {"proposal-info": PROPOSAL_INFO}, components: {"proposal-info": PROPOSAL_INFO},
props: {kind: {type: String, required: true}, pageKey: {type: String, required: true}}, props: {kind: {type: String, required: true}, pageKey: {type: String, required: true}},
data() { return {visible:false,bizId:"",action:"view",formData:{},formLoading:false,pendingSubmitType:1,feedbackPickerVisible:false,resultPickerVisible:false,implementPickerVisible:false,candidatePickerVisible:false,replyUnitPickerVisible:false,confirmVisible:false,supportCandidate:false,candidates:[],replyUnitOptions:[],pendingReplyUnitIds:[],unsubscribeHistoryLayer:null} }, data() {
return {
visible: false,
bizId: "",
action: "view",
formData: {},
formLoading: false,
pendingSubmitType: 1,
feedbackPickerVisible: false,
resultPickerVisible: false,
implementPickerVisible: false,
candidatePickerVisible: false,
replyUnitPickerVisible: false,
confirmVisible: false,
supportCandidate: false,
candidates: [],
replyUnitOptions: [],
pendingReplyUnitIds: [],
unsubscribeHistoryLayer: null
}
},
computed: { computed: {
isApproval() { return this.action === "approval" }, isApproval() {
title() { const titles={delegation:"团长审核",preAudit:"校工会预审核",schoolLeaderApproval:"分管校领导审批",feedbackEvaluation:"反馈评价",suggestion:"承办单位意见",unitReply:"承办单位答复",caseCheck:"提案委员会审查"}; return this.isApproval ? titles[this.kind] : "提案详情" }, return this.action === "approval"
selectedReplyUnitNames() { const ids=this.formData.tf_secondReplyUnitIds || []; return this.replyUnitOptions.filter((item) => ids.indexOf(item.unitId)>-1).map((item) => item.unitName+""+(item.isMaster?"主办":"协办")+"").join("、") } },
title() {
const titles = {
delegation: "团长审核",
preAudit: "校工会预审核",
schoolLeaderApproval: "分管校领导审批",
feedbackEvaluation: "反馈评价",
suggestion: "承办单位意见",
unitReply: "承办单位答复",
caseCheck: "提案委员会审查"
};
return this.isApproval ? titles[this.kind] : "提案详情"
},
selectedReplyUnitNames() {
const ids = this.formData.tf_secondReplyUnitIds || [];
return this.replyUnitOptions.filter((item) => ids.indexOf(item.unitId) > -1).map((item) => item.unitName + "" + (item.isMaster ? "主办" : "协办") + "").join("、")
}
}, },
methods: { methods: {
syncLayers(stack) { this.$set(this,"visible",stack.includes("detail")); this.$set(this,"feedbackPickerVisible",stack.includes("feedback-picker")); this.$set(this,"resultPickerVisible",stack.includes("result-picker")); this.$set(this,"implementPickerVisible",stack.includes("implement-picker")); this.$set(this,"candidatePickerVisible",stack.includes("candidate-picker")); this.$set(this,"replyUnitPickerVisible",stack.includes("reply-unit-picker")); this.$set(this,"confirmVisible",stack.includes("submit-confirm")) }, syncLayers(stack) {
openLayer(name) { window.h5HistoryLayerManager.open(name) }, closeLayer(name,callback) { window.h5HistoryLayerManager.close(name,callback) }, close() { this.closeLayer("detail") }, this.$set(this, "visible", stack.includes("detail"));
this.$set(this, "feedbackPickerVisible", stack.includes("feedback-picker"));
this.$set(this, "resultPickerVisible", stack.includes("result-picker"));
this.$set(this, "implementPickerVisible", stack.includes("implement-picker"));
this.$set(this, "candidatePickerVisible", stack.includes("candidate-picker"));
this.$set(this, "replyUnitPickerVisible", stack.includes("reply-unit-picker"));
this.$set(this, "confirmVisible", stack.includes("submit-confirm"))
},
openLayer(name) {
window.h5HistoryLayerManager.open(name)
}, closeLayer(name, callback) {
window.h5HistoryLayerManager.close(name, callback)
}, close() {
this.closeLayer("detail")
},
/** row 提供详情主键和当前 WF 任务参数,action 为 view 或 approval。 */ /** row 提供详情主键和当前 WF 任务参数,action 为 view 或 approval。 */
open(row, action) { open(row, action) {
const defaultTaskNames={delegation:"团长审核",preAudit:"校工会预审核",feedbackEvaluation:"反馈评价",unitReply:"承办单位答复",caseCheck:"提案委员会审查"} const defaultTaskNames = {
delegation: "团长审核",
preAudit: "校工会预审核",
feedbackEvaluation: "反馈评价",
unitReply: "承办单位答复",
caseCheck: "提案委员会审查"
}
const taskName = this.kind === "unitReply" ? (row.taskName || defaultTaskNames[this.kind]) : (row.curTaskName || row.taskName || defaultTaskNames[this.kind] || "") const taskName = this.kind === "unitReply" ? (row.taskName || defaultTaskNames[this.kind]) : (row.curTaskName || row.taskName || defaultTaskNames[this.kind] || "")
this.$set(this,"bizId",row.id); this.$set(this,"action",action); this.$set(this,"formData",{proposalId:row.id,processTaskId:row.taskId || "",taskKey:row.taskKey || "",taskName:taskName,instanceId:row.instanceId || "",underTakeName:row.underTakeName || "",underTakeIsMaster:!!row.underTakeIsMaster,tf_opinion:"",opinion:""}) this.$set(this, "bizId", row.id);
if (action === "approval" && this.kind === "unitReply") this.loadCandidates(row.taskId || "") this.$set(this, "action", action);
if (action === "approval" && this.kind === "caseCheck") { this.$set(this.formData,"tf_auditTime",moment().format("YYYY-MM-DD")); this.$set(this.formData,"tf_username",(this.$store.state.user && this.$store.state.user.username) || ""); this.$set(this.formData,"tf_caseCheckNeedSecondReply",0); this.$set(this.formData,"tf_secondReplyUnitIds",[]); this.loadReplyUnits(row.id) } this.$set(this, "formData", {
proposalId: row.id,
processTaskId: row.taskId || "",
taskKey: row.taskKey || "",
taskName: taskName,
instanceId: row.instanceId || "",
underTakeName: row.underTakeName || "",
underTakeIsMaster: !!row.underTakeIsMaster,
tf_opinion: "",
opinion: ""
})
if (action === "approval" && this.kind === "unitReply") {
// 承办单位答复附件上传入口暂时停用,保留初始化代码便于后续恢复。
// this.$set(this.formData, "tf_attachment", [])
this.loadCandidates(row.taskId || "")
}
if (action === "approval" && this.kind === "caseCheck") {
this.$set(this.formData, "tf_auditTime", moment().format("YYYY-MM-DD"));
this.$set(this.formData, "tf_username", (this.$store.state.user && this.$store.state.user.username) || "");
this.$set(this.formData, "tf_caseCheckNeedSecondReply", 0);
this.$set(this.formData, "tf_secondReplyUnitIds", []);
this.loadReplyUnits(row.id)
}
this.openLayer("detail") this.openLayer("detail")
}, },
loadCandidates(taskId) { this.$axios.post("/flow/common/candidate",{taskId:taskId}).then((res) => { if (res.code === 0) { this.$set(this,"candidates",res.data.candidates || []); this.$set(this,"supportCandidate",res.data.support) } }) }, loadCandidates(taskId) {
confirmFeedback(value) { this.$set(this.formData,"tf_feedback_name",value.text); this.$set(this.formData,"tf_feedback",value.value); this.closeLayer("feedback-picker") }, this.$axios.post("/flow/common/candidate", {taskId: taskId}).then((res) => {
confirmResult(value) { this.$set(this.formData,"result",value); this.closeLayer("result-picker") }, if (res.code === 0) {
confirmImplement(value) { this.$set(this.formData,"tf_implementStateName",value.text); this.$set(this.formData,"tf_implementState",value.value); this.closeLayer("implement-picker") }, this.$set(this, "candidates", res.data.candidates || []);
confirmCandidate(value) { this.$set(this.formData,"tf_nextNodeOperatorName",value.text); this.$set(this.formData,"tf_nextNodeOperator",value.value); this.closeLayer("candidate-picker") }, this.$set(this, "supportCandidate", res.data.support)
validateSecondReply(value) { return value === 0 || value === 1 }, }
validateSecondReplyUnits() { return this.formData.tf_caseCheckNeedSecondReply !== 1 || (this.formData.tf_secondReplyUnitIds || []).length > 0 }, })
onSecondReplyChange(value) { if (value !== 1) this.$set(this.formData,"tf_secondReplyUnitIds",[]) }, },
loadReplyUnits(proposalId) { this.$axios.post("/platform/proposal/case/check/listReplyUnits",{proposalId:proposalId}).then((res) => { if (res && res.code === 0) this.$set(this,"replyUnitOptions",res.data || []) }) }, confirmFeedback(value) {
openReplyUnitPicker() { this.$set(this,"pendingReplyUnitIds",[...(this.formData.tf_secondReplyUnitIds || [])]); this.openLayer("reply-unit-picker") }, this.$set(this.formData, "tf_feedback_name", value.text);
toggleReplyUnit(item) { const ids=[...this.pendingReplyUnitIds]; const index=ids.indexOf(item.unitId); if (index>-1) ids.splice(index,1); else ids.push(item.unitId); this.$set(this,"pendingReplyUnitIds",ids) }, this.$set(this.formData, "tf_feedback", value.value);
confirmReplyUnits() { this.$set(this.formData,"tf_secondReplyUnitIds",[...this.pendingReplyUnitIds]); this.closeLayer("reply-unit-picker") }, this.closeLayer("feedback-picker")
buildCaseCheckData() { const data={...this.formData}; const units=data.tf_caseCheckNeedSecondReply === 1 ? this.replyUnitOptions.filter((item) => data.tf_secondReplyUnitIds.indexOf(item.unitId)>-1) : []; const masters=units.filter((item) => item.isMaster); const slaves=units.filter((item) => !item.isMaster); data.tf_masterUnitIds=masters.map((item) => item.unitId); data.tf_masterUnitId=data.tf_masterUnitIds[0] || ""; data.tf_masterUnitNames=masters.map((item) => item.unitName); data.tf_masterUnitName=data.tf_masterUnitNames[0] || ""; data.tf_masterUnitNameStr=data.tf_masterUnitNames.join(","); data.tf_slaveUnitIds=slaves.map((item) => item.unitId); data.tf_slaveUnitNames=slaves.map((item) => item.unitName); data.tf_slaveUnitNameStr=data.tf_slaveUnitNames.join(","); return data }, },
confirmResult(value) {
this.$set(this.formData, "result", value);
this.closeLayer("result-picker")
},
confirmImplement(value) {
this.$set(this.formData, "tf_implementStateName", value.text);
this.$set(this.formData, "tf_implementState", value.value);
this.closeLayer("implement-picker")
},
confirmCandidate(value) {
this.$set(this.formData, "tf_nextNodeOperatorName", value.text);
this.$set(this.formData, "tf_nextNodeOperator", value.value);
this.closeLayer("candidate-picker")
},
validateSecondReply(value) {
return value === 0 || value === 1
},
validateSecondReplyUnits() {
return this.formData.tf_caseCheckNeedSecondReply !== 1 || (this.formData.tf_secondReplyUnitIds || []).length > 0
},
onSecondReplyChange(value) {
if (value !== 1) this.$set(this.formData, "tf_secondReplyUnitIds", [])
},
loadReplyUnits(proposalId) {
this.$axios.post("/platform/proposal/case/check/listReplyUnits", {proposalId: proposalId}).then((res) => {
if (res && res.code === 0) this.$set(this, "replyUnitOptions", res.data || [])
})
},
openReplyUnitPicker() {
this.$set(this, "pendingReplyUnitIds", [...(this.formData.tf_secondReplyUnitIds || [])]);
this.openLayer("reply-unit-picker")
},
toggleReplyUnit(item) {
const ids = [...this.pendingReplyUnitIds];
const index = ids.indexOf(item.unitId);
if (index > -1) ids.splice(index, 1); else ids.push(item.unitId);
this.$set(this, "pendingReplyUnitIds", ids)
},
confirmReplyUnits() {
this.$set(this.formData, "tf_secondReplyUnitIds", [...this.pendingReplyUnitIds]);
this.closeLayer("reply-unit-picker")
},
buildCaseCheckData() {
const data = {...this.formData};
const units = data.tf_caseCheckNeedSecondReply === 1 ? this.replyUnitOptions.filter((item) => data.tf_secondReplyUnitIds.indexOf(item.unitId) > -1) : [];
const masters = units.filter((item) => item.isMaster);
const slaves = units.filter((item) => !item.isMaster);
data.tf_masterUnitIds = masters.map((item) => item.unitId);
data.tf_masterUnitId = data.tf_masterUnitIds[0] || "";
data.tf_masterUnitNames = masters.map((item) => item.unitName);
data.tf_masterUnitName = data.tf_masterUnitNames[0] || "";
data.tf_masterUnitNameStr = data.tf_masterUnitNames.join(",");
data.tf_slaveUnitIds = slaves.map((item) => item.unitId);
data.tf_slaveUnitNames = slaves.map((item) => item.unitName);
data.tf_slaveUnitNameStr = data.tf_slaveUnitNames.join(",");
return data
},
prepareSubmit(value) { prepareSubmit(value) {
// 各办理页面提交前统一校验意见字段;空白字符按未填写处理,提示后不再打开确认弹框。 // 各办理页面提交前统一校验意见字段;空白字符按未填写处理,提示后不再打开确认弹框。
const opinionConfig = this.kind === "suggestion" const opinionConfig = this.kind === "suggestion"
@@ -120,24 +395,63 @@ const PROPOSAL_TASK_POPUP = {
this.$toast(opinionConfig.message) this.$toast(opinionConfig.message)
return return
} }
this.$refs.formRef.validate().then(() => { this.$set(this,"pendingSubmitType",value); this.openLayer("submit-confirm") }).catch(() => {}) this.$refs.formRef.validate().then(() => {
this.$set(this, "pendingSubmitType", value);
this.openLayer("submit-confirm")
}).catch(() => {
})
}, },
/** 根据页面类型保留原接口和参数分支,成功后清理全部弹层历史并通知列表刷新。 */ /** 根据页面类型保留原接口和参数分支,成功后清理全部弹层历史并通知列表刷新。 */
submit() { submit() {
this.$set(this, "formLoading", true) this.$set(this, "formLoading", true)
let request let request
if (this.kind === "suggestion") request = this.$axios.post("/platform/proposal/suggestion/suggest", this.formData) if (this.kind === "suggestion") request = this.$axios.post("/platform/proposal/suggestion/suggest", this.formData)
else if (this.kind === "schoolLeaderApproval") { const data={...this.formData,tf_approval:this.pendingSubmitType}; delete data.taskKey; delete data.taskName; request=this.$axios.post("/platform/proposal/schoolLeaderApproval/executeTask",{data:JSON.stringify(data)}) } else if (this.kind === "schoolLeaderApproval") {
else { const data = {...this.formData, tf_approval: this.pendingSubmitType};
const data=this.kind === "caseCheck" ? {...this.buildCaseCheckData(),submitType:this.pendingSubmitType} : {...this.formData,submitType:this.pendingSubmitType} delete data.taskKey;
if (this.kind === "preAudit") { data.taskName="startTask"; if (this.pendingSubmitType === 4) { data.tf_sourceTaskKey=this.formData.taskKey; data.tf_audit=false } } delete data.taskName;
if (this.kind === "unitReply") { delete data.underTakeIsMaster; delete data.underTakeName } request = this.$axios.post("/platform/proposal/schoolLeaderApproval/executeTask", {data: JSON.stringify(data)})
if (this.kind === "feedbackEvaluation" && data.tf_feedback === "DISSATISFIED") request=this.$axios.post("/platform/proposal/feedbackEvaluation/caseInfo",{instId:data.instanceId}).then((res) => { if (res.code !== 0) return Promise.reject(new Error(res.msg || "查询立案信息失败")); data.tf_masterUnitId=res.data.tf_masterUnitId; return this.$axios.post("/flow/common/executeTask",{data:JSON.stringify(data)}) }) } else {
const data = this.kind === "caseCheck" ? {
...this.buildCaseCheckData(),
submitType: this.pendingSubmitType
} : {...this.formData, submitType: this.pendingSubmitType}
if (this.kind === "preAudit") {
data.taskName = "startTask";
if (this.pendingSubmitType === 4) {
data.tf_sourceTaskKey = this.formData.taskKey;
data.tf_audit = false
}
}
if (this.kind === "unitReply") {
delete data.underTakeIsMaster;
delete data.underTakeName
}
if (this.kind === "feedbackEvaluation" && data.tf_feedback === "DISSATISFIED") request = this.$axios.post("/platform/proposal/feedbackEvaluation/caseInfo", {instId: data.instanceId}).then((res) => {
if (res.code !== 0) return Promise.reject(new Error(res.msg || "查询立案信息失败"));
data.tf_masterUnitId = res.data.tf_masterUnitId;
return this.$axios.post("/flow/common/executeTask", {data: JSON.stringify(data)})
})
else request = this.$axios.post("/flow/common/executeTask", {data: JSON.stringify(data)}) else request = this.$axios.post("/flow/common/executeTask", {data: JSON.stringify(data)})
} }
request.then((res) => { if (res && res.code === 0) { this.$toast.success(res.msg); window.h5HistoryLayerManager.closeAll(() => this.$emit("success")) } }).catch((error) => { if (error && error.message && error.message !== "cancel") this.$toast.fail(error.message) }).finally(() => { this.$set(this,"formLoading",false) }) request.then((res) => {
if (res && res.code === 0) {
this.$toast.success(res.msg);
window.h5HistoryLayerManager.closeAll(() => this.$emit("success"))
}
}).catch((error) => {
if (error && error.message && error.message !== "cancel") this.$toast.fail(error.message)
}).finally(() => {
this.$set(this, "formLoading", false)
})
} }
}, },
created() { window.h5HistoryLayerManager.ensureRegistered(this.pageKey); this.$set(this,"unsubscribeHistoryLayer",window.h5HistoryLayerManager.subscribe((stack) => this.syncLayers(stack))) }, created() {
beforeDestroy() { if (this.unsubscribeHistoryLayer) this.unsubscribeHistoryLayer(); if (window.h5HistoryLayerManager.pageKey === this.pageKey) window.h5HistoryLayerManager.unregister(this.pageKey) } window.h5HistoryLayerManager.ensureRegistered(this.pageKey);
this.$set(this, "unsubscribeHistoryLayer", window.h5HistoryLayerManager.subscribe((stack) => this.syncLayers(stack)))
},
beforeDestroy() {
if (this.unsubscribeHistoryLayer) this.unsubscribeHistoryLayer();
if (window.h5HistoryLayerManager.pageKey === this.pageKey) window.h5HistoryLayerManager.unregister(this.pageKey)
}
} }
@@ -47,6 +47,190 @@ layout("/layouts/platform_h5.html"){
color: #1989fa; color: #1989fa;
} }
/* Popup 内固定导航栏不依赖 placeholder 占位,避免遮挡邀请附议筛选框。 */
.proposal-invite-popup {
display: flex;
padding-top: 46px;
overflow: hidden;
box-sizing: border-box;
flex-direction: column;
}
/* 邀请附议人弹框固定展示工号、姓名查询入口,清空关键词后恢复完整人员列表。 */
.proposal-invite-search {
padding: 12px 12px 8px;
background-color: #f3f7fd;
}
.proposal-invite-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-invite-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-invite-search .van-field__control::placeholder {
color: #a7b0bf;
}
/* 邀请状态使用双分段切换,当前状态通过白色底和蓝色短线突出展示。 */
.proposal-invite-tabs {
padding: 4px 12px 8px;
background-color: #f3f7fd;
}
.proposal-invite-tabs .van-tabs__wrap {
height: 48px;
}
.proposal-invite-tabs .van-tabs__nav {
padding: 4px;
box-sizing: border-box;
border-radius: 14px;
background-color: #edf4ff;
}
.proposal-invite-tabs .van-tab {
height: 40px;
color: #6e7d92;
font-size: 14px;
line-height: 40px;
}
.proposal-invite-tabs .van-tab--active {
border-radius: 10px;
color: #1989fa;
font-weight: 600;
background-color: #fff;
box-shadow: 0 4px 10px rgba(31, 89, 164, .08);
}
.proposal-invite-tabs .van-tabs__line {
bottom: 4px;
width: 30px;
height: 3px;
border-radius: 3px;
background-color: #1989fa;
}
/* 人员卡片区域独立滚动,顶部筛选区和底部操作区始终保留在视口内。 */
.proposal-invite-scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
/* 人员列表以独立卡片呈现,姓名、工号、单位和勾选区域层级清晰。 */
.proposal-invite-list {
padding: 0 12px 12px;
}
.proposal-invite-list .van-cell-group {
background-color: transparent;
}
.proposal-invite-person {
min-height: 70px;
margin-top: 12px;
padding: 12px 14px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 6px 16px rgba(43, 73, 112, .08);
}
.proposal-invite-person::after {
display: none;
}
.proposal-invite-person__title {
display: block;
color: #253247;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.proposal-invite-person__login-name {
color: #79879a;
font-size: 14px;
font-weight: 400;
}
.proposal-invite-person__unit {
display: block;
margin-top: 3px;
color: #8c99aa;
font-size: 12px;
line-height: 18px;
}
.proposal-invite-person__checkbox {
display: flex;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
}
.proposal-invite-person__checkbox .van-icon {
width: 20px;
height: 20px;
box-sizing: border-box;
border: 1px solid #4b8cff;
border-radius: 50%;
background-color: #fff;
}
.proposal-invite-person__checkbox .van-checkbox__icon--checked .van-icon {
border-color: #1989fa;
background-color: #1989fa;
}
/* 分页和邀请按钮沿用同一圆角语言,与人员卡片形成统一的操作区域。 */
.proposal-invite-actions {
padding: 0 12px calc(12px + env(safe-area-inset-bottom));
background-color: #f3f7fd;
flex: none;
}
.proposal-invite-pagination {
margin-top: 2px;
}
.proposal-invite-pagination .van-pagination__item {
min-height: 36px;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 4px 12px rgba(43, 73, 112, .06);
}
.proposal-invite-pagination .van-pagination__item--active,
.proposal-invite-pagination .van-pagination__item:not(.van-pagination__item--disabled):active {
color: #fff;
background-color: #1989fa;
}
.proposal-invite-submit {
height: 42px;
margin-top: 12px;
border-radius: 12px;
}
.proposal-invite-empty {
min-height: 0;
overflow-y: auto;
flex: 1;
}
/* 详情弹框复用项目全屏 Popup 的固定导航结构,内容区域单独滚动。 */ /* 详情弹框复用项目全屏 Popup 的固定导航结构,内容区域单独滚动。 */
.proposal-detail-popup { .proposal-detail-popup {
overflow: hidden; overflow: hidden;
@@ -11,45 +11,64 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
left-arrow left-arrow
left-text="返回" left-text="返回"
fixed fixed
placeholder
@click-left="$emit('close')"> @click-left="$emit('close')">
</van-nav-bar> </van-nav-bar>
<van-search <van-search
v-model="pageForm.searchKeyword" v-model="pageForm.searchKeyword"
class="proposal-invite-search"
:show-action="false" :show-action="false"
shape="round"
clearable
placeholder="请输入工号或者姓名查询" placeholder="请输入工号或者姓名查询"
@search="doSearch"> @search="doSearch"
@clear="doSearch">
</van-search> </van-search>
<van-tabs v-model="isInvite" @click="isInviteClick"> <van-tabs v-model="isInvite" class="proposal-invite-tabs" @click="isInviteClick">
<van-tab title="已邀请" name="1"></van-tab> <van-tab title="已邀请" name="1"></van-tab>
<van-tab title="可邀请" name="0"></van-tab> <van-tab title="可邀请" name="0"></van-tab>
</van-tabs> </van-tabs>
<van-checkbox-group v-model="tableSelection" v-if="tableData && tableData.length > 0"> <div class="proposal-invite-scroll" v-if="tableData && tableData.length > 0">
<van-checkbox-group class="proposal-invite-list" v-model="tableSelection">
<van-cell-group> <van-cell-group>
<van-cell <van-cell
class="proposal-invite-person"
v-for="(item, index) in tableData" v-for="(item, index) in tableData"
clickable clickable
:key="item.loginName" :key="item.loginName"
@click="tableRowToggle(index)"> @click="tableRowToggle(index)">
<span slot="title"> <span slot="title" class="proposal-invite-person__title">
{{item.userName}}{{item.loginName}} {{item.userName}} <span class="proposal-invite-person__login-name">{{item.loginName}}</span>
</span> </span>
<span slot="label" class="font-size0 van-color-text-assist"> <span slot="label" class="proposal-invite-person__unit">
{{item.unitName}} {{item.unitName}}
</span> </span>
<van-checkbox <van-checkbox
:name="item" :name="item"
:ref="'checkboxes' + item.loginName" :ref="'checkboxes' + item.loginName"
class="proposal-invite-person__checkbox"
slot="right-icon" slot="right-icon"
@click.stop
v-if="!pageForm.isInvite"> v-if="!pageForm.isInvite">
</van-checkbox> </van-checkbox>
</van-cell> </van-cell>
</van-cell-group> </van-cell-group>
<div class="p10"> </van-checkbox-group>
</div>
<!-- 空状态提示随已邀请 / 可邀请标签切换便于用户判断当前无数据的具体范围 -->
<div v-else class="proposal-invite-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无附议人插画">
<div class="proposal-invite-empty__title">暂无附议人</div>
<div class="proposal-invite-empty__hint">
{{ isInvite === "1" ? "当前暂无已邀请的附议人" : "当前暂无可邀请的附议人" }}
</div>
</div>
<div class="proposal-invite-actions" v-if="tableData && tableData.length > 0">
<van-pagination <van-pagination
class="proposal-soft-pagination" class="proposal-soft-pagination proposal-invite-pagination"
v-model="pageForm.pageNumber" v-model="pageForm.pageNumber"
:total-items="pageForm.totalCount" :total-items="pageForm.totalCount"
:items-per-page="pageForm.pageSize" :items-per-page="pageForm.pageSize"
@@ -60,20 +79,10 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
type="primary" type="primary"
block block
:disabled="pageForm.isInvite" :disabled="pageForm.isInvite"
class="mt10 proposal-soft-button"> class="proposal-soft-button proposal-invite-submit">
邀请 邀请
</van-button> </van-button>
</div> </div>
</van-checkbox-group>
<!-- 空状态提示随已邀请 / 可邀请标签切换便于用户判断当前无数据的具体范围 -->
<div v-else class="proposal-invite-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无附议人插画">
<div class="proposal-invite-empty__title">暂无附议人</div>
<div class="proposal-invite-empty__hint">
{{ isInvite === "1" ? "当前暂无已邀请的附议人" : "当前暂无可邀请的附议人" }}
</div>
</div>
<van-dialog <van-dialog
:value="confirmVisible" :value="confirmVisible"
@@ -102,7 +111,7 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
tableData: [], tableData: [],
pageForm: { pageForm: {
pageNumber: 1, pageNumber: 1,
pageSize: 5, pageSize: 6,
totalCount: 0, totalCount: 0,
pageOrderName: "", pageOrderName: "",
pageOrderBy: "", pageOrderBy: "",
@@ -207,8 +216,16 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
this.doSearch() this.doSearch()
}, },
/**
* 点击人员卡片时切换对应的附议人勾选状态
* index 为当前页人员索引已邀请状态或找不到对应复选框时不处理无返回值
*/
tableRowToggle(index) { tableRowToggle(index) {
if (this.pageForm.isInvite || index < 0) return if (this.pageForm.isInvite || index < 0) return
const item = this.tableData[index]
const checkboxRef = item ? this.$refs["checkboxes" + item.loginName] : null
if (!checkboxRef || !checkboxRef[0]) return
checkboxRef[0].toggle()
}, },
/** /**
@@ -411,8 +411,8 @@ layout("/layouts/platform_h5.html"){
</van-form> </van-form>
</section> </section>
<div class="proposal-seconded-form-actions"> <div class="proposal-seconded-form-actions">
<van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="submitSeconded(false)">不同意</van-button>
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="submitSeconded(true)">同意</van-button> <van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="submitSeconded(true)">同意</van-button>
<van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="submitSeconded(false)">不同意</van-button>
</div> </div>
</div> </div>
</proposal-info> </proposal-info>
@@ -237,6 +237,109 @@ layout("/layouts/platform_h5.html"){
box-shadow: 0 4px 12px rgba(43, 73, 112, .08), inset 0 1px 1px rgba(255, 255, 255, .82); box-shadow: 0 4px 12px rgba(43, 73, 112, .08), inset 0 1px 1px rgba(255, 255, 255, .82);
} }
/* 选择转交人弹框沿用提案列表的圆角搜索框、人员卡片和插图空状态。 */
.proposal-transfer-user-popup .proposal-full-popup__page {
background-color: #f3f7fd;
}
.proposal-transfer-user-search {
padding: 12px 12px 8px;
background-color: #f3f7fd;
flex: none;
}
.proposal-transfer-user-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-transfer-user-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-transfer-user-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-transfer-user-scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.proposal-transfer-user-list {
padding: 0 12px 12px;
background-color: transparent;
}
.proposal-transfer-user-item {
min-height: 64px;
margin-top: 12px;
padding: 12px 14px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 6px 16px rgba(43, 73, 112, .08);
}
.proposal-transfer-user-item::after {
display: none;
}
.proposal-transfer-user-item__name {
display: block;
color: #253247;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.proposal-transfer-user-item__unit {
color: #79879a;
font-size: 13px;
font-weight: 400;
}
.proposal-transfer-user-empty {
display: flex;
min-height: 420px;
padding: 40px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-transfer-user-empty img {
display: block;
width: 82%;
max-width: 270px;
height: auto;
object-fit: contain;
}
.proposal-transfer-user-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.proposal-transfer-user-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
@media (max-width: 350px) { @media (max-width: 350px) {
.proposal-unit-reply-content, .proposal-unit-reply-content,
.proposal-unit-reply-sticky { .proposal-unit-reply-sticky {
@@ -340,7 +443,7 @@ layout("/layouts/platform_h5.html"){
<div class="action-btn" v-if="!pageForm.approval && $auth.hasRoleOr('SYSADMIN,PROPOSAL_UNIT_LEADER')" <div class="action-btn" v-if="!pageForm.approval && $auth.hasRoleOr('SYSADMIN,PROPOSAL_UNIT_LEADER')"
@click="openTransfer(row)"> @click="openTransfer(row)">
<van-icon class="proposal-action-icon" name="exchange"></van-icon> <van-icon class="proposal-action-icon" name="exchange"></van-icon>
<span></span> <span></span>
</div> </div>
</template> </template>
</table-list> </table-list>
@@ -350,6 +453,16 @@ layout("/layouts/platform_h5.html"){
<transfer ref="transferRef" @success="doSearch"></transfer> <transfer ref="transferRef" @success="doSearch"></transfer>
<proposal-task-popup ref="taskPopup" kind="unitReply" page-key="proposal-unit-reply-task" @success="doSearch"></proposal-task-popup> <proposal-task-popup ref="taskPopup" kind="unitReply" page-key="proposal-unit-reply-task" @success="doSearch"></proposal-task-popup>
<van-dialog
:value="communicationConfirmVisible"
title="提示"
message="是否已与提案代表进行充分沟通?"
confirm-button-text="是"
cancel-button-text="否"
show-cancel-button
@confirm="confirmCommunication"
@cancel="closeCommunicationConfirm">
</van-dialog>
</div> </div>
<script nonce="${cspNonce!}"> <script nonce="${cspNonce!}">
<!--#include("transfer.js"){}#--> <!--#include("transfer.js"){}#-->
@@ -378,6 +491,10 @@ layout("/layouts/platform_h5.html"){
{text: "未审核", value: "0"}, {text: "未审核", value: "0"},
{text: "已审核", value: "1"} {text: "已审核", value: "1"}
], ],
communicationConfirmVisible: false,
pendingApprovalRow: null,
historyLayerPageKey: "proposal-unit-reply",
unsubscribeHistoryLayer: null
} }
}, },
components: { components: {
@@ -414,7 +531,29 @@ layout("/layouts/platform_h5.html"){
}, },
onApproval(row, index) { onApproval(row, index) {
this.$refs.taskPopup.open(row, "approval") // row 为当前待答复提案;确认已充分沟通后,才进入原有答复表单。
this.$set(this, "pendingApprovalRow", row)
window.h5HistoryLayerManager.open("communication-confirm")
},
/** stack 为当前页面弹层名称数组;同步沟通确认框状态,无返回值。 */
syncHistoryLayers(stack) {
const visible = stack.includes("communication-confirm")
this.$set(this, "communicationConfirmVisible", visible)
if (!visible) this.$set(this, "pendingApprovalRow", null)
},
// 取消确认时同步回退浏览器历史,手机返回键与弹窗按钮行为保持一致。
closeCommunicationConfirm() {
window.h5HistoryLayerManager.close("communication-confirm")
},
// 确认后先关闭沟通弹层,再使用暂存的提案数据打开原有答复表单。
confirmCommunication() {
const row = this.pendingApprovalRow
window.h5HistoryLayerManager.close("communication-confirm", () => {
if (row) this.$refs.taskPopup.open(row, "approval")
})
}, },
onRevoke(row) { onRevoke(row) {
@@ -445,6 +584,10 @@ layout("/layouts/platform_h5.html"){
} }
}, },
created() { created() {
window.h5HistoryLayerManager.ensureRegistered(this.historyLayerPageKey)
this.$set(this, "unsubscribeHistoryLayer", window.h5HistoryLayerManager.subscribe((stack) => {
this.syncHistoryLayers(stack)
}))
const tab = new URLSearchParams(window.location.search).get("tab") const tab = new URLSearchParams(window.location.search).get("tab")
if (tab === "done") { if (tab === "done") {
this.$set(this.pageForm, "approval", true) this.$set(this.pageForm, "approval", true)
@@ -453,6 +596,12 @@ layout("/layouts/platform_h5.html"){
this.$set(this.pageForm, "approval", false) this.$set(this.pageForm, "approval", false)
this.$set(this.pageForm, "approvalText", "0") this.$set(this.pageForm, "approvalText", "0")
} }
},
beforeDestroy() {
if (this.unsubscribeHistoryLayer) this.unsubscribeHistoryLayer()
if (window.h5HistoryLayerManager.pageKey === this.historyLayerPageKey) {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
} }
}) })
</script> </script>
@@ -1,28 +1,58 @@
const transfer = { const transfer = {
template: /*language=HTML*/ ` template: /*language=HTML*/ `
<van-popup v-model="showTransferDialog" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app"> <van-popup v-model="showTransferDialog" position="right" class="proposal-full-popup proposal-transfer-dialog" :close-on-click-overlay="false" get-container="#app">
<div class="proposal-full-popup__page"> <div class="proposal-full-popup__page">
<van-nav-bar @click-left="closeTransfer" left-arrow left-text="返回" title="提案转办" placeholder fixed></van-nav-bar> <van-nav-bar @click-left="closeTransfer" left-arrow left-text="返回" title="提案转办" placeholder fixed></van-nav-bar>
<div class="proposal-full-popup__scroll transfer-dialog"> <div class="proposal-full-popup__scroll transfer-dialog">
<div class="proposal-info-card"> <section class="proposal-view-card proposal-transfer-info-card">
<div class="info-item"><label>提案编号</label><span>{{transferForm.code}}</span></div> <van-cell-group :border="false">
<div class="info-item"><label>提案名称</label><span>{{transferForm.name}}</span></div> <van-cell title="提案编号">{{transferForm.code || '--'}}</van-cell>
<div class="info-item"><label>代表团</label><span>{{transferForm.delegationName}}</span></div> <van-cell title="提案名称">{{transferForm.name || '--'}}</van-cell>
</div> <van-cell title="代表团">{{transferForm.delegationName || '--'}}</van-cell>
<div class="proposal-info-card"> </van-cell-group>
</section>
<section class="proposal-view-card proposal-transfer-user-card">
<van-cell-group :border="false">
<van-field v-model="searchKeyword" label="转交给" placeholder="搜索用户" required readonly is-link @click="openUserList"></van-field> <van-field v-model="searchKeyword" label="转交给" placeholder="搜索用户" required readonly is-link @click="openUserList"></van-field>
</van-cell-group>
</section>
</div>
<div class="button-group proposal-transfer-button-group">
<van-button block type="info" class="proposal-soft-button" @click="handleTransfer">提交</van-button>
<van-button block plain class="proposal-transfer-cancel-button" @click="closeTransfer">返回</van-button>
</div> </div>
</div> </div>
<div class="button-group proposal-transfer-button-group"><van-button block plain class="proposal-transfer-cancel-button" @click="closeTransfer">取消</van-button><van-button block type="info" class="proposal-soft-button" @click="handleTransfer"></van-button></div> <van-popup v-model="showUserList" position="right" class="proposal-full-popup proposal-transfer-user-popup" :close-on-click-overlay="false" get-container="#app">
</div>
<van-popup v-model="showUserList" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<div class="proposal-full-popup__page"> <div class="proposal-full-popup__page">
<van-nav-bar @click-left="closeUserList" left-arrow left-text="返回" title="选择转交人" placeholder fixed></van-nav-bar> <van-nav-bar @click-left="closeUserList" left-arrow left-text="返回" title="选择转交人" placeholder fixed></van-nav-bar>
<div class="proposal-full-popup__scroll"> <van-search
<van-search v-model="keyword" placeholder="搜索用户" @search="selectTransferUser(keyword)"></van-search> v-model="keyword"
<div class="proposal-view-main"> class="proposal-transfer-user-search"
<div v-if="transferUserOptions.length" class="proposal-info-card"><van-cell v-for="item in transferUserOptions" :key="item.id" :title="item.username" @click="selectUser(item)"></van-cell></div> shape="round"
<van-empty v-else class="empty-tip">请搜索需要转办的用户</van-empty> clearable
placeholder="请输入姓名或工号搜索"
@search="selectTransferUser(keyword)"
@clear="resetTransferUserSearch">
</van-search>
<div class="proposal-transfer-user-scroll">
<van-cell-group v-if="transferUserOptions.length" class="proposal-transfer-user-list" :border="false">
<van-cell
v-for="item in transferUserOptions"
:key="item.id"
class="proposal-transfer-user-item"
clickable
@click="selectUser(item)">
<span slot="title" class="proposal-transfer-user-item__name">
{{item.username}}<span class="proposal-transfer-user-item__unit">{{item.unitName || '--'}}</span>
</span>
</van-cell>
</van-cell-group>
<div v-else class="proposal-transfer-user-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无转交用户插画">
<div class="proposal-transfer-user-empty__title">
{{hasUserSearched ? '暂无搜索记录' : '请搜索需要转办的用户'}}
</div>
<div v-if="hasUserSearched" class="proposal-transfer-user-empty__hint">请调整搜索条件后重试</div>
</div> </div>
</div> </div>
</div> </div>
@@ -31,7 +61,7 @@ const transfer = {
</van-popup> </van-popup>
`, `,
data() { data() {
return {showTransferDialog:false,transferForm:{},transferUserOptions:[],searchKeyword:"",showUserList:false,keyword:"",confirmVisible:false,historyLayerPageKey:"proposal-unit-reply-transfer",unsubscribeHistoryLayer:null} return {showTransferDialog:false,transferForm:{},transferUserOptions:[],searchKeyword:"",showUserList:false,keyword:"",hasUserSearched:false,confirmVisible:false,historyLayerPageKey:"proposal-unit-reply-transfer",unsubscribeHistoryLayer:null}
}, },
computed: { computed: {
confirmMessage() { confirmMessage() {
@@ -43,12 +73,15 @@ const transfer = {
syncLayers(stack) { this.$set(this,"showTransferDialog",stack.includes("transfer")); this.$set(this,"showUserList",stack.includes("user-list")); this.$set(this,"confirmVisible",stack.includes("confirm")) }, syncLayers(stack) { this.$set(this,"showTransferDialog",stack.includes("transfer")); this.$set(this,"showUserList",stack.includes("user-list")); this.$set(this,"confirmVisible",stack.includes("confirm")) },
openTransfer(row) { this.$set(this,"transferForm",{taskId:row.taskId,name:row.name,code:row.code,delegationName:row.delegationName}); this.$set(this,"searchKeyword",""); window.h5HistoryLayerManager.open("transfer") }, openTransfer(row) { this.$set(this,"transferForm",{taskId:row.taskId,name:row.name,code:row.code,delegationName:row.delegationName}); this.$set(this,"searchKeyword",""); window.h5HistoryLayerManager.open("transfer") },
closeTransfer() { window.h5HistoryLayerManager.close("transfer") }, closeTransfer() { window.h5HistoryLayerManager.close("transfer") },
openUserList() { window.h5HistoryLayerManager.open("user-list") }, /** 打开用户列表前重置搜索条件和结果,确保首次进入展示搜索引导空状态。 */
openUserList() { this.resetTransferUserSearch(); window.h5HistoryLayerManager.open("user-list") },
closeUserList() { window.h5HistoryLayerManager.close("user-list") }, closeUserList() { window.h5HistoryLayerManager.close("user-list") },
closeConfirm() { window.h5HistoryLayerManager.close("confirm") }, closeConfirm() { window.h5HistoryLayerManager.close("confirm") },
selectUser(item) { this.$set(this.transferForm,"userId",item.id); this.$set(this,"searchKeyword",item.username); this.closeUserList() }, selectUser(item) { this.$set(this.transferForm,"userId",item.id); this.$set(this,"searchKeyword",item.username); this.closeUserList() },
/** keyword 为姓名关键字,接口返回可转交用户数组。 */ /** 清空姓名或工号关键词、搜索状态和人员结果,无返回值。 */
selectTransferUser(keyword) { this.$axios.post("/platform/proposal/unitReply/selectViceUser",{keyword:keyword}).then((res) => { if (res.code === 0) this.$set(this,"transferUserOptions",res.data || []) }) }, resetTransferUserSearch() { this.$set(this,"keyword",""); this.$set(this,"hasUserSearched",false); this.$set(this,"transferUserOptions",[]) },
/** keyword 为姓名或工号关键词;空值仅恢复搜索引导,非空值查询并返回可转交用户数组。 */
selectTransferUser(keyword) { const searchText=(keyword || "").trim(); if (!searchText) { this.resetTransferUserSearch(); return } this.$set(this,"hasUserSearched",true); this.$axios.post("/platform/proposal/unitReply/selectViceUser",{keyword:searchText}).then((res) => { if (res.code === 0) this.$set(this,"transferUserOptions",res.data || []) }) },
handleTransfer() { if (!this.transferForm.userId) { this.$toast("请选择转交用户"); return } window.h5HistoryLayerManager.open("confirm") }, handleTransfer() { if (!this.transferForm.userId) { this.$toast("请选择转交用户"); return } window.h5HistoryLayerManager.open("confirm") },
/** 提交 taskId 和 userId 完成转办,成功后清理弹层并通知父列表刷新。 */ /** 提交 taskId 和 userId 完成转办,成功后清理弹层并通知父列表刷新。 */
confirmTransfer() { this.$axios.post("/platform/proposal/unitReply/transfer",this.transferForm).then((res) => { if (res.code === 0) { this.$toast.success(res.msg); window.h5HistoryLayerManager.closeAll(() => this.$emit("success")) } }) } confirmTransfer() { this.$axios.post("/platform/proposal/unitReply/transfer",this.transferForm).then((res) => { if (res.code === 0) { this.$toast.success(res.msg); window.h5HistoryLayerManager.closeAll(() => this.$emit("success")) } }) }
@@ -278,11 +278,6 @@ layout("/layouts/platform_h5.html"){
.welfare-list-card__choices-arrow { .welfare-list-card__choices-arrow {
margin-left: 10px; margin-left: 10px;
flex: none; flex: none;
transition: transform .2s ease;
}
.welfare-list-card__choices-arrow.is-expanded {
transform: rotate(90deg);
} }
.welfare-list-card__choices > .welfare-inline-loading, .welfare-list-card__choices > .welfare-inline-loading,
@@ -446,32 +441,6 @@ layout("/layouts/platform_h5.html"){
white-space: nowrap; white-space: nowrap;
} }
.welfare-list-card__actions {
display: flex;
min-height: 42px;
margin-top: 10px;
padding-top: 10px;
align-items: center;
justify-content: flex-end;
border-top: 1px solid #edf1f6;
box-sizing: border-box;
}
.welfare-list-card__detail-button {
display: inline-flex;
height: 30px;
padding: 0 14px;
align-items: center;
justify-content: center;
border: 1px solid #1989fa;
border-radius: 16px;
color: #1989fa;
background-color: #fff;
font-size: 13px;
line-height: 28px;
box-sizing: border-box;
}
.welfare-list-card__time .van-icon { .welfare-list-card__time .van-icon {
margin-right: 5px; margin-right: 5px;
flex: none; flex: none;
@@ -486,76 +455,6 @@ layout("/layouts/platform_h5.html"){
font-size: 17px; font-size: 17px;
} }
.welfare-list-card__expanded {
padding: 12px;
border-top: 1px solid #edf1f6;
background-color: #f8fbff;
}
.welfare-inline-card {
margin-bottom: 10px;
padding: 0 12px;
overflow: hidden;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 4px 14px rgba(55, 85, 126, .06);
}
.welfare-inline-card:last-child {
margin-bottom: 0;
}
.welfare-inline-card__title {
position: relative;
height: 42px;
padding-left: 8px;
border-bottom: 1px solid #edf1f6;
color: #263548;
font-size: 14px;
font-weight: 600;
line-height: 42px;
}
.welfare-inline-card__title::before {
position: absolute;
top: 14px;
bottom: 14px;
left: 0;
width: 3px;
border-radius: 2px;
background-color: #1989fa;
content: '';
}
.welfare-inline-row {
display: flex;
min-height: 40px;
padding: 6px 0;
align-items: center;
border-bottom: 1px solid #edf1f6;
box-sizing: border-box;
}
.welfare-inline-row:last-child {
border-bottom: 0;
}
.welfare-inline-row__label {
width: 72px;
flex: none;
color: #7f8da1;
font-size: 12px;
}
.welfare-inline-row__value {
min-width: 0;
flex: 1;
color: #263548;
font-size: 13px;
line-height: 19px;
word-break: break-word;
}
.welfare-inline-selection { .welfare-inline-selection {
display: flex; display: flex;
min-height: 72px; min-height: 72px;
@@ -780,8 +679,10 @@ layout("/layouts/platform_h5.html"){
v-for="row in tableData" v-for="row in tableData"
:key="row.id" :key="row.id"
class="welfare-list-card" class="welfare-list-card"
role="button"
@click="modifySelection(row)"
> >
<div class="welfare-list-card__summary" @click="toggleRow(row)"> <div class="welfare-list-card__summary">
<div class="welfare-list-card__order-head"> <div class="welfare-list-card__order-head">
<div class="welfare-list-card__order-info"> <div class="welfare-list-card__order-info">
<div class="welfare-list-card__title">{{ row.name }}</div> <div class="welfare-list-card__title">{{ row.name }}</div>
@@ -812,66 +713,9 @@ layout("/layouts/platform_h5.html"){
</template> </template>
<div v-else class="welfare-inline-empty">暂无福利选择记录</div> <div v-else class="welfare-inline-empty">暂无福利选择记录</div>
</div> </div>
<van-icon class="welfare-list-card__arrow welfare-list-card__choices-arrow" :class="{'is-expanded': row.expanded}" name="arrow"></van-icon> <van-icon class="welfare-list-card__arrow welfare-list-card__choices-arrow" name="arrow"></van-icon>
</div> </div>
</div> </div>
<div class="welfare-list-card__expanded" v-show="row.expanded">
<div class="welfare-inline-loading" v-if="row.detailLoading">
<van-loading size="20" color="#1989fa">加载中...</van-loading>
</div>
<template v-else-if="row.projectInfo">
<section class="welfare-inline-card">
<div class="welfare-inline-card__title">项目基本信息</div>
<div class="welfare-inline-row">
<span class="welfare-inline-row__label">项目名称</span>
<span class="welfare-inline-row__value">{{ row.projectInfo.name || row.name }}</span>
</div>
<div class="welfare-inline-row">
<span class="welfare-inline-row__label">选择时间</span>
<span class="welfare-inline-row__value">{{ formatRange(row.projectInfo.choiceTimeStart, row.projectInfo.choiceTimeEnd) }}</span>
</div>
<div class="welfare-inline-row">
<span class="welfare-inline-row__label">发放时间</span>
<span class="welfare-inline-row__value">{{ formatRange(row.projectInfo.provideTimeStart, row.projectInfo.provideTimeEnd) }}</span>
</div>
<div class="welfare-inline-row">
<span class="welfare-inline-row__label">发放地点</span>
<span class="welfare-inline-row__value">{{ row.projectInfo.provideAddress || '暂无' }}</span>
</div>
</section>
<section class="welfare-inline-card">
<div class="welfare-inline-card__title">联系信息</div>
<div class="welfare-inline-row">
<span class="welfare-inline-row__label">联系电话</span>
<span class="welfare-inline-row__value">{{ firstSelection(row).mobile || '暂无' }}</span>
</div>
<div class="welfare-inline-row">
<span class="welfare-inline-row__label">收货地址</span>
<span class="welfare-inline-row__value">{{ firstSelection(row).receiveAddress || '暂无' }}</span>
</div>
</section>
<section class="welfare-inline-card">
<div class="welfare-inline-card__title">我的选择</div>
<div v-if="row.selections.length > 0">
<div class="welfare-inline-selection" v-for="item in row.selections" :key="item.selectId || item.id">
<van-image class="welfare-inline-selection__image" :src="item.imgUrl" fit="cover" @click.stop="previewSelectionImage(item.imgUrl)">
<template #error><van-icon name="photo-o" size="24"></van-icon></template>
</van-image>
<div class="welfare-inline-selection__content">
<div class="welfare-inline-selection__name">{{ item.optionName || '未知选项' }}</div>
<div class="welfare-inline-selection__meta">
<span class="welfare-inline-selection__tag">× {{ item.selectNum || 0 }}</span>
</div>
</div>
</div>
</div>
<div v-else class="welfare-inline-empty">暂无福利选择记录</div>
</section>
</template>
</div>
</article> </article>
</div> </div>
</van-list> </van-list>
@@ -928,10 +772,8 @@ layout("/layouts/platform_h5.html"){
if (res.code !== 0 || !res.data) return if (res.code !== 0 || !res.data) return
const list = (res.data.list || []).map((row) => ({ const list = (res.data.list || []).map((row) => ({
...row, ...row,
expanded: false,
detailLoading: false, detailLoading: false,
detailLoaded: false, detailLoaded: false,
projectInfo: null,
selections: [] selections: []
})) }))
this.tableData = this.tableData.concat(list) this.tableData = this.tableData.concat(list)
@@ -993,7 +835,6 @@ layout("/layouts/platform_h5.html"){
description: option.description description: option.description
} }
}) : [] }) : []
this.$set(row, "projectInfo", projectInfo)
this.$set(row, "selections", selections) this.$set(row, "selections", selections)
this.$set(row, "detailLoaded", true) this.$set(row, "detailLoaded", true)
} finally { } finally {
@@ -1001,36 +842,8 @@ layout("/layouts/platform_h5.html"){
} }
}, },
/** modifySelection(row) {
* 展开或收起当前福利卡片的内容详情。 this.$pjaxReplace("/platform/h5/welfare/userSelect/index?id=" + (row.projectId || row.id) + "&from=mine")
* @param row 当前福利项目
*/
toggleRow(row) {
row.expanded = !row.expanded
},
previewSelectionImage(imageUrl) {
if (imageUrl) vant.ImagePreview([imageUrl])
},
/**
* 获取当前福利的首条选择记录,用于展示联系电话和收货地址。
* @param row 当前福利项目
* @return {Object} 首条选择记录;无记录时返回空对象
*/
firstSelection(row) {
return row.selections && row.selections.length > 0 ? row.selections[0] : {}
},
/**
* 格式化项目起止时间。
* @param startTime 开始时间
* @param endTime 结束时间
* @return {string} 起止时间文本
*/
formatRange(startTime, endTime) {
if (!startTime || !endTime) return "暂无"
return this.$moment(startTime).format("YYYY-MM-DD HH:mm") + " 至 " + this.$moment(endTime).format("YYYY-MM-DD HH:mm")
}, },
/** /**
@@ -26,6 +26,10 @@ layout("/layouts/platform_h5.html"){
background: var(--bg-color); background: var(--bg-color);
} }
.page-container.has-footer-contact {
padding-bottom: calc(126px + env(safe-area-inset-bottom));
}
.page-container .van-nav-bar { .page-container .van-nav-bar {
background-color: #fff; background-color: #fff;
} }
@@ -383,27 +387,6 @@ layout("/layouts/platform_h5.html"){
text-align: center; text-align: center;
} }
.welfare-option-selected-mark {
position: absolute;
right: 0;
top: 0;
z-index: 2;
display: flex;
width: 32px;
height: 32px;
padding: 0 0 4px 4px;
align-items: center;
justify-content: center;
border-radius: 0 11px 0 18px;
color: #fff;
background: #35b878;
box-shadow: none;
box-sizing: border-box;
font-size: 14px;
line-height: 32px;
text-align: center;
}
.welfare-deadline { .welfare-deadline {
margin: 0 12px 8px; margin: 0 12px 8px;
padding: 9px 11px; padding: 9px 11px;
@@ -742,10 +725,6 @@ layout("/layouts/platform_h5.html"){
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
.van-sticky--fixed .welfare-section-title {
margin-top: 8px;
}
.welfare-section-title .title-text { .welfare-section-title .title-text {
position: relative; position: relative;
display: flex; display: flex;
@@ -773,10 +752,482 @@ layout("/layouts/platform_h5.html"){
font-weight: 600; font-weight: 600;
} }
/* 项目封面保持单图全宽展示,下拉时通过动态高度逐步露出完整画面。 */
.welfare-header {
height: 232px;
margin: 0;
overflow: hidden;
border-radius: 0;
box-shadow: none;
transition: height .28s ease-out;
}
.welfare-header.is-pulling {
transition: none;
}
.welfare-header.is-expanded {
background-color: #f3f7fd;
}
.welfare-header-img .van-image__img {
object-position: center;
}
.welfare-cover-pull-hint {
position: absolute;
right: 0;
bottom: 28px;
left: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, .92);
font-size: 12px;
line-height: 18px;
text-shadow: 0 1px 5px rgba(20, 50, 88, .5);
pointer-events: none;
}
.welfare-cover-pull-hint::before {
position: absolute;
right: 0;
bottom: -28px;
left: 0;
z-index: -1;
height: 76px;
background: linear-gradient(180deg, rgba(18, 45, 82, 0), rgba(18, 45, 82, .42));
content: "";
}
.welfare-cover-pull-hint .van-icon {
margin-right: 6px;
font-size: 16px;
}
.main-content {
z-index: 3;
}
.welfare-project-card {
margin: -20px 12px 12px;
padding: 16px;
border-radius: 14px;
background-color: #fff;
box-shadow: 0 8px 24px rgba(50, 83, 126, .12);
box-sizing: border-box;
}
.welfare-project-card__head {
display: flex;
min-width: 0;
align-items: center;
}
.welfare-project-card__title-wrap {
display: flex;
min-width: 0;
flex: 1;
align-items: center;
flex-wrap: wrap;
}
.welfare-project-card__title {
min-width: 0;
overflow: hidden;
color: var(--text-primary);
font-size: 18px;
line-height: 25px;
text-overflow: ellipsis;
white-space: nowrap;
}
.welfare-project-card__festival {
height: 22px;
margin-left: 8px;
padding: 0 7px;
flex: none;
border: 1px solid #acd3ff;
border-radius: 5px;
color: #1989fa;
background-color: #f4f9ff;
font-size: 12px;
line-height: 20px;
box-sizing: border-box;
}
.welfare-project-card__status {
display: flex;
margin-top: 8px;
align-items: center;
color: #1989fa;
font-size: 12px;
line-height: 18px;
}
.welfare-project-card__status::before {
width: 6px;
height: 6px;
margin-right: 5px;
border-radius: 50%;
background-color: currentColor;
content: "";
}
.welfare-project-card__status.is-ended {
color: #a8b0bc;
}
.welfare-project-card__facts {
display: grid;
margin-top: 12px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.welfare-project-fact {
display: flex;
min-width: 0;
padding: 10px;
align-items: center;
border-radius: 8px;
background: #f7faff;
box-sizing: border-box;
}
.welfare-project-fact__icon {
display: flex;
width: 32px;
height: 32px;
margin-right: 9px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 8px;
color: #fff;
background: #4c9cff;
box-shadow: 0 4px 10px rgba(25, 137, 250, .18);
font-size: 18px;
}
.welfare-project-fact__content {
min-width: 0;
}
.welfare-project-fact__label {
color: #8491a5;
font-size: 11px;
line-height: 17px;
}
.welfare-project-fact__value {
margin-top: 2px;
overflow: hidden;
color: #1989fa;
font-size: 13px;
font-weight: 600;
line-height: 19px;
text-overflow: ellipsis;
white-space: nowrap;
}
.welfare-project-card__meta {
display: flex;
min-width: 0;
margin-top: 10px;
align-items: center;
}
.welfare-project-card__address {
display: flex;
min-width: 0;
flex: 1;
align-items: center;
overflow: hidden;
color: #4c596b;
font-size: 13px;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.welfare-project-card__address .van-icon {
margin-right: 5px;
flex: none;
color: #66758a;
font-size: 15px;
}
.welfare-project-card__tags {
display: flex;
margin-top: 8px;
flex-wrap: wrap;
column-gap: 5px;
row-gap: 5px;
}
.welfare-project-card__tag {
height: 22px;
padding: 0 7px;
border: 1px solid #b8d9ff;
border-radius: 4px;
color: #1989fa;
background-color: #f7fbff;
font-size: 11px;
line-height: 20px;
box-sizing: border-box;
}
.welfare-deadline {
margin-top: 0;
}
.welfare-section-title {
min-height: 42px;
margin: 0 12px 8px;
padding: 7px 0;
border-radius: 0;
background: rgba(243, 247, 253, .96);
box-shadow: none;
}
.welfare-section-title .title-text {
align-self: stretch;
}
.welfare-section-title .title-label {
color: var(--text-primary);
font-size: 16px;
line-height: 28px;
}
.welfare-section-title .title-count {
margin-left: 5px;
color: #7d899b;
font-size: 13px;
}
.welfare-option {
border-color: #e2eaf4;
box-shadow: none;
}
.welfare-option.selected {
border-color: #c5dfff;
background: linear-gradient(90deg, #f5faff 0%, #fff 100%);
box-shadow: none;
}
.welfare-option-main {
min-height: 128px;
}
.welfare-option-image {
width: 94px;
height: 94px;
margin: 16px 0 16px 14px;
}
.welfare-option-content {
padding: 14px 12px 10px;
}
.welfare-option-title {
margin-bottom: 4px;
}
.welfare-option-supplier {
overflow: hidden;
margin-bottom: 4px;
color: #7a8799;
font-size: 11px;
line-height: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.welfare-option-rank {
margin-bottom: 5px;
}
.welfare-option-desc {
-webkit-line-clamp: 1;
}
.welfare-option-meta {
display: flex;
min-width: 0;
margin-top: 7px;
align-items: center;
overflow: hidden;
}
.welfare-option-meta__tags {
display: flex;
min-width: 0;
flex: 1;
column-gap: 5px;
overflow: hidden;
}
.welfare-option-meta__tag {
height: 19px;
padding: 0 5px;
flex: none;
border: 1px solid #b8d9ff;
border-radius: 3px;
color: #1989fa;
background-color: #f7fbff;
font-size: 10px;
line-height: 17px;
box-sizing: border-box;
}
.welfare-option-meta__total {
margin-left: 6px;
flex: none;
color: #8a96a8;
font-size: 10px;
line-height: 18px;
}
.welfare-option-select-row {
min-height: 44px;
padding: 7px 12px;
justify-content: space-between;
background: transparent;
}
.welfare-footer {
display: block;
}
.welfare-footer.has-contact {
padding-top: 8px;
padding-bottom: calc(8px + env(safe-area-inset-bottom));
}
.welfare-footer__actions {
display: flex;
align-items: center;
column-gap: 12px;
}
.welfare-footer__selection {
min-width: 92px;
color: #66758a;
font-size: 13px;
white-space: nowrap;
}
.welfare-footer__selection strong {
margin-left: 3px;
color: #1989fa;
font-size: 20px;
font-weight: 600;
}
.welfare-footer__contact {
display: flex;
min-width: 0;
min-height: 50px;
margin-bottom: 8px;
padding: 5px 8px;
align-items: center;
color: #66758a;
background-color: #fff;
font-size: 12px;
line-height: 18px;
box-sizing: border-box;
}
.welfare-footer__contact-icon {
display: flex;
width: 36px;
height: 36px;
margin-right: 10px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 8px;
color: #1989fa;
background-color: #edf5ff;
font-size: 20px;
}
.welfare-footer__contact-info {
min-width: 0;
flex: 1;
}
.welfare-footer__contact-primary,
.welfare-footer__contact-address {
display: flex;
min-width: 0;
align-items: baseline;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.welfare-footer__contact-primary {
color: #1f2937;
font-size: 12px;
line-height: 20px;
}
.welfare-footer__contact-address {
margin-top: 2px;
font-size: 11px;
line-height: 16px;
}
.welfare-footer__contact-label {
margin-right: 3px;
flex: none;
color: #929cab;
}
.welfare-footer__contact-name,
.welfare-footer__contact-mobile,
.welfare-footer__contact-address-value {
min-width: 0;
overflow: hidden;
color: #1f2937;
font-weight: 500;
text-overflow: ellipsis;
}
.welfare-footer__contact-name {
flex: none;
}
.welfare-footer__contact-mobile {
margin-left: 12px;
flex: none;
}
.welfare-footer__contact-address-value {
flex: 1;
}
.welfare-submit-btn {
width: auto;
flex: 1;
}
.welfare-submit-btn.is-deadline-passed.van-button--disabled {
color: #a8b0bc;
background: #edf0f4;
border-color: #edf0f4;
opacity: 1;
}
</style> </style>
<div id="app" v-cloak> <div id="app" v-cloak>
<div class="page-container"> <div class="page-container" :class="{'has-footer-contact': fromMine && hasSubmittedBefore}">
<van-nav-bar title="福利选择" left-text="返回" left-arrow @click-left="handlePageBack" fixed <van-nav-bar title="福利选择" left-text="返回" left-arrow @click-left="handlePageBack" fixed
placeholder></van-nav-bar> placeholder></van-nav-bar>
@@ -794,8 +1245,17 @@ layout("/layouts/platform_h5.html"){
<van-empty v-else-if="!projectInfo.id" description="暂无福利信息"></van-empty> <van-empty v-else-if="!projectInfo.id" description="暂无福利信息"></van-empty>
<!-- 项目头部信息 --> <!-- 项目头部信息 -->
<div class="welfare-header" v-if="!pageLoading && projectInfo.id"> <div
<van-image v-if="projectInfo.cover" :src="projectInfo.cover" class="welfare-header-img" fit="cover"> class="welfare-header"
:class="{'is-pulling': coverPullEnabled, 'is-expanded': coverExpanded}"
:style="coverHeaderStyle"
v-if="!pageLoading && projectInfo.id"
@touchstart="handleCoverTouchStart"
@touchmove="handleCoverTouchMove"
@touchend="handleCoverTouchEnd"
@touchcancel="handleCoverTouchEnd"
>
<van-image v-if="projectInfo.cover" :src="projectInfo.cover" class="welfare-header-img" :fit="coverImageFit">
<template #error> <template #error>
<div class="welfare-image-state"> <div class="welfare-image-state">
<van-icon name="photo-o"></van-icon> <van-icon name="photo-o"></van-icon>
@@ -807,24 +1267,67 @@ layout("/layouts/platform_h5.html"){
<van-icon name="photo-o"></van-icon> <van-icon name="photo-o"></van-icon>
<span>暂无图片</span> <span>暂无图片</span>
</div> </div>
<div class="welfare-cover-pull-hint" v-if="projectInfo.cover">
<van-icon :name="coverExpanded ? 'arrow-up' : 'arrow-down'"></van-icon>
<span>{{ coverExpanded ? '上滑收起封面' : '下拉查看完整封面' }}</span>
</div>
</div> </div>
<div class="main-content" v-if="!pageLoading && projectInfo.id"> <div class="main-content" v-if="!pageLoading && projectInfo.id">
<section class="welfare-project-card"
@touchstart="handleCoverTouchStart"
@touchmove="handleCoverTouchMove"
@touchend="handleCoverTouchEnd"
@touchcancel="handleCoverTouchEnd">
<div class="welfare-project-card__head">
<div class="welfare-project-card__title-wrap">
<div class="welfare-project-card__title">{{ projectInfo.name }}</div>
<span class="welfare-project-card__festival" v-if="projectInfo.festival">{{ projectInfo.festival }}</span>
</div>
</div>
<div class="welfare-project-card__status" :class="{'is-ended': isDeadlinePassed}">{{ projectChoiceStatus }}</div>
<div class="welfare-project-card__facts">
<div class="welfare-project-fact">
<van-icon class="welfare-project-fact__icon" name="calendar-o"></van-icon>
<div class="welfare-project-fact__content">
<div class="welfare-project-fact__label">选择截止</div>
<div class="welfare-project-fact__value">{{ formatProjectDateTime(projectInfo.choiceTimeEnd) }}</div>
</div>
</div>
<div class="welfare-project-fact">
<van-icon class="welfare-project-fact__icon" name="clock-o"></van-icon>
<div class="welfare-project-fact__content">
<div class="welfare-project-fact__label">发放时间</div>
<div class="welfare-project-fact__value">{{ formatProjectDateRange(projectInfo.provideTimeStart, projectInfo.provideTimeEnd) }}</div>
</div>
</div>
</div>
<div class="welfare-project-card__meta">
<div class="welfare-project-card__address">
<van-icon name="location-o"></van-icon>
<span>发放地点:{{ projectInfo.provideAddress || '待定' }}</span>
</div>
</div>
<div class="welfare-project-card__tags">
<span class="welfare-project-card__tag">{{ projectInfo.isCheckBox === 'radio' ? '单选' : '多选' }}</span>
<span class="welfare-project-card__tag">最多{{ selectionLimit }}份</span>
<span class="welfare-project-card__tag" v-if="hasExpressOption">支持快递</span>
</div>
</section>
<!-- 截止时间提醒 --> <!-- 截止时间提醒 -->
<div class="welfare-deadline" v-if="isDeadlineSoon"> <div class="welfare-deadline" v-if="isDeadlineSoon">
<van-icon name="warning-o"/> <van-icon name="warning-o"/>
<span>选择截止时间即将到期,请尽快选择!</span> <span>选择截止时间即将到期,请尽快选择!</span>
</div> </div>
<!-- 标题到达顶部导航栏下方后固定,福利卡片继续在标题下方滚动。 -->
<van-sticky :offset-top="46" :z-index="30">
<div class="welfare-section-title"> <div class="welfare-section-title">
<div class="title-text"> <div class="title-text">
<span class="title-count">共 {{ projectInfo.options.length }} </span> <span class="title-label">福利选</span>
<span class="title-count">{{ projectInfo.options.length }}</span>
</div> </div>
<div class="title-selection">已选 <strong>{{ selectedCount }}</strong>/{{ selectionLimit }}</div> <div class="title-selection">已选 <strong>{{ selectedCount }}</strong>/{{ selectionLimit }}</div>
</div> </div>
</van-sticky>
<!-- 福利选项列表 --> <!-- 福利选项列表 -->
<div class="welfare-options-container"> <div class="welfare-options-container">
@@ -834,12 +1337,6 @@ layout("/layouts/platform_h5.html"){
class="welfare-option" class="welfare-option"
:class="{ selected: projectInfo.isCheckBox === 'radio' ? selectedRadioId === option.id : option.selectNum > 0 }" :class="{ selected: projectInfo.isCheckBox === 'radio' ? selectedRadioId === option.id : option.selectNum > 0 }"
> >
<van-icon
v-if="projectInfo.isCheckBox === 'radio' ? selectedRadioId === option.id : option.selectNum > 0"
class="welfare-option-selected-mark"
name="success"
></van-icon>
<div class="welfare-option-main"> <div class="welfare-option-main">
<div class="welfare-option-image" role="button" aria-label="查看福利图片" <div class="welfare-option-image" role="button" aria-label="查看福利图片"
@click.stop="previewOptionImage(option)"> @click.stop="previewOptionImage(option)">
@@ -862,6 +1359,10 @@ layout("/layouts/platform_h5.html"){
{{ option.optionName }} {{ option.optionName }}
</div> </div>
<div class="welfare-option-supplier" v-if="option.supplier || option.shoppingName">
供应商:{{ option.supplier || option.shoppingName }}
</div>
<!-- <div class="welfare-option-tag-row">--> <!-- <div class="welfare-option-tag-row">-->
<!-- <span class="welfare-option-type-tag">套餐</span>--> <!-- <span class="welfare-option-type-tag">套餐</span>-->
<!-- </div>--> <!-- </div>-->
@@ -880,10 +1381,21 @@ layout("/layouts/platform_h5.html"){
<van-icon name="arrow"/> <van-icon name="arrow"/>
</div> </div>
</div> </div>
<div class="welfare-option-meta">
<div class="welfare-option-meta__tags">
<span class="welfare-option-meta__tag" v-if="option.isExpressDelivery">支持快递</span>
<span class="welfare-option-meta__tag" v-if="option.isSystemDefault">系统默认</span>
</div>
<span class="welfare-option-meta__total" v-if="option.selectedTotal !== undefined && option.selectedTotal !== null">
累计选择 {{ option.selectedTotal }} 份
</span>
</div>
</div> </div>
</div> </div>
<div class="welfare-option-select-row" @click.stop> <div class="welfare-option-select-row" @click.stop>
<div class="welfare-option-select-label">选择数量</div>
<div class="welfare-option-checkbox"> <div class="welfare-option-checkbox">
<van-stepper <van-stepper
:key="'input-number-'+index+'-'+(option.selectNumKey || 0)" :key="'input-number-'+index+'-'+(option.selectNumKey || 0)"
@@ -906,12 +1418,29 @@ layout("/layouts/platform_h5.html"){
</div> </div>
<!-- 底部提交按钮 --> <!-- 底部提交按钮 -->
<div class="welfare-footer" v-if="!pageLoading && projectInfo.id"> <div class="welfare-footer" :class="{'has-contact': fromMine && hasSubmittedBefore}" v-if="!pageLoading && projectInfo.id">
<van-button type="primary" class="welfare-submit-btn" :disabled="submitButtonDisabled" <div class="welfare-footer__contact" v-if="fromMine && hasSubmittedBefore">
<div class="welfare-footer__contact-icon"><van-icon name="location"></van-icon></div>
<div class="welfare-footer__contact-info">
<div class="welfare-footer__contact-primary">
<span class="welfare-footer__contact-label">收货人:</span>
<span class="welfare-footer__contact-name">{{ formData.userName || '暂无' }}</span>
<span class="welfare-footer__contact-mobile">{{ formData.mobile || '暂无' }}</span>
</div>
<div class="welfare-footer__contact-address">
<span class="welfare-footer__contact-label">收货地址:</span>
<span class="welfare-footer__contact-address-value">{{ formData.receiveAddress || '暂无' }}</span>
</div>
</div>
</div>
<div class="welfare-footer__actions">
<div class="welfare-footer__selection">已选 <strong>{{ selectedCount }}</strong>/{{ selectionLimit }}</div>
<van-button type="primary" class="welfare-submit-btn" :class="{'is-deadline-passed': isDeadlinePassed}" :disabled="submitButtonDisabled"
@click="submitSelection" round> @click="submitSelection" round>
{{ isDeadlinePassed ? '已截止' : '确认选择' }} {{ isDeadlinePassed ? '已截止' : '确认选择' }}
</van-button> </van-button>
</div> </div>
</div>
<!-- 全屏确认页从右侧滑入;顶部导航栏固定,表单内容在中间区域独立滚动。 --> <!-- 全屏确认页从右侧滑入;顶部导航栏固定,表单内容在中间区域独立滚动。 -->
<van-popup v-model="showConfirmDialog" class="confirm-full-popup" position="right" <van-popup v-model="showConfirmDialog" class="confirm-full-popup" position="right"
@@ -1057,6 +1586,12 @@ layout("/layouts/platform_h5.html"){
projectInfo: {}, projectInfo: {},
userSelection: [], userSelection: [],
projectId: "", projectId: "",
fromMine: false,
coverPullStartY: 0,
coverPullStartDistance: 0,
coverPullDistance: 0,
coverPullEnabled: false,
coverExpanded: false,
selectedRadioId: "", // 单选模式下选中的选项ID selectedRadioId: "", // 单选模式下选中的选项ID
showConfirmDialog: false, showConfirmDialog: false,
isSubmitting: false, isSubmitting: false,
@@ -1069,6 +1604,7 @@ layout("/layouts/platform_h5.html"){
userSign: "", // 用户签名 userSign: "", // 用户签名
mobile: "", // 手机号码 mobile: "", // 手机号码
address: "",// 收货地址 address: "",// 收货地址
receiveAddress: "", // 已提交的收货地址
userName: "",//收货人 userName: "",//收货人
}, },
@@ -1080,6 +1616,34 @@ layout("/layouts/platform_h5.html"){
computed: { computed: {
coverHeaderStyle() {
const viewportWidth = Math.min(document.documentElement.clientWidth || window.innerWidth || 390, 430)
const baseHeight = Math.max(230, Math.min(Math.round(viewportWidth * 0.66), 280))
return {height: baseHeight + this.coverPullDistance + "px"}
},
coverMaxPullDistance() {
const viewportHeight = document.documentElement.clientHeight || window.innerHeight || 800
return Math.max(170, Math.min(Math.round(viewportHeight * 0.25), 220))
},
coverImageFit() {
return this.coverPullDistance > 48 ? "contain" : "cover"
},
projectChoiceStatus() {
const now = Date.now()
const startTime = this.projectInfo.choiceTimeStart ? new Date(this.projectInfo.choiceTimeStart).getTime() : null
const endTime = this.projectInfo.choiceTimeEnd ? new Date(this.projectInfo.choiceTimeEnd).getTime() : null
if (startTime && now < startTime) return "选择未开始"
if (endTime && now > endTime) return "选择已截止"
return "选择进行中"
},
hasExpressOption() {
return !!(this.projectInfo.options || []).find((option) => option.isExpressDelivery)
},
// 系统默认福利必须被保留,管理端限制每个项目最多配置一个。 // 系统默认福利必须被保留,管理端限制每个项目最多配置一个。
systemDefaultOption() { systemDefaultOption() {
if (!this.projectInfo.options) return null if (!this.projectInfo.options) return null
@@ -1152,6 +1716,64 @@ layout("/layouts/platform_h5.html"){
}, },
methods: { methods: {
/**
* H5 布局由 page-wrapper 承担实际滚动,页面逻辑统一读取该容器而不是 window。
*/
getPageScroller() {
return document.querySelector("body > .main-page .page-wrapper") || document.querySelector(".page-wrapper")
},
resetPageScroll() {
const pageScroller = this.getPageScroller()
if (pageScroller) pageScroller.scrollTop = 0
window.scrollTo(0, 0)
},
/**
* 仅在页面位于顶部时启用封面下拉,避免与正常列表滚动冲突。
*/
handleCoverTouchStart(event) {
const pageScroller = this.getPageScroller()
const scrollTop = pageScroller ? pageScroller.scrollTop : window.pageYOffset
if (scrollTop > 0 || !event.touches || !event.touches.length) return
this.coverPullStartY = event.touches[0].clientY
this.coverPullStartDistance = this.coverPullDistance
this.coverPullEnabled = true
},
/**
* 封面展开后保留展示高度,主题信息卡片随封面底边移动;反向上滑可逐步收起。
*/
handleCoverTouchMove(event) {
if (!this.coverPullEnabled || !event.touches || !event.touches.length) return
const distance = event.touches[0].clientY - this.coverPullStartY
if (this.coverPullStartDistance === 0 && distance <= 0) return
this.coverPullDistance = Math.max(0, Math.min(
this.coverPullStartDistance + Math.round(distance * 0.65),
this.coverMaxPullDistance
))
if (event.cancelable) event.preventDefault()
},
handleCoverTouchEnd() {
if (!this.coverPullEnabled) return
const shouldExpand = this.coverPullDistance >= this.coverMaxPullDistance * 0.5
this.coverPullEnabled = false
this.coverExpanded = shouldExpand
this.coverPullDistance = shouldExpand ? this.coverMaxPullDistance : 0
},
formatProjectDateTime(value) {
return value ? this.$moment(value).format("MM/DD HH:mm") : "待定"
},
formatProjectDateRange(startTime, endTime) {
if (!startTime && !endTime) return "待定"
if (!startTime) return this.$moment(endTime).format("MM/DD")
if (!endTime) return this.$moment(startTime).format("MM/DD")
return this.$moment(startTime).format("MM/DD") + "" + this.$moment(endTime).format("MM/DD")
},
/** /**
* 注册当前福利页面的弹层历史。 * 注册当前福利页面的弹层历史。
* 页面标识由固定前缀和福利项目 ID 组成;回调参数是当前弹层名称数组,无返回值。 * 页面标识由固定前缀和福利项目 ID 组成;回调参数是当前弹层名称数组,无返回值。
@@ -1724,6 +2346,7 @@ layout("/layouts/platform_h5.html"){
created() { created() {
this.projectId = GetQueryString("id") this.projectId = GetQueryString("id")
this.fromMine = GetQueryString("from") === "mine"
if (this.projectId) { if (this.projectId) {
this.registerHistoryLayers() this.registerHistoryLayers()
this.loadPageData() this.loadPageData()
@@ -1735,6 +2358,11 @@ layout("/layouts/platform_h5.html"){
}, 1500) }, 1500)
} }
}, },
mounted() {
this.$nextTick(() => {
this.resetPageScroll()
})
},
beforeDestroy() { beforeDestroy() {
if (window.h5HistoryLayerManager) { if (window.h5HistoryLayerManager) {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey) window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
@@ -265,35 +265,6 @@ layout("/layouts/platform_h5.html"){
white-space: nowrap; white-space: nowrap;
} }
.welfare-list-card__info {
display: flex;
min-width: 0;
margin-top: 8px;
align-items: center;
color: #718198;
font-size: 12px;
line-height: 18px;
}
.welfare-list-card__info .van-icon {
margin-right: 5px;
flex: none;
color: #8da2bd;
font-size: 14px;
}
.welfare-list-card__info-label {
flex: none;
}
.welfare-list-card__info-value {
min-width: 0;
overflow: hidden;
color: #42546c;
text-overflow: ellipsis;
white-space: nowrap;
}
.welfare-list-card__status { .welfare-list-card__status {
display: inline-flex; display: inline-flex;
min-width: 54px; min-width: 54px;
@@ -517,11 +488,6 @@ layout("/layouts/platform_h5.html"){
{{ getChoiceModeText(row) }} {{ getChoiceModeText(row) }}
</span> </span>
</div> </div>
<div class="welfare-list-card__info" v-if="row.gift">
<van-icon name="gift-o"></van-icon>
<span class="welfare-list-card__info-label">福利:</span>
<span class="welfare-list-card__info-value">{{ row.gift }}</span>
</div>
<div class="welfare-list-card__time" v-if="row.choiceTimeStart && row.choiceTimeEnd"> <div class="welfare-list-card__time" v-if="row.choiceTimeStart && row.choiceTimeEnd">
<van-icon name="clock-o"></van-icon> <van-icon name="clock-o"></van-icon>
<span>选择时间:{{ $moment(row.choiceTimeStart).format('MM/DD HH:mm') }} - {{ $moment(row.choiceTimeEnd).format('MM/DD HH:mm') }}</span> <span>选择时间:{{ $moment(row.choiceTimeStart).format('MM/DD HH:mm') }} - {{ $moment(row.choiceTimeEnd).format('MM/DD HH:mm') }}</span>
@@ -116,7 +116,6 @@ layout("/layouts/platform_h5.html"){
</van-cell> </van-cell>
<van-cell :border="false" :value="projectInfo.name" title="福利名称"></van-cell> <van-cell :border="false" :value="projectInfo.name" title="福利名称"></van-cell>
<van-cell :border="false" :value="projectInfo.festival" title="发放节日"></van-cell> <van-cell :border="false" :value="projectInfo.festival" title="发放节日"></van-cell>
<van-cell :border="false" :value="projectInfo.gift" title="福利礼品" v-if="!projectInfo.flexible"></van-cell>
<van-cell :border="false" title="选择形式">{{projectInfo.isCheckBox==='checkBox'?'多选':'单选'}}</van-cell> <van-cell :border="false" title="选择形式">{{projectInfo.isCheckBox==='checkBox'?'多选':'单选'}}</van-cell>
<van-cell <van-cell
:border="false" :border="false"
@@ -0,0 +1 @@
71eb5eaecf2b7fd91d9dd7409a56314b9f2f132b3dffbb9e13ee56d0999b8c59
@@ -0,0 +1 @@
0d9b4629144a1fd15ed01362ce0dc2c9c5246e4f57250dd95fb52dbbbb75c532
@@ -0,0 +1,150 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>9.4.57.v20241219</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-client</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<name>Jetty :: Asynchronous HTTP Client</name>
<properties>
<bundle-symbolic-name>${project.groupId}.client</bundle-symbolic-name>
<jetty.test.policy.loc>target/test-policy</jetty.test.policy.loc>
<spotbugs.onlyAnalyze>org.eclipse.client.*</spotbugs.onlyAnalyze>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack</id>
<phase>generate-test-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-policy</artifactId>
<version>${jetty-test-policy.version}</version>
<type>jar</type>
<overWrite>true</overWrite>
<includes>**/*.keystore,**/*.pem</includes>
<outputDirectory>${jetty.test.policy.loc}</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>hybrid</shadedClassifierName>
<artifactSet>
<includes>
<include>org.eclipse.jetty:jetty-http</include>
<include>org.eclipse.jetty:jetty-io</include>
<include>org.eclipse.jetty:jetty-util</include>
</includes>
</artifactSet>
<relocations>
<relocation>
<pattern>org.eclipse.jetty.http</pattern>
<shadedPattern>org.eclipse.jetty.client.shaded.http</shadedPattern>
</relocation>
<relocation>
<pattern>org.eclipse.jetty.io</pattern>
<shadedPattern>org.eclipse.jetty.client.shaded.io</shadedPattern>
</relocation>
<relocation>
<pattern>org.eclipse.jetty.util</pattern>
<shadedPattern>org.eclipse.jetty.client.shaded.util</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>9.4.57.v20241219</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>
<version>9.4.57.v20241219</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-simplekdc</artifactId>
<version>2.1.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.jline</groupId>
<artifactId>jline</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- transitive dependency defined as a range and we don't want that -->
<dependency>
<groupId>net.minidev</groupId>
<artifactId>json-smart</artifactId>
<version>2.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1 @@
989a013455688dc528a988c41514fbfece0e3354a54398af5a404cda582c94bd
@@ -0,0 +1 @@
dcfe694ef6f27d9d6268e2fb135a1c9a7aabdfa55e7d738fc8df79f573086027
@@ -0,0 +1 @@
90058e595b73d85b96e192b7e6806098f83467680a95466974f445d6706ed682
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>jetty-project</artifactId>
<groupId>org.eclipse.jetty</groupId>
<version>9.4.57.v20241219</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-http</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<name>Jetty :: Http Utility</name>
<properties>
<bundle-symbolic-name>${project.groupId}.http</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.jetty.http.*</spotbugs.onlyAnalyze>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.4.57.v20241219</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>9.4.57.v20241219</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional</Require-Capability>
<Provide-Capability>osgi.serviceloader; osgi.serviceloader=org.eclipse.jetty.http.HttpFieldPreEncoder</Provide-Capability>
</instructions>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>test-jar</id>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>${jmhjar.name}</finalName>
<shadeTestJar>true</shadeTestJar>
<artifactSet>
<includes>
<include>org.openjdk.jmh:jmh-core</include>
</includes>
</artifactSet>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>org.openjdk.jmh:jmh-core</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1 @@
967fff3457fb62eb7485fe303cc4162edc1c02b15498c2f8401fbca263649ab8
@@ -0,0 +1 @@
770e71875fa37effaab5c11e932b9a52c2e019236062575b0403760242b0bf08
@@ -0,0 +1 @@
d757e04e165d940c019e9346c36e9bf8d1a71c8b6b196519bda4200e2a16f5d4
@@ -0,0 +1,89 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>9.4.57.v20241219</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-jaspi</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
<name>Jetty :: JASPI Security</name>
<description>Jetty security infrastructure</description>
<properties>
<bundle-symbolic-name>${project.groupId}.security.jaspi</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.jetty.jaspi.*</spotbugs.onlyAnalyze>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>manifest</goal>
</goals>
<configuration>
<instructions>
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
<Provide-Capability>osgi.serviceloader;osgi.serviceloader=org.eclipse.jetty.security.Authenticator$Factory</Provide-Capability>
</instructions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>
<version>9.4.57.v20241219-cug-patch1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.security.auth.message</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<!-- Must stay on 3.x as 4.x is for Java 11 -->
<version>3.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.geronimo.components</groupId>
<artifactId>geronimo-jaspi</artifactId>
<version>2.0.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jaspic_1.0_spec</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1 @@
8e49cd7df44f050c96d896726129fa5c9d0ac5895261cda774c8c899e617e5a6

Some files were not shown because too many files have changed in this diff Show More