This commit is contained in:
zhouhefeng
2026-04-24 16:39:05 +08:00
parent c2f93eb4b3
commit 6e246854fb
21 changed files with 3931 additions and 1040 deletions
@@ -0,0 +1,44 @@
package com.budwk.app.flow.handler;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.flow.engine.AssignmentHandler;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.engine.model.TaskModel;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import java.util.List;
public class FlowSchoolUnionActivityAdminHandler implements AssignmentHandler {
@Override
public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
Sql sql = Sqls.create("""
SELECT
userRole.userId
FROM
`sys_user_role` userRole
LEFT JOIN sys_role role ON role.id = userRole.roleId
WHERE
role.`code` = @roleCode
GROUP BY
userRole.userId
""").setParam("roleCode", RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
sql.setCallback(Sqls.callback.strList());
dao.execute(sql);
return sql.getList(String.class);
}
@Override
public String getMessage() {
return "\u83b7\u53d6\u6821\u5de5\u4f1a\u6d3b\u52a8\u7ba1\u7406\u5458";
}
@Override
public int getOrder() {
return AssignmentHandler.super.getOrder();
}
}
@@ -15,6 +15,7 @@ import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine; import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.sys.models.SysHoliday; import com.budwk.app.sys.models.SysHoliday;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
@@ -29,6 +30,8 @@ import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop; import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
@@ -40,16 +43,19 @@ import org.nutz.mvc.annotation.Param;
import java.time.Year; import java.time.Year;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@IocBean @IocBean
@Ok("json:full") @Ok("json:full")
@Api(tags = "场地预约-场馆") @Api(tags = "\u573a\u5730\u9884\u7ea6-\u573a\u9986")
@At("/platform/siteCug/apply") @At("/platform/siteCug/apply")
public class SiteCugApplyController { public class SiteCugApplyController {
@@ -67,20 +73,23 @@ public class SiteCugApplyController {
@At("/") @At("/")
@SaCheckPermission("siteCug.apply") @SaCheckPermission("siteCug.apply")
@Ok("beetl:/platform/zhgh/dayofficework/siteCug/apply/index.html") @Ok("beetl:/platform/zhgh/dayofficework/siteCug/apply/index.html")
public void index() {} public void index() {
}
@At("/h5") @At("/h5")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
@Ok("beetl:/platform/zhghh5/dayofficework/siteCug/apply/index.html") @Ok("beetl:/platform/zhghh5/dayofficework/siteCug/apply/index.html")
public void h5Index() {} public void h5Index() {
}
@At("/form/h5") @At("/form/h5")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
@Ok("beetl:/platform/zhghh5/dayofficework/siteCug/apply/form/index.html") @Ok("beetl:/platform/zhghh5/dayofficework/siteCug/apply/form/index.html")
public void h5Form() {} public void h5Form() {
}
@At @At
@ApiOperation("分页查询场地") @ApiOperation("\u5206\u9875\u67e5\u8be2\u573a\u5730")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm, @Param("type") String type) { public Result pageData(PageForm pageForm, @Param("type") String type) {
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
@@ -102,17 +111,530 @@ public class SiteCugApplyController {
List<SiteCugFunctionType> typeList = dao.query(SiteCugFunctionType.class, Cnd.NEW()); List<SiteCugFunctionType> typeList = dao.query(SiteCugFunctionType.class, Cnd.NEW());
Map<String, String> typeMap = typeList.stream().collect(Collectors.toMap(SiteCugFunctionType::getId, SiteCugFunctionType::getName)); Map<String, String> typeMap = typeList.stream().collect(Collectors.toMap(SiteCugFunctionType::getId, SiteCugFunctionType::getName));
List<String> siteIds = listMap.stream().map(item -> item.getString("id")).filter(StrUtil::isNotBlank).toList();
String today = DateUtil.today();
String dayStart = today + " 00:00:00";
String dayEnd = today + " 23:59:59";
Map<String, List<SiteCugApply>> applyMap = queryTodayApplyMap(siteIds, dayStart, dayEnd);
Set<String> holidaySet = dao.query(SysHoliday.class, Cnd.NEW()).stream()
.map(SysHoliday::getDay)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
for (NutMap map : listMap) { for (NutMap map : listMap) {
map.put("typeName", typeMap.get(map.getString("typeId"))); map.put("typeName", typeMap.get(map.getString("typeId")));
map.put("reserveTimeTypeName", getReserveTimeTypeName(map.getInt("reserveTimeType")));
map.put("timelineSegments", buildTimelineSegments(map, applyMap.getOrDefault(map.getString("id"), new ArrayList<>()), today, holidaySet));
} }
return Result.success(pagination); return Result.success(pagination);
} }
@At @At
@ApiOperation("提交预约") @ApiOperation("\u67e5\u8be2\u573a\u5730\u53ef\u9884\u7ea6\u65f6\u6bb5")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
@SLog(tag = "场馆功能管理-场地预约", msg = "提交场地预约") public Result availability(@Param("siteId") String siteId, @Param("date") String date) {
if (StrUtil.isBlank(siteId)) {
return Result.error("\u573a\u5730\u4e0d\u80fd\u4e3a\u7a7a");
}
String targetDate = StrUtil.isBlank(date) ? DateUtil.today() : date;
if (!targetDate.matches("^\\d{4}-\\d{2}-\\d{2}$")) {
return Result.error("\u9884\u7ea6\u65e5\u671f\u683c\u5f0f\u4e0d\u6b63\u786e");
}
SiteCugInfo siteInfo = infoService.fetch(siteId);
if (siteInfo == null) {
return Result.error("\u573a\u5730\u4e0d\u5b58\u5728");
}
Set<String> holidaySet = queryHolidaySet();
List<SiteCugApply> applyList = queryDayApplyList(siteId, targetDate + " 00:00:00", targetDate + " 23:59:59");
return Result.success(NutMap.NEW()
.addv("date", targetDate)
.addv("reserveTimeType", siteInfo.getReserveTimeType())
.addv("reserveTimeTypeName", getReserveTimeTypeName(siteInfo.getReserveTimeType()))
.addv("blocks", buildAvailabilityBlocks(siteInfo, targetDate, applyList, holidaySet)));
}
private Map<String, List<SiteCugApply>> queryTodayApplyMap(List<String> siteIds, String dayStart, String dayEnd) {
if (siteIds == null || siteIds.isEmpty()) {
return Map.of();
}
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", "in", siteIds);
cnd.and("ins.state", "in", List.of(
ProcessInstanceStateEnum.DOING.getCode(),
ProcessInstanceStateEnum.FINISHED.getCode(),
ProcessInstanceStateEnum.PENDING.getCode()
));
cnd.and("sa.reserveEndTime", ">=", dayStart);
cnd.and("sa.reserveStartTime", "<=", dayEnd);
sql.setCondition(cnd);
sql.setEntity(dao.getEntity(SiteCugApply.class));
sql.setCallback(Sqls.callback.entities());
dao.execute(sql);
List<SiteCugApply> applyList = sql.getList(SiteCugApply.class);
if (applyList == null || applyList.isEmpty()) {
return Map.of();
}
return applyList.stream()
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(SiteCugApply::getSiteId));
}
private List<SiteCugApply> queryDayApplyList(String siteId, String dayStart, String dayEnd) {
if (StrUtil.isBlank(siteId)) {
return new ArrayList<>();
}
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", "=", siteId);
cnd.and("ins.state", "in", List.of(
ProcessInstanceStateEnum.DOING.getCode(),
ProcessInstanceStateEnum.FINISHED.getCode(),
ProcessInstanceStateEnum.PENDING.getCode()
));
cnd.and("sa.reserveEndTime", ">=", dayStart);
cnd.and("sa.reserveStartTime", "<=", dayEnd);
sql.setCondition(cnd);
sql.setEntity(dao.getEntity(SiteCugApply.class));
sql.setCallback(Sqls.callback.entities());
dao.execute(sql);
List<SiteCugApply> applyList = sql.getList(SiteCugApply.class);
return applyList == null ? new ArrayList<>() : applyList;
}
private Set<String> queryHolidaySet() {
return dao.query(SysHoliday.class, Cnd.NEW()).stream()
.map(SysHoliday::getDay)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
}
private String getReserveTimeTypeName(Integer reserveTimeType) {
if (reserveTimeType != null && reserveTimeType == 2) {
return "\u5168\u5929\u5019\u9884\u7ea6";
}
return "\u5206\u6bb5\u9884\u7ea6";
}
private List<NutMap> buildTimelineSegments(NutMap site, List<SiteCugApply> applyList, String today, Set<String> holidaySet) {
List<NutMap> segments = new ArrayList<>();
boolean isWeekend = isWeekend(today);
boolean isHoliday = holidaySet.contains(today);
List<int[]> openRanges = buildOpenRanges(site, isWeekend, isHoliday);
List<int[]> disabledRanges = buildDisabledRanges(site, today);
List<int[]> reservedRanges = buildReservedRanges(applyList, today);
int totalMinutes = 24 * 60;
int segmentMinutes = 30;
for (int start = 0; start < totalMinutes; start += segmentMinutes) {
int end = start + segmentMinutes;
String status = "closed";
if (intersectsAny(start, end, openRanges)) {
status = "available";
}
if (intersectsAny(start, end, disabledRanges)) {
status = "closed";
}
if (!"closed".equals(status) && intersectsAny(start, end, reservedRanges)) {
status = "reserved";
}
segments.add(NutMap.NEW()
.addv("status", status)
.addv("startMinute", start)
.addv("endMinute", end));
}
return segments;
}
private List<int[]> buildOpenRanges(NutMap site, boolean isWeekend, boolean isHoliday) {
if (isWeekend) {
return new ArrayList<>();
}
if (Boolean.TRUE.equals(site.getBoolean("filterHolidays")) && isHoliday) {
return new ArrayList<>();
}
Integer reserveTimeType = site.getInt("reserveTimeType");
if (reserveTimeType != null && reserveTimeType == 2) {
NutMap fullDayOpenHour = toNutMap(site.get("fullDayOpenHour"));
if (fullDayOpenHour.isEmpty()) {
List<NutMap> openHours = toNutMapList(site.get("openHours"));
fullDayOpenHour = openHours.isEmpty() ? NutMap.NEW() : openHours.get(0);
}
return parseTimeRanges(List.of(fullDayOpenHour));
}
List<NutMap> segmentedOpenHours = toNutMapList(site.get("segmentedOpenHours"));
if (segmentedOpenHours.isEmpty()) {
segmentedOpenHours = toNutMapList(site.get("openHours"));
}
return parseTimeRanges(segmentedOpenHours);
}
private List<int[]> buildDisabledRanges(NutMap site, String today) {
List<NutMap> disabledList = toNutMapList(site.get("notApplyTimeList"));
List<int[]> result = new ArrayList<>();
for (NutMap item : disabledList) {
if (item == null || !StrUtil.equals(today, item.getString("date"))) {
continue;
}
Integer start = parseTimeToMinutes(item.getString("startTime"));
Integer end = parseTimeToMinutes(item.getString("endTime"));
if (start != null && end != null && end > start) {
result.add(new int[]{start, end});
}
}
return result;
}
private List<int[]> buildReservedRanges(List<SiteCugApply> applyList, String today) {
List<int[]> result = new ArrayList<>();
String dayStart = today + " 00:00:00";
String dayEnd = today + " 23:59:59";
DateTime startOfDay = DateUtil.parseDateTime(dayStart);
DateTime endOfDay = DateUtil.parseDateTime(dayEnd);
for (SiteCugApply item : applyList) {
if (item == null || StrUtil.hasBlank(item.getReserveStartTime(), item.getReserveEndTime())) {
continue;
}
DateTime reserveStart = DateUtil.parseDateTime(item.getReserveStartTime());
DateTime reserveEnd = DateUtil.parseDateTime(item.getReserveEndTime());
if (!reserveEnd.isAfter(reserveStart)) {
continue;
}
DateTime actualStart = reserveStart.isBefore(startOfDay) ? startOfDay : reserveStart;
DateTime actualEnd = reserveEnd.isAfter(endOfDay) ? endOfDay : reserveEnd;
int startMinute = actualStart.hour(true) * 60 + actualStart.minute();
int endMinute = actualEnd.hour(true) * 60 + actualEnd.minute();
if (actualEnd.second() > 0) {
endMinute = Math.min(24 * 60, endMinute + 1);
}
if (endMinute > startMinute) {
result.add(new int[]{startMinute, endMinute});
}
}
return result;
}
private List<int[]> parseTimeRanges(List<NutMap> sourceList) {
List<int[]> result = new ArrayList<>();
for (NutMap item : sourceList) {
if (item == null) {
continue;
}
Integer start = parseTimeToMinutes(item.getString("startTime"));
Integer end = parseTimeToMinutes(item.getString("endTime"));
if (start != null && end != null && end > start) {
result.add(new int[]{start, end});
}
}
return result;
}
private NutMap toNutMap(Object value) {
if (value instanceof NutMap nutMap) {
return nutMap;
}
if (value instanceof Map<?, ?> map) {
NutMap nutMap = NutMap.NEW();
map.forEach((key, val) -> nutMap.put(String.valueOf(key), val));
return nutMap;
}
return NutMap.NEW();
}
private List<NutMap> toNutMapList(Object value) {
List<NutMap> result = new ArrayList<>();
if (!(value instanceof List<?> list)) {
return result;
}
for (Object item : list) {
result.add(toNutMap(item));
}
return result;
}
private Integer parseTimeToMinutes(String timeText) {
if (StrUtil.isBlank(timeText)) {
return null;
}
List<String> parts = Arrays.stream(timeText.split(":"))
.filter(StrUtil::isNotBlank)
.toList();
if (parts.size() < 2) {
return null;
}
try {
int hour = Integer.parseInt(parts.get(0));
int minute = Integer.parseInt(parts.get(1));
if (hour < 0 || hour > 24 || minute < 0 || minute > 59) {
return null;
}
if (hour == 24 && minute > 0) {
return null;
}
return hour * 60 + minute;
} catch (NumberFormatException e) {
return null;
}
}
private boolean intersectsAny(int start, int end, List<int[]> ranges) {
for (int[] range : ranges) {
if (range != null && start < range[1] && end > range[0]) {
return true;
}
}
return false;
}
private boolean isWeekend(String dateText) {
DateTime date = DateUtil.parseDate(dateText);
int week = date.dayOfWeek() - 1;
return week == 0 || week == 6;
}
private List<NutMap> buildAvailabilityBlocks(SiteCugInfo siteInfo, String day, List<SiteCugApply> applyList, Set<String> holidaySet) {
if (siteInfo.getReserveTimeType() != null && siteInfo.getReserveTimeType() == 2) {
return buildFullDayAvailabilityBlocks(siteInfo, day, applyList, holidaySet);
}
return buildSegmentedAvailabilityBlocks(siteInfo, day, applyList, holidaySet);
}
private List<NutMap> buildSegmentedAvailabilityBlocks(SiteCugInfo siteInfo, String day, List<SiteCugApply> applyList, Set<String> holidaySet) {
List<NutMap> sourceList = siteInfo.getSegmentedOpenHours();
if (sourceList == null || sourceList.isEmpty()) {
sourceList = siteInfo.getOpenHours();
}
List<NutMap> result = new ArrayList<>();
List<int[]> reservedRanges = buildReservedRanges(applyList, day);
List<int[]> disabledRanges = buildDisabledRanges(toSiteNutMap(siteInfo), day);
boolean siteClosed = isSiteClosed(siteInfo, day, holidaySet);
for (int i = 0; i < sourceList.size(); i++) {
NutMap item = toNutMap(sourceList.get(i));
Integer slotStart = parseTimeToMinutes(item.getString("startTime"));
Integer slotEnd = parseTimeToMinutes(item.getString("endTime"));
int unitMinutes = parseTimeUnitMinutes(item.get("timeUnit"));
if (slotStart == null || slotEnd == null || slotEnd <= slotStart || unitMinutes <= 0) {
continue;
}
String groupLabel = "\u573a\u6b21" + (i + 1) + " " + formatMinutes(slotStart) + "-" + formatMinutes(slotEnd) + " / " + formatUnitText(unitMinutes);
for (int cursor = slotStart; cursor + unitMinutes <= slotEnd; cursor += unitMinutes) {
int blockEnd = cursor + unitMinutes;
result.add(buildAvailabilityBlock(day, cursor, blockEnd, reservedRanges, disabledRanges, siteClosed, i, groupLabel));
}
}
return result;
}
private List<NutMap> buildFullDayAvailabilityBlocks(SiteCugInfo siteInfo, String day, List<SiteCugApply> applyList, Set<String> holidaySet) {
NutMap source = getFullDayOpenHourConfig(siteInfo);
Integer slotStart = parseTimeToMinutes(source.getString("startTime"));
Integer slotEnd = parseTimeToMinutes(source.getString("endTime"));
int unitMinutes = parseTimeUnitMinutes(source.get("timeUnit"));
if (slotStart == null || slotEnd == null || slotEnd <= slotStart || unitMinutes <= 0) {
return new ArrayList<>();
}
List<NutMap> result = new ArrayList<>();
List<int[]> reservedRanges = buildReservedRanges(applyList, day);
List<int[]> disabledRanges = buildDisabledRanges(toSiteNutMap(siteInfo), day);
boolean siteClosed = isSiteClosed(siteInfo, day, holidaySet);
String groupLabel = "\u5168\u5929\u5019\u65f6\u6bb5 / " + formatUnitText(unitMinutes);
for (int cursor = slotStart; cursor + unitMinutes <= slotEnd; cursor += unitMinutes) {
int blockEnd = cursor + unitMinutes;
result.add(buildAvailabilityBlock(day, cursor, blockEnd, reservedRanges, disabledRanges, siteClosed, 0, groupLabel));
}
return result;
}
private NutMap buildAvailabilityBlock(String day, int startMinute, int endMinute, List<int[]> reservedRanges, List<int[]> disabledRanges, boolean siteClosed, int groupIndex, String groupLabel) {
String status = "available";
if (siteClosed || intersectsAny(startMinute, endMinute, disabledRanges)) {
status = "closed";
} else if (intersectsAny(startMinute, endMinute, reservedRanges)) {
status = "reserved";
}
return NutMap.NEW()
.addv("key", day + "_" + startMinute + "_" + endMinute)
.addv("groupIndex", groupIndex)
.addv("groupLabel", groupLabel)
.addv("status", status)
.addv("startMinute", startMinute)
.addv("endMinute", endMinute)
.addv("startTime", formatMinutes(startMinute))
.addv("endTime", formatMinutes(endMinute))
.addv("label", formatMinutes(startMinute) + "-" + formatMinutes(endMinute))
.addv("startDateTime", day + " " + formatMinutes(startMinute) + ":00")
.addv("endDateTime", day + " " + formatMinutes(endMinute) + ":00");
}
private boolean isSiteClosed(SiteCugInfo siteInfo, String day, Set<String> holidaySet) {
if (isWeekend(day)) {
return true;
}
return Boolean.TRUE.equals(siteInfo.getFilterHolidays()) && holidaySet.contains(day);
}
private NutMap toSiteNutMap(SiteCugInfo siteInfo) {
return NutMap.NEW()
.addv("notApplyTimeList", siteInfo.getNotApplyTimeList())
.addv("filterHolidays", siteInfo.getFilterHolidays())
.addv("reserveTimeType", siteInfo.getReserveTimeType())
.addv("openHours", siteInfo.getOpenHours())
.addv("segmentedOpenHours", siteInfo.getSegmentedOpenHours())
.addv("fullDayOpenHour", siteInfo.getFullDayOpenHour());
}
private NutMap getFullDayOpenHourConfig(SiteCugInfo siteInfo) {
NutMap source = toNutMap(siteInfo.getFullDayOpenHour());
if (!source.isEmpty()) {
return source;
}
List<NutMap> openHours = siteInfo.getOpenHours();
return openHours == null || openHours.isEmpty() ? NutMap.NEW() : toNutMap(openHours.get(0));
}
private int parseTimeUnitMinutes(Object value) {
if (value == null) {
return 60;
}
try {
double unit = Double.parseDouble(String.valueOf(value));
if (unit <= 0) {
return 60;
}
return (int) Math.round(unit);
} catch (NumberFormatException e) {
return 60;
}
}
private String formatMinutes(int totalMinutes) {
int hour = totalMinutes / 60;
int minute = totalMinutes % 60;
return String.format("%02d:%02d", hour, minute);
}
private String formatUnitText(int unitMinutes) {
return unitMinutes + "\u5206\u949f";
}
private Result validateApplySelectionBySchedule(SiteCugApply apply) {
if (apply == null || StrUtil.hasBlank(apply.getSiteId(), apply.getReserveStartTime(), apply.getReserveEndTime())) {
return Result.success();
}
SiteCugInfo siteInfo = infoService.fetch(apply.getSiteId());
if (siteInfo == null) {
return Result.error("\u573a\u5730\u4e0d\u5b58\u5728");
}
DateTime start = DateUtil.parseDateTime(apply.getReserveStartTime());
DateTime end = DateUtil.parseDateTime(apply.getReserveEndTime());
String startDay = DateUtil.formatDate(start);
String endDay = DateUtil.formatDate(end);
if (!StrUtil.equals(startDay, endDay)) {
return Result.error("\u9884\u7ea6\u65f6\u95f4\u5fc5\u987b\u5728\u540c\u4e00\u5929\u5185");
}
Set<String> holidaySet = queryHolidaySet();
List<SiteCugApply> applyList = queryDayApplyList(apply.getSiteId(), startDay + " 00:00:00", startDay + " 23:59:59").stream()
.filter(item -> item != null && !StrUtil.equals(item.getId(), apply.getId()))
.collect(Collectors.toList());
if (siteInfo.getReserveTimeType() != null && siteInfo.getReserveTimeType() == 2) {
if (!isWithinOpenRanges(siteInfo, startDay, start, end, holidaySet)) {
return Result.error("\u5168\u5929\u5019\u9884\u7ea6\u8bf7\u5728\u5f53\u5929\u5f00\u653e\u65f6\u95f4\u5185\u6309\u9884\u7ea6\u65f6\u95f4\u5355\u4f4d\u9009\u62e9\u4e00\u4e2a\u6216\u591a\u4e2a\u8fde\u7eed\u65f6\u6bb5");
}
if (!isAlignedWithFullDayTimeUnit(siteInfo, start, end)) {
return Result.error("\u5168\u5929\u5019\u9884\u7ea6\u7684\u5f00\u59cb\u548c\u7ed3\u675f\u65f6\u95f4\u5fc5\u987b\u7b26\u5408\u9884\u7ea6\u65f6\u95f4\u5355\u4f4d");
}
return Result.success();
}
List<NutMap> blocks = buildSegmentedAvailabilityBlocks(siteInfo, startDay, applyList, holidaySet);
String startText = apply.getReserveStartTime();
String endText = apply.getReserveEndTime();
for (int i = 0; i < blocks.size(); i++) {
NutMap block = blocks.get(i);
if (!"available".equals(block.getString("status"))) {
continue;
}
if (!StrUtil.equals(startText, block.getString("startDateTime"))) {
continue;
}
String currentEnd = block.getString("endDateTime");
if (StrUtil.equals(endText, currentEnd)) {
return Result.success();
}
for (int j = i + 1; j < blocks.size(); j++) {
NutMap nextBlock = blocks.get(j);
NutMap prevBlock = blocks.get(j - 1);
if (!"available".equals(nextBlock.getString("status"))) {
break;
}
if (!StrUtil.equals(prevBlock.getString("endDateTime"), nextBlock.getString("startDateTime"))) {
break;
}
currentEnd = nextBlock.getString("endDateTime");
if (StrUtil.equals(endText, currentEnd)) {
return Result.success();
}
}
}
return Result.error("\u5206\u6bb5\u9884\u7ea6\u8bf7\u6309\u573a\u6b21\u65f6\u95f4\u5355\u4f4d\u9009\u62e9\u4e00\u4e2a\u6216\u591a\u4e2a\u8fde\u7eed\u65f6\u6bb5");
}
private boolean isWithinOpenRanges(SiteCugInfo siteInfo, String day, DateTime start, DateTime end, Set<String> holidaySet) {
if (isSiteClosed(siteInfo, day, holidaySet)) {
return false;
}
List<int[]> openRanges = buildOpenRanges(toSiteNutMap(siteInfo), false, false);
List<int[]> disabledRanges = buildDisabledRanges(toSiteNutMap(siteInfo), day);
int startMinute = start.hour(true) * 60 + start.minute();
int endMinute = end.hour(true) * 60 + end.minute();
if (end.second() > 0) {
endMinute = Math.min(24 * 60, endMinute + 1);
}
final int finalEndMinute = endMinute;
boolean inOpenRange = openRanges.stream().anyMatch(range -> startMinute >= range[0] && finalEndMinute <= range[1]);
return inOpenRange && !intersectsAny(startMinute, finalEndMinute, disabledRanges);
}
private boolean isAlignedWithFullDayTimeUnit(SiteCugInfo siteInfo, DateTime start, DateTime end) {
if (start == null || end == null || start.second() > 0 || end.second() > 0) {
return false;
}
NutMap source = getFullDayOpenHourConfig(siteInfo);
Integer slotStart = parseTimeToMinutes(source.getString("startTime"));
Integer slotEnd = parseTimeToMinutes(source.getString("endTime"));
int unitMinutes = parseTimeUnitMinutes(source.get("timeUnit"));
if (slotStart == null || slotEnd == null || slotEnd <= slotStart || unitMinutes <= 0) {
return false;
}
int startMinute = start.hour(true) * 60 + start.minute();
int endMinute = end.hour(true) * 60 + end.minute();
return startMinute >= slotStart
&& endMinute <= slotEnd
&& endMinute > startMinute
&& (startMinute - slotStart) % unitMinutes == 0
&& (endMinute - slotStart) % unitMinutes == 0;
}
@At
@ApiOperation("\u63d0\u4ea4\u9884\u7ea6")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
@SLog(tag = "\u573a\u9986\u529f\u80fd\u7ba1\u7406-\u573a\u5730\u9884\u7ea6", msg = "\u63d0\u4ea4\u573a\u5730\u9884\u7ea6")
public Result submit(@Param("data") SiteCugApply apply) { public Result submit(@Param("data") SiteCugApply apply) {
Result scheduleValidate = validateApplySelectionBySchedule(apply);
if (scheduleValidate.getCode() != 0) {
return scheduleValidate;
}
Map<Boolean, String> validate = applyService.validateApply(apply); Map<Boolean, String> validate = applyService.validateApply(apply);
if (validate.containsKey(false)) { if (validate.containsKey(false)) {
return Result.error(validate.get(false)); return Result.error(validate.get(false));
@@ -122,19 +644,23 @@ public class SiteCugApplyController {
} }
@At("/submitYearly") @At("/submitYearly")
@ApiOperation("按本年后续同星期同时间段批量提交预约") @ApiOperation("\u6309\u672c\u5e74\u540e\u7eed\u540c\u661f\u671f\u540c\u65f6\u95f4\u6bb5\u6279\u91cf\u63d0\u4ea4\u9884\u7ea6")
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
@SLog(tag = "场馆功能管理-场地预约", msg = "批量提交本年场地预约") @SLog(tag = "\u573a\u9986\u529f\u80fd\u7ba1\u7406-\u573a\u5730\u9884\u7ea6", msg = "\u6279\u91cf\u63d0\u4ea4\u672c\u5e74\u573a\u5730\u9884\u7ea6")
public Result submitYearly(@Param("data") SiteCugApply apply) { public Result submitYearly(@Param("data") SiteCugApply apply) {
List<SiteCugApply> applyList = buildYearlyApplyList(apply); List<SiteCugApply> applyList = buildYearlyApplyList(apply);
if (applyList.isEmpty()) { if (applyList.isEmpty()) {
return Result.error("未生成可预约的日期"); return Result.error("\u672a\u751f\u6210\u53ef\u9884\u7ea6\u7684\u65e5\u671f");
} }
for (SiteCugApply item : applyList) { for (SiteCugApply item : applyList) {
Result scheduleValidate = validateApplySelectionBySchedule(item);
if (scheduleValidate.getCode() != 0) {
return scheduleValidate;
}
Map<Boolean, String> validate = applyService.validateApply(item); Map<Boolean, String> validate = applyService.validateApply(item);
if (validate.containsKey(false)) { if (validate.containsKey(false)) {
return Result.error(String.format("%s 预约失败:%s", item.getReserveStartTime(), validate.get(false))); return Result.error(String.format("%s \u9884\u7ea6\u5931\u8d25\uff1a%s", item.getReserveStartTime(), validate.get(false)));
} }
} }
String yearlyBatchNo = YEARLY_BATCH_PREFIX + UUID.randomUUID(); String yearlyBatchNo = YEARLY_BATCH_PREFIX + UUID.randomUUID();
@@ -142,11 +668,11 @@ public class SiteCugApplyController {
item.setBackOption(yearlyBatchNo); item.setBackOption(yearlyBatchNo);
submitSingleApply(item); submitSingleApply(item);
} }
return Result.success(String.format("已成功预约本年剩余%d个时间段", applyList.size())); return Result.success(String.format("\u5df2\u6210\u529f\u9884\u7ea6\u622a\u81f3%s\u5171%d\u4e2a\u65f6\u95f4\u6bb5", apply.getYearlyReserveEndDate(), applyList.size()));
} }
@At @At
@ApiOperation("查询预约限制配置") @ApiOperation("\u67e5\u8be2\u9884\u7ea6\u9650\u5236\u914d\u7f6e")
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
public Result timeLimitConfig(@Param("siteId") String siteId) { public Result timeLimitConfig(@Param("siteId") String siteId) {
if (StrUtil.isBlank(siteId)) { if (StrUtil.isBlank(siteId)) {
@@ -174,7 +700,7 @@ public class SiteCugApplyController {
} }
private List<SiteCugApply> buildYearlyApplyList(SiteCugApply apply) { private List<SiteCugApply> buildYearlyApplyList(SiteCugApply apply) {
if (apply == null || StrUtil.hasBlank(apply.getReserveStartTime(), apply.getReserveEndTime())) { if (apply == null || StrUtil.hasBlank(apply.getReserveStartTime(), apply.getReserveEndTime(), apply.getYearlyReserveEndDate())) {
return new ArrayList<>(); return new ArrayList<>();
} }
DateTime start = DateUtil.parseDateTime(apply.getReserveStartTime()); DateTime start = DateUtil.parseDateTime(apply.getReserveStartTime());
@@ -182,6 +708,10 @@ public class SiteCugApplyController {
if (!end.isAfter(start)) { if (!end.isAfter(start)) {
return new ArrayList<>(); return new ArrayList<>();
} }
DateTime reserveEndDate = parseReserveEndDate(apply.getYearlyReserveEndDate());
if (reserveEndDate == null || reserveEndDate.isBefore(DateUtil.beginOfDay(start))) {
return new ArrayList<>();
}
SiteCugInfo siteInfo = infoService.fetch(apply.getSiteId()); SiteCugInfo siteInfo = infoService.fetch(apply.getSiteId());
if (siteInfo == null) { if (siteInfo == null) {
return new ArrayList<>(); return new ArrayList<>();
@@ -205,9 +735,9 @@ public class SiteCugApplyController {
int endMinute = end.minute(); int endMinute = end.minute();
int endSecond = end.second(); int endSecond = end.second();
Date current = DateUtil.beginOfDay(start); Date current = DateUtil.beginOfDay(start);
Date endOfYear = DateUtil.endOfYear(start); Date batchEndDate = DateUtil.endOfDay(reserveEndDate);
List<SiteCugApply> result = new ArrayList<>(); List<SiteCugApply> result = new ArrayList<>();
while (current.compareTo(endOfYear) <= 0) { while (current.compareTo(batchEndDate) <= 0) {
DateTime currentDate = DateUtil.date(current); DateTime currentDate = DateUtil.date(current);
String currentDay = DateUtil.formatDate(currentDate); String currentDay = DateUtil.formatDate(currentDate);
if (currentDate.dayOfWeek() - 1 == targetWeek && !holidayList.contains(currentDay)) { if (currentDate.dayOfWeek() - 1 == targetWeek && !holidayList.contains(currentDay)) {
@@ -222,6 +752,17 @@ public class SiteCugApplyController {
return result; return result;
} }
private DateTime parseReserveEndDate(String endDate) {
if (StrUtil.isBlank(endDate)) {
return null;
}
try {
return DateUtil.parseDate(endDate);
} catch (Exception e) {
return null;
}
}
private DateTime buildDateTime(DateTime date, int hour, int minute, int second) { 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)); return DateUtil.parseDateTime(String.format("%s %02d:%02d:%02d", DateUtil.formatDate(date), hour, minute, second));
} }
@@ -245,7 +786,8 @@ public class SiteCugApplyController {
.setReserveEndTime(apply.getReserveEndTime()) .setReserveEndTime(apply.getReserveEndTime())
.setJoinCount(apply.getJoinCount()) .setJoinCount(apply.getJoinCount())
.setApplyCause(apply.getApplyCause()) .setApplyCause(apply.getApplyCause())
.setBackOption(apply.getBackOption()); .setBackOption(apply.getBackOption())
.setYearlyReserveEndDate(apply.getYearlyReserveEndDate());
} }
private void submitSingleApply(SiteCugApply apply) { private void submitSingleApply(SiteCugApply apply) {
@@ -27,12 +27,13 @@ import org.nutz.mvc.annotation.Param;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@IocBean @IocBean
@Ok("json:full") @Ok("json:full")
@Api(tags = "场地管理-场馆") @Api(tags = "siteCug manage")
@At("/platform/siteCug/manage") @At("/platform/siteCug/manage")
public class SiteCugManageController { public class SiteCugManageController {
@@ -47,7 +48,7 @@ public class SiteCugManageController {
public void index() {} public void index() {}
@At @At
@ApiOperation("分页查询") @ApiOperation("page data")
@SaCheckPermission("siteCug.manage") @SaCheckPermission("siteCug.manage")
public Result pageData(PageForm pageForm, @Param("type") String type) { public Result pageData(PageForm pageForm, @Param("type") String type) {
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
@@ -76,9 +77,9 @@ public class SiteCugManageController {
} }
@At @At
@ApiOperation("新增/修改场地") @ApiOperation("submit")
@SaCheckPermission("siteCug.manage") @SaCheckPermission("siteCug.manage")
@SLog(tag = "场馆功能管理-场地管理", msg = "新增/修改场地") @SLog(tag = "siteCug.manage", msg = "submit site")
public Object submit(@Param("data") SiteCugInfo info) { public Object submit(@Param("data") SiteCugInfo info) {
List<NutMap> notApplyTimeList = info.getNotApplyTimeList(); List<NutMap> notApplyTimeList = info.getNotApplyTimeList();
if (notApplyTimeList != null) { if (notApplyTimeList != null) {
@@ -89,27 +90,81 @@ public class SiteCugManageController {
|| StrUtil.isBlank(item.getString("endTime")) || StrUtil.isBlank(item.getString("endTime"))
|| item.getString("startTime").compareTo(item.getString("endTime")) >= 0); || item.getString("startTime").compareTo(item.getString("endTime")) >= 0);
if (invalid) { if (invalid) {
return Result.error("禁用时间设置有误"); return Result.error("Invalid disabled time settings");
} }
} }
if (info.getReserveTimeType() == null) {
info.setReserveTimeType(1);
}
List<NutMap> segmentedOpenHours = info.getSegmentedOpenHours();
NutMap fullDayOpenHour = info.getFullDayOpenHour();
if ((segmentedOpenHours == null || segmentedOpenHours.isEmpty()) && info.getReserveTimeType() == 1) {
segmentedOpenHours = info.getOpenHours();
}
if ((fullDayOpenHour == null || fullDayOpenHour.isEmpty()) && info.getReserveTimeType() == 2 && info.getOpenHours() != null && !info.getOpenHours().isEmpty()) {
fullDayOpenHour = info.getOpenHours().get(0);
}
if (info.getReserveTimeType() == 1) {
if (segmentedOpenHours == null || segmentedOpenHours.isEmpty()) {
return Result.error("Please add at least one booking slot");
}
boolean invalid = segmentedOpenHours.stream().anyMatch(item ->
item == null
|| StrUtil.isBlank(item.getString("startTime"))
|| StrUtil.isBlank(item.getString("endTime"))
|| item.getString("startTime").compareTo(item.getString("endTime")) >= 0
|| !isPositiveNumber(item.get("timeUnit")));
if (invalid) {
return Result.error("Invalid segmented booking slot settings");
}
if (hasOverlap(segmentedOpenHours)) {
return Result.error("Segmented booking slots cannot overlap");
}
segmentedOpenHours = segmentedOpenHours.stream()
.filter(Objects::nonNull)
.map(this::normalizeOpenHourTimeUnit)
.collect(Collectors.toList());
info.setSegmentedOpenHours(segmentedOpenHours);
info.setOpenHours(segmentedOpenHours);
} else if (info.getReserveTimeType() == 2) {
if (fullDayOpenHour == null || fullDayOpenHour.isEmpty()) {
return Result.error("Please set the full-day booking time range");
}
NutMap item = fullDayOpenHour;
if (item == null
|| StrUtil.isBlank(item.getString("startTime"))
|| StrUtil.isBlank(item.getString("endTime"))
|| item.getString("startTime").compareTo(item.getString("endTime")) >= 0) {
return Result.error("Invalid full-day booking time range");
}
if (!isPositiveNumber(item.get("timeUnit"))) {
return Result.error("Invalid full-day booking time unit");
}
fullDayOpenHour = normalizeOpenHourTimeUnit(fullDayOpenHour);
info.setFullDayOpenHour(fullDayOpenHour);
info.setOpenHours(List.of(fullDayOpenHour));
}
if (info.getFilterHolidays() == null) { if (info.getFilterHolidays() == null) {
info.setFilterHolidays(false); info.setFilterHolidays(false);
} }
infoService.insertOrUpdate(info); infoService.insertOrUpdate(info);
return Result.success(); SiteCugInfo savedInfo = infoService.fetch(info.getId());
return Result.success(savedInfo == null ? info : savedInfo);
} }
@At @At
@ApiOperation("删除场地") @ApiOperation("delete")
@SaCheckPermission("siteCug.manage") @SaCheckPermission("siteCug.manage")
@SLog(tag = "场馆功能管理-场地管理", msg = "删除场地") @SLog(tag = "siteCug.manage", msg = "delete site")
public Object delete(String id) { public Object delete(String id) {
infoService.delete(id); infoService.delete(id);
return Result.success(); return Result.success();
} }
@At @At
@ApiOperation("查询场地") @ApiOperation("query sites")
@SaCheckLogin @SaCheckLogin
public Result querySites() { public Result querySites() {
List<SiteCugInfo> list = infoService.query(Cnd.where(SiteCugInfo::getState, "=", true).desc(SiteCugInfo::getSortNum)); List<SiteCugInfo> list = infoService.query(Cnd.where(SiteCugInfo::getState, "=", true).desc(SiteCugInfo::getSortNum));
@@ -117,7 +172,7 @@ public class SiteCugManageController {
} }
@At @At
@ApiOperation("查询单个场地") @ApiOperation("query site info")
@SaCheckLogin @SaCheckLogin
public Result info(String id) { public Result info(String id) {
SiteCugInfo info = infoService.fetch(id); SiteCugInfo info = infoService.fetch(id);
@@ -129,4 +184,59 @@ public class SiteCugManageController {
map.put("typeName", type == null ? "" : type.getName()); map.put("typeName", type == null ? "" : type.getName());
return Result.success(map); return Result.success(map);
} }
private boolean isPositiveNumber(Object value) {
if (value == null) {
return false;
}
try {
return Double.parseDouble(String.valueOf(value)) > 0;
} catch (Exception e) {
return false;
}
}
private NutMap normalizeOpenHourTimeUnit(NutMap item) {
if (item == null) {
return null;
}
item.put("timeUnit", normalizeTimeUnitMinutes(item.get("timeUnit")));
return item;
}
private int normalizeTimeUnitMinutes(Object value) {
if (value == null) {
return 60;
}
try {
double parsed = Double.parseDouble(String.valueOf(value));
if (parsed <= 0) {
return 60;
}
return (int) Math.round(parsed);
} catch (Exception e) {
return 60;
}
}
private boolean hasOverlap(List<NutMap> openHours) {
if (openHours == null || openHours.size() < 2) {
return false;
}
List<NutMap> validHours = openHours.stream()
.filter(item -> item != null
&& StrUtil.isNotBlank(item.getString("startTime"))
&& StrUtil.isNotBlank(item.getString("endTime"))
&& item.getString("startTime").compareTo(item.getString("endTime")) < 0)
.sorted((a, b) -> a.getString("startTime").compareTo(b.getString("startTime")))
.collect(Collectors.toList());
for (int i = 1; i < validHours.size(); i++) {
NutMap prev = validHours.get(i - 1);
NutMap current = validHours.get(i);
if (current.getString("startTime").compareTo(prev.getString("endTime")) < 0) {
return true;
}
}
return false;
}
} }
@@ -81,6 +81,7 @@ public class SiteCugMineController {
t.finishTime, t.finishTime,
t.taskParentId, t.taskParentId,
t.variable taskVariable, t.variable taskVariable,
CASE WHEN info.reserveType = 'club' THEN info.clubName ELSE info.applyUnionName END AS reserveTargetName,
CASE CASE
WHEN ins.state in (10, 20) AND (t.displayName LIKE '%校工会允许%' OR t.taskName = 'c8c03407-cf26-4e0b-82f1-b2afc9c79996') WHEN ins.state in (10, 20) AND (t.displayName LIKE '%校工会允许%' OR t.taskName = 'c8c03407-cf26-4e0b-82f1-b2afc9c79996')
THEN 1 THEN 1
@@ -105,7 +106,6 @@ public class SiteCugMineController {
SqlExpressionGroup seg = new SqlExpressionGroup(); SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("si.name", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg); cnd.and(seg);
} }
cnd.andEX("si.typeId", "=", siteType); cnd.andEX("si.typeId", "=", siteType);
@@ -75,6 +75,7 @@ public class SiteCugSchoolUnionAuditController {
SELECT SELECT
info.*, info.*,
si.name as siteName, si.name as siteName,
CASE WHEN info.reserveType = 'club' THEN info.clubName ELSE info.applyUnionName END AS reserveTargetName,
ins.id AS instanceId, ins.id AS instanceId,
ins.businessNo, ins.businessNo,
ins.state instanceState, ins.state instanceState,
@@ -113,7 +114,6 @@ public class SiteCugSchoolUnionAuditController {
SqlExpressionGroup seg = new SqlExpressionGroup(); SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("si.name", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg); cnd.and(seg);
} }
cnd.andEX("si.typeId", "=", siteType); cnd.andEX("si.typeId", "=", siteType);
@@ -129,7 +129,7 @@ public class SiteCugSchoolUnionAuditController {
} }
if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) { if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); cnd.orderBy(resolveOrderColumn(pageForm.getPageOrderName()), PageUtil.getOrder(pageForm.getPageOrderBy()));
} else { } else {
cnd.desc("info.createdAt"); cnd.desc("info.createdAt");
} }
@@ -139,6 +139,36 @@ public class SiteCugSchoolUnionAuditController {
return Result.success(pageVO); return Result.success(pageVO);
} }
private String resolveOrderColumn(String pageOrderName) {
if (StrUtil.isBlank(pageOrderName)) {
return "info.createdAt";
}
switch (pageOrderName) {
case "siteName":
return "si.name";
case "applyUserName":
return "info.applyUserName";
case "applyLoginName":
return "info.applyLoginName";
case "yearlyBatch":
return "yearlyBatch";
case "reserveTargetName":
return "reserveTargetName";
case "reserveStartTime":
return "info.reserveStartTime";
case "reserveEndTime":
return "info.reserveEndTime";
case "applyMobile":
return "info.applyMobile";
case "curTaskName":
return "curTaskName";
case "instanceState":
return "ins.state";
default:
return "info.createdAt";
}
}
@At("/executeTask") @At("/executeTask")
@ApiOperation("执行审核任务") @ApiOperation("执行审核任务")
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@@ -109,4 +109,6 @@ public class SiteCugApply extends BaseModel {
@Comment("反馈意见") @Comment("反馈意见")
@ColDefine(type = ColType.VARCHAR, width = 200) @ColDefine(type = ColType.VARCHAR, width = 200)
private String backOption; private String backOption;
private String yearlyReserveEndDate;
} }
@@ -78,6 +78,27 @@ public class SiteCugInfo extends BaseModel {
@ColDefine(type = ColType.MYSQL_JSON) @ColDefine(type = ColType.MYSQL_JSON)
private List<NutMap> notApplyTimeList; private List<NutMap> notApplyTimeList;
@Column
@Comment("预约时间段类型(1分段预约,2全天候预约)")
@ColDefine(type = ColType.INT)
@Default("1")
private Integer reserveTimeType;
@Column
@Comment("场次信息")
@ColDefine(type = ColType.MYSQL_JSON)
private List<NutMap> openHours;
@Column
@Comment("分段预约场次信息")
@ColDefine(type = ColType.MYSQL_JSON)
private List<NutMap> segmentedOpenHours;
@Column
@Comment("全天候预约时间段")
@ColDefine(type = ColType.MYSQL_JSON)
private NutMap fullDayOpenHour;
@Column @Column
@Comment("开启状态") @Comment("开启状态")
@ColDefine(type = ColType.BOOLEAN) @ColDefine(type = ColType.BOOLEAN)
@@ -94,4 +115,9 @@ public class SiteCugInfo extends BaseModel {
@Comment("场地介绍") @Comment("场地介绍")
@ColDefine(type = ColType.TEXT) @ColDefine(type = ColType.TEXT)
private String introduce; private String introduce;
@Column
@Comment("场地照片")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String sitePhoto;
} }
@@ -136,6 +136,7 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
si.name AS siteName, si.name AS siteName,
ins.id AS instanceId, ins.id AS instanceId,
ins.processDefineId instanceProcessDefineId, 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, CASE WHEN info.reserveType = 'club' THEN '协会预约' ELSE '分工会预约' END AS reserveTypeName,
CASE WHEN info.reserveType = 'club' THEN info.clubName ELSE info.applyUnionName END AS reserveTargetName, 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 DATE_FORMAT(info.createdAt, '%Y-%m-%d %H:%i:%s') AS applyTime
@@ -155,7 +156,6 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
SqlExpressionGroup seg = new SqlExpressionGroup(); SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("si.name", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg); cnd.and(seg);
} }
if (StrUtil.isNotBlank(pageForm.getReserveTargetKeyword())) { if (StrUtil.isNotBlank(pageForm.getReserveTargetKeyword())) {
@@ -27,27 +27,71 @@ layout("/layouts/platform.html"){
<el-card shadow="never" class="mt20"> <el-card shadow="never" class="mt20">
<table-tool label="场地列表"></table-tool> <table-tool label="场地列表"></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize"> <div v-if="tableData.length > 0" class="site-card-grid">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column> <div v-for="row in tableData" :key="row.id" class="site-card">
<el-table-column <div class="site-card__media">
:label="column.label" <el-image
:prop="column.prop" v-if="row.sitePhoto"
:key="column.prop" :preview-src-list="[row.sitePhoto]"
:width="column.width" :src="row.sitePhoto"
:sortable="column.sortable" fit="cover"
align="center" class="site-card__image">
header-align="center" </el-image>
show-overflow-tooltip <div v-else class="site-card__image site-card__image--placeholder">
v-for="column in tableColumns" <i class="el-icon-picture-outline"></i>
> <span>暂无场地照片</span>
</el-table-column> </div>
<el-table-column label="操作" width="180"> </div>
<template v-slot="{ row }"> <div class="site-card__content">
<el-button @click="onViewSite(row)" size="mini" type="primary">查看场地</el-button> <div class="site-card__header">
<el-button @click="onApply(row)" size="mini" type="primary">预约</el-button> <div class="site-card__title-wrap">
</template> <div class="site-card__title" :title="row.name">{{ row.name || '--' }}</div>
</el-table-column> <div class="site-card__meta">
</el-table> <i class="el-icon-location-outline"></i>
<span :title="row.address">{{ row.address || '暂无场地地址' }}</span>
</div>
</div>
<div class="site-card__actions">
<el-tag size="mini" effect="dark" type="success">{{ row.reserveTimeTypeName || '分段预约' }}</el-tag>
<el-button size="mini" plain @click="onViewSite(row)">场地详情</el-button>
<el-button size="mini" type="primary" @click="onApply(row)">预约</el-button>
</div>
</div>
<div class="site-card__summary">
<span>场地类型:{{ row.typeName || '--' }}</span>
<span>容纳人数:{{ row.maxNum || 0 }}人</span>
<span>联系人:{{ row.contactName || '--' }}</span>
</div>
<div class="site-card__timeline">
<div class="site-card__timeline-title">今日预约情况</div>
<div class="timeline-bar">
<span
v-for="(segment, index) in row.timelineSegments || []"
:key="row.id + '-segment-' + index"
:class="['timeline-bar__segment', 'timeline-bar__segment--' + (segment.status || 'closed')]">
</span>
</div>
<div class="timeline-scale">
<span v-for="hour in hourMarks" :key="row.id + '-hour-' + hour">{{ hour }}</span>
</div>
<div class="timeline-legend">
<span class="timeline-legend__item">
<i class="timeline-legend__dot timeline-legend__dot--available"></i>未预约
</span>
<span class="timeline-legend__item">
<i class="timeline-legend__dot timeline-legend__dot--reserved"></i>已预约
</span>
<span class="timeline-legend__item">
<i class="timeline-legend__dot timeline-legend__dot--closed"></i>未开放
</span>
</div>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无可预约场地"></el-empty>
<!--#include("/layouts/pagination.html"){}#--> <!--#include("/layouts/pagination.html"){}#-->
</el-card> </el-card>
</template> </template>
@@ -63,7 +107,7 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}"> <script nonce="${cspNonce!}">
<!--#include('../manage/info.js'){}#--> <!--#include('../manage/info.js'){}#-->
<!--#include('apply.js'){}#--> <!--#include('apply_v2.js'){}#-->
const vue = new Vue({ const vue = new Vue({
el: "#app", el: "#app",
mixins: [initTableMixins], mixins: [initTableMixins],
@@ -74,14 +118,7 @@ layout("/layouts/platform.html"){
data() { data() {
return { return {
typeOptions: [], typeOptions: [],
tableColumns: [ hourMarks: ['0', '2', '4', '6', '8', '10', '12', '14', '16', '18', '20', '22']
{prop: 'name', label: '场地名称'},
{prop: 'address', label: '场地地址'},
{prop: 'contactName', label: '联系人'},
{prop: 'contactPhone', label: '联系电话'},
{prop: 'maxNum', label: '容纳人数'},
{prop: 'typeName', label: '场地类型'},
],
} }
}, },
methods: { methods: {
@@ -120,6 +157,374 @@ layout("/layouts/platform.html"){
}) })
</script> </script>
<style>
.site-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
}
.site-card {
display: flex;
min-height: 240px;
padding: 18px;
border: 1px solid #e8edf5;
border-radius: 16px;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
box-shadow: 0 10px 30px rgba(21, 66, 120, 0.06);
}
.site-card__media {
flex: 0 0 180px;
margin-right: 18px;
}
.site-card__image {
width: 180px;
height: 180px;
border-radius: 14px;
overflow: hidden;
}
.site-card__image--placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #8c9bb1;
background: linear-gradient(135deg, #eef4fb 0%, #dde8f6 100%);
font-size: 14px;
gap: 10px;
}
.site-card__image--placeholder i {
font-size: 34px;
}
.site-card__content {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
}
.site-card__header {
display: flex;
justify-content: space-between;
gap: 16px;
}
.site-card__title-wrap {
min-width: 0;
}
.site-card__title {
overflow: hidden;
color: #1f2d3d;
font-size: 22px;
font-weight: 700;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-card__meta {
display: flex;
align-items: center;
margin-top: 10px;
color: #7f8ea3;
font-size: 14px;
gap: 6px;
}
.site-card__meta span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-card__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
gap: 8px;
}
.site-card__summary {
display: flex;
flex-wrap: wrap;
margin-top: 16px;
color: #4f6277;
font-size: 13px;
gap: 16px;
}
.site-card__timeline {
margin-top: auto;
padding-top: 20px;
}
.site-card__timeline-title {
margin-bottom: 10px;
color: #25364d;
font-size: 14px;
font-weight: 600;
}
.timeline-bar {
display: grid;
grid-template-columns: repeat(48, minmax(0, 1fr));
gap: 2px;
align-items: center;
height: 14px;
}
.timeline-bar__segment {
height: 8px;
border-radius: 999px;
background: #e5ebf3;
}
.timeline-bar__segment--available {
background: #32b56c;
}
.timeline-bar__segment--reserved {
background: #9aa6b2;
}
.timeline-bar__segment--closed {
background: #e9edf3;
}
.timeline-scale {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
margin-top: 10px;
color: #607389;
font-size: 12px;
}
.timeline-scale span {
text-align: left;
}
.timeline-scale span:last-child {
text-align: right;
}
.timeline-legend {
display: flex;
flex-wrap: wrap;
margin-top: 12px;
color: #607389;
font-size: 13px;
gap: 14px;
}
.timeline-legend__item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.timeline-legend__dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.timeline-legend__dot--available {
background: #32b56c;
}
.timeline-legend__dot--reserved {
background: #9aa6b2;
}
.timeline-legend__dot--closed {
background: #e9edf3;
border: 1px solid #d6dde8;
}
.apply-visual-panel {
margin-bottom: 18px;
padding: 18px;
border: 1px solid #e8edf5;
border-radius: 14px;
background: linear-gradient(180deg, #ffffff 0%, #f6faff 100%);
}
.apply-visual-panel__toolbar {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.apply-visual-panel__title {
color: #1f2d3d;
font-size: 16px;
font-weight: 700;
}
.apply-visual-panel__subtitle {
margin-top: 6px;
color: #708399;
font-size: 13px;
line-height: 1.6;
}
.apply-visual-panel__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.apply-visual-panel__legend {
display: flex;
flex-wrap: wrap;
margin-top: 14px;
gap: 14px;
}
.apply-legend__item {
display: inline-flex;
align-items: center;
color: #607389;
font-size: 13px;
gap: 6px;
}
.apply-legend__dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.apply-legend__dot--available {
background: #32b56c;
}
.apply-legend__dot--reserved {
background: #9aa6b2;
}
.apply-legend__dot--selected {
background: #2f7df6;
}
.apply-legend__dot--closed {
background: #e9edf3;
border: 1px solid #d6dde8;
}
.apply-block-group {
margin-top: 16px;
}
.apply-block-group__title {
margin-bottom: 10px;
color: #1f2d3d;
font-size: 14px;
font-weight: 600;
}
.apply-block-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.apply-block {
min-height: 40px;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 10px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.apply-block--available {
color: #0f5132;
background: #d9f6e5;
border-color: #87d7a7;
}
.apply-block--reserved {
color: #4b5663;
background: #e1e6ec;
border-color: #c5ced8;
cursor: not-allowed;
}
.apply-block--closed {
color: #8b97a6;
background: #f3f5f8;
border-color: #e2e8f0;
cursor: not-allowed;
}
.apply-block--selected {
color: #ffffff;
background: #2f7df6;
border-color: #2f7df6;
box-shadow: 0 6px 16px rgba(47, 125, 246, 0.22);
}
.apply-block:not(.apply-block--reserved):not(.apply-block--closed):hover {
transform: translateY(-1px);
box-shadow: 0 8px 16px rgba(30, 72, 124, 0.12);
}
.apply-visual-panel__summary {
margin-top: 16px;
padding: 12px 14px;
color: #1f4e8c;
font-size: 13px;
background: #eef5ff;
border-radius: 10px;
}
@media screen and (max-width: 1400px) {
.site-card-grid {
grid-template-columns: 1fr;
}
}
@media screen and (max-width: 768px) {
.site-card {
flex-direction: column;
}
.site-card__media {
flex: none;
margin-right: 0;
margin-bottom: 16px;
}
.site-card__image {
width: 100%;
height: 220px;
}
.site-card__header {
flex-direction: column;
}
.site-card__actions {
justify-content: flex-start;
}
.apply-visual-panel__toolbar {
flex-direction: column;
}
}
</style>
<!--# <!--#
} }
#--> #-->
@@ -2,109 +2,226 @@
template: /*language=HTML*/ ` template: /*language=HTML*/ `
<div> <div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left"> <el-form :model="formData" ref="formRef" :rules="formRules" label-width="110px" label-position="left">
<el-row :gutter="20" type="flex"> <el-tabs v-model="activeTab">
<el-col :span="12"> <el-tab-pane label="场地基本信息" name="basicInfo">
<el-form-item label="创建人" prop="createUserName"> <el-row :gutter="20" type="flex">
<el-input disabled type="text" v-model="formData.createUserName" maxlength="20" placeholder="请输入创建人"></el-input> <el-col :span="12">
</el-form-item> <el-form-item label="创建人" prop="createUserName">
</el-col> <el-input disabled type="text" v-model="formData.createUserName" maxlength="20" placeholder="请输入创建人"></el-input>
<el-col :span="12"> </el-form-item>
<el-form-item prop="sortNum" label="排序编号"> </el-col>
<el-input v-model="formData.sortNum" placeholder="请输入排序编号" type="number"></el-input> <el-col :span="12">
</el-form-item> <el-form-item prop="sortNum" label="排序编号">
</el-col> <el-input v-model="formData.sortNum" placeholder="请输入排序编号" type="number"></el-input>
</el-row> </el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" type="flex"> <el-row :gutter="20" type="flex">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="场地名称" prop="name"> <el-form-item label="场地名称" prop="name">
<el-input type="text" v-model="formData.name" maxlength="50" placeholder="请输入场地名称"></el-input> <el-input type="text" v-model="formData.name" maxlength="50" placeholder="请输入场地名称"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="场地地址" prop="address"> <el-form-item label="场地地址" prop="address">
<el-input type="text" v-model="formData.address" maxlength="100" placeholder="请输入场地地址"></el-input> <el-input type="text" v-model="formData.address" maxlength="100" placeholder="请输入场地地址"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" type="flex"> <el-row :gutter="20" type="flex">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="联系人" prop="contactName"> <el-form-item label="联系人" prop="contactName">
<el-input type="text" v-model="formData.contactName" maxlength="20" placeholder="请输入联系人"></el-input> <el-input type="text" v-model="formData.contactName" maxlength="20" placeholder="请输入联系人"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone"> <el-form-item label="联系电话" prop="contactPhone">
<el-input type="text" v-model="formData.contactPhone" maxlength="20" placeholder="请输入联系电话"></el-input> <el-input type="text" v-model="formData.contactPhone" maxlength="20" placeholder="请输入联系电话"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" type="flex"> <el-row :gutter="20" type="flex">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="容纳人数" prop="maxNum"> <el-form-item label="容纳人数" prop="maxNum">
<el-input v-model="formData.maxNum" placeholder="请输入容纳人数" type="number"></el-input> <el-input v-model="formData.maxNum" placeholder="请输入容纳人数" type="number"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="场地类型" prop="typeId"> <el-form-item label="场地类型" prop="typeId">
<el-select v-model="formData.typeId" clearable filterable placeholder="请选择场地类型" style="width: 100%"> <el-select v-model="formData.typeId" clearable filterable placeholder="请选择场地类型" style="width: 100%">
<el-option <el-option
v-for="item in typeList" v-for="item in typeList"
:label="item.name" :label="item.name"
:value="item.id" :value="item.id"
:key="item.id" :key="item.id"
></el-option> ></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" type="flex"> <el-form-item label="场地介绍" prop="introduce">
<el-col :span="12"> <text-editor v-model="formData.introduce"></text-editor>
<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-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-form-item label="场地照片" prop="sitePhoto">
<el-col :span="12"> <file-upload
<el-form-item prop="filterHolidays" label="排除节假日"> :value.sync="formData.sitePhoto"
<el-radio-group v-model="formData.filterHolidays" size="medium"> :upload_number="1"
<el-radio-button :label="true">是</el-radio-button> :upload_size="1024 * 1024 * 10"
<el-radio-button :label="false">否</el-radio-button> accept=".jpg,.jpeg,.png"
</el-radio-group> upload_result_category="interval"
complete_result>
</file-upload>
</el-form-item> </el-form-item>
</el-col> </el-tab-pane>
<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"> <el-tab-pane label="设置场地" name="siteSetting">
<text-editor v-model="formData.introduce"></text-editor> <el-row :gutter="20" type="flex">
</el-form-item> <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-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-row>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item prop="reserveTimeType" label="预约时间段类型">
<el-radio-group v-model="formData.reserveTimeType" @change="handleReserveTimeTypeChange" size="medium">
<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="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-divider content-position="left">场次信息</el-divider>
<div v-if="formData.reserveTimeType === 1">
<div class="left-span-label">分段预约:可设置一个或多个场次,预约人按拆分后的场次进行预约。</div>
<el-table :data="formData.openHours" border size="mini">
<el-table-column type="index" label="序号" align="center" header-align="center" width="80"></el-table-column>
<el-table-column prop="startTime" label="开始时间" align="center" header-align="center">
<template v-slot="{ row }">
<el-time-select
style="width: 100%"
placeholder="开始时间"
v-model="row.startTime"
@change="checkSegmentedOpenHoursOverlap"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" align="center" header-align="center">
<template v-slot="{ row }">
<el-time-select
style="width: 100%"
placeholder="结束时间"
v-model="row.endTime"
@change="checkSegmentedOpenHoursOverlap"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</template>
</el-table-column>
<el-table-column prop="timeUnit" label="预约时间单位(分钟)" align="center" header-align="center">
<template v-slot="{ row }">
<el-input-number style="width: 100%" v-model="row.timeUnit" :min="1" :step="1" :precision="0" placeholder="请输入预约时间单位"></el-input-number>
</template>
</el-table-column>
<el-table-column label="操作" align="center" header-align="center" width="130">
<template slot="header">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="addOpenHour">添加场次</el-button>
</template>
<template v-slot="scope">
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
:disabled="formData.openHours.length <= 1"
@click="formData.openHours.splice(scope.$index, 1)">
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else>
<div class="left-span-label">全天候预约:设置可预约的开始时间、结束时间和预约时间单位(分钟),预约人按设定单位选择连续时段。</div>
<el-row :gutter="20" type="flex">
<el-col :span="12">
<el-form-item label="开始时间">
<el-time-select
style="width: 100%"
placeholder="开始时间"
v-model="fullDayOpenHour.startTime"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间">
<el-time-select
style="width: 100%"
placeholder="结束时间"
v-model="fullDayOpenHour.endTime"
:picker-options="{ start: '00:00', step: '00:30', end: '23:59' }">
</el-time-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约时间单位(分钟)">
<el-input-number
style="width: 100%"
v-model="fullDayOpenHour.timeUnit"
:min="1"
:step="1"
:precision="0"
placeholder="请输入预约时间单位">
</el-input-number>
</el-form-item>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</el-form> </el-form>
<div class="mt10" style="color: #909399; font-size: 13px;">
暂存:保存当前填写内容并留在本页继续编辑。正式提交:保存后返回列表页。
</div>
<el-row class="mt10" justify="end" type="flex"> <el-row class="mt10" justify="end" type="flex">
<el-button @click="$emit('refresh')">取消</el-button> <el-button @click="$emit('refresh')">取消</el-button>
<el-button @click="onSubmit" type="primary">提交</el-button> <el-button @click="onTempSave" plain type="warning">暂存并继续编辑</el-button>
<el-button @click="onSubmit" type="primary">正式提交</el-button>
</el-row> </el-row>
<el-dialog append-to-body :close-on-click-modal="false" :visible.sync="setUpTimeDialog" title="设置时间"> <el-dialog append-to-body :close-on-click-modal="false" :visible.sync="setUpTimeDialog" title="设置时间">
@@ -202,10 +319,17 @@
store, store,
data() { data() {
return { return {
activeTab: 'basicInfo',
segmentedOpenHoursCache: [{ startTime: '', endTime: '', timeUnit: 30 }],
fullDayOpenHourCache: { startTime: '', endTime: '', timeUnit: 30 },
formData: { formData: {
state: true, state: true,
sexLimit: 0, sexLimit: 0,
filterHolidays: false, filterHolidays: false,
reserveTimeType: 1,
openHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
segmentedOpenHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
fullDayOpenHour: { startTime: '', endTime: '', timeUnit: 30 },
notApplyTimeList: [], notApplyTimeList: [],
createUserName: this.$store.state.user.username, createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id, createUserId: this.$store.state.user.id,
@@ -222,6 +346,7 @@
sexLimit: [{required: true, message: '必填', trigger: ['blur', 'change']}], sexLimit: [{required: true, message: '必填', trigger: ['blur', 'change']}],
filterHolidays: [{required: true, message: '必填', trigger: ['blur', 'change']}], filterHolidays: [{required: true, message: '必填', trigger: ['blur', 'change']}],
state: [{required: true, message: '必填', trigger: ['blur', 'change']}], state: [{required: true, message: '必填', trigger: ['blur', 'change']}],
reserveTimeType: [{required: true, message: '必填', trigger: ['blur', 'change']}],
}, },
setUpTimeDialog: false, setUpTimeDialog: false,
timeOneKeySet: { timeOneKeySet: {
@@ -231,7 +356,218 @@
multipleSelection: [], multipleSelection: [],
} }
}, },
computed: {
fullDayOpenHour() {
if (!Array.isArray(this.formData.openHours)) {
this.$set(this.formData, 'openHours', [])
}
if (this.formData.openHours.length === 0) {
this.formData.openHours.push({ startTime: '', endTime: '', timeUnit: 30 })
}
if (!this.formData.openHours[0].timeUnit || this.formData.openHours[0].timeUnit <= 0) {
this.$set(this.formData.openHours[0], 'timeUnit', 30)
}
return this.formData.openHours[0]
}
},
methods: { methods: {
normalizeTimeUnit(value, defaultValue = 30) {
const num = Number(value)
if (!Number.isFinite(num) || num <= 0) {
return defaultValue
}
return Math.round(num)
},
syncOpenHoursCache() {
if (this.formData.reserveTimeType === 1) {
this.segmentedOpenHoursCache = Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0
? clone(this.formData.openHours).map(item => ({
startTime: item.startTime || '',
endTime: item.endTime || '',
timeUnit: this.normalizeTimeUnit(item.timeUnit),
}))
: [{ startTime: '', endTime: '', timeUnit: 30 }]
this.formData.segmentedOpenHours = clone(this.segmentedOpenHoursCache)
} else {
const first = (this.formData.openHours && this.formData.openHours[0]) || {}
this.fullDayOpenHourCache = {
startTime: first.startTime || '',
endTime: first.endTime || '',
timeUnit: this.normalizeTimeUnit(first.timeUnit),
}
this.formData.fullDayOpenHour = clone(this.fullDayOpenHourCache)
}
},
buildSubmitFormData() {
this.syncOpenHoursCache()
const submitData = clone(this.formData)
submitData.segmentedOpenHours = clone(this.segmentedOpenHoursCache || [])
submitData.fullDayOpenHour = clone(this.fullDayOpenHourCache || { startTime: '', endTime: '', timeUnit: 30 })
if (submitData.reserveTimeType === 1) {
submitData.openHours = clone(this.segmentedOpenHoursCache || [])
} else {
submitData.openHours = [clone(this.fullDayOpenHourCache || { startTime: '', endTime: '', timeUnit: 60 })]
}
return submitData
},
initOpenHoursCache() {
const segmentedOpenHours = Array.isArray(this.formData.segmentedOpenHours) && this.formData.segmentedOpenHours.length > 0
? clone(this.formData.segmentedOpenHours)
: (Array.isArray(this.formData.openHours) ? clone(this.formData.openHours) : [])
const fullDayOpenHour = this.formData.fullDayOpenHour && Object.keys(this.formData.fullDayOpenHour).length > 0
? clone(this.formData.fullDayOpenHour)
: ((Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0) ? clone(this.formData.openHours[0]) : {})
if (this.formData.reserveTimeType === 2) {
this.fullDayOpenHourCache = {
startTime: fullDayOpenHour.startTime || '',
endTime: fullDayOpenHour.endTime || '',
timeUnit: this.normalizeTimeUnit(fullDayOpenHour.timeUnit),
}
this.segmentedOpenHoursCache = segmentedOpenHours && segmentedOpenHours.length > 0
? segmentedOpenHours.map(item => ({
startTime: item.startTime || '',
endTime: item.endTime || '',
timeUnit: this.normalizeTimeUnit(item.timeUnit),
}))
: [{ startTime: '', endTime: '', timeUnit: 30 }]
this.formData.openHours = [clone(this.fullDayOpenHourCache)]
} else {
this.segmentedOpenHoursCache = segmentedOpenHours.length > 0
? segmentedOpenHours.map(item => ({
startTime: item.startTime || '',
endTime: item.endTime || '',
timeUnit: this.normalizeTimeUnit(item.timeUnit),
}))
: [{ startTime: '', endTime: '', timeUnit: 30 }]
const first = this.segmentedOpenHoursCache[0] || {}
this.fullDayOpenHourCache = {
startTime: fullDayOpenHour.startTime || first.startTime || '',
endTime: fullDayOpenHour.endTime || first.endTime || '',
timeUnit: this.normalizeTimeUnit(fullDayOpenHour.timeUnit || first.timeUnit),
}
this.formData.openHours = clone(this.segmentedOpenHoursCache)
}
this.formData.segmentedOpenHours = clone(this.segmentedOpenHoursCache)
this.formData.fullDayOpenHour = clone(this.fullDayOpenHourCache)
},
addOpenHour() {
if (!Array.isArray(this.formData.openHours)) {
this.$set(this.formData, 'openHours', [])
}
this.formData.openHours.push({ startTime: '', endTime: '', timeUnit: 30 })
this.segmentedOpenHoursCache = clone(this.formData.openHours)
},
hasSegmentedOpenHoursOverlap(openHours) {
const validHours = (openHours || [])
.map((item, index) => ({ ...item, index }))
.filter(item => item.startTime && item.endTime && item.startTime < item.endTime)
.sort((a, b) => a.startTime.localeCompare(b.startTime))
for (let i = 1; i < validHours.length; i++) {
const prev = validHours[i - 1]
const current = validHours[i]
if (current.startTime < prev.endTime) {
return {
overlap: true,
prevIndex: prev.index,
currentIndex: current.index,
}
}
}
return { overlap: false }
},
checkSegmentedOpenHoursOverlap() {
if (this.formData.reserveTimeType !== 1) {
return false
}
const result = this.hasSegmentedOpenHoursOverlap(this.formData.openHours)
if (result.overlap) {
this.$message.warning('第' + (result.prevIndex + 1) + '条和第' + (result.currentIndex + 1) + '条场次时间有重叠,请调整后再保存')
return true
}
return false
},
handleReserveTimeTypeChange(val) {
if (!Array.isArray(this.formData.openHours)) {
this.$set(this.formData, 'openHours', [])
}
if (val === 1) {
const currentFullDay = this.formData.openHours[0] || this.fullDayOpenHourCache || {}
this.fullDayOpenHourCache = {
startTime: currentFullDay.startTime || '',
endTime: currentFullDay.endTime || '',
timeUnit: this.normalizeTimeUnit(currentFullDay.timeUnit),
}
if (!Array.isArray(this.segmentedOpenHoursCache) || this.segmentedOpenHoursCache.length === 0) {
this.segmentedOpenHoursCache = [{
startTime: this.fullDayOpenHourCache.startTime || '',
endTime: this.fullDayOpenHourCache.endTime || '',
timeUnit: this.normalizeTimeUnit(this.fullDayOpenHourCache.timeUnit),
}]
}
this.formData.openHours = clone(this.segmentedOpenHoursCache)
} else if (val === 2) {
this.segmentedOpenHoursCache = Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0
? clone(this.formData.openHours)
: this.segmentedOpenHoursCache
const first = this.fullDayOpenHourCache && (this.fullDayOpenHourCache.startTime || this.fullDayOpenHourCache.endTime)
? this.fullDayOpenHourCache
: (this.segmentedOpenHoursCache[0] || {})
this.fullDayOpenHourCache = {
startTime: first.startTime || '',
endTime: first.endTime || '',
timeUnit: this.normalizeTimeUnit(first.timeUnit || this.fullDayOpenHourCache.timeUnit),
}
this.formData.openHours = [clone(this.fullDayOpenHourCache)]
}
},
validateOpenHours() {
const openHours = this.formData.openHours || []
if (this.formData.reserveTimeType === 1) {
if (openHours.length === 0) {
this.$message.warning('请至少添加一条场次信息')
return false
}
for (let i = 0; i < openHours.length; i++) {
const item = openHours[i] || {}
if (!item.startTime) {
this.$message.warning('第' + (i + 1) + '条场次信息中,开始时间必填')
return false
}
if (!item.endTime) {
this.$message.warning('第' + (i + 1) + '条场次信息中,结束时间必填')
return false
}
if (item.startTime >= item.endTime) {
this.$message.warning('第' + (i + 1) + '条场次信息中,开始时间必须早于结束时间')
return false
}
if (!item.timeUnit || item.timeUnit <= 0) {
this.$message.warning('第' + (i + 1) + '条场次信息中,预约时间单位必须大于0分钟')
return false
}
}
const overlapResult = this.hasSegmentedOpenHoursOverlap(openHours)
if (overlapResult.overlap) {
this.$message.warning('第' + (overlapResult.prevIndex + 1) + '条和第' + (overlapResult.currentIndex + 1) + '条场次时间有重叠,请调整后再保存')
return false
}
} else {
const item = openHours[0] || {}
if (!item.startTime || !item.endTime) {
this.$message.warning('请设置全天候预约的开始时间和结束时间')
return false
}
if (item.startTime >= item.endTime) {
this.$message.warning('全天候预约的开始时间必须早于结束时间')
return false
}
if (!item.timeUnit || item.timeUnit <= 0) {
this.$message.warning('全天候预约的预约时间单位必须大于0分钟')
return false
}
}
return true
},
doConfirmSetUpCourse() { doConfirmSetUpCourse() {
const timeList = this.formData.notApplyTimeList const timeList = this.formData.notApplyTimeList
if (timeList && timeList.length > 0) { if (timeList && timeList.length > 0) {
@@ -296,40 +632,103 @@
if (this.formData.notApplyTimeList) { if (this.formData.notApplyTimeList) {
this.$set(this.formData, 'setUpDate', this.formData.notApplyTimeList.map(o => o.date)) this.$set(this.formData, 'setUpDate', this.formData.notApplyTimeList.map(o => o.date))
} }
this.activeTab = 'siteSetting'
this.setUpTimeDialog = true this.setUpTimeDialog = true
}, },
onOpen(row) { onOpen(row) {
this.activeTab = 'basicInfo'
if (row && row.id) { if (row && row.id) {
this.formData = clone(row) this.formData = clone(row)
if (!Array.isArray(this.formData.notApplyTimeList)) { if (!Array.isArray(this.formData.notApplyTimeList)) {
this.$set(this.formData, 'notApplyTimeList', []) this.$set(this.formData, 'notApplyTimeList', [])
} }
if (!this.formData.reserveTimeType) {
this.$set(this.formData, 'reserveTimeType', 1)
}
if (!Array.isArray(this.formData.openHours) || this.formData.openHours.length === 0) {
this.$set(this.formData, 'openHours', this.formData.reserveTimeType === 2 ? [{ startTime: '', endTime: '', timeUnit: 30 }] : [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!Array.isArray(this.formData.segmentedOpenHours) || this.formData.segmentedOpenHours.length === 0) {
this.$set(this.formData, 'segmentedOpenHours', [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!this.formData.fullDayOpenHour) {
this.$set(this.formData, 'fullDayOpenHour', { startTime: '', endTime: '', timeUnit: 30 })
}
this.initOpenHoursCache()
} else { } else {
this.formData = { this.formData = {
state: true, state: true,
sexLimit: 0, sexLimit: 0,
filterHolidays: false, filterHolidays: false,
reserveTimeType: 1,
openHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
segmentedOpenHours: [{ startTime: '', endTime: '', timeUnit: 30 }],
fullDayOpenHour: { startTime: '', endTime: '', timeUnit: 30 },
notApplyTimeList: [], notApplyTimeList: [],
createUserName: this.$store.state.user.username, createUserName: this.$store.state.user.username,
createUserId: this.$store.state.user.id, createUserId: this.$store.state.user.id,
} }
this.segmentedOpenHoursCache = [{ startTime: '', endTime: '', timeUnit: 30 }]
this.fullDayOpenHourCache = { startTime: '', endTime: '', timeUnit: 30 }
} }
}, },
async doSave(stayOnPage) {
if (!this.validateOpenHours()) {
this.activeTab = 'siteSetting'
return
}
const submitData = this.buildSubmitFormData()
const resp = await this.$axios.post("/platform/siteCug/manage/submit", {data: JSON.stringify(submitData)})
if (resp.code === 0) {
this.$message.success(stayOnPage ? '暂存成功,您可以继续编辑当前内容' : '正式提交成功,已返回列表页')
if (resp.data) {
this.formData = clone(resp.data)
if (!Array.isArray(this.formData.notApplyTimeList)) {
this.$set(this.formData, 'notApplyTimeList', [])
}
if (!this.formData.reserveTimeType) {
this.$set(this.formData, 'reserveTimeType', 1)
}
if (!Array.isArray(this.formData.openHours) || this.formData.openHours.length === 0) {
this.$set(this.formData, 'openHours', this.formData.reserveTimeType === 2 ? [{ startTime: '', endTime: '', timeUnit: 30 }] : [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!Array.isArray(this.formData.segmentedOpenHours) || this.formData.segmentedOpenHours.length === 0) {
this.$set(this.formData, 'segmentedOpenHours', [{ startTime: '', endTime: '', timeUnit: 30 }])
}
if (!this.formData.fullDayOpenHour) {
this.$set(this.formData, 'fullDayOpenHour', { startTime: '', endTime: '', timeUnit: 30 })
}
this.initOpenHoursCache()
}
if (!stayOnPage) {
this.$emit('refresh')
}
} else {
this.$message.warning(resp.msg)
}
},
onTempSave() {
this.$refs.formRef.validate(async (valid) => {
if (valid) {
this.$confirm("确认暂存当前内容,并继续留在本页编辑吗?", "暂存确认", {
confirmButtonText: "确认暂存",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
await this.doSave(true)
}).catch(() => {})
}
})
},
onSubmit() { onSubmit() {
this.$refs.formRef.validate((valid) => { this.$refs.formRef.validate((valid) => {
if (valid) { if (valid) {
this.$confirm("您确定要提交吗?", "提示", { this.$confirm("确认正式提交当前内容吗?提交成功后将返回列表页。", "正式提交确认", {
confirmButtonText: "确", confirmButtonText: "确认提交",
cancelButtonText: "取消", cancelButtonText: "取消",
type: "warning" type: "primary"
}).then(async () => { }).then(async () => {
const resp = await this.$axios.post("/platform/siteCug/manage/submit", {data: JSON.stringify(this.formData)}) await this.doSave(false)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.warning(resp.msg)
}
}) })
} }
}) })
@@ -39,15 +39,15 @@ layout("/layouts/platform.html"){
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize"> <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="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column <el-table-column
v-for="column in tableColumns"
:key="column.prop"
:label="column.label" :label="column.label"
:prop="column.prop" :prop="column.prop"
:key="column.prop"
:width="column.width" :width="column.width"
:sortable="column.sortable" :sortable="column.sortable"
align="center" align="center"
header-align="center" header-align="center"
show-overflow-tooltip show-overflow-tooltip
v-for="column in tableColumns"
> >
<template v-slot="{ row }" v-if="column.prop === 'state'"> <template v-slot="{ row }" v-if="column.prop === 'state'">
<el-switch <el-switch
@@ -57,6 +57,10 @@ layout("/layouts/platform.html"){
inactive-color="#ff4949"> inactive-color="#ff4949">
</el-switch> </el-switch>
</template> </template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTimeType'">
<span v-if="row.reserveTimeType === 2">全天候预约</span>
<span v-else>分段预约</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'sexLimit'"> <template v-slot="{ row }" v-else-if="column.prop === 'sexLimit'">
<span v-if="row.sexLimit === 0">不限制</span> <span v-if="row.sexLimit === 0">不限制</span>
<span v-else-if="row.sexLimit === 1"></span> <span v-else-if="row.sexLimit === 1"></span>
@@ -98,16 +102,16 @@ layout("/layouts/platform.html"){
return { return {
typeOptions: [], typeOptions: [],
tableColumns: [ tableColumns: [
{prop: 'createUserName', label: '创建人'}, {prop: 'createUserName', label: '创建人', sortable: 'custom'},
{prop: 'sortNum', label: '排序编号'}, {prop: 'name', label: '场地名称', sortable: 'custom'},
{prop: 'name', label: '场地名称'}, {prop: 'address', label: '场地地址', sortable: 'custom'},
{prop: 'address', label: '场地地址'}, {prop: 'contactName', label: '联系人', sortable: 'custom'},
{prop: 'contactName', label: '联系'}, {prop: 'contactPhone', label: '联系电话', sortable: 'custom'},
{prop: 'contactPhone', label: '联系电话'}, {prop: 'maxNum', label: '容纳人数', sortable: 'custom'},
{prop: 'maxNum', label: '容纳人数'},
{prop: 'typeName', label: '场地类型'}, {prop: 'typeName', label: '场地类型'},
{prop: 'sexLimit', label: '性别限制'}, {prop: 'reserveTimeType', label: '预约时间段类型', sortable: 'custom'},
{prop: 'state', label: '开启状态'}, {prop: 'sexLimit', label: '性别限制', sortable: 'custom'},
{prop: 'state', label: '开启状态', sortable: 'custom'},
], ],
} }
}, },
@@ -132,7 +136,7 @@ layout("/layouts/platform.html"){
}) })
}, },
onDelete(row) { onDelete(row) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", { this.$confirm("您确定要删除吗? 是否继续?", "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消", cancelButtonText: "取消",
type: "warning" type: "warning"
@@ -1,51 +1,99 @@
const siteInfo = { const siteInfo = {
template: /*language=HTML*/ ` template: /*language=HTML*/ `
<div> <div>
<el-descriptions :column="2" border> <el-tabs v-model="activeTab">
<el-descriptions-item label="创建人">{{ viewData.createUserName }}</el-descriptions-item> <el-tab-pane label="场地基本信息" name="basicInfo">
<el-descriptions-item label="排序编号">{{ viewData.sortNum }}</el-descriptions-item> <el-descriptions :column="2" border>
<el-descriptions-item label="场地名称">{{ viewData.name }}</el-descriptions-item> <el-descriptions-item label="创建人">{{ viewData.createUserName }}</el-descriptions-item>
<el-descriptions-item label="场地地址">{{ viewData.address }}</el-descriptions-item> <el-descriptions-item label="排序编号">{{ viewData.sortNum }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ viewData.contactName }}</el-descriptions-item> <el-descriptions-item label="场地名称">{{ viewData.name }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewData.contactPhone }}</el-descriptions-item> <el-descriptions-item label="场地地址">{{ viewData.address }}</el-descriptions-item>
<el-descriptions-item label="容纳人数">{{ viewData.maxNum }}</el-descriptions-item> <el-descriptions-item label="联系人">{{ viewData.contactName }}</el-descriptions-item>
<el-descriptions-item label="场地类型">{{ viewData.typeName }}</el-descriptions-item> <el-descriptions-item label="联系电话">{{ viewData.contactPhone }}</el-descriptions-item>
<el-descriptions-item label="排除节假日"> <el-descriptions-item label="容纳人数">{{ viewData.maxNum }}</el-descriptions-item>
<span v-if="viewData.filterHolidays">是</span> <el-descriptions-item label="场地类型">{{ viewData.typeName }}</el-descriptions-item>
<span v-else>否</span> <el-descriptions-item label="场地介绍" :span="2">
</el-descriptions-item> <div v-if="viewData.introduce" v-html="viewData.introduce"></div>
<el-descriptions-item label="禁用时间" :span="2"> <div v-else>暂无场地介绍</div>
<el-table v-if="viewData.notApplyTimeList && viewData.notApplyTimeList.length > 0" </el-descriptions-item>
:data="viewData.notApplyTimeList" max-height="300" size="mini"> <el-descriptions-item label="场地照片" :span="2">
<el-table-column prop="date" label="日期"></el-table-column> <el-image
<el-table-column prop="startTime" label="开始时间"></el-table-column> v-if="viewData.sitePhoto"
<el-table-column prop="endTime" label="结束时间"></el-table-column> :preview-src-list="[viewData.sitePhoto]"
</el-table> :src="viewData.sitePhoto"
<span v-else>暂无禁用时间</span> fit="cover"
</el-descriptions-item> style="width: 160px; height: 160px; border-radius: 4px;">
<el-descriptions-item label="性别限制"> </el-image>
<span v-if="viewData.sexLimit === 0">不限制</span> <div v-else>暂无场地照片</div>
<span v-if="viewData.sexLimit === 1">男</span> </el-descriptions-item>
<span v-if="viewData.sexLimit === 2">女</span> </el-descriptions>
</el-descriptions-item> </el-tab-pane>
<el-descriptions-item label="开启状态">
<span v-if="viewData.state">开启</span> <el-tab-pane label="设置场地" name="siteSetting">
<span v-else>禁用</span> <el-descriptions :column="2" border>
</el-descriptions-item> <el-descriptions-item label="开启状态">
<el-descriptions-item label="场地介绍" :span="2"> <span v-if="viewData.state">开启</span>
<div v-if="viewData.introduce" v-html="viewData.introduce"></div> <span v-else>禁用</span>
<div v-else>暂无场地介绍</div> </el-descriptions-item>
</el-descriptions-item> <el-descriptions-item label="性别限制">
</el-descriptions> <span v-if="viewData.sexLimit === 0">不限制</span>
<span v-else-if="viewData.sexLimit === 1">男</span>
<span v-else-if="viewData.sexLimit === 2">女</span>
<span v-else>--</span>
</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 v-if="viewData.reserveTimeType === 1">分段预约</span>
<span v-else-if="viewData.reserveTimeType === 2">全天候预约</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="2">
<div v-if="viewData.reserveTimeType === 1">
<el-table v-if="viewData.openHours && viewData.openHours.length > 0"
:data="viewData.openHours" max-height="300" size="mini">
<el-table-column type="index" label="序号" width="80"></el-table-column>
<el-table-column prop="startTime" label="开始时间"></el-table-column>
<el-table-column prop="endTime" label="结束时间"></el-table-column>
<el-table-column prop="timeUnit" label="预约时间单位(分钟)"></el-table-column>
</el-table>
<span v-else>暂无场次信息</span>
</div>
<div v-else-if="viewData.reserveTimeType === 2">
<div v-if="viewData.openHours && viewData.openHours.length > 0">
<div><span>开始时间:</span><span>{{ viewData.openHours[0].startTime || '--' }}</span></div>
<div><span>结束时间:</span><span>{{ viewData.openHours[0].endTime || '--' }}</span></div>
<div><span>预约时间单位(分钟):</span><span>{{ viewData.openHours[0].timeUnit || '--' }}</span></div>
</div>
<span v-else>暂无场次信息</span>
</div>
<span v-else>暂无场次信息</span>
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
</div> </div>
`, `,
data() { data() {
return { return {
activeTab: 'basicInfo',
viewData: {}, viewData: {},
} }
}, },
methods: { methods: {
onOpen(row) { onOpen(row) {
this.activeTab = 'basicInfo'
this.viewData = row this.viewData = row
}, },
}, },
@@ -1,23 +1,55 @@
<!--# <!--#
layout("/layouts/platform.html"){ layout("/layouts/platform.html"){
#--> #-->
<style>
.mine-batch-toggle {
padding: 0;
color: #409EFF;
font-size: 16px;
}
.mine-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
}
.mine-batch-child-row {
background: #fafcff;
}
.mine-batch-child-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #7a8a9a;
}
.mine-batch-child-label::before {
content: "";
width: 16px;
height: 1px;
background: #c8d3df;
}
</style>
<div id="app" v-cloak> <div id="app" v-cloak>
<guava ref="guava"> <guava ref="guava">
<template> <template>
<el-card shadow="never"> <el-card shadow="never">
<search @search="doSearch"> <search @search="doSearch">
<search-item label="姓名/工号/场地"> <search-item label="姓名/工号">
<el-input placeholder="请输入姓名/工号/场地" clearable v-model="pageForm.searchKeyword" <el-input placeholder="请输入姓名工号" clearable v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch"> @keyup.enter.native="doSearch">
</el-input> </el-input>
</search-item> </search-item>
<search-item label="活动场地"> <search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%" placeholder="请选择活动场地" filterable clearable> <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-option v-for="item in siteOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
</el-select> </el-select>
</search-item> </search-item>
<search-item label="场地类型"> <search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%" placeholder="请选择场地类型" filterable clearable> <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-option v-for="item in typeOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
</el-select> </el-select>
</search-item> </search-item>
@@ -26,33 +58,46 @@ layout("/layouts/platform.html"){
<el-card shadow="never"> <el-card shadow="never">
<table-tool label="申请列表"></table-tool> <table-tool label="申请列表"></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%"> <el-table :data="tableData" @sort-change="pageOrder" :row-class-name="tableRowClassName" 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="mine-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="mine-batch-empty"></span>
</template>
</el-table-column>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column> <el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column <el-table-column
v-for="column in tableColumns"
:key="column.prop"
:label="column.label" :label="column.label"
:prop="column.prop" :prop="column.prop"
:key="column.prop"
:width="column.width" :width="column.width"
:sortable="column.sortable" :sortable="column.sortable"
align="center" align="center"
header-align="center" header-align="center"
show-overflow-tooltip show-overflow-tooltip
v-for="column in tableColumns"
> >
<template v-slot="{ row }" v-if="column.prop === 'instanceState'"> <template v-slot="{ row }" v-if="column.prop === 'siteName'">
<span v-if="row._isBatchChild" class="mine-batch-child-label">{{ row.siteName }}</span>
<span v-else>{{ row.siteName }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag> <enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template> </template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveType'"> <template v-slot="{ row }" v-else-if="column.prop === 'yearlyBatch'">
<span>{{ row.reserveType === 'club' ? '协会预约' : '分工会预约' }}</span> <span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" fixed="right" width="180"> <el-table-column label="操作" fixed="right" width="180">
<template v-slot="{row}"> <template v-slot="{ row }">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button> <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> <el-button v-if="row.canCancel && !row._isBatchChild" @click="onDelete(row)" size="mini" type="danger">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -79,29 +124,85 @@ layout("/layouts/platform.html"){
return { return {
typeOptions: [], typeOptions: [],
siteOptions: [], siteOptions: [],
rawTableData: [],
expandedBatchKeys: {},
tableColumns: [ tableColumns: [
{prop: 'siteName', label: '场地名称'}, {prop: 'siteName', label: '场地名称', sortable: 'custom'},
{prop: 'applyUserName', label: '预约人'}, {prop: 'applyUserName', label: '预约人', sortable: 'custom'},
{prop: 'reserveType', label: '预约类型'}, {prop: 'applyLoginName', label: '工号', sortable: 'custom'},
{prop: 'reserveTargetName', label: '预约单位'}, {prop: 'yearlyBatch', label: '是否批量预约', sortable: 'custom', width: 130},
{prop: 'applyUnitName', label: '所属单位'}, {prop: 'reserveTargetName', label: '预约单位', sortable: 'custom'},
{prop: 'reserveStartTime', label: '开始时间'}, {prop: 'reserveStartTime', label: '开始时间', sortable: 'custom'},
{prop: 'reserveEndTime', label: '结束时间'}, {prop: 'reserveEndTime', label: '结束时间', sortable: 'custom'},
{prop: 'applyMobile', label: '联系方式'}, {prop: 'applyMobile', label: '联系方式', sortable: 'custom'},
{prop: 'taskName', label: '当前节点'}, {prop: 'taskName', label: '当前节点', sortable: 'custom'},
{prop: 'instanceState', label: '流程状态'}, {prop: 'instanceState', label: '流程状态', sortable: 'custom', width: 120},
], ],
} }
}, },
methods: { methods: {
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
const batchNo = row.backOption || row.id
return 'batch_' + batchNo + '_' + (row.applyUserName || '') + '_' + (row.siteName || '')
},
buildTableData(rows) {
const groups = new Map()
;(rows || []).forEach(row => {
const key = this.getBatchGroupKey(row)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({...row})
})
const displayRows = []
groups.forEach((groupRows, key) => {
const sortedRows = groupRows.slice().sort((a, b) => {
return (b.reserveStartTime || '').localeCompare(a.reserveStartTime || '')
})
const parent = {...sortedRows[0]}
const children = sortedRows.slice(1).map(item => ({
...item,
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
}))
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
displayRows.push(...children)
}
})
return displayRows
},
tableRowClassName({ row }) {
return row && row._isBatchChild ? 'mine-batch-child-row' : ''
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.tableData = this.buildTableData(this.rawTableData)
},
onView(row) { onView(row) {
this.$refs.guava.edit(() => { this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row) this.$refs.infoRef.onOpen(row)
}) })
}, },
onDelete(row) { onDelete(row) {
const message = row.yearlyBatch const message = this.normalizeBatchFlag(row)
? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || 0) + ' 条预约记录,是否继续?') ? ('该记录属于“预约本年”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || row._batchCount || 0) + ' 条预约记录,是否继续?')
: '您确定要删除吗?' : '您确定要删除吗?'
this.$confirm(message, '提示', { this.$confirm(message, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
@@ -126,6 +227,15 @@ layout("/layouts/platform.html"){
this.siteOptions = res.data this.siteOptions = res.data
}) })
}, },
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.pageForm.totalCount = res.data.totalCount
}
})
},
}, },
async created() { async created() {
this.querySiteType() this.querySiteType()
@@ -1,64 +1,111 @@
<!--# <!--#
layout("/layouts/platform.html"){ layout("/layouts/platform.html"){
#--> #-->
<style>
.record-batch-toggle {
padding: 0;
color: #409EFF;
font-size: 16px;
}
.record-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
}
.record-batch-child-row {
background: #fafcff;
}
.record-batch-child-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #7a8a9a;
}
.record-batch-child-label::before {
content: "";
width: 16px;
height: 1px;
background: #c8d3df;
}
</style>
<div id="app" v-cloak> <div id="app" v-cloak>
<guava ref="guava"> <guava ref="guava">
<template> <template>
<el-card shadow="never"> <el-card shadow="never">
<search @search="doSearch"> <search @search="doSearch">
<search-item label="姓名/工号/场地"> <search-item label="姓名/工号">
<el-input placeholder="请输入姓名/工号/场地" clearable v-model="pageForm.searchKeyword" <el-input
@keyup.enter.native="doSearch"> v-model="pageForm.searchKeyword"
placeholder="请输入姓名或工号"
clearable
@keyup.enter.native="doSearch">
</el-input> </el-input>
</search-item> </search-item>
<search-item label="活动场地"> <search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%" <el-select
placeholder="请选择活动场地" filterable clearable> v-model="pageForm.siteId"
<el-option v-for="item in siteOptions" @change="doSearch"
:value="item.id" style="width: 100%"
:key="item.id" placeholder="请选择活动场地"
:label="item.name"></el-option> filterable
clearable>
<el-option
v-for="item in siteOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select> </el-select>
</search-item> </search-item>
<search-item label="场地类型"> <search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%" <el-select
placeholder="请选择场地类型" filterable clearable> v-model="pageForm.siteType"
<el-option v-for="item in typeOptions" @change="doSearch"
:value="item.id" style="width: 100%"
:key="item.id" placeholder="请选择场地类型"
:label="item.name"></el-option> filterable
</el-select> clearable>
</search-item> <el-option
<search-item label="预约类型"> v-for="item in typeOptions"
<el-select v-model="pageForm.reserveType" @change="doSearch" style="width: 100%" :key="item.id"
placeholder="请选择预约类型" clearable> :label="item.name"
<el-option label="分工会预约" value="union"></el-option> :value="item.id">
<el-option label="协会预约" value="club"></el-option> </el-option>
</el-select> </el-select>
</search-item> </search-item>
<search-item label="预约单位"> <search-item label="预约单位">
<el-input placeholder="请输入分工会或协会名称" clearable v-model="pageForm.reserveTargetKeyword" <el-input
@keyup.enter.native="doSearch"> v-model="pageForm.reserveTargetKeyword"
placeholder="请输入分工会或协会名称"
clearable
@keyup.enter.native="doSearch">
</el-input> </el-input>
</search-item> </search-item>
<search-item label="预约开始时间"> <search-item label="预约开始时间">
<el-date-picker v-model="pageForm.reserveTimeStart" <el-date-picker
type="datetime" v-model="pageForm.reserveTimeStart"
value-format="yyyy-MM-dd HH:mm:ss" type="datetime"
placeholder="请选择预约开始时间" value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%" placeholder="请选择预约开始时间"
clearable style="width: 100%"
@change="doSearch"> clearable
@change="doSearch">
</el-date-picker> </el-date-picker>
</search-item> </search-item>
<search-item label="预约结束时间"> <search-item label="预约结束时间">
<el-date-picker v-model="pageForm.reserveTimeEnd" <el-date-picker
type="datetime" v-model="pageForm.reserveTimeEnd"
value-format="yyyy-MM-dd HH:mm:ss" type="datetime"
placeholder="请选择预约结束时间" value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%" placeholder="请选择预约结束时间"
clearable style="width: 100%"
@change="doSearch"> clearable
@change="doSearch">
</el-date-picker> </el-date-picker>
</search-item> </search-item>
</search> </search>
@@ -68,27 +115,47 @@ layout("/layouts/platform.html"){
<table-tool label="申请列表"> <table-tool label="申请列表">
<el-button type="primary" size="small" @click="doExport">导出表格</el-button> <el-button type="primary" size="small" @click="doExport">导出表格</el-button>
</table-tool> </table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%"> <el-table
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column> :data="tableData"
:row-class-name="tableRowClassName"
@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 type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column <el-table-column
v-for="column in tableColumns"
:key="column.prop"
:label="column.label" :label="column.label"
:prop="column.prop" :prop="column.prop"
:key="column.prop"
:width="column.width" :width="column.width"
:sortable="column.sortable" :sortable="column.sortable"
align="center" align="center"
header-align="center" header-align="center"
show-overflow-tooltip show-overflow-tooltip>
v-for="column in tableColumns"> <template v-slot="{ row }" v-if="column.prop === 'siteName'">
<template v-slot="{ row }" v-if="column.prop === 'reserveType'"> <span v-if="row._isBatchChild" class="record-batch-child-label">{{ row.siteName }}</span>
<span>{{ row.reserveType === 'club' ? '协会预约' : '分工会预约' }}</span> <span v-else>{{ row.siteName }}</span>
</template> </template>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'"> <template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span> <span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template> </template>
<template v-slot="{ row }" v-else-if="column.prop === 'yearlyBatch'">
<span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
</el-table-column> </el-table-column>
<el-table-column label="操作" fixed="right" width="180"> <el-table-column label="操作" fixed="right" width="180">
<template v-slot="{row}"> <template v-slot="{ row }">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button> <el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onDelete(row.id)" size="mini" type="danger">删除</el-button> <el-button @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
</template> </template>
@@ -111,7 +178,7 @@ layout("/layouts/platform.html"){
store, store,
mixins: [initTableMixins], mixins: [initTableMixins],
components: { components: {
'info': siteCugApplyInfo, info: siteCugApplyInfo,
}, },
data() { data() {
return { return {
@@ -126,19 +193,75 @@ layout("/layouts/platform.html"){
}, },
typeOptions: [], typeOptions: [],
siteOptions: [], siteOptions: [],
rawTableData: [],
expandedBatchKeys: {},
tableColumns: [ tableColumns: [
{prop: 'siteName', label: '场地名称'}, { prop: 'siteName', label: '场地名称', sortable: 'custom' },
{prop: 'applyUserName', label: '预约人'}, { prop: 'applyUserName', label: '预约人', sortable: 'custom' },
{prop: 'reserveType', label: '预约类型'}, { prop: 'applyLoginName', label: '工号', sortable: 'custom' },
{prop: 'reserveTargetName', label: '预约单位'}, { prop: 'yearlyBatch', label: '是否批量预约', sortable: 'custom', width: 130 },
{prop: 'applyUnitName', label: '所属单位'}, { prop: 'reserveTargetName', label: '预约单位', sortable: 'custom' },
{prop: 'reserveStartTime', label: '开始时间'}, { prop: 'reserveStartTime', label: '开始时间', sortable: 'custom' },
{prop: 'reserveEndTime', label: '结束时间'}, { prop: 'reserveEndTime', label: '结束时间', sortable: 'custom' },
{prop: 'applyMobile', label: '联系方式'}, { prop: 'applyMobile', label: '联系方式', sortable: 'custom' },
], ],
} }
}, },
methods: { methods: {
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
const batchNo = row.backOption || row.id
return 'batch_' + batchNo + '_' + (row.applyUserName || '') + '_' + (row.siteName || '')
},
buildTableData(rows) {
const groups = new Map()
;(rows || []).forEach(row => {
const key = this.getBatchGroupKey(row)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({ ...row })
})
const displayRows = []
groups.forEach((groupRows, key) => {
const sortedRows = groupRows.slice().sort((a, b) => {
return (b.reserveStartTime || '').localeCompare(a.reserveStartTime || '')
})
const parent = { ...sortedRows[0] }
const children = sortedRows.slice(1).map(item => ({
...item,
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
}))
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
displayRows.push(...children)
}
})
return displayRows
},
tableRowClassName({ row }) {
return row && row._isBatchChild ? 'record-batch-child-row' : ''
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.tableData = this.buildTableData(this.rawTableData)
},
onView(row) { onView(row) {
this.$refs.guava.edit(() => { this.$refs.guava.edit(() => {
this.$refs.infoRef.onOpen(row) this.$refs.infoRef.onOpen(row)
@@ -148,7 +271,7 @@ layout("/layouts/platform.html"){
this.$confirm('您确定要删除吗?', '提示', { this.$confirm('您确定要删除吗?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning',
}).then(() => { }).then(() => {
this.$axios.post('/platform/siteCug/record/delete', { id }).then((res) => { this.$axios.post('/platform/siteCug/record/delete', { id }).then((res) => {
if (res.code === 0) { if (res.code === 0) {
@@ -158,7 +281,6 @@ layout("/layouts/platform.html"){
}) })
}) })
}, },
// 导出内容和列表当前筛选条件保持一致,实现所见即所得
doExport() { doExport() {
this.$downLoad('/platform/siteCug/record/doExport', this.pageForm) this.$downLoad('/platform/siteCug/record/doExport', this.pageForm)
}, },
@@ -172,13 +294,21 @@ layout("/layouts/platform.html"){
this.siteOptions = res.data this.siteOptions = res.data
}) })
}, },
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.pageForm.totalCount = res.data.totalCount
}
})
},
}, },
created() { created() {
// 查询字段在 data 中一次性声明完整,避免 Vue 2 对后加属性渲染不稳定
this.querySiteType() this.querySiteType()
this.querySites() this.querySites()
this.pageData() this.pageData()
} },
}) })
</script> </script>
@@ -1,35 +1,81 @@
<!--# <!--#
layout("/layouts/platform.html"){ layout("/layouts/platform.html"){
#--> #-->
<style>
.audit-batch-toggle {
padding: 0;
color: #409EFF;
font-size: 16px;
}
.audit-batch-empty {
display: inline-block;
width: 16px;
height: 16px;
}
.audit-batch-child-row {
background: #fafcff;
}
.audit-batch-child-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #7a8a9a;
}
.audit-batch-child-label::before {
content: "";
width: 16px;
height: 1px;
background: #c8d3df;
}
</style>
<div id="app"> <div id="app">
<guava ref="guava"> <guava ref="guava">
<el-card shadow="never"> <el-card shadow="never">
<search @search="doSearch"> <search @search="doSearch">
<search-item label="姓名/工号/场地"> <search-item label="姓名/工号">
<el-input placeholder="请输入姓名/工号/场地" clearable v-model="pageForm.searchKeyword" <el-input
@keyup.enter.native="doSearch"> v-model="pageForm.searchKeyword"
placeholder="请输入姓名或工号"
clearable
@keyup.enter.native="doSearch">
</el-input> </el-input>
</search-item> </search-item>
<search-item label="活动场地"> <search-item label="活动场地">
<el-select v-model="pageForm.siteId" @change="doSearch" style="width: 100%" <el-select
placeholder="请选择活动场地" filterable clearable> v-model="pageForm.siteId"
<el-option v-for="item in siteOptions" @change="doSearch"
:value="item.id" style="width: 100%"
:key="item.id" placeholder="请选择活动场地"
:label="item.name" filterable
></el-option> clearable>
<el-option
v-for="item in siteOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select> </el-select>
</search-item> </search-item>
<search-item label="场地类型"> <search-item label="场地类型">
<el-select v-model="pageForm.siteType" @change="doSearch" style="width: 100%" <el-select
placeholder="请选择场地类型" filterable clearable> v-model="pageForm.siteType"
<el-option v-for="item in typeOptions" @change="doSearch"
:value="item.id" style="width: 100%"
:key="item.id" placeholder="请选择场地类型"
:label="item.name" filterable
></el-option> clearable>
<el-option
v-for="item in typeOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select> </el-select>
</search-item> </search-item>
</search> </search>
@@ -42,34 +88,53 @@ layout("/layouts/platform.html"){
<el-radio-button :label="false">未审核</el-radio-button> <el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group> </el-radio-group>
</table-tool> </table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%"> <el-table
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column> :data="tableData"
:row-class-name="tableRowClassName"
@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="audit-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="audit-batch-empty"></span>
</template>
</el-table-column>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column <el-table-column
v-for="column in tableColumns"
:key="column.prop"
:label="column.label" :label="column.label"
:prop="column.prop" :prop="column.prop"
:key="column.prop"
:width="column.width" :width="column.width"
:sortable="column.sortable" :sortable="column.sortable"
align="center" align="center"
header-align="center" header-align="center"
show-overflow-tooltip show-overflow-tooltip>
v-for="column in tableColumns" <template v-slot="{ row }" v-if="column.prop === 'siteName'">
> <span v-if="row._isBatchChild" class="audit-batch-child-label">{{ row.siteName }}</span>
<template v-slot="{ row }" v-if="column.prop === 'instanceState'"> <span v-else>{{ row.siteName }}</span>
<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>
<template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'"> <template v-slot="{ row }" v-else-if="column.prop === 'reserveTargetName'">
<span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span> <span>{{ row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-') }}</span>
</template> </template>
<template v-slot="{ row }" v-else-if="column.prop === 'yearlyBatch'">
<span>{{ normalizeBatchFlag(row) ? '是' : '否' }}</span>
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'instanceState'">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column> </el-table-column>
<el-table-column label="操作" fixed="right" width="300px"> <el-table-column label="操作" fixed="right" width="220">
<template v-slot="{row}"> <template v-slot="{ row }">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button> <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="!row._isBatchChild && 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> <el-button v-if="!row._isBatchChild && canRevoke(row)" :loading="revokeLoading" :disabled="revokeLoading" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -80,12 +145,13 @@ layout("/layouts/platform.html"){
<info ref="infoRef"> <info ref="infoRef">
<div v-if="showApprovalForm"> <div v-if="showApprovalForm">
<div class="process-title"> <div class="process-title">
{{formData.taskName}} {{ formData.taskName }}
</div> </div>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix="" <el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" class="flow-task-form">
class="flow-task-form"> <el-form-item
<el-form-item label="审批意见" prop="tf_opinion" label="审批意见"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]"> prop="tf_opinion"
:rules="[{ required: true, message: '必填', trigger: ['change', 'blur'] }]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea> <user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -105,28 +171,30 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}"> <script nonce="${cspNonce!}">
<!--#include('../common/applyInfo.js'){}#--> <!--#include('../common/applyInfo.js'){}#-->
new Vue({ new Vue({
el: "#app", el: '#app',
store, store,
mixins: [initTableMixins], mixins: [initTableMixins],
components: { components: {
"info": siteCugApplyInfo, info: siteCugApplyInfo,
}, },
data() { data() {
return { return {
pageForm: { pageForm: {
approval: false approval: false,
}, },
rawTableData: [],
expandedBatchKeys: {},
tableColumns: [ tableColumns: [
{prop: 'siteName', label: '场地名称'}, { prop: 'siteName', label: '场地名称', sortable: 'custom' },
{prop: 'applyUserName', label: '预约人'}, { prop: 'applyUserName', label: '预约人', sortable: 'custom' },
{prop: 'reserveType', label: '预约类型'}, { prop: 'applyLoginName', label: '工号', sortable: 'custom' },
{prop: 'reserveTargetName', label: '预约单位'}, { prop: 'yearlyBatch', label: '是否批量预约', sortable: 'custom', width: 130 },
{prop: 'applyUnitName', label: '所属单位'}, { prop: 'reserveTargetName', label: '预约单位', sortable: 'custom' },
{prop: 'reserveStartTime', label: '开始时间'}, { prop: 'reserveStartTime', label: '开始时间', sortable: 'custom' },
{prop: 'reserveEndTime', label: '结束时间'}, { prop: 'reserveEndTime', label: '结束时间', sortable: 'custom' },
{prop: 'applyMobile', label: '联系方式'}, { prop: 'applyMobile', label: '联系方式', sortable: 'custom' },
{prop: 'curTaskName', label: '当前节点'}, { prop: 'curTaskName', label: '当前节点', sortable: 'custom' },
{prop: 'instanceState', label: '流程状态'}, { prop: 'instanceState', label: '流程状态', sortable: 'custom', width: 120 },
], ],
typeOptions: [], typeOptions: [],
siteOptions: [], siteOptions: [],
@@ -138,6 +206,60 @@ layout("/layouts/platform.html"){
} }
}, },
methods: { methods: {
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
const batchNo = row.backOption || row.id
return 'batch_' + batchNo + '_' + (row.applyUserName || '') + '_' + (row.siteName || '')
},
buildTableData(rows) {
const groups = new Map()
;(rows || []).forEach(row => {
const key = this.getBatchGroupKey(row)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key).push({ ...row })
})
const displayRows = []
groups.forEach((groupRows, key) => {
const sortedRows = groupRows.slice().sort((a, b) => {
return (b.reserveStartTime || '').localeCompare(a.reserveStartTime || '')
})
const parent = { ...sortedRows[0] }
const children = sortedRows.slice(1).map(item => ({
...item,
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
}))
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
displayRows.push(...children)
}
})
return displayRows
},
tableRowClassName({ row }) {
return row && row._isBatchChild ? 'audit-batch-child-row' : ''
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.tableData = this.buildTableData(this.rawTableData)
},
canRevoke(row) { canRevoke(row) {
return Number(row.instanceState) === 20 return Number(row.instanceState) === 20
}, },
@@ -156,7 +278,7 @@ layout("/layouts/platform.html"){
taskName: row.curTaskName, taskName: row.curTaskName,
tf_opinion: '', tf_opinion: '',
yearlyBatch: row.yearlyBatch, yearlyBatch: row.yearlyBatch,
batchAuditCount: row.batchAuditCount, batchAuditCount: row.batchAuditCount || row._batchCount,
} }
this.$refs.infoRef.onOpen(row) this.$refs.infoRef.onOpen(row)
}) })
@@ -171,14 +293,14 @@ layout("/layouts/platform.html"){
this.$confirm(message, '提示', { this.$confirm(message, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning',
}).then(() => { }).then(() => {
this.auditLoading = true this.auditLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/executeTask', { this.$axios.post('/platform/siteCug/schoolUnionAudit/executeTask', {
data: JSON.stringify({ data: JSON.stringify({
...this.formData, ...this.formData,
submitType: val submitType: val,
}) }),
}).then((res) => { }).then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.$refs.guava.index() this.$refs.guava.index()
@@ -194,13 +316,13 @@ layout("/layouts/platform.html"){
if (!this.canRevoke(row) || this.revokeLoading) { if (!this.canRevoke(row) || this.revokeLoading) {
return return
} }
const message = row.yearlyBatch const message = this.normalizeBatchFlag(row)
? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || 0) + ' 条审核任务,是否继续?') ? ('该记录属于“预约本年”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || row._batchCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?' : '您确定要撤回吗?'
this.$confirm(message, '提示', { this.$confirm(message, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'info' type: 'info',
}).then(() => { }).then(() => {
this.revokeLoading = true this.revokeLoading = true
this.$axios.post('/platform/siteCug/schoolUnionAudit/revokeTask', { taskId: row.taskId, applyId: row.id }).then((res) => { this.$axios.post('/platform/siteCug/schoolUnionAudit/revokeTask', { taskId: row.taskId, applyId: row.id }).then((res) => {
@@ -223,12 +345,21 @@ layout("/layouts/platform.html"){
this.siteOptions = res.data this.siteOptions = res.data
}) })
}, },
pageData() {
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.rawTableData = Array.isArray(res.data.list) ? res.data.list : []
this.tableData = this.buildTableData(this.rawTableData)
this.pageForm.totalCount = res.data.totalCount
}
})
},
}, },
async created() { created() {
this.querySiteType() this.querySiteType()
this.querySites() this.querySites()
this.pageData() this.pageData()
} },
}) })
</script> </script>
File diff suppressed because it is too large Load Diff
@@ -4,9 +4,304 @@ layout("/layouts/platform_h5.html"){
<style scoped> <style scoped>
.page-container { .page-container {
padding-bottom: 84px; padding: 12px 12px 96px;
background: #f7f8fa; background: #f5f7fb;
min-height: calc(100vh - 46px); min-height: calc(100vh - 46px);
box-sizing: border-box;
}
.panel-card {
margin-bottom: 12px;
overflow: hidden;
border-radius: 22px;
background: #ffffff;
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.04),
0 12px 28px rgba(15, 23, 42, 0.08),
0 0 0 1px rgba(226, 232, 240, 0.95);
}
.notice-card__header {
width: 100%;
padding: 16px;
border: none;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
box-sizing: border-box;
}
.notice-card__icon {
color: #64748b;
font-size: 16px;
transition: transform 0.2s ease;
}
.notice-card__icon--expanded {
transform: rotate(180deg);
}
.notice-card__body {
padding: 0 16px 14px;
border-top: 1px solid #eef2f7;
}
.notice-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.notice-item {
padding: 10px 12px;
border-radius: 14px;
background: #f8fafc;
}
.notice-item__label {
color: #334155;
font-size: 13px;
font-weight: 600;
line-height: 1.5;
}
.notice-item__value {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.7;
}
.disabled-time-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.disabled-time-item {
padding: 8px 10px;
border-radius: 10px;
background: #ffffff;
color: #475569;
line-height: 1.7;
}
.form-container {
margin: 0;
}
.section-tip {
padding: 0 16px 14px;
color: #94a3b8;
font-size: 12px;
line-height: 1.6;
}
.selection-summary {
margin: 0 16px 14px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 12px;
line-height: 1.7;
}
.batch-panel {
padding: 0 16px 14px;
}
.batch-summary {
margin-top: 10px;
padding: 10px 12px;
border-radius: 12px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 12px;
line-height: 1.7;
}
.availability-popup {
height: 82vh;
display: flex;
flex-direction: column;
background: #f8fafc;
}
.availability-popup__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 16px;
background: #ffffff;
border-bottom: 1px solid #eef2f7;
}
.availability-popup__title {
color: #0f172a;
font-size: 17px;
font-weight: 700;
line-height: 1.4;
}
.availability-popup__subtitle {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.6;
}
.availability-popup__close {
color: #94a3b8;
font-size: 18px;
line-height: 1;
}
.availability-popup__tabs {
display: flex;
gap: 8px;
padding: 12px 16px 0;
overflow-x: auto;
background: #f8fafc;
}
.availability-tab {
min-width: 72px;
height: 34px;
padding: 0 14px;
border: 1px solid #cbd5e1;
border-radius: 999px;
background: #ffffff;
color: #475569;
font-size: 13px;
font-weight: 500;
box-sizing: border-box;
}
.availability-tab--active {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.22);
}
.availability-popup__legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 12px 16px 0;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.availability-legend__item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.availability-legend__dot {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
}
.availability-legend__dot--available {
background: #22c55e;
}
.availability-legend__dot--reserved {
background: #9ca3af;
}
.availability-legend__dot--selected {
background: #2563eb;
}
.availability-legend__dot--closed {
background: #e5e7eb;
}
.availability-popup__body {
flex: 1;
overflow-y: auto;
padding: 16px;
box-sizing: border-box;
}
.slot-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.slot-block {
min-height: 48px;
padding: 0 12px;
border: 1px solid #dbe4ee;
border-radius: 14px;
background: #ffffff;
color: #475569;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: 13px;
font-weight: 500;
line-height: 1.4;
box-sizing: border-box;
}
.slot-block--available {
border-color: #86efac;
background: rgba(34, 197, 94, 0.1);
color: #15803d;
}
.slot-block--reserved {
background: #e5e7eb;
color: #6b7280;
}
.slot-block--closed {
background: #f8fafc;
color: #94a3b8;
}
.slot-block--selected {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.24);
}
.availability-popup__footer {
padding: 12px 16px calc(env(safe-area-inset-bottom) + 12px);
background: #ffffff;
border-top: 1px solid #eef2f7;
}
.availability-popup__summary {
margin-bottom: 12px;
color: #475569;
font-size: 12px;
line-height: 1.7;
}
.availability-popup__actions {
display: flex;
gap: 10px;
}
.availability-popup__actions .van-button {
flex: 1;
} }
.footer-actions { .footer-actions {
@@ -17,17 +312,39 @@ layout("/layouts/platform_h5.html"){
display: flex; display: flex;
gap: 12px; gap: 12px;
padding: 10px 12px calc(env(safe-area-inset-bottom) + 10px); padding: 10px 12px calc(env(safe-area-inset-bottom) + 10px);
background: #ffffff; background: rgba(255, 255, 255, 0.96);
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06); backdrop-filter: blur(10px);
box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.08);
} }
.footer-actions .van-button { .footer-actions .van-button {
flex: 1; flex: 1;
height: 44px;
} }
.disabled-time-item { /deep/ .panel-card .van-cell-group {
line-height: 20px; background: transparent;
margin-bottom: 4px; }
/deep/ .panel-card .van-cell-group__title {
padding: 16px 16px 8px;
margin: 0;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
background: #ffffff;
}
/deep/ .panel-card .van-cell {
padding-top: 12px;
padding-bottom: 12px;
}
/deep/ .panel-card .van-field__label,
/deep/ .panel-card .van-cell__title {
color: #334155;
font-weight: 600;
} }
/deep/ .direction-column-cell .van-cell__value { /deep/ .direction-column-cell .van-cell__value {
@@ -40,46 +357,43 @@ layout("/layouts/platform_h5.html"){
<van-nav-bar title="场馆申请" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar> <van-nav-bar title="场馆申请" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="page-container" v-if="siteLoaded"> <div class="page-container" v-if="siteLoaded">
<van-cell-group title="场馆信息"> <div class="panel-card">
<van-cell title="场馆名称" :value="row.name || '-' "></van-cell> <button type="button" class="notice-card__header" @click="noticeCollapsed = !noticeCollapsed">
<van-cell title="场地地址" class="direction-column-cell"> <span>预约须知</span>
<template #default> <van-icon :class="['notice-card__icon', noticeCollapsed ? '' : 'notice-card__icon--expanded']" name="arrow-down"></van-icon>
{{ row.address || '-' }} </button>
</template> <div v-if="!noticeCollapsed" class="notice-card__body">
</van-cell> <div class="notice-list">
<van-cell title="联系人" :value="row.contactName || '-' "></van-cell> <div class="notice-item">
<van-cell title="联系电话" :value="row.contactPhone || '-' "></van-cell> <div class="notice-item__label">可预约日期</div>
<van-cell title="场地类型" :value="row.typeName || '-' "></van-cell> <div class="notice-item__value">仅限工作日</div>
</van-cell-group> </div>
<div class="notice-item">
<van-cell-group title="预约须知"> <div class="notice-item__label">节假日限制</div>
<van-cell title="可预约日期" value="仅限工作日"></van-cell> <div class="notice-item__value">{{ timeLimitConfig.filterHolidays ? '节假日不可预约' : '不排除节假日' }}</div>
<van-cell title="节假日限制" </div>
:value="timeLimitConfig.filterHolidays ? '节假日不可预约' : '不排除节假日'"></van-cell> <div class="notice-item">
<van-cell title="禁用时段" class="direction-column-cell"> <div class="notice-item__label">禁用时段</div>
<template #default> <div class="notice-item__value">
<div v-if="timeLimitConfig.notApplyTimeList.length"> <div v-if="timeLimitConfig.notApplyTimeList.length" class="disabled-time-list">
<div <div
class="disabled-time-item" class="disabled-time-item"
v-for="(item, index) in timeLimitConfig.notApplyTimeList" v-for="(item, index) in timeLimitConfig.notApplyTimeList"
:key="item.date + item.startTime + item.endTime + index"> :key="item.date + item.startTime + item.endTime + index">
{{ item.date }} {{ item.startTime }} - {{ item.endTime }} {{ item.date }} {{ item.startTime }} - {{ item.endTime }}
</div>
</div>
<div v-else>暂无禁用时段</div>
</div> </div>
</div> </div>
<div v-else>暂无禁用时段</div> </div>
</template> </div>
</van-cell> </div>
</van-cell-group>
<van-form ref="formRef" class="form-container" :show-error-message="false"> <van-form ref="formRef" class="form-container" :show-error-message="false">
<van-cell-group title="申请信息"> <div class="panel-card">
<van-field v-model="formData.applyUserName" label="预约人" name="applyUserName" readonly <van-cell-group title="申请信息">
:rules="[{ required: true, message: '请确认预约人' }]"></van-field> <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" :value="reserveTypeText"
label="预约类型" label="预约类型"
name="reserveType" name="reserveType"
@@ -88,18 +402,24 @@ layout("/layouts/platform_h5.html"){
is-link is-link
required required
placeholder="请选择预约类型" placeholder="请选择预约类型"
@click="showReserveTypePicker = true" @click="openReserveTypePicker"
:rules="[{ required: true, message: '请选择预约类型' }]"> :rules="[{ required: true, message: '请选择预约类型' }]">
</van-field> </van-field>
<van-field <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
v-if="formData.reserveType === 'union'" v-if="formData.reserveType === 'union'"
v-model="formData.applyUnionName" v-model="formData.applyUnionName"
label="分工会" label="分工会"
name="applyUnionName" name="applyUnionName"
readonly readonly
placeholder="自动读取当前登录人的分工会"> placeholder="自动读取当前登录人的分工会">
</van-field> </van-field>
<van-field <van-field
v-if="formData.reserveType === 'club'" v-if="formData.reserveType === 'club'"
v-model="formData.clubName" v-model="formData.clubName"
label="协会" label="协会"
@@ -111,21 +431,58 @@ layout("/layouts/platform_h5.html"){
placeholder="请选择您管理的协会" placeholder="请选择您管理的协会"
@click="openClubPicker" @click="openClubPicker"
:rules="[{ required: true, message: '请选择您管理的协会' }]"> :rules="[{ required: true, message: '请选择您管理的协会' }]">
</van-field> </van-field>
<van-field v-model="formData.applyMobile" label="联系电话" name="applyMobile" type="tel" maxlength="11" <van-field v-model="formData.applyMobile" label="联系电话" name="applyMobile" type="tel" maxlength="11"
required placeholder="请输入联系电话" required placeholder="请输入联系电话"
:rules="[{ required: true, message: '请输入联系电话' }]"></van-field> :rules="[{ required: true, message: '请输入联系电话' }]"></van-field>
<van-field v-model="formData.reserveStartTime" label="开始时间" name="reserveStartTime" readonly <van-field
clickable is-link required placeholder="请选择预约开始时间" :value="scheduleDate"
@click="openTimePicker('reserveStartTime')" label="预约日期"
:rules="[{ required: true, message: '请选择预约开始时间' }]"></van-field> name="scheduleDate"
<van-field v-model="formData.reserveEndTime" label="结束时间" name="reserveEndTime" readonly clickable readonly
is-link required placeholder="请选择预约结束时间" @click="openTimePicker('reserveEndTime')" clickable
:rules="[{ required: true, message: '请选择预约结束时间' }]"></van-field> is-link
<van-field v-model="formData.applyCause" label="预约事由" name="applyCause" required rows="4" autosize required
type="textarea" maxlength="1000" show-word-limit placeholder="请输入预约事由" placeholder="请选择预约日期"
:rules="[{ required: true, message: '请输入预约事由' }]"></van-field> @click="openScheduleDatePicker">
</van-cell-group> </van-field>
<van-field
:value="selectionSummary || ''"
label="预约时段"
name="timeSelection"
readonly
clickable
is-link
required
:placeholder="scheduleDate ? '请选择预约时段' : '请先选择预约日期'"
@click="openAvailabilityPopup">
</van-field>
<div v-if="selectionSummary" class="selection-summary">当前已选:{{ selectionSummary }}</div>
<van-cell title="批量预约">
<template #default>
<van-switch v-model="yearlyReserve" size="22px"></van-switch>
</template>
</van-cell>
<div v-if="yearlyReserve" class="batch-panel">
<div class="section-tip">将自动预约截止日期内每周同一时段,默认到本月底。</div>
<van-field
:value="formData.yearlyReserveEndDate"
label="截至日期"
name="yearlyReserveEndDate"
readonly
clickable
is-link
required
placeholder="请选择批量预约截止日期"
@click="openYearlyEndDatePicker">
</van-field>
<div v-if="batchReserveSummary" class="batch-summary">{{ batchReserveSummary }}</div>
</div>
<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>
</div>
</van-form> </van-form>
</div> </div>
@@ -144,17 +501,76 @@ layout("/layouts/platform_h5.html"){
@cancel="showClubPicker = false"></van-picker> @cancel="showClubPicker = false"></van-picker>
</van-popup> </van-popup>
<van-popup v-model="showTimePicker" position="bottom" round> <van-popup v-model="showScheduleDatePicker" position="bottom" round>
<van-picker <van-datetime-picker
ref="timePickerRef" v-model="schedulePickerDate"
show-toolbar type="date"
value-key="text" title="选择预约日期"
:title="timePickerTitle" :min-date="scheduleMinDate"
:columns="timePickerColumns" :max-date="scheduleMaxDate"
@change="onTimePickerChange" @confirm="onScheduleDateConfirm"
@confirm="onTimeConfirm" @cancel="showScheduleDatePicker = false">
@cancel="timePickerSyncing = false; showTimePicker = false"> </van-datetime-picker>
</van-picker> </van-popup>
<van-popup v-model="showAvailabilityPopup" position="bottom" round class="availability-popup">
<div class="availability-popup__header">
<div>
<div class="availability-popup__title">选择预约时段</div>
<div class="availability-popup__subtitle">{{ scheduleDate || '请选择预约日期' }} {{ reserveModeText }}</div>
</div>
<van-icon class="availability-popup__close" name="cross" @click="showAvailabilityPopup = false"></van-icon>
</div>
<div v-if="timeGroupTabs.length" class="availability-popup__tabs">
<button
v-for="tab in timeGroupTabs"
:key="tab.key"
type="button"
:class="['availability-tab', activeTimeGroupKey === tab.key ? 'availability-tab--active' : '']"
@click="activeTimeGroupKey = tab.key">
{{ tab.label }}
</button>
</div>
<div class="availability-popup__legend">
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--available"></i>可预约</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--reserved"></i>已预约</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--selected"></i>已选择</span>
<span class="availability-legend__item"><i class="availability-legend__dot availability-legend__dot--closed"></i>不可预约</span>
</div>
<div class="availability-popup__body">
<van-loading v-if="availabilityLoading" vertical>时段加载中</van-loading>
<van-empty v-else-if="!availabilityBlocks.length" description="当前日期暂无可选时段"></van-empty>
<van-empty v-else-if="!displayedAvailabilityBlocks.length" description="当前标签暂无可选时段"></van-empty>
<div v-else class="slot-grid">
<button
v-for="block in displayedAvailabilityBlocks"
:key="block.key"
type="button"
:class="getBlockClass(block)"
@click="onAvailabilityBlockClick(block)">
<span>{{ block.label }}</span>
</button>
</div>
</div>
<div class="availability-popup__footer">
<div class="availability-popup__summary">{{ selectionSummary || '请选择一个或多个连续时段,不可跨越间隔选择。' }}</div>
<div class="availability-popup__actions">
<van-button plain round type="default" @click="clearSelection(true)">清空</van-button>
<van-button round type="primary" color="#246fb4" @click="confirmAvailabilitySelection">确定</van-button>
</div>
</div>
</van-popup>
<van-popup v-model="showYearlyEndDatePicker" position="bottom" round>
<van-datetime-picker
v-model="yearlyReservePickerDate"
type="date"
title="选择批量预约截止日期"
:min-date="yearlyReserveMinDate"
:max-date="yearlyReserveMaxDate"
@confirm="onYearlyReserveEndDateConfirm"
@cancel="showYearlyEndDatePicker = false">
</van-datetime-picker>
</van-popup> </van-popup>
</div> </div>
@@ -3,13 +3,311 @@ layout("/layouts/platform_h5.html"){
#--> #-->
<style scoped> <style scoped>
.site-page {
background: #f5f7fb;
min-height: 100vh;
}
.toolbar-wrap {
padding: 10px 10px 8px;
background: #f5f7fb;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.04);
}
.search-toolbar {
display: flex;
align-items: center;
gap: 8px;
}
.search-panel {
flex: 1;
min-width: 0;
border-radius: 16px;
overflow: hidden;
background: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
}
.search-panel /deep/ .van-search {
padding: 0;
background: transparent;
}
.search-panel /deep/ .van-search__content {
height: 42px;
background: #ffffff;
border-radius: 16px;
}
.toolbar-btn,
.toolbar-icon-btn {
height: 42px;
border: none;
border-radius: 14px;
background: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);
display: inline-flex;
align-items: center;
justify-content: center;
color: #2563eb;
box-sizing: border-box;
}
.toolbar-btn {
min-width: 62px;
padding: 0 14px;
font-size: 14px;
font-weight: 600;
}
.toolbar-icon-btn {
width: 42px;
font-size: 18px;
}
.selected-type-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
padding: 8px 12px;
border-radius: 14px;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-size: 13px;
line-height: 1.4;
}
.selected-type-text {
min-width: 0;
flex: 1;
padding-right: 12px;
font-weight: 500;
}
.selected-type-clear {
border: none;
padding: 0;
background: transparent;
color: #2563eb;
font-size: 13px;
font-weight: 600;
}
.type-popup {
width: calc(100vw - 48px);
max-width: 320px;
padding: 16px 16px 14px;
box-sizing: border-box;
}
.type-popup__header {
color: #111827;
font-size: 16px;
font-weight: 700;
line-height: 1.4;
}
.type-popup__list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.type-popup__item {
min-width: calc(50% - 5px);
min-height: 38px;
padding: 0 12px;
border: 1px solid #dbe4f0;
border-radius: 12px;
background: #ffffff;
color: #475569;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 13px;
line-height: 1.2;
box-sizing: border-box;
}
.type-popup__item--active {
border-color: #2563eb;
background: rgba(37, 99, 235, 0.08);
color: #2563eb;
font-weight: 600;
}
.site-card-header {
display: flex;
gap: 12px;
align-items: flex-start;
margin-bottom: 10px;
}
.site-thumb {
width: 92px;
height: 92px;
flex-shrink: 0;
overflow: hidden;
border-radius: 14px;
background: linear-gradient(135deg, #e5eefb 0%, #f5f9ff 100%);
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.06);
}
.site-thumb img,
.site-thumb .van-image {
width: 100%;
height: 100%;
display: block;
}
.site-thumb-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 12px;
text-align: center;
line-height: 1.4;
padding: 8px;
box-sizing: border-box;
}
.site-head-main {
min-width: 0;
flex: 1;
padding-top: 2px;
}
.site-name {
color: #111827;
font-size: 18px;
font-weight: 700;
line-height: 1.35;
word-break: break-all;
}
.site-address {
display: flex;
align-items: center;
gap: 4px;
margin-top: 8px;
color: #6b7280;
font-size: 13px;
line-height: 1.4;
}
.site-address i {
color: #9ca3af;
font-size: 12px;
}
.site-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.site-tag {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 24px;
padding: 0 8px;
border-radius: 8px;
border: 1px solid #93c5fd;
color: #2563eb;
font-size: 12px;
line-height: 1;
background: #ffffff;
box-sizing: border-box;
}
.site-tag--solid {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
}
.site-tag--muted {
border-color: #cbd5e1;
color: #64748b;
}
.site-timeline {
margin-top: 4px;
}
.timeline-bar {
display: grid;
grid-template-columns: repeat(48, minmax(0, 1fr));
gap: 1px;
align-items: center;
padding: 0 2px;
}
.timeline-bar__segment {
height: 4px;
border-radius: 999px;
background: #e5e7eb;
}
.timeline-bar__segment--available {
background: #22c55e;
}
.timeline-bar__segment--reserved {
background: #9ca3af;
}
.timeline-scale {
display: flex;
justify-content: space-between;
margin-top: 8px;
padding: 0 2px;
color: #64748b;
font-size: 11px;
line-height: 1;
}
/deep/ .table-list-container {
margin-top: 8px;
padding: 0 8px 12px;
}
/deep/ .table-list-container .table-list-item {
margin-bottom: 12px;
padding: 14px;
border: none;
overflow: hidden;
border-radius: 22px;
background: #ffffff;
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.05),
0 10px 24px rgba(15, 23, 42, 0.08),
0 0 0 1px rgba(226, 232, 240, 0.9);
}
/deep/ .table-list-container .table-list-item .item-actions {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #eef2f7;
}
</style> </style>
<div id="app"> <div id="app" class="site-page">
<van-nav-bar title="场馆预约" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar> <van-nav-bar title="场馆预约" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px"> <van-sticky offset-top="46px">
<div class="toolbar-wrap">
<div class="search-toolbar">
<div class="search-panel">
<van-search <van-search
v-model="pageForm.searchKeyword" v-model="pageForm.searchKeyword"
:show-action="false" :show-action="false"
@@ -17,18 +315,54 @@ layout("/layouts/platform_h5.html"){
input-align="left" input-align="left"
placeholder="请输入名称或地址搜索" placeholder="请输入名称或地址搜索"
@search="doSearch" @search="doSearch"
></van-search> ></van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false"> </div>
<van-dropdown-item v-model="pageForm.type" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item> <button type="button" class="toolbar-btn" @click="doSearch">搜索</button>
</van-dropdown-menu> <button type="button" class="toolbar-icon-btn" @click="typePopupVisible = true">
<van-icon name="apps-o"></van-icon>
</button>
</div>
<div v-if="selectedTypeText" class="selected-type-bar">
<div class="selected-type-text">已选场地类型:{{ selectedTypeText }}</div>
<button type="button" class="selected-type-clear" @click="selectType(null)">清除</button>
</div>
</div>
</van-sticky> </van-sticky>
<table-list api="/platform/siteCug/apply/pageData" :page_form.sync="pageForm" ref="tableListRef" title="name" @ready="onReady"> <table-list api="/platform/siteCug/apply/pageData" :page_form.sync="pageForm" ref="tableListRef" @ready="onReady">
<template #header="{ row }">
<div class="site-card-header">
<div class="site-thumb" @click="previewSitePhoto(row)">
<van-image v-if="row.sitePhoto" :src="row.sitePhoto" fit="cover"></van-image>
<div v-else class="site-thumb-empty">暂无场地照片</div>
</div>
<div class="site-head-main">
<div class="site-name">{{ row.name || '-' }}</div>
<div class="site-address">
<i class="fa fa-map-marker"></i>
<span>{{ row.address || '-' }}</span>
</div>
<div class="site-tags">
<span class="site-tag site-tag--solid">{{ row.typeName || '活动场地' }}</span>
<span class="site-tag">容{{ row.maxNum || '-' }}</span>
<span :class="['site-tag', row.state ? '' : 'site-tag--muted']">{{ row.state ? '已开启' : '未开启' }}</span>
</div>
</div>
</div>
</template>
<template v-slot="{ row }"> <template v-slot="{ row }">
<table-column label="场地地址">{{ row.address }}</table-column> <div class="site-timeline">
<table-column label="联系人">{{ row.contactName }}</table-column> <div class="timeline-bar">
<table-column label="联系方式">{{ row.contactPhone }}</table-column> <span
<table-column label="场地类型">{{ row.typeName }}</table-column> v-for="(segment, index) in row.timelineSegments || []"
:key="row.id + '-segment-' + index"
:class="['timeline-bar__segment', 'timeline-bar__segment--' + (segment.status || 'closed')]">
</span>
</div>
<div class="timeline-scale">
<span v-for="mark in timeMarks" :key="row.id + '-mark-' + mark">{{ mark }}</span>
</div>
</div>
</template> </template>
<template #actions="{ row }"> <template #actions="{ row }">
<div class="action-btn" @click="onView(row)"> <div class="action-btn" @click="onView(row)">
@@ -42,6 +376,21 @@ layout("/layouts/platform_h5.html"){
</template> </template>
</table-list> </table-list>
<van-popup v-model="typePopupVisible" round class="type-popup">
<div class="type-popup__header">选择场地类型</div>
<div class="type-popup__list">
<button
v-for="item in typeFilterOptions"
:key="String(item.value)"
type="button"
:class="['type-popup__item', pageForm.type === item.value ? 'type-popup__item--active' : '']"
@click="selectType(item.value)">
<span>{{ item.text }}</span>
<van-icon v-if="pageForm.type === item.value" name="success"></van-icon>
</button>
</div>
</van-popup>
<info ref="infoRef"></info> <info ref="infoRef"></info>
</div> </div>
@@ -62,11 +411,25 @@ layout("/layouts/platform_h5.html"){
searchKeyword: '', searchKeyword: '',
type: null, type: null,
}, },
typeOptions: [], typeFilterOptions: [],
typePopupVisible: false,
timeMarks: ['0', '2', '4', '6', '8', '10', '12', '14', '16', '18', '20', '22'],
} }
}, },
computed: {
selectedTypeText() {
const current = this.typeFilterOptions.find((item) => item.value === this.pageForm.type)
return current && current.value !== null ? current.text : ''
},
},
methods: { methods: {
historyBack, historyBack,
previewSitePhoto(row) {
if (!row || !row.sitePhoto) {
return
}
this.vant.ImagePreview([row.sitePhoto])
},
onView(row) { onView(row) {
this.$refs.infoRef.onOpen(row) this.$refs.infoRef.onOpen(row)
}, },
@@ -75,14 +438,14 @@ layout("/layouts/platform_h5.html"){
}, },
async onReady() { async onReady() {
const typeList = await this.querySiteType() const typeList = await this.querySiteType()
this.typeOptions = [ this.typeFilterOptions = [
{ {
text: '全部类型', text: '全部类型',
value: null, value: null,
} }
].concat(typeList.map((item) => ({ text: item.name, value: item.id }))) ].concat(typeList.map((item) => ({ text: item.name, value: item.id })))
if (this.typeOptions.length > 0) { if (this.typeFilterOptions.length > 0) {
this.pageForm.type = this.typeOptions[0].value this.pageForm.type = this.typeFilterOptions[0].value
this.doSearch() this.doSearch()
} }
}, },
@@ -90,6 +453,11 @@ layout("/layouts/platform_h5.html"){
const res = await this.$axios.post('/platform/siteCug/function/type/queryFunctionType') const res = await this.$axios.post('/platform/siteCug/function/type/queryFunctionType')
return Array.isArray(res.data) ? res.data : [] return Array.isArray(res.data) ? res.data : []
}, },
selectType(value) {
this.pageForm.type = value
this.typePopupVisible = false
this.doSearch()
},
doSearch() { doSearch() {
this.$nextTick(() => { this.$nextTick(() => {
this.pageForm.pageNumber = 1 this.pageForm.pageNumber = 1
@@ -18,14 +18,14 @@ const siteCugApplyInfoH5 = {
</van-cell-group> </van-cell-group>
<template v-for="task in doneTasks"> <template v-for="task in doneTasks">
<van-cell-group :title="task.displayName" v-if="task.ext.isFirstTaskNode" :key="task.id + '-first'"> <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.ext.initiatorName }}({{task.ext.initiatorAccount}})</van-cell>
<van-cell title="申请时间">{{ task.finishTime }}</van-cell> <van-cell title="申请时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果"> <van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag> <dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</van-cell> </van-cell>
</van-cell-group> </van-cell-group>
<van-cell-group :title="task.displayName" v-else :key="task.id + '-done'"> <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.taskFormData.userName }}({{task.taskFormData.loginName}})</van-cell>
<van-cell title="办理时间">{{ task.finishTime }}</van-cell> <van-cell title="办理时间">{{ task.finishTime }}</van-cell>
<van-cell title="办理结果"> <van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag> <dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
@@ -77,29 +77,66 @@ const siteInfo = {
}, },
}, },
style: /*language=CSS*/ ` style: /*language=CSS*/ `
/deep/ .van-action-sheet {
border-top-left-radius: 24px;
border-top-right-radius: 24px;
overflow: hidden;
background: #f8fafc;
}
/deep/ .van-action-sheet__header {
font-size: 17px;
font-weight: 700;
color: #0f172a;
background: #ffffff;
}
/deep/ .detail-container {
padding: 10px 12px 18px;
background: #f8fafc;
}
/deep/ .detail-container .van-cell-group {
margin-bottom: 12px;
overflow: hidden;
border-radius: 18px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
/deep/ .detail-container .van-cell-group__title {
padding: 12px 16px 8px;
color: #334155;
font-size: 14px;
font-weight: 700;
}
/deep/ .detail-container .van-cell {
min-height: 52px;
}
/deep/ .direction-column-cell .van-cell__value { /deep/ .direction-column-cell .van-cell__value {
white-space: normal; white-space: normal;
text-align: left; text-align: left;
} }
/deep/ .table-class { /deep/ .table-class {
width: 100%; width: 100%;
border-radius: 5px; border-radius: 10px;
overflow: hidden; overflow: hidden;
line-height: 1.5rem; line-height: 1.8rem;
font-size: 13px; font-size: 13px;
table-layout: fixed; table-layout: fixed;
border-collapse: collapse; border-collapse: collapse;
background: #ffffff;
} }
/deep/ .table-class th { /deep/ .table-class th {
background-color: #f2f2f2; padding: 10px 8px;
border: 1px solid #dddddd; background-color: #f1f5f9;
border: 1px solid #dbe4ee;
color: #334155;
font-weight: 600;
} }
/deep/ .table-class tr { /deep/ .table-class tr {
text-align: center; text-align: center;
border-bottom: 1px solid #dddddd; border-bottom: 1px solid #dbe4ee;
} }
/deep/ .table-class td { /deep/ .table-class td {
border: 1px solid #dddddd; padding: 10px 8px;
border: 1px solid #dbe4ee;
color: #475569;
} }
` `
} }
@@ -18,10 +18,8 @@ layout("/layouts/platform_h5.html"){
<table-list api="/platform/siteCug/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="siteName" @ready="onReady"> <table-list api="/platform/siteCug/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="siteName" @ready="onReady">
<template v-slot="{index,row}"> <template v-slot="{index,row}">
<table-column label="场地名称">{{row.siteName}}</table-column> <table-column label="预约人">{{row.applyUserName || '-'}}</table-column>
<table-column label="预约类型">{{row.reserveType === 'club' ? '协会预约' : '分工会预约'}}</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.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</table-column> <table-column label="结束时间">{{row.reserveEndTime}}</table-column>
<table-column label="当前节点">{{row.taskName || '-'}}</table-column> <table-column label="当前节点">{{row.taskName || '-'}}</table-column>
@@ -73,7 +71,6 @@ layout("/layouts/platform_h5.html"){
} }
}, },
methods: { methods: {
historyBack,
async onReady() { async onReady() {
await this.querySiteType() await this.querySiteType()
await this.querySites() await this.querySites()