This commit is contained in:
=
2026-07-03 17:25:27 +08:00
parent 6380842b34
commit 34627245ac
9 changed files with 503 additions and 25 deletions
@@ -307,6 +307,9 @@ public class SiteCugApplyController {
if (item == null || StrUtil.hasBlank(item.getReserveStartTime(), item.getReserveEndTime())) {
continue;
}
if (isOccupiedByCurrentUser(item)) {
continue;
}
DateTime reserveStart = DateUtil.parseDateTime(item.getReserveStartTime());
DateTime reserveEnd = DateUtil.parseDateTime(item.getReserveEndTime());
if (!reserveEnd.isAfter(reserveStart)) {
@@ -389,6 +392,13 @@ public class SiteCugApplyController {
}
}
// 当前登录人占用了该预约时,该预约占用的时间块对当前登录人放开,对其他人仍保持锁定
private boolean isOccupiedByCurrentUser(SiteCugApply apply) {
return apply != null
&& StrUtil.isNotBlank(apply.getOccupyUserId())
&& StrUtil.equals(apply.getOccupyUserId(), SecurityUtil.getUserId());
}
private boolean intersectsAny(int start, int end, List<int[]> ranges) {
for (int[] range : ranges) {
if (range != null && start < range[1] && end > range[0]) {
@@ -821,5 +831,22 @@ public class SiteCugApplyController {
for (ProcessTask task : doingTaskList) {
flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args);
}
if (applyService.shouldDirectFinishOccupiedApply(apply)) {
directFinishOccupiedApply(instance, args);
}
}
// 占用人提交与占用时间交叉的预约时,复用已有流程引擎自动结束后续审核任务
private void directFinishOccupiedApply(ProcessInstance instance, Dict args) {
List<ProcessTask> doingTaskList = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null);
if (doingTaskList.isEmpty()) {
return;
}
Dict directArgs = args.clone();
directArgs.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.AGREE.getCode());
directArgs.set(FlowConst.APPROVAL_COMMENT, "占用预约自动通过");
for (ProcessTask task : doingTaskList) {
flowEngine.executeAndJumpToEnd(task.getId(), FlowConst.AUTO_ID, directArgs);
}
}
}
@@ -105,6 +105,13 @@ public class SiteCugManageController {
if ((fullDayOpenHour == null || fullDayOpenHour.isEmpty()) && info.getReserveTimeType() == 2 && info.getOpenHours() != null && !info.getOpenHours().isEmpty()) {
fullDayOpenHour = info.getOpenHours().get(0);
}
// 场地预约次数限制作为后续提交校验的依据,保存场地时必须配置为正整数
if (info.getUnionYearReserveLimit() == null || info.getUnionYearReserveLimit() <= 0) {
return Result.error("分工会每年预约次数必须大于0");
}
if (info.getClubWeekReserveLimit() == null || info.getClubWeekReserveLimit() <= 0) {
return Result.error("社团每周预约次数必须大于0");
}
if (info.getReserveTimeType() == 1) {
if (segmentedOpenHours == null || segmentedOpenHours.isEmpty()) {
return Result.error("Please add at least one booking slot");
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.dayofficework.siteCug.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.FlowEngine;
@@ -24,6 +25,7 @@ import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
@IocBean
@Ok("json:full")
@@ -87,6 +89,28 @@ public class SiteCugRecordController {
return Result.success();
}
@At
@ApiOperation("占用预约记录")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("siteCug.record")
public Result occupy(@Param("id") String id, @Param("message") String message) {
Map<Boolean, String> result = applyService.occupyRecord(id, message, false);
return result.containsKey(false) ? Result.error(result.get(false)) : Result.success(result.get(true));
}
@At
@ApiOperation("批量占用预约记录")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("siteCug.record")
public Result occupyBatch(@Param("ids") String ids, @Param("message") String message) {
if (StrUtil.isBlank(ids)) {
return Result.error("请选择需要占用的预约");
}
List<String> idList = StrUtil.split(ids, ",").stream().filter(StrUtil::isNotBlank).toList();
Map<Boolean, String> result = applyService.occupyRecords(idList, message, false);
return result.containsKey(false) ? Result.error(result.get(false)) : Result.success(result.get(true));
}
@At
@Ok("void")
@ApiOperation("导出预约记录")
@@ -110,5 +110,15 @@ public class SiteCugApply extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 200)
private String backOption;
@Column
@Comment("占用人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String occupyUserId;
@Column
@Comment("占用通知消息")
@ColDefine(type = ColType.VARCHAR, width = 1000)
private String occupyMessage;
private String yearlyReserveEndDate;
}
@@ -83,6 +83,18 @@ public class SiteCugInfo extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 20)
private String termEndDate;
@Column
@Comment("分工会每年预约次数")
@ColDefine(type = ColType.INT)
@Default("2")
private Integer unionYearReserveLimit;
@Column
@Comment("社团每周预约次数")
@ColDefine(type = ColType.INT)
@Default("2")
private Integer clubWeekReserveLimit;
@Column
@Comment("场地的禁用时间")
@ColDefine(type = ColType.MYSQL_JSON)
@@ -7,6 +7,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.sql.Sql;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
public interface SiteCugApplyService extends BaseService<SiteCugApply> {
@@ -21,4 +22,13 @@ public interface SiteCugApplyService extends BaseService<SiteCugApply> {
// 场馆预约查询页的导出逻辑统一放在 servicecontroller 只负责接收请求
void exportRecord(SiteCugRecordPageForm pageForm, HttpServletResponse response);
// 占用单条预约记录保存占用人和通知内容真实发送消息代码当前保留为注释便于测试流程
Map<Boolean, String> occupyRecord(String id, String message, boolean sendMsg);
// 批量占用预约记录先统一校验再保存避免出现部分记录已占用部分记录失败的状态
Map<Boolean, String> occupyRecords(List<String> ids, String message, boolean sendMsg);
// 判断当前预约是否命中当前登录人的占用时间段命中后提交预约可自动通过审核
boolean shouldDirectFinishOccupiedApply(SiteCugApply apply);
}
@@ -10,10 +10,10 @@ 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.sys.services.SysMsgService;
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;
@@ -46,6 +46,8 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
@Inject
private SysClubService sysClubService;
@Inject
private SysMsgService sysMsgService;
public SiteCugApplyServiceImpl(Dao dao) {
super(dao);
@@ -96,6 +98,10 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
if (intersectsDisabledTime(siteInfo, apply.getReserveStartTime(), apply.getReserveEndTime())) {
return Map.of(false, "预约时间落在禁用时间内");
}
Map<Boolean, String> reserveLimitValidate = validateReserveLimit(apply, siteInfo);
if (reserveLimitValidate.containsKey(false)) {
return reserveLimitValidate;
}
Sql sql = Sqls.create("""
select
@@ -108,8 +114,7 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
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);
cnd.and("ins.state", "in", buildEffectiveProcessStateList());
sql.setCondition(cnd);
List<SiteCugApply> applyList = listEntity(sql);
@@ -118,6 +123,9 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
boolean overlap = DateUtil.parseDateTime(item.getReserveStartTime()).isBefore(DateUtil.parseDateTime(apply.getReserveEndTime()))
&& DateUtil.parseDateTime(item.getReserveEndTime()).isAfter(DateUtil.parseDateTime(apply.getReserveStartTime()));
if (overlap) {
if (isOccupiedByCurrentUser(item)) {
continue;
}
if (StrUtil.isBlank(apply.getId()) && StrUtil.equals(item.getApplyUserId(), SecurityUtil.getUserId())) {
return Map.of(false, "该时间段您已存在预约");
}
@@ -127,6 +135,216 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
return Map.of(true, "");
}
@Override
public Map<Boolean, String> occupyRecord(String id, String message, boolean sendMsg) {
if (StrUtil.isBlank(message)) {
return Map.of(false, "请填写通知消息");
}
SiteCugApply apply = fetch(id);
if (apply == null) {
return Map.of(false, "记录不存在");
}
Map<Boolean, String> validate = validateOccupyApply(apply);
if (validate.containsKey(false)) {
return validate;
}
saveOccupyApply(apply, message, sendMsg);
return Map.of(true, "占用成功");
}
// 已通过校验后统一保存占用信息测试阶段真实发送消息代码保留注释
private void saveOccupyApply(SiteCugApply apply, String message, boolean sendMsg) {
// 保存占用人和通知内容后续预约校验据此只放行当前占用人
apply.setOccupyUserId(SecurityUtil.getUserId());
apply.setOccupyMessage(message);
updateIgnoreNull(apply);
// 测试占用流程时先不真实发送消息保留日志用于确认保存和跳转链路是否正常
log.info("场地预约占用测试消息,applyId={}receiverLoginName={}message={}", apply.getId(), apply.getApplyLoginName(), message);
if (sendMsg) {
// sysMsgService.sendMsgInSys(List.of(apply.getApplyLoginName()), "场地预约占用通知", message, SecurityUtil.getUserId());
}
}
@Override
public Map<Boolean, String> occupyRecords(List<String> ids, String message, boolean sendMsg) {
if (ids == null || ids.isEmpty()) {
return Map.of(false, "请选择需要占用的预约");
}
if (StrUtil.isBlank(message)) {
return Map.of(false, "请填写通知消息");
}
List<SiteCugApply> applyList = new ArrayList<>();
for (String id : ids) {
SiteCugApply apply = fetch(id);
if (apply == null) {
return Map.of(false, "记录不存在");
}
Map<Boolean, String> validate = validateOccupyApply(apply);
if (validate.containsKey(false)) {
return validate;
}
applyList.add(apply);
}
for (SiteCugApply apply : applyList) {
saveOccupyApply(apply, message, sendMsg);
}
return Map.of(true, "占用成功");
}
// 占用前统一校验预约状态避免单条和批量接口出现不同判断
private Map<Boolean, String> validateOccupyApply(SiteCugApply apply) {
if (StrUtil.isBlank(apply.getApplyLoginName())) {
return Map.of(false, "预约人工号为空,无法发送消息");
}
if (StrUtil.isBlank(apply.getReserveStartTime()) || !DateUtil.parseDateTime(apply.getReserveStartTime()).isAfter(DateUtil.date())) {
return Map.of(false, "预约已开始,不能占用");
}
Integer state = queryInstanceState(apply.getId());
if (state == null || state != ProcessInstanceStateEnum.FINISHED.getCode()) {
return Map.of(false, "仅可占用已通过且未开始的预约记录");
}
if (StrUtil.isNotBlank(apply.getOccupyUserId()) && !StrUtil.equals(apply.getOccupyUserId(), SecurityUtil.getUserId())) {
return Map.of(false, "该预约已被其他人占用");
}
return Map.of(true, "");
}
private Integer queryInstanceState(String id) {
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());
dao().execute(sql);
List<NutMap> list = sql.getList(NutMap.class);
return list != null && !list.isEmpty() ? list.get(0).getInt("instanceState") : null;
}
// 当前登录人占用了该记录时允许其再次提交与该记录有交叉的预约
private boolean isOccupiedByCurrentUser(SiteCugApply apply) {
return apply != null
&& StrUtil.isNotBlank(apply.getOccupyUserId())
&& StrUtil.equals(apply.getOccupyUserId(), SecurityUtil.getUserId());
}
@Override
public boolean shouldDirectFinishOccupiedApply(SiteCugApply apply) {
if (apply == null || StrUtil.hasBlank(apply.getSiteId(), apply.getReserveStartTime(), apply.getReserveEndTime())) {
return 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("sa.siteId", "=", apply.getSiteId());
cnd.andEX("sa.id", "!=", apply.getId());
cnd.and("sa.occupyUserId", "=", SecurityUtil.getUserId());
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
sql.setCondition(cnd);
List<SiteCugApply> applyList = listEntity(sql);
// 只要新预约时间和当前登录人占用的已通过预约有交叉就允许本次预约免审通过
return applyList.stream().anyMatch(item ->
DateUtil.parseDateTime(item.getReserveStartTime()).isBefore(DateUtil.parseDateTime(apply.getReserveEndTime()))
&& DateUtil.parseDateTime(item.getReserveEndTime()).isAfter(DateUtil.parseDateTime(apply.getReserveStartTime()))
);
}
// 按预约主体统计有效预约次数批量预约同一个批次号只计一次避免一次批量提交占用多次额度
private Map<Boolean, String> validateReserveLimit(SiteCugApply apply, SiteCugInfo siteInfo) {
if (StrUtil.equals(apply.getReserveType(), "union")) {
if (siteInfo.getUnionYearReserveLimit() == null || siteInfo.getUnionYearReserveLimit() <= 0) {
return Map.of(false, "请先配置分工会每年预约次数");
}
String[] range = buildYearRange(apply.getReserveStartTime());
long count = countEffectiveReserveTimes(apply, "sa.applyUnionId", apply.getApplyUnionId(), range[0], range[1]);
if (count >= siteInfo.getUnionYearReserveLimit()) {
return Map.of(false, "该分工会本年度预约次数已达" + siteInfo.getUnionYearReserveLimit() + "次,不能继续预约");
}
}
if (StrUtil.equals(apply.getReserveType(), "club")) {
if (siteInfo.getClubWeekReserveLimit() == null || siteInfo.getClubWeekReserveLimit() <= 0) {
return Map.of(false, "请先配置社团每周预约次数");
}
String[] range = buildWeekRange(apply.getReserveStartTime());
long count = countEffectiveReserveTimes(apply, "sa.clubId", apply.getClubId(), range[0], range[1]);
if (count >= siteInfo.getClubWeekReserveLimit()) {
return Map.of(false, "该协会本周预约次数已达" + siteInfo.getClubWeekReserveLimit() + "次,不能继续预约");
}
}
return Map.of(true, "");
}
// 分工会每年次数按预约开始时间所在自然年统计
private String[] buildYearRange(String reserveStartTime) {
DateTime reserveStart = DateUtil.parseDateTime(reserveStartTime);
String year = String.valueOf(reserveStart.year());
return new String[]{year + "-01-01 00:00:00", year + "-12-31 23:59:59"};
}
// 协会每周次数按自然周统计周一为开始周日为结束
private String[] buildWeekRange(String reserveStartTime) {
DateTime reserveStart = DateUtil.parseDateTime(reserveStartTime);
int week = reserveStart.dayOfWeek() - 1;
int offsetToMonday = week == 0 ? -6 : 1 - week;
DateTime weekStart = DateUtil.offsetDay(reserveStart, offsetToMonday);
DateTime weekEnd = DateUtil.offsetDay(weekStart, 6);
return new String[]{DateUtil.formatDate(weekStart) + " 00:00:00", DateUtil.formatDate(weekEnd) + " 23:59:59"};
}
// 统计预约主体在指定时间范围内的有效预约次数所有场地合计不按场地过滤
private long countEffectiveReserveTimes(SiteCugApply apply, String targetField, String targetId, String rangeStart, String rangeEnd) {
if (StrUtil.isBlank(targetId)) {
return 0;
}
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.andEX("sa.reserveType", "=", apply.getReserveType());
cnd.andEX(targetField, "=", targetId);
cnd.andEX("sa.reserveStartTime", ">=", rangeStart);
cnd.andEX("sa.reserveStartTime", "<=", rangeEnd);
if (StrUtil.isNotBlank(apply.getId())) {
cnd.andEX("sa.id", "!=", apply.getId());
}
cnd.and("ins.state", "in", buildEffectiveProcessStateList());
sql.setCondition(cnd);
List<SiteCugApply> applyList = listEntity(sql);
return applyList.stream()
.map(this::buildReserveCountKey)
.distinct()
.count();
}
// 只有待审核办理中已通过的流程占用预约次数驳回记录不占额度
private List<Integer> buildEffectiveProcessStateList() {
return List.of(ProcessInstanceStateEnum.DOING.getCode(), ProcessInstanceStateEnum.FINISHED.getCode(), ProcessInstanceStateEnum.PENDING.getCode());
}
// 批量预约同一批次号只算一次普通预约按单条申请计算
private String buildReserveCountKey(SiteCugApply apply) {
if (apply != null && StrUtil.isNotBlank(apply.getBackOption()) && apply.getBackOption().startsWith("YEARLY_BATCH:")) {
return apply.getBackOption();
}
return apply == null ? "" : apply.getId();
}
@Override
public Sql buildRecordSql() {
// 场馆预约查询页和导出页共用同一套返回字段统一在 service 中维护避免 controller 重复拼 SQL
@@ -135,6 +353,7 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
info.*,
si.name AS siteName,
ins.id AS instanceId,
ins.state AS instanceState,
ins.processDefineId instanceProcessDefineId,
CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN 1 ELSE 0 END AS yearlyBatch,
CASE WHEN info.reserveType = 'club' THEN '协会预约' ELSE '分工会预约' END AS reserveTypeName,
@@ -315,7 +534,3 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
return Map.of(false, "预约类型不合法");
}
}
@@ -138,6 +138,33 @@
</el-col>
</el-row>
<el-row :gutter="20" type="flex">
<el-col :span="8">
<el-form-item prop="unionYearReserveLimit" label="分工会每年预约次数">
<el-input-number
v-model="formData.unionYearReserveLimit"
:min="1"
:step="1"
:precision="0"
controls-position="right"
style="width: 160px">
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="clubWeekReserveLimit" label="社团每周预约次数">
<el-input-number
v-model="formData.clubWeekReserveLimit"
:min="1"
:step="1"
:precision="0"
controls-position="right"
style="width: 160px">
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">场次信息</el-divider>
<div v-if="formData.reserveTimeType === 1">
@@ -339,6 +366,8 @@
state: true,
sexLimit: 0,
termDateRange: [],
unionYearReserveLimit: 2,
clubWeekReserveLimit: 2,
filterHolidays: false,
reserveTimeType: 1,
openHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
@@ -358,6 +387,8 @@
maxNum: [{required: true, message: '必填', trigger: ['blur', 'change']}],
typeId: [{required: true, message: '必填', trigger: ['blur', 'change']}],
sexLimit: [{required: true, message: '必填', trigger: ['blur', 'change']}],
unionYearReserveLimit: [{required: true, type: 'number', min: 1, message: '必填且必须大于0', trigger: ['blur', 'change']}],
clubWeekReserveLimit: [{required: true, type: 'number', min: 1, message: '必填且必须大于0', trigger: ['blur', 'change']}],
filterHolidays: [{required: true, message: '必填', trigger: ['blur', 'change']}],
state: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveTimeType: [{required: true, message: '必填', trigger: ['blur', 'change']}],
@@ -687,6 +718,8 @@
state: true,
sexLimit: 0,
termDateRange: [],
unionYearReserveLimit: 2,
clubWeekReserveLimit: 2,
filterHolidays: false,
reserveTimeType: 1,
openHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
@@ -9,9 +9,7 @@ layout("/layouts/platform.html"){
}
.record-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
display: none;
}
.record-batch-child-row {
@@ -31,6 +29,13 @@ layout("/layouts/platform.html"){
height: 1px;
background: #c8d3df;
}
.record-site-name-cell {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 100%;
}
</style>
<div id="app" v-cloak>
@@ -113,25 +118,16 @@ layout("/layouts/platform.html"){
<el-card shadow="never">
<table-tool label="申请列表">
<el-button v-if="$auth.hasRole('SYSADMIN')" type="primary" size="small" @click="openBatchOccupyDialog">占用</el-button>
<el-button type="primary" size="small" @click="doExport">导出表格</el-button>
</table-tool>
<el-table
:data="tableData"
:row-class-name="tableRowClassName"
@selection-change="handleSelectionChange"
@sort-change="pageOrder"
style="width: 100%">
<el-table-column label="" width="54" align="center" header-align="center">
<template v-slot="{ row }">
<el-button
v-if="row._hasFoldChildren"
class="record-batch-toggle"
type="text"
@click="toggleBatchGroup(row)">
<i :class="row._expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'"></i>
</el-button>
<span v-else class="record-batch-empty"></span>
</template>
</el-table-column>
<el-table-column v-if="$auth.hasRole('SYSADMIN')" type="selection" width="48" :selectable="canSelectOccupy"></el-table-column>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column
v-for="column in tableColumns"
@@ -144,8 +140,18 @@ layout("/layouts/platform.html"){
header-align="center"
show-overflow-tooltip>
<template v-slot="{ row }" v-if="column.prop === 'siteName'">
<span v-if="row._isBatchChild" class="record-batch-child-label">{{ row.siteName }}</span>
<span v-else>{{ row.siteName }}</span>
<span class="record-site-name-cell">
<el-button
v-if="row._hasFoldChildren"
class="record-batch-toggle"
type="text"
@click="toggleBatchGroup(row)">
<i :class="row._expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'"></i>
</el-button>
<span v-else class="record-batch-empty"></span>
<span v-if="row._isBatchChild" class="record-batch-child-label">{{ row.siteName }}</span>
<span v-else>{{ row.siteName }}</span>
</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
@@ -154,7 +160,7 @@ layout("/layouts/platform.html"){
<span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<el-table-column label="操作" fixed="right" width="150">
<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>
@@ -163,6 +169,32 @@ layout("/layouts/platform.html"){
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="占用预约" :visible.sync="occupyDialogVisible" width="520px" append-to-body :close-on-click-modal="false">
<el-form label-width="90px">
<el-form-item label="预约信息">
<div v-if="occupyForm.batch">已选择 {{ occupyForm.count }} 条预约</div>
<template v-else>
<div>{{ occupyForm.siteName || '-' }}</div>
<div>{{ occupyForm.applyUserName || '-' }}{{ occupyForm.reserveStartTime }} 至 {{ occupyForm.reserveEndTime }}</div>
</template>
</el-form-item>
<el-form-item label="通知内容" required>
<el-input
type="textarea"
:rows="6"
maxlength="1000"
show-word-limit
v-model="occupyForm.message"
placeholder="请输入发送给预约人的通知内容">
</el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="closeOccupyDialog">取消</el-button>
<el-button type="primary" :loading="occupySubmitting" @click="submitOccupy">确定占用</el-button>
</span>
</el-dialog>
</template>
<template #edit>
@@ -195,6 +227,20 @@ layout("/layouts/platform.html"){
siteOptions: [],
rawTableData: [],
expandedBatchKeys: {},
selectedOccupyRows: [],
occupyDialogVisible: false,
occupySubmitting: false,
occupyForm: {
id: '',
ids: '',
batch: false,
count: 0,
siteName: '',
applyUserName: '',
reserveStartTime: '',
reserveEndTime: '',
message: '',
},
tableColumns: [
{ prop: 'siteName', label: '场地名称', sortable: 'custom' },
{ prop: 'applyUserName', label: '预约人', sortable: 'custom' },
@@ -267,6 +313,99 @@ layout("/layouts/platform.html"){
this.$refs.infoRef.onOpen(row)
})
},
canSelectOccupy(row) {
return this.canOccupy(row)
},
handleSelectionChange(rows) {
this.$set(this, 'selectedOccupyRows', rows || [])
},
canOccupy(row) {
if (!row || !row.reserveStartTime) {
return false
}
const currentUserId = (this.$store.state.user && this.$store.state.user.id) || ''
if (row.occupyUserId && row.occupyUserId !== currentUserId) {
return false
}
return Number(row.instanceState) === 20 && this.$moment(row.reserveStartTime).isAfter(this.$moment())
},
buildOccupyMessage(row) {
return '您预约的' + (row.siteName || '场地') + '' + row.reserveStartTime + ' 至 ' + row.reserveEndTime + ')因场地安排需要被占用,请您知悉并重新协调预约时间。'
},
openOccupyDialog(row) {
const occupyForm = {
id: row.id || '',
ids: row.id || '',
batch: false,
count: 1,
siteName: row.siteName || '',
applyUserName: row.applyUserName || '',
reserveStartTime: row.reserveStartTime || '',
reserveEndTime: row.reserveEndTime || '',
message: row.occupyMessage || this.buildOccupyMessage(row),
}
Object.keys(occupyForm).forEach(key => {
this.$set(this.occupyForm, key, occupyForm[key])
})
this.$set(this, 'occupyDialogVisible', true)
},
openBatchOccupyDialog() {
if (!this.selectedOccupyRows || this.selectedOccupyRows.length === 0) {
this.$message.warning('请选择需要占用的预约')
return
}
const ids = this.selectedOccupyRows.map(row => row.id).filter(id => !!id)
if (ids.length === 0) {
this.$message.warning('请选择需要占用的预约')
return
}
const occupyForm = {
id: '',
ids: ids.join(','),
batch: true,
count: ids.length,
siteName: '',
applyUserName: '',
reserveStartTime: '',
reserveEndTime: '',
message: '您预约的场地因场地安排需要被占用,请您知悉并重新协调预约时间。',
}
Object.keys(occupyForm).forEach(key => {
this.$set(this.occupyForm, key, occupyForm[key])
})
this.$set(this, 'occupyDialogVisible', true)
},
closeOccupyDialog() {
this.$set(this, 'occupyDialogVisible', false)
},
async submitOccupy() {
if (!this.occupyForm.message || !this.occupyForm.message.trim()) {
this.$message.warning('请填写通知内容')
return
}
this.$set(this, 'occupySubmitting', true)
try {
const url = this.occupyForm.batch ? '/platform/siteCug/record/occupyBatch' : '/platform/siteCug/record/occupy'
const params = {
message: this.occupyForm.message,
}
if (this.occupyForm.batch) {
this.$set(params, 'ids', this.occupyForm.ids)
} else {
this.$set(params, 'id', this.occupyForm.id)
}
const res = await this.$axios.post(url, params)
if (res.code === 0) {
this.$message.success(res.msg || '占用成功')
this.closeOccupyDialog()
window.location.href = '/platform/siteCug/apply'
} else {
this.$message.warning(res.msg || '占用失败')
}
} finally {
this.$set(this, 'occupySubmitting', false)
}
},
onDelete(id) {
this.$confirm('您确定要删除吗?', '提示', {
confirmButtonText: '确定',
@@ -299,6 +438,7 @@ layout("/layouts/platform.html"){
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.$set(this, 'selectedOccupyRows', [])
this.pageForm.totalCount = res.data.totalCount
}
})