场地预约整改
This commit is contained in:
@@ -33,14 +33,14 @@ public class SysCompletedController {
|
||||
@Inject
|
||||
private MainPageNeedItemsService mainPageNeedItemsService;
|
||||
|
||||
@Inject
|
||||
private io.v.nutz.sys.services.SysCompletedService completedService;
|
||||
|
||||
/** 返回当前登录人的手机已办,已在 service 中校验目标页面权限。 */
|
||||
@At
|
||||
@ViReturn
|
||||
public Object getCompletes(){
|
||||
CndPlus cnd = CndPlus.create();
|
||||
cnd.and("auditBy","=", ShiroUtil.getPrincipalProperty("id"));
|
||||
cnd.and("YEAR(auditTime)","=",DateUtil.thisYear());
|
||||
cnd.desc("auditTime");
|
||||
return baseService.dao().query(Sys_completed.class,cnd);
|
||||
return completedService.getCompletes();
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public class SysCompletedController {
|
||||
//审核完成前的待办
|
||||
List<NeedItems> itemsList = Arrays.asList(items);
|
||||
//审核完成后的待办
|
||||
List<NeedItems> needItemsList = mainPageNeedItemsService.getNeedItems();
|
||||
List<NeedItems> needItemsList = mainPageNeedItemsService.getNeedItems(true);
|
||||
//过滤没有移动端地址和数量为0的待办
|
||||
List<NeedItems> needItems = needItemsList.stream().filter(v-> v.getCount()>0 && Strings.isNotBlank(v.getMobileHref())).collect(Collectors.toList());
|
||||
//审核前与审核后待办取差集
|
||||
|
||||
+5
-67
@@ -1,22 +1,13 @@
|
||||
package io.v.nutz.sys.controllers.platform.sys;
|
||||
|
||||
import cn.wizzer.framework.base.Result;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.dao.util.Daos;
|
||||
import org.nutz.dao.util.cri.Static;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 任务待办
|
||||
@@ -27,67 +18,14 @@ import java.util.List;
|
||||
public class SysLocalProcessController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
private io.v.nutz.sys.services.SysLocalProcessService sysLocalProcessService;
|
||||
|
||||
/**
|
||||
* 查询待办
|
||||
* @param mode 1待处理 2已处理 3我发起的
|
||||
* @return
|
||||
*/
|
||||
/** mode=1 待办、2 已办、3 本人发起;mobile=true 手机端,默认 PC;返回 code/data,data 为包含双端处理地址的任务数组。 */
|
||||
@At
|
||||
@RequiresAuthentication
|
||||
@Ok("json:full")
|
||||
public Result todoList(Integer mode) {
|
||||
if (mode == 1 || mode == 2) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
t.taskNodeName,
|
||||
t.createdByUserName,
|
||||
t.formUrl,
|
||||
t.formUrlView,
|
||||
t.createdOn,
|
||||
t2.processName,
|
||||
t2.nodeName
|
||||
FROM
|
||||
sys_local_process_instance_task t
|
||||
LEFT JOIN sys_local_process_instance t2 ON t2.processUniqueId = t.processUniqueId
|
||||
$condition
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and(new Static("JSON_CONTAINS( t.assignments, '\"" + ShiroUtil.getPrincipalProperty("loginname") + "\"')"));
|
||||
cnd.and("t.status", "=", mode);
|
||||
cnd.and("t.taskDeleteFlag", "=", 0);
|
||||
cnd.desc("t2.processInitiationTime");
|
||||
sql.setCondition(cnd);
|
||||
List<NutMap> list = (List<NutMap>) Daos.query(dao, sql.toString(), Sqls.callback.maps());
|
||||
return Result.success(list);
|
||||
} else if (mode == 3) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
t.id,
|
||||
t.processUniqueId,
|
||||
t.processName,
|
||||
t.nodeName,
|
||||
t.pcUrl,
|
||||
t.mobileUrl,
|
||||
t.pcUrl as formUrl,
|
||||
t.processInstanceStatus,
|
||||
t.processInitiationTime as createdOn,
|
||||
u.username as createdByUserName
|
||||
FROM
|
||||
sys_local_process_instance t
|
||||
LEFT JOIN sys_user u ON u.id = t.processInitiatorId
|
||||
WHERE
|
||||
t.processInitiatorId = @id
|
||||
AND t.processDeleteFlag = 0
|
||||
AND t.delFlag = 0
|
||||
ORDER BY t.processInitiationTime DESC
|
||||
""");
|
||||
sql.setParam("id", ShiroUtil.getUserId());
|
||||
List<NutMap> list = (List<NutMap>) Daos.query(dao, sql.toString(), Sqls.callback.maps());
|
||||
return Result.success(list);
|
||||
}
|
||||
return null;
|
||||
public Result todoList(Integer mode, Boolean mobile) {
|
||||
if (mode == null || mode < 1 || mode > 3) return Result.error("请选择有效的待办查询类型");
|
||||
return Result.success(sysLocalProcessService.todoList(mode, Boolean.TRUE.equals(mobile)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -59,7 +59,8 @@ public class ClubExamineRegisterAgentController {
|
||||
|
||||
Sys_user user = (Sys_user) ShiroUtil.getPrincipal();
|
||||
List<Sys_menu> menus = Optional.ofNullable(user).orElse(new Sys_user()).getMenus();
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).distinct().collect(Collectors.toList());
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).filter(v -> io.v.nutz.sys.services.TodoAccessService.canAccess(v.getString("url"))).distinct().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
+8
-4
@@ -60,7 +60,8 @@ public class ClubNeedItemsController {
|
||||
|
||||
Sys_user user = (Sys_user) ShiroUtil.getPrincipal();
|
||||
List<Sys_menu> menus = Optional.ofNullable(user).orElse(new Sys_user()).getMenus();
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).distinct().collect(Collectors.toList());
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).filter(v -> io.v.nutz.sys.services.TodoAccessService.canAccess(v.getString("url"))).distinct().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private int getClubRegistCount(Boolean flag, String roleId, Integer state) {
|
||||
@@ -87,7 +88,8 @@ public class ClubNeedItemsController {
|
||||
|
||||
Sys_user user = (Sys_user) ShiroUtil.getPrincipal();
|
||||
List<Sys_menu> menus = Optional.ofNullable(user).orElse(new Sys_user()).getMenus();
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).distinct().collect(Collectors.toList());
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).filter(v -> io.v.nutz.sys.services.TodoAccessService.canAccess(v.getString("url"))).distinct().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private int getClubApplyCount(String roleId, Integer state) {
|
||||
@@ -116,7 +118,8 @@ public class ClubNeedItemsController {
|
||||
|
||||
Sys_user user = (Sys_user) ShiroUtil.getPrincipal();
|
||||
List<Sys_menu> menus = Optional.ofNullable(user).orElse(new Sys_user()).getMenus();
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).distinct().collect(Collectors.toList());
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).filter(v -> io.v.nutz.sys.services.TodoAccessService.canAccess(v.getString("url"))).distinct().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private int getClubManageCount() {
|
||||
@@ -139,7 +142,8 @@ public class ClubNeedItemsController {
|
||||
|
||||
Sys_user user = (Sys_user) ShiroUtil.getPrincipal();
|
||||
List<Sys_menu> menus = Optional.ofNullable(user).orElse(new Sys_user()).getMenus();
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).distinct().collect(Collectors.toList());
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).filter(v -> io.v.nutz.sys.services.TodoAccessService.canAccess(v.getString("url"))).distinct().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private int getClubEvaluateCount(Boolean flag, String roleId, Integer state) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.v.nutz.sys.services;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import io.v.nutz.base.dao.CndPlus;
|
||||
import io.v.nutz.sys.models.Sys_completed;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 旧版手机已办查询,权限过滤保留原有本人和年度范围。 */
|
||||
@IocBean
|
||||
public class SysCompletedService {
|
||||
@Inject private Dao dao;
|
||||
|
||||
/** 无外部身份参数;返回当前登录人本年度、手机查看入口仍有权限的已办记录。 */
|
||||
public List<Sys_completed> getCompletes() {
|
||||
CndPlus cnd = CndPlus.create();
|
||||
cnd.and("auditBy", "=", ShiroUtil.getUserId());
|
||||
cnd.and("YEAR(auditTime)", "=", DateUtil.thisYear());
|
||||
cnd.desc("auditTime");
|
||||
return dao.query(Sys_completed.class, cnd).stream()
|
||||
.filter(row -> TodoAccessService.canAccess(row.getMobileUrl())).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
public interface SysLocalProcessService {
|
||||
/** mode=1 待办、2 已办、3 本人发起;返回含原 PC 字段及手机处理/查看地址的任务列表,身份取当前登录人。 */
|
||||
List<org.nutz.lang.util.NutMap> todoList(Integer mode);
|
||||
/** mobile=true 按手机跳转地址鉴权,false 按 PC 地址鉴权;返回已过滤的任务数组。 */
|
||||
List<org.nutz.lang.util.NutMap> todoList(Integer mode, boolean mobile);
|
||||
|
||||
/**
|
||||
* 开始一个实例流程
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package io.v.nutz.sys.services;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authz.annotation.*;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.nutz.mvc.*;
|
||||
import org.nutz.mvc.impl.MappingNode;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 待办入口访问判断:读取 MVC 实际注册的路由及权限,不执行目标方法、不修改业务数据。 */
|
||||
public class TodoAccessService {
|
||||
private static final String KEY = TodoAccessService.class.getName();
|
||||
private final Map<String, ActionInfo> exact = new HashMap<>();
|
||||
private final MappingNode<ActionInfo> patterns = new MappingNode<>();
|
||||
|
||||
/** MVC 启动时登记入口;注册表跟随本次应用配置,重启后重新建立,不缓存用户权限。 */
|
||||
public static synchronized void register(NutConfig config, ActionInfo info) {
|
||||
// 待办点击使用 GET;同一路径的提交接口不能覆盖页面自身权限。
|
||||
if (info.getHttpMethods() != null && !info.getHttpMethods().isEmpty()
|
||||
&& info.getHttpMethods().stream().noneMatch(method -> "GET".equalsIgnoreCase(method))) return;
|
||||
TodoAccessService service = (TodoAccessService) config.getAttribute(KEY);
|
||||
if (service == null) { service = new TodoAccessService(); config.setAttribute(KEY, service); }
|
||||
for (String path : info.getPaths()) {
|
||||
if (!path.startsWith("/")) path = "/" + path;
|
||||
service.exact.put(path, info);
|
||||
service.patterns.add(path, info);
|
||||
}
|
||||
}
|
||||
|
||||
/** url 为待办真实跳转地址;返回当前登录账号能否访问。不存在的入口不展示。 */
|
||||
public static boolean canAccess(String url) {
|
||||
NutConfig config = Mvcs.getNutConfig();
|
||||
TodoAccessService service = config == null ? null : (TodoAccessService) config.getAttribute(KEY);
|
||||
return service != null && service.allowed(url, SecurityUtils.getSubject());
|
||||
}
|
||||
|
||||
/** 去掉查询参数、锚点和应用上下文后匹配路由;权限保留原来的 AND/OR 组合语义。 */
|
||||
public boolean allowed(String url, Subject subject) {
|
||||
if (url == null || url.trim().isEmpty() || subject == null || !subject.isAuthenticated()) return false;
|
||||
try {
|
||||
URI uri = URI.create(url.trim());
|
||||
// 允许配置为本系统绝对地址的旧任务,外部域名不能按本系统权限推断。
|
||||
if (uri.isAbsolute() || uri.getRawAuthority() != null) {
|
||||
String domain = io.v.nutz.web.commons.base.Globals.AppDomain;
|
||||
if (domain == null || domain.isBlank()) return false;
|
||||
URI app = URI.create(domain);
|
||||
if (!Objects.equals(app.getScheme(), uri.getScheme()) || !Objects.equals(app.getRawAuthority(), uri.getRawAuthority())) return false;
|
||||
}
|
||||
String path = uri.getPath();
|
||||
if (path == null || !path.startsWith("/")) return false;
|
||||
if (Mvcs.getReq() != null) {
|
||||
String context = Mvcs.getReq().getContextPath();
|
||||
if (context != null && !context.isEmpty() && path.startsWith(context + "/")) path = path.substring(context.length());
|
||||
}
|
||||
ActionInfo info = exact.get(path);
|
||||
if (info == null && path.endsWith("/")) info = exact.get(path.substring(0, path.length()-1));
|
||||
if (info == null) info = patterns.get(new ActionContext().setPath(path), path);
|
||||
return info != null && check(info.getModuleType(), subject) && check(info.getMethod(), subject);
|
||||
} catch (IllegalArgumentException e) { return false; }
|
||||
}
|
||||
|
||||
private boolean check(AnnotatedElement element, Subject subject) {
|
||||
if (element == null) return true;
|
||||
if (element.isAnnotationPresent(RequiresGuest.class)) return subject.getPrincipal() == null;
|
||||
RequiresPermissions permission = element.getAnnotation(RequiresPermissions.class);
|
||||
if (permission != null) {
|
||||
boolean passed = permission.logical() == Logical.OR
|
||||
? Arrays.stream(permission.value()).anyMatch(subject::isPermitted)
|
||||
: Arrays.stream(permission.value()).allMatch(subject::isPermitted);
|
||||
if (!passed) return false;
|
||||
}
|
||||
RequiresRoles roles = element.getAnnotation(RequiresRoles.class);
|
||||
return roles == null || (roles.logical() == Logical.OR
|
||||
? Arrays.stream(roles.value()).anyMatch(subject::hasRole)
|
||||
: Arrays.stream(roles.value()).allMatch(subject::hasRole));
|
||||
}
|
||||
|
||||
/** rows 为已有数据范围内的待办;urlField 是本端点击使用的地址字段,返回过滤后的列表。 */
|
||||
public static List<NutMap> filter(List<NutMap> rows, String urlField) {
|
||||
return rows.stream().filter(row -> canAccess(row.getString(urlField))).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,80 @@ public class SysLocalProcessServiceImpl implements SysLocalProcessService {
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
/**
|
||||
* mode 为 1 待办、2 已办、3 本人发起;返回字段保留 PC 兼容,并增加 id、formMobileUrl、formMobileUrlView、endOn。
|
||||
* 待办/已办按接收人工号查询,发起记录按当前用户 ID 查询;删除标记的任务和流程不再展示。
|
||||
*/
|
||||
@Override
|
||||
public List<org.nutz.lang.util.NutMap> todoList(Integer mode) {
|
||||
return todoList(mode, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<org.nutz.lang.util.NutMap> todoList(Integer mode, boolean mobile) {
|
||||
if (mode == null || mode < 1 || mode > 3) throw new IllegalArgumentException("请选择有效的待办查询类型");
|
||||
Sql sql;
|
||||
if (mode == 1 || mode == 2) {
|
||||
sql = Sqls.create("""
|
||||
select t.id,t.processUniqueId,t.taskNodeName,t.createdByUserName,t.formUrl,t.formUrlView,
|
||||
t.formMobileUrl,t.formMobileUrlView,t.createdOn,t.endOn,t2.processName,t2.nodeName
|
||||
from sys_local_process_instance_task t
|
||||
join sys_local_process_instance t2 on t2.processUniqueId=t.processUniqueId
|
||||
where JSON_CONTAINS(t.assignments,JSON_QUOTE(@loginname)) and t.status=@mode
|
||||
and coalesce(t.taskDeleteFlag,0)=0 and coalesce(t.delFlag,0)=0
|
||||
and coalesce(t2.processDeleteFlag,0)=0 and coalesce(t2.delFlag,0)=0
|
||||
order by t2.processInitiationTime desc,t.id desc
|
||||
""").setParam("loginname",io.v.nutz.web.commons.utils.ShiroUtil.getPrincipalProperty("loginname"))
|
||||
.setParam("mode",mode);
|
||||
} else {
|
||||
sql = Sqls.create("""
|
||||
select t.id,t.processUniqueId,t.processName,t.nodeName,t.pcUrl,t.mobileUrl,t.pcUrl as formUrl,
|
||||
t.mobileUrl as formMobileUrl,t.mobileUrl as formMobileUrlView,t.processInstanceStatus,
|
||||
t.processInitiationTime as createdOn,u.username as createdByUserName
|
||||
from sys_local_process_instance t left join sys_user u on u.id=t.processInitiatorId
|
||||
where t.processInitiatorId=@id and coalesce(t.processDeleteFlag,0)=0 and coalesce(t.delFlag,0)=0
|
||||
order by t.processInitiationTime desc
|
||||
""").setParam("id",io.v.nutz.web.commons.utils.ShiroUtil.getUserId());
|
||||
}
|
||||
sql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(sql);
|
||||
List<org.nutz.lang.util.NutMap> rows = sql.getList(org.nutz.lang.util.NutMap.class);
|
||||
// 兼容历史会员申请任务:只修正返回的手机地址,不回写数据库,也不改变 PC 地址和查询权限。
|
||||
for (org.nutz.lang.util.NutMap row : rows) {
|
||||
if (!row.getString("processUniqueId", "").startsWith("MEMBER_APPLY@")) continue;
|
||||
for (String field : List.of("formMobileUrl", "formMobileUrlView", "mobileUrl")) {
|
||||
if (row.containsKey(field)) row.put(field, memberApplyMobileUrl(row.getString(field)));
|
||||
}
|
||||
}
|
||||
// 权限仅进一步收紧已有候选人/本人范围;已办按查看入口判断,不能因历史授权继续展示。
|
||||
return rows.stream().filter(row -> {
|
||||
String url = mobile ? row.getString("formMobileUrl") : row.getString("formUrl");
|
||||
if (mode == 2) {
|
||||
String view = row.getString(mobile ? "formMobileUrlView" : "formUrlView");
|
||||
if (view != null && !view.isBlank()) url = view;
|
||||
}
|
||||
return io.v.nutz.sys.services.TodoAccessService.canAccess(url);
|
||||
}).collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入历史手机 URL,返回对应 H5 地址;仅匹配已存在 H5 页面的三个会员申请入口。
|
||||
* 保留查询参数与锚点,空值、正确 H5 地址及其他业务地址原样返回,避免重复添加 /h5。
|
||||
*/
|
||||
private String memberApplyMobileUrl(String url) {
|
||||
if (url == null || url.isEmpty()) return url;
|
||||
int end = url.length();
|
||||
if (url.indexOf('?') >= 0) end = Math.min(end, url.indexOf('?'));
|
||||
if (url.indexOf('#') >= 0) end = Math.min(end, url.indexOf('#'));
|
||||
String path = url.substring(0, end);
|
||||
if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
|
||||
if (List.of("/platform/member/apply/mine", "/platform/member/apply/branchUnion/audit",
|
||||
"/platform/member/apply/schoolUnion/audit").contains(path)) {
|
||||
return path + "/h5" + url.substring(end);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startProcess(String processName, String processUniqueId, String nodeName, String processInitiatorId, String pcUrl, String mobileUrl) {
|
||||
int count = dao.count(Sys_local_process_instance.class, Cnd.where("processUniqueId", "=", processUniqueId));
|
||||
|
||||
@@ -56,6 +56,7 @@ public class NutShiroProcessor extends AbstractProcessor {
|
||||
throw new IllegalStateException("this Processor have bean inited!!");
|
||||
}
|
||||
super.init(config, ai);
|
||||
io.v.nutz.sys.services.TodoAccessService.register(config, ai);
|
||||
match = NutShiro.match(ai.getMethod());
|
||||
init = true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import io.v.nutz.base.annontation.ViReturn;
|
||||
import io.v.nutz.base.utils.PageUtil;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
@@ -70,22 +72,27 @@ public class SiteManageController {
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("activity.site.manage")
|
||||
/** data 为新增场地表单,返回统一 code/msg;类型关联和校区校验由 service 完成。 */
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Object doAdd(@Param("data") ActivitySiteInfo activitySiteInfo) {
|
||||
siteInfoService.insert(activitySiteInfo);
|
||||
siteInfoService.saveManagedSite(activitySiteInfo, false);
|
||||
return null;
|
||||
}
|
||||
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("activity.site.manage")
|
||||
/** data 为包含 id 的场地表单,返回统一 code/msg,成功后可重新查询校区回显。 */
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Object doEdit(@Param("data") ActivitySiteInfo activitySiteInfo) {
|
||||
siteInfoService.updateIgnoreNull(activitySiteInfo);
|
||||
siteInfoService.saveManagedSite(activitySiteInfo, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("activity.site.manage")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Object doDelete(String id) {
|
||||
siteInfoService.delete(id);
|
||||
return null;
|
||||
|
||||
@@ -28,6 +28,8 @@ import org.nutz.mvc.annotation.Param;
|
||||
@At("/platform/activity/site/record")
|
||||
public class SiteRecordController {
|
||||
|
||||
@Inject private io.v.nutz.zhgh.activity.services.impl.SiteBookingService siteBookingService;
|
||||
|
||||
@Inject
|
||||
private SiteReserveService siteReserveService;
|
||||
|
||||
@@ -57,8 +59,7 @@ public class SiteRecordController {
|
||||
ass.`stateName` state_name,
|
||||
ass.`stateColor` state_color,
|
||||
ass.stateAuditType,
|
||||
count(sqid) days,
|
||||
GROUP_CONCAT(reserve_day order by reserve_day asc) concat_day
|
||||
count(distinct reserve_day) days
|
||||
FROM
|
||||
activity_site_reserve asr
|
||||
LEFT JOIN
|
||||
@@ -96,7 +97,7 @@ public class SiteRecordController {
|
||||
cnd.groupBy("sqid");
|
||||
|
||||
sql.setCondition(cnd);
|
||||
return siteReserveService.listPage(pageNumber, pageSize, sql);
|
||||
return siteBookingService.reservationPage(pageNumber, pageSize, sql, month);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -99,8 +99,7 @@ public class SiteReserveController {
|
||||
ass.`stateName` state_name,
|
||||
ass.`stateColor` state_color,
|
||||
ass.stateAuditType,
|
||||
count(sqid) days,
|
||||
GROUP_CONCAT(reserve_day order by reserve_day asc) concat_day
|
||||
count(distinct reserve_day) days
|
||||
FROM
|
||||
activity_site_reserve asr
|
||||
LEFT JOIN
|
||||
@@ -140,15 +139,19 @@ public class SiteReserveController {
|
||||
cnd.groupBy("sqid");
|
||||
|
||||
sql.setCondition(cnd);
|
||||
return siteReserveService.listPage(pageNumber, pageSize, sql);
|
||||
return siteBookingService.reservationPage(pageNumber, pageSize, sql, time);
|
||||
}
|
||||
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresPermissions("activity.site.reserve")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
/** id 为预约记录主键,op=1 删除已撤销申请、op=2 撤销申请;成功返回空 data,由 ViReturn 包装结果。 */
|
||||
public Object doDelete(String id, Integer op) {
|
||||
siteBookingService.cancel(id);
|
||||
if (org.nutz.lang.Strings.isBlank(id)) throw new IllegalArgumentException("请选择预约记录");
|
||||
if (Integer.valueOf(1).equals(op)) siteBookingService.deleteCancelled(id);
|
||||
else if (Integer.valueOf(2).equals(op)) siteBookingService.cancel(id);
|
||||
else throw new IllegalArgumentException("不支持的预约操作");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -165,10 +168,16 @@ public class SiteReserveController {
|
||||
@RequiresPermissions("activity.site.reserve")
|
||||
/** data 为场地、类型、协会及事由;days 为日期数组,起止时间取 data;返回新申请 sqid。 */
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days) {
|
||||
public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days, @Param("times") String[] times) {
|
||||
List<NutMap> slots = new ArrayList<>();
|
||||
if (activitySiteReserve == null || days == null) throw new IllegalArgumentException("请选择预约日期");
|
||||
for (String day : days) slots.add(NutMap.NEW().setv("day",day).setv("start_time",activitySiteReserve.getStart_time()).setv("end_time",activitySiteReserve.getEnd_time()));
|
||||
// times为所选HH:mm-HH:mm场次数组;保留旧客户端单场次参数的兼容。
|
||||
if(times==null || times.length==0)times=new String[]{activitySiteReserve.getStart_time()+"-"+activitySiteReserve.getEnd_time()};
|
||||
for(String day:days)for(String time:times){
|
||||
String[] parts=time.split("-",-1);
|
||||
if(parts.length!=2)throw new IllegalArgumentException("请选择有效预约场次");
|
||||
slots.add(NutMap.NEW().setv("day",day).setv("start_time",parts[0]).setv("end_time",parts[1]));
|
||||
}
|
||||
return siteBookingService.submit(activitySiteReserve,slots);
|
||||
}
|
||||
|
||||
@@ -253,4 +262,8 @@ public class SiteReserveController {
|
||||
/** 返回当前用户有效协会数组(id/name),供个人所属协会展示和协会预约选择。 */
|
||||
@At @ViReturn @org.apache.shiro.authz.annotation.RequiresAuthentication
|
||||
public Object myClubs() { return siteBookingService.myClubs(); }
|
||||
|
||||
/** 无入参;返回统一code/data,data.id/name为当前申请人所属分工会,仅供表单展示。 */
|
||||
@At @ViReturn @RequiresPermissions("activity.site.reserve")
|
||||
public Object myUnion() { return siteBookingService.myBookingUnion(); }
|
||||
}
|
||||
|
||||
@@ -28,13 +28,38 @@ public class ActivitySiteInfo {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String name;
|
||||
|
||||
@Column
|
||||
@Comment("校区名称,取自系统校区选项")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String campus;
|
||||
|
||||
@Column
|
||||
@Comment("预约时间段类型:1分段预约,2全天候预约;旧场地按分段兼容")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer reserveTimeType;
|
||||
|
||||
@Column
|
||||
@Comment("禁用时段:date、startTime、endTime")
|
||||
@ColDefine(type = ColType.MYSQL_JSON)
|
||||
private List<org.nutz.lang.util.NutMap> notApplyTimeList;
|
||||
|
||||
@Column
|
||||
@Comment("分段场次原始配置,保留切换前的时间及timeUnit分钟单位")
|
||||
@ColDefine(type = ColType.MYSQL_JSON)
|
||||
private List<org.nutz.lang.util.NutMap> segmentedOpenHours;
|
||||
|
||||
@Column
|
||||
@Comment("全天候起止时间及timeUnit分钟单位")
|
||||
@ColDefine(type = ColType.MYSQL_JSON)
|
||||
private org.nutz.lang.util.NutMap fullDayOpenHour;
|
||||
|
||||
@Column
|
||||
@Comment("场地地址")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String address;
|
||||
|
||||
@Column
|
||||
@Comment("联系人")
|
||||
@Comment("场地管理员")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String contact_person;
|
||||
|
||||
@@ -84,7 +109,7 @@ public class ActivitySiteInfo {
|
||||
private Integer sexLimit;
|
||||
|
||||
@Column
|
||||
@Comment("个人预约限定人数")
|
||||
@Comment("限定人数")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer limitNum;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ public class ActivitySiteReserve extends BaseModel {
|
||||
private String joinUser;
|
||||
|
||||
@Column
|
||||
@Comment("预约类型(1.个人预约,2.单位预约,3.协会预约)")
|
||||
@Comment("预约类型(1.个人预约,2.分工会预约,3.协会预约)")
|
||||
@ColDefine(type = ColType.INT, width = 10)
|
||||
private Integer reserve_type;
|
||||
}
|
||||
|
||||
@@ -5,4 +5,12 @@ import cn.wizzer.framework.base.service.BaseService;
|
||||
import io.v.nutz.zhgh.activity.models.ActivitySiteInfo;
|
||||
|
||||
public interface SiteInfoService extends BaseService<ActivitySiteInfo> {
|
||||
/** form 为场地表单,editing 区分新增和编辑;保存校区并由后端关联职工之家类型,返回 void。 */
|
||||
void saveManagedSite(ActivitySiteInfo form, boolean editing);
|
||||
/** 校验 day(yyyy-MM-dd)、start/end(HH:mm,结束可24:00)属于有效场次且不与禁用时间交叉;不通过抛出参数异常。 */
|
||||
void validateBookingSlot(ActivitySiteInfo site, String day, String start, String end);
|
||||
/** 查询某天可预约场次,reserveType=1/2/3;返回 start_time/end_time、code(1可约)、msg 和人数。 */
|
||||
java.util.List<org.nutz.lang.util.NutMap> availableSlots(String siteId, String day, Integer reserveType);
|
||||
/** 返回start/end范围内实际同时占用的峰值人数,避免配置扩大后把相邻历史场次累加成超额。 */
|
||||
int occupiedCount(java.util.List<io.v.nutz.zhgh.activity.models.ActivitySiteReserve> rows, String start, String end);
|
||||
}
|
||||
|
||||
@@ -30,11 +30,15 @@ import java.util.stream.Collectors;
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
public static final int UNION = 4000, CLUB = 4010, SCHOOL = 4020, PASS = 4030, REJECT = 4040, CANCEL = 4050;
|
||||
/** 校工会节点统一按专属场地管理员角色分配待办和校验审核资格。 */
|
||||
private static final String SCHOOL_REVIEW_ROLE = "SchoolUnionActivityVenueAdmin";
|
||||
/** 兼容现有日历组件的状态字段;名称统一读取 state,不再读取 audit_state。 */
|
||||
public static final String STATES = "(select state_id stateId,state_name stateName,state_color stateColor,"
|
||||
+ "case when state_id=4030 then 3 when state_id in (4040,4050) then 1 else 0 end stateAuditType "
|
||||
+ "from state where belong='activity_site')";
|
||||
@Inject private SysLocalProcessService sysLocalProcessService;
|
||||
@Inject private io.v.nutz.zhgh.activity.services.SiteInfoService siteInfoService;
|
||||
@Inject private io.v.nutz.base.utils.MsgApi msgApi;
|
||||
|
||||
public SiteBookingService(Dao dao) { super(dao); }
|
||||
|
||||
@@ -48,17 +52,24 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
+ "order by c.name").setParam("uid", uid()));
|
||||
}
|
||||
|
||||
/** 预约类型 1 个人、2 单位、3 协会,返回应进入的首个状态 ID。 */
|
||||
/** 无入参,返回当前申请人的id/name所属分工会;采用与提交相同的user视图组织来源,未配置时返回空字符串。 */
|
||||
public NutMap myBookingUnion() {
|
||||
List<NutMap> rows=listMap(Sqls.create("select u.unionid id,un.unionname name from `user` u left join sys_union un on un.id=u.unionid where u.id=@id")
|
||||
.setParam("id",uid()));
|
||||
return rows.isEmpty() ? NutMap.NEW().setv("id","").setv("name","") : rows.get(0);
|
||||
}
|
||||
|
||||
/** 预约类型 1 历史个人、2 分工会、3 协会,返回应进入的首个状态 ID。 */
|
||||
public int firstState(Integer type) {
|
||||
if (Integer.valueOf(1).equals(type)) return UNION;
|
||||
if (Integer.valueOf(2).equals(type)) return SCHOOL;
|
||||
if (Integer.valueOf(2).equals(type)) return UNION;
|
||||
if (Integer.valueOf(3).equals(type)) return CLUB;
|
||||
throw new IllegalArgumentException("请选择有效的预约类型");
|
||||
}
|
||||
|
||||
/** 按节点角色及组织范围查实际审核人;不允许空组织退化为全校范围。 */
|
||||
private List<NutMap> reviewers(int node, ActivitySiteReserve booking) {
|
||||
String role = node == UNION ? "gh01" : node == CLUB ? "club01" : "sysadmin";
|
||||
String role = node == UNION ? "gh01" : node == CLUB ? "club01" : SCHOOL_REVIEW_ROLE;
|
||||
String filter = "";
|
||||
if (node == UNION) {
|
||||
if (Strings.isBlank(booking.getReserve_person_unionid())) throw new IllegalArgumentException("申请人未关联分工会,请先完善所属分工会");
|
||||
@@ -78,7 +89,7 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
List<String> names = reviewers(node, booking).stream().map(v -> v.getString("loginname"))
|
||||
.filter(Strings::isNotBlank).distinct().collect(Collectors.toList());
|
||||
if (names.isEmpty()) throw new IllegalArgumentException(node == UNION ? "所属分工会未配置可用的分工会主席"
|
||||
: node == CLUB ? "所选协会未配置可用的协会会长" : "未配置可用的校工会审核人(系统管理员)");
|
||||
: node == CLUB ? "所选协会未配置可用的协会会长" : "未配置可用的校工会场地管理员");
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -102,6 +113,8 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
public String submit(ActivitySiteReserve form, List<NutMap> slots) {
|
||||
if (form == null || Strings.isBlank(form.getSite_id())) throw new IllegalArgumentException("请选择场地");
|
||||
int first = firstState(form.getReserve_type());
|
||||
// 个人日期及共享容量规则按业务类型判断,不能随首审节点变更套用到分工会预约。
|
||||
boolean personal = Integer.valueOf(1).equals(form.getReserve_type());
|
||||
if (slots == null || slots.isEmpty()) throw new IllegalArgumentException("请选择预约时段");
|
||||
listMap(Sqls.create("select id from activity_site_info where id=@id for update").setParam("id", form.getSite_id()));
|
||||
ActivitySiteInfo site = dao().fetch(ActivitySiteInfo.class, form.getSite_id());
|
||||
@@ -125,15 +138,25 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
assignments(first, form);
|
||||
assignments(SCHOOL, form);
|
||||
List<NutMap> open = Json.fromJsonAsList(NutMap.class, Json.toJson(site.getOpen_hours()));
|
||||
// 全天候按每个日期连续选择时间单位,避免跨禁用间隔或拆成多段。
|
||||
if (Integer.valueOf(2).equals(site.getReserveTimeType())) {
|
||||
Map<String,List<NutMap>> byDay=slots.stream().collect(Collectors.groupingBy(v->v.getString("day")));
|
||||
for(List<NutMap> selected:byDay.values()) {
|
||||
selected.sort(Comparator.comparing(v->v.getString("start_time")));
|
||||
for(int i=1;i<selected.size();i++) if(!selected.get(i-1).getString("end_time").equals(selected.get(i).getString("start_time")))
|
||||
throw new IllegalArgumentException("全天候预约请选择连续时段");
|
||||
}
|
||||
}
|
||||
Set<String> distinct = new HashSet<>();
|
||||
for (NutMap slot : slots) {
|
||||
String day = slot.getString("day"), start = slot.getString("start_time"), end = slot.getString("end_time");
|
||||
if (Strings.isBlank(day) || Strings.isBlank(start) || Strings.isBlank(end)) throw new IllegalArgumentException("预约日期和起止时间不能为空");
|
||||
siteInfoService.validateBookingSlot(site,day,start,end);
|
||||
LocalDate date = LocalDate.parse(day);
|
||||
LocalTime startTime = LocalTime.parse(start), endTime = "24:00".equals(end) ? LocalTime.MIDNIGHT : LocalTime.parse(end);
|
||||
if (!"24:00".equals(end) && !endTime.isAfter(startTime)) throw new IllegalArgumentException("结束时间必须晚于开始时间");
|
||||
if (!LocalDateTime.of(date, startTime).isAfter(LocalDateTime.now())) throw new IllegalArgumentException("预约时段已过期");
|
||||
if (first == UNION && !date.equals(LocalDate.now().plusDays(1))) throw new IllegalArgumentException("个人预约只支持预约明天的时间");
|
||||
if (personal && !date.equals(LocalDate.now().plusDays(1))) throw new IllegalArgumentException("个人预约只支持预约明天的时间");
|
||||
if (Boolean.TRUE.equals(site.getWorkday()) && date.getDayOfWeek().getValue() >= 6) throw new IllegalArgumentException("该场地仅支持工作日预约");
|
||||
if (open == null || open.stream().noneMatch(v -> start.equals(v.getString("start_time")) && end.equals(v.getString("end_time"))))
|
||||
throw new IllegalArgumentException("所选时段不在场地开放场次中,请刷新重选");
|
||||
@@ -141,9 +164,9 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
List<ActivitySiteReserve> existing = query(Cnd.where("site_id", "=", site.getId()).and("reserve_day", "=", day)
|
||||
.and("start_time", "<", end).and("end_time", ">", start).and("reserve_state", "not in", Arrays.asList(REJECT, CANCEL)));
|
||||
if (existing.stream().anyMatch(v -> uid().equals(v.getReserve_person_id()))) throw new IllegalArgumentException("您已预约该时间段");
|
||||
if (first != UNION && !existing.isEmpty()) throw new IllegalArgumentException("该时段已有预约,协会或单位预约需要空闲时段");
|
||||
if (first == UNION && (existing.stream().anyMatch(v -> !Integer.valueOf(1).equals(v.getReserve_type()))
|
||||
|| site.getLimitNum() == null || existing.size() >= site.getLimitNum())) throw new IllegalArgumentException("该时段预约人数已满或已被单位/协会预约");
|
||||
if (!personal && !existing.isEmpty()) throw new IllegalArgumentException("该时段已有预约,协会或分工会预约需要空闲时段");
|
||||
if (personal && (existing.stream().anyMatch(v -> !Integer.valueOf(1).equals(v.getReserve_type()))
|
||||
|| site.getLimitNum() == null || siteInfoService.occupiedCount(existing,start,end) >= site.getLimitNum())) throw new IllegalArgumentException("该时段预约人数已满或已被分工会/协会预约");
|
||||
}
|
||||
String sqid = UUID.randomUUID().toString().replace("-", "");
|
||||
for (NutMap slot : slots) {
|
||||
@@ -160,6 +183,7 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
sysLocalProcessService.startProcess("场地预约:" + site.getName(), process(form), title(first), uid(),
|
||||
"/platform/activity/site/reserve", "/mobile/activity/site/info/my");
|
||||
createTask(first, form);
|
||||
notifyBooking(form,first,null);
|
||||
return sqid;
|
||||
}
|
||||
|
||||
@@ -173,9 +197,52 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
sysLocalProcessService.updateProcessNodeName(process(booking), title(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* booking 为整条申请,state 为新待审节点或最终 PASS/REJECT,opinion 为最终审核意见(待审时为空)。
|
||||
* 返回 void;每次按 sqid 组织一条钉钉消息、接收人工号去重,复用消息开关与发送日志。
|
||||
* 只由首次提交和审核流转调用,撤回审核、新建恢复待办、撤销和删除不发送。
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
private void notifyBooking(ActivitySiteReserve booking,int state,String opinion) {
|
||||
try {
|
||||
boolean result = state == PASS || state == REJECT;
|
||||
List<String> receivers;
|
||||
if (result) {
|
||||
Sys_user applicant = dao().fetch(Sys_user.class,booking.getReserve_person_id());
|
||||
receivers = applicant == null || Strings.isBlank(applicant.getLoginname())
|
||||
? Collections.emptyList() : Collections.singletonList(applicant.getLoginname());
|
||||
} else receivers = assignments(state,booking);
|
||||
if (receivers.isEmpty()) throw new IllegalArgumentException("消息接收人工号为空");
|
||||
ActivitySiteInfo site = dao().fetch(ActivitySiteInfo.class,booking.getSite_id());
|
||||
List<NutMap> slots = listMap(Sqls.create("select reserve_day,start_time,end_time from activity_site_reserve where sqid=@sqid")
|
||||
.setParam("sqid",booking.getSqid()));
|
||||
List<NutMap> groups = groupReservationTimes(slots);
|
||||
// 长申请只列前五个日期,完整时段在业务页查看,避免超出消息长度限制。
|
||||
List<String> days = new ArrayList<>();
|
||||
for (NutMap group : groups.stream().limit(5).collect(Collectors.toList())) {
|
||||
List<NutMap> ranges = (List<NutMap>) group.get("ranges");
|
||||
String times = ranges.stream().limit(4).map(range -> range.getString("start") + "–" + range.getString("end"))
|
||||
.collect(Collectors.joining("、"));
|
||||
days.add(group.getString("date") + " " + times + (ranges.size()>4 ? "等时段" : ""));
|
||||
}
|
||||
String status = state == PASS ? "审核通过" : state == REJECT ? "审核拒绝" : "待" + title(state);
|
||||
String content = "申请人:" + booking.getReserve_person() + ";场地:" + (site == null ? "" : site.getName())
|
||||
+ ";预约时段:" + String.join(";",days) + (groups.size()>5 ? "等,共" + groups.size() + "天" : "")
|
||||
+ ";状态:" + status + (result && Strings.isNotBlank(opinion) ? ";审核意见:" + opinion : "")
|
||||
+ (result ? "。请点击查看我的预约。" : "。请点击进入审核页面处理。");
|
||||
String path = result ? "/mobile/activity/site/info/my" : "/mobile/activity/site/audit/" + stage(state);
|
||||
String domain = Strings.sNull(io.v.nutz.web.commons.base.Globals.AppDomain).replaceAll("/+$","");
|
||||
msgApi.sendMsgInsertLog(Collections.singletonList("DingTalk"),receivers.stream().distinct().collect(Collectors.toList()),
|
||||
2,result ? "场地预约" + status : "场地预约审核通知",content,"",domain + path,"场地预约");
|
||||
} catch (Exception e) {
|
||||
// 外部发送失败不回滚已完成的预约操作;记录申请编号以便排查,避免用户重试造成重复申请。
|
||||
org.nutz.log.Logs.get().error("场地预约钉钉通知失败,sqid=" + booking.getSqid() + ",state=" + state,e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回指定审核入口的数据范围,分工会/协会使用角色关系上的组织字段。 */
|
||||
private String scope(int node) {
|
||||
if (node == SCHOOL) return "exists(select 1 from sys_user_role ur join sys_role r on r.id=ur.roleId where ur.userId=@uid and r.code='sysadmin')";
|
||||
if (node == SCHOOL) return "exists(select 1 from sys_user_role ur join sys_role r on r.id=ur.roleId where ur.userId=@uid and r.code='" + SCHOOL_REVIEW_ROLE + "')";
|
||||
return "exists(select 1 from sys_user_role ur join sys_role r on r.id=ur.roleId where ur.userId=@uid and r.code='"
|
||||
+ (node == UNION ? "gh01" : "club01") + "' and "
|
||||
+ (node == UNION ? "ur.unionid=b.reserve_person_unionid" : "ur.stid=b.clubId") + ")";
|
||||
@@ -210,16 +277,75 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
.setParam("uid", uid()).setParam("keyword", Strings.sNull(keyword)).setParam("like", "%" + Strings.sNull(keyword) + "%")
|
||||
.setParam("month", Strings.sNull(month)).setParam("typeId", Strings.sNull(typeId));
|
||||
Pagination result = listPageMap(Math.max(1,page), Math.min(100,Math.max(1,size)), sql);
|
||||
// 按当前页的申请一次读取原始时段,不依赖可能被数据库截断的group_concat。
|
||||
List<String> sqids=result.<Map<String,Object>>getList().stream().map(row->String.valueOf(row.get("sqid"))).collect(Collectors.toList());
|
||||
Map<String,List<NutMap>> slotsByBooking=new HashMap<>();
|
||||
if(!sqids.isEmpty()) {
|
||||
List<NutMap> slots=listMap(Sqls.create("select sqid,reserve_day,start_time,end_time from activity_site_reserve where sqid in (@sqids)").setParam("sqids",sqids));
|
||||
slotsByBooking=slots.stream().collect(Collectors.groupingBy(row->row.getString("sqid")));
|
||||
}
|
||||
// 两端共用后端资格判断;真正撤回仍在加锁后复查,不能信任列表中的旧状态。
|
||||
for (Map<String,Object> row : result.<Map<String,Object>>getList()) {
|
||||
ActivitySiteReserve booking = fetch(String.valueOf(row.get("id")));
|
||||
String reason = revokeReason(node, booking);
|
||||
row.put("canRevoke", reason == null);
|
||||
row.put("revokeReason", reason);
|
||||
row.put("reservationTimeGroups",groupReservationTimes(slotsByBooking.getOrDefault(String.valueOf(row.get("sqid")),Collections.emptyList())));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* pageNumber/pageSize 为分页参数,sql 为调用页已包含权限及筛选的分组查询,month 为可空的 YYYY-MM。
|
||||
* 返回原分页结构,list 每行补充 reservationTimeGroups(日期及连续时段)和去重后的 days;不扩大月份范围。
|
||||
*/
|
||||
public Pagination reservationPage(int pageNumber, int pageSize, Sql sql, String month) {
|
||||
Pagination page = listPage(pageNumber,pageSize,sql);
|
||||
List<Record> rows = page.getList();
|
||||
if (rows.isEmpty()) return page;
|
||||
List<String> sqids = rows.stream().map(row -> row.getString("sqid")).collect(Collectors.toList());
|
||||
List<NutMap> slots = listMap(Sqls.create("select sqid,reserve_day,start_time,end_time from activity_site_reserve "
|
||||
+ "where sqid in (@sqids) and (@month='' or left(reserve_day,7)=@month)")
|
||||
.setParam("sqids",sqids).setParam("month",Strings.sNull(month)));
|
||||
Map<String,List<NutMap>> grouped = slots.stream().collect(Collectors.groupingBy(row -> row.getString("sqid")));
|
||||
// 仅批量读取当前页申请,避免逐条查询及 group_concat 长度限制导致的时段丢失。
|
||||
List<Map<String,Object>> output = new ArrayList<>();
|
||||
for (Record row : rows) {
|
||||
List<NutMap> groups = groupReservationTimes(grouped.getOrDefault(row.getString("sqid"),Collections.emptyList()));
|
||||
// Record 会将新增键转成小写;转换后保留既有字段名及 Vue 所需的驼峰分组字段。
|
||||
Map<String,Object> item = new LinkedHashMap<>(row);
|
||||
item.put("reservationTimeGroups",groups);
|
||||
item.put("days",groups.size());
|
||||
output.add(item);
|
||||
}
|
||||
page.setList(output);
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* slots为原始reserve_day/start_time/end_time记录,返回按日期排序的[{date,ranges:[{start,end}]}]。
|
||||
* 仅合并同日首尾相接的时段,跨日和有间隔的场次独立保留;不改变原始预约记录。
|
||||
*/
|
||||
public List<NutMap> groupReservationTimes(List<NutMap> slots) {
|
||||
List<NutMap> ordered=new ArrayList<>(slots);
|
||||
ordered.sort(Comparator.comparing((NutMap row)->row.getString("reserve_day"))
|
||||
.thenComparing(row->row.getString("start_time")).thenComparing(row->row.getString("end_time")));
|
||||
List<NutMap> groups=new ArrayList<>();
|
||||
String day=null;
|
||||
List<NutMap> ranges=null;
|
||||
for(NutMap slot:ordered) {
|
||||
String date=slot.getString("reserve_day"),start=slot.getString("start_time"),end=slot.getString("end_time");
|
||||
if(!Objects.equals(day,date)) {
|
||||
day=date;ranges=new ArrayList<>();
|
||||
groups.add(NutMap.NEW().setv("date",date).setv("ranges",ranges));
|
||||
}
|
||||
NutMap previous=ranges.isEmpty() ? null : ranges.get(ranges.size()-1);
|
||||
if(previous!=null && Objects.equals(previous.getString("end"),start))previous.setv("end",end);
|
||||
else ranges.add(NutMap.NEW().setv("start",start).setv("end",end));
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** 公共展示字段,保留既有组件所需别名,并显示预约类型和所属协会。 */
|
||||
private String baseSelect() {
|
||||
return "select b.*,u.loginname,u.sex,s.name site_name,c.name club_name,un.unionname union_name,st.stateName state_name,st.stateColor state_color,st.stateAuditType,"
|
||||
@@ -245,8 +371,10 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
NutMap result = listMap(Sqls.create(baseSelect() + " where b.id=@id").setParam("id",id)).get(0);
|
||||
result.put("auditListTable",history);
|
||||
// 日历仅携带本次申请的时段,避免通过详情获取其他申请人的资料。
|
||||
result.put("site_info", listMap(Sqls.create("select b.*,st.stateAuditType from activity_site_reserve b left join "
|
||||
+ STATES + " st on st.stateId=b.reserve_state where b.sqid=@sqid").setParam("sqid",b.getSqid())));
|
||||
List<NutMap> slots=listMap(Sqls.create("select b.*,st.stateAuditType from activity_site_reserve b left join "
|
||||
+ STATES + " st on st.stateId=b.reserve_state where b.sqid=@sqid").setParam("sqid",b.getSqid()));
|
||||
result.put("site_info",slots);
|
||||
result.put("reservationTimeGroups",groupReservationTimes(slots));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -260,6 +388,7 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
if (ids == null || ids.length == 0 || pass == null) throw new IllegalArgumentException("请选择申请及审核结果");
|
||||
if (Strings.isBlank(opinion) || opinion.length() > 500) throw new IllegalArgumentException("请填写不超过500字的审核意见");
|
||||
Set<String> handled = new HashSet<>();
|
||||
List<ActivitySiteReserve> notifications = new ArrayList<>();
|
||||
// 固定加锁顺序降低批量审核之间的死锁风险。
|
||||
List<String> ordered = Arrays.stream(ids).sorted().collect(Collectors.toList());
|
||||
for (String id : ordered) {
|
||||
@@ -288,7 +417,11 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
if (next == SCHOOL) createTask(SCHOOL,b);
|
||||
else if (next == REJECT) sysLocalProcessService.refuseProcess(process(b),"审核拒绝,流程结束");
|
||||
else sysLocalProcessService.completeProcess(process(b));
|
||||
b.setReserve_state(next);
|
||||
notifications.add(b);
|
||||
}
|
||||
// 整批审核业务处理完成后再通知,避免后续申请校验失败时提前发送前几条消息。
|
||||
for (ActivitySiteReserve booking : notifications) notifyBooking(booking,booking.getReserve_state(),opinion);
|
||||
}
|
||||
|
||||
/** 返回不可撤回原因;仅最近一次有效审核的本人且仍拥有该组织审核资格可以撤回。 */
|
||||
@@ -332,6 +465,8 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
throw new IllegalArgumentException("场地或场地类型已停用,不能恢复待审核预约");
|
||||
assignments(node,booking);
|
||||
List<ActivitySiteReserve> slots = query(Cnd.where("sqid","=",booking.getSqid()));
|
||||
// 恢复预约同样受最新开放配置与禁用时间约束,不复活已禁用的时段。
|
||||
for (ActivitySiteReserve slot : slots) siteInfoService.validateBookingSlot(site,slot.getReserve_day(),slot.getStart_time(),slot.getEnd_time());
|
||||
if (Integer.valueOf(REJECT).equals(booking.getReserve_state())) {
|
||||
for (ActivitySiteReserve slot : slots) {
|
||||
List<ActivitySiteReserve> occupied = query(Cnd.where("site_id","=",booking.getSite_id())
|
||||
@@ -341,7 +476,7 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
boolean conflict = Integer.valueOf(1).equals(booking.getReserve_type())
|
||||
? occupied.stream().anyMatch(row -> !Integer.valueOf(1).equals(row.getReserve_type())
|
||||
|| row.getReserve_person_id().equals(slot.getReserve_person_id()))
|
||||
|| site.getLimitNum() == null || occupied.size() >= site.getLimitNum()
|
||||
|| site.getLimitNum() == null || siteInfoService.occupiedCount(occupied,slot.getStart_time(),slot.getEnd_time()) >= site.getLimitNum()
|
||||
: !occupied.isEmpty();
|
||||
if (conflict) throw new IllegalArgumentException("原预约时段已被占用或人数已满,不能撤回拒绝结果");
|
||||
}
|
||||
@@ -378,6 +513,26 @@ public class SiteBookingService extends BaseServiceImpl<ActivitySiteReserve> {
|
||||
createTask(node,booking);
|
||||
}
|
||||
|
||||
/**
|
||||
* id 为申请中任一时段的记录主键,返回 void;仅系统管理员可删除全部已撤销的申请。
|
||||
* 按 sqid 锁定并删除所有时段,关联流程和待办沿用系统删除标记,保留流程痕迹。
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void deleteCancelled(String id) {
|
||||
if (!ShiroUtil.hasRole("sysadmin")) throw new IllegalArgumentException("仅系统管理员可删除已撤销预约");
|
||||
ActivitySiteReserve booking = fetch(id);
|
||||
if (booking == null) throw new IllegalArgumentException("预约不存在或已删除");
|
||||
if (Strings.isBlank(booking.getSqid())) throw new IllegalArgumentException("预约申请编号缺失,不能删除");
|
||||
List<NutMap> slots = listMap(Sqls.create("select id,reserve_state from activity_site_reserve where sqid=@sqid order by id for update")
|
||||
.setParam("sqid",booking.getSqid()));
|
||||
// 锁后再次检查整组状态,避免重复删除或误删同一申请中尚未撤销的时段。
|
||||
if (slots.isEmpty()) throw new IllegalArgumentException("预约不存在或已删除");
|
||||
if (slots.stream().anyMatch(slot -> slot.getInt("reserve_state") != CANCEL))
|
||||
throw new IllegalArgumentException("只能删除已撤销的预约");
|
||||
sysLocalProcessService.deleteProcessInstance(process(booking));
|
||||
dao().clear(ActivitySiteReserve.class,Cnd.where("sqid","=",booking.getSqid()));
|
||||
}
|
||||
|
||||
/** 仅申请人可撤销尚未处理的首节点申请;保留取消状态及流程记录。 */
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void cancel(String id) {
|
||||
|
||||
@@ -6,6 +6,17 @@ import io.v.nutz.zhgh.activity.models.ActivitySiteInfo;
|
||||
import io.v.nutz.zhgh.activity.services.SiteInfoService;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import io.v.nutz.zhgh.activity.models.ActivityType;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import java.util.*;
|
||||
import java.time.*;
|
||||
import org.nutz.json.Json;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import io.v.nutz.zhgh.activity.models.ActivitySiteReserve;
|
||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||
|
||||
/**
|
||||
* @author: Aaron
|
||||
@@ -18,4 +29,149 @@ public class SiteInfoServiceImpl extends BaseServiceImpl<ActivitySiteInfo> imple
|
||||
public SiteInfoServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
|
||||
/**
|
||||
* form 包含名称、校区、地址、管理员、电话、人数、开放时段和开关;editing=true 时必须携带有效 id。
|
||||
* 返回 void;校区按 sys_dq 校验,typeId 不信任前端传值,避免隐藏类型选择后产生不可预约的场地。
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void saveManagedSite(ActivitySiteInfo form, boolean editing) {
|
||||
if (form == null || !"职工之家".equals(form.getName())) throw new IllegalArgumentException("场地名称请选择职工之家");
|
||||
if (Arrays.asList("青山湖科创中心", "不固定校区").contains(form.getCampus()) || Strings.isBlank(form.getCampus()) || form.getCampus().length() > 100
|
||||
|| dao().count("sys_dq", Cnd.where("dq_name", "=", form.getCampus())) == 0)
|
||||
throw new IllegalArgumentException("请选择有效校区");
|
||||
if (Strings.isBlank(form.getAddress()) || form.getAddress().length() > 100)
|
||||
throw new IllegalArgumentException("请填写不超过100字的场地地址");
|
||||
if (Strings.isBlank(form.getContact_person()) || form.getContact_person().length() > 32)
|
||||
throw new IllegalArgumentException("请填写不超过32字的场地管理员");
|
||||
if (Strings.isBlank(form.getContact_phone()) || form.getContact_phone().length() > 30)
|
||||
throw new IllegalArgumentException("请填写不超过30字的联系方式");
|
||||
if (form.getLimitNum() == null || form.getLimitNum() < 1 || form.getLimitNum() > 100)
|
||||
throw new IllegalArgumentException("限定人数应为1至100");
|
||||
if (form.getState() == null) throw new IllegalArgumentException("请选择开启状态");
|
||||
ActivitySiteInfo old = editing && Strings.isNotBlank(form.getId()) ? fetch(form.getId()) : null;
|
||||
if (editing && old == null) throw new IllegalArgumentException("场地不存在,请刷新列表");
|
||||
List<ActivityType> types = dao().query(ActivityType.class,
|
||||
Cnd.where("meetingTypeName", "=", "职工之家").and("enabled", "=", true));
|
||||
if (types.size() != 1) throw new IllegalArgumentException("请配置唯一且启用的职工之家场地类型");
|
||||
// 保存和提交使用同一场地行锁,避免配置变更与预约提交并发穿透。
|
||||
if (editing) {
|
||||
org.nutz.dao.sql.Sql lock = org.nutz.dao.Sqls.create("select id from activity_site_info where id=@id for update").setParam("id", form.getId());
|
||||
dao().execute(lock);
|
||||
}
|
||||
normalizeSchedule(form, editing);
|
||||
form.setTypeId(types.get(0).getId());
|
||||
// 控件已隐藏:新增不限制性别,编辑保留原值,避免未展示的字段被请求篡改。
|
||||
form.setSexLimit(editing && old.getSexLimit()!=null ? old.getSexLimit() : 0);
|
||||
if (editing) {
|
||||
form.setCreate_username(old.getCreate_username());
|
||||
form.setCreate_time(old.getCreate_time());
|
||||
updateIgnoreNull(form);
|
||||
} else {
|
||||
form.setId(null);
|
||||
insert(form);
|
||||
}
|
||||
}
|
||||
|
||||
/** 严格解析分钟,24:00仅用于结束边界,不接受跨日或秒级配置。 */
|
||||
private int minute(String value, boolean end) {
|
||||
if (end && "24:00".equals(value)) return 1440;
|
||||
if (value == null || !value.matches("[0-2][0-9]:[0-5][0-9]")) throw new IllegalArgumentException("请填写有效的HH:mm时间");
|
||||
try { LocalTime t = LocalTime.parse(value); return t.getHour()*60+t.getMinute(); }
|
||||
catch (RuntimeException e) { throw new IllegalArgumentException("时间必须在00:00至24:00之间"); }
|
||||
}
|
||||
|
||||
private String clockText(int value) {
|
||||
return String.format("%02d:%02d",value/60,value%60);
|
||||
}
|
||||
|
||||
/** 同一分钟先合并结束和开始事件,首尾相接的预约不会重复占用。 */
|
||||
public int occupiedCount(List<ActivitySiteReserve> rows,String start,String end) {
|
||||
SortedMap<Integer,Integer> events=new TreeMap<>();
|
||||
int from=minute(start,false),to=minute(end,true),current=0,peak=0;
|
||||
for(ActivitySiteReserve row:rows){
|
||||
int a=Math.max(from,minute(row.getStart_time(),false)),b=Math.min(to,minute(row.getEnd_time(),true));
|
||||
if(a<b){events.merge(a,1,Integer::sum);events.merge(b,-1,Integer::sum);}
|
||||
}
|
||||
for(int change:events.values()){current+=change;peak=Math.max(peak,current);}
|
||||
return peak;
|
||||
}
|
||||
|
||||
/** 两套原始配置独立保存,仅将当前模式展开为既有open_hours,兼容原有列表和日历。 */
|
||||
private void normalizeSchedule(ActivitySiteInfo form, boolean editing) {
|
||||
int mode = form.getReserveTimeType()==null ? (editing ? 1 : 2) : form.getReserveTimeType();
|
||||
if (mode!=1 && mode!=2) throw new IllegalArgumentException("请选择有效预约时间段类型");
|
||||
form.setReserveTimeType(mode);
|
||||
List<NutMap> ranges = mode==2 ? Collections.singletonList(form.getFullDayOpenHour()) : form.getSegmentedOpenHours();
|
||||
// 老场地第一次编辑没有独立配置时沿用原始场次,不擅自缩短预约时长。
|
||||
if (mode==1 && (ranges==null || ranges.isEmpty())) ranges=Json.fromJsonAsList(NutMap.class,Json.toJson(form.getOpen_hours()));
|
||||
if (ranges==null || ranges.isEmpty()) throw new IllegalArgumentException("请配置至少一个开放时间段");
|
||||
List<NutMap> normalized=new ArrayList<>(), slots=new ArrayList<>();
|
||||
for (NutMap row:ranges) {
|
||||
if(row==null) throw new IllegalArgumentException("请配置开放起止时间");
|
||||
int start=minute(row.getString("start_time"),false), end=minute(row.getString("end_time"),true);
|
||||
if(end<=start) throw new IllegalArgumentException("开放结束时间必须晚于开始时间");
|
||||
int unit=end-start;
|
||||
if(row.get("timeUnit")!=null) {
|
||||
try { unit=Integer.parseInt(String.valueOf(row.get("timeUnit"))); }
|
||||
catch(RuntimeException e){throw new IllegalArgumentException("预约时间单位必须为正整数分钟");}
|
||||
} else if(mode==2) throw new IllegalArgumentException("请配置全天候预约时间单位");
|
||||
if(unit<1 || unit>end-start || (end-start)%unit!=0) throw new IllegalArgumentException("预约时间单位必须为正整数,且能整除开放时长");
|
||||
for(NutMap previous:normalized) if(start<minute(previous.getString("end_time"),true) && end>minute(previous.getString("start_time"),false))
|
||||
throw new IllegalArgumentException("分段场次不能互相重叠");
|
||||
normalized.add(NutMap.NEW().setv("start_time",clockText(start)).setv("end_time",clockText(end)).setv("timeUnit",unit));
|
||||
for(int cursor=start;cursor<end;cursor+=unit) slots.add(NutMap.NEW().setv("start_time",clockText(cursor)).setv("end_time",clockText(cursor+unit)));
|
||||
}
|
||||
slots.sort(Comparator.comparing(row->row.getString("start_time")));
|
||||
if(mode==1)form.setSegmentedOpenHours(normalized);else form.setFullDayOpenHour(normalized.get(0));
|
||||
form.setOpen_hours(slots);
|
||||
List<NutMap> disabled=form.getNotApplyTimeList()==null ? new ArrayList<>() : form.getNotApplyTimeList();
|
||||
for(NutMap row:disabled){
|
||||
if(row==null)throw new IllegalArgumentException("请填写完整的禁用时间");
|
||||
try{LocalDate.parse(row.getString("date"));}catch(RuntimeException e){throw new IllegalArgumentException("请选择有效的禁用日期");}
|
||||
if(minute(row.getString("endTime"),true)<=minute(row.getString("startTime"),false))throw new IllegalArgumentException("禁用结束时间必须晚于开始时间");
|
||||
}
|
||||
form.setNotApplyTimeList(disabled);
|
||||
}
|
||||
|
||||
/** 半开区间判定:结束恰好等于禁用开始允许预约,有实际交叉才拦截。 */
|
||||
public void validateBookingSlot(ActivitySiteInfo site,String day,String start,String end) {
|
||||
LocalDate date;
|
||||
try{date=LocalDate.parse(day);}catch(RuntimeException e){throw new IllegalArgumentException("请选择有效预约日期");}
|
||||
int from=minute(start,false),to=minute(end,true);
|
||||
if(to<=from)throw new IllegalArgumentException("预约结束时间必须晚于开始时间");
|
||||
if(Boolean.TRUE.equals(site.getWorkday()) && date.getDayOfWeek().getValue()>=6)throw new IllegalArgumentException("该场地仅支持工作日预约");
|
||||
List<NutMap> open=Json.fromJsonAsList(NutMap.class,Json.toJson(site.getOpen_hours()));
|
||||
if(open==null || open.stream().noneMatch(row->start.equals(row.getString("start_time")) && end.equals(row.getString("end_time"))))
|
||||
throw new IllegalArgumentException("所选时段不在当前开放场次中,请刷新重选");
|
||||
if(site.getNotApplyTimeList()!=null)for(NutMap range:site.getNotApplyTimeList()){
|
||||
if(day.equals(range.getString("date")) && from<minute(range.getString("endTime"),true) && to>minute(range.getString("startTime"),false))
|
||||
throw new IllegalArgumentException("所选时段与禁用时间冲突");
|
||||
}
|
||||
}
|
||||
|
||||
/** PC/H5共享可约判定,按重叠时段查占用,兼容历史整段预约与新拆分时段。 */
|
||||
public List<NutMap> availableSlots(String siteId,String day,Integer reserveType){
|
||||
ActivitySiteInfo site=fetch(siteId);
|
||||
if(site==null || !Boolean.TRUE.equals(site.getState()))throw new IllegalArgumentException("场地不存在或已停用");
|
||||
LocalDate date=LocalDate.parse(day);
|
||||
String uid=String.valueOf(ShiroUtil.getPrincipalProperty("id"));
|
||||
List<NutMap> slots=Json.fromJsonAsList(NutMap.class,Json.toJson(site.getOpen_hours()));
|
||||
if(slots==null)return new ArrayList<>();
|
||||
for(NutMap slot:slots){
|
||||
String start=slot.getString("start_time"),end=slot.getString("end_time"),reason=null;
|
||||
List<ActivitySiteReserve> occupied=dao().query(ActivitySiteReserve.class,Cnd.where("site_id","=",siteId).and("reserve_day","=",day)
|
||||
.and("start_time","<",end).and("end_time",">",start).and("reserve_state","not in",Arrays.asList(4040,4050)));
|
||||
try{validateBookingSlot(site,day,start,end);}catch(IllegalArgumentException e){reason=e.getMessage();}
|
||||
if(reason==null && !LocalDateTime.of(date,LocalTime.parse(start)).isAfter(LocalDateTime.now()))reason="预约时段已过期";
|
||||
if(reason==null && occupied.stream().anyMatch(row->uid.equals(row.getReserve_person_id())))reason="已预约";
|
||||
if(reason==null && (occupied.stream().anyMatch(row->!Integer.valueOf(1).equals(row.getReserve_type()))
|
||||
|| site.getLimitNum()==null || occupiedCount(occupied,start,end)>=site.getLimitNum()
|
||||
|| reserveType!=null && reserveType!=1 && !occupied.isEmpty()))reason="已约满";
|
||||
slot.setv("code",reason==null ? 1 : -2).setv("msg",reason==null ? "可预约" : reason)
|
||||
.setv("limitNum",site.getLimitNum()).setv("reserveNum",occupiedCount(occupied,start,end)).setv("disabled",reason!=null)
|
||||
.setv("backColor",reason==null ? "#e8ffef" : "").setv("color",reason==null ? "#52986a" : "");
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,8 @@ public class GhkhLssjcxController {
|
||||
if (ShiroUtil.hasAnyRoles("sysadmin,SchoolUnionAdmin")) {
|
||||
result.add(new NutMap().setv("title", title).setv("url", "/platform/ghkh/xghsh").setv("iconClass", vi.getIconByPath("/platform/ghkh/xghsh")).setv("label", "校工会审核").setv("number", getXghCount()));
|
||||
}
|
||||
return result;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(result, "url");
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ public class dbYxAgentController {
|
||||
if (ShiroUtil.hasAnyRoles("sysadmin,SchoolUnionAdmin")) {
|
||||
result.add(new NutMap().setv("title", title).setv("url", UNION).setv("iconClass", vi.getIconByPath(UNION)).setv("label", "待校工会审核").setv("number", getUnionCount()));
|
||||
}
|
||||
return result;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(result, "url");
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -80,7 +80,8 @@ public class ActivitybxAgentController {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(result, "url");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ public class LxyAgentController {
|
||||
result.add(new NutMap().setv("title", title).setv("url", FGHSH).setv("iconClass", vi.getIconByPath(FGHSH)).setv("label", "待院级工会审核").setv("number", getFghshCount()));
|
||||
result.add(new NutMap().setv("title", title).setv("url", XGHSH).setv("iconClass", vi.getIconByPath(XGHSH)).setv("label", "待校工会审核").setv("number", getXghshCount().size()));
|
||||
}
|
||||
return result;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(result, "url");
|
||||
}
|
||||
|
||||
private List<Record> getXghshCount() {
|
||||
|
||||
@@ -171,8 +171,8 @@ public class MainPageNeedItemsController {
|
||||
@At
|
||||
@ViReturn
|
||||
@RequiresAuthentication
|
||||
public Object getNeedItems() {
|
||||
return needItemsService.getNeedItems();
|
||||
public Object getNeedItems(Boolean mobile) {
|
||||
return needItemsService.getNeedItems(Boolean.TRUE.equals(mobile));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ import java.util.List;
|
||||
public interface MainPageNeedItemsService extends ViService<NeedItems> {
|
||||
|
||||
List<NeedItems> getNeedItems();
|
||||
/** mobile 指明跳转端,返回当前权限可访问且数量大于零的待办。 */
|
||||
List<NeedItems> getNeedItems(boolean mobile);
|
||||
}
|
||||
|
||||
+7
-2
@@ -27,7 +27,10 @@ public class MainPageNeedItemsServiceImpl extends ViServiceImpl<NeedItems> imple
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NeedItems> getNeedItems() {
|
||||
public List<NeedItems> getNeedItems() { return getNeedItems(false); }
|
||||
|
||||
@Override
|
||||
public List<NeedItems> getNeedItems(boolean mobile) {
|
||||
//查询数据库中的待办事项
|
||||
List<NeedItems> needItemsList = dao().queryByJoin(NeedItems.class, "^needItemsSource$", Cnd.NEW());
|
||||
|
||||
@@ -91,6 +94,8 @@ public class MainPageNeedItemsServiceImpl extends ViServiceImpl<NeedItems> imple
|
||||
|
||||
List<NeedItems> result = needItemsList.stream().filter(v -> Strings.isNotBlank(v.getHref())
|
||||
&& menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && x.getHref().equals(v.getHref()))).distinct().collect(Collectors.toList());
|
||||
return result.stream().filter(v -> v.getCount() > 0).collect(Collectors.toList());
|
||||
return result.stream().filter(v -> v.getCount() > 0)
|
||||
.filter(v -> io.v.nutz.sys.services.TodoAccessService.canAccess(mobile ? v.getMobileHref() : v.getHref()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ public class ManuscriptAgentController {
|
||||
|
||||
Sys_user user = (Sys_user) ShiroUtil.getPrincipal();
|
||||
List<Sys_menu> menus = Optional.ofNullable(user).orElse(new Sys_user()).getMenus();
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).distinct().collect(Collectors.toList());
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).filter(v -> io.v.nutz.sys.services.TodoAccessService.canAccess(v.getString("url"))).distinct().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -148,8 +148,7 @@ public class SiteInfoMobileController {
|
||||
info.name,
|
||||
info.address,
|
||||
`as`.stateAuditType,
|
||||
count(sqid) days,
|
||||
GROUP_CONCAT(reserve_day order by reserve_day asc) concat_day
|
||||
count(distinct reserve_day) days
|
||||
from
|
||||
activity_site_reserve ar
|
||||
left join activity_site_info info on info.id = ar.site_id
|
||||
@@ -176,7 +175,8 @@ public class SiteInfoMobileController {
|
||||
|
||||
sql.setCondition(cnd);
|
||||
|
||||
return siteInfoService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||
return siteBookingService.reservationPage(pageForm.getPageNumber(), pageForm.getPageSize(), sql,
|
||||
Boolean.TRUE.equals(timeSwitch) ? null : time);
|
||||
}
|
||||
|
||||
@At("/getDetail")
|
||||
@@ -207,65 +207,9 @@ public class SiteInfoMobileController {
|
||||
@At("/getReserve")
|
||||
@Ok("json:full")
|
||||
@ViReturn
|
||||
public Object getReserve(String siteId, String day) {
|
||||
|
||||
//获取场地的开放时间
|
||||
ActivitySiteInfo fetch = siteInfoService.fetch(siteId);
|
||||
List<NutMap> hours = fetch.getOpen_hours();
|
||||
Integer limitNum = fetch.getLimitNum();
|
||||
|
||||
hours.forEach(item -> {
|
||||
String startTime = item.getString("start_time");
|
||||
String endTime = item.getString("end_time");
|
||||
item.put("code", 1);
|
||||
item.put("msg", "可预约");
|
||||
item.put("backColor", "#e8ffef");
|
||||
item.put("color", "#52986a");
|
||||
item.put("limitNum", limitNum);
|
||||
//如果这个时间段过了,显示已过期
|
||||
String time = day + " " + startTime;
|
||||
int compare = DateUtil.compare(new Date(), DateUtil.parse(time), "yyyy-MM-dd HH:mm");
|
||||
if(compare > 0) {
|
||||
item.put("code", -1);
|
||||
item.put("msg", "已过期");
|
||||
item.put("backColor", "");
|
||||
item.put("color", "");
|
||||
}
|
||||
|
||||
List<ActivitySiteReserve> list = siteReserveService.query(Cnd.NEW()
|
||||
.and("reserve_day", "=", day)
|
||||
.and("start_time", "=", startTime)
|
||||
.and("end_time", "=", endTime)
|
||||
.and("site_id", "=", siteId)
|
||||
.and("reserve_state", "not in", java.util.Arrays.asList(4040,4050)));
|
||||
|
||||
//如果这个时间段被单位预约了,直接显示约满
|
||||
ActivitySiteReserve reserve1 = list.stream().filter(o -> o.getReserve_type() != 1).findAny().orElse(null);
|
||||
if(reserve1 != null) {
|
||||
item.put("code", -2);
|
||||
item.put("msg", "已约满");
|
||||
item.put("backColor", "");
|
||||
item.put("color", "");
|
||||
}
|
||||
//如果这个时间段预约的人数满了,显示约满
|
||||
if(list.size() >= limitNum) {
|
||||
item.put("code", -1);
|
||||
item.put("msg", "已约满");
|
||||
item.put("backColor", "");
|
||||
item.put("color", "");
|
||||
}
|
||||
//如果预约过了,显示已预约
|
||||
ActivitySiteReserve reserve = list.stream().filter(o -> o.getReserve_person_id().equals(ShiroUtil.getPrincipalProperty("id"))).findAny().orElse(null);
|
||||
if(reserve != null && reserve.getReserve_type() != 2) {
|
||||
item.put("code", -1);
|
||||
item.put("msg", "已预约");
|
||||
item.put("backColor", "#0e78c5");
|
||||
item.put("color", "#fff3f3");
|
||||
}
|
||||
item.put("reserveNum", list.size());
|
||||
});
|
||||
|
||||
return hours;
|
||||
/** siteId为场地,day为日期,reserveType为1个人/2单位/3协会;返回带code/msg的可预约场次数组。 */
|
||||
public Object getReserve(String siteId, String day, Integer reserveType) {
|
||||
return siteInfoService.availableSlots(siteId,day,reserveType);
|
||||
}
|
||||
|
||||
@At("/reserveDo")
|
||||
|
||||
+2
-1
@@ -109,6 +109,7 @@ public class MemberAgendaController {
|
||||
|
||||
Sys_user user = (Sys_user) ShiroUtil.getPrincipal();
|
||||
List<Sys_menu> menus = Optional.ofNullable(user).orElse(new Sys_user()).getMenus();
|
||||
return list.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).distinct().collect(Collectors.toList());
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return list.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").contains(x.getHref()))).filter(v -> io.v.nutz.sys.services.TodoAccessService.canAccess(v.getString("url"))).distinct().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -56,7 +56,8 @@ public class MemberSelfAgentController {
|
||||
.addv("mobileHref", "/mobile/member/edit?taskId=" + v.getId()));
|
||||
});
|
||||
}
|
||||
return list;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(list, "mobileHref");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import io.v.nutz.zhgh.staffmanage.member.model.MemberApplyRecord;
|
||||
import io.v.nutz.zhgh.staffmanage.member.service.MemberCommonService;
|
||||
import io.v.nutz.zhgh.staffmanage.member.service.impl.MemberCommonServiceImpl;
|
||||
import lombok.Getter;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
@@ -24,6 +26,7 @@ import java.util.stream.Collectors;
|
||||
* @name:MemberApplyController
|
||||
* @Date 2025/1/20 19:01
|
||||
* @注释 会员申请流程
|
||||
* 手机处理和查看地址必须指向 /h5;PC 页面依赖不能在手机 PJAX 容器内加载。
|
||||
*/
|
||||
|
||||
@Getter
|
||||
@@ -34,6 +37,7 @@ public enum MemberApplyToDoHandler {
|
||||
*/
|
||||
START_PROCESS() {
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void exec(MemberApplyRecord record, NutMap extra) {
|
||||
User user = dao.fetch(User.class, Cnd.where("id", "=", record.getUserId()));
|
||||
localProcessService.startProcess(
|
||||
@@ -42,7 +46,7 @@ public enum MemberApplyToDoHandler {
|
||||
"",
|
||||
record.getUserId(),
|
||||
"/platform/member/apply/mine",
|
||||
"/platform/member/apply/mine"
|
||||
"/platform/member/apply/mine/h5"
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -53,6 +57,7 @@ public enum MemberApplyToDoHandler {
|
||||
*/
|
||||
CREATE_APPLY_RE_MODIFY_TASK() {
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void exec(MemberApplyRecord record, NutMap extra) {
|
||||
Sql sql = Sqls.fetchString(Sqls.create("select loginname from sys_user where id = @id").setParam("id", record.getUserId()).toString());
|
||||
dao.execute(sql);
|
||||
@@ -66,8 +71,8 @@ public enum MemberApplyToDoHandler {
|
||||
List.of(loginname),
|
||||
"/platform/member/apply/mine",
|
||||
"/platform/member/apply/mine",
|
||||
"/platform/member/apply/mine",
|
||||
"/platform/member/apply/mine"
|
||||
"/platform/member/apply/mine/h5",
|
||||
"/platform/member/apply/mine/h5"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("MEMBER_APPLY@" + record.getId(), "退回重新修改");
|
||||
@@ -98,6 +103,7 @@ public enum MemberApplyToDoHandler {
|
||||
*/
|
||||
CREATE_SCHOOL_TASK() {
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void exec(MemberApplyRecord record, NutMap extra) {
|
||||
// 获取校工会会员管理员
|
||||
List<String> schoolLeaderLoginNames = memberService.getSchoolOrBranchUnionMemberAdminLoginNames("school", record.getUserId());
|
||||
@@ -109,8 +115,8 @@ public enum MemberApplyToDoHandler {
|
||||
schoolLeaderLoginNames,
|
||||
"/platform/member/apply/schoolUnion/audit",
|
||||
"/platform/member/apply/schoolUnion/audit",
|
||||
"/platform/member/apply/schoolUnion/audit",
|
||||
"/platform/member/apply/schoolUnion/audit"
|
||||
"/platform/member/apply/schoolUnion/audit/h5",
|
||||
"/platform/member/apply/schoolUnion/audit/h5"
|
||||
);
|
||||
|
||||
localProcessService.updateProcessNodeName("MEMBER_APPLY@" + record.getId(), "校工会审核");
|
||||
@@ -141,6 +147,7 @@ public enum MemberApplyToDoHandler {
|
||||
*/
|
||||
CREATE_UNION_TASK() {
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void exec(MemberApplyRecord record, NutMap extra) {
|
||||
List<String> unionLeaderLoginNames = extra.getAsList("unionLeaderLoginNames", String.class);
|
||||
localProcessService.createTask(
|
||||
@@ -151,8 +158,8 @@ public enum MemberApplyToDoHandler {
|
||||
unionLeaderLoginNames,
|
||||
"/platform/member/apply/branchUnion/audit",
|
||||
"/platform/member/apply/branchUnion/audit",
|
||||
"/platform/member/apply/branchUnion/audit",
|
||||
"/platform/member/apply/branchUnion/audit"
|
||||
"/platform/member/apply/branchUnion/audit/h5",
|
||||
"/platform/member/apply/branchUnion/audit/h5"
|
||||
);
|
||||
localProcessService.updateProcessNodeName("MEMBER_APPLY@" + record.getId(), "分工会审核");
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ public class WelfareAgendaController {
|
||||
public Object getAgenda() {
|
||||
List<NutMap> list = new ArrayList<>();
|
||||
if (!ShiroUtil.hasAnyRoles("flwyh01,sysadmin")) {
|
||||
return list;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(list, "url");
|
||||
}
|
||||
|
||||
int welfareMemberAuditNumber = service.count(Sqls.create("SELECT count(1) from welfare_member_apply_record WHERE stateId = @state").setParam("state", WelfareMemberApplyState.SCHOOL_UNION));
|
||||
@@ -48,6 +49,7 @@ public class WelfareAgendaController {
|
||||
// int welfareMemberChangeAuditNumber = service.count(Sqls.create("SELECT count(1) from member_change_record WHERE recordMode = @mode and stateId = @state").setParam("mode", MemberChangeRecordMode.WELFARE_MEMBER.getCode()).setParam("state", MemberChangeApplyState.SCHOOL_UNION));
|
||||
// list.add(new NutMap().addv("iconClass", vi.getIconByPath("/platform/welfare/member/change/audit")).addv("label", "福利会员变更审核").addv("number", welfareMemberChangeAuditNumber).addv("url", "/platform/welfare/member/change/audit"));
|
||||
|
||||
return list;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(list, "url");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ public class WelfareSelfAgentController {
|
||||
.addv("mobileHref", "/mobile/member/edit?taskId=" + v.getId()));
|
||||
});
|
||||
}
|
||||
return list;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(list, "mobileHref");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,8 @@ public class XhkhLssjcxController {
|
||||
if (ShiroUtil.hasAnyRoles("sysadmin,SchoolUnionAdmin")) {
|
||||
result.add(new NutMap().setv("title", title).setv("url", "/platform/xhkh/xghsh").setv("iconClass", vi.getIconByPath("/platform/xhkh/xghsh")).setv("label", "校工会审核").setv("number", getXghCount()));
|
||||
}
|
||||
return result;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(result, "url");
|
||||
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -59,7 +59,8 @@ public class CondolenceAgentController {
|
||||
result.add(new NutMap().setv("title", title).setv("url", ECR).setv("iconClass", vi.getIconByPath(ECR)).setv("label", "待工会常务副主席审核").setv("number", getEcrCount()));
|
||||
Sys_user user = (Sys_user) ShiroUtil.getPrincipal();
|
||||
List<Sys_menu> menus = Optional.ofNullable(user).orElse(new Sys_user()).getMenus();
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").equals(x.getHref()))).distinct().collect(Collectors.toList());
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return result.stream().filter(v -> Strings.isNotBlank(v.getString("url")) && menus.stream().anyMatch(x -> Strings.isNotBlank(x.getHref()) && v.getString("url").equals(x.getHref()))).filter(v -> io.v.nutz.sys.services.TodoAccessService.canAccess(v.getString("url"))).distinct().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ public class rxdjAgentController {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(result, "url");
|
||||
}
|
||||
|
||||
public Integer getUnitCount() {
|
||||
|
||||
+2
-1
@@ -54,7 +54,8 @@ public class singleChildAgentController {
|
||||
result.add(new NutMap().setv("url", SCHOOL).setv("iconClass", vi.getIconByPath(SCHOOL)).setv("label", "独生子女待工会主席审核").setv("number", getSchoolCount()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
// 保留原有任务数据范围,再按当前账号的目标页面权限过滤入口。
|
||||
return io.v.nutz.sys.services.TodoAccessService.filter(result, "url");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<el-tab-pane label="预约基本信息" name="2">
|
||||
<el-descriptions class="margin-top" :column="3" border>
|
||||
<el-descriptions-item label="预约类型">{{ {1:'个人预约',2:'单位预约',3:'协会预约'}[viewData.reserve_type] }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预约类型">{{ {1:'个人预约',2:'分工会预约',3:'协会预约'}[viewData.reserve_type] }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属协会">{{viewData.club_name || '—'}}</el-descriptions-item>
|
||||
<el-descriptions-item label="预约人">{{ viewData.reserve_person }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="2" label="场地名称"> {{ viewData.site_name }}</el-descriptions-item>
|
||||
@@ -41,8 +41,19 @@
|
||||
<el-descriptions-item label="工号">{{ viewData.loginname }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属单位">{{ viewData.reserve_person_unit }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ viewData.reserve_person_phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预约时间"> ({{ viewData.concat_day }}) {{ viewData.start_time }} - {{ viewData.end_time }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预约状态">
|
||||
<!-- 复用详情接口按日期合并的时段;独占一行,避免与状态、事由挤在同一行。 -->
|
||||
<el-descriptions-item label="预约时间" :span="3">
|
||||
<div class="reservation-times">
|
||||
<div v-for="group in viewData.reservationTimeGroups" :key="group.date" class="reservation-time-day">
|
||||
<span class="reservation-time-date">{{group.date}}</span>
|
||||
<div class="reservation-time-ranges">
|
||||
<span v-for="(range,index) in group.ranges" :key="index" class="reservation-time-range">{{range.start}}–{{range.end}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="!viewData.reservationTimeGroups || !viewData.reservationTimeGroups.length">—</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="预约状态" :span="3">
|
||||
<span v-if="[0].includes(viewData.stateAuditType)" style="color: #e6a23c">{{viewData.state_name}}</span>
|
||||
<span v-if="viewData.stateAuditType==3" style="color: #67c23a">{{viewData.state_name}}</span>
|
||||
<span v-if="viewData.stateAuditType==1" style="color: #f56c6c">{{viewData.state_name}}</span>
|
||||
@@ -146,4 +157,33 @@ module.exports = {
|
||||
.el-descriptions-item__cell {
|
||||
text-align: center !important;
|
||||
}
|
||||
/* 日期与完整时段整体居中,长时段列表仅在各时间段之间换行。 */
|
||||
.reservation-times {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
font-variant-numeric: tabular-nums;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.reservation-time-day {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 12px;
|
||||
padding: 3px 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
.reservation-time-date {
|
||||
flex: 0 0 88px;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
.reservation-time-ranges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 12px;
|
||||
}
|
||||
.reservation-time-range {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<!--# layout("/mobile/platform.html"){ #-->
|
||||
<style>
|
||||
/* 日期和每个时间段保持完整,同日连续场次由后端统一合并展示。 */
|
||||
.site-audit .reservation-times { text-align:left; font-variant-numeric:tabular-nums; }
|
||||
.site-audit .reservation-time-day { display:flex; align-items:baseline; flex-wrap:wrap; gap:4px 12px; padding:3px 0; line-height:22px; }
|
||||
.site-audit .reservation-time-date { flex:0 0 88px; white-space:nowrap; font-weight:500; }
|
||||
.site-audit .reservation-time-ranges { display:flex; flex-wrap:wrap; gap:2px 12px; }
|
||||
.site-audit .reservation-time-range { white-space:nowrap; }
|
||||
.site-audit .reservation-times { padding:4px 0 6px; font-size:14px; overflow-wrap:normal; }
|
||||
.site-audit .reservation-time-day { display:block; padding:4px 0; }
|
||||
.site-audit .reservation-time-date { display:block; color:#606266; }
|
||||
|
||||
.site-audit .van-doc-card { margin:14px; padding:12px; background:#fff; border-radius:10px; box-shadow:0 8px 12px #ebedf0; line-height:20px; font-size:15px; position:relative; }
|
||||
.site-audit .card-title { font-size:18px; font-weight:bold; color:#1867b0; padding-left:4px; }
|
||||
.site-audit .title-mark { position:absolute; left:0; top:12px; color:#1867b0; font-weight:bold; }
|
||||
@@ -33,7 +43,13 @@
|
||||
<div><span class="field-label">所属单位:</span>{{row.reserve_person_unit || '—'}}</div>
|
||||
<div><span class="field-label">预约类型:</span>{{typeName(row.reserve_type)}}</div>
|
||||
<div v-if="row.club_name"><span class="field-label">所属协会:</span>{{row.club_name}}</div>
|
||||
<div><span class="field-label">预约时段:</span>{{row.concat_day}}</div>
|
||||
<div><span class="field-label">预约时段:</span><div class="reservation-times">
|
||||
<div v-for="group in row.reservationTimeGroups" :key="group.date" class="reservation-time-day">
|
||||
<span class="reservation-time-date">{{group.date}}</span>
|
||||
<div class="reservation-time-ranges"><span v-for="(range,index) in group.ranges" :key="index" class="reservation-time-range">{{range.start}}–{{range.end}}</span></div>
|
||||
</div>
|
||||
<span v-if="!row.reservationTimeGroups || !row.reservationTimeGroups.length">—</span>
|
||||
</div></div>
|
||||
</div>
|
||||
<van-divider></van-divider>
|
||||
<div class="card-footer">
|
||||
@@ -134,7 +150,7 @@
|
||||
this.clearPopupHistory(() => pjaxReplace('/mobile/index'))
|
||||
}
|
||||
},
|
||||
typeName(type) { return {1:'个人预约',2:'单位预约',3:'协会预约'}[type] || '—' },
|
||||
typeName(type) { return {1:'个人预约',2:'分工会预约',3:'协会预约'}[type] || '—' },
|
||||
filterOpened() { this.$set(this, 'filterShow', true) },
|
||||
filterClosed() { this.$set(this, 'filterShow', false) },
|
||||
canReview(row) {
|
||||
|
||||
@@ -19,7 +19,13 @@ const SITE_AUDIT_INFO = {
|
||||
<van-cell title="当前状态">
|
||||
<template #default><span :style="{color: viewData.state_color || null}">{{viewData.state_name}}</span></template>
|
||||
</van-cell>
|
||||
<van-cell title="预约时段" :label="viewData.concat_day"></van-cell>
|
||||
<van-cell title="预约时段"><template #label><div class="reservation-times">
|
||||
<div v-for="group in viewData.reservationTimeGroups" :key="group.date" class="reservation-time-day">
|
||||
<span class="reservation-time-date">{{group.date}}</span>
|
||||
<div class="reservation-time-ranges"><span v-for="(range,index) in group.ranges" :key="index" class="reservation-time-range">{{range.start}}–{{range.end}}</span></div>
|
||||
</div>
|
||||
<span v-if="!viewData.reservationTimeGroups || !viewData.reservationTimeGroups.length">—</span>
|
||||
</div></template></van-cell>
|
||||
<van-cell title="预约事由" :label="viewData.reserve_cause" class="site-audit-text"></van-cell>
|
||||
</template>
|
||||
</van-cell-group>
|
||||
@@ -40,7 +46,7 @@ const SITE_AUDIT_INFO = {
|
||||
props: {reviewApi: {type: String, required: true}},
|
||||
data() { return {viewData: {}, activeName: 'basic', expanded: true, detailLoading: false, requestId: 0} },
|
||||
methods: {
|
||||
typeName(type) { return {1:'个人预约',2:'单位预约',3:'协会预约'}[type] || '—' },
|
||||
typeName(type) { return {1:'个人预约',2:'分工会预约',3:'协会预约'}[type] || '—' },
|
||||
toggleExpanded() { this.$set(this, 'expanded', !this.expanded) },
|
||||
// id 为预约记录主键;返回预约字段及 auditListTable 历史,loaded 通知父页核验可审核状态。
|
||||
onOpen(id) {
|
||||
|
||||
@@ -222,7 +222,7 @@ layout("/mobile/platform.html"){
|
||||
|
||||
<div style="margin-top: 10px">
|
||||
<van-row>
|
||||
<van-col span="24"><span style="color: grey"> 联系人:</span>{{(o.unitname
|
||||
<van-col span="24"><span style="color: grey">场地管理员:</span>{{(o.unitname
|
||||
? o.unitname :
|
||||
'') + ' ' + o.contact_person}}
|
||||
</van-col>
|
||||
@@ -285,7 +285,7 @@ layout("/mobile/platform.html"){
|
||||
typeFilterShow:false, formLoading:false,
|
||||
tarBarActive: 0,
|
||||
list: ['a', 'b'],
|
||||
typeList: [{text:'全部场地类型',value:''}],
|
||||
typeList: [],
|
||||
siteId: '',
|
||||
// 初始化完成前禁止列表触发请求;requesting 独立于 Vant 的 loading 双向绑定。
|
||||
listReady: false, requesting: false, requestVersion: 0, listError: false,
|
||||
@@ -385,17 +385,28 @@ layout("/mobile/platform.html"){
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// 类型加载结束后统一启动首屏;类型请求失败时仍允许查看全部场地。
|
||||
// 首屏取得职工之家实际类型 ID 后才查询;缺失或失败时不降级为全部类型。
|
||||
siteTypeUtil.getAllType().then((rows) => {
|
||||
if (this._isDestroyed || this._isBeingDestroyed) return
|
||||
this.$set(this, 'typeList', [{text:'全部场地类型',value:''}].concat(rows))
|
||||
}, () => {
|
||||
if (!this._isDestroyed && !this._isBeingDestroyed) vant.Toast('场地类型加载失败,先显示全部场地')
|
||||
}).always(() => {
|
||||
if (this._isDestroyed || this._isBeingDestroyed) return
|
||||
this.$set(this.pageForm, 'typeId', '')
|
||||
this.$set(this, 'typeList', rows)
|
||||
if (!rows.length) {
|
||||
this.$set(this, 'listError', true)
|
||||
vant.Toast('未配置职工之家场地类型')
|
||||
return
|
||||
}
|
||||
this.$set(this.pageForm, 'typeId', rows[0].value)
|
||||
this.$set(this, 'listReady', true)
|
||||
this.doSearch()
|
||||
}, () => {
|
||||
if (this._isDestroyed || this._isBeingDestroyed) return
|
||||
this.$set(this, 'listError', true)
|
||||
vant.Toast('场地类型加载失败,请重新进入')
|
||||
}).always(() => {
|
||||
if (this._isDestroyed || this._isBeingDestroyed) return
|
||||
if (!this.listReady) {
|
||||
this.$set(this, 'loading', false)
|
||||
this.$set(this, 'mLoading', false)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -163,6 +163,14 @@ layout("/mobile/platform.html"){
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 我的预约按日期展示完整时段,不再重复使用申请中某一条记录的起止时间。 */
|
||||
.site-my-times { font-variant-numeric:tabular-nums; line-height:22px; }
|
||||
.site-my-day { padding:4px 0; }
|
||||
.site-my-date { font-weight:500; }
|
||||
.site-my-ranges { display:flex; flex-wrap:wrap; gap:2px 12px; }
|
||||
.site-my-ranges span { white-space:nowrap; }
|
||||
</style>
|
||||
<div id="app" v-cloak>
|
||||
|
||||
<m-page-loading v-if="mLoading"></m-page-loading>
|
||||
@@ -250,10 +258,14 @@ layout("/mobile/platform.html"){
|
||||
</van-row>
|
||||
<van-row>
|
||||
<van-col span="24">
|
||||
<span style="color: grey; float: left">预约时间:</span>
|
||||
<span v-for="(item,index) in o.concat_day.split(',')">
|
||||
<div :style="index !== 0 ? 'text-indent: 5em' : ''">{{getTime(o, item)}}</div>
|
||||
</span>
|
||||
<span style="color: grey">预约时间:</span>
|
||||
<div class="site-my-times">
|
||||
<div v-for="group in o.reservationTimeGroups" :key="group.date" class="site-my-day">
|
||||
<div class="site-my-date">{{group.date}}</div>
|
||||
<div class="site-my-ranges"><span v-for="(range,index) in group.ranges" :key="index">{{range.start}}–{{range.end}}</span></div>
|
||||
</div>
|
||||
<span v-if="!o.reservationTimeGroups || !o.reservationTimeGroups.length">—</span>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
<van-row>
|
||||
@@ -265,10 +277,10 @@ layout("/mobile/platform.html"){
|
||||
<span style="color: #f56c6c" v-if="o.stateaudittype==1">{{o.statename}}</span>
|
||||
</van-col>
|
||||
</van-row>
|
||||
<van-row v-if="o.stateaudittype == '1'">
|
||||
<van-row v-if="Number(o.reserve_state) === 4040">
|
||||
<van-col span="24">
|
||||
<span style="color: grey">审核意见:</span>
|
||||
<span>{{JSON.parse(o.auditlist).find(x => x.auditState === false).auditOption}}</span>
|
||||
<span>{{rejectionOpinion(o)}}</span>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
@@ -364,6 +376,16 @@ layout("/mobile/platform.html"){
|
||||
$(document).off('pjax:beforeReplace.siteList', this._siteListDispose)
|
||||
},
|
||||
methods: {
|
||||
// 历史审核记录可能是 JSON 字符串、数组或空值;仅取最近一条拒绝意见,缺失时不阻断整页渲染。
|
||||
rejectionOpinion(row) {
|
||||
let history = row.auditlist || row.auditList || []
|
||||
if (typeof history === 'string') {
|
||||
try { history = JSON.parse(history) } catch (error) { return '—' }
|
||||
}
|
||||
if (!Array.isArray(history)) return '—'
|
||||
const rejection = history.slice().reverse().find(item => item && item.auditState === false)
|
||||
return rejection && rejection.auditOption || '—'
|
||||
},
|
||||
setPopupState(key, value) { this.$set(this, key, value) },
|
||||
// 列表由 PJAX replace 进入,导航返回明确回首页;有弹层时只关最上层。
|
||||
goBack() {
|
||||
@@ -427,11 +449,6 @@ layout("/mobile/platform.html"){
|
||||
backDo() {
|
||||
if(this.formLoading)return;if(!this.option.trim()){vant.Toast('请填写反馈意见');return}this.formLoading=true;$.post('/mobile/activity/site/info/backOption',{id:this.info.id,option:this.option}).then((res)=>{vant.Toast(res.msg);if(res.code===0)this.clearPopupHistory(()=>this.doSearch())}).always(()=>{this.formLoading=false});
|
||||
},
|
||||
getTime(o, day) {
|
||||
const week = new Date(day).getDay()
|
||||
const arr = ['日', '一', '二', '三', '四', '五', '六']
|
||||
return moment(day).format('MM月DD日') + ' 周' + arr[week] + ' ' + o.start_time + '-' + o.end_time
|
||||
},
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.tableData = []
|
||||
@@ -439,11 +456,27 @@ layout("/mobile/platform.html"){
|
||||
this.onLoad()
|
||||
},
|
||||
onLoad() {
|
||||
// 类型未加载成功时停止请求,避免筛选操作意外查询其他场地类型。
|
||||
if (this.pageForm.typeId == null) { this.$set(this,'loading',false); return }
|
||||
this.loading=true;return $.post('/mobile/activity/site/info/myReserve',this.pageForm).then((res)=>{if(res.code===0){this.tableData=this.tableData.concat(res.data.list);this.finished=this.tableData.length>=res.data.totalCount;if(!this.finished)this.pageForm.pageNumber++}else{vant.Toast(res.msg)}}).always(()=>{this.loading=false});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
siteTypeUtil.getAllType().then((rows)=>{this.typeList=[{text:'全部场地类型'}].concat(rows);this.$set(this.pageForm,'typeId',this.typeList[0].value);this.timeList=[{text:'全部',value:'全部'}];this.$set(this.pageForm,'time','全部');this.$set(this.pageForm,'timeSwitch',true);this.onLoad()});
|
||||
// 本页只查询职工之家,使用接口返回的实际类型 ID;类型缺失时不降级成全部类型查询。
|
||||
siteTypeUtil.getAllType().then((rows) => {
|
||||
const types = rows
|
||||
this.$set(this, 'typeList', types)
|
||||
if (!types.length) {
|
||||
this.$set(this, 'finished', true)
|
||||
vant.Toast('未配置职工之家场地类型')
|
||||
return
|
||||
}
|
||||
this.$set(this.pageForm, 'typeId', types[0].value)
|
||||
this.$set(this, 'timeList', [{text:'全部',value:'全部'}])
|
||||
this.$set(this.pageForm, 'time', '全部')
|
||||
this.$set(this.pageForm, 'timeSwitch', true)
|
||||
this.onLoad()
|
||||
}, () => { vant.Toast('场地类型加载失败,请重新进入') });
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,26 +23,31 @@ layout("/mobile/platform.html"){
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.time {
|
||||
width: 45%;
|
||||
.site-reserve-slots .time {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.num {
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state {
|
||||
width: 80px;
|
||||
height: 36px;
|
||||
/* 长禁用原因由内容撑开行高,避免固定高度导致文字覆盖下一场次。 */
|
||||
.site-reserve-slots .state {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 8px;
|
||||
line-height: 20px;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: center;
|
||||
background-color: #f2f2f2;
|
||||
border-radius: 2px;
|
||||
color: #929292;
|
||||
}
|
||||
|
||||
.container {
|
||||
.site-reserve-slots {
|
||||
padding: 10px 0px;
|
||||
background-color: white;
|
||||
width: 95%;
|
||||
@@ -51,8 +56,16 @@ layout("/mobile/platform.html"){
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
tr {
|
||||
line-height: 36px;
|
||||
.site-reserve-slots table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 4px;
|
||||
}
|
||||
|
||||
.site-reserve-slots td {
|
||||
padding: 0 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.van-action-sheet__content {
|
||||
@@ -182,21 +195,14 @@ layout("/mobile/platform.html"){
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="site-reserve-slots">
|
||||
<table>
|
||||
<tr v-for="item in timeData">
|
||||
<td class="time">{{item.start_time + '-' + item.end_time}}</td>
|
||||
<td class="num">
|
||||
<span v-if="item.code !== -2">
|
||||
{{(item.limitNum - item.reserveNum) + '/' + item.limitNum}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{0 + '/' + item.limitNum}}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="item.code !== 1" @click.stop="">
|
||||
<div class="state" :style="'background-color: ' + item.backColor + ';color:' +item.color">
|
||||
{{item.msg}}
|
||||
<!-- 仅缩短过期展示文案,保留接口原始状态和其他禁用原因。 -->
|
||||
{{item.msg === '预约时段已过期' ? '已过期' : item.msg}}
|
||||
</div>
|
||||
</td>
|
||||
<td v-else @click.stop="chooseTime(item)">
|
||||
@@ -231,8 +237,10 @@ layout("/mobile/platform.html"){
|
||||
</div>
|
||||
</van-cell>
|
||||
<van-cell title="预约地点">{{site.name}}</van-cell>
|
||||
<van-cell title="预约类型" :value="reserveTypeName" is-link class="reserve-choice" @click="openOptions('type')"></van-cell>
|
||||
<van-cell title="所属协会" v-if="myClubs.length || reserve_type===3" class="reserve-choice"
|
||||
<van-cell title="预约类型" required :value="reserveTypeName" is-link class="reserve-choice" @click="openOptions('type')"></van-cell>
|
||||
<!-- 分工会使用后端查询的当前组织,只读显示;协会仅在协会预约时选择。 -->
|
||||
<van-cell title="所属分工会" v-if="reserve_type===2" :value="unionLoading ? '加载中...' : bookingUnionName || '未配置所属分工会'"></van-cell>
|
||||
<van-cell title="所属协会" v-if="reserve_type===3" class="reserve-choice"
|
||||
:value="reserve_type===3 ? selectedClubName : myClubs.map(club=>club.name).join('、')"
|
||||
:is-link="reserve_type===3" @click="openOptions('club')"></van-cell>
|
||||
<div v-if="reserve_type===3 && !myClubs.length" style="padding:12px;color:#ee0a24">您暂无已通过入会审核的有效协会</div>
|
||||
@@ -252,6 +260,7 @@ layout("/mobile/platform.html"){
|
||||
rows="4"
|
||||
autosize
|
||||
label="预约事由"
|
||||
required
|
||||
type="textarea"
|
||||
maxlength="50"
|
||||
placeholder="请输入预约事由"
|
||||
@@ -301,9 +310,12 @@ layout("/mobile/platform.html"){
|
||||
return {
|
||||
historyPopupKeys: ['show','calendarVisible','optionsShow'],
|
||||
optionsShow: false, optionKind: 'type',
|
||||
reserveTypeOptions: [{text:'个人预约',value:1},{text:'单位预约',value:2},{text:'协会预约',value:3}],
|
||||
// 新申请仅开放分工会和协会,个人类型保留用于历史数据。
|
||||
reserveTypeOptions: [{text:'分工会预约',value:2},{text:'协会预约',value:3}],
|
||||
bookingUnionName:'', unionLoading:false,
|
||||
myClubs: [], clubId: '', formLoading: false,
|
||||
reserve_type: 1,
|
||||
// 新预约须主动选择类型,初始不选中任何选项。
|
||||
reserve_type: null,
|
||||
person: "${@shiro.getPrincipalProperty('username')}",
|
||||
unit: "${@shiro.getPrincipalProperty('unit').getName()}",
|
||||
phone: "${@shiro.getPrincipalProperty('mobile')}",
|
||||
@@ -312,6 +324,7 @@ layout("/mobile/platform.html"){
|
||||
site_id: '',
|
||||
site: {},
|
||||
timeData: [],
|
||||
slotRequestVersion:0, slotsLoading:false,
|
||||
times: [],
|
||||
weekList: [],
|
||||
joinUser: '',
|
||||
@@ -319,8 +332,16 @@ layout("/mobile/platform.html"){
|
||||
selectDate: null,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 分工会与协会预约的时段占用规则一致,切换时保留跨日期选择,仅刷新场次状态;提交时仍由后端校验冲突。
|
||||
reserve_type() { this.getReserve() }
|
||||
},
|
||||
computed: {
|
||||
reserveTypeName() { return this.reserveTypeOptions.find(option => option.value===this.reserve_type).text },
|
||||
reserveTypeName() {
|
||||
// 未选择时显示提示,避免读取不存在的选项名称。
|
||||
const option = this.reserveTypeOptions.find(item => item.value===this.reserve_type)
|
||||
return option ? option.text : '请选择预约类型'
|
||||
},
|
||||
selectedClubName() {
|
||||
const club = this.myClubs.find(item => item.id===this.clubId)
|
||||
return club ? club.name : '请选择所属协会'
|
||||
@@ -367,6 +388,7 @@ layout("/mobile/platform.html"){
|
||||
// PJAX 替换页面时显式销毁,触发统一历史 mixin 的监听注销。
|
||||
this._reserveDispose = () => this.$destroy()
|
||||
$(document).one('pjax:beforeReplace.siteReserve', this._reserveDispose)
|
||||
this.loadBookingUnion()
|
||||
$.post('/platform/activity/site/reserve/myClubs').then((res)=>{
|
||||
if(res.code===0){this.$set(this,'myClubs',res.data);this.$set(this,'clubId',res.data.length===1 ? res.data[0].id : '')}
|
||||
else{vant.Toast(res.msg)}
|
||||
@@ -374,8 +396,20 @@ layout("/mobile/platform.html"){
|
||||
},
|
||||
beforeDestroy() {
|
||||
$(document).off('pjax:beforeReplace.siteReserve', this._reserveDispose)
|
||||
this.$set(this,'slotRequestVersion',this.slotRequestVersion+1)
|
||||
},
|
||||
methods: {
|
||||
// 与PC共用所属分工会接口,页面销毁后的响应不再更新界面。
|
||||
loadBookingUnion() {
|
||||
this.$set(this,'unionLoading',true)
|
||||
$.post('/platform/activity/site/reserve/myUnion').then(res=>{
|
||||
if(this._isDestroyed || this._isBeingDestroyed)return
|
||||
if(res && res.code===0)this.$set(this,'bookingUnionName',res.data && res.data.name || '')
|
||||
else vant.Toast(res && res.msg || '所属分工会加载失败')
|
||||
},()=>{if(!this._isDestroyed && !this._isBeingDestroyed)vant.Toast('所属分工会加载失败,请重新进入')}).always(()=>{
|
||||
if(!this._isDestroyed && !this._isBeingDestroyed)this.$set(this,'unionLoading',false)
|
||||
})
|
||||
},
|
||||
// type 显示预约类型,club 仅在协会预约时可选;不修改所属协会的成员校验规则。
|
||||
openOptions(kind) {
|
||||
if (this.formLoading || this.optionsShow || kind==='club' && this.reserve_type!==3) return
|
||||
@@ -442,6 +476,19 @@ layout("/mobile/platform.html"){
|
||||
this.show = true
|
||||
},
|
||||
reserveDo() {
|
||||
if(this.slotsLoading || this.formLoading)return
|
||||
if(!this.times.length){vant.Toast('请选择预约时段');return}
|
||||
if(Number(this.site.reserveTimeType || 1)===2){
|
||||
const days=Array.from(new Set(this.times.map(row=>row.day)))
|
||||
const gaps=days.some(day=>{
|
||||
const selected=this.times.filter(row=>row.day===day).slice().sort((a,b)=>a.start_time.localeCompare(b.start_time))
|
||||
return selected.some((row,i)=>i>0 && selected[i-1].end_time!==row.start_time)
|
||||
})
|
||||
if(gaps){vant.Toast('全天候预约请选择连续时段');return}
|
||||
}
|
||||
// 类型必须为当前可选项,事由不能为空或仅含空白;校验通过后才发送预约请求。
|
||||
if(!this.reserveTypeOptions.some(option => option.value===this.reserve_type)){vant.Toast('请选择预约类型');return}
|
||||
if(!this.message || !this.message.trim()){vant.Toast('请填写预约事由');return}
|
||||
if(this.reserve_type===3 && !this.clubId){vant.Toast('请选择所属协会');return}
|
||||
this.formLoading=true;
|
||||
$.post('/mobile/activity/site/info/reserveDo',{siteId:this.site_id,times:JSON.stringify(this.times),message:this.message,reserve_type:this.reserve_type,clubId:this.clubId}).then((res)=>{
|
||||
@@ -450,6 +497,7 @@ layout("/mobile/platform.html"){
|
||||
}).always(()=>{this.formLoading=false})
|
||||
},
|
||||
chooseTime(item) {
|
||||
if(this.slotsLoading || item.code!==1)return
|
||||
const cloneItem = clone(item)
|
||||
const map = this.times.map(o => o.fullDay)
|
||||
const index = map.indexOf(this.selectDate.format('YYYY-MM-DD') + ' ' + cloneItem.start_time + '-' + cloneItem.end_time)
|
||||
@@ -467,7 +515,19 @@ layout("/mobile/platform.html"){
|
||||
|
||||
},
|
||||
getReserve() {
|
||||
return $.post('/mobile/activity/site/info/getReserve',{siteId:this.site_id,day:moment(this.selectDate).format('YYYY-MM-DD')}).then((res)=>{if(res.code===0){this.timeData=res.data}else{vant.Toast(res.msg)}});
|
||||
if(!this.site_id || !this.selectDate)return
|
||||
const version=this.slotRequestVersion+1
|
||||
this.$set(this,'slotRequestVersion',version)
|
||||
this.$set(this,'slotsLoading',true)
|
||||
this.$set(this,'timeData',[])
|
||||
// 未选类型时仍按团体预约规则查询占用,仅用于场次查询,不回填表单类型。
|
||||
return $.post('/mobile/activity/site/info/getReserve',{siteId:this.site_id,day:moment(this.selectDate).format('YYYY-MM-DD'),reserveType:this.reserve_type===null ? 2 : this.reserve_type}).then(res=>{
|
||||
if(version!==this.slotRequestVersion)return
|
||||
if(res && res.code===0)this.$set(this,'timeData',res.data)
|
||||
else vant.Toast(res && res.msg || '场次加载失败')
|
||||
},()=>{if(version===this.slotRequestVersion)vant.Toast('场次加载失败,请重试')}).always(()=>{
|
||||
if(version===this.slotRequestVersion)this.$set(this,'slotsLoading',false)
|
||||
})
|
||||
},
|
||||
//获取最新的一个工作日设为当前时间
|
||||
getLatestWorkDay() {
|
||||
|
||||
@@ -240,9 +240,10 @@ layout("/mobile/platform.html"){
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div style="margin-top: 6px; color: grey; font-size: 11px">
|
||||
<div>开始时间:{{ moment(item.startDate).format('MM/DD HH:mm') }}
|
||||
<!-- 场地入口仅显示时分秒,其他活动保留原日期格式。 -->
|
||||
<div>开始时间:{{ moment(item.startDate).format(item.id === 'home_site_reserve_entry' ? 'HH:mm:ss' : 'MM/DD HH:mm') }}
|
||||
</div>
|
||||
<div>结束时间:{{ moment(item.endDate).format('MM/DD HH:mm') }}
|
||||
<div>结束时间:{{ moment(item.endDate).format(item.id === 'home_site_reserve_entry' ? 'HH:mm:ss' : 'MM/DD HH:mm') }}
|
||||
</div>
|
||||
</div>
|
||||
</van-col>
|
||||
@@ -332,13 +333,22 @@ layout("/mobile/platform.html"){
|
||||
<div style="background-color: #f6f7f9; min-height: 100vh" v-if="active === 1">
|
||||
<van-notice-bar
|
||||
background="#ecf9ff" color="#1989fa" left-icon="volume-o"
|
||||
text="温馨提示:只显示当前年份的待办和已办事项"
|
||||
text="温馨提示:点击待办进行处理,点击已办查看办理情况"
|
||||
></van-notice-bar>
|
||||
|
||||
<van-loading v-if="legacyTaskLoading" style="text-align: center; padding: 8px">正在加载其他办理事项</van-loading>
|
||||
<div v-if="legacyTaskError" style="text-align: center; padding: 8px" @click="initData">其他办理事项加载失败,点击重试</div>
|
||||
<van-tabs @click="initData" v-model="activeName">
|
||||
<van-tab name="0" title="待办">
|
||||
<div style="min-height: 80vh">
|
||||
<div v-for="o in needItems">
|
||||
<van-loading v-if="taskLoading" style="padding: 16px; text-align: center">正在加载办理事项</van-loading>
|
||||
<div v-if="taskError" style="padding: 16px; text-align: center" @click="loadLocalTasks">流程待办加载失败,点击重试</div>
|
||||
<div v-for="o in localNeedItems" :key="'pending-' + o.id" class="van-doc-card" @click="openLocalTask(o, false)">
|
||||
<div class="title">{{ o.processName }}</div>
|
||||
<div>{{ o.taskNodeName }}</div>
|
||||
<div style="color: grey; margin-top: 6px">{{ o.createdByUserName }} · {{ o.createdOn }}</div>
|
||||
</div>
|
||||
<div v-for="o in visibleNeedItems">
|
||||
<div @click="toUrl(o)" class="van-doc-card" v-if="o.count > 0">
|
||||
<div class="title">{{ o.title }}</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
@@ -356,14 +366,21 @@ layout("/mobile/platform.html"){
|
||||
description="暂无待办"
|
||||
image="https://img01.yzcdn.cn/vant/custom-empty-image.png"
|
||||
style="margin-top: 44px"
|
||||
v-if="needItems.length === 0"
|
||||
v-if="!taskLoading && !taskError && !legacyTaskLoading && !legacyTaskError && visibleNeedItems.length === 0 && localNeedItems.length === 0"
|
||||
></van-empty>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="1" title="已办">
|
||||
<div style="min-height: 80vh">
|
||||
<div @click="toHref(o)" class="van-doc-card" v-for="o in completedItems">
|
||||
<van-loading v-if="taskLoading" style="padding: 16px; text-align: center">正在加载办理事项</van-loading>
|
||||
<div v-if="taskError" style="padding: 16px; text-align: center" @click="loadLocalTasks">流程已办加载失败,点击重试</div>
|
||||
<div v-for="o in localCompletedItems" :key="'completed-' + o.id" class="van-doc-card" @click="openLocalTask(o, true)">
|
||||
<div class="title">{{ o.processName }}</div>
|
||||
<div>{{ o.taskNodeName }} · 已办理</div>
|
||||
<div style="color: grey; margin-top: 6px">{{ o.createdByUserName }} · {{ o.endOn || o.createdOn }}</div>
|
||||
</div>
|
||||
<div @click="toHref(o)" class="van-doc-card" v-for="o in visibleCompletedItems">
|
||||
<div class="title">{{ o.moduleName }}</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div>
|
||||
@@ -379,7 +396,7 @@ layout("/mobile/platform.html"){
|
||||
description="暂无已办"
|
||||
image="https://img01.yzcdn.cn/vant/custom-empty-image.png"
|
||||
style="margin-top: 44px"
|
||||
v-if="completedItems.length === 0"
|
||||
v-if="!taskLoading && !taskError && !legacyTaskLoading && !legacyTaskError && visibleCompletedItems.length === 0 && localCompletedItems.length === 0"
|
||||
></van-empty>
|
||||
</div>
|
||||
</van-tab>
|
||||
@@ -517,7 +534,13 @@ layout("/mobile/platform.html"){
|
||||
moduleMenus: [],
|
||||
roles: [],
|
||||
active: 0,
|
||||
needCount: 0,
|
||||
localNeedItems: [],
|
||||
localCompletedItems: [],
|
||||
taskLoading: false,
|
||||
taskError: false,
|
||||
taskRequestVersion: 0,
|
||||
legacyTaskLoading: false,
|
||||
legacyTaskError: false,
|
||||
needItems: [],
|
||||
activeName: '0',
|
||||
completedItems: [],
|
||||
@@ -545,6 +568,20 @@ layout("/mobile/platform.html"){
|
||||
activityQrCodeShow: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 同一手机入口已接入流程任务时,不再重复显示旧接口的汇总卡片。
|
||||
visibleNeedItems() {
|
||||
const urls = this.localNeedItems.map(o => this.taskPath(o.formMobileUrl));
|
||||
return this.needItems.filter(o => !urls.includes(this.taskPath(o.mobileHref)));
|
||||
},
|
||||
visibleCompletedItems() {
|
||||
const urls = this.localCompletedItems.map(o => this.taskPath(o.formMobileUrlView || o.formMobileUrl));
|
||||
return this.completedItems.filter(o => !urls.includes(this.taskPath(o.mobileUrl)));
|
||||
},
|
||||
needCount() {
|
||||
return this.visibleNeedItems.length + this.localNeedItems.length > 0;
|
||||
}
|
||||
},
|
||||
components: {},
|
||||
methods: {
|
||||
async getMenus() {
|
||||
@@ -571,7 +608,6 @@ layout("/mobile/platform.html"){
|
||||
tabbrChange(o) {
|
||||
console.log(o)
|
||||
sessionStorage.setItem("zhgh-mobile-home-active",o)
|
||||
this.needItems = []
|
||||
this.initData()
|
||||
},
|
||||
toUrl(o) {
|
||||
@@ -580,14 +616,11 @@ layout("/mobile/platform.html"){
|
||||
toHref(o) {
|
||||
pjaxReplace(o.mobileUrl + '?audit=1');
|
||||
},
|
||||
async getNeedItems() {
|
||||
const {data, code} = await $.get('/platform/needItems/getNeedItems')
|
||||
if (code === 0) {
|
||||
const obj = data.filter(v => {
|
||||
return v.mobileHref != null && v.count > 0
|
||||
})
|
||||
this.needItems = obj
|
||||
}
|
||||
getNeedItems() {
|
||||
// mobile=true 由后端按实际手机入口过滤,返回数组用于数量及红点计算。
|
||||
return $.get('/platform/needItems/getNeedItems', {mobile: true}).then(res => {
|
||||
if (res.code === 0) this.$set(this, 'needItems', res.data.filter(v => v.mobileHref && v.count > 0));
|
||||
});
|
||||
},
|
||||
async getCompletes() {
|
||||
const {data, code} = await $.get('/platform/sys/completed/getCompletes')
|
||||
@@ -615,23 +648,68 @@ layout("/mobile/platform.html"){
|
||||
this.needItems = this.needItems.concat(data.filter(v => v.count > 0))
|
||||
}
|
||||
},
|
||||
async initData() {
|
||||
await this.getNeedItems();
|
||||
await this.getMemberCheckSelfAgenda()
|
||||
await this.getWelfareCheckSelfAgenda()
|
||||
await this.getCompletes();
|
||||
await this.getMsg()
|
||||
await this.listActivity();
|
||||
this.needCount = this.needItems.length
|
||||
// 双端共用流程任务来源;只使用手机地址,避免手机进入 PC 表单。
|
||||
taskPath(url) {
|
||||
return (url || '').split('?')[0].replace(/\/$/, '');
|
||||
},
|
||||
async setNeedItems() {
|
||||
const {data, code} = await $.get('/platform/needItems/getNeedItems')
|
||||
if (code === 0) {
|
||||
const obj = data.filter(v => {
|
||||
return v.mobileHref != null && v.count > 0
|
||||
})
|
||||
window.localStorage.setItem('needItems', escape(JSON.stringify(obj)))
|
||||
openLocalTask(row, completed) {
|
||||
const url = completed ? (row.formMobileUrlView || row.formMobileUrl) : row.formMobileUrl;
|
||||
if (url) pjaxReplace(url);
|
||||
},
|
||||
loadLocalTasks() {
|
||||
const version = this.taskRequestVersion + 1;
|
||||
this.$set(this, 'taskRequestVersion', version);
|
||||
this.$set(this, 'taskLoading', true);
|
||||
this.$set(this, 'taskError', false);
|
||||
let remaining = 2;
|
||||
// mode=1 待办、2 已办;data 是任务数组。忽略旧请求,防止快速切换覆盖新结果。
|
||||
[1, 2].forEach(mode => {
|
||||
$.post('/platform/sys/localProcess/todoList', {mode: mode, mobile: true})
|
||||
.then(res => {
|
||||
if (version !== this.taskRequestVersion || this._isDestroyed) return;
|
||||
if (res.code !== 0 || !Array.isArray(res.data)) {
|
||||
this.$set(this, 'taskError', true);
|
||||
return;
|
||||
}
|
||||
const ids = new Set();
|
||||
const rows = res.data.filter(row => {
|
||||
const url = mode === 1 ? row.formMobileUrl : (row.formMobileUrlView || row.formMobileUrl);
|
||||
if (!url || ids.has(row.id)) return false;
|
||||
ids.add(row.id);
|
||||
return true;
|
||||
});
|
||||
this.$set(this, mode === 1 ? 'localNeedItems' : 'localCompletedItems', rows);
|
||||
}, () => {
|
||||
if (version === this.taskRequestVersion && !this._isDestroyed) this.$set(this, 'taskError', true);
|
||||
}).always(() => {
|
||||
remaining--;
|
||||
if (remaining === 0 && version === this.taskRequestVersion && !this._isDestroyed) this.$set(this, 'taskLoading', false);
|
||||
});
|
||||
});
|
||||
},
|
||||
initData() {
|
||||
this.loadLocalTasks();
|
||||
// 保留原有业务待办;重复点击不并发追加旧接口的汇总记录。
|
||||
if (!this.legacyTaskLoading) {
|
||||
this.$set(this, 'legacyTaskLoading', true);
|
||||
this.$set(this, 'legacyTaskError', false);
|
||||
this.getNeedItems().then(() => this.getMemberCheckSelfAgenda())
|
||||
.then(() => this.getWelfareCheckSelfAgenda())
|
||||
.then(() => this.getCompletes())
|
||||
.catch(() => this.$set(this, 'legacyTaskError', true))
|
||||
.finally(() => this.$set(this, 'legacyTaskLoading', false));
|
||||
}
|
||||
// 首页消息、活动请求不阻塞办理事项展示。
|
||||
this.getMsg().catch(() => {});
|
||||
return this.listActivity().catch(() => {});
|
||||
},
|
||||
setNeedItems() {
|
||||
return $.get('/platform/needItems/getNeedItems', {mobile: true}).then(res => {
|
||||
if (res.code === 0) {
|
||||
const items = res.data.filter(v => v.mobileHref && v.count > 0);
|
||||
window.localStorage.setItem('needItems', escape(JSON.stringify(items)));
|
||||
}
|
||||
});
|
||||
},
|
||||
//获取站内消息
|
||||
async getMsg() {
|
||||
@@ -719,18 +797,18 @@ layout("/mobile/platform.html"){
|
||||
beforeMount() {
|
||||
this.setNeedItems();
|
||||
},
|
||||
async created() {
|
||||
await this.getMenus();
|
||||
created() {
|
||||
this.getAllMonth();
|
||||
this.createYearList();
|
||||
await this.initData();
|
||||
const active = sessionStorage.getItem("zhgh-mobile-home-active")
|
||||
this.active = active ? parseInt(active) : 0
|
||||
|
||||
if (!this.activityList || !this.activityList.length > 0) {
|
||||
this.clickIndex = 2
|
||||
this.clickMenu = this.moduleMenus.find(o => o.moduleName === '职工权益')
|
||||
}
|
||||
const active = sessionStorage.getItem("zhgh-mobile-home-active");
|
||||
this.$set(this, 'active', active ? parseInt(active) : 0);
|
||||
// 菜单加载失败不应阻止待办请求;活动和菜单加载完成后再选择默认栏目。
|
||||
Promise.all([this.getMenus().catch(() => {}), this.initData()]).then(() => {
|
||||
if (!this.activityList || this.activityList.length === 0) {
|
||||
this.$set(this, 'clickIndex', 2);
|
||||
this.$set(this, 'clickMenu', this.moduleMenus.find(o => o.moduleName === '职工权益') || {});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
class typeUtil {
|
||||
// 返回类型数组,text/value 为现有选择组件需要的别名;默认保留停用类型用于历史筛选。
|
||||
// 场地模块查询只提供职工之家;保留接口实际 ID,text/value 供双端筛选组件使用,不改类型管理数据。
|
||||
getAllType(enabledOnly = false) {
|
||||
return $.post('/platform/activity/site/type/findAll', {enabledOnly}).then((resp) => {
|
||||
if (resp.code !== 0) return [];
|
||||
return resp.data.map((row) => Object.assign({}, row, {text:row.meetingTypeName,value:row.id}));
|
||||
return resp.data.filter((row) => row.meetingTypeName === '职工之家')
|
||||
.map((row) => Object.assign({}, row, {text:row.meetingTypeName,value:row.id}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ layout("/layouts/platform.html"){
|
||||
prop="name"></el-table-column>
|
||||
<el-table-column align="center" show-overflow-tooltip header-align="center" label="场地地址"
|
||||
prop="address"></el-table-column>
|
||||
<el-table-column align="center" show-overflow-tooltip header-align="center" label="联系人"
|
||||
<el-table-column align="center" show-overflow-tooltip header-align="center" label="场地管理员"
|
||||
prop="contact_person"></el-table-column>
|
||||
<el-table-column align="center" show-overflow-tooltip header-align="center" label="联系方式"
|
||||
prop="contact_phone"></el-table-column>
|
||||
@@ -137,20 +137,37 @@ layout("/layouts/platform.html"){
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="name" label="场地名称">
|
||||
<el-input maxlength="100" placeholder="请填写场地名称" v-model="formData.name"></el-input>
|
||||
<el-select v-model="formData.name" placeholder="请选择场地名称" style="width:100%">
|
||||
<el-option label="职工之家" value="职工之家"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="campus" label="校区">
|
||||
<el-select v-model="formData.campus" placeholder="请选择校区" style="width:100%">
|
||||
<el-option v-for="item in campusList" :key="item.campus_id" :label="item.campus_name" :value="item.campus_name"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="address" label="场地地址">
|
||||
<el-input maxlength="100" placeholder="请填写场地地址" v-model="formData.address"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="limitNum" label="限定人数">
|
||||
<el-input-number v-model="formData.limitNum" :min="1" :max="100"
|
||||
placeholder="请输入限定人数" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="contact_person" label="联系人">
|
||||
<el-input v-model="formData.contact_person" placeholder="请填写联系人" type="text"></el-input>
|
||||
<el-form-item prop="contact_person" label="场地管理员">
|
||||
<el-input v-model="formData.contact_person" placeholder="请填写场地管理员" type="text"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -160,24 +177,6 @@ layout("/layouts/platform.html"){
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="typeId" label="场地类型">
|
||||
<el-select v-model="formData.typeId" style="width: 100%" filterable
|
||||
placeholder="请选择场地类型">
|
||||
<el-option v-for="item in activityTypeList" :label="item.meetingTypeName"
|
||||
:value="item.id" :disabled="!item.enabled"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="limitNum" label="个人预约限定人数">
|
||||
<el-input-number v-model="formData.limitNum" :min="1" :max="100"
|
||||
placeholder="请输入个人预约限定人数" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="state" label="开启状态">
|
||||
@@ -206,6 +205,7 @@ layout("/layouts/platform.html"){
|
||||
</el-col>-->
|
||||
</el-row>
|
||||
|
||||
<!-- 性别限制暂不开放配置,新增默认不限制,编辑保留历史设置。
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="sexLimit" label="性别限制">
|
||||
@@ -217,41 +217,37 @@ layout("/layouts/platform.html"){
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
-->
|
||||
|
||||
<el-form-item label="添加场次 " label-width="110px" class="view-header">
|
||||
<el-button style="float: right;margin-bottom: 10px" icon="el-icon-plus" size="mini"
|
||||
type="primary" @click="formData.open_hours.push({})">添加
|
||||
</el-button>
|
||||
|
||||
<el-table :data="formData.open_hours" border size="mini">
|
||||
<el-table-column prop="start_time" label="开始时间" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-time-select
|
||||
placeholder="开始时间"
|
||||
v-model="row.start_time"
|
||||
:picker-options="{ start: '00:00',step: '00:30',end: '24:00'}">
|
||||
</el-time-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="end_time" label="结束时间" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-time-select
|
||||
placeholder="结束时间"
|
||||
v-model="row.end_time"
|
||||
:picker-options="{start: '00:00',step: '00:30',end: '24:00',minTime: row.start_time }">
|
||||
</el-time-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" align="center" header-align="center" width="100px">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="danger" icon="el-icon-delete" circle
|
||||
:disabled="formData.open_hours.length==0"
|
||||
@click="formData.open_hours.splice(scope.$index, 1)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12"><el-form-item label="预约时间段类型" prop="reserveTimeType">
|
||||
<el-radio-group v-model="formData.reserveTimeType" @change="changeTimeMode">
|
||||
<el-radio-button :label="2">全天候预约</el-radio-button>
|
||||
<el-radio-button :label="1">分段预约</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="禁用时间">
|
||||
<el-button type="primary" size="small" @click="openDisabledTimes">点击设置禁用时间</el-button>
|
||||
<span> 已设置 {{(formData.notApplyTimeList || []).length}} 个禁用时间</span>
|
||||
</el-form-item></el-col>
|
||||
</el-row>
|
||||
<!-- 两种模式独立保留原始配置,open_hours由后端按时间单位展开。 -->
|
||||
<el-form-item label="场次信息">
|
||||
<div>{{formData.reserveTimeType===2 ? '全天候预约:配置一个开放范围,预约人按时间单位选择连续时段。' : '分段预约:配置一个或多个互不重叠的场次。'}}</div>
|
||||
<el-button v-if="formData.reserveTimeType===1" type="primary" size="mini" @click="addOpenRange">添加场次</el-button>
|
||||
<el-table :data="editableRanges" border size="mini">
|
||||
<el-table-column label="开始时间"><template slot-scope="{row}">
|
||||
<el-time-select v-model="row.start_time" :picker-options="{start:'00:00',step:'00:05',end:'23:55'}" placeholder="开始时间"></el-time-select>
|
||||
</template></el-table-column>
|
||||
<el-table-column label="结束时间"><template slot-scope="{row}">
|
||||
<el-time-select v-model="row.end_time" :picker-options="{start:'00:05',step:'00:05',end:'24:00',minTime:row.start_time}" placeholder="结束时间"></el-time-select>
|
||||
</template></el-table-column>
|
||||
<el-table-column label="预约时间单位(分钟)"><template slot-scope="{row}">
|
||||
<el-input-number v-model="row.timeUnit" :min="1" :max="1440" :precision="0" style="width:150px"></el-input-number>
|
||||
</template></el-table-column>
|
||||
<el-table-column v-if="formData.reserveTimeType===1" label="操作" width="100"><template slot-scope="{$index}">
|
||||
<el-button type="danger" size="mini" :disabled="formData.segmentedOpenHours.length<=1" @click="removeOpenRange($index)">删除</el-button>
|
||||
</template></el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
|
||||
@@ -265,6 +261,28 @@ layout("/layouts/platform.html"){
|
||||
<template #view>
|
||||
</template>
|
||||
</guava>
|
||||
<el-dialog title="设置禁用时间" :visible.sync="disabledTimesShow" width="850px" append-to-body>
|
||||
<el-date-picker v-model="disabledDates" type="dates" value-format="yyyy-MM-dd" placeholder="选择一个或多个日期" @change="syncDisabledDates" style="width:100%"></el-date-picker>
|
||||
<div class="mt10">
|
||||
<el-time-select v-model="disabledStart" :picker-options="{start:'00:00',step:'00:05',end:'23:55'}" placeholder="批量开始时间"></el-time-select>
|
||||
<el-time-select v-model="disabledEnd" :picker-options="{start:'00:05',step:'00:05',end:'24:00'}" placeholder="批量结束时间"></el-time-select>
|
||||
<el-button type="primary" @click="batchDisabledTimes">应用到全部日期</el-button>
|
||||
</div>
|
||||
<el-table :data="formData.notApplyTimeList || []" max-height="420" class="mt10">
|
||||
<el-table-column label="日期" prop="date" width="115"></el-table-column>
|
||||
<el-table-column label="开始时间"><template slot-scope="{row}">
|
||||
<el-time-select v-model="row.startTime" :picker-options="{start:'00:00',step:'00:05',end:'23:55'}"></el-time-select>
|
||||
</template></el-table-column>
|
||||
<el-table-column label="结束时间"><template slot-scope="{row}">
|
||||
<el-time-select v-model="row.endTime" :picker-options="{start:'00:05',step:'00:05',end:'24:00',minTime:row.startTime}"></el-time-select>
|
||||
</template></el-table-column>
|
||||
<el-table-column label="操作" width="160"><template slot-scope="{row,$index}">
|
||||
<el-button size="mini" @click="addDisabledRange(row.date)">新增</el-button>
|
||||
<el-button size="mini" type="danger" @click="removeDisabledRange($index)">删除</el-button>
|
||||
</template></el-table-column>
|
||||
</el-table>
|
||||
<span slot="footer"><el-button type="primary" @click="finishDisabledTimes">确定</el-button></span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -275,6 +293,8 @@ layout("/layouts/platform.html"){
|
||||
data() {
|
||||
return {
|
||||
activityTypeList: [],
|
||||
campusList: [],
|
||||
disabledTimesShow:false, disabledDates:[], disabledStart:'', disabledEnd:'',
|
||||
tabKey: '',
|
||||
tableLoading: false,
|
||||
subDis: false,
|
||||
@@ -284,6 +304,9 @@ layout("/layouts/platform.html"){
|
||||
open_hours: [{}],
|
||||
sexLimit: '0',
|
||||
workday: false,
|
||||
reserveTimeType:2, notApplyTimeList:[],
|
||||
segmentedOpenHours:[{start_time:'',end_time:'',timeUnit:30}],
|
||||
fullDayOpenHour:{start_time:'',end_time:'',timeUnit:30},
|
||||
},
|
||||
pageForm: {
|
||||
searchName: "name",
|
||||
@@ -296,20 +319,74 @@ layout("/layouts/platform.html"){
|
||||
year: ""
|
||||
},
|
||||
formRules: {
|
||||
name: [{required: true, message: '请填写场地名称', trigger: ['blur', 'change']}],
|
||||
name: [{required: true, message: '请选择场地名称', trigger: ['blur', 'change']}],
|
||||
address: [{required: true, message: '请填写场地地址', trigger: ['blur', 'change']}],
|
||||
contact_person: [{required: true, message: '请填写联系人', trigger: ['blur', 'change']}],
|
||||
contact_person: [{required: true, message: '请填写场地管理员', trigger: ['blur', 'change']}],
|
||||
contact_phone: [{required: true, message: '请填写联系电话', trigger: ['blur', 'change']}],
|
||||
state: [{required: true, message: '请选择开启状态', trigger: ['blur', 'change']}],
|
||||
typeId: [{required: true, message: '请选择场地类型', trigger: ['blur', 'change']}],
|
||||
limitNum: [{required: true, message: '请填写个人预约限定人数', trigger: ['blur', 'change']}],
|
||||
campus: [{required: true, message: '请选择校区', trigger: 'change'}],
|
||||
limitNum: [{required: true, message: '请填写限定人数', trigger: ['blur', 'change']}],
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
editableRanges() { return this.formData.reserveTimeType===2 ? [this.formData.fullDayOpenHour] : this.formData.segmentedOpenHours || [] }
|
||||
},
|
||||
components: {
|
||||
'guava': httpVueLoader('/components/plugins/Guava.vue'),
|
||||
},
|
||||
methods: {
|
||||
// 老数据以原场次长度作为时间单位,避免首次编辑自动拆分既有场次。
|
||||
initSchedule(row) {
|
||||
this.$set(row, 'reserveTimeType', row.reserveTimeType || 1)
|
||||
this.$set(row, 'notApplyTimeList', row.notApplyTimeList || [])
|
||||
if (!row.segmentedOpenHours || !row.segmentedOpenHours.length) this.$set(row, 'segmentedOpenHours', (row.open_hours || []).map(item => ({
|
||||
start_time:item.start_time, end_time:item.end_time,
|
||||
timeUnit:this.timeMinutes(item.end_time)-this.timeMinutes(item.start_time)
|
||||
})))
|
||||
if (!row.fullDayOpenHour) this.$set(row, 'fullDayOpenHour', {start_time:'',end_time:'',timeUnit:30})
|
||||
},
|
||||
timeMinutes(value) { const parts=(value || '').split(':'); return Number(parts[0])*60+Number(parts[1]) },
|
||||
changeTimeMode() {
|
||||
// 切换只改变展示,保留两套场次配置。
|
||||
if (!this.formData.segmentedOpenHours.length) this.addOpenRange()
|
||||
},
|
||||
addOpenRange() { this.$set(this.formData, 'segmentedOpenHours', this.formData.segmentedOpenHours.concat([{start_time:'',end_time:'',timeUnit:30}])) },
|
||||
removeOpenRange(index) { this.$set(this.formData, 'segmentedOpenHours', this.formData.segmentedOpenHours.filter((row,i)=>i!==index)) },
|
||||
openDisabledTimes() {
|
||||
this.$set(this, 'disabledDates', Array.from(new Set((this.formData.notApplyTimeList || []).map(row=>row.date))))
|
||||
this.$set(this, 'disabledTimesShow', true)
|
||||
},
|
||||
syncDisabledDates(dates) {
|
||||
const selected=dates || []
|
||||
const rows=(this.formData.notApplyTimeList || []).filter(row=>selected.includes(row.date))
|
||||
selected.forEach(date=>{if(!rows.some(row=>row.date===date))rows.push({date,startTime:'',endTime:''})})
|
||||
this.$set(this.formData, 'notApplyTimeList', rows.sort((a,b)=>a.date.localeCompare(b.date)))
|
||||
},
|
||||
addDisabledRange(date) { this.$set(this.formData, 'notApplyTimeList', this.formData.notApplyTimeList.concat([{date,startTime:'',endTime:''}])) },
|
||||
removeDisabledRange(index) {
|
||||
this.$set(this.formData, 'notApplyTimeList', this.formData.notApplyTimeList.filter((row,i)=>i!==index))
|
||||
this.$set(this, 'disabledDates', Array.from(new Set(this.formData.notApplyTimeList.map(row=>row.date))))
|
||||
},
|
||||
batchDisabledTimes() {
|
||||
if(!this.disabledStart || !this.disabledEnd || this.disabledStart>=this.disabledEnd){this.$message.warning('请设置有效的禁用起止时间');return}
|
||||
this.formData.notApplyTimeList.forEach(row=>{this.$set(row,'startTime',this.disabledStart);this.$set(row,'endTime',this.disabledEnd)})
|
||||
},
|
||||
validDisabledTimes() { return (this.formData.notApplyTimeList || []).every(row=>row.date && row.startTime && row.endTime && row.startTime<row.endTime) },
|
||||
finishDisabledTimes() {
|
||||
if(!this.validDisabledTimes()){this.$message.warning('请完整填写禁用日期及起止时间');return}
|
||||
this.$set(this, 'disabledTimesShow', false)
|
||||
},
|
||||
validateSchedule() {
|
||||
if(!this.validDisabledTimes()){this.$message.warning('请完整填写禁用日期及起止时间');return false}
|
||||
const rows=this.editableRanges
|
||||
if(!rows.length || rows.some(row=>!row || !row.start_time || !row.end_time || row.start_time>=row.end_time || !Number.isInteger(row.timeUnit) || row.timeUnit<=0 || (this.timeMinutes(row.end_time)-this.timeMinutes(row.start_time))%row.timeUnit!==0)){
|
||||
this.$message.warning('请配置有效开放时间,时间单位须为正整数且能整除开放时长');return false
|
||||
}
|
||||
const ordered=rows.slice().sort((a,b)=>a.start_time.localeCompare(b.start_time))
|
||||
if(ordered.some((row,i)=>i>0 && row.start_time<ordered[i-1].end_time)){this.$message.warning('分段场次不能重叠');return false}
|
||||
return true
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm('此操作将删除【' + row.name + '】场地的所有信息!', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -330,6 +407,7 @@ layout("/layouts/platform.html"){
|
||||
});
|
||||
},
|
||||
operation() {
|
||||
if (!this.validateSchedule()) return
|
||||
this.$refs.addForm.validate((valid)=>{if(!valid)return;this.formLoading=true;this.subDis=true;
|
||||
$.post(loc()+(this.formData.id ? '/doEdit' : '/doAdd'),{data:JSON.stringify(this.formData)}).then((res)=>{if(res.code===0){this.pageData();this.$refs.guava.index()}else{this.$message.error(res.msg)}}).always(()=>{this.formLoading=false;this.subDis=false})
|
||||
});
|
||||
@@ -341,10 +419,13 @@ layout("/layouts/platform.html"){
|
||||
this.$refs.guava.view()
|
||||
},
|
||||
openEdit(id) {
|
||||
this.findOneSite(id).then((row)=>{if(row){this.formData=row;this.$refs.guava.edit()}});
|
||||
this.findOneSite(id).then((row)=>{if(row){this.initSchedule(row);this.$set(this, 'formData', row);this.$refs.guava.edit()}});
|
||||
},
|
||||
openAdd() {
|
||||
this.formData = {
|
||||
// 新建只提供职工之家;校区由用户明确选择,不沿用上次编辑值。
|
||||
this.$set(this, 'formData', {
|
||||
name: '职工之家',
|
||||
campus: '',
|
||||
state: true,
|
||||
multiple: true,
|
||||
open_hours: [{}],
|
||||
@@ -352,12 +433,13 @@ layout("/layouts/platform.html"){
|
||||
create_time: moment().format('YYYY-MM-DD'),
|
||||
sexLimit: '0',
|
||||
workday: false,
|
||||
}
|
||||
reserveTimeType:2, notApplyTimeList:[],
|
||||
segmentedOpenHours:[{start_time:'',end_time:'',timeUnit:30}],
|
||||
fullDayOpenHour:{start_time:'',end_time:'',timeUnit:30},
|
||||
})
|
||||
this.$refs.guava.edit()
|
||||
|
||||
if (this.$refs['addForm']) {
|
||||
this.$refs['addForm'].resetFields()
|
||||
}
|
||||
this.$nextTick(() => { if (this.$refs.addForm) this.$refs.addForm.clearValidate() })
|
||||
},
|
||||
doSearch() {
|
||||
this.tabKey = new Date().getTime()
|
||||
@@ -398,6 +480,11 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
created() {
|
||||
this.pageData();activityUtil.getAllType().then((rows)=>{this.activityTypeList=rows});
|
||||
// 校区复用系统选项接口,value 保存校区名称,与现有业务页面一致。
|
||||
$.post('/platform/vi/common/getCampus').then((res) => {
|
||||
if (res && res.code===0) this.$set(this, 'campusList', res.data.filter(row=>!['青山湖科创中心','不固定校区'].includes(row.campus_name)))
|
||||
else this.$message.error(res && res.msg || '校区加载失败')
|
||||
}, () => this.$message.error('校区加载失败,请刷新重试'))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
/* 预约列表按日期分组,日期和完整时段整体居中,长内容只在时段之间换行。 */
|
||||
.site-booking-times { display:inline-block; max-width:100%; text-align:left; vertical-align:middle; font-variant-numeric:tabular-nums; }
|
||||
.site-booking-day { display:flex; align-items:baseline; flex-wrap:wrap; gap:4px 12px; padding:3px 0; line-height:22px; }
|
||||
.site-booking-date { flex:0 0 88px; white-space:nowrap; font-weight:500; }
|
||||
.site-booking-ranges { display:flex; flex-wrap:wrap; gap:2px 12px; }
|
||||
.site-booking-range { white-space:nowrap; }
|
||||
</style>
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
@@ -53,23 +61,17 @@ layout("/layouts/platform.html"){
|
||||
<el-table-column align="center" sortable show-overflow-tooltip header-align="center"
|
||||
label="预约天数"
|
||||
prop="days"></el-table-column>
|
||||
<el-table-column align="center" sortable show-overflow-tooltip header-align="center"
|
||||
label="预约日期"
|
||||
prop="concat_day"></el-table-column>
|
||||
<el-table-column align="center" sortable show-overflow-tooltip header-align="center"
|
||||
label="开始时间"
|
||||
prop="start_time">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{row.start_time}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" sortable show-overflow-tooltip header-align="center"
|
||||
label="结束时间"
|
||||
prop="end_time">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{row.end_time}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="预约时段" min-width="245">
|
||||
<template slot-scope="{row}">
|
||||
<div class="site-booking-times">
|
||||
<div v-for="group in row.reservationTimeGroups" :key="group.date" class="site-booking-day">
|
||||
<span class="site-booking-date">{{group.date}}</span>
|
||||
<div class="site-booking-ranges"><span v-for="(range,index) in group.ranges" :key="index" class="site-booking-range">{{range.start}}–{{range.end}}</span></div>
|
||||
</div>
|
||||
<span v-if="!row.reservationTimeGroups || !row.reservationTimeGroups.length">—</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" show-overflow-tooltip header-align="center" label="反馈意见"
|
||||
prop="back_option">
|
||||
<template slot-scope="{row}">
|
||||
@@ -148,7 +150,7 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'reserve-info': httpVueLoader('/components/activity/site/ReserveInfo.vue'),
|
||||
'reserve-info': httpVueLoader('/components/activity/site/ReserveInfo.vue?v=20260910-2'),
|
||||
},
|
||||
methods: {
|
||||
openView(id) {
|
||||
@@ -190,9 +192,11 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.activityTypeList = await activityUtil.getAllType()
|
||||
this.pageData();
|
||||
created() {
|
||||
activityUtil.getAllType().then((rows) => {
|
||||
this.$set(this, 'activityTypeList', rows)
|
||||
this.pageData()
|
||||
}, () => { this.$message.error('场地类型加载失败,请刷新重试') })
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -101,6 +101,14 @@ layout("/layouts/platform.html"){
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* 预约列表按日期分组,日期和完整时段整体居中,长内容只在时段之间换行。 */
|
||||
.site-booking-times { display:inline-block; max-width:100%; text-align:left; vertical-align:middle; font-variant-numeric:tabular-nums; }
|
||||
.site-booking-day { display:flex; align-items:baseline; flex-wrap:wrap; gap:4px 12px; padding:3px 0; line-height:22px; }
|
||||
.site-booking-date { flex:0 0 88px; white-space:nowrap; font-weight:500; }
|
||||
.site-booking-ranges { display:flex; flex-wrap:wrap; gap:2px 12px; }
|
||||
.site-booking-range { white-space:nowrap; }
|
||||
</style>
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
@@ -222,23 +230,17 @@ layout("/layouts/platform.html"){
|
||||
<el-table-column align="center" sortable show-overflow-tooltip header-align="center"
|
||||
label="预约天数"
|
||||
prop="days"></el-table-column>
|
||||
<el-table-column align="center" sortable show-overflow-tooltip header-align="center"
|
||||
label="预约日期"
|
||||
prop="concat_day"></el-table-column>
|
||||
<el-table-column align="center" sortable show-overflow-tooltip header-align="center"
|
||||
label="开始时间"
|
||||
prop="start_time">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{row.start_time}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" sortable show-overflow-tooltip header-align="center"
|
||||
label="结束时间"
|
||||
prop="end_time">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{row.end_time}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="预约时段" min-width="245">
|
||||
<template slot-scope="{row}">
|
||||
<div class="site-booking-times">
|
||||
<div v-for="group in row.reservationTimeGroups" :key="group.date" class="site-booking-day">
|
||||
<span class="site-booking-date">{{group.date}}</span>
|
||||
<div class="site-booking-ranges"><span v-for="(range,index) in group.ranges" :key="index" class="site-booking-range">{{range.start}}–{{range.end}}</span></div>
|
||||
</div>
|
||||
<span v-if="!row.reservationTimeGroups || !row.reservationTimeGroups.length">—</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="联系方式"
|
||||
prop="reserve_person_phone"></el-table-column>
|
||||
<el-table-column align="center" show-overflow-tooltip header-align="center" label="预约事由"
|
||||
@@ -274,13 +276,14 @@ layout("/layouts/platform.html"){
|
||||
</el-dropdown-item>
|
||||
<!--# if(@shiro.hasRole('sysadmin')){ #-->
|
||||
<el-dropdown-item
|
||||
v-if="Number(scope.row.reserve_state) === 4050"
|
||||
:command="{type:'delete',data:scope.row}">
|
||||
删除
|
||||
</el-dropdown-item>
|
||||
<!--# } #-->
|
||||
|
||||
<el-dropdown-item
|
||||
v-if="scope.row.reserve_person_id === '${@shiro.getPrincipalProperty('id')}'"
|
||||
v-if="scope.row.reserve_person_id === '${@shiro.getPrincipalProperty('id')}' && Number(scope.row.reserve_state) !== 4050"
|
||||
:command="{type:'cancel',data:scope.row}">
|
||||
撤销预约
|
||||
</el-dropdown-item>
|
||||
@@ -424,10 +427,10 @@ layout("/layouts/platform.html"){
|
||||
</el-form-item>-->
|
||||
|
||||
<el-form-item label="预约时间" prop="time">
|
||||
<el-select v-model="formData.time" style="width: 100%" filterable @change="timeChange"
|
||||
<el-select v-model="formData.time" style="width: 100%" multiple :disabled="slotsLoading" filterable @change="timeChange"
|
||||
placeholder="请选择预约时间">
|
||||
<el-option v-for="item in infoViewData.open_hours"
|
||||
:label="item.disabled ? item.start_time + ' - ' + item.end_time + '(已约满)' : item.start_time + ' - ' + item.end_time"
|
||||
<el-option v-for="item in bookingSlots" :key="item.start_time + item.end_time"
|
||||
:label="item.disabled ? item.start_time + ' - ' + item.end_time + '(' + item.msg + ')' : item.start_time + ' - ' + item.end_time"
|
||||
:disabled="item.disabled"
|
||||
:value="item.start_time + '-' + item.end_time"></el-option>
|
||||
</el-select>
|
||||
@@ -459,13 +462,18 @@ layout("/layouts/platform.html"){
|
||||
</el-row>
|
||||
</el-form-item>-->
|
||||
<el-form-item label="预约类型" prop="reserve_type">
|
||||
<el-radio-group v-model="formData.reserve_type" size="medium">
|
||||
<el-radio-button :label="1">个人预约</el-radio-button>
|
||||
<el-radio-button :label="2">单位预约</el-radio-button>
|
||||
<el-radio-group v-model="formData.reserve_type" @change="loadBookingSlots" size="medium">
|
||||
<!-- PC暂不开放个人预约,保留历史类型值及其他入口的兼容。 -->
|
||||
<!--<el-radio-button :label="1">个人预约</el-radio-button>-->
|
||||
<el-radio-button :label="2">分工会预约</el-radio-button>
|
||||
<el-radio-button :label="3">协会预约</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<!-- 所属协会仅用于协会预约,个人和单位预约不展示该字段。 -->
|
||||
<!-- 分工会由后端按当前申请人解析,只读展示,不接受前端指定组织。 -->
|
||||
<el-form-item label="所属分工会" v-if="formData.reserve_type===2">
|
||||
<el-input :value="bookingUnionName" readonly :placeholder="unionLoading ? '正在加载所属分工会' : '未配置所属分工会'"></el-input>
|
||||
</el-form-item>
|
||||
<!-- 所属协会仅用于协会预约。 -->
|
||||
<el-form-item label="所属协会" v-if="formData.reserve_type===3">
|
||||
<el-select v-model="formData.clubId" placeholder="请选择所属协会" style="width:100%">
|
||||
<el-option v-for="club in myClubs" :key="club.id" :label="club.name" :value="club.id"></el-option>
|
||||
@@ -503,6 +511,8 @@ layout("/layouts/platform.html"){
|
||||
max: 24,
|
||||
min: 0,
|
||||
myClubs: [],
|
||||
bookingUnionName:'', unionLoading:false, unionRequestVersion:0,
|
||||
bookingSlots:[], slotsLoading:false, slotsVersion:0,
|
||||
formLoading: false,
|
||||
slider_width: 0,
|
||||
single_width: 0,
|
||||
@@ -529,7 +539,7 @@ layout("/layouts/platform.html"){
|
||||
end_time: '',
|
||||
reserve_person_phone: '',
|
||||
reserve_cause: '',
|
||||
time: '',
|
||||
time: [],
|
||||
sex: "${@shiro.getPrincipalProperty('sex')}",
|
||||
loginname: "${@shiro.getPrincipalProperty('loginname')}"
|
||||
},
|
||||
@@ -551,7 +561,8 @@ layout("/layouts/platform.html"){
|
||||
start_time: [{required: true, message: '请选择预约时间', trigger: ['blur', 'change']}],
|
||||
time: [{required: true, message: '请选择预约时间', trigger: ['blur', 'change']}],
|
||||
reserve_type: [{required: true, message: '请选择预约类型', trigger: ['blur', 'change']}],
|
||||
/*reserve_cause: [{required: true, message: '请选择预约事由', trigger: ['blur', 'change']}],*/
|
||||
// 与后端必填要求一致,显示星号并阻止空白事由提交。
|
||||
reserve_cause: [{required: true, whitespace: true, message: '请填写预约事由', trigger: ['blur', 'change']}],
|
||||
},
|
||||
dayArray: [],
|
||||
holidays: ['2022-06-03', '2022-06-04', '2022-06-05', '2022-09-10', '2022-09-11', '2022-09-12',
|
||||
@@ -560,17 +571,60 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
components: {
|
||||
'guava': httpVueLoader('/components/plugins/Guava.vue'),
|
||||
'reserve-info': httpVueLoader('/components/activity/site/ReserveInfo.vue'),
|
||||
'reserve-info': httpVueLoader('/components/activity/site/ReserveInfo.vue?v=20260910-2'),
|
||||
},
|
||||
methods: {
|
||||
// 每次打开表单刷新组织信息;结果只用于展示,提交仍由服务端取得真实组织。
|
||||
loadBookingUnion() {
|
||||
const version=this.unionRequestVersion+1
|
||||
this.$set(this,'unionRequestVersion',version)
|
||||
this.$set(this,'bookingUnionName','')
|
||||
this.$set(this,'unionLoading',true)
|
||||
$.post('/platform/activity/site/reserve/myUnion').then(res=>{
|
||||
if(version!==this.unionRequestVersion)return
|
||||
if(res && res.code===0)this.$set(this,'bookingUnionName',res.data && res.data.name || '')
|
||||
else this.$message.error(res && res.msg || '所属分工会加载失败')
|
||||
},()=>{if(version===this.unionRequestVersion)this.$message.error('所属分工会加载失败,请重新打开表单')}).always(()=>{
|
||||
if(version===this.unionRequestVersion)this.$set(this,'unionLoading',false)
|
||||
})
|
||||
},
|
||||
diaClose() {
|
||||
this.formData.time = ''
|
||||
this.$set(this.formData, 'time', [])
|
||||
this.formData.start_time = ''
|
||||
this.formData.end_time = ''
|
||||
},
|
||||
timeChange(o) {
|
||||
this.formData.start_time = o.split('-')[0]
|
||||
this.formData.end_time = o.split('-')[1]
|
||||
timeChange(values) {
|
||||
const times=values.slice().sort()
|
||||
this.$set(this.formData, 'start_time', times.length ? times[0].split('-')[0] : '')
|
||||
this.$set(this.formData, 'end_time', times.length ? times[times.length-1].split('-')[1] : '')
|
||||
},
|
||||
// 分工会与协会切换时保留已选时间,仅刷新所有所选日期的可用场次;版本号阻止旧响应覆盖。
|
||||
loadBookingSlots() {
|
||||
const version=this.slotsVersion+1
|
||||
this.$set(this,'slotsVersion',version)
|
||||
this.$set(this,'slotsLoading',true)
|
||||
this.$set(this,'bookingSlots',[])
|
||||
const days=this.dayArray.slice(), results=[]
|
||||
let pending=days.length, failed=false
|
||||
if(!pending){this.$set(this,'slotsLoading',false);return}
|
||||
days.forEach(day=>{
|
||||
$.post('/mobile/activity/site/info/getReserve',{siteId:this.formData.site_id,day,reserveType:this.formData.reserve_type}).then(res=>{
|
||||
if(version!==this.slotsVersion)return
|
||||
if(res && res.code===0)results.push(res.data)
|
||||
else{failed=true;this.$message.error(res && res.msg || '场次加载失败')}
|
||||
},()=>{if(version===this.slotsVersion){failed=true;this.$message.error('场次加载失败')}}).always(()=>{
|
||||
if(version!==this.slotsVersion)return
|
||||
pending--
|
||||
if(pending===0){
|
||||
if(!failed && results.length)this.$set(this,'bookingSlots',results[0].map(row=>{
|
||||
const blocked=results.map(rows=>rows.find(item=>item.start_time===row.start_time && item.end_time===row.end_time)).find(item=>!item || item.code!==1)
|
||||
const missing=results.some(rows=>!rows.some(item=>item.start_time===row.start_time && item.end_time===row.end_time))
|
||||
return Object.assign({},row,{disabled:missing || !!blocked,msg:missing ? '场次已变化' : blocked ? blocked.msg : row.msg})
|
||||
}))
|
||||
this.$set(this,'slotsLoading',false)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
dropdownCommand(command) {
|
||||
const {type, data} = command
|
||||
@@ -705,7 +759,7 @@ layout("/layouts/platform.html"){
|
||||
this.$set(this.formData, "reserve_person", '${@shiro.getPrincipalProperty("username")}')
|
||||
this.$set(this.formData, "reserve_person_unit", '${@shiro.getPrincipalProperty("unit").getName()}')
|
||||
this.$set(this.formData, "reserve_person_phone", '${@shiro.getPrincipalProperty("mobile")}')
|
||||
this.$set(this.formData, 'reserve_type', 1)
|
||||
this.$set(this.formData, 'reserve_type', 2)
|
||||
// 加载协会预约的可选协会;只有一个时默认选中,多协会由申请人选择。
|
||||
$.post('/platform/activity/site/reserve/myClubs').then((res)=>{
|
||||
if(res.code===0){this.myClubs=res.data;this.$set(this.formData,'clubId',res.data.length===1 ? res.data[0].id : null)}
|
||||
@@ -716,7 +770,13 @@ layout("/layouts/platform.html"){
|
||||
this.$refs['addForm'].resetFields()
|
||||
}
|
||||
|
||||
this.$set(this.formData, 'reserve_type', 2)
|
||||
// 新开申请时清空上次时段;类型切换刷新场次时不重置选择。
|
||||
this.$set(this.formData, 'time', [])
|
||||
this.timeChange([])
|
||||
this.loadBookingUnion()
|
||||
this.addDialogVisible = true
|
||||
this.loadBookingSlots()
|
||||
|
||||
this.$nextTick(() => {
|
||||
|
||||
@@ -728,7 +788,9 @@ layout("/layouts/platform.html"){
|
||||
|
||||
},
|
||||
doDelete(id, op) {
|
||||
this.$confirm('确定撤销此预约?已审核的申请不能撤销。','提示').then(()=>{
|
||||
// op=1 删除已撤销申请的全部时段,op=2 仅撤销申请,分别明确操作后果。
|
||||
const message = op === 1 ? '确定删除此已撤销预约及其全部时段?删除后不可恢复。' : '确定撤销此预约?已审核的申请不能撤销。'
|
||||
this.$confirm(message,'提示').then(()=>{
|
||||
this.formLoading=true;
|
||||
return $.post(loc()+'/doDelete',{id,op}).then((res)=>{if(res.code===0){this.pageData()}else{this.$message.error(res.msg)}}).always(()=>{this.formLoading=false})
|
||||
}).catch(()=>{});
|
||||
@@ -737,12 +799,16 @@ layout("/layouts/platform.html"){
|
||||
return (moment("1970-01-01 " + hm).valueOf() + HOUR8) / HOUR1
|
||||
},
|
||||
doAdd() {
|
||||
if (!this.formData.time) { this.$message.warning('请选择预约时间'); return; }
|
||||
if (this.slotsLoading || !this.formData.time || !this.formData.time.length) { this.$message.warning('请选择预约时间'); return; }
|
||||
if (this.formData.reserve_type===3 && !this.formData.clubId) { this.$message.warning('请选择所属协会'); return; }
|
||||
const selected=this.formData.time.slice().sort()
|
||||
if(Number(this.infoViewData.reserveTimeType || this.infoViewData.reservetimetype || 1)===2 && selected.some((time,i)=>i>0 && selected[i-1].split('-')[1]!==time.split('-')[0])){
|
||||
this.$message.warning('全天候预约请选择连续时段');return
|
||||
}
|
||||
this.$refs.addForm.validate((valid)=>{
|
||||
if(!valid) return;
|
||||
this.formLoading=true; this.subDis=true;
|
||||
$.post(loc()+'/doAdd',{data:JSON.stringify(this.formData),days:JSON.stringify(this.dayArray)}).then((res)=>{
|
||||
$.post(loc()+'/doAdd',{data:JSON.stringify(this.formData),days:JSON.stringify(this.dayArray),times:JSON.stringify(this.formData.time)}).then((res)=>{
|
||||
if(res.code===0){this.addDialogVisible=false;this.$message.success('预约成功');this.pageData();this.$refs.guava.index()}
|
||||
else{this.$message.error(res.msg)}
|
||||
}).always(()=>{this.formLoading=false;this.subDis=false})
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
<!--# layout("/layouts/platform.html"){ #-->
|
||||
<style>
|
||||
/* 日期和每个时间段保持完整,同日连续场次由后端统一合并展示。 */
|
||||
.reservation-times { text-align:left; font-variant-numeric:tabular-nums; }
|
||||
.reservation-time-day { display:flex; align-items:baseline; flex-wrap:wrap; gap:4px 12px; padding:3px 0; line-height:22px; }
|
||||
.reservation-time-date { flex:0 0 88px; white-space:nowrap; font-weight:500; }
|
||||
.reservation-time-ranges { display:flex; flex-wrap:wrap; gap:2px 12px; }
|
||||
.reservation-time-range { white-space:nowrap; }
|
||||
/* 仅审核列表居中整个日期时段块,详情继续使用原有对齐。 */
|
||||
.reservation-times-list { display:inline-block; max-width:100%; vertical-align:middle; }
|
||||
</style>
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
@@ -44,7 +54,13 @@
|
||||
<el-table-column align="center" header-align="center" label="所属协会" min-width="130">
|
||||
<template slot-scope="{row}">{{row.club_name || '—'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="预约时段" prop="concat_day" min-width="230"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="预约时段" min-width="245"><template slot-scope="{row}"><div class="reservation-times reservation-times-list">
|
||||
<div v-for="group in row.reservationTimeGroups" :key="group.date" class="reservation-time-day">
|
||||
<span class="reservation-time-date">{{group.date}}</span>
|
||||
<div class="reservation-time-ranges"><span v-for="(range,index) in group.ranges" :key="index" class="reservation-time-range">{{range.start}}–{{range.end}}</span></div>
|
||||
</div>
|
||||
<span v-if="!row.reservationTimeGroups || !row.reservationTimeGroups.length">—</span>
|
||||
</div></template></el-table-column>
|
||||
<!-- 状态颜色取自后端 state 配置,未配置时使用页面默认字体颜色。 -->
|
||||
<el-table-column align="center" header-align="center" label="申请状态" prop="state_name" min-width="120">
|
||||
<template slot-scope="{row}"><span :style="{color: row.state_color || null}">{{row.state_name}}</span></template>
|
||||
@@ -135,7 +151,7 @@
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
typeName(type) { return {1:'个人预约',2:'单位预约',3:'协会预约'}[type] || '—' },
|
||||
typeName(type) { return {1:'个人预约',2:'分工会预约',3:'协会预约'}[type] || '—' },
|
||||
// 以申请实际节点决定审核入口,已流转的申请只允许查看。
|
||||
canReview(row) {
|
||||
const stage = this.reviewApi.substring(this.reviewApi.lastIndexOf('/') + 1)
|
||||
@@ -195,7 +211,7 @@
|
||||
},
|
||||
created() {
|
||||
this.pageData()
|
||||
// 历史审核可按停用的场地类型查询,复用记录页面的类型列表工具。
|
||||
// 三个审核页面共用职工之家的类型选项,不再展示其他场地类型。
|
||||
new typeUtil().getAllType().then((types) => {
|
||||
this.$set(this, 'activityTypeList', types)
|
||||
}).fail(() => { this.$message.error('场地类型加载失败,请刷新重试') })
|
||||
|
||||
@@ -13,7 +13,13 @@ const info = {
|
||||
<el-descriptions-item label="场地名称" :span="2">{{viewData.site_name}}</el-descriptions-item>
|
||||
<!-- 详情与审核列表使用同一状态配置颜色。 -->
|
||||
<el-descriptions-item label="申请状态"><span :style="{color: viewData.state_color || null}">{{viewData.state_name}}</span></el-descriptions-item>
|
||||
<el-descriptions-item label="预约时段" :span="3">{{viewData.concat_day}}</el-descriptions-item>
|
||||
<el-descriptions-item label="预约时段" :span="3"><div class="reservation-times">
|
||||
<div v-for="group in viewData.reservationTimeGroups" :key="group.date" class="reservation-time-day">
|
||||
<span class="reservation-time-date">{{group.date}}</span>
|
||||
<div class="reservation-time-ranges"><span v-for="(range,index) in group.ranges" :key="index" class="reservation-time-range">{{range.start}}–{{range.end}}</span></div>
|
||||
</div>
|
||||
<span v-if="!viewData.reservationTimeGroups || !viewData.reservationTimeGroups.length">—</span>
|
||||
</div></el-descriptions-item>
|
||||
<el-descriptions-item label="预约事由" :span="3">
|
||||
<div class="text-left" style="white-space:pre-wrap">{{viewData.reserve_cause}}</div>
|
||||
</el-descriptions-item>
|
||||
@@ -39,7 +45,7 @@ const info = {
|
||||
return {viewData: {}, activeName: 'info', detailLoading: false, requestId: 0}
|
||||
},
|
||||
methods: {
|
||||
typeName(type) { return {1:'个人预约',2:'单位预约',3:'协会预约'}[type] || '—' },
|
||||
typeName(type) { return {1:'个人预约',2:'分工会预约',3:'协会预约'}[type] || '—' },
|
||||
// id 为预约记录主键,activeName 为申请信息或审核标签;结果含预约字段及 auditListTable 审核历史。
|
||||
onOpen(id, activeName = 'info') {
|
||||
const requestId = this.requestId + 1
|
||||
|
||||
@@ -18,8 +18,8 @@ const activity = {
|
||||
<i class="el-icon-star-on" style="color: red; font-size: 17px"></i>{{item.name}}
|
||||
<el-tag type="danger" v-if="moment(item.endDate).valueOf()<moment().valueOf()" size="mini">报名已结束</el-tag>
|
||||
</div>
|
||||
<div style="font-size: 12px; line-height: 20px; color: #999">开始时间:{{moment(item.startDate).format('YYYY-MM-DD HH:mm')}}</div>
|
||||
<div style="font-size: 12px; line-height: 20px; color: #999">结束时间:{{moment(item.endDate).format('YYYY-MM-DD HH:mm')}}</div>
|
||||
<div style="font-size: 12px; line-height: 20px; color: #999">开始时间:{{formatActivityTime(item, item.startDate)}}</div>
|
||||
<div style="font-size: 12px; line-height: 20px; color: #999">结束时间:{{formatActivityTime(item, item.endDate)}}</div>
|
||||
</div>
|
||||
<div @click="enterActivity(item)">
|
||||
<el-tag style="cursor: pointer">查看<i class="el-icon-s-promotion"></i></el-tag>
|
||||
@@ -43,6 +43,10 @@ const activity = {
|
||||
"open-qr-code": httpVueLoader("/components/plugins/OpenQRCode.vue?v=" + new Date().getTime())
|
||||
},
|
||||
methods: {
|
||||
// 场地入口仅展示每日时间;其他活动仍展示完整日期,数据库日期继续用于首页有效期筛选。
|
||||
formatActivityTime(item, value) {
|
||||
return moment(value).format(item.id === 'home_site_reserve_entry' ? 'HH:mm:ss' : 'YYYY-MM-DD HH:mm')
|
||||
},
|
||||
listActivity() {
|
||||
$.get("/platform/sys/homeActivity/listHomeActivity").then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
|
||||
@@ -14,7 +14,7 @@ const todo = {
|
||||
<el-table-column label="流程流转环节" prop="nodeName"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template scope="{row}">
|
||||
<el-button @click="sublime.jumpPagePjax(row.formUrl)" size="mini" type="primary">查看</el-button>
|
||||
<el-button @click="sublime.jumpPagePjax(todoActive === '2' ? (row.formUrlView || row.formUrl) : row.formUrl)" size="mini" type="primary">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -32,7 +32,7 @@ const todo = {
|
||||
<el-table-column label="流程流转环节" prop="nodeName"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template scope="{row}">
|
||||
<el-button @click="sublime.jumpPagePjax(row.formUrl)" size="mini" type="primary">查看</el-button>
|
||||
<el-button @click="sublime.jumpPagePjax(todoActive === '2' ? (row.formUrlView || row.formUrl) : row.formUrl)" size="mini" type="primary">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -50,7 +50,7 @@ const todo = {
|
||||
<el-table-column label="流程流转环节" prop="nodeName"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template scope="{row}">
|
||||
<el-button @click="sublime.jumpPagePjax(row.formUrl)" size="mini" type="primary">查看</el-button>
|
||||
<el-button @click="sublime.jumpPagePjax(todoActive === '2' ? (row.formUrlView || row.formUrl) : row.formUrl)" size="mini" type="primary">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -69,7 +69,7 @@ const todo = {
|
||||
<!-- <el-table-column label="流程流转环节" prop="nodeName"></el-table-column>-->
|
||||
<!-- <el-table-column label="操作" width="100">-->
|
||||
<!-- <template scope="{row}">-->
|
||||
<!-- <el-button @click="sublime.jumpPagePjax(row.formUrl)" size="mini" type="primary">查看</el-button>-->
|
||||
<!-- <el-button @click="sublime.jumpPagePjax(todoActive === '2' ? (row.formUrlView || row.formUrl) : row.formUrl)" size="mini" type="primary">查看</el-button>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<!-- </el-table>-->
|
||||
|
||||
Reference in New Issue
Block a user