feat:场地预约-面向分工会场地,1、支持协会负责人进行预约,2、预约前签字

This commit is contained in:
2026-09-30 15:35:56 +08:00
parent 593500cecc
commit c909f0a3fb
17 changed files with 1052 additions and 68 deletions
@@ -0,0 +1,58 @@
package io.v.nutz.activity.controller.site;
import cn.wizzer.framework.base.Result;
import io.v.nutz.activity.services.SiteReserveService;
import io.v.nutz.annontation.ViReturn;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.POST;
/**
* 电脑端和手机端共用的场地预约承诺书阅读、签署入口。
*/
@IocBean
@At("/platform/activity/site/commitment")
@Ok("json:full")
@RequiresAuthentication
public class SiteCommitmentController {
@Inject
private SiteReserveService siteReserveService;
/**
* 校验预约资格并记录服务器阅读开始时间。
* @param siteId 场地ID
* @return 承诺书及阅读凭证,或不可预约原因
*/
@At
@POST
@ViReturn
public Object begin(String siteId) {
try {
return siteReserveService.beginCommitment(siteId);
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 阅读十秒后签署本次承诺书。
* @param commitmentId 本次阅读凭证
* @param signature 本次手写签名PNG数据
* @return 签署结果
*/
@At
@POST
@ViReturn
public Object sign(String commitmentId, String signature) {
try {
siteReserveService.signCommitment(commitmentId, signature);
return Result.success("签署成功");
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
}
@@ -2,6 +2,10 @@ package io.v.nutz.activity.controller.site;
import io.v.nutz.activity.models.ActivitySiteInfo;
import cn.wizzer.framework.base.Result;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
import java.nio.charset.StandardCharsets;
import io.v.nutz.activity.services.SiteInfoService;
import io.v.nutz.annontation.ViReturn;
import io.v.nutz.base.utils.PageUtil;
@@ -62,16 +66,34 @@ public class SiteManageController {
return siteInfoService.listPage(pageNumber, pageSize, cnd);
}
/**
* 新增场地,面向分工会时要求配置预约前承诺书。
* @param activitySiteInfo 场地信息及承诺书标题、正文
* @return 保存结果或配置校验错误
*/
@At
@ViReturn
public Object doAdd(@Param("data") ActivitySiteInfo activitySiteInfo) {
String error = prepareCommitment(activitySiteInfo);
if (error != null) {
return Result.error(error);
}
siteInfoService.insert(activitySiteInfo);
return null;
}
/**
* 保存场地及承诺书配置,已签署申请继续保留原正文快照。
* @param activitySiteInfo 修改后的场地信息
* @return 保存结果或配置校验错误
*/
@At
@ViReturn
public Object doEdit(@Param("data") ActivitySiteInfo activitySiteInfo) {
String error = prepareCommitment(activitySiteInfo);
if (error != null) {
return Result.error(error);
}
siteInfoService.update(activitySiteInfo);
return null;
}
@@ -88,4 +110,25 @@ public class SiteManageController {
public Object findOneSite(String id) {
return siteInfoService.fetch(id);
}
// 承诺书会在预约弹窗和历史签署记录中展示,仅保留正文所需的安全HTML。
private String prepareCommitment(ActivitySiteInfo site) {
if (site.getCommitmentContent() != null) {
site.setCommitmentContent(Jsoup.clean(site.getCommitmentContent(), Whitelist.relaxed()));
}
if (Integer.valueOf(1).equals(site.getReserveTarget())) {
if (Strings.isBlank(site.getCommitmentTitle()) || Strings.isBlank(site.getCommitmentContent())
|| Strings.isBlank(Jsoup.parse(site.getCommitmentContent()).text())) {
return "面向分工会的场地必须配置承诺书标题和正文";
}
}
if (site.getCommitmentTitle() != null && site.getCommitmentTitle().length() > 200) {
return "承诺书标题不能超过200字";
}
if (site.getCommitmentContent() != null
&& site.getCommitmentContent().getBytes(StandardCharsets.UTF_8).length > 60000) {
return "承诺书正文过长,请精简内容";
}
return null;
}
}
@@ -41,6 +41,7 @@ import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import org.nutz.trans.Trans;
import java.util.ArrayList;
import java.util.Date;
@@ -161,9 +162,25 @@ public class SiteReserveController {
return fetch.getReserve_state() > stateCode;
}
/**
* 创建预约,面向分工会的场地必须先完成本次承诺书签署。
* @param activitySiteReserve 预约表单
* @param days 本次申请的预约日期
* @param commitmentId 当前用户签署的场地承诺书凭证
* @return 预约结果或资格、签署校验失败原因
*/
@At
@ViReturn
public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days) {
public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days, String commitmentId) {
if (activitySiteReserve == null || days == null || days.length == 0) {
return Result.error("请选择预约场地和日期");
}
try {
siteReserveService.checkCommitment(activitySiteReserve.getSite_id(), commitmentId);
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
Map<Boolean, String> validateSiteReserve = siteReserveService.validateSiteReserve(
activitySiteReserve.getSite_id(),
@@ -183,22 +200,31 @@ public class SiteReserveController {
Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName);
if (stateCode == null) {
return null;
return Result.error("场地尚未配置预约审核流程");
}
for (String d : days) {
activitySiteReserve.setReserve_day(d);
//查询这个人是否已经预约这个时间段
int count = siteReserveService.count("activity_site_reserve", Cnd.NEW().and("reserve_person_id", "=", ShiroUtil.getPrincipalProperty("id"))
.and("reserve_day", "=", activitySiteReserve.getReserve_day()).and("start_time", "=", activitySiteReserve.getStart_time())
.and("end_time", "=", activitySiteReserve.getEnd_time()));
if (count > 0) {
return Result.error("您已预约该时间段!");
}
try {
// 同一申请的多天预约和签署凭证一起提交,失败时不消耗签署凭证。
Trans.exec(() -> {
siteReserveService.bindCommitment(activitySiteReserve.getSite_id(), commitmentId, r);
for (String d : days) {
activitySiteReserve.setReserve_day(d);
//查询这个人是否已经预约这个时间段
int count = siteReserveService.count("activity_site_reserve", Cnd.NEW().and("reserve_person_id", "=", ShiroUtil.getPrincipalProperty("id"))
.and("reserve_day", "=", activitySiteReserve.getReserve_day()).and("start_time", "=", activitySiteReserve.getStart_time())
.and("end_time", "=", activitySiteReserve.getEnd_time()));
if (count > 0) {
throw new IllegalArgumentException("您已预约该时间段!");
}
activitySiteReserve.setReserve_state(stateCode);
activitySiteReserve.setReserve_person_id(ShiroUtil.getPrincipalProperty("id").toString());
siteReserveService.insert(activitySiteReserve);
activitySiteReserve.setReserve_state(stateCode);
activitySiteReserve.setReserve_person_id(ShiroUtil.getPrincipalProperty("id").toString());
activitySiteReserve.setId(R.UU32());
siteReserveService.insert(activitySiteReserve);
}
});
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
//发送微信提醒审核人,查询状态码对应的审核人
@@ -307,10 +333,18 @@ public class SiteReserveController {
return siteReserveService.count(cnd);
}
/**
* 查询预约详情及本次申请签署的承诺书快照。
* @param id 预约记录ID
* @return 预约详情、审核记录和承诺书签署信息
*/
@At
@ViReturn
public Object findOne(String id) {
ActivitySiteReserve fetch = siteReserveService.fetch(id);
if (fetch == null) {
return Result.error("预约记录不存在");
}
Sql sql = Sqls.create("""
SELECT
asr.*,
@@ -386,6 +420,7 @@ public class SiteReserveController {
i.setCondition(cnd);
List<Record> list1 = siteReserveService.list(i);
record.set("site_info", list1);
record.set("commitment", siteReserveService.findCommitment(fetch.getSqid()));
return record;
}
@@ -455,6 +490,13 @@ public class SiteReserveController {
return result;
}
/**
* 获取指定日期的场地预约时段,支持以24:00作为营业结束时间。
*
* @param siteId 场地ID
* @param day 预约日期,格式为yyyy-MM-dd
* @return 预约时段及不可预约状态,保留配置的结束时间用于匹配预约记录
*/
@At
@ViReturn
public Object getTimesByDay(String siteId, String day) {
@@ -480,16 +522,19 @@ public class SiteReserveController {
} else {
// 构造起始时间
DateTime start = DateUtil.parse(startTime, "HH:mm"); // "09:00"
DateTime end = DateUtil.parse(endTime, "HH:mm"); // "17:00"
// 场地允许营业至24:00,按次日零点计算,返回时保留24:00以匹配预约记录。
DateTime end = "24:00".equals(endTime)
? DateUtil.offsetDay(DateUtil.parse("00:00", "HH:mm"), 1)
: DateUtil.parse(endTime, "HH:mm");
DateTime current = start;
while (DateUtil.compare(current, end) < 0) { // 当前时间 <= 结束时间
DateTime next = DateUtil.offsetSecond(current, (int) (timeUnit * 3600));
// 如果超过结束时间,则使用 endTime
if (DateUtil.compare(next, end) > 0) {
result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", DateUtil.format(end, "HH:mm")));
result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", endTime));
break;
} else {
result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", DateUtil.format(next, "HH:mm")));
result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", DateUtil.compare(next, end) == 0 ? endTime : DateUtil.format(next, "HH:mm")));
}
current = next;
}
@@ -0,0 +1,60 @@
package io.v.nutz.activity.models;
import lombok.Data;
import org.nutz.dao.entity.annotation.*;
/**
* 场地预约承诺书阅读、签署及申请关联记录,保留签署时的正文快照。
*/
@Data
@Table("activity_site_commitment")
public class ActivitySiteCommitment {
@Name
@Column
@ColDefine(type = ColType.VARCHAR, width = 32)
private String id;
@Column
@Comment("场地ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String siteId;
@Column
@Comment("签署人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("签署人姓名快照")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String userName;
@Column
@Comment("承诺书标题快照")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String title;
@Column
@Comment("承诺书正文快照")
@ColDefine(type = ColType.TEXT)
private String content;
@Column
@Comment("服务器记录的阅读开始时间,毫秒")
private Long readAt;
@Column
@Comment("服务器记录的签署时间,毫秒")
private Long signedAt;
@Column
@Comment("本次手写签名PNG数据")
@ColDefine(type = ColType.TEXT)
private String signature;
@Column
@Comment("预约申请编号,未提交时为空")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String sqid;
}
@@ -83,6 +83,16 @@ public class ActivitySiteInfo {
@ColDefine(type = ColType.TEXT)
private String introduce;
@Column
@Comment("预约前承诺书标题")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String commitmentTitle;
@Column
@Comment("预约前承诺书正文")
@ColDefine(type = ColType.TEXT)
private String commitmentContent;
@Column
@Comment("可容纳人数")
@ColDefine(type = ColType.INT)
@@ -2,11 +2,53 @@ package io.v.nutz.activity.services;
import io.v.nutz.activity.models.ActivitySiteReserve;
import io.v.nutz.activity.models.ActivitySiteCommitment;
import io.v.nutz.base.service.BaseService;
import org.nutz.lang.util.NutMap;
import java.util.Map;
public interface SiteReserveService extends BaseService<ActivitySiteReserve> {
Map<Boolean, String> validateSiteReserve(String siteId, String day, String startTime, String endTime, Integer joinCount);
/**
* 校验当前用户的预约资格并开始本次承诺书阅读。
* @param siteId 预约场地ID
* @return 是否需要签署及本次承诺书快照、凭证
* @throws IllegalArgumentException 无预约资格或场地未配置承诺书
*/
NutMap beginCommitment(String siteId);
/**
* 阅读满十秒后保存当前用户本次手写签名。
* @param commitmentId 本次阅读凭证
* @param signature PNG格式的签名数据
* @throws IllegalArgumentException 阅读不足十秒、凭证失效或签名无效
*/
void signCommitment(String commitmentId, String signature);
/**
* 预约提交前校验身份、承诺书版本和本次签署状态。
* @param siteId 预约场地ID
* @param commitmentId 签署凭证,非分工会场地可为空
* @throws IllegalArgumentException 预约资格或签署条件不满足
*/
void checkCommitment(String siteId, String commitmentId);
/**
* 在预约保存事务中一次性关联签署凭证,失败时随预约一起回滚。
* @param siteId 预约场地ID
* @param commitmentId 签署凭证
* @param sqid 本次预约申请编号
* @throws IllegalArgumentException 凭证失效或已被其他申请使用
*/
void bindCommitment(String siteId, String commitmentId, String sqid);
/**
* 查询申请签署时的承诺书,供已有预约详情展示。
* @param sqid 预约申请编号
* @return 签署快照,历史未签署申请返回null
*/
ActivitySiteCommitment findCommitment(String sqid);
}
@@ -2,6 +2,7 @@ package io.v.nutz.activity.services.impl;
import io.v.nutz.activity.models.ActivitySiteInfo;
import io.v.nutz.activity.models.ActivitySiteCommitment;
import io.v.nutz.activity.models.ActivitySiteReserve;
import io.v.nutz.activity.models.ActivityType;
import io.v.nutz.activity.services.SiteReserveService;
@@ -9,8 +10,20 @@ import io.v.nutz.base.service.impl.BaseServiceImpl;
import io.v.nutz.constant.AuditTypeEnum;
import io.v.nutz.modules.models.AuditState;
import org.nutz.dao.Cnd;
import org.nutz.dao.Chain;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import org.apache.shiro.SecurityUtils;
import io.v.nutz.sys.models.Sys_user;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.util.Base64;
import java.util.Objects;
import java.util.List;
import java.util.Map;
@@ -28,6 +41,138 @@ public class SiteReserveServiceImpl extends BaseServiceImpl<ActivitySiteReserve>
super(dao);
}
private ActivitySiteInfo commitmentSite(String siteId) {
ActivitySiteInfo site = Strings.isBlank(siteId) ? null : dao().fetch(ActivitySiteInfo.class, siteId);
if (site == null || !Boolean.TRUE.equals(site.getState())) {
throw new IllegalArgumentException("场地不存在或已关闭预约");
}
if (!SecurityUtils.getSubject().isAuthenticated()) {
throw new IllegalArgumentException("请先登录后预约");
}
if (Integer.valueOf(1).equals(site.getReserveTarget())
&& !SecurityUtils.getSubject().hasRole("sysadmin")
&& !SecurityUtils.getSubject().hasRole("gh01")
&& !SecurityUtils.getSubject().hasRole("H04")
&& !SecurityUtils.getSubject().hasRole("H02")) {
throw new IllegalArgumentException("该场地仅限分工会预约人员或协会负责人预约");
}
return site;
}
private ActivitySiteCommitment currentCommitment(String commitmentId) {
ActivitySiteCommitment record = Strings.isBlank(commitmentId) ? null
: dao().fetch(ActivitySiteCommitment.class, commitmentId);
Sys_user user = (Sys_user) SecurityUtils.getSubject().getPrincipal();
if (user == null || record == null || !user.getId().equals(record.getUserId())
|| record.getReadAt() == null || System.currentTimeMillis() - record.getReadAt() > 30 * 60 * 1000L
|| Strings.isNotBlank(record.getSqid())) {
throw new IllegalArgumentException("签署凭证已失效,请重新阅读并签署承诺书");
}
ActivitySiteInfo site = commitmentSite(record.getSiteId());
if (!Objects.equals(site.getCommitmentTitle(), record.getTitle())
|| !Objects.equals(site.getCommitmentContent(), record.getContent())) {
throw new IllegalArgumentException("承诺书内容已更新,请重新阅读并签署");
}
return record;
}
/** {@inheritDoc} */
@Override
public NutMap beginCommitment(String siteId) {
ActivitySiteInfo site = commitmentSite(siteId);
if (!Integer.valueOf(1).equals(site.getReserveTarget())) {
return NutMap.NEW().addv("required", false);
}
if (Strings.isBlank(site.getCommitmentTitle()) || Strings.isBlank(site.getCommitmentContent())) {
throw new IllegalArgumentException("场地尚未配置承诺书,暂不能预约,请联系场地管理员");
}
Sys_user user = (Sys_user) SecurityUtils.getSubject().getPrincipal();
ActivitySiteCommitment record = new ActivitySiteCommitment();
record.setId(R.UU32());
record.setSiteId(siteId);
record.setUserId(user.getId());
record.setUserName(user.getUsername());
record.setTitle(site.getCommitmentTitle());
record.setContent(site.getCommitmentContent());
record.setReadAt(System.currentTimeMillis());
dao().insert(record);
return NutMap.NEW().addv("required", true).addv("record", record).addv("seconds", 10);
}
/** {@inheritDoc} */
@Override
public void signCommitment(String commitmentId, String signature) {
ActivitySiteCommitment record = currentCommitment(commitmentId);
if (System.currentTimeMillis() - record.getReadAt() < 10000) {
throw new IllegalArgumentException("请阅读承诺书满10秒后再签字确认");
}
validateSignature(signature);
int changed = dao().update(ActivitySiteCommitment.class,
Chain.make("signature", signature).add("signedAt", System.currentTimeMillis()),
Cnd.where("id", "=", record.getId()).and("signedAt", "is", null).and("sqid", "is", null));
if (changed != 1) {
throw new IllegalArgumentException("本次签署已完成,请勿重复提交");
}
}
// 只接受有实际笔迹的PNG,避免空白画布被当成已签署承诺书。
private void validateSignature(String signature) {
if (signature == null || signature.length() > 60000 || !signature.startsWith("data:image/png;base64,")) {
throw new IllegalArgumentException("请填写有效的手写签名");
}
try {
byte[] bytes = Base64.getDecoder().decode(signature.substring("data:image/png;base64,".length()));
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
if (image != null && image.getWidth() <= 1600 && image.getHeight() <= 800) {
int ink = 0;
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int pixel = image.getRGB(x, y);
if ((pixel >>> 24) > 128 && (pixel & 0x00ffffff) < 0xdddddd && ++ink >= 20) {
return;
}
}
}
}
} catch (Exception e) {
throw new IllegalArgumentException("签名数据无效,请重新签字");
}
throw new IllegalArgumentException("签名不能为空,请在签字区域手写签名");
}
/** {@inheritDoc} */
@Override
public void checkCommitment(String siteId, String commitmentId) {
ActivitySiteInfo site = commitmentSite(siteId);
if (Integer.valueOf(1).equals(site.getReserveTarget())) {
ActivitySiteCommitment record = currentCommitment(commitmentId);
if (!siteId.equals(record.getSiteId()) || record.getSignedAt() == null
|| record.getSignedAt() - record.getReadAt() < 10000 || Strings.isBlank(record.getSignature())) {
throw new IllegalArgumentException("请先阅读并签署当前场地的承诺书");
}
}
}
/** {@inheritDoc} */
@Override
public void bindCommitment(String siteId, String commitmentId, String sqid) {
checkCommitment(siteId, commitmentId);
ActivitySiteInfo site = dao().fetch(ActivitySiteInfo.class, siteId);
if (Integer.valueOf(1).equals(site.getReserveTarget())) {
int changed = dao().update(ActivitySiteCommitment.class, Chain.make("sqid", sqid),
Cnd.where("id", "=", commitmentId).and("sqid", "is", null));
if (changed != 1) {
throw new IllegalArgumentException("签署凭证已用于其他申请,请重新签署");
}
}
}
/** {@inheritDoc} */
@Override
public ActivitySiteCommitment findCommitment(String sqid) {
return Strings.isBlank(sqid) ? null : dao().fetch(ActivitySiteCommitment.class, Cnd.where("sqid", "=", sqid));
}
@Override
public Map<Boolean, String> validateSiteReserve(String siteId, String day, String startTime, String endTime, Integer joinCount) {
ActivitySiteInfo siteInfo = dao().fetch(ActivitySiteInfo.class, siteId);
@@ -31,6 +31,7 @@ import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import org.nutz.trans.Trans;
import java.util.ArrayList;
import java.util.List;
@@ -234,10 +235,26 @@ public class SiteInfoMobileController {
return hours;
}
/**
* 手机端预约入口,使用与电脑端相同的预约资格和承诺书校验。
* @param siteId 场地ID
* @param times 预约日期及时段JSON
* @param message 预约事由
* @param joinUser 参与人员
* @param joinCount 预约人数
* @param commitmentId 当前用户的本次承诺书签署凭证
* @return 预约结果或签署校验失败原因
*/
@At("/reserveDo")
@Ok("json:full")
@ViReturn
public Object reserveDo(String siteId, String times, String message, String joinUser, Integer joinCount) {
public Object reserveDo(String siteId, String times, String message, String joinUser, Integer joinCount, String commitmentId) {
try {
siteReserveService.checkCommitment(siteId, commitmentId);
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
String moduleName = activityCommonService.findModuleNameByActivityId(siteId);
Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName);
@@ -272,7 +289,14 @@ public class SiteInfoMobileController {
}
str = str.substring(0, str.length() - 1);
siteReserveService.insert(li);
try {
Trans.exec(() -> {
siteReserveService.bindCommitment(siteId, commitmentId, r);
siteReserveService.insert(li);
});
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
//发送微信提醒审核人
//查询状态码对应的审核人
@@ -0,0 +1,38 @@
-- 场地预约承诺书:在已选定的业务数据库中执行,不包含历史数据修改。
-- 项目启动时可能已通过实体自动建表、补列,因此跳过已存在的表和字段。
CREATE TABLE IF NOT EXISTS `activity_site_commitment` (
`id` VARCHAR(32) NOT NULL,
`siteId` VARCHAR(32) DEFAULT NULL COMMENT '场地ID',
`userId` VARCHAR(32) DEFAULT NULL COMMENT '签署人ID',
`userName` VARCHAR(120) DEFAULT NULL COMMENT '签署人姓名快照',
`title` VARCHAR(200) DEFAULT NULL COMMENT '承诺书标题快照',
`content` TEXT COMMENT '承诺书正文快照',
`readAt` BIGINT DEFAULT NULL COMMENT '服务器记录的阅读开始时间,毫秒',
`signedAt` BIGINT DEFAULT NULL COMMENT '服务器记录的签署时间,毫秒',
`signature` TEXT COMMENT '本次手写签名PNG数据',
`sqid` VARCHAR(32) DEFAULT NULL COMMENT '预约申请编号,未提交时为空',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场地预约承诺书签署记录';
-- 仅补充缺失的场地配置列,保留已经配置的承诺书及已有字段定义。
SET @site_commitment_ddl = IF(
EXISTS(SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activity_site_info'
AND COLUMN_NAME = 'commitmentTitle'),
'SELECT ''commitmentTitle already exists'' AS message',
'ALTER TABLE `activity_site_info` ADD COLUMN `commitmentTitle` VARCHAR(200) DEFAULT NULL COMMENT ''预约前承诺书标题'''
);
PREPARE site_commitment_stmt FROM @site_commitment_ddl;
EXECUTE site_commitment_stmt;
DEALLOCATE PREPARE site_commitment_stmt;
SET @site_commitment_ddl = IF(
EXISTS(SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activity_site_info'
AND COLUMN_NAME = 'commitmentContent'),
'SELECT ''commitmentContent already exists'' AS message',
'ALTER TABLE `activity_site_info` ADD COLUMN `commitmentContent` TEXT COMMENT ''预约前承诺书正文'''
);
PREPARE site_commitment_stmt FROM @site_commitment_ddl;
EXECUTE site_commitment_stmt;
DEALLOCATE PREPARE site_commitment_stmt;
@@ -56,6 +56,17 @@
</el-tab-pane>
<el-tab-pane label="预约承诺书" name="commitment">
<template v-if="viewData.commitment">
<h3>{{ viewData.commitment.title }}</h3>
<div v-html="viewData.commitment.content"></div>
<p>签署人:{{ viewData.commitment.userName }}</p>
<p>签署时间:{{ commitmentTime(viewData.commitment.signedAt) }}</p>
<img :src="viewData.commitment.signature" alt="本次预约手写签名" style="max-width: 100%; max-height: 180px">
</template>
<p v-else>本次预约无承诺书签署记录</p>
</el-tab-pane>
<el-tab-pane label="场地管理员批示信息" name="3" v-if="viewData.site_manager_id&&viewData.back_out_opinion">
<el-form label-width="130px" style="padding: 20px 0">
@@ -166,6 +177,9 @@ module.exports = {
}
},
methods: {
commitmentTime(value) {
return moment(value).format('YYYY-MM-DD HH:mm:ss')
},
hasPane(name) {
if (!this.handle) {
return true
@@ -0,0 +1,219 @@
<template>
<div v-if="visible" class="site-commitment-mask" role="dialog" aria-modal="true" :aria-label="record.title || '预约承诺书'">
<section class="site-commitment-dialog">
<h3>{{ record.title || '预约承诺书' }}</h3>
<div v-if="record.content" class="site-commitment-content" v-html="record.content"></div>
<template v-if="record.id && !readOnly">
<p>签署人:{{ record.userName }} <span aria-live="polite">{{ remaining > 0 ? '请阅读(' + remaining + '秒)' : '阅读时间已满,请在下方手写签字' }}</span></p>
<canvas ref="canvas" width="900" height="260" class="site-commitment-canvas"
:class="{'site-commitment-disabled': remaining > 0}"
@pointerdown="startStroke" @pointermove="drawStroke"
@pointerup="endStroke" @pointercancel="endStroke" @lostpointercapture="endStroke"></canvas>
<div class="site-commitment-actions">
<button type="button" :disabled="remaining > 0 || busy" @click="clearSignature">清空签名</button>
<label><input type="checkbox" v-model="agreed" :disabled="remaining > 0 || busy">我已阅读并承诺遵守上述内容</label>
</div>
</template>
<template v-if="record.id && readOnly">
<p>签署人:{{ record.userName }}</p>
<p>签署时间:{{ signedTime(record.signedAt) }}</p>
<img :src="record.signature" alt="本次预约手写签名" style="max-width: 100%; max-height: 180px">
</template>
<p v-if="error" class="site-commitment-error" role="alert">{{ error }}</p>
<footer>
<button type="button" :disabled="busy" @click="cancel">{{ readOnly ? '关闭' : '取消预约' }}</button>
<button v-if="record.id && !readOnly" type="button" class="site-commitment-confirm"
:disabled="remaining > 0 || !hasInk || !agreed || busy" @click="submit">
{{ busy ? '签署中…' : '签字确认,继续预约' }}
</button>
</footer>
</section>
</div>
</template>
<script>
// 电脑端和H5共用本次阅读、手写签署弹窗,不读取或复用历史签名。
module.exports = {
data() {
return {
visible: false,
readOnly: false,
record: {},
remaining: 10,
readMillis: 0,
lastTick: 0,
busy: false,
agreed: false,
hasInk: false,
drawing: false,
error: ''
}
},
methods: {
request(action, data) {
return new Promise((resolve, reject) => {
$.ajax({
url: action === 'detail' ? '/platform/activity/site/reserve/findOne' : '/platform/activity/site/commitment/' + action,
type: 'POST', data, dataType: 'json',
success: resolve,
error: () => reject(new Error('承诺书请求失败,请稍后重新进入预约'))
})
})
},
async open(siteId) {
this.cancel()
this.readOnly = false
this.record = {}
this.error = ''
this.agreed = false
this.hasInk = false
this.remaining = 10
this.readMillis = 0
try {
const res = await this.request('begin', {siteId})
if (this._isDestroyed) return null
if (res.code !== 0 || !res.data) throw new Error(res.msg || '无法获取承诺书')
if (!res.data.required) return {id: ''}
this.record = res.data.record
} catch (e) {
if (this._isDestroyed) return null
this.error = e.message
}
this.visible = true
const result = new Promise(resolve => { this.resolveReading = resolve })
await this.$nextTick()
if (this.record.id) {
this.clearSignature()
this.lastTick = Date.now()
// 前端只累计页面可见时的阅读时间;服务器另外校验至少十秒。
this.readTimer = setInterval(() => {
const now = Date.now()
if (!document.hidden) this.readMillis += Math.min(now - this.lastTick, 1000)
this.lastTick = now
this.remaining = Math.max(0, Math.ceil((10000 - this.readMillis) / 1000))
if (this.remaining === 0) clearInterval(this.readTimer)
}, 200)
}
return result
},
async view(reserveId) {
this.cancel()
this.readOnly = true
this.record = {}
this.error = ''
this.visible = true
this.busy = true
try {
const res = await this.request('detail', {id: reserveId})
if (res.code !== 0 || !res.data) throw new Error(res.msg || '获取签署记录失败')
this.record = res.data.commitment || {}
if (!this.record.id) this.error = '本次预约无承诺书签署记录'
} catch (e) {
this.error = e.message
} finally {
this.busy = false
}
},
signedTime(value) {
return moment(value).format('YYYY-MM-DD HH:mm:ss')
},
clearSignature() {
const canvas = this.$refs.canvas
if (!canvas) return
const context = canvas.getContext('2d')
context.clearRect(0, 0, canvas.width, canvas.height)
context.lineWidth = 4
context.lineCap = 'round'
context.lineJoin = 'round'
context.strokeStyle = '#222222'
this.hasInk = false
this.drawing = false
},
point(event) {
const canvas = this.$refs.canvas
const rect = canvas.getBoundingClientRect()
return {x: (event.clientX - rect.left) * canvas.width / rect.width,
y: (event.clientY - rect.top) * canvas.height / rect.height}
},
startStroke(event) {
if (this.remaining > 0 || this.busy || (event.pointerType === 'mouse' && event.button !== 0)) return
event.preventDefault()
const canvas = this.$refs.canvas
canvas.setPointerCapture(event.pointerId)
this.strokePointer = event.pointerId
this.lastPoint = this.point(event)
this.drawing = true
},
drawStroke(event) {
if (!this.drawing || this.busy || event.pointerId !== this.strokePointer) return
event.preventDefault()
const point = this.point(event)
const context = this.$refs.canvas.getContext('2d')
context.beginPath()
context.moveTo(this.lastPoint.x, this.lastPoint.y)
context.lineTo(point.x, point.y)
context.stroke()
this.lastPoint = point
this.hasInk = true
},
endStroke(event) {
if (event.pointerId === this.strokePointer) this.drawing = false
},
async submit() {
if (this.remaining > 0 || !this.hasInk || !this.agreed || this.busy) return
this.busy = true
this.error = ''
try {
const res = await this.request('sign', {
commitmentId: this.record.id,
signature: this.$refs.canvas.toDataURL('image/png')
})
if (res.code !== 0) throw new Error(res.msg || '签署失败,请重新进入预约')
const resolve = this.resolveReading
this.resolveReading = null
this.visible = false
clearInterval(this.readTimer)
if (resolve) resolve({id: this.record.id})
} catch (e) {
this.error = e.message
} finally {
this.busy = false
}
},
cancel() {
clearInterval(this.readTimer)
this.visible = false
this.drawing = false
if (this.resolveReading) this.resolveReading(null)
this.resolveReading = null
},
visibilityChanged() {
this.lastTick = Date.now()
}
},
mounted() {
document.addEventListener('visibilitychange', this.visibilityChanged)
this.$nextTick(() => this.$emit('ready'))
},
beforeDestroy() {
this.cancel()
document.removeEventListener('visibilitychange', this.visibilityChanged)
}
}
</script>
<style scoped>
.site-commitment-mask { position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; }
.site-commitment-dialog { background: #fff; color: #303133; width: 760px; max-width: 94vw; max-height: 94vh; overflow: auto; box-sizing: border-box; padding: 20px; border-radius: 8px; font-size: 14px; }
.site-commitment-dialog h3 { margin: 0 0 16px; text-align: center; }
.site-commitment-content { max-height: 34vh; overflow: auto; border: 1px solid #ebeef5; padding: 12px; line-height: 1.8; overflow-wrap: anywhere; }
.site-commitment-content img { max-width: 100%; }
.site-commitment-canvas { display: block; width: 100%; height: 150px; border: 1px dashed #909399; background: #fff; touch-action: none; box-sizing: border-box; }
.site-commitment-disabled { background: #f5f7fa; }
.site-commitment-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 10px; }
.site-commitment-dialog footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
.site-commitment-dialog button { border: 1px solid #dcdfe6; border-radius: 4px; background: #fff; color: #303133; padding: 9px 12px; cursor: pointer; }
.site-commitment-dialog .site-commitment-confirm { background: #1867b0; color: white; }
.site-commitment-dialog button:disabled { opacity: .5; cursor: not-allowed; }
.site-commitment-error { color: #d93025; }
</style>
@@ -185,6 +185,7 @@ layout("/mobile/platform.html"){
</style>
<div id="app" v-cloak>
<site-commitment ref="commitmentRecord"></site-commitment>
<m-page-loading v-if="mLoading"></m-page-loading>
@@ -357,6 +358,8 @@ layout("/mobile/platform.html"){
</van-cell>
</van-cell-group>
</van-checkbox-group>
<van-button @click.stop="$refs.commitmentRecord.view(item.id)" size="small" type="primary"
style="position: relative; z-index: 2; width: auto; margin-top: 8px">查看承诺书</van-button>
</div>
<!--<div v-if="queryType === 'notAudit'" style="display: flex; justify-content: space-evenly; position: fixed; bottom: 50px; width: 95%">
<van-button @click="audit('all', 'reject')" color="#ff976a" size="small" style="height: 38px">一键全部驳回</van-button>
@@ -399,6 +402,9 @@ layout("/mobile/platform.html"){
const vue = new Vue({
el: '#app',
mixins: [mobileMixins],
components: {
'site-commitment': httpVueLoader('/components/activity/site/SiteCommitment.vue'),
},
data() {
return {
desc: '',
@@ -546,4 +552,4 @@ layout("/mobile/platform.html"){
<!--#
}
#-->
#-->
@@ -291,8 +291,8 @@ layout("/mobile/platform.html"){
this.show = true
},
openView(o) {
if(o.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') {
vant.Toast('该场地只能分工会主席预约')
if(o.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04', 'H02')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') {
vant.Toast('该场地仅限分工会预约人员或协会负责人预约')
return
}
this.siteId = o.id
@@ -155,12 +155,16 @@ layout("/mobile/platform.html"){
right: 0;
top: 10px;
}
.reservation-commitment-content img {
max-width: 100%;
}
.van-cell__title {
font-size: 16px;
}
</style>
<div id="app" v-cloak>
<site-commitment ref="commitmentRecord"></site-commitment>
<m-page-loading v-if="mLoading"></m-page-loading>
@@ -217,7 +221,7 @@ layout("/mobile/platform.html"){
@load="onLoad">
<div class="van-doc-card" v-for="o in tableData">
<div @click="openView(o.id, o.modulename)">
<div @click="openView(o.id)">
<div style="display: flex; justify-content: space-between">
<div style="width: 74%">
@@ -274,6 +278,12 @@ layout("/mobile/platform.html"){
<van-button @click.stop="backOption(o)" class="cus_button"
type="primary" size="small" color="#1867b0"
style="margin-right: 8px; width: auto;">反馈意见</van-button>
<van-button @click.stop="openView(o.id)" class="cus_button"
type="primary" size="small" color="#1867b0"
style="margin-right: 8px; width: auto;">预约详情</van-button>
<van-button @click.stop="$refs.commitmentRecord.view(o.id)" class="cus_button"
type="primary" size="small" color="#1867b0"
style="width: auto;">查看承诺书</van-button>
</div>
</div>
</div>
@@ -288,6 +298,32 @@ layout("/mobile/platform.html"){
</div>
<van-action-sheet v-model="detailShow" title="预约详情">
<div style="padding: 16px; max-height: 75vh; overflow-y: auto; word-break: break-word">
<p v-if="detailLoading">正在加载预约信息…</p>
<p v-else-if="detailError">{{detailError}}</p>
<template v-else>
<van-cell title="场地名称" :value="reserveDetail.site_name"></van-cell>
<van-cell title="预约人" :value="reserveDetail.reserve_person"></van-cell>
<van-cell title="所属单位" :value="reserveDetail.reserve_person_unit"></van-cell>
<van-cell title="联系电话" :value="reserveDetail.reserve_person_phone"></van-cell>
<van-cell title="预约日期" :value="reserveDetail.concat_day"></van-cell>
<van-cell title="预约时间" :value="reserveDetail.start_time + ' - ' + reserveDetail.end_time"></van-cell>
<van-cell title="预约状态" :value="reserveDetail.state_name"></van-cell>
<van-cell title="预约事由" :value="reserveDetail.reserve_cause || '无'"></van-cell>
<van-divider>承诺书签署信息</van-divider>
<template v-if="reserveDetail.commitment">
<h3>{{reserveDetail.commitment.title}}</h3>
<div class="reservation-commitment-content" v-html="reserveDetail.commitment.content"></div>
<p>签署人:{{reserveDetail.commitment.userName}}</p>
<p>签署时间:{{moment(reserveDetail.commitment.signedAt).format('YYYY-MM-DD HH:mm:ss')}}</p>
<img :src="reserveDetail.commitment.signature" alt="本次预约手写签名" style="max-width: 100%; max-height: 180px">
</template>
<p v-else>本次预约无承诺书签署记录</p>
</template>
</div>
</van-action-sheet>
<van-dialog v-model="show" title="反馈意见" show-cancel-button @confirm="backDo">
<van-field
v-model="option"
@@ -308,6 +344,9 @@ layout("/mobile/platform.html"){
const vue = new Vue({
el: '#app',
mixins: [mobileMixins],
components: {
'site-commitment': httpVueLoader('/components/activity/site/SiteCommitment.vue'),
},
data() {
return {
time: new Date(),
@@ -317,9 +356,32 @@ layout("/mobile/platform.html"){
typeList: [],
timeList: [],
info: {},
detailShow: false,
detailLoading: false,
detailError: '',
reserveDetail: {},
}
},
methods: {
// 从预约详情接口读取本次申请及已入库的签署快照,不使用当前场地配置替代历史正文。
async openView(id) {
if (this.detailLoading) return
this.detailShow = true
this.detailLoading = true
this.detailError = ''
this.reserveDetail = {}
try {
const res = await $.post('/platform/activity/site/reserve/findOne', {id})
if (res.code !== 0 || !res.data) {
throw new Error(res.msg || '获取预约详情失败')
}
this.reserveDetail = res.data
} catch (e) {
this.detailError = e.message || '获取预约详情失败,请稍后重试'
} finally {
this.detailLoading = false
}
},
onConfirm() {
if(this.pageForm.timeSwitch === true) {
this.timeList = [{text: '全部', value: '全部'}]
@@ -423,4 +485,4 @@ layout("/mobile/platform.html"){
<!--#
}
#-->
#-->
@@ -3,6 +3,112 @@ layout("/mobile/platform.html"){
#-->
<style>
.reserve-entry {
min-height: 100vh;
box-sizing: border-box;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
background: linear-gradient(180deg, #eaf3fc 0, #f6f7f9 360px);
color: #26384b;
}
.reserve-entry .van-nav-bar {
background: transparent;
}
.reserve-entry .van-nav-bar__title {
color: #26384b;
font-weight: 600;
}
.reserve-entry .van-nav-bar .van-icon {
color: #246fb4;
}
.reserve-entry-main {
max-width: 480px;
margin: 0 auto;
padding: 36px 20px 0;
}
.reserve-entry-hero {
text-align: center;
padding: 0 12px 28px;
}
.reserve-entry-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 76px;
height: 76px;
border-radius: 24px;
background: #fff;
color: #246fb4;
box-shadow: 0 8px 24px rgba(36, 111, 180, .10);
}
.reserve-entry-hero h1 {
margin: 20px 0 10px;
font-size: 24px;
line-height: 1.4;
font-weight: 600;
}
.reserve-entry-hero p {
margin: 0;
color: #768699;
font-size: 14px;
line-height: 1.7;
}
.reserve-entry-card {
padding: 24px 20px;
background: #fff;
border: 1px solid #edf1f6;
border-radius: 18px;
box-shadow: 0 6px 24px rgba(38, 56, 75, .04);
}
.reserve-entry-steps {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 24px;
}
.reserve-entry-step {
text-align: center;
font-size: 13px;
line-height: 1.6;
}
.reserve-entry-step span {
display: block;
width: 28px;
line-height: 28px;
margin: 0 auto 8px;
border-radius: 50%;
background: #edf5fd;
color: #246fb4;
font-weight: 600;
}
.reserve-entry-steps > .van-icon {
color: #bdcad8;
margin-top: -24px;
}
.reserve-entry-note {
margin: 0 0 22px;
padding: 14px;
background: #f5f8fc;
border-radius: 10px;
color: #64758a;
font-size: 13px;
line-height: 1.8;
}
.reserve-entry-start.van-button {
height: 46px;
border-radius: 10px;
font-size: 16px;
font-weight: 500;
}
.reserve-entry-back {
display: block;
width: fit-content;
margin: 16px auto 0;
padding: 12px 18px;
color: #64758a;
font-size: 14px;
line-height: 20px;
}
.my_calendar {
width: 100%;
height: 100px;
@@ -115,7 +221,33 @@ layout("/mobile/platform.html"){
}
</style>
<div id="app" v-cloak v-if="isLoad">
<div id="app" v-cloak>
<site-commitment ref="commitment" @ready="initializeReservation"></site-commitment>
<div v-if="!isLoad && !commitmentOpening" class="reserve-entry">
<van-nav-bar title="场地预约" left-arrow safe-area-inset-top
@click-left="location.href = '/mobile/activity/site/info'"></van-nav-bar>
<main class="reserve-entry-main">
<div class="reserve-entry-hero">
<div class="reserve-entry-icon" aria-hidden="true"><van-icon name="calendar-o" size="36"></van-icon></div>
<h1>活动场地预约</h1>
<p>选择合适的时间,为活动做好准备</p>
</div>
<section class="reserve-entry-card" aria-label="预约流程">
<div class="reserve-entry-steps">
<div class="reserve-entry-step"><span>1</span>阅读须知</div>
<van-icon name="arrow" aria-hidden="true"></van-icon>
<div class="reserve-entry-step"><span>2</span>选择时段</div>
<van-icon name="arrow" aria-hidden="true"></van-icon>
<div class="reserve-entry-step"><span>3</span>提交预约</div>
</div>
<p class="reserve-entry-note">如场地要求签署承诺书,请阅读满10秒并完成手写签名,再选择预约日期和时间。</p>
<van-button class="reserve-entry-start" type="info" color="#246fb4" block
:loading="commitmentOpening" @click="initializeReservation">进入场地预约</van-button>
</section>
<a class="reserve-entry-back" href="/mobile/activity/site/info">返回场地列表</a>
</main>
</div>
<template v-if="isLoad">
<van-nav-bar
:title="site.name"
left-arrow
@@ -222,9 +354,10 @@ layout("/mobile/platform.html"){
<van-button @click="formData.message = '';show = false" color="#246fb4" style="width: 47%" size="small" plain
type="info">关闭
</van-button>
<van-button @click="reserveDo" color="#246fb4" style="width: 47%" size="small" type="info">确认</van-button>
<van-button @click="reserveDo" :loading="reserveSubmitting" color="#246fb4" style="width: 47%" size="small" type="info">确认</van-button>
</div>
</van-action-sheet>
</template>
</div>
<script>
@@ -241,6 +374,9 @@ layout("/mobile/platform.html"){
data() {
return {
isLoad: false,
commitmentId: '',
commitmentOpening: false,
reserveSubmitting: false,
formData: {},
show: false,
site_id: '',
@@ -256,6 +392,9 @@ layout("/mobile/platform.html"){
allowDayList: [],
}
},
components: {
'site-commitment': httpVueLoader('/components/activity/site/SiteCommitment.vue'),
},
computed: {
getWeekTextByDate() {
return (date) => {
@@ -356,25 +495,40 @@ layout("/mobile/platform.html"){
this.show = true
},
async reserveDo() {
if (this.reserveSubmitting || !this.isLoad) return
if (this.times.length === 0) {
vant.Toast('请选择预约时段')
return
}
this.$set(this.formData, 'start_time', this.times[0].start_time)
this.$set(this.formData, 'end_time', this.times[0].end_time)
this.$set(this.formData, 'site_id', this.site_id)
const resp = await $.post('/platform/activity/site/reserve/doAdd', {
data: JSON.stringify(this.formData),
days: JSON.stringify(this.times.map(o => o.day))
})
if (resp.code === 0) {
vant.Toast('预约成功')
this.reserveSubmitting = true
try {
const resp = await $.post('/platform/activity/site/reserve/doAdd', {
data: JSON.stringify(this.formData),
days: JSON.stringify(this.times.map(o => o.day)),
commitmentId: this.commitmentId
})
if (resp.code === 0) {
vant.Toast('预约成功')
this.show = false
this.times = []
this.message = null
if (this.site.reserveTarget === 1) {
this.commitmentId = ''
this.isLoad = false
}
await this.getMyReservation()
await this.getReserve()
await this.onLoad()
} else {
vant.Toast(resp.msg)
}
} else {
vant.Toast(resp.msg)
} catch (e) {
vant.Toast('预约提交失败,请稍后重试')
} finally {
this.reserveSubmitting = false
}
},
chooseTime(item) {
@@ -448,17 +602,30 @@ layout("/mobile/platform.html"){
})
this.allowDayList = data
},
// 直接访问H5预约地址也必须先签署,签署完成后才展示日期和时段。
async initializeReservation() {
if (this.commitmentOpening || !this.$refs.commitment) return
this.commitmentOpening = true
this.commitmentId = ''
this.isLoad = false
this.site_id = getQueryString('id')
this.selectDate = moment()
try {
const signed = await this.$refs.commitment.open(this.site_id)
if (!signed) return
this.commitmentId = signed.id
this.times = []
await this.onLoad()
await this.getReserve()
await this.getAllowTime()
await this.setWeekList(this.selectDate)
await this.getMyReservation()
this.isLoad = true
} finally {
this.commitmentOpening = false
}
},
},
async created() {
this.site_id = getQueryString('id')
this.selectDate = moment()
await this.getReserve()
await this.onLoad()
await this.getAllowTime()
await this.setWeekList(this.selectDate)
await this.getMyReservation()
this.isLoad = true
}
})
</script>
@@ -193,6 +193,19 @@ layout("/layouts/platform.html"){
<el-form-item label="场地介绍">
<text-editor v-model="formData.introduce"></text-editor>
</el-form-item>
<template v-if="formData.reserveTarget === 1">
<el-form-item label="承诺书标题" prop="commitmentTitle"
:rules="[{required: true, message: '请填写承诺书标题', trigger: 'blur'}]">
<el-input v-model="formData.commitmentTitle" maxlength="200"></el-input>
</el-form-item>
<el-form-item label="承诺书正文" prop="commitmentContent"
:rules="[{required: true, message: '请填写承诺书正文', trigger: 'change'}]">
<text-editor v-model="formData.commitmentContent"></text-editor>
</el-form-item>
<el-form-item label="预约前阅读要求">
分工会预约人员及协会负责人均须阅读10秒并手写签字后才能预约。
</el-form-item>
</template>
</el-form>
<vi-title2 title="添加场次(温馨提醒:如果设置了预约时间单位,那么系统会在开始时间至结束时间范围内,按照设置的单位来拆分成多个时间段,如果未设置或者为0,则表示开始时间至结束时间是一个时间段)">
<template #func>
@@ -373,6 +386,8 @@ layout("/layouts/platform.html"){
notApplyTimeList: [],
reserveTarget: 2,
openDayNum: 7,
commitmentTitle: '教职工活动场地安全承诺书',
commitmentContent: '',
},
pageForm: {
searchName: "name",
@@ -495,6 +510,7 @@ layout("/layouts/platform.html"){
});
},
async operation() {
if (this.subDis) return
let method = this.formData.id ? "/doEdit" : "/doAdd"
this.subDis = true
this.$refs["addForm"].validate(async (valid) => {
@@ -516,6 +532,8 @@ layout("/layouts/platform.html"){
}
loading.close()
this.subDis = false
} else {
this.subDis = false
}
});
},
@@ -528,6 +546,12 @@ layout("/layouts/platform.html"){
},
async openEdit(id) {
this.formData = await this.findOneSite(id)
if (!this.formData.commitmentTitle) {
this.$set(this.formData, 'commitmentTitle', '教职工活动场地安全承诺书')
}
if (!this.formData.commitmentContent) {
this.$set(this.formData, 'commitmentContent', '')
}
if(!this.formData.notApplyTimeList) {
this.$set(this.formData, 'notApplyTimeList', [])
}
@@ -543,6 +567,8 @@ layout("/layouts/platform.html"){
notApplyTimeList: [],
reserveTarget: 2,
openDayNum: 7,
commitmentTitle: '教职工活动场地安全承诺书',
commitmentContent: '',
}
this.$refs.guava.edit()
@@ -77,6 +77,7 @@ layout("/layouts/platform.html"){
}
</style>
<div id="app" v-cloak>
<site-commitment ref="commitment"></site-commitment>
<guava ref="guava">
<template>
<el-card shadow="never">
@@ -383,6 +384,8 @@ layout("/layouts/platform.html"){
data() {
return {
activityTypeList: [],
commitmentId: '',
commitmentOpening: false,
drawer: false,
siteList: [],
restaurants: [],
@@ -433,6 +436,7 @@ layout("/layouts/platform.html"){
}
},
components: {
'site-commitment': httpVueLoader('/components/activity/site/SiteCommitment.vue'),
'guava': httpVueLoader('/components/plugins/Guava.vue'),
'reserve-info': httpVueLoader('/components/activity/site/ReserveInfo.vue?v=1.0.0'),
},
@@ -511,23 +515,35 @@ layout("/layouts/platform.html"){
this.addDialogVisible = false
this.formData.end_time = ''
},
onReserve(data) {
if(data.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') {
this.$message.warning('该场地只能分工会预约')
async onReserve(data) {
if (this.commitmentOpening) return
if(data.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04', 'H02')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') {
this.$message.warning('该场地仅限分工会预约人员或协会负责人预约')
return
}
this.reverseData = data
this.dayArray = []
this.getAllowTime()
if (data.sexlimit === 1 || data.sexlimit === 2) {
const sex = data.sexlimit === 1 ? '男' : '女'
if ("${@shiro.getPrincipalProperty('sex')}" === sex) {
this.openReserve(data)
} else {
if ("${@shiro.getPrincipalProperty('sex')}" !== sex) {
this.$message.warning('抱歉,该场地仅限' + sex + '性会员预约')
return
}
} else {
this.openReserve(data)
}
if (!this.$refs.commitment) {
this.$message.warning('承诺书组件加载中,请稍后再试')
return
}
this.commitmentOpening = true
this.commitmentId = ''
try {
const signed = await this.$refs.commitment.open(data.id)
if (!signed) return
this.commitmentId = signed.id
this.reverseData = data
this.dayArray = []
this.getAllowTime()
await this.openReserve(data)
} finally {
this.commitmentOpening = false
}
},
getDayClass(data) {
@@ -632,6 +648,7 @@ layout("/layouts/platform.html"){
});
},
async doAdd() {
if (this.subDis) return
if (!this.formData.time) {
this.$message.warning('未选择预约时间')
return
@@ -644,6 +661,7 @@ layout("/layouts/platform.html"){
}
this.$refs["addForm"].validate(async (valid) => {
if (valid) {
if (this.subDis) return
this.subDis = true
const loading = this.$loading({
lock: true,
@@ -651,20 +669,27 @@ layout("/layouts/platform.html"){
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
const {code, msg} = await $.post(location.href + "/doAdd", {
data: JSON.stringify(this.formData),
days: JSON.stringify(this.dayArray)
})
if (code === 0) {
this.addDialogVisible = false
this.$message.success(msg);
this.pageData()
this.$refs.guava.index()
} else {
this.$message.warning(msg)
try {
const {code, msg} = await $.post(location.href + "/doAdd", {
data: JSON.stringify(this.formData),
days: JSON.stringify(this.dayArray),
commitmentId: this.commitmentId
})
if (code === 0) {
this.commitmentId = ''
this.addDialogVisible = false
this.$message.success(msg);
this.pageData()
this.$refs.guava.index()
} else {
this.$message.warning(msg)
}
} catch (e) {
this.$message.error('预约提交失败,请稍后重试')
} finally {
loading.close()
this.subDis = false
}
loading.close()
this.subDis = false
}
});
},