场地预约问题

This commit is contained in:
2026-09-16 17:10:17 +08:00
parent 8748fa0c7d
commit 4b3c41921a
9 changed files with 517 additions and 159 deletions
@@ -5,6 +5,9 @@ import io.v.nutz.activity.models.ActivitySiteInfo;
import io.v.nutz.activity.services.SiteInfoService; import io.v.nutz.activity.services.SiteInfoService;
import io.v.nutz.annontation.ViReturn; import io.v.nutz.annontation.ViReturn;
import io.v.nutz.base.utils.PageUtil; import io.v.nutz.base.utils.PageUtil;
import cn.wizzer.framework.base.Result;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
@@ -62,18 +65,28 @@ public class SiteManageController {
return siteInfoService.listPage(pageNumber, pageSize, cnd); return siteInfoService.listPage(pageNumber, pageSize, cnd);
} }
/**
* @param activitySiteInfo data为场地表单,reserve_deadline传yyyy-MM-dd或空值(不限制)
* @return 保存成功沿用ViReturn默认成功结构,校验失败返回Result错误信息
*/
@At @At
@ViReturn @ViReturn
@Aop(TransAop.READ_COMMITTED)
public Object doAdd(@Param("data") ActivitySiteInfo activitySiteInfo) { public Object doAdd(@Param("data") ActivitySiteInfo activitySiteInfo) {
siteInfoService.insert(activitySiteInfo); String error = siteInfoService.saveSite(activitySiteInfo, false);
return null; return error == null ? null : Result.error(error);
} }
/**
* @param activitySiteInfo data为带id的场地表单,reserve_deadline空值表示清除截止日期
* @return 保存成功沿用ViReturn默认成功结构,校验失败返回Result错误信息
*/
@At @At
@ViReturn @ViReturn
@Aop(TransAop.READ_COMMITTED)
public Object doEdit(@Param("data") ActivitySiteInfo activitySiteInfo) { public Object doEdit(@Param("data") ActivitySiteInfo activitySiteInfo) {
siteInfoService.updateIgnoreNull(activitySiteInfo); String error = siteInfoService.saveSite(activitySiteInfo, true);
return null; return error == null ? null : Result.error(error);
} }
@At @At
@@ -7,6 +7,9 @@ import io.v.nutz.activity.models.ActivitySiteInfo;
import io.v.nutz.activity.models.ActivitySiteReserve; import io.v.nutz.activity.models.ActivitySiteReserve;
import io.v.nutz.activity.services.ActivityCommonService; import io.v.nutz.activity.services.ActivityCommonService;
import io.v.nutz.activity.services.SiteInfoService; import io.v.nutz.activity.services.SiteInfoService;
import io.v.nutz.sys.services.SysHolidayService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import io.v.nutz.activity.services.SiteReserveService; import io.v.nutz.activity.services.SiteReserveService;
import io.v.nutz.annontation.ViReturn; import io.v.nutz.annontation.ViReturn;
import io.v.nutz.base.utils.PageUtil; import io.v.nutz.base.utils.PageUtil;
@@ -53,6 +56,19 @@ public class SiteReserveController {
@Inject @Inject
private SiteInfoService siteInfoService; private SiteInfoService siteInfoService;
@Inject
private SysHolidayService sysHolidayService;
/**
* 无请求参数。返回日历对象:days为yyyy-MM-dd节假日列表,years为已同步年份列表。
* PC和手机使用同一本地数据源,预约请求不访问外部接口。
*/
@At
@ViReturn
public Object holidayCalendar() {
return sysHolidayService.getCalendar();
}
@Inject @Inject
private SiteReserveService siteReserveService; private SiteReserveService siteReserveService;
@@ -152,8 +168,14 @@ public class SiteReserveController {
return fetch.getReserve_state() > stateCode; return fetch.getReserve_state() > stateCode;
} }
/**
* @param activitySiteReserve data提交预约表单,包含site_id及起止时间等原有字段
* @param days days提交yyyy-MM-dd日期数组,必须整批通过日期限制后才保存
* @return 校验失败返回Result错误对象;成功保持原接口由ViReturn包装的结果结构
*/
@At @At
@ViReturn @ViReturn
@Aop(TransAop.READ_COMMITTED)
public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days) { public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days) {
String r = R.UU32(); String r = R.UU32();
@@ -167,10 +189,12 @@ public class SiteReserveController {
} }
ActivitySiteInfo siteInfo = siteInfoService.fetch(activitySiteReserve.getSite_id()); ActivitySiteInfo siteInfo = siteInfoService.fetch(activitySiteReserve.getSite_id());
// 在第一条记录写入前校验全部日期,避免后面的节假日导致前面的日期已被保存。
String dateError = siteInfoService.validateReserveDays(siteInfo, days == null ? null : Arrays.asList(days));
if (dateError != null) {
return Result.error(dateError);
}
for (String d : days) { for (String d : days) {
if (siteInfoService.isInCloseDates(siteInfo, d)) {
return Result.error(siteInfoService.getCloseDateMessage(siteInfo));
}
activitySiteReserve.setReserve_day(d); activitySiteReserve.setReserve_day(d);
//查询这个人是否已经预约这个时间段 //查询这个人是否已经预约这个时间段
int count = siteReserveService.count("activity_site_reserve", Cnd.NEW().and("reserve_person_id", "=", ShiroUtil.getPrincipalProperty("id")) int count = siteReserveService.count("activity_site_reserve", Cnd.NEW().and("reserve_person_id", "=", ShiroUtil.getPrincipalProperty("id"))
@@ -57,6 +57,11 @@ public class ActivitySiteInfo {
@ColDefine(type = ColType.MYSQL_JSON) @ColDefine(type = ColType.MYSQL_JSON)
private List close_dates; private List close_dates;
@Column
@Comment("最后可预约日期,yyyy-MM-dd,含当天;空值表示不限制")
@ColDefine(type = ColType.VARCHAR, width = 10)
private String reserve_deadline;
@Column @Column
@Comment("创建人") @Comment("创建人")
@ColDefine(type = ColType.VARCHAR, width = 30) @ColDefine(type = ColType.VARCHAR, width = 30)
@@ -6,6 +6,22 @@ import io.v.nutz.activity.models.ActivitySiteInfo;
public interface SiteInfoService extends BaseService<ActivitySiteInfo> { public interface SiteInfoService extends BaseService<ActivitySiteInfo> {
/**
* 校验并保存场地,清空截止日期时显式保存空字符串,避免忽略null更新保留旧值。
* @param siteInfo 场地表单,reserve_deadline为yyyy-MM-dd,null或空串表示不限制
* @param edit true表示编辑已有场地(必须提供id),false表示新增
* @return null表示保存成功,非空字符串为校验失败原因,校验失败不写入
*/
String saveSite(ActivitySiteInfo siteInfo, boolean edit);
/**
* 在写入预约记录前检查整批日期,截止日期和关闭时段始终生效,workday开启时额外排除周末及节假日。
* @param siteInfo 从数据库读取的场地,不能使用客户端传入的开关
* @param days 预约日期列表,每项必须为 yyyy-MM-dd
* @return null 表示全部日期允许预约,否则返回第一条拒绝原因
*/
String validateReserveDays(ActivitySiteInfo siteInfo, java.util.List<String> days);
/** /**
* 判断指定预约日期是否落在场地关闭时段内。 * 判断指定预约日期是否落在场地关闭时段内。
* *
@@ -4,9 +4,14 @@ package io.v.nutz.activity.services.impl;
import cn.wizzer.framework.base.service.BaseServiceImpl; import cn.wizzer.framework.base.service.BaseServiceImpl;
import io.v.nutz.activity.models.ActivitySiteInfo; import io.v.nutz.activity.models.ActivitySiteInfo;
import io.v.nutz.activity.services.SiteInfoService; import io.v.nutz.activity.services.SiteInfoService;
import io.v.nutz.sys.services.SysHolidayService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.lang.util.NutMap;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
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.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -20,6 +25,85 @@ import java.util.List;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class SiteInfoServiceImpl extends BaseServiceImpl<ActivitySiteInfo> implements SiteInfoService { public class SiteInfoServiceImpl extends BaseServiceImpl<ActivitySiteInfo> implements SiteInfoService {
@Inject
private SysHolidayService sysHolidayService;
@Override
@Aop(TransAop.READ_COMMITTED)
public String saveSite(ActivitySiteInfo siteInfo, boolean edit) {
if (siteInfo == null || (edit && Strings.isBlank(siteInfo.getId()))) {
return "场地信息不完整,请刷新后重试";
}
String deadline = Strings.sNull(siteInfo.getReserve_deadline()).trim();
if (!deadline.isEmpty()) {
try {
if (!LocalDate.parse(deadline).toString().equals(deadline)) {
return "预约截止日期格式必须为yyyy-MM-dd";
}
} catch (Exception e) {
return "预约截止日期格式必须为yyyy-MM-dd";
}
}
// 日期框清空返回null;转为空串后updateIgnoreNull才能真正清除原来的截止日期。
siteInfo.setReserve_deadline(deadline);
if (edit) {
updateIgnoreNull(siteInfo);
} else {
insert(siteInfo);
}
return null;
}
@Override
public String validateReserveDays(ActivitySiteInfo siteInfo, List<String> days) {
if (siteInfo == null) {
return "场地不存在,请刷新后重试";
}
if (days == null || days.isEmpty()) {
return "请选择预约日期";
}
LocalDate deadline = null;
if (!Strings.isBlank(siteInfo.getReserve_deadline())) {
try {
deadline = LocalDate.parse(siteInfo.getReserve_deadline());
} catch (Exception e) {
return "场地预约截止日期配置异常,请联系管理员";
}
}
// 整批只加载一次日历;未开启限定工作日时,不依赖节假日同步状态。
NutMap calendar = Boolean.TRUE.equals(siteInfo.getWorkday()) ? sysHolidayService.getCalendar() : null;
for (String day : days) {
LocalDate date;
try {
date = LocalDate.parse(day);
if (!date.toString().equals(day)) {
return "预约日期格式必须为yyyy-MM-dd";
}
} catch (Exception e) {
return "预约日期格式必须为yyyy-MM-dd";
}
// 截止当天仍可预约,只有晚于截止日才拒绝;与限定工作日开关无关。
if (deadline != null && date.isAfter(deadline)) {
return "该场地最晚可预约至" + deadline + ",请选择截止日期当天或之前的日期";
}
if (isInCloseDates(siteInfo, day)) {
return getCloseDateMessage(siteInfo);
}
if (calendar != null) {
if (date.getDayOfWeek().getValue() >= 6) {
return day + "为周末,该场地仅限工作日预约";
}
if (!((List<?>) calendar.get("years")).contains(date.getYear())) {
return date.getYear() + "年节假日尚未同步,暂不能预约,请联系管理员";
}
if (((List<?>) calendar.get("days")).contains(day)) {
return day + "为节假日,该场地仅限工作日预约";
}
}
}
return null;
}
public SiteInfoServiceImpl(Dao dao) { public SiteInfoServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@@ -7,6 +7,9 @@ import io.v.nutz.activity.models.ActivitySiteInfo;
import io.v.nutz.activity.models.ActivitySiteReserve; import io.v.nutz.activity.models.ActivitySiteReserve;
import io.v.nutz.activity.services.ActivityCommonService; import io.v.nutz.activity.services.ActivityCommonService;
import io.v.nutz.activity.services.SiteInfoService; import io.v.nutz.activity.services.SiteInfoService;
import io.v.nutz.sys.services.SysHolidayService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import io.v.nutz.activity.services.SiteReserveService; import io.v.nutz.activity.services.SiteReserveService;
import io.v.nutz.annontation.ViReturn; import io.v.nutz.annontation.ViReturn;
import io.v.nutz.base.utils.SendMsg; import io.v.nutz.base.utils.SendMsg;
@@ -50,6 +53,19 @@ public class SiteInfoMobileController {
@Inject @Inject
private SiteInfoService siteInfoService; private SiteInfoService siteInfoService;
@Inject
private SysHolidayService sysHolidayService;
/**
* 无请求参数。返回日历对象:days为yyyy-MM-dd节假日列表,years为已同步年份列表。
* PC和手机使用同一本地数据源,预约请求不访问外部接口。
*/
@At
@ViReturn
public Object holidayCalendar() {
return sysHolidayService.getCalendar();
}
@Inject @Inject
private SiteReserveService siteReserveService; private SiteReserveService siteReserveService;
@@ -238,9 +254,16 @@ public class SiteInfoMobileController {
return hours; return hours;
} }
/**
* @param siteId 预约场地ID,限制开关以数据库场地配置为准
* @param times JSON数组,每项包含day(yyyy-MM-dd)、start_time、end_time
* @param message 预约事由
* @return 校验失败返回Result错误对象;成功保持原接口由ViReturn包装的结果结构
*/
@At("/reserveDo") @At("/reserveDo")
@Ok("json:full") @Ok("json:full")
@ViReturn @ViReturn
@Aop(TransAop.READ_COMMITTED)
public Object reserveDo(String siteId, String times, String message) { public Object reserveDo(String siteId, String times, String message) {
String moduleName = activityCommonService.findModuleNameByActivityId(siteId); String moduleName = activityCommonService.findModuleNameByActivityId(siteId);
Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName); Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName);
@@ -254,13 +277,17 @@ public class SiteInfoMobileController {
ActivitySiteInfo siteInfo = siteInfoService.fetch(siteId); ActivitySiteInfo siteInfo = siteInfoService.fetch(siteId);
List<NutMap> nutMaps = Json.fromJsonAsList(NutMap.class, times); List<NutMap> nutMaps = Json.fromJsonAsList(NutMap.class, times);
// 校验整批日期后再构造和保存预约,客户端传来的日期不能绕过工作日限制。
List<String> reserveDays = nutMaps == null ? null : nutMaps.stream()
.map(item -> item == null ? null : item.getString("day")).collect(Collectors.toList());
String dateError = siteInfoService.validateReserveDays(siteInfo, reserveDays);
if (dateError != null) {
return Result.error(dateError);
}
String str = ""; String str = "";
String r = R.UU32(); String r = R.UU32();
for (NutMap item : nutMaps) { for (NutMap item : nutMaps) {
String reserveDay = item.getString("day"); String reserveDay = item.getString("day");
if (siteInfoService.isInCloseDates(siteInfo, reserveDay)) {
return Result.error(siteInfoService.getCloseDateMessage(siteInfo));
}
ActivitySiteReserve as = new ActivitySiteReserve(); ActivitySiteReserve as = new ActivitySiteReserve();
as.setSqid(r); as.setSqid(r);
as.setSite_id(siteId); as.setSite_id(siteId);
@@ -268,17 +268,15 @@ layout("/mobile/platform.html"){
times: [], times: [],
weekList: [], weekList: [],
calendarVisible: false, calendarVisible: false,
selectDate: null, selectDate: moment(),
//已经预约的记录 //已经预约的记录
alreadyReserveRecord: [], alreadyReserveRecord: [],
holidays: [ holidays: [],
'2025-01-01', holidayYears: [],
'2025-01-28','2025-01-29','2025-01-30','2025-01-31','2025-02-01','2025-02-02','2025-02-03','2025-02-04', holidayReady: false,
'2025-04-04', '2025-04-05', '2025-04-06', holidayLoading: false,
'2025-05-01', '2025-05-02', '2025-05-03', '2025-05-04', '2025-05-05', reserveDataLoading: false,
'2025-05-31', '2025-06-01', '2025-06-02', reservationLoading: false,
'2025-10-01', '2025-10-02', '2025-10-03', '2025-10-04', '2025-10-05', '2025-10-06', '2025-10-07', '2025-10-08'
],
} }
}, },
computed: { computed: {
@@ -306,17 +304,64 @@ layout("/mobile/platform.html"){
calendarFormatter() { calendarFormatter() {
return (day) => { return (day) => {
// 休息日不能选择 // 休息日不能选择
if(this.site.workday === true) { if (this.getReserveDateMessage(moment(day.date).format('YYYY-MM-DD'), this.site)) {
let week = day.date.getDay() day.type = 'disabled'
if (week === 0 || week === 6) {
day.type = 'disabled'
}
} }
return day return day
} }
} }
}, },
methods: { methods: {
// 后端返回days日期数组及years已同步年份,加载失败时禁止按空日历放行。
loadHolidayCalendar() {
this.holidayReady = false
this.holidayLoading = true
return $.post('/mobile/activity/site/info/holidayCalendar', {})
.then((res) => {
if (res.code !== 0 || !res.data || !Array.isArray(res.data.days) || !Array.isArray(res.data.years)) {
return $.Deferred().reject('节假日日历加载失败,请稍后重试').promise()
}
this.holidays = res.data.days
this.holidayYears = res.data.years
this.holidayReady = true
})
.always(() => {
this.holidayLoading = false
})
},
// 截止日期独立于工作日开关:截止当天允许,超过截止日禁止;原有假期规则继续生效。
getReserveDateMessage(day, siteData) {
if (!siteData) {
return ''
}
const date = moment(day, 'YYYY-MM-DD', true)
if (!date.isValid()) {
return '预约日期格式不正确'
}
if (siteData.reserve_deadline) {
const deadline = moment(siteData.reserve_deadline, 'YYYY-MM-DD', true)
if (!deadline.isValid()) {
return '场地预约截止日期配置异常,请联系管理员'
}
if (date.isAfter(deadline, 'day')) {
return '该场地最晚可预约至' + siteData.reserve_deadline + ',请选择截止日期当天或之前的日期'
}
}
if (siteData.workday !== true) {
return ''
}
if (date.day() === 0 || date.day() === 6) {
return day + '为周末,该场地仅限工作日预约'
}
if (!this.holidayReady) {
return '节假日日历尚未加载,请稍后重试'
}
if (!this.holidayYears.includes(date.year())) {
return date.year() + '年节假日尚未同步,暂不能预约,请联系管理员'
}
return this.holidays.includes(day) ? day + '为节假日,该场地仅限工作日预约' : ''
},
close(f) { close(f) {
if (this.times.length === 1) { if (this.times.length === 1) {
vant.Toast('必须保留一个时间段') vant.Toast('必须保留一个时间段')
@@ -333,23 +378,18 @@ layout("/mobile/platform.html"){
//这里要排除掉休息日 //这里要排除掉休息日
setWeekList(startDate) { setWeekList(startDate) {
const wk = [] const wk = []
wk.push(moment(startDate)) // 包含起始日期逐天校验;设定上限,跨入尚未同步年度时不会无限查找。
let count = 1 for (let count = 0; count < 366 && wk.length < 7; count++) {
while (wk.length < 7) { const day = moment(startDate).add(count, 'd')
const addDate = moment(startDate).add(count, 'd') if (this.site.reserve_deadline && day.format('YYYY-MM-DD') > this.site.reserve_deadline) {
//这里不是休息日才能加到数组里 break
if (this.site.workday === true) { }
if( if (this.site.workday === true && (!this.holidayReady || !this.holidayYears.includes(day.year()))) {
addDate.day() !== 0 && break
addDate.day() !== 6 && }
!this.holidays.includes(moment(addDate).format('YYYY-MM-DD')) if (!this.getReserveDateMessage(day.format('YYYY-MM-DD'), this.site)) {
) { wk.push(day)
wk.push(addDate)
}
} else {
wk.push(addDate)
} }
count++
} }
this.weekList = wk this.weekList = wk
}, },
@@ -398,7 +438,8 @@ layout("/mobile/platform.html"){
return day >= start && day <= end return day >= start && day <= end
}, },
showCloseDateMessage() { showCloseDateMessage() {
vant.Toast(this.getCloseReserveMessage(this.site)) const message = this.getReserveDateMessage(this.selectDate.format('YYYY-MM-DD'), this.site)
vant.Toast(message || this.getCloseReserveMessage(this.site))
}, },
reserve() { reserve() {
if (this.times.length === 0) { if (this.times.length === 0) {
@@ -407,6 +448,11 @@ layout("/mobile/platform.html"){
} }
for (let i = 0; i < this.times.length; i++) { for (let i = 0; i < this.times.length; i++) {
let time = this.times[i] let time = this.times[i]
const workdayMessage = this.getReserveDateMessage(time.day, this.site)
if (workdayMessage) {
vant.Toast(workdayMessage)
return
}
if (this.isInCloseDates(time.day, this.site)) { if (this.isInCloseDates(time.day, this.site)) {
vant.Toast(this.getCloseReserveMessage(this.site)) vant.Toast(this.getCloseReserveMessage(this.site))
this.times.splice(i, 1) this.times.splice(i, 1)
@@ -452,6 +498,11 @@ layout("/mobile/platform.html"){
this.reserveLoading = false this.reserveLoading = false
}, },
chooseTime(item) { chooseTime(item) {
const workdayMessage = this.getReserveDateMessage(this.selectDate.format('YYYY-MM-DD'), this.site)
if (workdayMessage) {
vant.Toast(workdayMessage)
return
}
if (this.isInCloseDates(this.selectDate.format('YYYY-MM-DD'), this.site)) { if (this.isInCloseDates(this.selectDate.format('YYYY-MM-DD'), this.site)) {
vant.Toast(this.getCloseReserveMessage(this.site)) vant.Toast(this.getCloseReserveMessage(this.site))
return return
@@ -477,7 +528,8 @@ layout("/mobile/platform.html"){
return 3 return 3
} }
if (this.isInCloseDates(this.selectDate.format('YYYY-MM-DD'), this.site)) { if (this.getReserveDateMessage(this.selectDate.format('YYYY-MM-DD'), this.site)
|| this.isInCloseDates(this.selectDate.format('YYYY-MM-DD'), this.site)) {
return 4 return 4
} }
@@ -492,66 +544,86 @@ layout("/mobile/platform.html"){
} }
}, },
async onLoad() { onLoad() {
this.loading = true this.loading = true
const resp = await $.post('/mobile/activity/site/info/getDetail', {id: this.site_id}) return $.post('/mobile/activity/site/info/getDetail', {id: this.site_id})
if (resp.code === 0) { .then((resp) => {
this.site = resp.data if (resp.code !== 0 || !resp.data) {
if (this.site.close_dates && typeof this.site.close_dates === 'string') { return $.Deferred().reject('场地信息加载失败').promise()
this.$set(this.site, 'close_dates', JSON.parse(this.site.close_dates)) }
} else if (!this.site.close_dates) { this.site = resp.data
this.$set(this.site, 'close_dates', []) if (typeof this.site.close_dates === 'string') {
} this.$set(this.site, 'close_dates', JSON.parse(this.site.close_dates))
} } else if (!this.site.close_dates) {
this.loading = false this.$set(this.site, 'close_dates', [])
}, }
async getReserve() { return this.site.workday === true ? this.loadHolidayCalendar() : null
const resp = await $.post('/mobile/activity/site/info/getReserve', { })
siteId: this.site_id, .always(() => {
day: moment(this.selectDate).format('YYYY-MM-DD'), this.loading = false
})
if (resp.code === 0) {
resp.data.forEach(item => {
item.isClick = true
}) })
this.timeData = resp.data
}
}, },
async getMyReservation() { getReserve() {
const resp = await $.post('/mobile/activity/site/info/getMyReservation', { // siteId为场地ID、day为选中日期;返回场次及占用状态数组。
site_id: this.site_id this.reserveDataLoading = true
return $.post('/mobile/activity/site/info/getReserve', {
siteId: this.site_id,
day: this.selectDate.format('YYYY-MM-DD')
}).then((resp) => {
if (resp.code !== 0 || !Array.isArray(resp.data)) {
return $.Deferred().reject().promise()
}
resp.data.forEach(item => this.$set(item, 'isClick', true))
this.timeData = resp.data
}).always(() => {
this.reserveDataLoading = false
}) })
if (resp.code === 0) {
this.alreadyReserveRecord = resp.data.map(v => v.reserve_day + " " + v.start_time + "-" + v.end_time)
}
}, },
//获取最新的一个工作日设为当前时间 getMyReservation() {
// site_id为当前场地ID;预约日期和起止时间合并为前端已选状态标识。
this.reservationLoading = true
return $.post('/mobile/activity/site/info/getMyReservation', {site_id: this.site_id})
.then((resp) => {
if (resp.code !== 0 || !Array.isArray(resp.data)) {
return $.Deferred().reject().promise()
}
this.alreadyReserveRecord = resp.data.map(v => v.reserve_day + ' ' + v.start_time + '-' + v.end_time)
}).always(() => {
this.reservationLoading = false
})
},
// 场地与日历加载后再确定默认日期,普通周一至周五也始终返回有效日期。
getLatestWorkDay() { getLatestWorkDay() {
//1.首先判断今天是不是工作日 const today = moment()
const weekNum = moment().day() for (let offset = 0; offset < 366; offset++) {
let selectDate = null const date = moment(today).add(offset, 'd')
if(this.site.workday === true) { if (this.site.reserve_deadline && date.format('YYYY-MM-DD') > this.site.reserve_deadline) {
if (weekNum === 0) { break
//周日 }
selectDate = moment().add(1, 'd') if (this.site.workday === true && (!this.holidayReady || !this.holidayYears.includes(date.year()))) {
} else if (weekNum === 6) { break
//周六 }
selectDate = moment().add(2, 'd') if (!this.getReserveDateMessage(date.format('YYYY-MM-DD'), this.site)) {
return date
} }
} else {
selectDate = moment()
} }
return selectDate // 年度未同步时保留显示日期,但选择与后端提交都保持禁止。
return today
} }
}, },
async created() { created() {
this.site_id = getQueryString('id') this.site_id = getQueryString('id')
this.selectDate = this.getLatestWorkDay() this.onLoad().then(() => {
await this.onLoad() this.selectDate = this.getLatestWorkDay()
this.setWeekList(this.selectDate) this.setWeekList(this.selectDate)
await this.getReserve() const message = this.getReserveDateMessage(this.selectDate.format('YYYY-MM-DD'), this.site)
await this.getMyReservation() if (message) {
vant.Toast(message)
}
return this.getReserve()
}).then(() => this.getMyReservation(), () => {
vant.Toast('场地或节假日数据加载失败,请刷新重试')
})
} }
}) })
</script> </script>
@@ -187,6 +187,19 @@ layout("/layouts/platform.html"){
</el-col> </el-col>
</el-row> </el-row>
<el-form-item label="预约截止日期" prop="reserve_deadline">
<!-- 截止日期含当天,清空表示不限制最晚预约日期。 -->
<el-date-picker
:value="formData.reserve_deadline"
@input="value => this.$set(formData, 'reserve_deadline', value || '')"
type="date"
value-format="yyyy-MM-dd"
placeholder="不设置则不限制"
clearable>
</el-date-picker>
<span style="color: #909399; margin-left: 12px">截止当天仍可预约,之后不可预约</span>
</el-form-item>
<el-form-item label="关闭时段" label-width="110px"> <el-form-item label="关闭时段" label-width="110px">
<el-date-picker <el-date-picker
v-model="formData.close_dates" v-model="formData.close_dates"
@@ -238,7 +251,7 @@ layout("/layouts/platform.html"){
</el-form> </el-form>
<div style="float: right;margin: 20px 0"> <div style="float: right;margin: 20px 0">
<el-button type="primary" :disabled="subDis" @click="operation">确 定</el-button> <el-button type="primary" :disabled="formLoading" @click="operation">确 定</el-button>
</div> </div>
</div> </div>
</template> </template>
@@ -258,10 +271,11 @@ layout("/layouts/platform.html"){
activityTypeList: [], activityTypeList: [],
tabKey: '', tabKey: '',
tableLoading: false, tableLoading: false,
subDis: false, formLoading: false,
tableData: [], tableData: [],
formData: { formData: {
open_hours: [{}], open_hours: [{}],
reserve_deadline: '',
close_dates: [] close_dates: []
}, },
pageForm: { pageForm: {
@@ -307,50 +321,71 @@ layout("/layouts/platform.html"){
} }
}); });
}, },
async operation() { operation() {
let method = this.formData.id ? "/doEdit" : "/doAdd" const method = this.formData.id ? '/doEdit' : '/doAdd'
this.$refs["addForm"].validate(async (valid) => { this.$refs['addForm'].validate((valid) => {
if (valid) { if (!valid) {
this.subDis = true return
const loading = this.$loading({
lock: true,
text: '正在提交...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
const {code, data, msg} = await $.post(location.href + method, {data: JSON.stringify(this.formData)})
if (code === 0) {
this.pageData()
this.$message.success(msg);
this.$refs.guava.index()
} else {
this.$message.error(msg);
}
loading.close()
this.subDis = false
} }
}); // 清空日期时仍提交空字符串,确保后端删除旧的截止配置。
this.$set(this.formData, 'reserve_deadline', this.formData.reserve_deadline || '')
this.formLoading = true
const loading = this.$loading({
lock: true,
text: '正在提交...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
// data包含原有场地字段及reserve_deadline;code为0表示保存成功,msg为结果提示。
$.post(location.href + method, {data: JSON.stringify(this.formData)})
.then((res) => {
if (res.code === 0) {
this.pageData()
this.$message.success(res.msg)
this.$refs.guava.index()
} else {
this.$message.error(res.msg)
}
}, () => {
this.$message.error('场地保存失败,请稍后重试')
}).always(() => {
loading.close()
this.formLoading = false
})
})
}, },
async findOneSite(id) { findOneSite(id) {
const {data} = await $.get("/platform/activity/site/mange/findOneSite", {id: id}) // id为场地ID;data为场地实体,旧场地没有截止日期时按不限处理。
if (!data.close_dates) { this.formLoading = true
this.$set(data, 'close_dates', []) return $.post('/platform/activity/site/mange/findOneSite', {id: id})
} .then((res) => {
return data if (res.code !== 0 || !res.data) {
return $.Deferred().reject().promise()
}
this.$set(res.data, 'close_dates', res.data.close_dates || [])
this.$set(res.data, 'reserve_deadline', res.data.reserve_deadline || '')
return res.data
}).always(() => {
this.formLoading = false
})
}, },
openView(row) { openView(row) {
this.$refs.guava.view() this.$refs.guava.view()
}, },
async openEdit(id) { openEdit(id) {
this.formData = await this.findOneSite(id) this.findOneSite(id).then((data) => {
this.$refs.guava.edit() this.formData = data
this.$refs.guava.edit()
}, () => {
this.$message.error('场地信息加载失败,请稍后重试')
})
}, },
openAdd() { openAdd() {
this.formData = { this.formData = {
state: true, state: true,
multiple: true, multiple: true,
open_hours: [{}], open_hours: [{}],
reserve_deadline: '',
close_dates: [], close_dates: [],
create_username: '${@shiro.getPrincipalProperty("username")}', create_username: '${@shiro.getPrincipalProperty("username")}',
create_time: moment().format('YYYY-MM-DD'), create_time: moment().format('YYYY-MM-DD'),
@@ -29,6 +29,11 @@ layout("/layouts/platform.html"){
color: #1989FA; color: #1989FA;
} }
.reserve-date-disabled {
opacity: 0.45;
cursor: not-allowed;
}
.div-Calendar { .div-Calendar {
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
@@ -325,7 +330,9 @@ layout("/layouts/platform.html"){
</el-row> </el-row>
<el-calendar v-model="reserve_time"> <el-calendar v-model="reserve_time">
<template slot="dateCell" slot-scope="{date, data}"> <template slot="dateCell" slot-scope="{date, data}">
<div class="div-Calendar" @click="pushDay(data)"> <div class="div-Calendar" @click="pushDay(data)"
:class="{'reserve-date-disabled': !!getReserveDateMessage(data.day, infoViewData)}"
:aria-disabled="!!getReserveDateMessage(data.day, infoViewData)">
<p :style="dayArray.includes(data.day) ? 'color: #246fb4' : ''"> <p :style="dayArray.includes(data.day) ? 'color: #246fb4' : ''">
{{ data.day.split('-').slice(1).join('-') }} {{ dayArray.includes(data.day) ? '√' : ''}} {{ data.day.split('-').slice(1).join('-') }} {{ dayArray.includes(data.day) ? '√' : ''}}
</p> </p>
@@ -465,7 +472,7 @@ layout("/layouts/platform.html"){
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="closeAdd">取 消</el-button> <el-button @click="closeAdd">取 消</el-button>
<el-button type="primary" :disabled="subDis" @click="doAdd">确 定</el-button> <el-button type="primary" :disabled="formLoading" @click="doAdd">确 定</el-button>
</span> </span>
</el-dialog> </el-dialog>
@@ -535,7 +542,7 @@ layout("/layouts/platform.html"){
reserveViewData: {}, reserveViewData: {},
tableLoading: false, tableLoading: false,
addDialogVisible: false, addDialogVisible: false,
subDis: false, formLoading: false,
loading: false, loading: false,
tableData: [], tableData: [],
formData: { formData: {
@@ -568,8 +575,11 @@ layout("/layouts/platform.html"){
/*reserve_cause: [{required: true, message: '请选择预约事由', trigger: ['blur', 'change']}],*/ /*reserve_cause: [{required: true, message: '请选择预约事由', trigger: ['blur', 'change']}],*/
}, },
dayArray: [], dayArray: [],
holidays: ['2022-06-03','2022-06-04','2022-06-05','2022-09-10','2022-09-11','2022-09-12', holidays: [],
'2022-10-01','2022-10-02','2022-10-03','2022-10-04','2022-10-05','2022-10-06','2022-10-07',], holidayYears: [],
holidayReady: false,
holidayLoading: false,
reserveDataLoading: false,
} }
}, },
components: { components: {
@@ -577,6 +587,56 @@ layout("/layouts/platform.html"){
'reserve-info': httpVueLoader('/components/activity/site/ReserveInfo.vue'), 'reserve-info': httpVueLoader('/components/activity/site/ReserveInfo.vue'),
}, },
methods: { methods: {
// 后端返回days日期数组及years已同步年份,加载失败时禁止按空日历放行。
loadHolidayCalendar() {
this.holidayReady = false
this.holidayLoading = true
return $.post('/platform/activity/site/reserve/holidayCalendar', {})
.then((res) => {
if (res.code !== 0 || !res.data || !Array.isArray(res.data.days) || !Array.isArray(res.data.years)) {
return $.Deferred().reject('节假日日历加载失败,请稍后重试').promise()
}
this.holidays = res.data.days
this.holidayYears = res.data.years
this.holidayReady = true
})
.always(() => {
this.holidayLoading = false
})
},
// 截止日期独立于工作日开关:截止当天允许,超过截止日禁止;原有假期规则继续生效。
getReserveDateMessage(day, siteData) {
if (!siteData) {
return ''
}
const date = moment(day, 'YYYY-MM-DD', true)
if (!date.isValid()) {
return '预约日期格式不正确'
}
if (siteData.reserve_deadline) {
const deadline = moment(siteData.reserve_deadline, 'YYYY-MM-DD', true)
if (!deadline.isValid()) {
return '场地预约截止日期配置异常,请联系管理员'
}
if (date.isAfter(deadline, 'day')) {
return '该场地最晚可预约至' + siteData.reserve_deadline + ',请选择截止日期当天或之前的日期'
}
}
if (siteData.workday !== true) {
return ''
}
if (date.day() === 0 || date.day() === 6) {
return day + '为周末,该场地仅限工作日预约'
}
if (!this.holidayReady) {
return '节假日日历尚未加载,请稍后重试'
}
if (!this.holidayYears.includes(date.year())) {
return date.year() + '年节假日尚未同步,暂不能预约,请联系管理员'
}
return this.holidays.includes(day) ? day + '为节假日,该场地仅限工作日预约' : ''
},
diaOpen() { diaOpen() {
if(this.infoViewData.multiple === true || if(this.infoViewData.multiple === true ||
(this.infoViewData.open_hours.length === 1 && this.infoViewData.open_hours[0].disabled !== true)) { (this.infoViewData.open_hours.length === 1 && this.infoViewData.open_hours[0].disabled !== true)) {
@@ -776,7 +836,6 @@ layout("/layouts/platform.html"){
return day >= start && day <= end return day >= start && day <= end
}, },
pushDay (data) { pushDay (data) {
const d = new Date(data.day).getDay()
if (this.isInCloseDates(data.day, this.textData)) { if (this.isInCloseDates(data.day, this.textData)) {
this.$message.warning(this.getCloseReserveMessage(this.textData)) this.$message.warning(this.getCloseReserveMessage(this.textData))
return return
@@ -791,8 +850,9 @@ layout("/layouts/platform.html"){
return return
} }
} }
if(this.textData.workday === true && (d === 0 || d === 6 || this.holidays.includes(data.day))) { const workdayMessage = this.getReserveDateMessage(data.day, this.textData)
this.$message.warning('抱歉,该场地只能预约工作日') if (workdayMessage) {
this.$message.warning(workdayMessage)
return return
} }
if (data.day < moment(new Date()).format('YYYY-MM-DD')) { if (data.day < moment(new Date()).format('YYYY-MM-DD')) {
@@ -895,11 +955,16 @@ layout("/layouts/platform.html"){
transNum(hm) { transNum(hm) {
return (moment("1970-01-01 " + hm).valueOf() + HOUR8) / HOUR1 return (moment("1970-01-01 " + hm).valueOf() + HOUR8) / HOUR1
}, },
async doAdd() { doAdd() {
if (!this.formData.time) { if (!this.formData.time) {
this.$notify.warning({title: '警告', message: '未选择预约时间!'}); this.$notify.warning({title: '警告', message: '未选择预约时间!'});
return return
} }
const invalidDay = this.dayArray.find(day => this.getReserveDateMessage(day, this.infoViewData))
if (invalidDay) {
this.$notify.warning({title: '警告', message: this.getReserveDateMessage(invalidDay, this.infoViewData)});
return
}
const closeDay = this.dayArray.find(day => this.isInCloseDates(day, this.infoViewData)) const closeDay = this.dayArray.find(day => this.isInCloseDates(day, this.infoViewData))
if (closeDay) { if (closeDay) {
this.$notify.warning({title: '警告', message: this.getCloseReserveMessage(this.infoViewData)}); this.$notify.warning({title: '警告', message: this.getCloseReserveMessage(this.infoViewData)});
@@ -918,9 +983,9 @@ layout("/layouts/platform.html"){
this.$notify.warning({title: '警告', message: '您预约的时间段已过!'}); this.$notify.warning({title: '警告', message: '您预约的时间段已过!'});
return return
} }
this.$refs["addForm"].validate(async (valid) => { this.$refs["addForm"].validate((valid) => {
if (valid) { if (valid) {
this.subDis = true this.formLoading = true
const loading = this.$loading({ const loading = this.$loading({
lock: true, lock: true,
text: '正在提交...', text: '正在提交...',
@@ -928,20 +993,24 @@ layout("/layouts/platform.html"){
background: 'rgba(0, 0, 0, 0.7)' background: 'rgba(0, 0, 0, 0.7)'
}); });
const {code, msg} = await $.post(location.href + "/doAdd", { $.post(location.href + "/doAdd", {
data: JSON.stringify(this.formData), data: JSON.stringify(this.formData),
days: JSON.stringify(this.dayArray) days: JSON.stringify(this.dayArray)
}).then((res) => {
if (res.code === 0) {
this.addDialogVisible = false
this.$message.success(res.msg)
this.pageData()
this.$refs.guava.index()
} else {
this.$notify.warning({title: '警告', message: res.msg})
}
}, () => {
this.$message.warning('预约提交失败,请稍后重试')
}).always(() => {
loading.close()
this.formLoading = false
}) })
if (code === 0) {
this.addDialogVisible = false
this.$message.success(msg);
this.pageData()
this.$refs.guava.index()
} else {
this.$notify.warning({title: '警告', message: msg});
}
loading.close()
this.subDis = false
} }
}); });
}, },
@@ -953,16 +1022,23 @@ layout("/layouts/platform.html"){
this.$refs.reserve.backOptionShow = (role || backOptionShow) this.$refs.reserve.backOptionShow = (role || backOptionShow)
this.$refs.reserve.openView(row.id) this.$refs.reserve.openView(row.id)
}, },
async openReserve(data) { openReserve(data) {
this.drawer = false this.drawer = false
data.open_hours = site.reverseRankingDate(data.open_hours) this.$set(data, 'open_hours', site.reverseRankingDate(data.open_hours))
this.reserveViewData = await this.findReserveInfo(data.id) // 进入预约日历前先读取服务端假期,再显示可选日期。
this.reserveViewData.forEach(item => { const calendarRequest = data.workday === true ? this.loadHolidayCalendar() : $.Deferred().resolve().promise()
item.index = 0 calendarRequest.then(() => this.findReserveInfo(data.id))
}) .then((records) => {
this.formData.site_id = data.id this.reserveViewData = records
this.infoViewData = data this.reserveViewData.forEach(item => {
this.$refs.guava.edit() this.$set(item, 'index', 0)
})
this.$set(this.formData, 'site_id', data.id)
this.infoViewData = data
this.$refs.guava.edit()
}, () => {
this.$message.warning('预约数据加载失败,请稍后重试')
})
}, },
async doSearch() { async doSearch() {
this.pageForm.pageNumber = 1 this.pageForm.pageNumber = 1
@@ -997,9 +1073,15 @@ layout("/layouts/platform.html"){
} }
}, "json"); }, "json");
}, },
async findReserveInfo(siteId) { findReserveInfo(siteId) {
const {data} = await $.get("/platform/activity/site/reserve/findReserveInfo", {siteId}) // siteId为场地ID,返回该场地预约记录数组,供预约日历显示。
return data; this.reserveDataLoading = true
return $.post('/platform/activity/site/reserve/findReserveInfo', {siteId: siteId})
.then((res) => {
return res.code === 0 && Array.isArray(res.data) ? res.data : $.Deferred().reject().promise()
}).always(() => {
this.reserveDataLoading = false
})
}, },
async findReserveInfoByClickDay(siteId, day) { async findReserveInfoByClickDay(siteId, day) {
const {data} = await $.get("/platform/activity/site/reserve/findReserveInfo", {siteId: siteId, day: day}) const {data} = await $.get("/platform/activity/site/reserve/findReserveInfo", {siteId: siteId, day: day})