This commit is contained in:
2026-04-01 18:53:08 +08:00
parent dc71dc22d8
commit d799d11f69
34 changed files with 5509 additions and 0 deletions
@@ -0,0 +1,264 @@
package com.budwk.app.zhgh.dayofficework.siteCug.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.sys.models.SysHoliday;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.time.Year;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "场地预约-场馆")
@At("/platform/siteCug/apply")
public class SiteCugApplyController {
private static final String YEARLY_BATCH_PREFIX = "YEARLY_BATCH:";
@Inject
private Dao dao;
@Inject
private SiteCugInfoService infoService;
@Inject
private SiteCugApplyService applyService;
@Inject
private FlowEngine flowEngine;
@At("/")
@SaCheckPermission("siteCug.apply")
@Ok("beetl:/platform/zhgh/dayofficework/siteCug/apply/index.html")
public void index() {}
@At("/h5")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
@Ok("beetl:/platform/zhghh5/dayofficework/siteCug/apply/index.html")
public void h5Index() {}
@At("/form/h5")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
@Ok("beetl:/platform/zhghh5/dayofficework/siteCug/apply/form/index.html")
public void h5Form() {}
@At
@ApiOperation("分页查询场地")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm, @Param("type") String type) {
Cnd cnd = Cnd.NEW();
cnd.and(SiteCugInfo::getState, "=", true);
cnd.andEX(SiteCugInfo::getTypeId, "=", type);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SiteCugInfo::getName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SiteCugInfo::getAddress, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.asc("sortNum * 1");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
Pagination pagination = infoService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
List<NutMap> listMap = pagination.getList(NutMap.class);
List<SiteCugFunctionType> typeList = dao.query(SiteCugFunctionType.class, Cnd.NEW());
Map<String, String> typeMap = typeList.stream().collect(Collectors.toMap(SiteCugFunctionType::getId, SiteCugFunctionType::getName));
for (NutMap map : listMap) {
map.put("typeName", typeMap.get(map.getString("typeId")));
}
return Result.success(pagination);
}
@At
@ApiOperation("提交预约")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
@SLog(tag = "场馆功能管理-场地预约", msg = "提交场地预约")
public Result submit(@Param("data") SiteCugApply apply) {
Map<Boolean, String> validate = applyService.validateApply(apply);
if (validate.containsKey(false)) {
return Result.error(validate.get(false));
}
submitSingleApply(apply);
return Result.success();
}
@At("/submitYearly")
@ApiOperation("按本年后续同星期同时间段批量提交预约")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
@SLog(tag = "场馆功能管理-场地预约", msg = "批量提交本年场地预约")
public Result submitYearly(@Param("data") SiteCugApply apply) {
List<SiteCugApply> applyList = buildYearlyApplyList(apply);
if (applyList.isEmpty()) {
return Result.error("未生成可预约的日期");
}
for (SiteCugApply item : applyList) {
Map<Boolean, String> validate = applyService.validateApply(item);
if (validate.containsKey(false)) {
return Result.error(String.format("%s 预约失败:%s", item.getReserveStartTime(), validate.get(false)));
}
}
String yearlyBatchNo = YEARLY_BATCH_PREFIX + UUID.randomUUID();
for (SiteCugApply item : applyList) {
item.setBackOption(yearlyBatchNo);
submitSingleApply(item);
}
return Result.success(String.format("已成功预约本年剩余%d个时间段", applyList.size()));
}
@At
@ApiOperation("查询预约限制配置")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
public Result timeLimitConfig(@Param("siteId") String siteId) {
if (StrUtil.isBlank(siteId)) {
return Result.success(NutMap.NEW().addv("filterHolidays", false).addv("holidayList", new ArrayList<>()).addv("notApplyTimeList", new ArrayList<>()));
}
SiteCugInfo info = infoService.fetch(siteId);
if (info == null) {
return Result.success(NutMap.NEW().addv("filterHolidays", false).addv("holidayList", new ArrayList<>()).addv("notApplyTimeList", new ArrayList<>()));
}
List<String> holidayList = new ArrayList<>();
if (Boolean.TRUE.equals(info.getFilterHolidays())) {
int currentYear = Year.now().getValue();
List<String> yearPrefixList = List.of(currentYear + "-", (currentYear + 1) + "-");
List<SysHoliday> holidays = dao.query(SysHoliday.class, Cnd.NEW());
holidayList = holidays.stream()
.map(SysHoliday::getDay)
.filter(day -> day != null && yearPrefixList.stream().anyMatch(day::startsWith))
.distinct()
.collect(Collectors.toList());
}
return Result.success(NutMap.NEW()
.addv("filterHolidays", Boolean.TRUE.equals(info.getFilterHolidays()))
.addv("holidayList", holidayList)
.addv("notApplyTimeList", info.getNotApplyTimeList() == null ? new ArrayList<>() : info.getNotApplyTimeList()));
}
private List<SiteCugApply> buildYearlyApplyList(SiteCugApply apply) {
if (apply == null || StrUtil.hasBlank(apply.getReserveStartTime(), apply.getReserveEndTime())) {
return new ArrayList<>();
}
DateTime start = DateUtil.parseDateTime(apply.getReserveStartTime());
DateTime end = DateUtil.parseDateTime(apply.getReserveEndTime());
if (!end.isAfter(start)) {
return new ArrayList<>();
}
SiteCugInfo siteInfo = infoService.fetch(apply.getSiteId());
if (siteInfo == null) {
return new ArrayList<>();
}
List<String> holidayList = new ArrayList<>();
if (Boolean.TRUE.equals(siteInfo.getFilterHolidays())) {
int currentYear = start.year();
List<String> yearPrefixList = List.of(currentYear + "-", (currentYear + 1) + "-");
List<SysHoliday> holidays = dao.query(SysHoliday.class, Cnd.NEW());
holidayList = holidays.stream()
.map(SysHoliday::getDay)
.filter(day -> day != null && yearPrefixList.stream().anyMatch(day::startsWith))
.distinct()
.collect(Collectors.toList());
}
int targetWeek = start.dayOfWeek() - 1;
int startHour = start.hour(true);
int startMinute = start.minute();
int startSecond = start.second();
int endHour = end.hour(true);
int endMinute = end.minute();
int endSecond = end.second();
Date current = DateUtil.beginOfDay(start);
Date endOfYear = DateUtil.endOfYear(start);
List<SiteCugApply> result = new ArrayList<>();
while (current.compareTo(endOfYear) <= 0) {
DateTime currentDate = DateUtil.date(current);
String currentDay = DateUtil.formatDate(currentDate);
if (currentDate.dayOfWeek() - 1 == targetWeek && !holidayList.contains(currentDay)) {
SiteCugApply item = copyApply(apply);
item.setId(null);
item.setReserveStartTime(DateUtil.formatDateTime(buildDateTime(currentDate, startHour, startMinute, startSecond)));
item.setReserveEndTime(DateUtil.formatDateTime(buildDateTime(currentDate, endHour, endMinute, endSecond)));
result.add(item);
}
current = DateUtil.offsetDay(current, 1);
}
return result;
}
private DateTime buildDateTime(DateTime date, int hour, int minute, int second) {
return DateUtil.parseDateTime(String.format("%s %02d:%02d:%02d", DateUtil.formatDate(date), hour, minute, second));
}
private SiteCugApply copyApply(SiteCugApply apply) {
return new SiteCugApply()
.setSiteId(apply.getSiteId())
.setApplyUserId(apply.getApplyUserId())
.setApplyUserName(apply.getApplyUserName())
.setApplySex(apply.getApplySex())
.setApplyLoginName(apply.getApplyLoginName())
.setApplyUnitId(apply.getApplyUnitId())
.setApplyUnitName(apply.getApplyUnitName())
.setReserveType(apply.getReserveType())
.setApplyUnionId(apply.getApplyUnionId())
.setApplyUnionName(apply.getApplyUnionName())
.setClubId(apply.getClubId())
.setClubName(apply.getClubName())
.setApplyMobile(apply.getApplyMobile())
.setReserveStartTime(apply.getReserveStartTime())
.setReserveEndTime(apply.getReserveEndTime())
.setJoinCount(apply.getJoinCount())
.setApplyCause(apply.getApplyCause())
.setBackOption(apply.getBackOption());
}
private void submitSingleApply(SiteCugApply apply) {
dao.insertOrUpdate(apply);
Dict args = Dict.create();
args.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode());
args.set(FlowConst.FORM_DATA, apply);
ProcessInstance instance = flowEngine.startProcessInstanceByKey("CDYY", apply.getId(), SecurityUtil.getUserId(), args);
List<ProcessTask> doingTaskList = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null);
for (ProcessTask task : doingTaskList) {
flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args);
}
}
}
@@ -0,0 +1,85 @@
package com.budwk.app.zhgh.dayofficework.siteCug.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugFunctionTypeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.util.cri.SqlExpressionGroup;
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 java.util.List;
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "功能类型")
@At("/platform/siteCug/function/type")
public class SiteCugFunctionTypeController {
@Inject
private SiteCugFunctionTypeService typeService;
@At("")
@SaCheckPermission("siteCug.function.type")
@Ok("beetl:/platform/zhgh/dayofficework/siteCug/type/index.html")
public void index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission("siteCug.function.type")
public Result pageData(PageForm pageForm) {
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SiteCugFunctionType::getName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SiteCugFunctionType::getCode, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.asc("code");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
Pagination pagination = typeService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
@At
@ApiOperation("新增/修改功能类型")
@SaCheckPermission("siteCug.function.type")
@SLog(tag = "场馆功能管理-功能类型", msg = "新增/修改功能类型")
public Object submit(SiteCugFunctionType type) {
typeService.insertOrUpdate(type);
return Result.success();
}
@At
@ApiOperation("删除功能类型")
@SaCheckPermission("siteCug.function.type")
@SLog(tag = "场馆功能管理-功能类型", msg = "删除功能类型")
public Object delete(String id) {
typeService.delete(id);
return Result.success();
}
@At
@ApiOperation("查询功能类型")
@SaCheckLogin
public Result queryFunctionType() {
List<SiteCugFunctionType> list = typeService.query(Cnd.where(SiteCugFunctionType::getEnable, "=", true).asc(SiteCugFunctionType::getCode));
return Result.success(list);
}
}
@@ -0,0 +1,132 @@
package com.budwk.app.zhgh.dayofficework.siteCug.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
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 java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "场地管理-场馆")
@At("/platform/siteCug/manage")
public class SiteCugManageController {
@Inject
private Dao dao;
@Inject
private SiteCugInfoService infoService;
@At("")
@SaCheckPermission("siteCug.manage")
@Ok("beetl:/platform/zhgh/dayofficework/siteCug/manage/index.html")
public void index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission("siteCug.manage")
public Result pageData(PageForm pageForm, @Param("type") String type) {
Cnd cnd = Cnd.NEW();
cnd.andEX(SiteCugInfo::getTypeId, "=", type);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SiteCugInfo::getName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SiteCugInfo::getAddress, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.asc("sortNum * 1");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
Pagination pagination = infoService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
List<NutMap> listMap = pagination.getList(NutMap.class);
List<SiteCugFunctionType> typeList = dao.query(SiteCugFunctionType.class, Cnd.NEW());
Map<String, String> typeMap = typeList.stream().collect(Collectors.toMap(SiteCugFunctionType::getId, SiteCugFunctionType::getName));
for (NutMap map : listMap) {
map.put("typeName", typeMap.get(map.getString("typeId")));
}
return Result.success(pagination);
}
@At
@ApiOperation("新增/修改场地")
@SaCheckPermission("siteCug.manage")
@SLog(tag = "场馆功能管理-场地管理", msg = "新增/修改场地")
public Object submit(@Param("data") SiteCugInfo info) {
List<NutMap> notApplyTimeList = info.getNotApplyTimeList();
if (notApplyTimeList != null) {
boolean invalid = notApplyTimeList.stream().anyMatch(item ->
item == null
|| StrUtil.isBlank(item.getString("date"))
|| StrUtil.isBlank(item.getString("startTime"))
|| StrUtil.isBlank(item.getString("endTime"))
|| item.getString("startTime").compareTo(item.getString("endTime")) >= 0);
if (invalid) {
return Result.error("禁用时间设置有误");
}
}
if (info.getFilterHolidays() == null) {
info.setFilterHolidays(false);
}
infoService.insertOrUpdate(info);
return Result.success();
}
@At
@ApiOperation("删除场地")
@SaCheckPermission("siteCug.manage")
@SLog(tag = "场馆功能管理-场地管理", msg = "删除场地")
public Object delete(String id) {
infoService.delete(id);
return Result.success();
}
@At
@ApiOperation("查询场地")
@SaCheckLogin
public Result querySites() {
List<SiteCugInfo> list = infoService.query(Cnd.where(SiteCugInfo::getState, "=", true).desc(SiteCugInfo::getSortNum));
return Result.success(list);
}
@At
@ApiOperation("查询单个场地")
@SaCheckLogin
public Result info(String id) {
SiteCugInfo info = infoService.fetch(id);
if (info == null) {
return Result.success(null);
}
NutMap map = Lang.obj2nutmap(info);
SiteCugFunctionType type = dao.fetch(SiteCugFunctionType.class, info.getTypeId());
map.put("typeName", type == null ? "" : type.getName());
return Result.success(map);
}
}
@@ -0,0 +1,184 @@
package com.budwk.app.zhgh.dayofficework.siteCug.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "场地预约-我的预约")
@At("/platform/siteCug/mine")
public class SiteCugMineController {
private static final String YEARLY_BATCH_PREFIX = "YEARLY_BATCH:";
@Inject
private FlowEngine flowEngine;
@Inject
private SiteCugApplyService applyService;
@At("/")
@Ok("beetl:/platform/zhgh/dayofficework/siteCug/mine/index.html")
@SaCheckPermission("siteCug.mine")
public void index() {
}
@At("/h5")
@Ok("beetl:/platform/zhghh5/dayofficework/siteCug/mine/index.html")
@SaCheckPermission(value = {"siteCug.mine", "h5.siteCug.mine"}, mode = SaMode.OR)
public void h5Index() {
}
@At
@ApiOperation("分页查询我的预约")
@SaCheckPermission(value = {"siteCug.mine", "h5.siteCug.mine"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param("siteType") String siteType,
@Param("siteId") String siteId) {
Sql sql = Sqls.create("""
SELECT
info.*,
si.name as siteName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
CASE
WHEN ins.state in (10, 20) AND (t.displayName LIKE '%校工会允许%' OR t.taskName = 'c8c03407-cf26-4e0b-82f1-b2afc9c79996')
THEN 1
ELSE 0
END canCancel,
IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = t.taskParentId) = 'startTask', 1, 0) canRevoke,
(select MAX(id) from wf_process_task where processInstanceId = ins.id and taskName = 'startTask') AS startTaskId,
CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN 1 ELSE 0 END AS yearlyBatch,
CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN (
SELECT COUNT(1) FROM site_cug_apply batch_info WHERE batch_info.backOption = info.backOption AND batch_info.createdBy = info.createdBy
) ELSE 1 END AS batchDeleteCount
FROM
site_cug_apply info
LEFT JOIN site_cug_info si ON info.siteId = si.id
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
$condition
""");
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("si.name", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("si.typeId", "=", siteType);
cnd.andEX("info.siteId", "=", siteId);
cnd.and("info.createdBy", "=", SecurityUtil.getUserId());
if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
} else {
cnd.desc("info.createdAt");
}
cnd.groupBy("info.id");
sql.setCondition(cnd);
Pagination<NutMap> pagination = applyService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@ApiOperation("取消我的预约")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"siteCug.mine", "h5.siteCug.mine"}, mode = SaMode.OR)
@SLog(tag = "场馆功能管理-我的预约", msg = "取消场地预约")
public Result delete(@Param("id") String id) {
SiteCugApply apply = applyService.fetch(id);
if (apply == null) {
return Result.error("预约记录不存在");
}
if (!StrUtil.equals(apply.getCreatedBy(), SecurityUtil.getUserId())) {
return Result.error("无权取消该预约");
}
List<String> deleteIds = new ArrayList<>();
if (StrUtil.isNotBlank(apply.getBackOption()) && apply.getBackOption().startsWith(YEARLY_BATCH_PREFIX)) {
List<SiteCugApply> batchList = applyService.query(Cnd.where("backOption", "=", apply.getBackOption()).and("createdBy", "=", SecurityUtil.getUserId()));
deleteIds.addAll(batchList.stream().map(SiteCugApply::getId).toList());
} else {
deleteIds.add(id);
}
if (deleteIds.isEmpty()) {
return Result.error("未找到可取消的预约记录");
}
for (String deleteId : deleteIds) {
Sql sql = Sqls.create("""
SELECT
CASE
WHEN ins.state in (10, 20) AND (t.displayName LIKE '%校工会允许%' OR t.taskName = 'c8c03407-cf26-4e0b-82f1-b2afc9c79996')
THEN 1
ELSE 0
END canCancel
FROM
site_cug_apply info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
WHERE info.id = @id
""");
sql.params().set("id", deleteId);
sql.setCallback(Sqls.callback.maps());
applyService.dao().execute(sql);
List<NutMap> list = sql.getList(NutMap.class);
boolean canCancel = list != null && !list.isEmpty() && list.get(0).getInt("canCancel", 0) == 1;
if (!canCancel) {
return Result.error("当前批次中存在不可取消的预约,无法整批删除");
}
}
for (String deleteId : deleteIds) {
applyService.delete(deleteId);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(deleteId);
}
if (deleteIds.size() > 1) {
return Result.success("已取消该批次下全部预约记录");
}
return Result.success("取消成功");
}
}
@@ -0,0 +1,98 @@
package com.budwk.app.zhgh.dayofficework.siteCug.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply;
import com.budwk.app.zhgh.dayofficework.siteCug.param.SiteCugRecordPageForm;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@IocBean
@Ok("json:full")
@Api(tags = "场馆预约查询")
@At("/platform/siteCug/record")
public class SiteCugRecordController {
@Inject
private FlowEngine flowEngine;
@Inject
private SiteCugApplyService applyService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/siteCug/record/index.html")
@SaCheckPermission("siteCug.record")
public void index() {
}
@At
@ApiOperation("分页查询")
@SaCheckPermission("siteCug.record")
public Result pageData(SiteCugRecordPageForm pageForm) {
// 查询页只负责调用 service 公共查询方法,controller 不再维护重复 SQL/Cnd 逻辑
Sql sql = applyService.buildRecordSql();
Cnd cnd = applyService.buildRecordCnd(pageForm);
sql.setCondition(cnd);
Pagination<NutMap> pageVO = applyService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
@At
@ApiOperation("删除预约记录")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("siteCug.record")
public Result delete(@Param("id") String id) {
SiteCugApply apply = applyService.fetch(id);
if (apply == null) {
return Result.error("记录不存在");
}
Sql sql = Sqls.create("""
SELECT
ins.state AS instanceState
FROM
site_cug_apply info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
WHERE info.id = @id
""");
sql.params().set("id", id);
sql.setCallback(Sqls.callback.maps());
applyService.dao().execute(sql);
List<NutMap> list = sql.getList(NutMap.class);
Integer state = list != null && !list.isEmpty() ? list.get(0).getInt("instanceState") : null;
// 预约查询页只展示审核结束数据,这里也只允许删除已完成/已拒绝的流程记录
if (state == null || !List.of(ProcessInstanceStateEnum.FINISHED.getCode(), ProcessInstanceStateEnum.REJECT.getCode()).contains(state)) {
return Result.error("仅可删除审核结束的预约记录");
}
applyService.delete(id);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
return Result.success();
}
@At
@Ok("void")
@ApiOperation("导出预约记录")
@SaCheckPermission("siteCug.record")
public void doExport(SiteCugRecordPageForm pageForm, HttpServletResponse response) {
// 导出逻辑统一下沉到 service,导出结果和当前筛选条件保持一致
applyService.exportRecord(pageForm, response);
}
}
@@ -0,0 +1,215 @@
package com.budwk.app.zhgh.dayofficework.siteCug.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.json.Json;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.List;
@IocBean
@At("/platform/siteCug/schoolUnionAudit")
@Api("场馆校工会审核")
@Ok("json:full")
public class SiteCugSchoolUnionAuditController {
private static final String YEARLY_BATCH_PREFIX = "YEARLY_BATCH:";
@Inject
private SiteCugApplyService applyService;
@Inject
private ProcessTaskService processTaskService;
@Inject
private FlowCommonService flowCommonService;
@At("/")
@Ok("beetl:/platform/zhgh/dayofficework/siteCug/schoolUnionAudit/index.html")
@SaCheckPermission("siteCug.schoolUnionAudit")
public void index() {
}
@At("/h5")
@Ok("beetl:/platform/zhghh5/dayofficework/siteCug/schoolUnionAudit/index.html")
@SaCheckPermission("h5.siteCug.schoolUnionAudit")
public void h5Index() {
}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"siteCug.schoolUnionAudit", "h5.siteCug.schoolUnionAudit"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param("siteType") String siteType,
@Param("siteId") String siteId,
@Param("approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
si.name as siteName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
CASE WHEN EXISTS (
SELECT 1 FROM wf_process_task rt
WHERE rt.processInstanceId = ins.id AND rt.taskParentId = t.id
) THEN 1 ELSE 0 END AS canRevoke,
CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN 1 ELSE 0 END AS yearlyBatch,
CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN (
SELECT COUNT(1) FROM site_cug_apply batch_info WHERE batch_info.backOption = info.backOption AND batch_info.createdBy = info.createdBy
) ELSE 1 END AS batchAuditCount
FROM
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN site_cug_apply info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN site_cug_info si ON info.siteId = si.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id
$condition
""");
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("si.name", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("si.typeId", "=", siteType);
cnd.andEX("info.siteId", "=", siteId);
cnd.and("t.taskName", "=", "c8c03407-cf26-4e0b-82f1-b2afc9c79996");
if (!StpUtil.hasRole(RoleConstant.SYSADMIN.name())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
}
if (Boolean.TRUE.equals(approval)) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
} else {
cnd.desc("info.createdAt");
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = applyService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
@At("/executeTask")
@ApiOperation("执行审核任务")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"siteCug.schoolUnionAudit", "h5.siteCug.schoolUnionAudit"}, mode = SaMode.OR)
public Result executeTask(@Param("data") String data) {
Dict args = Json.fromJson(Dict.class, data);
Long processTaskId = args.getLong(FlowConst.PROCESS_TASK_ID_KEY);
ProcessTask currentTask = processTaskService.getById(processTaskId);
if (currentTask == null) {
return Result.error("审核任务不存在");
}
SiteCugApply apply = applyService.fetch(args.getStr("applyId"));
if (apply == null) {
return Result.error("预约记录不存在");
}
List<ProcessTask> taskList = new ArrayList<>();
if (StrUtil.isNotBlank(apply.getBackOption()) && apply.getBackOption().startsWith(YEARLY_BATCH_PREFIX)) {
List<SiteCugApply> batchList = applyService.query(Cnd.where("backOption", "=", apply.getBackOption()).and("createdBy", "=", apply.getCreatedBy()));
List<String> bizIds = batchList.stream().map(SiteCugApply::getId).toList();
taskList = processTaskService.getDoingTaskByBizIdTaskName(bizIds, currentTask.getTaskName());
}
if (taskList.isEmpty()) {
taskList.add(currentTask);
}
for (ProcessTask task : taskList) {
Dict cloneArgs = args.clone();
cloneArgs.put(FlowConst.PROCESS_TASK_ID_KEY, task.getId());
flowCommonService.executeTask(cloneArgs);
}
if (taskList.size() > 1) {
return Result.success("已完成该全年预约批次的审核");
}
return Result.success("提交成功");
}
@At("/revokeTask")
@ApiOperation("撤回审核任务")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"siteCug.schoolUnionAudit", "h5.siteCug.schoolUnionAudit"}, mode = SaMode.OR)
public Result revokeTask(@Param("taskId") Long taskId, @Param("applyId") String applyId) {
ProcessTask currentTask = processTaskService.getById(taskId);
if (currentTask == null) {
return Result.error("撤回任务不存在");
}
SiteCugApply apply = applyService.fetch(applyId);
if (apply == null) {
return Result.error("预约记录不存在");
}
List<ProcessTask> taskList = new ArrayList<>();
if (StrUtil.isNotBlank(apply.getBackOption()) && apply.getBackOption().startsWith(YEARLY_BATCH_PREFIX)) {
List<SiteCugApply> batchList = applyService.query(Cnd.where("backOption", "=", apply.getBackOption()).and("createdBy", "=", apply.getCreatedBy()));
List<String> bizIds = batchList.stream().map(SiteCugApply::getId).toList();
taskList = processTaskService.getDoneTaskByBizIdTaskName(bizIds, currentTask.getTaskName());
}
if (taskList.isEmpty()) {
taskList.add(currentTask);
}
for (ProcessTask task : taskList) {
flowCommonService.revokeTask(task.getId());
}
if (taskList.size() > 1) {
return Result.success("已撤回该全年预约批次的审核任务");
}
return Result.success("撤回成功");
}
}
@@ -0,0 +1,112 @@
package com.budwk.app.zhgh.dayofficework.siteCug.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
@Data
@Comment("场地预约-场馆")
@Accessors(chain = true)
@Table("site_cug_apply")
@EqualsAndHashCode(callSuper = true)
public class SiteCugApply extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("场地id")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String siteId;
@Column
@Comment("预约人")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyUserId;
@Column
@Comment("预约人姓名")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String applyUserName;
@Column
@Comment("性别")
@ColDefine(type = ColType.VARCHAR, width = 10)
private String applySex;
@Column
@Comment("预约人工号")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String applyLoginName;
@Column
@Comment("预约人单位id")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyUnitId;
@Column
@Comment("预约人单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String applyUnitName;
@Column
@Comment("预约类型")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String reserveType;
@Column
@Comment("分工会id")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyUnionId;
@Column
@Comment("分工会名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String applyUnionName;
@Column
@Comment("协会id")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String clubId;
@Column
@Comment("协会名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String clubName;
@Column
@Comment("预约人联系方式")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String applyMobile;
@Column
@Comment("预约开始时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String reserveStartTime;
@Column
@Comment("预约结束时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String reserveEndTime;
@Column
@Comment("预约人数")
@ColDefine(type = ColType.INT)
private Integer joinCount;
@Column
@Comment("预约事由")
@ColDefine(type = ColType.VARCHAR, width = 1000)
private String applyCause;
@Column
@Comment("反馈意见")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String backOption;
}
@@ -0,0 +1,43 @@
package com.budwk.app.zhgh.dayofficework.siteCug.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
@Data
@Comment("功能类型")
@Accessors(chain = true)
@Table("site_cug_function_type")
@EqualsAndHashCode(callSuper = true)
public class SiteCugFunctionType extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@ColDefine(type = ColType.VARCHAR, width = 30)
@Comment("类型编码")
private String code;
@Column
@ColDefine(type = ColType.VARCHAR, width = 100)
@Comment("类型名称")
private String name;
@Column
@ColDefine(type = ColType.BOOLEAN)
@Comment("是否启用")
@Default("1")
private Boolean enable;
@Column
@ColDefine(type = ColType.INT)
@Comment("排序编号")
private Integer sortNum;
}
@@ -0,0 +1,97 @@
package com.budwk.app.zhgh.dayofficework.siteCug.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import org.nutz.lang.util.NutMap;
import java.util.List;
@Data
@Comment("场地信息-场馆")
@Accessors(chain = true)
@Table("site_cug_info")
@EqualsAndHashCode(callSuper = true)
public class SiteCugInfo extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("创建人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String createUserId;
@Column
@Comment("创建人")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String createUserName;
@Column
@Comment("排序编号")
@ColDefine(type = ColType.INT)
private Integer sortNum;
@Column
@Comment("场地名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String name;
@Column
@Comment("场地地址")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String address;
@Column
@Comment("联系人")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String contactName;
@Column
@Comment("联系电话")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String contactPhone;
@Column
@Comment("可容纳人数")
@ColDefine(type = ColType.INT)
private Integer maxNum;
@Column
@Comment("场地类型")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String typeId;
@Column
@Comment("性别限制(0不限,1男,2女)")
@ColDefine(type = ColType.INT)
private Integer sexLimit;
@Column
@Comment("场地的禁用时间")
@ColDefine(type = ColType.MYSQL_JSON)
private List<NutMap> notApplyTimeList;
@Column
@Comment("开启状态")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean state;
@Column
@Comment("排除节假日")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean filterHolidays;
@Column
@Comment("场地介绍")
@ColDefine(type = ColType.TEXT)
private String introduce;
}
@@ -0,0 +1,31 @@
package com.budwk.app.zhgh.dayofficework.siteCug.param;
import com.budwk.app.base.param.PageForm;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ApiModel("场馆预约查询参数")
@EqualsAndHashCode(callSuper = true)
@Data
public class SiteCugRecordPageForm extends PageForm {
@ApiModelProperty("场地类型ID")
private String siteType;
@ApiModelProperty("场地ID")
private String siteId;
@ApiModelProperty("预约类型")
private String reserveType;
@ApiModelProperty("预约单位关键字")
private String reserveTargetKeyword;
@ApiModelProperty("预约时间开始")
private String reserveTimeStart;
@ApiModelProperty("预约时间结束")
private String reserveTimeEnd;
}
@@ -0,0 +1,24 @@
package com.budwk.app.zhgh.dayofficework.siteCug.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply;
import com.budwk.app.zhgh.dayofficework.siteCug.param.SiteCugRecordPageForm;
import org.nutz.dao.Cnd;
import org.nutz.dao.sql.Sql;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
public interface SiteCugApplyService extends BaseService<SiteCugApply> {
Map<Boolean, String> validateApply(SiteCugApply apply);
// 场馆预约查询页和导出共用同一套查询字段定义,统一由 service 生成 SQL
Sql buildRecordSql();
// 场馆预约查询页和导出的筛选条件统一由 service 组装,避免 controller 重复维护
Cnd buildRecordCnd(SiteCugRecordPageForm pageForm);
// 场馆预约查询页的导出逻辑统一放在 service,controller 只负责接收请求
void exportRecord(SiteCugRecordPageForm pageForm, HttpServletResponse response);
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.siteCug.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType;
public interface SiteCugFunctionTypeService extends BaseService<SiteCugFunctionType> {
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.siteCug.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
public interface SiteCugInfoService extends BaseService<SiteCugInfo> {
}
@@ -0,0 +1,321 @@
package com.budwk.app.zhgh.dayofficework.siteCug.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.sys.models.SysHoliday;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.club.model.SysClub;
import com.budwk.app.zhgh.club.service.SysClubService;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
import com.budwk.app.zhgh.dayofficework.siteCug.param.SiteCugRecordPageForm;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@IocBean(args = {"refer:dao"})
public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> implements SiteCugApplyService {
@Inject
private SysClubService sysClubService;
public SiteCugApplyServiceImpl(Dao dao) {
super(dao);
}
@Override
public Map<Boolean, String> validateApply(SiteCugApply apply) {
// 预约类型校验和自动回填:
// 1. 分工会预约时,强制读取当前登录人的分工会信息
// 2. 协会预约时,只允许选择当前登录人实际管理的协会
Map<Boolean, String> reserveTypeValidate = fillReserveTypeData(apply);
if (reserveTypeValidate.containsKey(false)) {
return reserveTypeValidate;
}
if (StrUtil.isBlank(apply.getReserveStartTime()) || StrUtil.isBlank(apply.getReserveEndTime())) {
return Map.of(false, "预约时间不能为空");
}
if (!DateUtil.parseDateTime(apply.getReserveEndTime()).isAfter(DateUtil.parseDateTime(apply.getReserveStartTime()))) {
return Map.of(false, "结束时间必须晚于开始时间");
}
if (!DateUtil.parseDateTime(apply.getReserveStartTime()).isAfter(DateUtil.date())) {
return Map.of(false, "预约开始时间必须晚于当前时间");
}
SiteCugInfo siteInfo = dao().fetch(SiteCugInfo.class, apply.getSiteId());
if (siteInfo == null) {
return Map.of(false, "场地不存在");
}
if (!Boolean.TRUE.equals(siteInfo.getState())) {
return Map.of(false, "该场地未开启预约");
}
if (apply.getJoinCount() == null || apply.getJoinCount() <= 0) {
return Map.of(false, "预约人数必须大于0");
}
if (apply.getJoinCount() > siteInfo.getMaxNum()) {
return Map.of(false, "预约人数最多为%s人".formatted(siteInfo.getMaxNum()));
}
// 周六、周日整天不可预约,避免只做前端限制被绕过
if (containsWeekend(apply.getReserveStartTime(), apply.getReserveEndTime())) {
return Map.of(false, "预约时间不能落在周六或周日");
}
// 开启排除节假日后,节假日整天不可预约
if (Boolean.TRUE.equals(siteInfo.getFilterHolidays()) && containsHoliday(apply.getReserveStartTime(), apply.getReserveEndTime())) {
return Map.of(false, "预约时间不能落在节假日");
}
// 只要预约时间范围和禁用时间段有交叉,就不允许预约
if (intersectsDisabledTime(siteInfo, apply.getReserveStartTime(), apply.getReserveEndTime())) {
return Map.of(false, "预约时间落在禁用时间内");
}
Sql sql = Sqls.create("""
select
sa.*
from
site_cug_apply sa
left join wf_process_instance ins on ins.businessNo = sa.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and(SiteCugApply::getSiteId, "=", apply.getSiteId());
cnd.andEX("sa.id", "!=", apply.getId());
List<Integer> stateList = List.of(ProcessInstanceStateEnum.DOING.getCode(), ProcessInstanceStateEnum.FINISHED.getCode(), ProcessInstanceStateEnum.PENDING.getCode());
cnd.and(ProcessInstance::getState, "in", stateList);
sql.setCondition(cnd);
List<SiteCugApply> applyList = listEntity(sql);
// 只要已有预约和当前申请时间段有交叉,并且流程状态是待审核/办理中/已通过,就直接判定冲突
for (SiteCugApply item : applyList) {
boolean overlap = DateUtil.parseDateTime(item.getReserveStartTime()).isBefore(DateUtil.parseDateTime(apply.getReserveEndTime()))
&& DateUtil.parseDateTime(item.getReserveEndTime()).isAfter(DateUtil.parseDateTime(apply.getReserveStartTime()));
if (overlap) {
if (StrUtil.isBlank(apply.getId()) && StrUtil.equals(item.getApplyUserId(), SecurityUtil.getUserId())) {
return Map.of(false, "该时间段您已存在预约");
}
return Map.of(false, String.format("预约时间冲突,%s 至 %s 已被预约,请重新选择", item.getReserveStartTime(), item.getReserveEndTime()));
}
}
return Map.of(true, "");
}
@Override
public Sql buildRecordSql() {
// 场馆预约查询页和导出页共用同一套返回字段,统一在 service 中维护,避免 controller 重复拼 SQL
return Sqls.create("""
SELECT
info.*,
si.name AS siteName,
ins.id AS instanceId,
ins.processDefineId instanceProcessDefineId,
CASE WHEN info.reserveType = 'club' THEN '协会预约' ELSE '分工会预约' END AS reserveTypeName,
CASE WHEN info.reserveType = 'club' THEN info.clubName ELSE info.applyUnionName END AS reserveTargetName,
DATE_FORMAT(info.createdAt, '%Y-%m-%d %H:%i:%s') AS applyTime
FROM
site_cug_apply info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN site_cug_info si ON info.siteId = si.id
$condition
""");
}
@Override
public Cnd buildRecordCnd(SiteCugRecordPageForm pageForm) {
// 场馆预约查询页和导出共用同一套筛选条件,保证列表看见什么,导出就拿到什么
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("si.name", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (StrUtil.isNotBlank(pageForm.getReserveTargetKeyword())) {
// 预约单位支持同时匹配分工会名称和协会名称,方便直接查某个预约主体的全部记录
SqlExpressionGroup reserveTargetSeg = new SqlExpressionGroup();
reserveTargetSeg.or("info.applyUnionName", "like", "%" + pageForm.getReserveTargetKeyword() + "%");
reserveTargetSeg.or("info.clubName", "like", "%" + pageForm.getReserveTargetKeyword() + "%");
cnd.and(reserveTargetSeg);
}
if (StrUtil.isNotBlank(pageForm.getReserveType())) {
cnd.andEX("info.reserveType", "=", pageForm.getReserveType());
}
if (StrUtil.isNotBlank(pageForm.getReserveTimeStart())) {
// 按预约时间查询时,开始边界取“预约结束时间大于等于筛选开始”,这样跨段预约也能查出来
cnd.andEX("info.reserveEndTime", ">=", pageForm.getReserveTimeStart());
}
if (StrUtil.isNotBlank(pageForm.getReserveTimeEnd())) {
// 按预约时间查询时,结束边界取“预约开始时间小于等于筛选结束”,保证时间段有交叉就能命中
cnd.andEX("info.reserveStartTime", "<=", pageForm.getReserveTimeEnd());
}
cnd.andEX("si.typeId", "=", pageForm.getSiteType());
cnd.andEX("info.siteId", "=", pageForm.getSiteId());
// 预约查询页固定只看审核结束记录;导出复用同一条件,实现所见即所得
cnd.and("ins.state", "in", List.of(ProcessInstanceStateEnum.FINISHED.getCode(), ProcessInstanceStateEnum.REJECT.getCode()));
if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
} else {
cnd.desc("info.createdAt");
}
cnd.groupBy("info.id");
return cnd;
}
@Override
public void exportRecord(SiteCugRecordPageForm pageForm, HttpServletResponse response) {
// 导出直接复用列表同一套查询条件和字段定义,保证导出结果与当前列表查询结果一致
Sql sql = buildRecordSql();
Cnd cnd = buildRecordCnd(pageForm);
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
dao().execute(sql);
List<NutMap> list = sql.getList(NutMap.class);
ExportParams exportParams = new ExportParams("预约查询", "预约查询");
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, buildRecordExportEntities(), list);
CommonDownloadUtil.download("场馆预约查询.xlsx", workbook, response);
}
// 场馆预约查询导出字段统一在 service 中定义,并与当前列表展示字段保持一致
private List<ExcelExportEntity> buildRecordExportEntities() {
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("场地名称", "siteName", 20));
exportEntities.add(new ExcelExportEntity("预约人", "applyUserName", 20));
exportEntities.add(new ExcelExportEntity("预约类型", "reserveTypeName", 20));
exportEntities.add(new ExcelExportEntity("预约单位", "reserveTargetName", 20));
exportEntities.add(new ExcelExportEntity("所属单位", "applyUnitName", 20));
exportEntities.add(new ExcelExportEntity("预约开始时间", "reserveStartTime", 24));
exportEntities.add(new ExcelExportEntity("预约结束时间", "reserveEndTime", 24));
exportEntities.add(new ExcelExportEntity("联系方式", "applyMobile", 20));
return exportEntities;
}
// 预约时间范围内只要包含周六或周日,就视为不可预约
private boolean containsWeekend(String reserveStartTime, String reserveEndTime) {
Date start = DateUtil.beginOfDay(DateUtil.parseDateTime(reserveStartTime));
Date end = DateUtil.beginOfDay(DateUtil.parseDateTime(reserveEndTime));
Date current = start;
while (current.compareTo(end) <= 0) {
DateTime currentDay = DateUtil.date(current);
int week = currentDay.dayOfWeek() - 1;
if (week == 0 || week == 6) {
return true;
}
current = DateUtil.offsetDay(current, 1);
}
return false;
}
// 预约时间范围内只要包含节假日,就视为不可预约
private boolean containsHoliday(String reserveStartTime, String reserveEndTime) {
Date start = DateUtil.beginOfDay(DateUtil.parseDateTime(reserveStartTime));
Date end = DateUtil.beginOfDay(DateUtil.parseDateTime(reserveEndTime));
List<String> dayList = new ArrayList<>();
Date current = start;
while (current.compareTo(end) <= 0) {
dayList.add(DateUtil.formatDate(current));
current = DateUtil.offsetDay(current, 1);
}
if (dayList.isEmpty()) {
return false;
}
List<SysHoliday> holidays = dao().query(SysHoliday.class, Cnd.NEW());
Set<String> holidaySet = holidays.stream().map(SysHoliday::getDay).filter(StrUtil::isNotBlank).collect(Collectors.toSet());
return dayList.stream().anyMatch(holidaySet::contains);
}
// 预约时间范围只要与后台配置的禁用时间段有交叉,就直接拦截
private boolean intersectsDisabledTime(SiteCugInfo siteInfo, String reserveStartTime, String reserveEndTime) {
List<NutMap> timeRanges = siteInfo.getNotApplyTimeList();
if (timeRanges == null || timeRanges.isEmpty()) {
return false;
}
DateTime reserveStart = DateUtil.parseDateTime(reserveStartTime);
DateTime reserveEnd = DateUtil.parseDateTime(reserveEndTime);
for (NutMap range : timeRanges) {
if (range == null || StrUtil.hasBlank(range.getString("date"), range.getString("startTime"), range.getString("endTime"))) {
continue;
}
DateTime disabledStart = DateUtil.parseDateTime(range.getString("date") + " " + range.getString("startTime") + ":00");
DateTime disabledEnd = DateUtil.parseDateTime(range.getString("date") + " " + range.getString("endTime") + ":00");
boolean overlap = reserveStart.isBefore(disabledEnd) && reserveEnd.isAfter(disabledStart);
if (overlap) {
return true;
}
}
return false;
}
// 按预约类型补齐最终落库数据,避免前端传错或被绕过
private Map<Boolean, String> fillReserveTypeData(SiteCugApply apply) {
if (StrUtil.isBlank(apply.getReserveType())) {
return Map.of(false, "请选择预约类型");
}
if (StrUtil.equals(apply.getReserveType(), "union")) {
String unionId = SecurityUtil.getUnionId();
if (StrUtil.isBlank(unionId)) {
return Map.of(false, "当前用户未关联分工会,不能发起分工会预约");
}
Sys_union union = dao().fetch(Sys_union.class, unionId);
if (union == null) {
return Map.of(false, "当前用户分工会信息不存在,请联系管理员核查");
}
apply.setApplyUnionId(union.getId());
apply.setApplyUnionName(union.getName());
apply.setClubId("");
apply.setClubName("");
return Map.of(true, "");
}
if (StrUtil.equals(apply.getReserveType(), "club")) {
if (StrUtil.isBlank(apply.getClubId())) {
return Map.of(false, "请选择协会");
}
// 协会预约直接复用项目现成能力;getMyManageClub() 对 SYSADMIN 已放开,因此超级管理员也能代任意协会预约
List<SysClub> myManageClub = sysClubService.getMyManageClub();
SysClub club = myManageClub.stream()
.filter(item -> StrUtil.equals(item.getId(), apply.getClubId()))
.findFirst()
.orElse(null);
if (club == null) {
return Map.of(false, "所选协会不是您当前管理的协会");
}
apply.setClubName(club.getClubName());
apply.setApplyUnionId("");
apply.setApplyUnionName("");
return Map.of(true, "");
}
return Map.of(false, "预约类型不合法");
}
}
@@ -0,0 +1,17 @@
package com.budwk.app.zhgh.dayofficework.siteCug.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugFunctionTypeService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@Slf4j
@IocBean(args = {"refer:dao"})
public class SiteCugFunctionTypeServiceImpl extends BaseServiceImpl<SiteCugFunctionType> implements SiteCugFunctionTypeService {
public SiteCugFunctionTypeServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,17 @@
package com.budwk.app.zhgh.dayofficework.siteCug.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@Slf4j
@IocBean(args = {"refer:dao"})
public class SiteCugInfoServiceImpl extends BaseServiceImpl<SiteCugInfo> implements SiteCugInfoService {
public SiteCugInfoServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,531 @@
const apply = {
template: /*language=HTML*/ `
<div>
<el-row :gutter="20" class="mb10">
<el-col :span="24" style="text-align: center">
<div style="font-size: large;color: #303133">
您正在预约【<span style="color: #409EFF">{{ row.name }}</span>】活动场地
</div>
</el-col>
</el-row>
<el-form :model="formData" ref="formRef" :rules="formRules" label-position="left" label-width="110px">
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="预约人" prop="applyUserName">
<el-input readonly v-model="formData.applyUserName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工号" prop="applyLoginName">
<el-input readonly v-model="formData.applyLoginName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属单位" prop="applyUnitName">
<el-input readonly v-model="formData.applyUnitName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约类型" prop="reserveType">
<el-radio-group v-model="formData.reserveType" @change="reserveTypeChange">
<el-radio-button label="union">分工会预约</el-radio-button>
<el-radio-button label="club">协会预约</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分工会" prop="applyUnionName" v-if="formData.reserveType === 'union'">
<el-input readonly v-model="formData.applyUnionName"
placeholder="自动读取当前登录人的分工会"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="协会"
prop="clubId"
v-if="formData.reserveType === 'club'"
:rules="[{ required: true, message: '请选择您管理的协会', trigger: ['change', 'blur'] }]">
<el-select v-model="formData.clubId" placeholder="请选择您管理的协会" filterable clearable
style="width: 100%" @change="clubChange">
<el-option
v-for="item in clubOptions"
:key="item.id"
:label="item.clubName"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="applyMobile">
<el-input maxlength="11" placeholder="请输入联系电话" v-model="formData.applyMobile"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约开始时间" prop="reserveStartTime">
<el-date-picker
:key="'start-' + pickerRefreshKey"
v-model="formData.reserveStartTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约开始时间"
:picker-options="startPickerOptions"
style="width: 100%"
@change="timeFieldChange('reserveStartTime')">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约结束时间" prop="reserveEndTime">
<el-date-picker
:key="'end-' + pickerRefreshKey"
v-model="formData.reserveEndTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约结束时间"
:picker-options="endPickerOptions"
style="width: 100%"
@change="timeFieldChange('reserveEndTime')">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="批量预约">
<div style="display:flex;align-items:flex-start;column-gap:12px;flex-wrap:wrap;line-height:1.7;">
<el-checkbox v-model="yearlyReserve">预约本年后续每周同一时段</el-checkbox>
<span style="color:#909399;">
例如先选某个周一 09:00-11:00,勾选后会自动预约本年剩余所有周一的这个时段。
</span>
</div>
<div v-if="yearlyReserve && yearlyReserveSummary" style="margin-top:8px;color:#E6A23C;">
{{ yearlyReserveSummary }}
</div>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="预约事由" prop="applyCause">
<el-input maxlength="1000" placeholder="请输入预约事由" v-model="formData.applyCause"
type="textarea" :autosize="{ minRows: 4, maxRows: 6}"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button type="primary" @click="onSubmit">提交预约</el-button>
</el-row>
</div>
`,
store,
data() {
return {
row: {},
clubOptions: [],
yearlyReserve: false,
timeLimitConfig: {
filterHolidays: false,
holidayList: [],
notApplyTimeList: [],
},
pickerRefreshKey: 0,
formData: {
reserveType: 'union',
applyUnionId: '',
applyUnionName: '',
clubId: '',
clubName: '',
reserveStartTime: '',
reserveEndTime: '',
joinCount: 1,
applyCause: '',
},
formRules: {
applyUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applySex: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applyLoginName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applyUnitName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveType: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applyMobile: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveStartTime: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveEndTime: [{required: true, message: '必填', trigger: ['blur', 'change']}],
joinCount: [{required: true, message: '必填', trigger: ['blur', 'change']}],
applyCause: [{required: true, message: '必填', trigger: ['blur', 'change']}],
clubId: [{ validator: (rule, value, callback) => {
if (this.formData.reserveType === 'club' && !value) {
callback(new Error('请选择您管理的协会'))
return
}
callback()
}, trigger: ['blur', 'change']}],
},
}
},
computed: {
startPickerOptions() {
return {
disabledDate: (time) => this.isDisabledDate(time),
selectableRange: this.buildSelectableRange(this.formData.reserveStartTime),
}
},
endPickerOptions() {
return {
disabledDate: (time) => this.isDisabledDate(time),
selectableRange: this.buildSelectableRange(this.formData.reserveEndTime),
}
},
yearlyReserveDates() {
if (!this.yearlyReserve || !this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return []
}
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(this.formData.reserveEndTime)
if (!start.isValid() || !end.isValid() || !end.isAfter(start)) {
return []
}
const result = []
const current = start.clone().startOf('day')
const endOfYear = start.clone().endOf('year').startOf('day')
const targetWeekDay = start.day()
const startClock = start.format('HH:mm:ss')
const endClock = end.format('HH:mm:ss')
while (current.isSameOrBefore(endOfYear, 'day')) {
if (current.day() === targetWeekDay) {
const day = current.format('YYYY-MM-DD')
const startTime = day + ' ' + startClock
const endTime = day + ' ' + endClock
if (this.validateTimeLimit(false, startTime, endTime)) {
result.push(day)
}
}
current.add(1, 'day')
}
return result
},
yearlyReserveSummary() {
if (!this.yearlyReserve) {
return ''
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return '请先选择开始和结束时间后,再批量预约本年同星期时段'
}
if (!this.yearlyReserveDates.length) {
return '当前时间段无法生成本年批量预约日期'
}
return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.yearlyReserveDates[this.yearlyReserveDates.length - 1] + ',共 ' + this.yearlyReserveDates.length + ' 个时段'
},
},
methods: {
async onOpen(row) {
this.row = row
this.clubOptions = []
this.yearlyReserve = false
this.formData = {
siteId: row.id,
applyUserId: this.$store.state.user.id,
applyUserName: this.$store.state.user.username,
applySex: this.$store.state.user.sex,
applyLoginName: this.$store.state.user.loginname,
applyUnitId: this.$store.state.user?.unit?.id,
applyUnitName: this.$store.state.user?.unit?.name,
reserveType: 'union',
applyUnionId: this.$store.state.user?.union?.id,
applyUnionName: this.$store.state.user?.union?.name,
clubId: '',
clubName: '',
applyMobile: this.$store.state.user.mobile,
reserveStartTime: '',
reserveEndTime: '',
joinCount: 1,
applyCause: '',
}
await this.queryTimeLimitConfig()
},
async queryTimeLimitConfig() {
const fallback = {
filterHolidays: !!this.row.filterHolidays,
holidayList: [],
notApplyTimeList: Array.isArray(this.row.notApplyTimeList) ? this.row.notApplyTimeList : [],
}
try {
const res = await this.$axios.post('/platform/siteCug/apply/timeLimitConfig', {
siteId: this.row.id,
})
if (res.code === 0 && res.data) {
this.timeLimitConfig = {
filterHolidays: !!res.data.filterHolidays,
holidayList: Array.isArray(res.data.holidayList) ? res.data.holidayList : [],
notApplyTimeList: Array.isArray(res.data.notApplyTimeList) ? res.data.notApplyTimeList : [],
}
} else {
this.timeLimitConfig = fallback
}
} catch (e) {
this.timeLimitConfig = fallback
}
this.pickerRefreshKey += 1
},
async reserveTypeChange(value) {
if (value === 'union') {
this.formData.applyUnionId = this.$store.state.user?.union?.id || ''
this.formData.applyUnionName = this.$store.state.user?.union?.name || ''
this.formData.clubId = ''
this.formData.clubName = ''
}
if (value === 'club') {
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
this.formData.clubId = ''
this.formData.clubName = ''
await this.loadManagedClubs()
}
this.$nextTick(() => {
this.$refs.formRef?.clearValidate(['applyUnionName', 'clubId'])
})
},
async loadManagedClubs() {
try {
const res = await this.$axios.post('/platform/club/examine/apply/getClubsByRole')
this.clubOptions = Array.isArray(res.data) ? res.data : []
if (!this.clubOptions.length) {
this.$message.warning('您当前没有可预约的协会管理权限')
}
} catch (e) {
this.clubOptions = []
this.$message.warning('协会列表加载失败,请稍后重试')
}
},
clubChange(clubId) {
const club = this.clubOptions.find(item => item.id === clubId)
this.formData.clubName = club ? club.clubName : ''
},
async normalizeReserveTypeData() {
if (this.formData.reserveType === 'union') {
this.formData.applyUnionId = this.$store.state.user?.union?.id || ''
this.formData.applyUnionName = this.$store.state.user?.union?.name || ''
this.formData.clubId = ''
this.formData.clubName = ''
return true
}
if (this.formData.reserveType === 'club') {
if (!this.clubOptions.length) {
await this.loadManagedClubs()
}
const club = this.clubOptions.find(item => item.id === this.formData.clubId)
this.formData.clubName = club ? club.clubName : ''
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
return true
}
return false
},
isDisabledDate(time) {
const day = this.$moment(time).format('YYYY-MM-DD')
const today = this.$moment().startOf('day')
const currentDay = this.$moment(day)
if (currentDay.isBefore(today)) {
return true
}
if (currentDay.day() === 0 || currentDay.day() === 6) {
return true
}
return this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(day)
},
normalizeTime(timeStr) {
if (!timeStr) {
return '00:00:00'
}
return timeStr.length === 5 ? timeStr + ':00' : timeStr
},
getDisabledRangesByDay(dayStr) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list
.filter(item => item && item.date && this.$moment(item.date).format('YYYY-MM-DD') === dayStr)
.sort((a, b) => this.normalizeTime(a.startTime).localeCompare(this.normalizeTime(b.startTime)))
},
buildSelectableRange(dateTimeValue) {
if (!dateTimeValue) {
return ['00:00:00 - 23:59:59']
}
const dayStr = this.$moment(dateTimeValue).format('YYYY-MM-DD')
const disabledRanges = this.getDisabledRangesByDay(dayStr)
if (!disabledRanges.length) {
return ['00:00:00 - 23:59:59']
}
const result = []
let cursor = '00:00:00'
disabledRanges.forEach(item => {
const start = this.normalizeTime(item.startTime)
const end = this.normalizeTime(item.endTime)
if (cursor < start) {
result.push(cursor + ' - ' + start)
}
if (cursor < end) {
cursor = end
}
})
if (cursor < '23:59:59') {
result.push(cursor + ' - 23:59:59')
}
return result.length ? result : ['00:00:00 - 00:00:00']
},
isDateTimeBlocked(dateTimeStr) {
if (!dateTimeStr) {
return false
}
const target = this.$moment(dateTimeStr)
const dayStr = target.format('YYYY-MM-DD')
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(dayStr)) {
return true
}
const disabledRanges = this.getDisabledRangesByDay(dayStr)
return disabledRanges.some(item => {
const start = this.$moment(dayStr + ' ' + this.normalizeTime(item.startTime))
const end = this.$moment(dayStr + ' ' + this.normalizeTime(item.endTime))
return target.isSameOrAfter(start) && target.isBefore(end)
})
},
hasHolidayInRange(start, end) {
const current = start.clone().startOf('day')
const endDay = end.clone().startOf('day')
while (current.isSameOrBefore(endDay)) {
if (current.day() === 0 || current.day() === 6) {
return true
}
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(current.format('YYYY-MM-DD'))) {
return true
}
current.add(1, 'day')
}
return false
},
overlapsDisabledRange(start, end) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list.some(item => {
if (!item || !item.date || !item.startTime || !item.endTime) {
return false
}
const rangeStart = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.startTime))
const rangeEnd = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.endTime))
return start.isBefore(rangeEnd) && end.isAfter(rangeStart)
})
},
validateTimeLimit(showMessage = true, startTime = this.formData.reserveStartTime, endTime = this.formData.reserveEndTime) {
if (!startTime || !endTime) {
return true
}
const start = this.$moment(startTime)
const end = this.$moment(endTime)
if (this.isDateTimeBlocked(startTime) || this.isDateTimeBlocked(endTime)) {
if (showMessage) {
this.$message.warning('预约时间不能选择周末、节假日或禁用时间')
}
return false
}
if (this.hasHolidayInRange(start, end)) {
if (showMessage) {
this.$message.warning('预约时间范围内包含周末或节假日,请重新选择')
}
return false
}
if (this.overlapsDisabledRange(start, end)) {
if (showMessage) {
this.$message.warning('预约时间范围与禁用时间冲突,请重新选择')
}
return false
}
return true
},
timeFieldChange(field) {
const value = this.formData[field]
if (!value) {
return
}
if (this.isDateTimeBlocked(value)) {
this.$message.warning('该时间点不可预约,请重新选择')
this.$set(this.formData, field, '')
return
}
if (this.formData.reserveStartTime && this.formData.reserveEndTime && !this.validateTimeLimit()) {
this.$set(this.formData, field, '')
}
},
validateYearlyReserve() {
if (!this.yearlyReserve) {
return true
}
if (!this.yearlyReserveDates.length) {
this.$message.warning('?????????????????????')
return false
}
return true
},
buildSubmitApi() {
return this.yearlyReserve ? '/platform/siteCug/apply/submitYearly' : '/platform/siteCug/apply/submit'
},
buildSubmitConfirmMessage() {
if (!this.yearlyReserve) {
return '您确定要提交吗?'
}
return '将一次性预约本年后续 ' + this.yearlyReserveDates.length + ' 个同星期时段,确认提交吗?'
},
onSubmit() {
this.$refs.formRef.validate(async (valid) => {
if (!valid) return
if (!this.formData.reserveType) {
this.$message.warning('请选择预约类型')
return
}
await this.normalizeReserveTypeData()
if (this.formData.reserveType === 'union' && !this.formData.applyUnionId) {
this.$message.warning('当前用户未关联分工会,不能发起分工会预约')
return
}
if (this.formData.reserveType === 'club' && !this.formData.clubId) {
this.$message.warning('请选择您管理的协会')
return
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
this.$message.warning('请选择预约开始和结束时间')
return
}
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(this.formData.reserveEndTime)
if (!start.isAfter(this.$moment())) {
this.$message.warning('预约开始时间必须晚于当前时间')
return
}
if (!end.isAfter(start)) {
this.$message.warning('预约结束时间必须晚于开始时间')
return
}
if (!this.validateTimeLimit()) {
return
}
if (!this.validateYearlyReserve()) {
return
}
this.$confirm(this.buildSubmitConfirmMessage(), '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post(this.buildSubmitApi(), {
data: JSON.stringify(this.formData),
}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg || '提交成功')
this.$emit('refresh')
} else {
this.$message.warning(resp.msg || '提交失败')
}
}).catch((err) => {
this.$message.warning((err && err.msg) || '提交失败')
})
}).catch(() => {})
})
},
},
};
@@ -0,0 +1,125 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/地址">
<el-input placeholder="请输入名称或地址" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.type" clearable filterable placeholder="请选择场地类型" @change="doSearch">
<el-option
v-for="item in typeOptions"
:label="item.name"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="场地列表"></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
</el-table-column>
<el-table-column label="操作" width="180">
<template v-slot="{ row }">
<el-button @click="onViewSite(row)" size="mini" type="primary">查看场地</el-button>
<el-button @click="onApply(row)" size="mini" type="primary">预约</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<site-apply ref="applyRef" @refresh="refresh"></site-apply>
</template>
<template #view>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../manage/info.js'){}#-->
<!--#include('apply.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"info": siteInfo,
"site-apply": apply,
},
data() {
return {
typeOptions: [],
tableColumns: [
{prop: 'name', label: '场地名称'},
{prop: 'address', label: '场地地址'},
{prop: 'contactName', label: '联系人'},
{prop: 'contactPhone', label: '联系电话'},
{prop: 'maxNum', label: '容纳人数'},
{prop: 'typeName', label: '场地类型'},
],
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onApply(row) {
this.$refs.guava.edit(() => {
this.$refs.applyRef.onOpen(row)
})
},
onViewSite(row) {
this.$refs.guava.view(() => {
this.$refs.infoRef.onOpen(row)
})
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.querySiteType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,80 @@
const siteCugApplyInfo = {
template: /*language=HTML*/ `
<div>
<div class="process-title">
申请信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="预约场地">{{ viewData.siteName }}</el-descriptions-item>
<el-descriptions-item label="预约人">{{ viewData.applyUserName }}</el-descriptions-item>
<el-descriptions-item label="预约人工号">{{ viewData.applyLoginName }}</el-descriptions-item>
<el-descriptions-item label="所属单位">{{ viewData.applyUnitName }}</el-descriptions-item>
<el-descriptions-item label="预约类型">{{ reserveTypeLabel }}</el-descriptions-item>
<el-descriptions-item label="预约主体">{{ reserveTargetName }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ viewData.reserveStartTime }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ viewData.reserveEndTime }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewData.applyMobile }}</el-descriptions-item>
<el-descriptions-item label="预约人数">{{ viewData.joinCount }}</el-descriptions-item>
<el-descriptions-item label="预约事由" :span="2">{{ viewData.applyCause }}</el-descriptions-item>
</el-descriptions>
<template v-for="task in doneTasks">
<div class="mt10">
<div class="process-title">{{ task.displayName }}</div>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见">{{ task.taskFormData.opinion }}</el-descriptions-item>
</el-descriptions>
</div>
</template>
<slot></slot>
<snaker-chart ref="snakerChartRef"></snaker-chart>
</div>
`,
store,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
viewData: {},
doneTasks: [],
row: null
}
},
computed: {
reserveTypeLabel() {
return this.viewData.reserveType === 'club' ? '协会预约' : '分工会预约'
},
reserveTargetName() {
return this.viewData.reserveType === 'club' ? (this.viewData.clubName || '-') : (this.viewData.applyUnionName || '-')
},
},
methods: {
onOpen(row) {
this.row = row
this.viewData = row
this.getDoneTasks()
},
getDoneTasks() {
this.$axios.post('/flow/common/doneTasks', {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
openChart() {
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
}
},
}
@@ -0,0 +1,341 @@
const basicForm = {
template: /*language=HTML*/ `
<div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left">
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="创建人" prop="createUserName">
<el-input disabled type="text" v-model="formData.createUserName" maxlength="20" placeholder="请输入创建人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="sortNum" label="排序编号">
<el-input v-model="formData.sortNum" placeholder="请输入排序编号" type="number"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="场地名称" prop="name">
<el-input type="text" v-model="formData.name" maxlength="50" placeholder="请输入场地名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地地址" prop="address">
<el-input type="text" v-model="formData.address" maxlength="100" placeholder="请输入场地地址"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="联系人" prop="contactName">
<el-input type="text" v-model="formData.contactName" maxlength="20" placeholder="请输入联系人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input type="text" v-model="formData.contactPhone" maxlength="20" placeholder="请输入联系电话"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="容纳人数" prop="maxNum">
<el-input v-model="formData.maxNum" placeholder="请输入容纳人数" type="number"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="场地类型" prop="typeId">
<el-select v-model="formData.typeId" clearable filterable placeholder="请选择场地类型" style="width: 100%">
<el-option
v-for="item in typeList"
:label="item.name"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item prop="sexLimit" label="性别限制">
<el-radio-group v-model="formData.sexLimit" size="medium">
<el-radio-button :label="0">不限制</el-radio-button>
<el-radio-button :label="1">男</el-radio-button>
<el-radio-button :label="2">女</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="state" label="开启状态">
<el-radio-group v-model="formData.state" size="medium">
<el-radio-button :label="true">开启</el-radio-button>
<el-radio-button :label="false">禁用</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item prop="filterHolidays" label="排除节假日">
<el-radio-group v-model="formData.filterHolidays" size="medium">
<el-radio-button :label="true">是</el-radio-button>
<el-radio-button :label="false">否</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="禁用时间">
<el-button type="primary" @click="openSetUpTime" size="small">点击设置禁用时间</el-button>
<span style="color: #c64120">您已设置{{ formData.notApplyTimeList ? formData.notApplyTimeList.length : 0 }}个禁用时间</span>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="场地介绍" prop="introduce">
<text-editor v-model="formData.introduce"></text-editor>
</el-form-item>
</el-form>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onSubmit" type="primary">提交</el-button>
</el-row>
<el-dialog append-to-body :close-on-click-modal="false" :visible.sync="setUpTimeDialog" title="设置时间">
<div class="left-span-label">选择日期</div>
<el-date-picker
@change="setUpDateChange"
placeholder="请选择一个或多个日期"
style="width: 100%"
type="dates"
v-model="formData.setUpDate"
value-format="yyyy-MM-dd">
</el-date-picker>
<div class="left-span-label mt20">设置禁用时间</div>
<el-row>
<el-time-select
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
placeholder="开始时间"
v-model="timeOneKeySet.startTime">
</el-time-select>
<el-time-select
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
placeholder="结束时间"
v-model="timeOneKeySet.endTime">
</el-time-select>
<el-button @click="oneKeySetStartEndTime" type="primary">一键设置开始/结束时间</el-button>
</el-row>
<el-table :data="formData.notApplyTimeList" class="mt10" max-height="520px" size="mini" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="日期" width="200">
<template v-slot="{row}">
<i class="el-icon-time"></i>
{{$moment(row.date).format('YYYY-MM-DD')}}
</template>
</el-table-column>
<el-table-column label="开始时间">
<template v-slot="{row}">
<el-time-select
size="mini"
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
v-model="row.startTime">
</el-time-select>
</template>
</el-table-column>
<el-table-column label="结束时间">
<template v-slot="{row}">
<el-time-select
size="mini"
:picker-options="{
start: '00:00',
step: '00:05',
end: '23:55'
}"
v-model="row.endTime">
</el-time-select>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template v-slot="{row, $index}">
<el-button
size="mini"
@click="formData.notApplyTimeList.splice($index,0,{date:row.date,startTime:'',endTime:''})"
type="primary">
新增同天时段
</el-button>
<el-button size="mini" @click="removeSetUpTableRow(row,$index)" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="mt20" justify="end" type="flex">
<el-button @click="setUpTimeDialog = false">取消</el-button>
<el-button @click="doConfirmSetUpCourse" type="primary">确定</el-button>
</el-row>
</el-dialog>
</div>
`,
props: {
typeList: {
type: Array,
required: false,
default: [],
}
},
store,
data() {
return {
formData: {
state: true,
sexLimit: 0,
filterHolidays: false,
notApplyTimeList: [],
createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id,
},
formRules: {
createUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
sortNum: [{required: true, message: '必填', trigger: ['blur', 'change']}],
name: [{required: true, message: '必填', trigger: ['blur', 'change']}],
address: [{required: true, message: '必填', trigger: ['blur', 'change']}],
contactName: [{required: true, message: '必填', trigger: ['blur', 'change']}],
contactPhone: [{required: true, message: '必填', trigger: ['blur', 'change']}],
maxNum: [{required: true, message: '必填', trigger: ['blur', 'change']}],
typeId: [{required: true, message: '必填', trigger: ['blur', 'change']}],
sexLimit: [{required: true, message: '必填', trigger: ['blur', 'change']}],
filterHolidays: [{required: true, message: '必填', trigger: ['blur', 'change']}],
state: [{required: true, message: '必填', trigger: ['blur', 'change']}],
},
setUpTimeDialog: false,
timeOneKeySet: {
startTime: "",
endTime: ""
},
multipleSelection: [],
}
},
methods: {
doConfirmSetUpCourse() {
const timeList = this.formData.notApplyTimeList
if (timeList && timeList.length > 0) {
const valid = timeList.every(v => v.date && v.startTime && v.endTime && (v.startTime < v.endTime))
if (!valid) {
this.$message.warning("时间不完整或者有误")
return
}
}
this.setUpTimeDialog = false
},
removeSetUpTableRow(row, index) {
this.formData.notApplyTimeList.splice(index, 1)
const dateArray = this.formData.notApplyTimeList.map(v => this.$moment(v.date).format("YYYY-MM-DD"))
const setUpDate = this.formData.setUpDate || []
this.formData.setUpDate = setUpDate.filter(v => {
return dateArray.includes(this.$moment(v).format("YYYY-MM-DD"))
})
},
handleSelectionChange(val) {
this.multipleSelection = val
},
oneKeySetStartEndTime() {
if (this.multipleSelection.length === 0) {
this.$message.warning('请在下面表格多选框中选择需要一键设置的时间')
return
}
const { startTime, endTime } = this.timeOneKeySet
this.formData.notApplyTimeList.forEach(v => {
const selected = this.multipleSelection.find(o => o.date === v.date && o.startTime === v.startTime && o.endTime === v.endTime)
if (selected) {
this.$set(v, "startTime", startTime)
this.$set(v, "endTime", endTime)
}
})
this.$forceUpdate()
},
setUpDateChange(val) {
if (!val) {
this.formData.notApplyTimeList = []
return
}
if (this.formData.notApplyTimeList === undefined) {
this.$set(this.formData, "notApplyTimeList", [])
}
const dateSet = new Set(this.formData.notApplyTimeList.map(v => this.$moment(v.date).format("YYYY-MM-DD")))
val.forEach(v => {
if (!dateSet.has(v)) {
this.formData.notApplyTimeList.push({
date: v, startTime: "", endTime: ""
})
}
})
this.formData.notApplyTimeList = this.formData.notApplyTimeList.filter(v => {
return val.includes(this.$moment(v.date).format("YYYY-MM-DD"))
})
this.formData.notApplyTimeList.sort((a, b) => {
return Date.parse(a.date) - Date.parse(b.date)
})
},
openSetUpTime() {
if (this.formData.notApplyTimeList) {
this.$set(this.formData, 'setUpDate', this.formData.notApplyTimeList.map(o => o.date))
}
this.setUpTimeDialog = true
},
onOpen(row) {
if (row && row.id) {
this.formData = clone(row)
if (!Array.isArray(this.formData.notApplyTimeList)) {
this.$set(this.formData, 'notApplyTimeList', [])
}
} else {
this.formData = {
state: true,
sexLimit: 0,
filterHolidays: false,
notApplyTimeList: [],
createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id,
}
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post("/platform/siteCug/manage/submit", {data: JSON.stringify(this.formData)})
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.warning(resp.msg)
}
})
}
})
},
},
style: /*language=CSS*/ `
`
};
@@ -0,0 +1,180 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/地址">
<el-input placeholder="请输入名称或地址" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.type" clearable filterable placeholder="请选择场地类型" @change="doSearch">
<el-option
v-for="item in typeOptions"
:label="item.name"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="场地列表">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增场地
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'state'">
<el-switch
@change="switchChange(row)"
v-model="row.state"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'sexLimit'">
<span v-if="row.sexLimit === 0">不限制</span>
<span v-else-if="row.sexLimit === 1"></span>
<span v-else-if="row.sexLimit === 2"></span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="操作" width="230">
<template v-slot="{ row }">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<basic-form :type-list="typeOptions" ref="basicFormRef" @refresh="refresh"></basic-form>
</template>
<template #view>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('basicForm.js'){}#-->
<!--#include('info.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
"info": siteInfo,
},
data() {
return {
typeOptions: [],
tableColumns: [
{prop: 'createUserName', label: '创建人'},
{prop: 'sortNum', label: '排序编号'},
{prop: 'name', label: '场地名称'},
{prop: 'address', label: '场地地址'},
{prop: 'contactName', label: '联系人'},
{prop: 'contactPhone', label: '联系电话'},
{prop: 'maxNum', label: '容纳人数'},
{prop: 'typeName', label: '场地类型'},
{prop: 'sexLimit', label: '性别限制'},
{prop: 'state', label: '开启状态'},
],
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onAdd() {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen()
})
},
onEdit(row) {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen(row)
})
},
onView(row) {
this.$refs.guava.view(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(row) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/siteCug/manage/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
switchChange(row) {
this.$axios.post("/platform/siteCug/manage/submit", row).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
querySiteType() {
this.$axios.post("/platform/siteCug/function/type/queryFunctionType").then((res) => {
this.typeOptions = res.data
})
},
pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.querySiteType()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,59 @@
const siteInfo = {
template: /*language=HTML*/ `
<div>
<el-descriptions :column="2" border>
<el-descriptions-item label="创建人">{{ viewData.createUserName }}</el-descriptions-item>
<el-descriptions-item label="排序编号">{{ viewData.sortNum }}</el-descriptions-item>
<el-descriptions-item label="场地名称">{{ viewData.name }}</el-descriptions-item>
<el-descriptions-item label="场地地址">{{ viewData.address }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ viewData.contactName }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewData.contactPhone }}</el-descriptions-item>
<el-descriptions-item label="容纳人数">{{ viewData.maxNum }}</el-descriptions-item>
<el-descriptions-item label="场地类型">{{ viewData.typeName }}</el-descriptions-item>
<el-descriptions-item label="排除节假日">
<span v-if="viewData.filterHolidays">是</span>
<span v-else>否</span>
</el-descriptions-item>
<el-descriptions-item label="禁用时间" :span="2">
<el-table v-if="viewData.notApplyTimeList && viewData.notApplyTimeList.length > 0"
:data="viewData.notApplyTimeList" max-height="300" size="mini">
<el-table-column prop="date" label="日期"></el-table-column>
<el-table-column prop="startTime" label="开始时间"></el-table-column>
<el-table-column prop="endTime" label="结束时间"></el-table-column>
</el-table>
<span v-else>暂无禁用时间</span>
</el-descriptions-item>
<el-descriptions-item label="性别限制">
<span v-if="viewData.sexLimit === 0">不限制</span>
<span v-if="viewData.sexLimit === 1">男</span>
<span v-if="viewData.sexLimit === 2">女</span>
</el-descriptions-item>
<el-descriptions-item label="开启状态">
<span v-if="viewData.state">开启</span>
<span v-else>禁用</span>
</el-descriptions-item>
<el-descriptions-item label="场地介绍" :span="2">
<div v-if="viewData.introduce" v-html="viewData.introduce"></div>
<div v-else>暂无场地介绍</div>
</el-descriptions-item>
</el-descriptions>
</div>
`,
data() {
return {
viewData: {},
}
},
methods: {
onOpen(row) {
this.viewData = row
},
},
style: /*language=CSS*/ `
.el-descriptions-item__label {
width: 200px;
min-width: 200px;
max-width: 200px;
}
`
};
@@ -0,0 +1,140 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号/场地">
<el-input placeholder="请输入姓名/工号/场地" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%" placeholder="请选择活动场地" filterable clearable>
<el-option v-for="item in siteOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
</el-select>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%" placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表"></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveType'">
<span>{{ row.reserveType === 'club' ? '协会预约' : '分工会预约' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template v-slot="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.canCancel" @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
mixins: [initTableMixins],
components: {
'info': siteCugApplyInfo,
},
data() {
return {
typeOptions: [],
siteOptions: [],
tableColumns: [
{prop: 'siteName', label: '场地名称'},
{prop: 'applyUserName', label: '预约人'},
{prop: 'reserveType', label: '预约类型'},
{prop: 'reserveTargetName', label: '预约单位'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'reserveStartTime', label: '开始时间'},
{prop: 'reserveEndTime', label: '结束时间'},
{prop: 'applyMobile', label: '联系方式'},
{prop: 'taskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
}
},
methods: {
onView(row) {
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(row) {
const message = row.yearlyBatch
? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || 0) + ' 条预约记录,是否继续?')
: '您确定要删除吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post('/platform/siteCug/mine/delete', { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
querySites() {
this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
this.siteOptions = res.data
})
},
},
async created() {
this.querySiteType()
this.querySites()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,187 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号/场地">
<el-input placeholder="请输入姓名/工号/场地" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%"
placeholder="请选择活动场地" filterable clearable>
<el-option v-for="item in siteOptions"
:value="item.id"
:key="item.id"
:label="item.name"></el-option>
</el-select>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%"
placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
:label="item.name"></el-option>
</el-select>
</search-item>
<search-item label="预约类型">
<el-select v-model="pageForm.reserveType" @change="doSearch" style="width: 100%"
placeholder="请选择预约类型" clearable>
<el-option label="分工会预约" value="union"></el-option>
<el-option label="协会预约" value="club"></el-option>
</el-select>
</search-item>
<search-item label="预约单位">
<el-input placeholder="请输入分工会或协会名称" clearable v-model="pageForm.reserveTargetKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="预约开始时间">
<el-date-picker v-model="pageForm.reserveTimeStart"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约开始时间"
style="width: 100%"
clearable
@change="doSearch">
</el-date-picker>
</search-item>
<search-item label="预约结束时间">
<el-date-picker v-model="pageForm.reserveTimeEnd"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择预约结束时间"
style="width: 100%"
clearable
@change="doSearch">
</el-date-picker>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-button type="primary" size="small" @click="doExport">导出表格</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns">
<template v-slot="{ row }" v-if="column.prop === 'reserveType'">
<span>{{ row.reserveType === 'club' ? '协会预约' : '分工会预约' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template v-slot="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<info ref="infoRef"></info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
mixins: [initTableMixins],
components: {
'info': siteCugApplyInfo,
},
data() {
return {
pageForm: {
audit: false,
siteType: '',
siteId: '',
reserveType: '',
reserveTargetKeyword: '',
reserveTimeStart: '',
reserveTimeEnd: '',
},
typeOptions: [],
siteOptions: [],
tableColumns: [
{prop: 'siteName', label: '场地名称'},
{prop: 'applyUserName', label: '预约人'},
{prop: 'reserveType', label: '预约类型'},
{prop: 'reserveTargetName', label: '预约单位'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'reserveStartTime', label: '开始时间'},
{prop: 'reserveEndTime', label: '结束时间'},
{prop: 'applyMobile', label: '联系方式'},
],
}
},
methods: {
onView(row) {
this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row)
})
},
onDelete(id) {
this.$confirm('您确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post('/platform/siteCug/record/delete', { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
// 导出内容和列表当前筛选条件保持一致,实现所见即所得
doExport() {
this.$downLoad('/platform/siteCug/record/doExport', this.pageForm)
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
querySites() {
this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
this.siteOptions = res.data
})
},
},
created() {
// 查询字段在 data 中一次性声明完整,避免 Vue 2 对后加属性渲染不稳定
this.querySiteType()
this.querySites()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,237 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号/场地">
<el-input placeholder="请输入姓名/工号/场地" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%"
placeholder="请选择活动场地" filterable clearable>
<el-option v-for="item in siteOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
<search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%"
placeholder="请选择场地类型" filterable clearable>
<el-option v-for="item in typeOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="申请列表">
<el-radio-group v-model="pageForm.approval" size="small" @change="doSearch">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveType'">
<span>{{ row.reserveType === 'club' ? '协会预约' : '分工会预约' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300px">
<template v-slot="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.taskState === 10" @click="onAudit(row)" size="mini" type="primary">审核</el-button>
<el-button v-if="canRevoke(row)" :loading="revokeLoading" :disabled="revokeLoading" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{formData.taskName}}
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=""
class="flow-task-form">
<el-form-item label="审批意见" prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction(6)" :loading="auditLoading" :disabled="auditLoading" size="small" type="info">退回到发起人</el-button>
<el-button @click="handleTaskAction(2)" :loading="auditLoading" :disabled="auditLoading" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(1)" :loading="auditLoading" :disabled="auditLoading" size="small" type="primary">同意申请</el-button>
</el-row>
</div>
</info>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
components: {
"info": siteCugApplyInfo,
},
data() {
return {
pageForm: {
approval: false
},
tableColumns: [
{prop: 'siteName', label: '场地名称'},
{prop: 'applyUserName', label: '预约人'},
{prop: 'reserveType', label: '预约类型'},
{prop: 'reserveTargetName', label: '预约单位'},
{prop: 'applyUnitName', label: '所属单位'},
{prop: 'reserveStartTime', label: '开始时间'},
{prop: 'reserveEndTime', label: '结束时间'},
{prop: 'applyMobile', label: '联系方式'},
{prop: 'curTaskName', label: '当前节点'},
{prop: 'instanceState', label: '流程状态'},
],
typeOptions: [],
siteOptions: [],
formData: {},
formRules: {},
showApprovalForm: false,
auditLoading: false,
revokeLoading: false,
}
},
methods: {
canRevoke(row) {
return Number(row.instanceState) === 20
},
onView(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
})
},
onAudit(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
applyId: row.id,
taskName: row.curTaskName,
tf_opinion: '',
yearlyBatch: row.yearlyBatch,
batchAuditCount: row.batchAuditCount,
}
this.$refs.infoRef.onOpen(row)
})
},
handleTaskAction(val) {
if (this.auditLoading) {
return
}
const message = this.formData.yearlyBatch
? ('该记录属于“预约本年”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
: '您确定要提交吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.auditLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg)
this.doSearch()
}
}).finally(() => {
this.auditLoading = false
})
}).catch(() => {})
},
onRevoke(row) {
if (!this.canRevoke(row) || this.revokeLoading) {
return
}
const message = row.yearlyBatch
? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
this.revokeLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/revokeTask', { taskId: row.taskId, applyId: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
}).finally(() => {
this.revokeLoading = false
})
}).catch(() => {})
},
querySiteType() {
this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
this.typeOptions = res.data
})
},
querySites() {
this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
this.siteOptions = res.data
})
},
},
async created() {
this.querySiteType()
this.querySites()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,68 @@
const basicForm = {
template: /*language=HTML*/ `
<div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left">
<el-form-item label="类型编码" prop="code">
<el-input type="text" v-model="formData.code" maxlength="50"
placeholder="请输入类型编码"></el-input>
</el-form-item>
<el-form-item label="类型名称" prop="name">
<el-input type="text" v-model="formData.name" maxlength="50"
placeholder="请输入类型名称"></el-input>
</el-form-item>
<el-form-item label="是否启用" prop="enable">
<el-switch
v-model="formData.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</el-form-item>
</el-form>
<el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onSubmit" type="primary">提交</el-button>
</el-row>
</div>
`,
data() {
return {
formData: {
enable: true
},
formRules: {
name: [{required: true, message: '必填', trigger: ['blur', 'change']}],
code: [{required: true, message: '必填', trigger: ['blur', 'change']}],
enable: [{required: true, message: '必填', trigger: ['blur', 'change']}],
},
}
},
methods: {
onOpen(row) {
if (row && row.id) {
this.formData = clone(row)
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post("/platform/siteCug/function/type/submit", this.formData)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.warning(resp.msg)
}
})
}
})
},
},
style: /*language=CSS*/ `
`
}
@@ -0,0 +1,139 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称/编码">
<el-input placeholder="请输入名称或编码" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="功能类型列表">
<el-button type="primary" size="small" @click="onAdd">
<i class="ti-plus"></i>
新增类型
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'enable'">
<el-switch
@change="switchChange(row)"
v-model="row.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="230">
<template v-slot="{ row }">
<el-button @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<basic-form ref="basicFormRef" @refresh="refresh"></basic-form>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include('basicForm.js'){}#-->
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
},
data() {
return {
tableColumns: [
{prop: 'code', label: '类型编码'},
{prop: 'name', label: '类型名称'},
{prop: 'enable', label: '是否启用'},
],
}
},
methods: {
refresh() {
this.doSearch()
this.$refs.guava.index()
},
onAdd() {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen()
})
},
onEdit(row) {
this.$refs.guava.edit(() => {
this.$refs.basicFormRef.onOpen(row)
})
},
onDelete(row) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/siteCug/function/type/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
switchChange(row) {
this.$axios.post("/platform/siteCug/function/type/submit", row).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
pageData() {
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
},
async created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,887 @@
new Vue({
el: '#app',
store,
data() {
return {
siteId: GetQueryString('siteId'),
row: {},
siteLoaded: false,
clubOptions: [],
timeLimitConfig: {
filterHolidays: false,
holidayList: [],
notApplyTimeList: [],
},
formData: {
reserveType: 'union',
applyUnionId: '',
applyUnionName: '',
clubId: '',
clubName: '',
applyMobile: '',
reserveStartTime: '',
reserveEndTime: '',
joinCount: 1,
applyCause: '',
},
showReserveTypePicker: false,
showClubPicker: false,
showTimePicker: false,
timePickerField: '',
timePickerColumns: [],
timePickerParts: {
date: '',
hour: null,
minute: null,
},
timePickerSyncing: false,
pickerFilterCache: {},
}
},
computed: {
reserveTypeColumns() {
return ['分工会预约', '协会预约']
},
reserveTypeText() {
if (this.formData.reserveType === 'club') {
return '协会预约'
}
if (this.formData.reserveType === 'union') {
return '分工会预约'
}
return ''
},
clubColumns() {
return this.clubOptions.map(item => item.clubName)
},
timePickerTitle() {
return this.timePickerField === 'reserveEndTime' ? '选择预约结束时间' : '选择预约开始时间'
},
},
methods: {
historyBack,
clearTimePickerCache() {
this.pickerFilterCache = {}
},
buildDefaultFormData() {
const user = this.$store.state.user || {}
return {
siteId: this.row.id || '',
applyUserId: user.id || '',
applyUserName: user.username || '',
applySex: user.sex || '',
applyLoginName: user.loginname || '',
applyUnitId: user.unit ? user.unit.id : '',
applyUnitName: user.unit ? user.unit.name : '',
reserveType: 'union',
applyUnionId: user.union ? user.union.id : '',
applyUnionName: user.union ? user.union.name : '',
clubId: '',
clubName: '',
applyMobile: user.mobile || '',
reserveStartTime: '',
reserveEndTime: '',
joinCount: 1,
applyCause: '',
}
},
getNormalizedMoment(value) {
return this.$moment(value).seconds(0).milliseconds(0)
},
getFieldMinMoment(field) {
const now = this.$moment().add(1, 'minute').seconds(0).milliseconds(0)
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
const start = this.getNormalizedMoment(this.formData.reserveStartTime).add(1, 'minute')
return start.isAfter(now) ? start : now
}
return now
},
getFieldLatestMoment(field) {
if (field === 'reserveStartTime' && this.formData.reserveEndTime) {
return this.getNormalizedMoment(this.formData.reserveEndTime).subtract(1, 'minute')
}
return null
},
getFieldMaxMoment(field) {
const defaultMax = this.$moment().add(365, 'day').endOf('day').seconds(0).milliseconds(0)
const latest = this.getFieldLatestMoment(field)
if (latest && latest.isBefore(defaultMax)) {
return latest.clone().seconds(0).milliseconds(0)
}
return defaultMax
},
getContinuousEndBounds() {
if (!this.formData.reserveStartTime) {
return null
}
const minMoment = this.getFieldMinMoment('reserveEndTime')
let maxMoment = this.getFieldMaxMoment('reserveEndTime')
if (maxMoment.isBefore(minMoment)) {
return null
}
let boundary = null
const blockedDayCursor = minMoment.clone().startOf('day').add(1, 'day')
while (blockedDayCursor.isSameOrBefore(maxMoment, 'day')) {
if (this.isBlockedDay(blockedDayCursor)) {
boundary = blockedDayCursor.clone().startOf('day')
break
}
blockedDayCursor.add(1, 'day')
}
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
list.forEach(item => {
if (!item || !item.date || !item.startTime || !item.endTime) {
return
}
const dayStr = this.$moment(item.date).format('YYYY-MM-DD')
const rangeStart = this.$moment(dayStr + ' ' + this.normalizeTime(item.startTime))
const rangeEnd = this.$moment(dayStr + ' ' + this.normalizeTime(item.endTime))
if (rangeEnd.isSameOrBefore(minMoment)) {
return
}
if (rangeStart.isSameOrBefore(minMoment) && rangeEnd.isAfter(minMoment)) {
boundary = minMoment.clone()
return
}
if (rangeStart.isAfter(minMoment) && rangeStart.isSameOrBefore(maxMoment)) {
if (!boundary || rangeStart.isBefore(boundary)) {
boundary = rangeStart.clone()
}
}
})
if (boundary && boundary.isSameOrBefore(maxMoment)) {
maxMoment = boundary.clone().subtract(1, 'minute')
}
if (maxMoment.isBefore(minMoment)) {
return null
}
return {
minMoment: minMoment,
maxMoment: maxMoment,
}
},
getEffectiveFieldMaxMoment(field) {
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
const bounds = this.getContinuousEndBounds()
return bounds ? bounds.maxMoment.clone() : this.getFieldMinMoment(field).clone().subtract(1, 'minute')
}
return this.getFieldMaxMoment(field)
},
buildPickerMoment(parts) {
if (parts && parts.date) {
return this.$moment(parts.date, 'YYYY-MM-DD')
.hour(parts.hour || 0)
.minute(parts.minute || 0)
.second(0)
.millisecond(0)
}
return this.$moment({
year: parts.year,
month: parts.month - 1,
date: parts.day,
hour: parts.hour,
minute: parts.minute,
second: 0,
millisecond: 0,
})
},
extractTimePickerPartsFromMoment(momentValue) {
const pickerMoment = this.getNormalizedMoment(momentValue)
return {
date: pickerMoment.format('YYYY-MM-DD'),
hour: pickerMoment.hour(),
minute: pickerMoment.minute(),
}
},
getPickerPartValue(item, fallback) {
if (item && typeof item === 'object' && item !== null && typeof item.value !== 'undefined') {
return item.value
}
if (item === '' || item === null || typeof item === 'undefined') {
return fallback
}
return item
},
extractTimePickerParts(values) {
const fallback = this.timePickerParts || {}
return {
date: this.getPickerPartValue(values && values[0], fallback.date),
hour: Number(this.getPickerPartValue(values && values[1], fallback.hour)),
minute: Number(this.getPickerPartValue(values && values[2], fallback.minute)),
}
},
isValidCalendarMoment(momentValue, month, day) {
return momentValue.isValid() && momentValue.month() + 1 === month && momentValue.date() === day
},
isBlockedDay(momentValue) {
if (!momentValue || !momentValue.isValid()) {
return true
}
if (momentValue.day() === 0 || momentValue.day() === 6) {
return true
}
return this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(momentValue.format('YYYY-MM-DD'))
},
isRangeSelectable(start, end) {
if (!start || !end || !start.isValid() || !end.isValid()) {
return false
}
if (!end.isAfter(start)) {
return false
}
if (this.hasHolidayInRange(start, end)) {
return false
}
if (this.overlapsDisabledRange(start, end)) {
return false
}
return true
},
isExactTimeSelectable(momentValue, field) {
if (!momentValue || !momentValue.isValid()) {
return false
}
const candidate = momentValue.clone().seconds(0).milliseconds(0)
const minMoment = this.getFieldMinMoment(field)
if (candidate.isBefore(minMoment)) {
return false
}
const latestMoment = this.getFieldLatestMoment(field)
if (latestMoment && candidate.isAfter(latestMoment)) {
return false
}
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
const bounds = this.getContinuousEndBounds()
if (!bounds) {
return false
}
return candidate.isSameOrAfter(bounds.minMoment) && candidate.isSameOrBefore(bounds.maxMoment)
}
if (this.isDateTimeBlocked(candidate.format('YYYY-MM-DD HH:mm:ss'))) {
return false
}
if (field === 'reserveStartTime' && this.formData.reserveEndTime) {
const end = this.getNormalizedMoment(this.formData.reserveEndTime)
if (!candidate.isBefore(end)) {
return false
}
return this.isRangeSelectable(candidate, end)
}
return true
},
dayHasAvailableTime(year, month, day, field) {
const cacheKey = ['day', field, year, month, day, this.formData.reserveStartTime || '', this.formData.reserveEndTime || ''].join('|')
if (cacheKey in this.pickerFilterCache) {
return this.pickerFilterCache[cacheKey]
}
const dayMoment = this.buildPickerMoment({ year: year, month: month, day: day, hour: 0, minute: 0 })
if (!this.isValidCalendarMoment(dayMoment, month, day) || this.isBlockedDay(dayMoment)) {
this.pickerFilterCache[cacheKey] = false
return false
}
const minMoment = this.getFieldMinMoment(field)
const maxMoment = this.getEffectiveFieldMaxMoment(field)
const dayStart = dayMoment.clone().startOf('day')
const dayEnd = dayMoment.clone().endOf('day')
if (dayEnd.isBefore(minMoment) || dayStart.isAfter(maxMoment)) {
this.pickerFilterCache[cacheKey] = false
return false
}
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
this.pickerFilterCache[cacheKey] = true
return true
}
for (let hour = 0; hour < 24; hour++) {
if (this.hourHasAvailableTime(year, month, day, hour, field)) {
this.pickerFilterCache[cacheKey] = true
return true
}
}
this.pickerFilterCache[cacheKey] = false
return false
},
hourHasAvailableTime(year, month, day, hour, field) {
const cacheKey = ['hour', field, year, month, day, hour, this.formData.reserveStartTime || '', this.formData.reserveEndTime || ''].join('|')
if (cacheKey in this.pickerFilterCache) {
return this.pickerFilterCache[cacheKey]
}
const hourMoment = this.buildPickerMoment({ year: year, month: month, day: day, hour: hour, minute: 0 })
if (!this.isValidCalendarMoment(hourMoment, month, day)) {
this.pickerFilterCache[cacheKey] = false
return false
}
const minMoment = this.getFieldMinMoment(field)
const maxMoment = this.getEffectiveFieldMaxMoment(field)
const hourStart = hourMoment.clone().startOf('hour')
const hourEnd = hourMoment.clone().endOf('hour')
if (hourEnd.isBefore(minMoment) || hourStart.isAfter(maxMoment)) {
this.pickerFilterCache[cacheKey] = false
return false
}
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
this.pickerFilterCache[cacheKey] = true
return true
}
for (let minute = 0; minute < 60; minute++) {
const candidate = this.buildPickerMoment({ year: year, month: month, day: day, hour: hour, minute: minute })
if (this.isExactTimeSelectable(candidate, field)) {
this.pickerFilterCache[cacheKey] = true
return true
}
}
this.pickerFilterCache[cacheKey] = false
return false
},
findFirstSelectableDateTime(field, baseMoment) {
let startMoment = this.getNormalizedMoment(baseMoment)
const minMoment = this.getFieldMinMoment(field)
const maxMoment = this.getEffectiveFieldMaxMoment(field)
if (maxMoment.isBefore(minMoment)) {
return null
}
if (startMoment.isBefore(minMoment)) {
startMoment = minMoment.clone()
}
for (let dayOffset = 0; dayOffset <= 366; dayOffset++) {
const dayMoment = startMoment.clone().startOf('day').add(dayOffset, 'day')
if (dayMoment.isAfter(maxMoment, 'day')) {
break
}
if (this.isBlockedDay(dayMoment)) {
continue
}
const startHour = dayMoment.isSame(startMoment, 'day') ? startMoment.hour() : 0
for (let hour = startHour; hour < 24; hour++) {
const startMinute = dayMoment.isSame(startMoment, 'day') && hour === startMoment.hour() ? startMoment.minute() : 0
for (let minute = startMinute; minute < 60; minute++) {
const candidate = dayMoment.clone().hour(hour).minute(minute).second(0).millisecond(0)
if (candidate.isAfter(maxMoment)) {
return null
}
if (this.isExactTimeSelectable(candidate, field)) {
return candidate
}
}
}
}
return null
},
formatPickerNumber(value) {
return value < 10 ? '0' + value : '' + value
},
getWeekdayText(momentValue) {
const weekdayList = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return weekdayList[momentValue.day()]
},
createPickerOption(text, value, disabled) {
return {
text: text,
value: value,
disabled: !!disabled,
}
},
findNearestEnabledIndex(options, preferredValue) {
if (!Array.isArray(options) || !options.length) {
return -1
}
const enabledIndexes = []
options.forEach((item, index) => {
if (!item.disabled) {
enabledIndexes.push(index)
}
})
if (!enabledIndexes.length) {
return -1
}
if (preferredValue === '' || preferredValue === null || typeof preferredValue === 'undefined' || Number.isNaN(preferredValue)) {
return enabledIndexes[0]
}
let targetIndex = enabledIndexes[0]
let targetDistance = Math.abs(Number(options[targetIndex].value) - Number(preferredValue))
enabledIndexes.forEach(index => {
const distance = Math.abs(Number(options[index].value) - Number(preferredValue))
if (distance < targetDistance) {
targetIndex = index
targetDistance = distance
}
})
return targetIndex
},
buildDateOptions(field) {
const minMoment = this.getFieldMinMoment(field).clone().startOf('day')
const maxMoment = this.getEffectiveFieldMaxMoment(field).clone().startOf('day')
const options = []
const cursor = minMoment.clone()
while (cursor.isSameOrBefore(maxMoment, 'day')) {
const text = cursor.format('MM-DD') + ' ' + this.getWeekdayText(cursor)
options.push(this.createPickerOption(text, cursor.format('YYYY-MM-DD'), !this.dayHasAvailableTime(cursor.year(), cursor.month() + 1, cursor.date(), field)))
cursor.add(1, 'day')
}
return options
},
buildHourOptionsByDate(field, dateValue) {
const dateMoment = this.$moment(dateValue, 'YYYY-MM-DD')
const year = dateMoment.year()
const month = dateMoment.month() + 1
const day = dateMoment.date()
const options = []
for (let hour = 0; hour < 24; hour++) {
options.push(this.createPickerOption(this.formatPickerNumber(hour) + '时', hour, !this.hourHasAvailableTime(year, month, day, hour, field)))
}
return options
},
buildMinuteOptionsByDateHour(field, dateValue, hour) {
const dateMoment = this.$moment(dateValue, 'YYYY-MM-DD')
const options = []
for (let minute = 0; minute < 60; minute++) {
const candidate = this.buildPickerMoment({
year: dateMoment.year(),
month: dateMoment.month() + 1,
day: dateMoment.date(),
hour: hour,
minute: minute,
})
options.push(this.createPickerOption(this.formatPickerNumber(minute) + '分', minute, !this.isExactTimeSelectable(candidate, field)))
}
return options
},
buildTimePickerColumns(field, preferredParts) {
const dateOptions = this.buildDateOptions(field)
const enabledDateIndex = dateOptions.findIndex(item => !item.disabled)
if (enabledDateIndex < 0) {
return null
}
let dateIndex = enabledDateIndex
if (preferredParts && preferredParts.date) {
const exactIndex = dateOptions.findIndex(item => item.value === preferredParts.date && !item.disabled)
if (exactIndex >= 0) {
dateIndex = exactIndex
}
}
const selectedDate = dateOptions[dateIndex].value
const hourOptions = this.buildHourOptionsByDate(field, selectedDate)
const hourIndex = this.findNearestEnabledIndex(hourOptions, preferredParts ? preferredParts.hour : null)
if (hourIndex < 0) {
return null
}
const selectedHour = hourOptions[hourIndex].value
const minuteOptions = this.buildMinuteOptionsByDateHour(field, selectedDate, selectedHour)
const minuteIndex = this.findNearestEnabledIndex(minuteOptions, preferredParts ? preferredParts.minute : null)
if (minuteIndex < 0) {
return null
}
const selectedMinute = minuteOptions[minuteIndex].value
return {
parts: {
date: selectedDate,
hour: selectedHour,
minute: selectedMinute,
},
indexes: [dateIndex, hourIndex, minuteIndex],
columns: [
{ values: dateOptions, defaultIndex: dateIndex },
{ values: hourOptions, defaultIndex: hourIndex },
{ values: minuteOptions, defaultIndex: minuteIndex },
],
}
},
sameTimePickerParts(left, right) {
if (!left || !right) {
return false
}
return left.date === right.date
&& left.hour === right.hour
&& left.minute === right.minute
},
syncTimePickerColumns(picker, built) {
if (!picker || !built) {
this.timePickerSyncing = false
return
}
this.timePickerSyncing = true
built.columns.forEach((column, index) => {
picker.setColumnValues(index, column.values)
})
picker.setIndexes(built.indexes)
this.timePickerParts = built.parts
setTimeout(() => {
this.timePickerSyncing = false
}, 0)
},
applyTimePickerState(built, picker, syncColumns = true) {
if (!built) {
return false
}
const currentPicker = picker || this.$refs.timePickerRef
if (syncColumns) {
this.timePickerColumns = built.columns
this.$nextTick(() => {
this.syncTimePickerColumns(currentPicker || this.$refs.timePickerRef, built)
})
return true
}
this.syncTimePickerColumns(currentPicker, built)
return true
},
async init() {
if (!this.siteId) {
this.$toast('未获取到场馆信息')
this.historyBack()
return
}
const siteRes = await this.$axios.post('/platform/siteCug/manage/info', { id: this.siteId })
if (siteRes.code !== 0 || !siteRes.data) {
this.$toast(siteRes.msg || '场馆信息加载失败')
this.historyBack()
return
}
this.row = siteRes.data
this.formData = this.buildDefaultFormData()
await this.queryTimeLimitConfig()
this.siteLoaded = true
},
async queryTimeLimitConfig() {
const fallback = {
filterHolidays: !!this.row.filterHolidays,
holidayList: [],
notApplyTimeList: Array.isArray(this.row.notApplyTimeList) ? this.row.notApplyTimeList : [],
}
try {
const res = await this.$axios.post('/platform/siteCug/apply/timeLimitConfig', {
siteId: this.row.id,
})
if (res.code === 0 && res.data) {
this.timeLimitConfig = {
filterHolidays: !!res.data.filterHolidays,
holidayList: Array.isArray(res.data.holidayList) ? res.data.holidayList : [],
notApplyTimeList: Array.isArray(res.data.notApplyTimeList) ? res.data.notApplyTimeList : [],
}
this.clearTimePickerCache()
return
}
} catch (e) {
}
this.timeLimitConfig = fallback
this.clearTimePickerCache()
},
async reserveTypeChange(value) {
const user = this.$store.state.user || {}
if (value === 'union') {
this.formData.applyUnionId = user.union ? user.union.id : ''
this.formData.applyUnionName = user.union ? user.union.name : ''
this.formData.clubId = ''
this.formData.clubName = ''
return
}
if (value === 'club') {
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
this.formData.clubId = ''
this.formData.clubName = ''
await this.loadManagedClubs()
}
},
async loadManagedClubs() {
try {
const res = await this.$axios.post('/platform/club/examine/apply/getClubsByRole')
this.clubOptions = Array.isArray(res.data) ? res.data : []
if (!this.clubOptions.length) {
this.$toast('您当前没有可预约的协会管理权限')
}
} catch (e) {
this.clubOptions = []
this.$toast('协会列表加载失败,请稍后重试')
}
},
onReserveTypeConfirm(value) {
this.showReserveTypePicker = false
const reserveType = value === '协会预约' ? 'club' : 'union'
this.$set(this.formData, 'reserveType', reserveType)
this.reserveTypeChange(reserveType)
},
async openClubPicker() {
if (this.formData.reserveType !== 'club') {
this.$toast('请先选择协会预约')
return
}
if (!this.clubOptions.length) {
await this.loadManagedClubs()
}
if (!this.clubOptions.length) {
return
}
this.showClubPicker = true
},
onClubConfirm(value, index) {
this.showClubPicker = false
const club = this.clubOptions[index]
this.formData.clubId = club ? club.id : ''
this.formData.clubName = club ? club.clubName : value
},
openTimePicker(field) {
this.timePickerField = field
this.timePickerSyncing = false
this.clearTimePickerCache()
let defaultTime = this.formData[field] ? this.getNormalizedMoment(this.formData[field]) : null
if (!defaultTime || !defaultTime.isValid()) {
defaultTime = field === 'reserveEndTime' && this.formData.reserveStartTime
? this.getNormalizedMoment(this.formData.reserveStartTime).add(1, 'hour')
: this.$moment().add(1, 'hour').startOf('hour')
}
const availableTime = this.findFirstSelectableDateTime(field, defaultTime)
if (!availableTime) {
this.$toast('当前没有可预约的时间')
return
}
const built = this.buildTimePickerColumns(field, this.extractTimePickerPartsFromMoment(availableTime))
if (!built) {
this.$toast('当前没有可预约的时间')
return
}
this.showTimePicker = true
this.applyTimePickerState(built, null, true)
},
onTimePickerChange(picker, values) {
if (!this.timePickerField || this.timePickerSyncing) {
return
}
const parts = this.extractTimePickerParts(values)
const built = this.buildTimePickerColumns(this.timePickerField, parts)
if (!built) {
return
}
if (this.sameTimePickerParts(parts, built.parts)) {
this.timePickerParts = built.parts
return
}
this.applyTimePickerState(built, picker, false)
},
onTimeConfirm(values) {
const parts = this.extractTimePickerParts(values)
const candidate = this.buildPickerMoment(parts)
if (!this.isExactTimeSelectable(candidate, this.timePickerField)) {
this.$toast('该时间不可预约,请重新选择')
const built = this.buildTimePickerColumns(this.timePickerField, parts)
this.applyTimePickerState(built, this.$refs.timePickerRef)
return
}
const formatted = candidate.format('YYYY-MM-DD HH:mm:ss')
this.timePickerSyncing = false
this.showTimePicker = false
this.$set(this.formData, this.timePickerField, formatted)
this.clearTimePickerCache()
this.timeFieldChange(this.timePickerField)
},
async normalizeReserveTypeData() {
const user = this.$store.state.user || {}
if (this.formData.reserveType === 'union') {
this.formData.applyUnionId = user.union ? user.union.id : ''
this.formData.applyUnionName = user.union ? user.union.name : ''
this.formData.clubId = ''
this.formData.clubName = ''
return true
}
if (this.formData.reserveType === 'club') {
if (!this.clubOptions.length) {
await this.loadManagedClubs()
}
const club = this.clubOptions.find(item => item.id === this.formData.clubId)
this.formData.clubName = club ? club.clubName : ''
this.formData.applyUnionId = ''
this.formData.applyUnionName = ''
return true
}
return false
},
normalizeTime(timeStr) {
if (!timeStr) {
return '00:00:00'
}
return timeStr.length === 5 ? timeStr + ':00' : timeStr
},
getDisabledRangesByDay(dayStr) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list
.filter(item => item && item.date && this.$moment(item.date).format('YYYY-MM-DD') === dayStr)
.sort((a, b) => this.normalizeTime(a.startTime).localeCompare(this.normalizeTime(b.startTime)))
},
isDateTimeBlocked(dateTimeStr) {
if (!dateTimeStr) {
return false
}
const target = this.$moment(dateTimeStr)
const dayStr = target.format('YYYY-MM-DD')
if (target.day() === 0 || target.day() === 6) {
return true
}
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(dayStr)) {
return true
}
const disabledRanges = this.getDisabledRangesByDay(dayStr)
return disabledRanges.some(item => {
const start = this.$moment(dayStr + ' ' + this.normalizeTime(item.startTime))
const end = this.$moment(dayStr + ' ' + this.normalizeTime(item.endTime))
return target.isSameOrAfter(start) && target.isBefore(end)
})
},
hasHolidayInRange(start, end) {
const current = start.clone().startOf('day')
const endDay = end.clone().startOf('day')
while (current.isSameOrBefore(endDay)) {
if (current.day() === 0 || current.day() === 6) {
return true
}
if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(current.format('YYYY-MM-DD'))) {
return true
}
current.add(1, 'day')
}
return false
},
overlapsDisabledRange(start, end) {
const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : []
return list.some(item => {
if (!item || !item.date || !item.startTime || !item.endTime) {
return false
}
const rangeStart = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.startTime))
const rangeEnd = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.endTime))
return start.isBefore(rangeEnd) && end.isAfter(rangeStart)
})
},
showSubmitError(message) {
return this.$dialog.alert({
title: '提示',
message: message || '提交失败',
}).catch(() => {})
},
validateTimeLimit(showMessage = true, useDialog = false) {
const reserveStartTime = this.formData.reserveStartTime
const reserveEndTime = this.formData.reserveEndTime
if (!reserveStartTime || !reserveEndTime) {
return true
}
const start = this.$moment(reserveStartTime)
const end = this.$moment(reserveEndTime)
const showError = (message) => {
if (!showMessage) {
return
}
if (useDialog) {
this.showSubmitError(message)
return
}
this.$toast(message)
}
if (this.isDateTimeBlocked(reserveStartTime) || this.isDateTimeBlocked(reserveEndTime)) {
showError('预约时间不能选择周末、节假日或禁用时间')
return false
}
if (this.hasHolidayInRange(start, end)) {
showError('预约时间范围内包含周末或节假日,请重新选择')
return false
}
if (this.overlapsDisabledRange(start, end)) {
showError('预约时间范围与禁用时间冲突,请重新选择')
return false
}
return true
},
timeFieldChange(field) {
const value = this.formData[field]
this.clearTimePickerCache()
if (!value) {
return
}
if (this.isDateTimeBlocked(value)) {
this.$toast('该时间点不可预约,请重新选择')
this.$set(this.formData, field, '')
return
}
if (field === 'reserveStartTime' && this.formData.reserveEndTime) {
const start = this.$moment(value)
const end = this.$moment(this.formData.reserveEndTime)
if (!end.isAfter(start)) {
this.$set(this.formData, 'reserveEndTime', '')
this.$toast('结束时间需晚于开始时间,请重新选择')
return
}
}
if (field === 'reserveEndTime' && this.formData.reserveStartTime) {
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(value)
if (!end.isAfter(start)) {
this.$toast('预约结束时间必须晚于开始时间')
this.$set(this.formData, field, '')
return
}
}
if (this.formData.reserveStartTime && this.formData.reserveEndTime && !this.validateTimeLimit()) {
this.$set(this.formData, field, '')
}
},
onSubmit() {
this.$refs.formRef.validate().then(async () => {
if (!this.formData.reserveType) {
await this.showSubmitError('请选择预约类型')
return
}
await this.normalizeReserveTypeData()
if (this.formData.reserveType === 'union' && !this.formData.applyUnionId) {
await this.showSubmitError('当前用户未关联分工会,不能发起分工会预约')
return
}
if (this.formData.reserveType === 'club' && !this.formData.clubId) {
await this.showSubmitError('请选择您管理的协会')
return
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
await this.showSubmitError('请选择预约开始和结束时间')
return
}
const start = this.$moment(this.formData.reserveStartTime)
const end = this.$moment(this.formData.reserveEndTime)
if (!start.isAfter(this.$moment())) {
await this.showSubmitError('预约开始时间必须晚于当前时间')
return
}
if (!end.isAfter(start)) {
await this.showSubmitError('预约结束时间必须晚于开始时间')
return
}
if (!this.validateTimeLimit(true, true)) {
return
}
this.$dialog.confirm({
title: '提示',
message: '您确定要提交吗?',
}).then(() => {
const loading = this.$toast.loading({
message: '提交中...',
forbidClick: true,
overlay: true,
duration: 0,
})
this.$axios.post('/platform/siteCug/apply/submit', {
data: JSON.stringify(this.formData),
}).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '提交成功')
this.$pjaxReplace('/platform/siteCug/apply/h5')
}
}).catch((err) => {
this.showSubmitError((err && err.msg) || '提交失败')
}).finally(() => {
loading.close()
})
}).catch(() => {})
}).catch(() => {})
},
},
created() {
this.init()
}
})
@@ -0,0 +1,167 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.page-container {
padding-bottom: 84px;
background: #f7f8fa;
min-height: calc(100vh - 46px);
}
.footer-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 12px;
padding: 10px 12px calc(env(safe-area-inset-bottom) + 10px);
background: #ffffff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
}
.footer-actions .van-button {
flex: 1;
}
.disabled-time-item {
line-height: 20px;
margin-bottom: 4px;
}
/deep/ .direction-column-cell .van-cell__value {
white-space: normal;
text-align: left;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="场馆申请" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="page-container" v-if="siteLoaded">
<van-cell-group title="场馆信息">
<van-cell title="场馆名称" :value="row.name || '-' "></van-cell>
<van-cell title="场地地址" class="direction-column-cell">
<template #default>
{{ row.address || '-' }}
</template>
</van-cell>
<van-cell title="联系人" :value="row.contactName || '-' "></van-cell>
<van-cell title="联系电话" :value="row.contactPhone || '-' "></van-cell>
<van-cell title="场地类型" :value="row.typeName || '-' "></van-cell>
</van-cell-group>
<van-cell-group title="预约须知">
<van-cell title="可预约日期" value="仅限工作日"></van-cell>
<van-cell title="节假日限制"
:value="timeLimitConfig.filterHolidays ? '节假日不可预约' : '不排除节假日'"></van-cell>
<van-cell title="禁用时段" class="direction-column-cell">
<template #default>
<div v-if="timeLimitConfig.notApplyTimeList.length">
<div
class="disabled-time-item"
v-for="(item, index) in timeLimitConfig.notApplyTimeList"
:key="item.date + item.startTime + item.endTime + index">
{{ item.date }} {{ item.startTime }} - {{ item.endTime }}
</div>
</div>
<div v-else>暂无禁用时段</div>
</template>
</van-cell>
</van-cell-group>
<van-form ref="formRef" class="form-container" :show-error-message="false">
<van-cell-group title="申请信息">
<van-field v-model="formData.applyUserName" label="预约人" name="applyUserName" readonly
:rules="[{ required: true, message: '请确认预约人' }]"></van-field>
<van-field v-model="formData.applyLoginName" label="工号" name="applyLoginName" readonly
:rules="[{ required: true, message: '请确认工号' }]"></van-field>
<van-field v-model="formData.applyUnitName" label="所属单位" name="applyUnitName" readonly
:rules="[{ required: true, message: '请确认所属单位' }]"></van-field>
<van-field
:value="reserveTypeText"
label="预约类型"
name="reserveType"
readonly
clickable
is-link
required
placeholder="请选择预约类型"
@click="showReserveTypePicker = true"
:rules="[{ required: true, message: '请选择预约类型' }]">
</van-field>
<van-field
v-if="formData.reserveType === 'union'"
v-model="formData.applyUnionName"
label="分工会"
name="applyUnionName"
readonly
placeholder="自动读取当前登录人的分工会">
</van-field>
<van-field
v-if="formData.reserveType === 'club'"
v-model="formData.clubName"
label="协会"
name="clubName"
readonly
clickable
is-link
required
placeholder="请选择您管理的协会"
@click="openClubPicker"
:rules="[{ required: true, message: '请选择您管理的协会' }]">
</van-field>
<van-field v-model="formData.applyMobile" label="联系电话" name="applyMobile" type="tel" maxlength="11"
required placeholder="请输入联系电话"
:rules="[{ required: true, message: '请输入联系电话' }]"></van-field>
<van-field v-model="formData.reserveStartTime" label="开始时间" name="reserveStartTime" readonly
clickable is-link required placeholder="请选择预约开始时间"
@click="openTimePicker('reserveStartTime')"
:rules="[{ required: true, message: '请选择预约开始时间' }]"></van-field>
<van-field v-model="formData.reserveEndTime" label="结束时间" name="reserveEndTime" readonly clickable
is-link required placeholder="请选择预约结束时间" @click="openTimePicker('reserveEndTime')"
:rules="[{ required: true, message: '请选择预约结束时间' }]"></van-field>
<van-field v-model="formData.applyCause" label="预约事由" name="applyCause" required rows="4" autosize
type="textarea" maxlength="1000" show-word-limit placeholder="请输入预约事由"
:rules="[{ required: true, message: '请输入预约事由' }]"></van-field>
</van-cell-group>
</van-form>
</div>
<div class="footer-actions" v-if="siteLoaded">
<van-button plain round type="info" @click="historyBack">取消</van-button>
<van-button round type="primary" color="#246fb4" @click="onSubmit">提交预约</van-button>
</div>
<van-popup v-model="showReserveTypePicker" position="bottom" round>
<van-picker show-toolbar :columns="reserveTypeColumns" @confirm="onReserveTypeConfirm"
@cancel="showReserveTypePicker = false"></van-picker>
</van-popup>
<van-popup v-model="showClubPicker" position="bottom" round>
<van-picker show-toolbar :columns="clubColumns" @confirm="onClubConfirm"
@cancel="showClubPicker = false"></van-picker>
</van-popup>
<van-popup v-model="showTimePicker" position="bottom" round>
<van-picker
ref="timePickerRef"
show-toolbar
value-key="text"
:title="timePickerTitle"
:columns="timePickerColumns"
@change="onTimePickerChange"
@confirm="onTimeConfirm"
@cancel="timePickerSyncing = false; showTimePicker = false">
</van-picker>
</van-popup>
</div>
<script nonce="${cspNonce!}">
<!--#include('apply.js'){}#-->
</script>
<!--#
}
#-->
@@ -0,0 +1,106 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</style>
<div id="app">
<van-nav-bar title="场馆预约" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入名称或地址搜索"
@search="doSearch"
></van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.type" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/siteCug/apply/pageData" :page_form.sync="pageForm" ref="tableListRef" title="name" @ready="onReady">
<template v-slot="{ row }">
<table-column label="场地地址">{{ row.address }}</table-column>
<table-column label="联系人">{{ row.contactName }}</table-column>
<table-column label="联系方式">{{ row.contactPhone }}</table-column>
<table-column label="场地类型">{{ row.typeName }}</table-column>
</template>
<template #actions="{ row }">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" @click="onApply(row)">
<i class="fa fa-edit"></i>
<span>预约</span>
</div>
</template>
</table-list>
<info ref="infoRef"></info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/siteInfo.js'){}#-->
new Vue({
el: '#app',
store,
components: {
info: siteInfo,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
type: null,
},
typeOptions: [],
}
},
methods: {
historyBack,
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onApply(row) {
this.$pjaxReplace('/platform/siteCug/apply/form/h5?siteId=' + row.id)
},
async onReady() {
const typeList = await this.querySiteType()
this.typeOptions = [
{
text: '全部类型',
value: null,
}
].concat(typeList.map((item) => ({ text: item.name, value: item.id })))
if (this.typeOptions.length > 0) {
this.pageForm.type = this.typeOptions[0].value
this.doSearch()
}
},
async querySiteType() {
const res = await this.$axios.post('/platform/siteCug/function/type/queryFunctionType')
return Array.isArray(res.data) ? res.data : []
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,80 @@
const siteCugApplyInfoH5 = {
template:
/*language=HTML*/
`
<van-action-sheet v-model="visible" :title="sheetTitle">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="预约场地">{{ viewData.siteName }}</van-cell>
<van-cell title="预约人">{{ viewData.applyUserName }}</van-cell>
<van-cell title="预约人工号">{{ viewData.applyLoginName }}</van-cell>
<van-cell title="所属单位">{{ viewData.applyUnitName }}</van-cell>
<van-cell title="预约类型">{{ reserveTypeLabel }}</van-cell>
<van-cell title="预约主体">{{ reserveTargetName }}</van-cell>
<van-cell title="开始时间">{{ viewData.reserveStartTime }}</van-cell>
<van-cell title="结束时间">{{ viewData.reserveEndTime }}</van-cell>
<van-cell title="联系电话">{{ viewData.applyMobile }}</van-cell>
<van-cell title="预约事由" class="direction-column-cell">{{ viewData.applyCause }}</van-cell>
</van-cell-group>
<template v-for="task in doneTasks">
<van-cell-group :title="task.displayName" v-if="task.ext.isFirstTaskNode" :key="task.id + '-first'">
<van-cell title="申请用户">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})</van-cell>
<van-cell title="申请时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</van-cell>
</van-cell-group>
<van-cell-group :title="task.displayName" v-else :key="task.id + '-done'">
<van-cell title="办理用户">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})</van-cell>
<van-cell title="办理时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" class="direction-column-cell">
<div v-html="task.taskFormData.opinion || task.taskFormData.tf_opinion || '-' "></div>
</van-cell>
</van-cell-group>
</template>
</div>
<slot></slot>
</van-action-sheet>
`,
dicts: ['PROCESS_TASK_SUBMIT_TYPE'],
data() {
return {
visible: false,
viewData: {},
doneTasks: [],
row: null,
}
},
computed: {
reserveTypeLabel() {
return this.viewData.reserveType === 'club' ? '协会预约' : '分工会预约'
},
reserveTargetName() {
return this.viewData.reserveType === 'club' ? (this.viewData.clubName || '-') : (this.viewData.applyUnionName || '-')
},
sheetTitle() {
return this.visible && this.$slots.default ? '审核预约' : '预约详情'
},
},
methods: {
onOpen(row) {
this.row = row
this.visible = true
this.viewData = row
this.getDoneTasks()
},
onClose() {
this.visible = false
},
getDoneTasks() {
this.$axios.post('/flow/common/doneTasks', { bizId: this.row.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
},
},
};
@@ -0,0 +1,105 @@
const siteInfo = {
template: /*language=HTML*/ `
<div>
<van-action-sheet v-model="visible" title="场馆信息">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="创建人" :value="viewData.createUserName || '-' "></van-cell>
<van-cell title="场馆名称" :value="viewData.name || '-' "></van-cell>
<van-cell title="场地地址" class="direction-column-cell">
<template #default>
{{ viewData.address || '-' }}
</template>
</van-cell>
<van-cell title="联系人" :value="viewData.contactName || '-' "></van-cell>
<van-cell title="联系电话" :value="viewData.contactPhone || '-' "></van-cell>
<van-cell title="排序编号" :value="viewData.sortNum || '-' "></van-cell>
<van-cell title="容纳人数" :value="viewData.maxNum || '-' "></van-cell>
<van-cell title="场地类型" :value="viewData.typeName || '-' "></van-cell>
<van-cell title="性别限制">
<template #default>
<span v-if="viewData.sexLimit === 1">男</span>
<span v-else-if="viewData.sexLimit === 2">女</span>
<span v-else>不限制</span>
</template>
</van-cell>
<van-cell title="开启状态">
<template #default>
<span v-if="viewData.state">开启</span>
<span v-else>禁用</span>
</template>
</van-cell>
<van-cell title="排除节假日">
<template #default>
<span v-if="viewData.filterHolidays">是</span>
<span v-else>否</span>
</template>
</van-cell>
<van-cell title="场地介绍" class="direction-column-cell">
<template #default>
<div v-if="viewData.introduce" v-html="viewData.introduce"></div>
<div v-else>暂无场地介绍</div>
</template>
</van-cell>
</van-cell-group>
<van-cell-group title="禁用时间" v-if="viewData.notApplyTimeList && viewData.notApplyTimeList.length > 0">
<table class="table-class">
<thead>
<tr>
<th>日期</th>
<th>开始时间</th>
<th>结束时间</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in viewData.notApplyTimeList" :key="item.date + item.startTime + item.endTime + index">
<td>{{ item.date }}</td>
<td>{{ item.startTime }}</td>
<td>{{ item.endTime }}</td>
</tr>
</tbody>
</table>
</van-cell-group>
</div>
</van-action-sheet>
</div>
`,
data() {
return {
viewData: {},
visible: false,
}
},
methods: {
onOpen(row) {
this.viewData = row
this.visible = true
},
},
style: /*language=CSS*/ `
/deep/ .direction-column-cell .van-cell__value {
white-space: normal;
text-align: left;
}
/deep/ .table-class {
width: 100%;
border-radius: 5px;
overflow: hidden;
line-height: 1.5rem;
font-size: 13px;
table-layout: fixed;
border-collapse: collapse;
}
/deep/ .table-class th {
background-color: #f2f2f2;
border: 1px solid #dddddd;
}
/deep/ .table-class tr {
text-align: center;
border-bottom: 1px solid #dddddd;
}
/deep/ .table-class td {
border: 1px solid #dddddd;
}
`
}
@@ -0,0 +1,151 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
</style>
<div id="app" v-cloak>
<van-nav-bar title="我的预约" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.siteType" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.siteId" :options="siteOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<table-list api="/platform/siteCug/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="siteName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="场地名称">{{row.siteName}}</table-column>
<table-column label="预约类型">{{row.reserveType === 'club' ? '协会预约' : '分工会预约'}}</table-column>
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
<table-column label="当前节点">{{row.taskName || '-'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn delete" @click="onDelete(row)" v-if="row.canCancel">
<i class="fa fa-trash"></i>
<span>取消预约</span>
</div>
</template>
</table-list>
<info ref="infoRef"></info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
components: {
info: siteCugApplyInfoH5,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
siteType: null,
siteId: null,
},
typeOptions: [
{
text: '全部类型',
value: null,
}
],
siteOptions: [
{
text: '全部场地',
value: null,
}
],
}
},
methods: {
historyBack,
async onReady() {
await this.querySiteType()
await this.querySites()
this.doSearch()
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onDelete(row) {
this.$dialog.confirm({
title: '提示',
message: row.yearlyBatch
? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || 0) + ' 条预约记录,是否继续?')
: '您确定要取消该预约吗?',
}).then(() => {
this.$axios.post('/platform/siteCug/mine/delete', { id: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '取消成功')
this.doSearch()
} else {
this.$dialog.alert({
title: '提示',
message: res.msg || '取消失败',
}).catch(() => {})
}
}).catch((err) => {
this.$dialog.alert({
title: '提示',
message: (err && err.msg) || '取消失败',
}).catch(() => {})
})
}).catch(() => {})
},
querySiteType() {
return this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.typeOptions = [
{
text: '全部类型',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
querySites() {
return this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.siteOptions = [
{
text: '全部场地',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
})
</script>
<!--#
}
#-->
@@ -0,0 +1,272 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.action-btn.disabled {
opacity: 0.45;
pointer-events: none;
}
.action-btn.loading {
opacity: 0.9;
}
.action-btn .van-loading {
margin-right: 4px;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="校工会审核" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
input-align="left"
placeholder="请输入姓名/工号/场地搜索"
@search="doSearch"
></van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.siteType" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.siteId" :options="siteOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
</van-dropdown-menu>
<van-tabs v-model="pageForm.approvalText" @change="onApprovalTabChange">
<van-tab title="已审核" name="1"></van-tab>
<van-tab title="未审核" name="0"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/siteCug/schoolUnionAudit/pageData" :page_form.sync="pageForm" ref="tableListRef" title="applyUserName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="场地名称">{{row.siteName}}</table-column>
<table-column label="预约类型">{{row.reserveType === 'club' ? '协会预约' : '分工会预约'}}</table-column>
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '-'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="canRevoke(row)" :class="{ loading: revokeLoading }" @click="onRevoke(row)">
<van-loading v-if="revokeLoading" size="14px" color="#fff"></van-loading>
<i v-else class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">{{formData.taskName}}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
maxlength="100"
show-word-limit
></van-field>
</van-form>
<div style="display:flex;justify-content:space-between;column-gap:10px;padding:10px;">
<van-button type="info" block :loading="auditLoading" :disabled="auditLoading" @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" block :loading="auditLoading" :disabled="auditLoading" @click="handleTaskAction(2)">不同意</van-button>
<van-button type="primary" block :loading="auditLoading" :disabled="auditLoading" @click="handleTaskAction(1)">同意</van-button>
</div>
</div>
</info>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#-->
new Vue({
el: '#app',
store,
components: {
info: siteCugApplyInfoH5,
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
approvalText: '0',
approval: false,
siteType: null,
siteId: null,
},
typeOptions: [
{
text: '全部类型',
value: null,
}
],
siteOptions: [
{
text: '全部场地',
value: null,
}
],
formData: {},
showApprovalForm: false,
auditLoading: false,
revokeLoading: false,
}
},
methods: {
historyBack,
async onReady() {
await this.querySiteType()
await this.querySites()
this.doSearch()
},
onApprovalTabChange(val) {
this.pageForm.approval = val === '1'
this.doSearch()
},
canRevoke(row) {
return Number(row.instanceState) === 20
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onAudit(row) {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
applyId: row.id,
taskName: row.curTaskName,
tf_opinion: '',
yearlyBatch: row.yearlyBatch,
batchAuditCount: row.batchAuditCount,
}
this.$refs.infoRef.onOpen(row)
},
async handleTaskAction(submitType) {
if (this.auditLoading) {
return
}
try {
await this.$refs.formRef.validate()
this.$dialog.confirm({
title: '提示',
message: this.formData.yearlyBatch
? ('该记录属于“预约本年”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
: '您确定要提交吗?',
}).then(() => {
this.auditLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/executeTask', {
data: JSON.stringify({
...this.formData,
submitType: submitType,
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg || '提交成功')
this.doSearch()
} else {
this.$dialog.alert({
title: '提示',
message: res.msg || '提交失败',
}).catch(() => {})
}
}).catch((err) => {
this.$dialog.alert({
title: '提示',
message: (err && err.msg) || '提交失败',
}).catch(() => {})
}).finally(() => {
this.auditLoading = false
})
}).catch(() => {})
} catch (e) {
}
},
onRevoke(row) {
if (!this.canRevoke(row) || this.revokeLoading) {
return
}
this.$dialog.confirm({
title: '提示',
message: row.yearlyBatch
? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?',
}).then(() => {
this.revokeLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/revokeTask', { taskId: row.taskId, applyId: row.id }).then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg || '撤回成功')
this.doSearch()
} else {
this.$dialog.alert({
title: '提示',
message: res.msg || '撤回失败',
}).catch(() => {})
}
}).catch((err) => {
this.$dialog.alert({
title: '提示',
message: (err && err.msg) || '撤回失败',
}).catch(() => {})
}).finally(() => {
this.revokeLoading = false
})
}).catch(() => {})
},
querySiteType() {
return this.$axios.post('/platform/siteCug/function/type/queryFunctionType').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.typeOptions = [
{
text: '全部类型',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
querySites() {
return this.$axios.post('/platform/siteCug/manage/querySites').then((res) => {
const list = Array.isArray(res.data) ? res.data : []
this.siteOptions = [
{
text: '全部场地',
value: null,
}
].concat(list.map((item) => ({
text: item.name,
value: item.id,
})))
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
},
})
</script>
<!--#
}
#-->