Compare commits
4
Commits
97977b0d05
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54c90f9d73 | ||
|
|
b86bbcc7ea | ||
|
|
9d51c70adb | ||
|
|
be401ab0f0 |
@@ -2,16 +2,24 @@ package io.v.nutz.web.commons.controller;
|
|||||||
|
|
||||||
import cn.hutool.core.codec.Base64;
|
import cn.hutool.core.codec.Base64;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.io.IoUtil;
|
|
||||||
import cn.wizzer.framework.base.Result;
|
import cn.wizzer.framework.base.Result;
|
||||||
import com.aliyun.core.utils.Base64Util;
|
|
||||||
import io.v.nutz.sys.models.Sys_file;
|
import io.v.nutz.sys.models.Sys_file;
|
||||||
|
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||||
|
import io.v.nutz.zhgh.zgfw.model.UserSign;
|
||||||
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.boot.starter.ftp.FtpService;
|
import org.nutz.boot.starter.ftp.FtpService;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
|
import org.nutz.dao.sql.Sql;
|
||||||
import org.nutz.integration.jedis.RedisService;
|
import org.nutz.integration.jedis.RedisService;
|
||||||
|
import org.nutz.ioc.Ioc;
|
||||||
|
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.Strings;
|
||||||
import org.nutz.lang.random.R;
|
import org.nutz.lang.random.R;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
@@ -19,6 +27,7 @@ import org.nutz.mvc.annotation.Ok;
|
|||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* TODO
|
||||||
@@ -30,15 +39,23 @@ import java.io.InputStream;
|
|||||||
@At("/signature")
|
@At("/signature")
|
||||||
public class SignatureController {
|
public class SignatureController {
|
||||||
|
|
||||||
@Inject
|
|
||||||
private Dao dao;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private RedisService redisService;
|
private RedisService redisService;
|
||||||
|
|
||||||
|
// 服务由本类工厂注册,按需获取避免控制器与工厂产物形成循环注入。
|
||||||
|
@Inject("refer:$ioc")
|
||||||
|
private Ioc ioc;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private FtpService ftpService;
|
private FtpService ftpService;
|
||||||
|
|
||||||
|
/** 当前 Nutz 不扫描内部类,通过同文件的工厂方法注册个人签名服务及其依赖。 */
|
||||||
|
@IocBean(name = "personalSignatureService")
|
||||||
|
public PersonalSignatureServiceImpl createPersonalSignatureService(@Inject("dao") Dao dao,
|
||||||
|
@Inject("ftpService") FtpService ftp, @Inject("redisService") RedisService redis) {
|
||||||
|
return new PersonalSignatureServiceImpl(dao, ftp, redis);
|
||||||
|
}
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("re")
|
@Ok("re")
|
||||||
@RequiresAuthentication
|
@RequiresAuthentication
|
||||||
@@ -47,48 +64,21 @@ public class SignatureController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移动端提交签名数据存到redis里
|
* prefix/id 为二维码对应的轮询标识;base64 兼容手写图片数据及当前用户已保存的文件路径。
|
||||||
*
|
* is_value_base64 默认 true,false 时返回文件路径给 PC;返回 {code,msg,data},data 为已保存签名。
|
||||||
* @param prefix 前缀
|
|
||||||
* @param id 唯一id
|
|
||||||
* @param base64
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@At
|
@At
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
@RequiresAuthentication
|
@RequiresAuthentication
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Object doSub(String prefix, String id, String base64, Boolean is_value_base64) {
|
public Object doSub(String prefix, String id, String base64, Boolean is_value_base64) {
|
||||||
if (is_value_base64) {
|
if (Strings.isBlank(prefix) || Strings.isBlank(id) || Strings.isBlank(base64)) {
|
||||||
redisService.setex(prefix + ":" + id, 60 * 10, base64);
|
return Result.error("缺少签字参数,请重新扫码");
|
||||||
} else {
|
|
||||||
String today = DateUtil.today();
|
|
||||||
String[] split = today.split("-");
|
|
||||||
String todayPath = "/signature/" + split[0] + "/" + split[1] + "/" + split[2];
|
|
||||||
String fileName = R.UU32() + ".png";
|
|
||||||
String fullPath = todayPath + "/" + fileName;
|
|
||||||
|
|
||||||
String replaceData = base64.replaceAll("data:image/png;base64,", "");
|
|
||||||
|
|
||||||
try (InputStream is = new ByteArrayInputStream(Base64.decode(replaceData))) {
|
|
||||||
boolean uploadRes = ftpService.upload(todayPath, fileName, is);
|
|
||||||
if (uploadRes) {
|
|
||||||
//上传成功记录到文件表
|
|
||||||
Sys_file sysFile = new Sys_file();
|
|
||||||
sysFile.setFilename(fileName);
|
|
||||||
sysFile.setFilepath(fullPath);
|
|
||||||
sysFile.setSource(null);
|
|
||||||
dao.insert(sysFile);
|
|
||||||
redisService.setex(prefix + ":" + id, 60 * 10, fullPath);
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("上传文件失败");
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
String sign = ioc.get(PersonalSignatureService.class, "personalSignatureService")
|
||||||
return Result.error("签字数据上传失败");
|
.submitScan(ShiroUtil.getPrincipalProperty("id").toString(),
|
||||||
}
|
prefix, id, base64, !Boolean.FALSE.equals(is_value_base64));
|
||||||
}
|
return Result.success().addData(sign);
|
||||||
|
|
||||||
|
|
||||||
return Result.success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -108,40 +98,15 @@ public class SignatureController {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签字base64转图片链接
|
* data 为 data:image/...;base64,... 图片字符串;返回 {code,msg,data},data 是上传后的文件路径。
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@At
|
@At
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
@RequiresAuthentication
|
@RequiresAuthentication
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result uploadSignData(String data) {
|
public Result uploadSignData(String data) {
|
||||||
String today = DateUtil.today();
|
if (Strings.isBlank(data)) return Result.error("请先签字");
|
||||||
String[] split = today.split("-");
|
return Result.success().addData(ioc.get(PersonalSignatureService.class, "personalSignatureService").uploadImage(data));
|
||||||
String todayPath = "/signature/" + split[0] + "/" + split[1] + "/" + split[2];
|
|
||||||
String fileName = R.UU32() + ".png";
|
|
||||||
String fullPath = todayPath + "/" + fileName;
|
|
||||||
|
|
||||||
String replaceData = data.replaceAll("data:image/png;base64,", "");
|
|
||||||
|
|
||||||
try (InputStream is = new ByteArrayInputStream(Base64.decode(replaceData))) {
|
|
||||||
boolean uploadRes = ftpService.upload(todayPath, fileName, is);
|
|
||||||
if (uploadRes) {
|
|
||||||
//上传成功记录到文件表
|
|
||||||
Sys_file sysFile = new Sys_file();
|
|
||||||
sysFile.setFilename(fileName);
|
|
||||||
sysFile.setFilepath(fullPath);
|
|
||||||
sysFile.setSource(null);
|
|
||||||
dao.insert(sysFile);
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("上传文件失败");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
return Result.error("签字数据上传失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.success().addData(fullPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -158,4 +123,128 @@ public class SignatureController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 个人签名服务与控制器同文件定义,查询、上传和按类型保存由独立 IoC 服务承接。 */
|
||||||
|
public interface PersonalSignatureService {
|
||||||
|
/** userId 为登录用户ID;base64 为签名类型;返回同类型签名原值,未找到返回 null。 */
|
||||||
|
String findPersonal(String userId, boolean base64);
|
||||||
|
|
||||||
|
/** sign 为手写 Base64 或本人已有文件路径;按 base64 类型保存,返回实际保存值。 */
|
||||||
|
String savePersonal(String userId, String sign, boolean base64);
|
||||||
|
|
||||||
|
/** data 为带图片类型头的 Base64;上传并记录文件信息,返回文件路径。 */
|
||||||
|
String uploadImage(String data);
|
||||||
|
|
||||||
|
/** prefix/id 对应 PC 轮询;保存本人签名后发布结果,返回与 base64 参数一致的数据格式。 */
|
||||||
|
String submitScan(String userId, String prefix, String id, String sign, boolean base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PersonalSignatureServiceImpl implements PersonalSignatureService {
|
||||||
|
private Dao dao;
|
||||||
|
private FtpService ftpService;
|
||||||
|
private RedisService redisService;
|
||||||
|
|
||||||
|
public PersonalSignatureServiceImpl(Dao dao, FtpService ftpService, RedisService redisService) {
|
||||||
|
this.dao = dao;
|
||||||
|
this.ftpService = ftpService;
|
||||||
|
this.redisService = redisService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 只把有明确图片数据头的内容归为 Base64,空值和异常内容不冒充文件签名。 */
|
||||||
|
private boolean isBase64(String sign) {
|
||||||
|
return sign != null && sign.matches("^data:image/(png|jpeg|jpg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 文件模式使用站内存储路径;排除 URL、路径穿越和图片数据字符串。 */
|
||||||
|
private boolean isFile(String sign) {
|
||||||
|
return sign != null && sign.startsWith("/") && !sign.startsWith("//")
|
||||||
|
&& !sign.contains("..") && !sign.contains("?") && !sign.contains("#")
|
||||||
|
&& !sign.contains("\\") && !sign.contains("\n") && !sign.contains("\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同类型历史多条按 ID 固定选择,优先公共记录;更新时保留该记录原有 prefix,不批量覆盖。 */
|
||||||
|
private UserSign findRecord(String userId, boolean base64) {
|
||||||
|
if (Strings.isBlank(userId)) throw new IllegalArgumentException("请先登录");
|
||||||
|
List<UserSign> records = dao.query(UserSign.class, Cnd.where("user_id", "=", userId).asc("id"));
|
||||||
|
UserSign first = null;
|
||||||
|
for (UserSign record : records) {
|
||||||
|
if (!(base64 ? isBase64(record.getData()) : isFile(record.getData()))) continue;
|
||||||
|
if (Strings.isBlank(record.getPrefix())) return record;
|
||||||
|
if (first == null) first = record;
|
||||||
|
}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String findPersonal(String userId, boolean base64) {
|
||||||
|
UserSign record = findRecord(userId, base64);
|
||||||
|
return record == null ? null : record.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public String savePersonal(String userId, String sign, boolean base64) {
|
||||||
|
if (Strings.isBlank(userId) || Strings.isBlank(sign)) throw new IllegalArgumentException("请先登录并签字");
|
||||||
|
// 锁定已有用户行,使同一用户的首次并发保存串行执行,无需新增类型字段或唯一索引。
|
||||||
|
Sql lock = Sqls.create("SELECT id FROM sys_user WHERE id=@id FOR UPDATE").setParam("id", userId);
|
||||||
|
lock.setCallback(Sqls.callback.map());
|
||||||
|
dao.execute(lock);
|
||||||
|
if (lock.getResult() == null) throw new IllegalArgumentException("当前用户不存在");
|
||||||
|
|
||||||
|
String saved;
|
||||||
|
if (base64) {
|
||||||
|
if (!isBase64(sign)) throw new IllegalArgumentException("当前页面需要 Base64 签名,请重新签字");
|
||||||
|
saved = sign;
|
||||||
|
} else if (isBase64(sign)) {
|
||||||
|
// 这里只上传本次新手写的图片;查询个人签名时不转换另一种类型的历史记录。
|
||||||
|
saved = uploadImage(sign);
|
||||||
|
} else {
|
||||||
|
if (!isFile(sign) || dao.fetch(UserSign.class, Cnd.where("user_id", "=", userId)
|
||||||
|
.and("data", "=", sign)) == null) {
|
||||||
|
throw new IllegalArgumentException("请使用本人的文件签名或重新签字");
|
||||||
|
}
|
||||||
|
saved = sign;
|
||||||
|
}
|
||||||
|
UserSign record = findRecord(userId, base64);
|
||||||
|
if (record == null) {
|
||||||
|
dao.insert(new UserSign(userId, saved));
|
||||||
|
} else if (!saved.equals(record.getData())) {
|
||||||
|
dao.update(UserSign.class, Chain.make("data", saved), Cnd.where("id", "=", record.getId())
|
||||||
|
.and("user_id", "=", userId));
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public String uploadImage(String data) {
|
||||||
|
if (!isBase64(data)) throw new IllegalArgumentException("签字图片格式不正确");
|
||||||
|
String type = data.substring("data:image/".length(), data.indexOf(';'));
|
||||||
|
String directory = "/signature/" + DateUtil.format(new java.util.Date(), "yyyy/MM/dd");
|
||||||
|
String filename = R.UU32() + "." + ("jpeg".equals(type) ? "jpg" : type);
|
||||||
|
String path = directory + "/" + filename;
|
||||||
|
byte[] bytes = Base64.decode(data.substring(data.indexOf(',') + 1));
|
||||||
|
try (InputStream stream = new ByteArrayInputStream(bytes)) {
|
||||||
|
if (!ftpService.upload(directory, filename, stream)) throw new IllegalStateException("签字图片上传失败");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("签字图片上传失败,请重试", e);
|
||||||
|
}
|
||||||
|
Sys_file file = new Sys_file();
|
||||||
|
file.setFilename(filename);
|
||||||
|
file.setFilepath(path);
|
||||||
|
file.setSource(null);
|
||||||
|
dao.insert(file);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public String submitScan(String userId, String prefix, String id, String sign, boolean base64) {
|
||||||
|
if (Strings.isBlank(prefix) || Strings.isBlank(id)) throw new IllegalArgumentException("请重新扫码");
|
||||||
|
// 新版组件已保存个人签名,文件路径直接复用;旧扫码客户端仍可提交原始手写图片。
|
||||||
|
String saved = savePersonal(userId, sign, base64);
|
||||||
|
redisService.setex(prefix + ":" + id, 60 * 10, saved);
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
package io.v.nutz.zhgh.mobile.common;
|
package io.v.nutz.zhgh.mobile.common;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import io.v.nutz.base.annontation.ViReturn;
|
|
||||||
import io.v.nutz.base.result.Result;
|
import io.v.nutz.base.result.Result;
|
||||||
import io.v.nutz.base.service.ViService;
|
import io.v.nutz.web.commons.controller.SignatureController.PersonalSignatureService;
|
||||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||||
import io.v.nutz.zhgh.zgfw.model.UserSign;
|
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
||||||
import org.nutz.dao.Chain;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
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.Strings;
|
import org.nutz.lang.Strings;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author: Aaron
|
* @author: Aaron
|
||||||
* @create: 2020-11-25 11:29
|
* @create: 2020-11-25 11:29
|
||||||
@@ -26,26 +22,41 @@ import java.util.List;
|
|||||||
@Ok("json:full")
|
@Ok("json:full")
|
||||||
public class updateSign {
|
public class updateSign {
|
||||||
|
|
||||||
@Inject("UserSign")
|
@Inject("personalSignatureService")
|
||||||
private ViService<UserSign> userSignService;
|
private PersonalSignatureService personalSignatureService;
|
||||||
|
|
||||||
|
|
||||||
@At
|
/**
|
||||||
|
* 兼容旧 Java 调用:sign 为带图片数据头的 Base64,user_id 为空或当前登录人ID。
|
||||||
|
* 默认按 Base64 类型保存;返回 Result,code=0 表示成功,data 为保存后的签名原值。
|
||||||
|
* 此重载仅用于内部调用,不标注 @At,HTTP 请求仍使用三参数入口。
|
||||||
|
*/
|
||||||
|
@RequiresAuthentication
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result update(String sign, String user_id) {
|
public Result update(String sign, String user_id) {
|
||||||
if(StrUtil.isBlank(user_id)){
|
return update(sign, user_id, Boolean.TRUE);
|
||||||
user_id = (String) ShiroUtil.getPrincipalProperty("id");
|
|
||||||
}
|
|
||||||
if (Strings.isNotBlank(getMySign())) {
|
|
||||||
userSignService.update(Chain.make("data", sign), Cnd.where("user_id", "=", user_id));
|
|
||||||
} else {
|
|
||||||
userSignService.insert(new UserSign(user_id, sign));
|
|
||||||
}
|
|
||||||
return Result.success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sign 为新手写 Base64 或本人已有文件路径;is_value_base64 默认 true,false 保存文件类型。
|
||||||
|
* user_id 兼容旧参数但只允许当前登录人;返回 {code,msg,data},data 为保存后的签名原值。
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
public String getMySign() {
|
@RequiresAuthentication
|
||||||
List<UserSign> userSigns = userSignService.query(Cnd.NEW().and("user_id", "=", ShiroUtil.getPrincipalProperty("id")));
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
return userSigns.isEmpty() ? null : userSigns.get(0).getData();
|
public Result update(String sign, String user_id, Boolean is_value_base64) {
|
||||||
|
String currentUserId = ShiroUtil.getPrincipalProperty("id").toString();
|
||||||
|
if (Strings.isNotBlank(user_id) && !currentUserId.equals(user_id)) return Result.error("只能保存本人的签名");
|
||||||
|
if (Strings.isBlank(sign)) return Result.error("请先签字");
|
||||||
|
String saved = personalSignatureService.savePersonal(currentUserId, sign, !Boolean.FALSE.equals(is_value_base64));
|
||||||
|
return Result.success().addData(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** is_value_base64 默认 true;按当前登录用户查询对应类型,返回签名字符串,未找到返回 null。 */
|
||||||
|
@At
|
||||||
|
@RequiresAuthentication
|
||||||
|
public String getMySign(Boolean is_value_base64) {
|
||||||
|
return personalSignatureService.findPersonal(ShiroUtil.getPrincipalProperty("id").toString(),
|
||||||
|
!Boolean.FALSE.equals(is_value_base64));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
<div class="main-panel">
|
<div class="main-panel">
|
||||||
<slot v-if="$slots.default"></slot>
|
<slot v-if="$slots.default"></slot>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<van-button @click="openSignature" class="open-button" size="middle" plain native-type="button" :disabled="disabled">打开签字板</van-button>
|
<van-button @click="openSignature" class="open-button" size="middle" plain native-type="button" :disabled="disabled || saving">打开签字板</van-button>
|
||||||
<van-image :src="signatureUrl" fit="contain">
|
<!-- 默认回显仅供预览,主动使用后才回传,避免扫码页收到 input 就自动提交。 -->
|
||||||
|
<van-button v-if="canUsePersonalSignature" @click="usePersonalSignature" class="open-button" size="middle"
|
||||||
|
native-type="button" :loading="saving" :disabled="disabled || saving">使用此签名</van-button>
|
||||||
|
<van-image :src="signatureImageUrl" fit="contain">
|
||||||
<template v-slot:error>请点击下方打开签字板按钮进行签字</template>
|
<template v-slot:error>请点击下方打开签字板按钮进行签字</template>
|
||||||
</van-image>
|
</van-image>
|
||||||
</template>
|
</template>
|
||||||
@@ -29,8 +32,6 @@ module.exports = {
|
|||||||
signature: httpVueLoader("/components/sign/index.vue?v=" + new Date().getTime())
|
signature: httpVueLoader("/components/sign/index.vue?v=" + new Date().getTime())
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
// 协会路径模式优先于 saveToProfile,仅上传本次签字,不覆盖个人 Base64 签名。
|
|
||||||
pathMode: {type: Boolean, default: false},
|
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
@@ -39,16 +40,14 @@ module.exports = {
|
|||||||
my_sign: {type: String, default: ""},
|
my_sign: {type: String, default: ""},
|
||||||
// 不传 popupVisible 时保持组件自行开关;传入布尔值时由页面统一历史栈管理。
|
// 不传 popupVisible 时保持组件自行开关;传入布尔值时由页面统一历史栈管理。
|
||||||
popupVisible: {type: Boolean, default: null},
|
popupVisible: {type: Boolean, default: null},
|
||||||
// 默认仍保存个人签名;业务只需本次签字时传 false,通过 input 回传 Base64。
|
// 与 PC 一致:true 使用 Base64,false 使用个人签名表中的文件路径。
|
||||||
|
is_value_base64: {type: Boolean, default: true},
|
||||||
|
// 仅接收旧页面属性以兼容模板;不再参与判断,所有主动提交均保存个人签名。
|
||||||
saveToProfile: {type: Boolean, default: true},
|
saveToProfile: {type: Boolean, default: true},
|
||||||
disabled: {type: Boolean, default: false},
|
disabled: {type: Boolean, default: false},
|
||||||
panelTitle: {type: String, default: "签字"}
|
panelTitle: {type: String, default: "签字"}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
signatureUrl() {
|
|
||||||
if (!this.signatureContent) return ""
|
|
||||||
return this.pathMode ? "/signature/getSignData?path=" + encodeURIComponent(this.signatureContent) : this.signatureContent
|
|
||||||
},
|
|
||||||
controlled() {
|
controlled() {
|
||||||
return this.popupVisible !== null
|
return this.popupVisible !== null
|
||||||
},
|
},
|
||||||
@@ -57,15 +56,34 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
boundSignature() {
|
boundSignature() {
|
||||||
return Object.prototype.hasOwnProperty.call(this.$options.propsData || {}, "value") ? this.value : this.my_sign
|
return Object.prototype.hasOwnProperty.call(this.$options.propsData || {}, "value") ? this.value : this.my_sign
|
||||||
|
},
|
||||||
|
canUsePersonalSignature() {
|
||||||
|
return !this.matchesSignatureType(this.boundSignature) && this.matchesSignatureType(this.personalSignature)
|
||||||
|
&& this.signatureContent === this.personalSignature
|
||||||
|
},
|
||||||
|
signatureImageUrl() {
|
||||||
|
// 显示地址与提交值分离:文件接口 URL 只用于 img,回填仍使用原始文件路径。
|
||||||
|
if (!this.matchesSignatureType(this.signatureContent)) return ''
|
||||||
|
return this.is_value_base64 ? this.signatureContent
|
||||||
|
: '/signature/getSignData?path=' + encodeURIComponent(this.signatureContent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
boundSignature: {
|
boundSignature: {
|
||||||
handler(val) {
|
handler(val) {
|
||||||
this.signatureContent = val
|
// 业务回显优先;表单清空时恢复个人签名预览,仍须点击使用才回填。
|
||||||
|
this.signatureVersion++
|
||||||
|
this.signatureContent = this.matchesSignatureType(val) ? val : this.personalSignature
|
||||||
},
|
},
|
||||||
immediate: true
|
immediate: true
|
||||||
},
|
},
|
||||||
|
is_value_base64() {
|
||||||
|
// 类型切换后清除另一种个人签名,只重新读取新类型,不互转历史签名。
|
||||||
|
this.signatureVersion++
|
||||||
|
this.personalSignature = ''
|
||||||
|
this.signatureContent = this.matchesSignatureType(this.boundSignature) ? this.boundSignature : ''
|
||||||
|
this.getMySign(true)
|
||||||
|
},
|
||||||
signaturePanelVisible(visible) {
|
signaturePanelVisible(visible) {
|
||||||
// 每次打开重新创建画布;等待弹出动画完成后再按真实可用区域计算尺寸。
|
// 每次打开重新创建画布;等待弹出动画完成后再按真实可用区域计算尺寸。
|
||||||
if (!visible) this.boardReady = false
|
if (!visible) this.boardReady = false
|
||||||
@@ -74,12 +92,27 @@ module.exports = {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
signatureContent: null,
|
signatureContent: null,
|
||||||
|
personalSignature: '',
|
||||||
|
signatureVersion: 0,
|
||||||
|
signatureRequest: 0,
|
||||||
|
signatureLoading: false,
|
||||||
showSignaturePanel: false,
|
showSignaturePanel: false,
|
||||||
boardReady: false,
|
boardReady: false,
|
||||||
saving: false
|
saving: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** value 为签名原值;只接受当前参数对应的数据,空值或另一种格式不能冒充本类型签名。 */
|
||||||
|
matchesSignatureType(value) {
|
||||||
|
if (typeof value !== 'string' || !value) return false
|
||||||
|
return this.is_value_base64 ? /^data:image\/(png|jpeg|jpg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/.test(value)
|
||||||
|
: value.startsWith('/') && !value.startsWith('//') && !/[?#\\\r\n]/.test(value) && !value.includes('..')
|
||||||
|
},
|
||||||
|
/** 复用签名同样经过个人签名保存;没有打开签字层时不能误关父页面的详情弹框。 */
|
||||||
|
usePersonalSignature() {
|
||||||
|
if (this.disabled || this.saving || !this.canUsePersonalSignature) return
|
||||||
|
return this.save(this.personalSignature, false)
|
||||||
|
},
|
||||||
openSignature() {
|
openSignature() {
|
||||||
if (!this.disabled && !this.saving) this.changePanelVisible(true)
|
if (!this.disabled && !this.saving) this.changePanelVisible(true)
|
||||||
},
|
},
|
||||||
@@ -99,61 +132,73 @@ module.exports = {
|
|||||||
onPanelOpened() {
|
onPanelOpened() {
|
||||||
this.boardReady = this.signaturePanelVisible
|
this.boardReady = this.signaturePanelVisible
|
||||||
},
|
},
|
||||||
/** data 为签名图片字符串,同时通知两种已有绑定方式,父表单提交字段保持不变。 */
|
/** data 为当前模式的 Base64 或文件路径,同时通知两种已有绑定方式。 */
|
||||||
updateSignature(data) {
|
updateSignature(data) {
|
||||||
|
this.signatureVersion++
|
||||||
this.signatureContent = data
|
this.signatureContent = data
|
||||||
this.$emit("input", data)
|
this.$emit("input", data)
|
||||||
this.$emit("update:my_sign", data)
|
this.$emit("update:my_sign", data)
|
||||||
},
|
},
|
||||||
/** signature 为公共签字板生成的 Base64;本次签字模式不调用个人签名保存接口。 */
|
/**
|
||||||
save(signature) {
|
* signature 为新手写 Base64 或已读取的同类型个人签名;closePanel=false 表示在预览区复用。
|
||||||
|
* 提交 sign/is_value_base64,接口返回 {code,msg,data};data 为保存后的对应类型原值。
|
||||||
|
* 个人签名保存成功后才回填业务表单,文件模式的新手写图片由服务端上传。
|
||||||
|
*/
|
||||||
|
save(signature, closePanel = true) {
|
||||||
if (this.saving || this.disabled) return
|
if (this.saving || this.disabled) return
|
||||||
if (this.pathMode) {
|
// 新签字开始保存后,之前发出的个人签名查询不得覆盖本次操作。
|
||||||
this.$set(this, "saving", true)
|
const version = ++this.signatureVersion
|
||||||
// data 参数为画板 PNG Base64;成功响应 data 为文件路径,统一回传两种签字绑定。
|
const base64 = this.is_value_base64
|
||||||
return $.post("/signature/uploadSignData", {data: signature})
|
this.saving = true
|
||||||
|
return $.post("/mobile/common/signing/update", {sign: signature, is_value_base64: base64})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (this._isDestroyed) return
|
if (this._isDestroyed || this._isBeingDestroyed || version !== this.signatureVersion
|
||||||
if (res.code === 0 && res.data) {
|
|| base64 !== this.is_value_base64) return
|
||||||
this.updateSignature(res.data)
|
if (res.code === 0) {
|
||||||
// 上传期间系统返回可能已收起签字板,不能再关闭下一层申请详情。
|
if (!this.matchesSignatureType(res.data)) {
|
||||||
if (this.signaturePanelVisible) this.changePanelVisible(false)
|
this.$toast.fail("返回的签名格式不正确,请重试")
|
||||||
this.$toast.success("保存成功")
|
|
||||||
} else {
|
|
||||||
this.$toast.fail(res.msg || "签字上传失败,请重试")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.fail(() => {
|
|
||||||
if (!this._isDestroyed) this.$toast.fail("签字上传失败,请重试")
|
|
||||||
})
|
|
||||||
.always(() => { this.$set(this, "saving", false) })
|
|
||||||
}
|
|
||||||
if (!this.saveToProfile) {
|
|
||||||
this.updateSignature(signature)
|
|
||||||
this.changePanelVisible(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.saving = true
|
this.personalSignature = res.data
|
||||||
return $.post("/mobile/common/signing/update", {sign: signature})
|
if (closePanel && this.signaturePanelVisible) this.changePanelVisible(false)
|
||||||
.then((res) => {
|
this.updateSignature(res.data)
|
||||||
if (res.code === 0) {
|
|
||||||
this.$toast.success("保存成功")
|
this.$toast.success("保存成功")
|
||||||
this.changePanelVisible(false)
|
|
||||||
return this.getMySign()
|
|
||||||
} else {
|
} else {
|
||||||
this.$toast.fail("保存失败")
|
this.$toast.fail(res.msg || "保存失败")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fail(() => this.$toast.fail("签字保存失败,请重试"))
|
.fail(() => this.$toast.fail("签字保存失败,请重试"))
|
||||||
.always(() => { this.saving = false })
|
.always(() => { this.saving = false })
|
||||||
},
|
},
|
||||||
/** 沿用个人签名查询接口,返回图片数据并同步给已有 v-model 调用方。 */
|
/**
|
||||||
getMySign() {
|
* previewOnly=true 仅初始化预览;省略参数保留旧页面主动调用时的回填行为。
|
||||||
return $.get("/mobile/common/signing/getMySign").then((data) => {
|
* 接口传 is_value_base64,不传用户ID;按登录账号返回对应类型签名或 null,方法返回 jQuery Promise。
|
||||||
if (data) {
|
* 无个人签名时保留当前内容;请求期间表单变更、重签或组件销毁后不再应用旧结果。
|
||||||
this.updateSignature(data)
|
*/
|
||||||
|
getMySign(previewOnly = false) {
|
||||||
|
const request = ++this.signatureRequest
|
||||||
|
const version = this.signatureVersion
|
||||||
|
this.signatureLoading = true
|
||||||
|
return $.get("/mobile/common/signing/getMySign", {is_value_base64: this.is_value_base64})
|
||||||
|
.then((data) => {
|
||||||
|
if (request !== this.signatureRequest || version !== this.signatureVersion
|
||||||
|
|| this._isDestroyed || this._isBeingDestroyed) return
|
||||||
|
this.personalSignature = this.matchesSignatureType(data) ? data : ''
|
||||||
|
if (previewOnly) {
|
||||||
|
if (!this.matchesSignatureType(this.boundSignature)) this.signatureContent = this.personalSignature
|
||||||
|
} else if (this.personalSignature) {
|
||||||
|
this.updateSignature(this.personalSignature)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.fail(() => {
|
||||||
|
// 自动预览失败仍可手写;仅主动读取失败时提示,避免阻断业务页面进入。
|
||||||
|
if (!previewOnly && request === this.signatureRequest && !this._isDestroyed && !this._isBeingDestroyed) {
|
||||||
|
this.$toast.fail("个人签名读取失败,请重新签字")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
if (request === this.signatureRequest) this.signatureLoading = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -163,6 +208,8 @@ module.exports = {
|
|||||||
})
|
})
|
||||||
// 旧表单通过 PJAX 替换 DOM 时不会自动销毁 Vue,主动释放签字实例和公共栈回调。
|
// 旧表单通过 PJAX 替换 DOM 时不会自动销毁 Vue,主动释放签字实例和公共栈回调。
|
||||||
$(document).one("pjax:beforeReplace.mobileSignature" + this._uid, () => this.$destroy())
|
$(document).one("pjax:beforeReplace.mobileSignature" + this._uid, () => this.$destroy())
|
||||||
|
// 所有页面均按类型预览个人签名,初始化不触发 input、保存或打开签字板。
|
||||||
|
this.getMySign(true)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
$(document).off("pjax:beforeReplace.mobileSignature" + this._uid)
|
$(document).off("pjax:beforeReplace.mobileSignature" + this._uid)
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ module.exports = {
|
|||||||
if (this.qz) {
|
if (this.qz) {
|
||||||
this.getImageUrl(this.qz)
|
this.getImageUrl(this.qz)
|
||||||
} else {
|
} else {
|
||||||
$.get("/mobile/common/signing/getMySign").then(data => {
|
// 与手机共用分类查询:只读取当前模式对应的个人签名。
|
||||||
|
$.get("/mobile/common/signing/getMySign", {is_value_base64: this.is_value_base64}).then(data => {
|
||||||
if (data) {
|
if (data) {
|
||||||
this.getImageUrl(data)
|
this.getImageUrl(data)
|
||||||
this.$emit("change", data)
|
this.$emit("change", data)
|
||||||
|
|||||||
@@ -243,7 +243,8 @@ layout("/mobile/platform.html"){
|
|||||||
<div class="mb10">审核签字</div>
|
<div class="mb10">审核签字</div>
|
||||||
</template>
|
</template>
|
||||||
<template #input>
|
<template #input>
|
||||||
<mobile-sign :path-mode="true" ref="signature" v-model="formData.auditSign"
|
<!-- 新版签字组件使用文件模式,回填路径供协会审核保存和详情展示。 -->
|
||||||
|
<mobile-sign :is_value_base64="false" ref="signature" v-model="formData.auditSign"
|
||||||
:popup-visible="signatureShow" :disabled="formLoading"
|
:popup-visible="signatureShow" :disabled="formLoading"
|
||||||
@open="openSignature" @close="closePopup"></mobile-sign>
|
@open="openSignature" @close="closePopup"></mobile-sign>
|
||||||
</template>
|
</template>
|
||||||
@@ -288,7 +289,7 @@ layout("/mobile/platform.html"){
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
'club-user-apply-info': httpVueLoader('/components/mobile/club/mobileClubUserApplyInfo.vue?v=1.0.4'),
|
'club-user-apply-info': httpVueLoader('/components/mobile/club/mobileClubUserApplyInfo.vue?v=1.0.4'),
|
||||||
'mobile-sign': httpVueLoader('/components/sign/mobileSign.vue?v=1.0.2'),
|
'mobile-sign': httpVueLoader('/components/sign/mobileSign.vue?v=1.0.3'),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 使用远端统一历史会话;签字层和详情层由同一回调驱动,避免重复压栈。
|
// 使用远端统一历史会话;签字层和详情层由同一回调驱动,避免重复压栈。
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ layout("/mobile/platform.html"){
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/info.js'){}#-->
|
<!--#include('/mobile/difficulty/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ layout("/mobile/platform.html"){
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/info.js'){}#-->
|
<!--#include('/mobile/difficulty/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ layout("/mobile/platform.html"){
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/info.js'){}#-->
|
<!--#include('/mobile/difficulty/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ layout("/mobile/platform.html"){
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/info.js'){}#-->
|
<!--#include('/mobile/difficulty/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ layout("/mobile/platform.html"){
|
|||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<!--首页-->
|
<!--首页-->
|
||||||
<div style="margin-bottom: 60px" v-if="active === 0">
|
<div v-if="active === 0">
|
||||||
<!--轮播图-->
|
<!--轮播图-->
|
||||||
<van-swipe :autoplay="3000" class="swiper">
|
<van-swipe :autoplay="3000" class="swiper">
|
||||||
<van-swipe-item :key="index" v-for="(image, index) in images">
|
<van-swipe-item :key="index" v-for="(image, index) in images">
|
||||||
@@ -385,7 +385,7 @@ layout("/mobile/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--工作台-->
|
<!--工作台-->
|
||||||
<div style="margin-bottom: 60px" v-if="active === 2">
|
<div v-if="active === 2">
|
||||||
<van-row class="module" v-for="m in moduleMenus">
|
<van-row class="module" v-for="m in moduleMenus">
|
||||||
<div class="van-sidebar-item van-sidebar-item--select">
|
<div class="van-sidebar-item van-sidebar-item--select">
|
||||||
{{m.moduleName}}
|
{{m.moduleName}}
|
||||||
@@ -401,7 +401,8 @@ layout("/mobile/platform.html"){
|
|||||||
</van-row>
|
</van-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<van-tabbar @change="tabbrChange" v-model="active">
|
<!-- 按底部导航实际高度(含手机安全区域)占位,确保末行菜单可完整滚动到导航上方。 -->
|
||||||
|
<van-tabbar @change="tabbrChange" v-model="active" placeholder>
|
||||||
<van-tabbar-item icon="home-o">首页</van-tabbar-item>
|
<van-tabbar-item icon="home-o">首页</van-tabbar-item>
|
||||||
<van-tabbar-item :dot="needCount" icon="envelop-o">我的办理</van-tabbar-item>
|
<van-tabbar-item :dot="needCount" icon="envelop-o">我的办理</van-tabbar-item>
|
||||||
<van-tabbar-item icon="apps-o">工作台</van-tabbar-item>
|
<van-tabbar-item icon="apps-o">工作台</van-tabbar-item>
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ layout("/mobile/platform.html"){
|
|||||||
.association-description { padding: 0 14px; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; }
|
.association-description { padding: 0 14px; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
.association-muted { margin: 10px 14px; color: #969799; }
|
.association-muted { margin: 10px 14px; color: #969799; }
|
||||||
.association-total .van-cell__value { color: #086bc3; font-weight: 600; }
|
.association-total .van-cell__value { color: #086bc3; font-weight: 600; }
|
||||||
|
/* 共用详情中的预算分组沿用申报审核页的分隔样式。 */
|
||||||
|
.association-budget-item + .association-budget-item { border-top: 1px solid #f2f3f5; }
|
||||||
.association-sign-image { display: block; max-width: calc(100% - 28px); width: 240px; height: 100px; object-fit: contain; margin: 8px 14px; }
|
.association-sign-image { display: block; max-width: calc(100% - 28px); width: 240px; height: 100px; object-fit: contain; margin: 8px 14px; }
|
||||||
.association-actions { flex-shrink: 0; display: flex; gap: 12px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); background: white; border-top: 1px solid #eee; z-index: 2; }
|
.association-actions { flex-shrink: 0; display: flex; gap: 12px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); background: white; border-top: 1px solid #eee; z-index: 2; }
|
||||||
.association-actions .van-button { flex: 1; border-radius: 6px; height: 40px; }
|
.association-actions .van-button { flex: 1; border-radius: 6px; height: 40px; }
|
||||||
@@ -127,7 +129,7 @@ layout("/mobile/platform.html"){
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include('info.js'){}#-->
|
<!--#include('/mobile/jf/activitybx/review/info.js'){}#-->
|
||||||
<!--#include('page.js'){}#-->
|
<!--#include('page.js'){}#-->
|
||||||
</script>
|
</script>
|
||||||
<!--#
|
<!--#
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
/** 协会活动申报详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
|
||||||
const associationInfo = {
|
|
||||||
template: /*language=HTML*/ `
|
|
||||||
<div class="association-info">
|
|
||||||
<section class="association-section">
|
|
||||||
<h3>{{ data.activity_name }}</h3>
|
|
||||||
<van-cell title="当前状态" :value="data.state_name"></van-cell>
|
|
||||||
<van-cell title="申请人" :value="data.user_name + '(' + (data.loginname || '—') + ')'"></van-cell>
|
|
||||||
<van-cell title="联系方式" :value="data.mobile || '—'"></van-cell>
|
|
||||||
<van-cell title="申请时间" :value="data.apply_time || '—'"></van-cell>
|
|
||||||
<van-cell title="承办协会" :value="data.clubName || data.fundsUnitName || '—'"></van-cell>
|
|
||||||
<van-cell title="项目编号" :value="data.projectCode || '—'"></van-cell>
|
|
||||||
<van-cell title="经费类型" :value="data.projectJfName || '—'"></van-cell>
|
|
||||||
<van-cell title="计划时间" :value="(data.startPlannedDate || '—') + ' 至 ' + (data.endPlannedDate || '—')"></van-cell>
|
|
||||||
<van-cell title="活动地点" :value="data.address || '—'"></van-cell>
|
|
||||||
<van-cell title="活动人数" :value="data.activity_number || '—'"></van-cell>
|
|
||||||
</section>
|
|
||||||
<section class="association-section">
|
|
||||||
<h3>活动方案及简介</h3>
|
|
||||||
<div class="association-description">{{ activityText || '暂无' }}</div>
|
|
||||||
<img v-for="(url, index) in activityImages" :key="index" :src="url" class="association-content-image"
|
|
||||||
alt="活动方案配图" @click="$emit('preview', {imageUrl: url})">
|
|
||||||
</section>
|
|
||||||
<section class="association-section">
|
|
||||||
<h3>预算明细</h3>
|
|
||||||
<van-cell v-for="(item, index) in budgets" :key="index" :title="item.name || '费用项目'"
|
|
||||||
:value="'¥' + money(item.budgetPrice)"></van-cell>
|
|
||||||
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="association-total"></van-cell>
|
|
||||||
</section>
|
|
||||||
<section class="association-section">
|
|
||||||
<h3>申请附件</h3>
|
|
||||||
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="association-file"
|
|
||||||
value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
|
||||||
<p v-if="!files.length" class="association-muted">暂无附件</p>
|
|
||||||
<h3>申请人签字</h3>
|
|
||||||
<img v-if="data.us_s" class="association-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
|
|
||||||
<p v-else class="association-muted">暂无签字</p>
|
|
||||||
</section>
|
|
||||||
<section v-for="audit in audits" :key="audit.key" class="association-section">
|
|
||||||
<h3>{{ audit.title }}</h3>
|
|
||||||
<van-cell title="审核人" :value="audit.name || '—'"></van-cell>
|
|
||||||
<van-cell title="审核时间" :value="audit.time || '—'"></van-cell>
|
|
||||||
<van-cell title="审核意见" :value="audit.idea || '暂无'"></van-cell>
|
|
||||||
<img v-if="audit.sign" class="association-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
|
|
||||||
</section>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
data: {type: Object, default: () => ({})}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
budgets() {
|
|
||||||
return this.asList(this.data.budgets)
|
|
||||||
},
|
|
||||||
files() {
|
|
||||||
return this.asList(this.data.files)
|
|
||||||
},
|
|
||||||
activityText() {
|
|
||||||
// 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。
|
|
||||||
const container = document.createElement('div')
|
|
||||||
container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n')
|
|
||||||
container.querySelectorAll('script,style').forEach((node) => node.remove())
|
|
||||||
return container.textContent.trim()
|
|
||||||
},
|
|
||||||
activityImages() {
|
|
||||||
// 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。
|
|
||||||
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
|
||||||
return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '')
|
|
||||||
.filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url))
|
|
||||||
},
|
|
||||||
audits() {
|
|
||||||
const data = this.data
|
|
||||||
return [
|
|
||||||
{key: 'association', title: '协会负责人审核', id: data.association_principal_id,
|
|
||||||
name: data.ap_username, time: data.association_principal_time, idea: data.association_principal_idea, sign: data.ap_s},
|
|
||||||
{key: 'accounting', title: '校工会负责人审核', id: data.accounting_name,
|
|
||||||
name: data.a_username, time: data.accounting_time, idea: data.accounting_idea, sign: data.a_s},
|
|
||||||
{key: 'union', title: '校工会主席审核', id: data.union_chairman_name,
|
|
||||||
name: data.ucc_username, time: data.union_chairman_time, idea: data.union_chairman_idea, sign: data.ucc_s}
|
|
||||||
].filter((audit) => audit.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
|
||||||
asList(value) {
|
|
||||||
if (Array.isArray(value)) return value
|
|
||||||
if (!value) return []
|
|
||||||
try {
|
|
||||||
const list = JSON.parse(value)
|
|
||||||
return Array.isArray(list) ? list : []
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
money(value) {
|
|
||||||
return value == null || !Number.isFinite(Number(value)) ? '—' : Number(value).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})
|
|
||||||
},
|
|
||||||
/** 签字兼容 PC 保存的 Base64 图片和历史文件路径。 */
|
|
||||||
signUrl(value) {
|
|
||||||
return value && value.startsWith('data:image/') ? value : '/signature/getSignData?path=' + encodeURIComponent(value || '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// 独立手机审核页面;不复用旧 declare 页面及其有缺陷的滚动分页。
|
// 独立手机审核页面;不复用旧 declare 页面及其有缺陷的滚动分页。
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
components: {'association-info': associationInfo},
|
components: {'association-info': activityReviewInfo},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
baseUrl: '/platform/jf/activity_bx/association',
|
baseUrl: '/platform/jf/activity_bx/association',
|
||||||
|
|||||||
@@ -1,67 +1,69 @@
|
|||||||
/** 活动申报审核详情:仅展示服务端已校验权限的数据,审核表单通过末尾插槽传入。 */
|
/** 四个活动申报审核入口共用详情;可选字段按返回数据显示,审核表单通过末尾插槽传入。 */
|
||||||
const activityReviewInfo = {
|
const activityReviewInfo = {
|
||||||
template: /*language=HTML*/ `
|
template: /*language=HTML*/ `
|
||||||
<div class="review-info">
|
<!-- 同时保留两套入口的既有样式类,详情内容和显隐规则共用一份。 -->
|
||||||
<section class="review-section">
|
<div class="review-info association-info">
|
||||||
|
<section class="review-section association-section">
|
||||||
<h3>{{ data.activity_name }}</h3>
|
<h3>{{ data.activity_name }}</h3>
|
||||||
<van-cell title="当前状态" :value="data.state_name"></van-cell>
|
<van-cell title="当前状态" :value="data.state_name"></van-cell>
|
||||||
<van-cell title="申请人" :value="data.user_name + '(' + (data.loginname || '—') + ')'"></van-cell>
|
<van-cell title="申请人" :value="data.user_name + '(' + (data.loginname || '—') + ')'"></van-cell>
|
||||||
<van-cell title="所属工会" :value="data.bxr_unionname || '—'"></van-cell>
|
<!-- 可选组织和类型字段由详情数据决定显隐,不根据审核入口切换展示模式。 -->
|
||||||
<van-cell title="所属单位" :value="data.bxr_unitname || '—'"></van-cell>
|
<van-cell v-if="data.bxr_unionname && String(data.bxr_unionname).trim()" title="所属工会" :value="data.bxr_unionname"></van-cell>
|
||||||
<van-cell title="活动主体" :value="activityType"></van-cell>
|
<van-cell v-if="data.bxr_unitname && String(data.bxr_unitname).trim()" title="所属单位" :value="data.bxr_unitname"></van-cell>
|
||||||
<van-cell title="申请类型" :value="Number(data.apply_type) === 1 ? '经费申报' : '报销申请'"></van-cell>
|
<van-cell v-if="data.activity_type" title="活动主体" :value="activityType"></van-cell>
|
||||||
|
<van-cell v-if="data.apply_type" title="申请类型" :value="Number(data.apply_type) === 1 ? '经费申报' : '报销申请'"></van-cell>
|
||||||
<van-cell title="联系方式" :value="data.mobile || '—'"></van-cell>
|
<van-cell title="联系方式" :value="data.mobile || '—'"></van-cell>
|
||||||
<van-cell title="申请时间" :value="data.apply_time || '—'"></van-cell>
|
<van-cell title="申请时间" :value="data.apply_time || '—'"></van-cell>
|
||||||
<van-cell title="承办单位/协会" :value="data.clubName || data.fundsUnitName || '—'"></van-cell>
|
<van-cell :title="Number(data.activity_type) === 40003 ? '承办协会' : '承办单位/协会'" :value="data.clubName || data.fundsUnitName || '—'"></van-cell>
|
||||||
<van-cell title="项目编号" :value="data.projectCode || '—'"></van-cell>
|
<van-cell title="项目编号" :value="data.projectCode || '—'"></van-cell>
|
||||||
<van-cell title="经费类型" :value="data.projectJfName || '—'"></van-cell>
|
<van-cell title="经费类型" :value="data.projectJfName || '—'"></van-cell>
|
||||||
<van-cell title="计划时间" :value="(data.startPlannedDate || '—') + ' 至 ' + (data.endPlannedDate || '—')"></van-cell>
|
<van-cell title="计划时间" :value="(data.startPlannedDate || '—') + ' 至 ' + (data.endPlannedDate || '—')"></van-cell>
|
||||||
<van-cell title="活动时间" :value="data.startTime ? data.startTime + ' 至 ' + (data.endTime || '—') : '暂无'"></van-cell>
|
<van-cell v-if="data.startTime || data.endTime" title="活动时间" :value="(data.startTime || '—') + ' 至 ' + (data.endTime || '—')"></van-cell>
|
||||||
<van-cell title="实际费用" :value="'¥' + money(data.activity_money)"></van-cell>
|
<!-- 零元是有效费用;仅未提供金额时隐藏,不能用金额真假判断。 -->
|
||||||
|
<van-cell v-if="data.activity_money != null && String(data.activity_money).trim() !== ''" title="实际费用" :value="'¥' + money(data.activity_money)"></van-cell>
|
||||||
<van-cell title="活动地点" :value="data.address || '—'"></van-cell>
|
<van-cell title="活动地点" :value="data.address || '—'"></van-cell>
|
||||||
<van-cell title="活动人数" :value="data.activity_number || '—'"></van-cell>
|
<van-cell title="活动人数" :value="data.activity_number || '—'"></van-cell>
|
||||||
</section>
|
</section>
|
||||||
<section class="review-section">
|
<section class="review-section association-section">
|
||||||
<h3>活动方案及简介</h3>
|
<h3>活动方案及简介</h3>
|
||||||
<div class="review-description">{{ activityText || '暂无' }}</div>
|
<div class="review-description association-description" style="white-space:normal;min-width:0"
|
||||||
<img v-for="(url, index) in activityImages" :key="index" :src="url" class="review-content-image"
|
v-html="activityHtml || '暂无'" @click="previewActivityImage"></div>
|
||||||
alt="活动方案配图" @click="$emit('preview', {imageUrl: url})">
|
|
||||||
</section>
|
</section>
|
||||||
<section class="review-section">
|
<section class="review-section association-section">
|
||||||
<h3>预算明细</h3>
|
<h3>预算明细</h3>
|
||||||
<div v-for="(item, index) in budgets" :key="index" class="review-budget-item">
|
<div v-for="(item, index) in budgets" :key="index" class="review-budget-item association-budget-item">
|
||||||
<van-cell :title="item.name || '费用项目'" :value="'¥' + money(item.budgetPrice)"></van-cell>
|
<van-cell :title="item.name || '费用项目'" :value="'¥' + money(item.budgetPrice)"></van-cell>
|
||||||
<template v-if="data.reimbursement_state_id">
|
<template v-if="data.reimbursement_state_id">
|
||||||
<van-cell title="实际费用" :value="'¥' + money(item.actualPrice)"></van-cell>
|
<van-cell v-if="item.actualPrice != null && String(item.actualPrice).trim() !== ''" title="实际费用" :value="'¥' + money(item.actualPrice)"></van-cell>
|
||||||
<van-cell title="收款人" :value="item.username || '—'"></van-cell>
|
<van-cell v-if="item.username" title="收款人" :value="item.username"></van-cell>
|
||||||
<van-cell title="支行名称" :value="item.bankName || '—'"></van-cell>
|
<van-cell v-if="item.bankName" title="支行名称" :value="item.bankName"></van-cell>
|
||||||
<van-cell title="报销卡号" :value="item.bankCardNum || '—'"></van-cell>
|
<van-cell v-if="item.bankCardNum" title="报销卡号" :value="item.bankCardNum"></van-cell>
|
||||||
<van-cell title="备注" :value="item.remark || '—'"></van-cell>
|
<van-cell v-if="item.remark" title="备注" :value="item.remark"></van-cell>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="review-total"></van-cell>
|
<van-cell title="预算合计" :value="'¥' + money(data.funds_money)" class="review-total association-total"></van-cell>
|
||||||
</section>
|
</section>
|
||||||
<section class="review-section">
|
<section class="review-section association-section">
|
||||||
<h3>申请附件</h3>
|
<h3>申请附件</h3>
|
||||||
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="review-file"
|
<van-cell v-for="(file, index) in files" :key="file.id || index" :title="file.filename || file.name || '附件'" class="review-file association-file"
|
||||||
value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||||
<p v-if="!files.length" class="review-muted">暂无附件</p>
|
<p v-if="!files.length" class="review-muted association-muted">暂无附件</p>
|
||||||
<h3>申请人签字</h3>
|
<h3>申请人签字</h3>
|
||||||
<img v-if="data.us_s" class="review-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
|
<img v-if="data.us_s" class="review-sign-image association-sign-image" :src="signUrl(data.us_s)" alt="申请人签字">
|
||||||
<p v-else class="review-muted">暂无签字</p>
|
<p v-else class="review-muted association-muted">暂无签字</p>
|
||||||
</section>
|
</section>
|
||||||
<section v-for="group in reimbursementFiles" :key="group.key" class="review-section">
|
<section v-for="group in reimbursementFiles" :key="group.key" class="review-section association-section">
|
||||||
<h3>{{ group.title }}</h3>
|
<h3>{{ group.title }}</h3>
|
||||||
<van-cell v-for="(file, index) in group.files" :key="file.id || index" :title="file.filename || file.name || '附件'"
|
<van-cell v-for="(file, index) in group.files" :key="file.id || index" :title="file.filename || file.name || '附件'"
|
||||||
class="review-file" value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
class="review-file association-file" value="查看" is-link @click="$emit('preview', file)"></van-cell>
|
||||||
<p v-if="!group.files.length" class="review-muted">暂无附件</p>
|
<p v-if="!group.files.length" class="review-muted association-muted">暂无附件</p>
|
||||||
</section>
|
</section>
|
||||||
<section v-for="audit in audits" :key="audit.key" class="review-section">
|
<section v-for="audit in audits" :key="audit.key" class="review-section association-section">
|
||||||
<h3>{{ audit.title }}</h3>
|
<h3>{{ audit.title }}</h3>
|
||||||
<van-cell title="审核人" :value="audit.name || '—'"></van-cell>
|
<van-cell title="审核人" :value="audit.name || '—'"></van-cell>
|
||||||
<van-cell title="审核时间" :value="audit.time || '—'"></van-cell>
|
<van-cell title="审核时间" :value="audit.time || '—'"></van-cell>
|
||||||
<van-cell title="审核意见" :value="audit.idea || '暂无'"></van-cell>
|
<van-cell title="审核意见" :value="audit.idea || '暂无'"></van-cell>
|
||||||
<img v-if="audit.sign" class="review-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
|
<img v-if="audit.sign" class="review-sign-image association-sign-image" :src="signUrl(audit.sign)" :alt="audit.title + '签字'">
|
||||||
</section>
|
</section>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,9 +76,11 @@ const activityReviewInfo = {
|
|||||||
return {40001: '校工会活动', 40002: '基层工会活动', 40003: '协会活动', 40004: '女职工活动'}[this.data.activity_type] || '—'
|
return {40001: '校工会活动', 40002: '基层工会活动', 40003: '协会活动', 40004: '女职工活动'}[this.data.activity_type] || '—'
|
||||||
},
|
},
|
||||||
reimbursementFiles() {
|
reimbursementFiles() {
|
||||||
|
// 报销状态存在且附件组有内容时才展示,未上传的可选附件不占用空白分组。
|
||||||
if (!this.data.reimbursement_state_id) return []
|
if (!this.data.reimbursement_state_id) return []
|
||||||
return [{key: 'billFiles', title: '发票'}, {key: 'photoFiles', title: '活动报道照片/总结/链接'}, {key: 'otherFiles', title: '奖品/纪念品/服装/道具等活动人员名单'}]
|
return [{key: 'billFiles', title: '发票'}, {key: 'photoFiles', title: '活动报道照片/总结/链接'}, {key: 'otherFiles', title: '奖品/纪念品/服装/道具等活动人员名单'}]
|
||||||
.map((group) => Object.assign({}, group, {files: this.asList(this.data[group.key])}))
|
.map((group) => Object.assign({}, group, {files: this.asList(this.data[group.key])}))
|
||||||
|
.filter((group) => group.files.length)
|
||||||
},
|
},
|
||||||
budgets() {
|
budgets() {
|
||||||
return this.asList(this.data.budgets)
|
return this.asList(this.data.budgets)
|
||||||
@@ -84,18 +88,94 @@ const activityReviewInfo = {
|
|||||||
files() {
|
files() {
|
||||||
return this.asList(this.data.files)
|
return this.asList(this.data.files)
|
||||||
},
|
},
|
||||||
activityText() {
|
activityHtml() {
|
||||||
// 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。
|
// activity_content 为历史富文本;只重建允许的内容节点,不复制脚本、事件、类名或任意内联样式。
|
||||||
const container = document.createElement('div')
|
|
||||||
container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n')
|
|
||||||
container.querySelectorAll('script,style').forEach((node) => node.remove())
|
|
||||||
return container.textContent.trim()
|
|
||||||
},
|
|
||||||
activityImages() {
|
|
||||||
// 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。
|
|
||||||
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
||||||
return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '')
|
const container = content.createElement('div')
|
||||||
.filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url))
|
const allowed = new Set(['P', 'DIV', 'SPAN', 'BR', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'DEL', 'SUB', 'SUP',
|
||||||
|
'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'PRE', 'CODE', 'HR',
|
||||||
|
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'IMG', 'FONT', 'MARK'])
|
||||||
|
const blocked = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'SVG', 'MATH', 'TEMPLATE',
|
||||||
|
'FORM', 'INPUT', 'BUTTON', 'TEXTAREA', 'SELECT', 'LINK', 'META', 'BASE'])
|
||||||
|
const appendContent = (source, parent) => {
|
||||||
|
if (source.nodeType === 3) {
|
||||||
|
parent.appendChild(content.createTextNode(source.textContent))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (source.nodeType !== 1 || blocked.has(source.tagName)) return
|
||||||
|
const tag = source.tagName
|
||||||
|
if (!allowed.has(tag)) {
|
||||||
|
Array.from(source.childNodes).forEach((child) => appendContent(child, parent))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const node = content.createElement(tag === 'FONT' ? 'span' : tag.toLowerCase())
|
||||||
|
// 仅保留文字强调、颜色和对齐;固定宽高、定位和 Word 段落间距不带入手机页面。
|
||||||
|
const textStyles = ['color', 'backgroundColor', 'fontWeight', 'fontStyle', 'textDecorationLine', 'textAlign', 'verticalAlign']
|
||||||
|
textStyles.forEach((name) => { node.style[name] = source.style[name] })
|
||||||
|
if (tag === 'FONT' && !node.style.color) node.style.color = source.getAttribute('color') || ''
|
||||||
|
if (!node.style.textAlign && /^(left|center|right|justify)$/i.test(source.getAttribute('align') || '')) {
|
||||||
|
node.style.textAlign = source.getAttribute('align')
|
||||||
|
}
|
||||||
|
if (/^(P|DIV|H[1-6]|BLOCKQUOTE|PRE|UL|OL)$/.test(tag)) {
|
||||||
|
node.style.margin = '0 0 8px'
|
||||||
|
node.style.lineHeight = '1.8'
|
||||||
|
}
|
||||||
|
if (/^H[1-6]$/.test(tag)) node.style.fontSize = '16px'
|
||||||
|
if (tag === 'UL' || tag === 'OL') {
|
||||||
|
node.style.paddingLeft = '24px'
|
||||||
|
node.style.listStyleType = tag === 'UL' ? 'disc' : 'decimal'
|
||||||
|
if (tag === 'OL' && /^-?\d{1,6}$/.test(source.getAttribute('start') || '')) {
|
||||||
|
node.setAttribute('start', source.getAttribute('start'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tag === 'PRE') {
|
||||||
|
node.style.whiteSpace = 'pre-wrap'
|
||||||
|
node.style.overflowWrap = 'anywhere'
|
||||||
|
}
|
||||||
|
if (tag === 'IMG') {
|
||||||
|
// 图片沿用原有地址范围;保留正文中的位置,点击后交给现有预览弹框和历史栈。
|
||||||
|
const url = (source.getAttribute('src') || '').trim()
|
||||||
|
if (!/^(https?:\/\/|\/(?![\/\\])|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url)) return
|
||||||
|
node.setAttribute('src', url)
|
||||||
|
node.setAttribute('alt', source.getAttribute('alt') || '活动方案配图')
|
||||||
|
node.style.cssText = 'display:block;max-width:100%;height:auto;margin:8px 0;cursor:pointer'
|
||||||
|
}
|
||||||
|
if (tag === 'TABLE') {
|
||||||
|
// 每张表单独横向滚动,保留行列关系;不让宽表撑开整个审核页面。
|
||||||
|
const scroll = content.createElement('div')
|
||||||
|
scroll.style.cssText = 'max-width:100%;overflow-x:auto;margin:8px 0;white-space:normal'
|
||||||
|
parent.appendChild(scroll)
|
||||||
|
parent = scroll
|
||||||
|
node.style.width = '100%'
|
||||||
|
node.style.borderCollapse = 'collapse'
|
||||||
|
node.style.tableLayout = 'auto'
|
||||||
|
}
|
||||||
|
if (tag === 'TD' || tag === 'TH') {
|
||||||
|
node.style.border = '1px solid #dcdfe6'
|
||||||
|
node.style.padding = '6px 8px'
|
||||||
|
node.style.minWidth = '72px'
|
||||||
|
node.style.overflowWrap = 'anywhere'
|
||||||
|
// 合并单元格只接受浏览器支持范围内的整数,保留预算表的表头和分组结构。
|
||||||
|
;['rowspan', 'colspan'].forEach((name) => {
|
||||||
|
const value = source.getAttribute(name)
|
||||||
|
if (/^\d{1,4}$/.test(value || '') && Number(value) >= 1 && Number(value) <= 1000) {
|
||||||
|
node.setAttribute(name, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Array.from(source.childNodes).forEach((child) => appendContent(child, node))
|
||||||
|
parent.appendChild(node)
|
||||||
|
}
|
||||||
|
if (!content.body.children.length) {
|
||||||
|
// 旧手机申请可能保存纯文字;保留用户输入的换行,但不把它作为 HTML 再解析。
|
||||||
|
const plain = content.createElement('div')
|
||||||
|
plain.style.whiteSpace = 'pre-wrap'
|
||||||
|
plain.textContent = content.body.textContent.trim()
|
||||||
|
container.appendChild(plain)
|
||||||
|
} else {
|
||||||
|
Array.from(content.body.childNodes).forEach((node) => appendContent(node, container))
|
||||||
|
}
|
||||||
|
return container.textContent.trim() || container.querySelector('img') ? container.innerHTML : ''
|
||||||
},
|
},
|
||||||
audits() {
|
audits() {
|
||||||
const data = this.data
|
const data = this.data
|
||||||
@@ -124,6 +204,11 @@ const activityReviewInfo = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** event 为正文点击事件;仅图片触发 preview,参数 imageUrl 交由父页面现有弹框处理,无返回值。 */
|
||||||
|
previewActivityImage(event) {
|
||||||
|
const target = event.target
|
||||||
|
if (target && target.tagName === 'IMG') this.$emit('preview', {imageUrl: target.getAttribute('src')})
|
||||||
|
},
|
||||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||||
asList(value) {
|
asList(value) {
|
||||||
if (Array.isArray(value)) return value
|
if (Array.isArray(value)) return value
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ const reimbursementInfo = {
|
|||||||
</section>
|
</section>
|
||||||
<section class="reimbursement-review-section">
|
<section class="reimbursement-review-section">
|
||||||
<h3>活动方案及简介</h3>
|
<h3>活动方案及简介</h3>
|
||||||
<div class="reimbursement-review-description">{{ activityText || '暂无' }}</div>
|
<div class="reimbursement-review-description" style="white-space:normal;min-width:0"
|
||||||
<img v-for="(url, index) in activityImages" :key="index" :src="url" class="reimbursement-review-content-image"
|
v-html="activityHtml || '暂无'" @click="previewActivityImage"></div>
|
||||||
alt="活动方案配图" @click="$emit('preview', {imageUrl: url})">
|
|
||||||
</section>
|
</section>
|
||||||
<section class="reimbursement-review-section">
|
<section class="reimbursement-review-section">
|
||||||
<h3>费用及收款信息</h3>
|
<h3>费用及收款信息</h3>
|
||||||
@@ -85,18 +84,94 @@ const reimbursementInfo = {
|
|||||||
files() {
|
files() {
|
||||||
return this.asList(this.data.files)
|
return this.asList(this.data.files)
|
||||||
},
|
},
|
||||||
activityText() {
|
activityHtml() {
|
||||||
// 申请内容来自富文本,按段落提取文本,避免在审核页面执行其中的脚本或事件属性。
|
// activity_content 为历史富文本;只重建允许的内容节点,不复制脚本、事件、类名或任意内联样式。
|
||||||
const container = document.createElement('div')
|
|
||||||
container.innerHTML = (this.data.activity_content || '').replace(/<\/(p|div|li)>/gi, '\n')
|
|
||||||
container.querySelectorAll('script,style').forEach((node) => node.remove())
|
|
||||||
return container.textContent.trim()
|
|
||||||
},
|
|
||||||
activityImages() {
|
|
||||||
// 保留富文本中的方案配图,只提取图片地址,不执行原文中的 HTML 属性和事件。
|
|
||||||
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
const content = new DOMParser().parseFromString(this.data.activity_content || '', 'text/html')
|
||||||
return Array.from(content.querySelectorAll('img')).map((img) => img.getAttribute('src') || '')
|
const container = content.createElement('div')
|
||||||
.filter((url) => /^(https?:\/\/|\/(?!\/)|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url))
|
const allowed = new Set(['P', 'DIV', 'SPAN', 'BR', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'DEL', 'SUB', 'SUP',
|
||||||
|
'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'PRE', 'CODE', 'HR',
|
||||||
|
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'IMG', 'FONT', 'MARK'])
|
||||||
|
const blocked = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'SVG', 'MATH', 'TEMPLATE',
|
||||||
|
'FORM', 'INPUT', 'BUTTON', 'TEXTAREA', 'SELECT', 'LINK', 'META', 'BASE'])
|
||||||
|
const appendContent = (source, parent) => {
|
||||||
|
if (source.nodeType === 3) {
|
||||||
|
parent.appendChild(content.createTextNode(source.textContent))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (source.nodeType !== 1 || blocked.has(source.tagName)) return
|
||||||
|
const tag = source.tagName
|
||||||
|
if (!allowed.has(tag)) {
|
||||||
|
Array.from(source.childNodes).forEach((child) => appendContent(child, parent))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const node = content.createElement(tag === 'FONT' ? 'span' : tag.toLowerCase())
|
||||||
|
// 仅保留文字强调、颜色和对齐;固定宽高、定位和 Word 段落间距不带入手机页面。
|
||||||
|
const textStyles = ['color', 'backgroundColor', 'fontWeight', 'fontStyle', 'textDecorationLine', 'textAlign', 'verticalAlign']
|
||||||
|
textStyles.forEach((name) => { node.style[name] = source.style[name] })
|
||||||
|
if (tag === 'FONT' && !node.style.color) node.style.color = source.getAttribute('color') || ''
|
||||||
|
if (!node.style.textAlign && /^(left|center|right|justify)$/i.test(source.getAttribute('align') || '')) {
|
||||||
|
node.style.textAlign = source.getAttribute('align')
|
||||||
|
}
|
||||||
|
if (/^(P|DIV|H[1-6]|BLOCKQUOTE|PRE|UL|OL)$/.test(tag)) {
|
||||||
|
node.style.margin = '0 0 8px'
|
||||||
|
node.style.lineHeight = '1.8'
|
||||||
|
}
|
||||||
|
if (/^H[1-6]$/.test(tag)) node.style.fontSize = '16px'
|
||||||
|
if (tag === 'UL' || tag === 'OL') {
|
||||||
|
node.style.paddingLeft = '24px'
|
||||||
|
node.style.listStyleType = tag === 'UL' ? 'disc' : 'decimal'
|
||||||
|
if (tag === 'OL' && /^-?\d{1,6}$/.test(source.getAttribute('start') || '')) {
|
||||||
|
node.setAttribute('start', source.getAttribute('start'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tag === 'PRE') {
|
||||||
|
node.style.whiteSpace = 'pre-wrap'
|
||||||
|
node.style.overflowWrap = 'anywhere'
|
||||||
|
}
|
||||||
|
if (tag === 'IMG') {
|
||||||
|
// 图片沿用原有地址范围;保留正文中的位置,点击后交给现有预览弹框和历史栈。
|
||||||
|
const url = (source.getAttribute('src') || '').trim()
|
||||||
|
if (!/^(https?:\/\/|\/(?![\/\\])|data:image\/(?:png|jpeg|gif|webp);base64,)/i.test(url)) return
|
||||||
|
node.setAttribute('src', url)
|
||||||
|
node.setAttribute('alt', source.getAttribute('alt') || '活动方案配图')
|
||||||
|
node.style.cssText = 'display:block;max-width:100%;height:auto;margin:8px 0;cursor:pointer'
|
||||||
|
}
|
||||||
|
if (tag === 'TABLE') {
|
||||||
|
// 每张表单独横向滚动,保留行列关系;不让宽表撑开整个审核页面。
|
||||||
|
const scroll = content.createElement('div')
|
||||||
|
scroll.style.cssText = 'max-width:100%;overflow-x:auto;margin:8px 0;white-space:normal'
|
||||||
|
parent.appendChild(scroll)
|
||||||
|
parent = scroll
|
||||||
|
node.style.width = '100%'
|
||||||
|
node.style.borderCollapse = 'collapse'
|
||||||
|
node.style.tableLayout = 'auto'
|
||||||
|
}
|
||||||
|
if (tag === 'TD' || tag === 'TH') {
|
||||||
|
node.style.border = '1px solid #dcdfe6'
|
||||||
|
node.style.padding = '6px 8px'
|
||||||
|
node.style.minWidth = '72px'
|
||||||
|
node.style.overflowWrap = 'anywhere'
|
||||||
|
// 合并单元格只接受浏览器支持范围内的整数,保留预算表的表头和分组结构。
|
||||||
|
;['rowspan', 'colspan'].forEach((name) => {
|
||||||
|
const value = source.getAttribute(name)
|
||||||
|
if (/^\d{1,4}$/.test(value || '') && Number(value) >= 1 && Number(value) <= 1000) {
|
||||||
|
node.setAttribute(name, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Array.from(source.childNodes).forEach((child) => appendContent(child, node))
|
||||||
|
parent.appendChild(node)
|
||||||
|
}
|
||||||
|
if (!content.body.children.length) {
|
||||||
|
// 旧手机申请可能保存纯文字;保留用户输入的换行,但不把它作为 HTML 再解析。
|
||||||
|
const plain = content.createElement('div')
|
||||||
|
plain.style.whiteSpace = 'pre-wrap'
|
||||||
|
plain.textContent = content.body.textContent.trim()
|
||||||
|
container.appendChild(plain)
|
||||||
|
} else {
|
||||||
|
Array.from(content.body.childNodes).forEach((node) => appendContent(node, container))
|
||||||
|
}
|
||||||
|
return container.textContent.trim() || container.querySelector('img') ? container.innerHTML : ''
|
||||||
},
|
},
|
||||||
audits() {
|
audits() {
|
||||||
const data = this.data
|
const data = this.data
|
||||||
@@ -125,6 +200,11 @@ const reimbursementInfo = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** event 为正文点击事件;仅图片触发 preview,参数 imageUrl 交由父页面现有弹框处理,无返回值。 */
|
||||||
|
previewActivityImage(event) {
|
||||||
|
const target = event.target
|
||||||
|
if (target && target.tagName === 'IMG') this.$emit('preview', {imageUrl: target.getAttribute('src')})
|
||||||
|
},
|
||||||
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
/** 历史接口可能返回 JSON 字符串或数组;空值、异常数据均不阻断整页展示。 */
|
||||||
asList(value) {
|
asList(value) {
|
||||||
if (Array.isArray(value)) return value
|
if (Array.isArray(value)) return value
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ layout("/mobile/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/siteInfo.js'){}#-->
|
<!--#include('/mobile/site/common/siteInfo.js'){}#-->
|
||||||
<!--#include('apply.js'){}#-->
|
<!--#include('apply.js'){}#-->
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ layout("/mobile/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../apply/apply.js'){}#-->
|
<!--#include('/mobile/site/apply/apply.js'){}#-->
|
||||||
<!--#include('../common/info.js'){}#-->
|
<!--#include('/mobile/site/common/info.js'){}#-->
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ layout("/mobile/platform.html"){
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/info.js'){}#-->
|
<!--#include('/mobile/site/nursing/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ layout("/mobile/platform.html"){
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/info.js'){}#-->
|
<!--#include('/mobile/site/nursing/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ layout("/mobile/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/info.js'){}#-->
|
<!--#include('/mobile/site/common/info.js'){}#-->
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak>
|
||||||
<van-nav-bar title="签字"></van-nav-bar>
|
<van-nav-bar title="签字"></van-nav-bar>
|
||||||
<van-notice-bar v-if="subDis" text="缺少签字参数,请重新扫码。"></van-notice-bar>
|
<van-notice-bar v-if="subDis" text="缺少签字参数,请重新扫码。"></van-notice-bar>
|
||||||
<mobile-sign :value="sign" :save-to-profile="false" :popup-visible="showSignaturePanel"
|
<mobile-sign :value="sign" :is_value_base64="isValueBase64" :popup-visible="showSignaturePanel"
|
||||||
:disabled="subDis || formLoading" @input="save" @open="openSignature" @close="closeSignature"></mobile-sign>
|
:disabled="subDis || formLoading" @input="save" @open="openSignature" @close="closeSignature"></mobile-sign>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
'mobile-sign': httpVueLoader("/components/sign/mobileSign.vue?v=" + new Date().getTime())
|
'mobile-sign': httpVueLoader("/components/sign/mobileSign.vue?v=" + new Date().getTime())
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {prefix: '', id: '', sign: '', subDis: false, formLoading: false, showSignaturePanel: false}
|
return {prefix: '', id: '', sign: '', isValueBase64: true, subDis: false, formLoading: false, showSignaturePanel: false}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openSignature() {
|
openSignature() {
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
if (!this.formLoading) this.$options.popupSession.close()
|
if (!this.formLoading) this.$options.popupSession.close()
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* signature 为公共签字板回传的 PNG Base64;prefix/id 来自二维码,关联 PC 端轮询。
|
* signature 为组件已保存的个人签名:true 为 Base64,false 为文件路径。
|
||||||
* 保留 is_value_base64=false 的文件保存方式;接口返回 {code, msg},code=0 表示成功。
|
* prefix/id 和 is_value_base64 来自二维码;接口返回 {code,msg,data},code=0 表示发布给 PC 成功。
|
||||||
*/
|
*/
|
||||||
save(signature) {
|
save(signature) {
|
||||||
if (this.formLoading || this.subDis) return
|
if (this.formLoading || this.subDis) return
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
prefix: this.prefix,
|
prefix: this.prefix,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
base64: signature,
|
base64: signature,
|
||||||
is_value_base64: false
|
is_value_base64: this.isValueBase64
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -70,7 +70,10 @@
|
|||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
this.id = query.get('id') || ''
|
this.id = query.get('id') || ''
|
||||||
this.prefix = query.get('prefix') || ''
|
this.prefix = query.get('prefix') || ''
|
||||||
this.subDis = !this.id || !this.prefix
|
// 缺省沿用 PC 默认 true;显式 false 使用文件签名,异常参数不默默切换类型。
|
||||||
|
const signType = query.get('is_value_base64')
|
||||||
|
this.isValueBase64 = signType !== 'false'
|
||||||
|
this.subDis = !this.id || !this.prefix || (signType !== null && signType !== 'true' && signType !== 'false')
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
const session = this.$options.popupSession
|
const session = this.$options.popupSession
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ layout("/layouts/platform.html"){
|
|||||||
</guava>
|
</guava>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../manage/info.js'){}#-->
|
<!--#include('/platform/site/manage/info.js'){}#-->
|
||||||
<!--#include('apply.js'){}#-->
|
<!--#include('apply.js'){}#-->
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ layout("/layouts/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/applyInfo.js'){}#-->
|
<!--#include('/platform/site/common/applyInfo.js'){}#-->
|
||||||
<!--#include('../apply/apply.js'){}#-->
|
<!--#include('/platform/site/apply/apply.js'){}#-->
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ layout("/layouts/platform.html"){
|
|||||||
</guava>
|
</guava>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include("../common/info.js"){}#-->
|
<!--#include("/platform/site/nursing/common/info.js"){}#-->
|
||||||
<!--#include("../common/commonQuery.js"){}#-->
|
<!--#include("/platform/site/nursing/common/commonQuery.js"){}#-->
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ layout("/layouts/platform.html"){
|
|||||||
</guava>
|
</guava>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include("../common/info.js"){}#-->
|
<!--#include("/platform/site/nursing/common/info.js"){}#-->
|
||||||
<!--#include("../common/commonQuery.js"){}#-->
|
<!--#include("/platform/site/nursing/common/commonQuery.js"){}#-->
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ layout("/layouts/platform.html"){
|
|||||||
</guava>
|
</guava>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
<!--#include("../common/info.js"){}#-->
|
<!--#include("/platform/site/nursing/common/info.js"){}#-->
|
||||||
<!--#include("../common/commonQuery.js"){}#-->
|
<!--#include("/platform/site/nursing/common/commonQuery.js"){}#-->
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ layout("/layouts/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/applyInfo.js'){}#-->
|
<!--#include('/platform/site/common/applyInfo.js'){}#-->
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ layout("/layouts/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('../common/applyInfo.js'){}#-->
|
<!--#include('/platform/site/common/applyInfo.js'){}#-->
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ layout("/layouts/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('./common/info.js'){}#-->
|
<!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ layout("/layouts/platform.html"){
|
|||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('./common/info.js'){}#-->
|
<!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ layout("/layouts/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('./common/info.js'){}#-->
|
<!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ layout("/layouts/platform.html"){
|
|||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('./common/info.js'){}#-->
|
<!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ layout("/layouts/platform.html"){
|
|||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
<!--#include('./common/info.js'){}#-->
|
<!--#include('/platform/zgfw/difficulty/common/info.js'){}#-->
|
||||||
|
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
|||||||
Reference in New Issue
Block a user