Compare commits
8
Commits
release_prd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
743e6c5428 | ||
|
|
09e955fb38 | ||
|
|
7f578e1f9e | ||
|
|
d8f6298dd8 | ||
|
|
6b0586b638 | ||
|
|
15aef647d1 | ||
|
|
f3021514fd | ||
|
|
4856cd5ddb |
@@ -166,7 +166,15 @@ public class SysHomeController {
|
|||||||
@Ok("json")
|
@Ok("json")
|
||||||
public Result listHomeActivity() {
|
public Result listHomeActivity() {
|
||||||
FieldFilter fieldFilter = FieldFilter.locked(Sys_home_activity.class, "classPath");
|
FieldFilter fieldFilter = FieldFilter.locked(Sys_home_activity.class, "classPath");
|
||||||
List<Sys_home_activity> list = Daos.ext(dao, fieldFilter).query(Sys_home_activity.class, Cnd.where("enable", "=", 1).desc("top").desc("createdAt").desc("updatedAt"));
|
List<Sys_home_activity> list = Daos.ext(dao, fieldFilter)
|
||||||
|
.query(
|
||||||
|
Sys_home_activity.class,
|
||||||
|
Cnd.where("enable", "=", 1)
|
||||||
|
.desc("top")
|
||||||
|
.desc("startDate")
|
||||||
|
.desc("createdAt")
|
||||||
|
.desc("updatedAt")
|
||||||
|
);
|
||||||
String userId = SecurityUtil.getUserId();
|
String userId = SecurityUtil.getUserId();
|
||||||
List<Sys_home_activity> allowActivityList = new ArrayList<>();
|
List<Sys_home_activity> allowActivityList = new ArrayList<>();
|
||||||
// 今天的时间
|
// 今天的时间
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
Map<String, Date> doctoralData = pullPostDoctoral();
|
Map<String, Date> doctoralData = pullPostDoctoral();
|
||||||
|
|
||||||
Date nowDate = new Date();
|
Date nowDate = new Date();
|
||||||
String appId = "1926887238437818370";
|
String appId = "XXXXXXXXXXXXXXXXXXXXX";
|
||||||
String secret = "32df31fcf4fc43f9a647ee1f47134f36";
|
String secret = "XXXXXXXXXXXXXXXXXXXX";
|
||||||
|
|
||||||
List<JSONObject> rawDataList = new ArrayList<>();
|
List<JSONObject> rawDataList = new ArrayList<>();
|
||||||
int skip = 0;
|
int skip = 0;
|
||||||
@@ -280,8 +280,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Date> pullPostDoctoral() {
|
public Map<String, Date> pullPostDoctoral() {
|
||||||
String appId = "1926887238437818370";
|
String appId = "XXXXXXXXXXXXXXXXXXXXXX";
|
||||||
String secret = "32df31fcf4fc43f9a647ee1f47134f36";
|
String secret = "XXXXXXXXXXXXXXXXXXXXXX";
|
||||||
|
|
||||||
List<JSONObject> allUsers = new ArrayList<>();
|
List<JSONObject> allUsers = new ArrayList<>();
|
||||||
int skip = 0;
|
int skip = 0;
|
||||||
|
|||||||
+1
-5
@@ -112,11 +112,7 @@ public class ActivityCultureUserStatisticsController {
|
|||||||
if (Strings.isNotBlank(page.getSearchName()) && Strings.isNotBlank(page.getSearchKeyword())) {
|
if (Strings.isNotBlank(page.getSearchName()) && Strings.isNotBlank(page.getSearchKeyword())) {
|
||||||
cnd.where().andLike(page.getSearchName(), page.getSearchKeyword());
|
cnd.where().andLike(page.getSearchName(), page.getSearchKeyword());
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(page.getPageOrderName()) && StrUtil.isNotBlank(page.getPageOrderBy())) {
|
activityCultureApplyUserService.applyUserStatisticsSort(cnd, page);
|
||||||
cnd.orderBy(page.getPageOrderName(), page.getPageOrderBy().equalsIgnoreCase("ascending") ? "ASC" : "DESC");
|
|
||||||
} else {
|
|
||||||
cnd.desc("atp.applyDateTime");
|
|
||||||
}
|
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
Pagination pagination = activityCultureApplyUserService.listPageMap(page.getPageNumber(), page.getPageSize(), sql);
|
Pagination pagination = activityCultureApplyUserService.listPageMap(page.getPageNumber(), page.getPageSize(), sql);
|
||||||
List<NutMap> list = pagination.getList();
|
List<NutMap> list = pagination.getList();
|
||||||
|
|||||||
+9
@@ -6,6 +6,7 @@ import com.budwk.app.base.param.PageForm;
|
|||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
import com.budwk.app.zhgh.activity.culture.models.ActivityTissuePerson;
|
import com.budwk.app.zhgh.activity.culture.models.ActivityTissuePerson;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -19,6 +20,14 @@ public interface ActivityCultureApplyUserService extends BaseService<ActivityTis
|
|||||||
boolean isEnrolled,
|
boolean isEnrolled,
|
||||||
Integer state);
|
Integer state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置学校、工会和社团活动人员统计的分页顺序。
|
||||||
|
*
|
||||||
|
* @param cnd 查询条件;方法会在其上追加 ORDER BY
|
||||||
|
* @param page 分页参数;pageOrderName 和 pageOrderBy 均有值时采用用户选择的排序,否则按工号正序
|
||||||
|
*/
|
||||||
|
void applyUserStatisticsSort(Cnd cnd, PageForm page);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 活动报名
|
* 活动报名
|
||||||
|
|||||||
+11
@@ -110,6 +110,17 @@ public class ActivityCultureApplyUserServiceImpl extends BaseServiceImpl<Activit
|
|||||||
return pagination;
|
return pagination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyUserStatisticsSort(Cnd cnd, PageForm page) {
|
||||||
|
if (StrUtil.isNotBlank(page.getPageOrderName()) && StrUtil.isNotBlank(page.getPageOrderBy())) {
|
||||||
|
cnd.orderBy(page.getPageOrderName(), page.getPageOrderBy().equalsIgnoreCase("ascending") ? "ASC" : "DESC");
|
||||||
|
} else {
|
||||||
|
// 三类活动统计默认按工号正序;相同工号再按报名时间倒序保证分页顺序稳定。
|
||||||
|
cnd.asc("atp.loginName");
|
||||||
|
cnd.desc("atp.applyDateTime");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void signUp(String activityId, List<String> personIds, JSONObject dynamicFormParam) {
|
public void signUp(String activityId, List<String> personIds, JSONObject dynamicFormParam) {
|
||||||
ActivityTissue tissue = dao().fetch(ActivityTissue.class, activityId);
|
ActivityTissue tissue = dao().fetch(ActivityTissue.class, activityId);
|
||||||
|
|||||||
+7
-31
@@ -521,6 +521,12 @@ public class FamilyActivityApplyController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交当前登录教职工的报名,不接受客户端指定报名用户或已有报名ID。
|
||||||
|
* @param familyUser courseId为子活动ID,mobile为联系方式,mobileColumnsValue为家属动态字段二维列表,
|
||||||
|
* 独立报名课程还需activityCourseId时段ID;活动归属及用户由service确定
|
||||||
|
* @return Result JSON,code为0表示正常或候补报名成功,msg说明结果或未通过的规则
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("活动报名")
|
@ApiOperation("活动报名")
|
||||||
@SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR)
|
||||||
@@ -529,37 +535,7 @@ public class FamilyActivityApplyController {
|
|||||||
public Result doSignUp(FamilyUser familyUser) {
|
public Result doSignUp(FamilyUser familyUser) {
|
||||||
try {
|
try {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
FamilyActivity activity = dao.fetch(FamilyActivity.class, familyUser.getActivityId());
|
// 家属、名额和时间冲突由service在数据库锁及事务内统一校验,避免普通报名与快速报名规则分叉。
|
||||||
if(Lang.isEmpty(familyUser.getMobileColumnsValue())) {
|
|
||||||
return Result.error(99,"请填写" + activity.getKeyWord() + "信息");
|
|
||||||
}
|
|
||||||
boolean courseByUser = familyActivityService.isSignCourseByUser(familyUser.getCourseId(), SecurityUtil.getUserId());
|
|
||||||
if(courseByUser) {
|
|
||||||
return Result.error(99,"您已报过该活动");
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<Boolean, String> validFamilyCount = familyActivityService.validFamilyCount(familyUser);
|
|
||||||
if(validFamilyCount.containsKey(false)) {
|
|
||||||
return Result.error(99,validFamilyCount.get(false));
|
|
||||||
}
|
|
||||||
// 判断人数
|
|
||||||
int number = 0;
|
|
||||||
FamilyCourse course = familyActivityService.dao().fetch(FamilyCourse.class, familyUser.getCourseId());
|
|
||||||
FamilyType type = familyActivityService.dao().fetch(FamilyType.class, course.getCourseType());
|
|
||||||
if(type != null) {
|
|
||||||
if(type.getIsBringFamily() && type.getIsAddFamily()) {
|
|
||||||
List<List<NutMap>> mobileColumnsValue = familyUser.getMobileColumnsValue();
|
|
||||||
number = Lang.isNotEmpty(mobileColumnsValue) ? mobileColumnsValue.size() : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
boolean signFull = familyActivityService.isSignFull(course, number);
|
|
||||||
if(signFull) {
|
|
||||||
return Result.error(99,"当前报名人数已满");
|
|
||||||
}
|
|
||||||
boolean signFullByUnionId = familyActivityService.isSignFullByUnionId(course, number);
|
|
||||||
if(signFullByUnionId) {
|
|
||||||
return Result.error(99,"该活动您所在的分工会名额不足");
|
|
||||||
}
|
|
||||||
FamilyUser finishUser = familyActivityService.doSignUp(familyUser);
|
FamilyUser finishUser = familyActivityService.doSignUp(familyUser);
|
||||||
if(finishUser.getState() == 2) {
|
if(finishUser.getState() == 2) {
|
||||||
return Result.success("正式报名已满,您已列为替补,如有老师放弃报名,将会按先后顺序依次替补。请您自行关注后续结果。谢谢!");
|
return Result.success("正式报名已满,您已列为替补,如有老师放弃报名,将会按先后顺序依次替补。请您自行关注后续结果。谢谢!");
|
||||||
|
|||||||
+9
@@ -13,6 +13,8 @@ import io.swagger.annotations.Api;
|
|||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.dao.Chain;
|
import org.nutz.dao.Chain;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
@@ -144,11 +146,18 @@ public class FamilyActivityController {
|
|||||||
return Result.success().addData(dataMap);
|
return Result.success().addData(dataMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存或发布活动配置,业务校验由service执行。
|
||||||
|
* @param activity 活动及子活动列表;familyCountScope为1按类型、2按活动,timeConflictCheck为重叠校验开关,
|
||||||
|
* onlyKey为各类型共有且必填的字段编码,familyMaxCount为活动累计上限
|
||||||
|
* @return Result JSON,code为0表示保存成功,配置不完整时返回具体业务错误
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@Ok("json:full")
|
@Ok("json:full")
|
||||||
@ApiOperation("亲子活动新增/修改")
|
@ApiOperation("亲子活动新增/修改")
|
||||||
@SaCheckPermission("family.manage")
|
@SaCheckPermission("family.manage")
|
||||||
@SLog(tag = "亲子活动-活动管理", msg = "新增/修改活动")
|
@SLog(tag = "亲子活动-活动管理", msg = "新增/修改活动")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result doHandle(FamilyActivity activity) {
|
public Result doHandle(FamilyActivity activity) {
|
||||||
if (StrUtil.isBlank(activity.getId())) {
|
if (StrUtil.isBlank(activity.getId())) {
|
||||||
familyActivityManageService.add(activity, null);
|
familyActivityManageService.add(activity, null);
|
||||||
|
|||||||
+28
-42
@@ -1,8 +1,10 @@
|
|||||||
package com.budwk.app.zhgh.activity.family.controller.manage;
|
package com.budwk.app.zhgh.activity.family.controller.manage;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
@@ -24,7 +26,6 @@ import org.nutz.mvc.annotation.At;
|
|||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,59 +119,44 @@ public class FamilyAdjustController {
|
|||||||
return Result.success(list);
|
return Result.success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param activityId 当前活动ID
|
||||||
|
* @param oldCourseId 人员原报名子活动ID
|
||||||
|
* @param newCourseId 同活动内的目标子活动ID
|
||||||
|
* @param userId 被调整教职工ID,按该人员而非管理员校验家属和时间规则
|
||||||
|
* @return Result JSON;code为0表示调整成功,失败时msg说明原因且保留原报名
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("人员调整")
|
@ApiOperation("人员调整")
|
||||||
@SaCheckPermission("family.adjust")
|
@SaCheckPermission("family.adjust")
|
||||||
@SLog(tag = "亲子活动-人员调整", msg = "人员调整")
|
@SLog(tag = "亲子活动-人员调整", msg = "人员调整")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result adjust(String activityId, String oldCourseId, String newCourseId, String userId) {
|
public Result adjust(String activityId, String oldCourseId, String newCourseId, String userId) {
|
||||||
|
try {
|
||||||
Cnd oldCnd = Cnd.where("activityId", "=", activityId)
|
familyActivityManageService.adjustSignUp(activityId, oldCourseId, newCourseId, userId);
|
||||||
.and("courseId", "=", oldCourseId).and("userId", "=", userId);
|
|
||||||
|
|
||||||
Cnd newCnd = Cnd.where("activityId", "=", activityId)
|
|
||||||
.and("courseId", "=", newCourseId).and("userId", "=", userId);
|
|
||||||
|
|
||||||
//旧的报名信息
|
|
||||||
FamilyUser oldfamilyUser = dao.fetch(FamilyUser.class, oldCnd);
|
|
||||||
oldfamilyUser.setCourseId(newCourseId);
|
|
||||||
oldfamilyUser.setSignUpTime(DateUtil.date());
|
|
||||||
dao.update(oldfamilyUser);
|
|
||||||
|
|
||||||
//新的课程的信息,上课时间
|
|
||||||
List<FamilyActivityCourse> activityCourseList = dao.query(FamilyActivityCourse.class, Cnd.where("activityId", "=", activityId)
|
|
||||||
.and("courseId", "=", newCourseId));
|
|
||||||
//先清楚旧的信息
|
|
||||||
dao.clear(FamilyUserCourse.class, oldCnd);
|
|
||||||
//添加新的信息
|
|
||||||
List<FamilyUserCourse> familyUserCourseList = new ArrayList<>();
|
|
||||||
activityCourseList.forEach(item -> {
|
|
||||||
FamilyUserCourse course = new FamilyUserCourse();
|
|
||||||
course.setActivityCourseId(activityId);
|
|
||||||
course.setCourseId(newCourseId);
|
|
||||||
course.setUserId(userId);
|
|
||||||
course.setCourseStartTime(item.getCourseStartTime());
|
|
||||||
course.setCourseEndTime(item.getCourseEndTime());
|
|
||||||
course.setActivityCourseId(item.getId());
|
|
||||||
familyUserCourseList.add(course);
|
|
||||||
});
|
|
||||||
dao.insert(familyUserCourseList);
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
|
} catch (BaseException e) {
|
||||||
|
return Result.error(99, e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param activityId 当前活动ID
|
||||||
|
* @param courseId 待删除报名所属子活动ID
|
||||||
|
* @param userId 被删除报名的教职工ID
|
||||||
|
* @return Result JSON;code为0表示对应报名及其时段记录已删除
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("删除报名人员")
|
@ApiOperation("删除报名人员")
|
||||||
@SaCheckPermission("family.adjust")
|
@SaCheckPermission("family.adjust")
|
||||||
@SLog(tag = "亲子活动-人员调整", msg = "删除报名人员")
|
@SLog(tag = "亲子活动-人员调整", msg = "删除报名人员")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result deleteSignUser(String activityId, String courseId, String userId) {
|
public Result deleteSignUser(String activityId, String courseId, String userId) {
|
||||||
|
try {
|
||||||
//删除
|
familyActivityManageService.deleteSignUser(activityId, courseId, userId);
|
||||||
Cnd cnd = Cnd.NEW();
|
|
||||||
cnd.and("activityId", "=", activityId);
|
|
||||||
cnd.and("courseId", "=", courseId);
|
|
||||||
cnd.and("userId", "=", userId);
|
|
||||||
|
|
||||||
dao.clear(FamilyUser.class, cnd);
|
|
||||||
dao.clear(FamilyUserCourse.class, cnd);
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
|
} catch (BaseException e) {
|
||||||
|
return Result.error(99, e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,18 @@ public class FamilyActivity extends BaseModel implements Serializable, SysHomeCo
|
|||||||
@ColDefine(type = ColType.INT)
|
@ColDefine(type = ColType.INT)
|
||||||
private Integer familyMaxCount;
|
private Integer familyMaxCount;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("家属人数限制范围:1按子活动类型累计,2按整个活动累计;历史空值按1处理")
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
@Default("1")
|
||||||
|
private Integer familyCountScope;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("是否禁止同一教职工下同一家属报名本活动内时间重叠的子活动,含候补报名")
|
||||||
|
@ColDefine(type = ColType.BOOLEAN)
|
||||||
|
@Default("0")
|
||||||
|
private Boolean timeConflictCheck;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("判断家属数量的唯一标识")
|
@Comment("判断家属数量的唯一标识")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
|
|||||||
+22
-2
@@ -76,6 +76,25 @@ public interface FamilyActivityService extends BaseService<FamilyActivity> {
|
|||||||
*/
|
*/
|
||||||
void cancelSignUp(String activityId, String courseId);
|
void cancelSignUp(String activityId, String courseId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台将指定教职工的正常报名调整到同一活动的另一子活动,复用家属及时间冲突规则。
|
||||||
|
* @param activityId 活动ID,原课程与目标课程必须属于该活动
|
||||||
|
* @param oldCourseId 原课程ID
|
||||||
|
* @param newCourseId 目标课程ID;独立时段课程须只有一个可确定的时段
|
||||||
|
* @param userId 被调整教职工的用户ID,不使用管理员身份参与统计
|
||||||
|
* @return 无返回值;不满足限制时抛出业务异常,原报名不变
|
||||||
|
*/
|
||||||
|
void adjustSignUp(String activityId, String oldCourseId, String newCourseId, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台删除指定教职工的报名,与报名和调班共用数据库锁,避免并发统计读取到中间状态。
|
||||||
|
* @param activityId 活动ID
|
||||||
|
* @param courseId 课程ID
|
||||||
|
* @param userId 被删除报名的教职工ID
|
||||||
|
* @return 无返回值;仅删除匹配的报名及其课程时段记录
|
||||||
|
*/
|
||||||
|
void deleteSignUser(String activityId, String courseId, String userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询当前用户在指定课程中的预填信息。
|
* 查询当前用户在指定课程中的预填信息。
|
||||||
*
|
*
|
||||||
@@ -120,7 +139,8 @@ public interface FamilyActivityService extends BaseService<FamilyActivity> {
|
|||||||
FamilyUser getSignUpInfo(String courseId, String userId);
|
FamilyUser getSignUpInfo(String courseId, String userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验当前用户是否可以修改指定课程的报名信息。
|
* 打开编辑表单前校验当前用户报名归属、报名时间和人员范围。
|
||||||
|
* 家属、人数和时间冲突在保存新数据时重新校验,允许历史冲突记录进入表单整改。
|
||||||
*
|
*
|
||||||
* @param courseId 课程ID;当前用户必须已经报名该课程
|
* @param courseId 课程ID;当前用户必须已经报名该课程
|
||||||
* @return 无返回值;校验不通过时抛出包含具体原因的业务异常
|
* @return 无返回值;校验不通过时抛出包含具体原因的业务异常
|
||||||
@@ -166,7 +186,7 @@ public interface FamilyActivityService extends BaseService<FamilyActivity> {
|
|||||||
boolean isSignFull(FamilyCourse course, Integer currentFamilyNumber);
|
boolean isSignFull(FamilyCourse course, Integer currentFamilyNumber);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验本次提交的家属数量、唯一标识及分场冲突。
|
* 校验本次提交的类型家属上限、活动累计家属上限、唯一标识及分场冲突。
|
||||||
* 当参数包含报名记录ID时,会排除该记录后再校验,用于修改报名。
|
* 当参数包含报名记录ID时,会排除该记录后再校验,用于修改报名。
|
||||||
*
|
*
|
||||||
* @param user 待校验的报名信息
|
* @param user 待校验的报名信息
|
||||||
|
|||||||
+385
-29
@@ -48,10 +48,74 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增、沿用和编辑活动共用配置校验。activity 为页面提交的完整活动,oldActivity 为编辑前记录。
|
||||||
|
* 唯一标识必须使用所有课程类型共有且必填的字段,不限定验证规则,不改写历史字段编码。
|
||||||
|
* 返回值为空;字段缺失、编码不一致或历史数据可能失联时抛出可定位类型的业务异常。
|
||||||
|
*/
|
||||||
|
private void validateActivityRuleConfig(FamilyActivity activity, FamilyActivity oldActivity) {
|
||||||
|
if (activity.getFamilyCountScope() == null) {
|
||||||
|
activity.setFamilyCountScope(1);
|
||||||
|
}
|
||||||
|
if (activity.getFamilyCountScope() != 1 && activity.getFamilyCountScope() != 2) {
|
||||||
|
throw new BaseException("家属人数限制范围不正确");
|
||||||
|
}
|
||||||
|
boolean activityScope = Objects.equals(activity.getFamilyCountScope(), 2);
|
||||||
|
if (activityScope && (activity.getFamilyMaxCount() == null || activity.getFamilyMaxCount() < 1)) {
|
||||||
|
throw new BaseException("按整个活动累计时,家属最多数必须为正整数");
|
||||||
|
}
|
||||||
|
boolean keyChanged = oldActivity == null || !Objects.equals(oldActivity.getOnlyKey(), activity.getOnlyKey());
|
||||||
|
if (oldActivity != null && keyChanged
|
||||||
|
&& (dao().count(FamilyUser.class, Cnd.where("activityId", "=", activity.getId())) > 0
|
||||||
|
|| dao().count(FamilyApplyPrefill.class, Cnd.where("activityId", "=", activity.getId())) > 0)) {
|
||||||
|
throw new BaseException("活动已有报名或预填信息,不能直接更换唯一标识,请先核对历史数据");
|
||||||
|
}
|
||||||
|
List<FamilyCourse> courses = activity.getCourseList();
|
||||||
|
if (Lang.isEmpty(courses)) {
|
||||||
|
if (activityScope || Boolean.TRUE.equals(activity.getTimeConflictCheck())) {
|
||||||
|
throw new BaseException("启用报名规则前请先配置子活动");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 已选唯一标识即检查所有参与类型,即使编码未更换也必须满足共有与必填要求。
|
||||||
|
boolean checkKey = activityScope || StrUtil.isNotBlank(activity.getOnlyKey());
|
||||||
|
if (activityScope && StrUtil.isBlank(activity.getOnlyKey())) {
|
||||||
|
throw new BaseException("请选择各子活动类型共有且必填的字段作为唯一标识");
|
||||||
|
}
|
||||||
|
for (FamilyCourse course : courses) {
|
||||||
|
if (checkKey) {
|
||||||
|
FamilyType type = StrUtil.isBlank(course.getCourseType()) ? null
|
||||||
|
: dao().fetch(FamilyType.class, course.getCourseType());
|
||||||
|
if (type == null) {
|
||||||
|
throw new BaseException("请为子活动配置有效类型:" + course.getCourseName());
|
||||||
|
}
|
||||||
|
List<FamilyMobileSignColumn> columns = dao().query(FamilyMobileSignColumn.class,
|
||||||
|
Cnd.where("typeId", "=", type.getId()).and("columnCode", "=", activity.getOnlyKey()));
|
||||||
|
if (columns.size() != 1 || !Boolean.TRUE.equals(columns.get(0).getIsRequired())) {
|
||||||
|
throw new BaseException("类型【" + type.getTypeName() + "】必须配置同编码且必填的唯一标识字段:"
|
||||||
|
+ activity.getOnlyKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(activity.getTimeConflictCheck())) {
|
||||||
|
if (Lang.isEmpty(course.getCourseTimeList())) {
|
||||||
|
throw new BaseException("启用时间冲突校验前请配置子活动时间:" + course.getCourseName());
|
||||||
|
}
|
||||||
|
for (FamilyActivityCourse time : course.getCourseTimeList()) {
|
||||||
|
if (time == null || time.getCourseDate() == null || time.getCourseStartTime() == null
|
||||||
|
|| time.getCourseEndTime() == null
|
||||||
|
|| DateUtil.format(time.getCourseStartTime(), "HH:mm").compareTo(
|
||||||
|
DateUtil.format(time.getCourseEndTime(), "HH:mm")) >= 0) {
|
||||||
|
throw new BaseException("子活动日期或起止时间不完整、无效:" + course.getCourseName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void add(FamilyActivity activity, FamilyCourse course) {
|
public void add(FamilyActivity activity, FamilyCourse course) {
|
||||||
|
validateActivityRuleConfig(activity, null);
|
||||||
dao().insert(activity);
|
dao().insert(activity);
|
||||||
|
|
||||||
//插入类型限制
|
//插入类型限制
|
||||||
@@ -81,7 +145,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void edit(FamilyActivity activity) {
|
public void edit(FamilyActivity activity) {
|
||||||
|
validateActivityRuleConfig(activity, dao().fetch(FamilyActivity.class, activity.getId()));
|
||||||
//修改活动
|
//修改活动
|
||||||
update(activity);
|
update(activity);
|
||||||
|
|
||||||
@@ -155,6 +219,8 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将页面的课程日期与时分合成为实际上课时间,保存到同一事务的课程时段表。 */
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
private void setCourseTimeAndInsert(FamilyCourse course) {
|
private void setCourseTimeAndInsert(FamilyCourse course) {
|
||||||
course.getCourseTimeList().forEach(courseTime -> {
|
course.getCourseTimeList().forEach(courseTime -> {
|
||||||
Calendar calendar = Calendar.getInstance();
|
Calendar calendar = Calendar.getInstance();
|
||||||
@@ -254,7 +320,13 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public FamilyUser doSignUp(FamilyUser familyUser) throws Exception {
|
public FamilyUser doSignUp(FamilyUser familyUser) throws Exception {
|
||||||
String userId = SecurityUtil.getUserId();
|
String userId = SecurityUtil.getUserId();
|
||||||
|
if (familyUser == null) {
|
||||||
|
throw new BaseException("报名信息不能为空");
|
||||||
|
}
|
||||||
|
// 先锁教职工,再锁课程;跨课程报名也必须串行校验同一活动的家属总数和时间。
|
||||||
|
lockSignUpUser(userId);
|
||||||
|
familyUser.setId(null);
|
||||||
|
familyUser.setUserId(userId);
|
||||||
// 对课程行加数据库锁,使名额校验和正式写入在多节点部署时仍按顺序执行。
|
// 对课程行加数据库锁,使名额校验和正式写入在多节点部署时仍按顺序执行。
|
||||||
lockCourseForSignUp(familyUser.getCourseId());
|
lockCourseForSignUp(familyUser.getCourseId());
|
||||||
//查询课程
|
//查询课程
|
||||||
@@ -272,6 +344,10 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId());
|
FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId());
|
||||||
validateSignUpTimeAndScope(activity, userId, false);
|
validateSignUpTimeAndScope(activity, userId, false);
|
||||||
familyUser.setActivityId(course.getActivityId());
|
familyUser.setActivityId(course.getActivityId());
|
||||||
|
if (!Boolean.TRUE.equals(course.getCourseIsLimitApply())) {
|
||||||
|
// 非独立时段课程必须参加全部时段,忽略客户端多传的单个时段ID。
|
||||||
|
familyUser.setActivityCourseId(null);
|
||||||
|
}
|
||||||
if (getSignUpInfo(course.getId(), userId) != null) {
|
if (getSignUpInfo(course.getId(), userId) != null) {
|
||||||
throw new BaseException("抱歉,您已经报名");
|
throw new BaseException("抱歉,您已经报名");
|
||||||
}
|
}
|
||||||
@@ -298,6 +374,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
: "您选择的类型已达上限,不能再报该类型的了");
|
: "您选择的类型已达上限,不能再报该类型的了");
|
||||||
}
|
}
|
||||||
validateCourseTime(course, familyUser.getActivityCourseId(), null);
|
validateCourseTime(course, familyUser.getActivityCourseId(), null);
|
||||||
|
validateTimeConflict(familyUser, course, activity, userId);
|
||||||
//如果这个课程的预留名额方式为报名人数不变
|
//如果这个课程的预留名额方式为报名人数不变
|
||||||
if (course.getReserveMode() == 2) {
|
if (course.getReserveMode() == 2) {
|
||||||
//如果当前报名+已报小于这个课程限制人数
|
//如果当前报名+已报小于这个课程限制人数
|
||||||
@@ -345,6 +422,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
throw new BaseException("取消报名参数不完整");
|
throw new BaseException("取消报名参数不完整");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lockSignUpUser(SecurityUtil.getUserId());
|
||||||
// 与正式报名共用课程行锁,保证删除、重新统计和候补转正在同一串行区间内完成。
|
// 与正式报名共用课程行锁,保证删除、重新统计和候补转正在同一串行区间内完成。
|
||||||
lockCourseForSignUp(courseId);
|
lockCourseForSignUp(courseId);
|
||||||
FamilyCourse course = dao().fetch(FamilyCourse.class, courseId);
|
FamilyCourse course = dao().fetch(FamilyCourse.class, courseId);
|
||||||
@@ -378,6 +456,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
* 按报名时间顺序从全部分工会候补中查找当前可容纳的人员。
|
* 按报名时间顺序从全部分工会候补中查找当前可容纳的人员。
|
||||||
* 本人和家属是否计入总人数,严格按课程类型配置计算。
|
* 本人和家属是否计入总人数,严格按课程类型配置计算。
|
||||||
*/
|
*/
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
private void promoteWaitingUsers(FamilyCourse course) {
|
private void promoteWaitingUsers(FamilyCourse course) {
|
||||||
FamilyType type = dao().fetch(FamilyType.class, course.getCourseType());
|
FamilyType type = dao().fetch(FamilyType.class, course.getCourseType());
|
||||||
if (type == null) {
|
if (type == null) {
|
||||||
@@ -459,6 +538,78 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
&& DateUtil.compare(now, activity.getActivitySignUpEndTime()) < 0;
|
&& DateUtil.compare(now, activity.getActivitySignUpEndTime()) < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void adjustSignUp(String activityId, String oldCourseId, String newCourseId, String userId) {
|
||||||
|
if (StrUtil.hasBlank(activityId, oldCourseId, newCourseId, userId)) {
|
||||||
|
throw new BaseException("人员调整参数不完整");
|
||||||
|
}
|
||||||
|
lockSignUpUser(userId);
|
||||||
|
// 调班涉及两门课,固定按ID排序加锁,避免不同教职工反向调班时死锁。
|
||||||
|
List.of(oldCourseId, newCourseId).stream().distinct().sorted().forEach(this::lockCourseForSignUp);
|
||||||
|
FamilyUser original = dao().fetch(FamilyUser.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("courseId", "=", oldCourseId).and("userId", "=", userId));
|
||||||
|
if (original == null || (!Objects.equals(original.getState(), 1) && !Objects.equals(original.getState(), 3))) {
|
||||||
|
throw new BaseException("只允许调整当前有效的正常报名");
|
||||||
|
}
|
||||||
|
FamilyCourse source = dao().fetch(FamilyCourse.class, oldCourseId);
|
||||||
|
FamilyCourse target = dao().fetch(FamilyCourse.class, newCourseId);
|
||||||
|
if (!Objects.equals(source.getActivityId(), activityId) || !Objects.equals(target.getActivityId(), activityId)) {
|
||||||
|
throw new BaseException("原子活动和目标子活动必须属于当前活动");
|
||||||
|
}
|
||||||
|
if (Objects.equals(oldCourseId, newCourseId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (getSignUpInfo(newCourseId, userId) != null) {
|
||||||
|
throw new BaseException("该教职工已经报名目标子活动");
|
||||||
|
}
|
||||||
|
FamilyActivity activity = dao().fetch(FamilyActivity.class, activityId);
|
||||||
|
FamilyType type = dao().fetch(FamilyType.class, target.getCourseType());
|
||||||
|
if (activity == null || type == null) {
|
||||||
|
throw new BaseException("活动或目标子活动类型不存在");
|
||||||
|
}
|
||||||
|
original.setCourseId(newCourseId);
|
||||||
|
original.setActivityCourseId(null);
|
||||||
|
if (Boolean.TRUE.equals(target.getCourseIsLimitApply())) {
|
||||||
|
List<FamilyActivityCourse> times = dao().query(FamilyActivityCourse.class, Cnd.where("courseId", "=", newCourseId));
|
||||||
|
if (times.size() != 1) {
|
||||||
|
throw new BaseException("目标子活动包含多个独立报名时段,请通过报名表单明确选择时段");
|
||||||
|
}
|
||||||
|
original.setActivityCourseId(times.get(0).getId());
|
||||||
|
}
|
||||||
|
original.setMobileColumnsValue(normalizeAndValidateFamilyColumns(original.getMobileColumnsValue(), type, target, activity));
|
||||||
|
Map<Boolean, String> result = validFamilyCount(original, userId);
|
||||||
|
if (result.containsKey(false)) {
|
||||||
|
throw new BaseException(result.get(false));
|
||||||
|
}
|
||||||
|
validateTimeConflict(original, target, activity, userId);
|
||||||
|
validateCourseTime(target, original.getActivityCourseId(), null);
|
||||||
|
int contribution = getFamilyContribution(type, original.getMobileColumnsValue().size());
|
||||||
|
if (statisticsService.queryCourseCount(newCourseId, target.getCourseType()) + contribution
|
||||||
|
+ target.getCourseReservedNumber() > target.getCoursePeopleNumber() || target.getCoursePeopleNumber() == 0) {
|
||||||
|
throw new BaseException("目标子活动名额不足");
|
||||||
|
}
|
||||||
|
original.setSignUpTime(new Date());
|
||||||
|
dao().update(original);
|
||||||
|
// 调班后的所有校验通过才删除旧时段,异常时事务回滚,保留原报名。
|
||||||
|
dao().clear(FamilyUserCourse.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("courseId", "=", oldCourseId).and("userId", "=", userId));
|
||||||
|
rebuildUserCourseTimes(original);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void deleteSignUser(String activityId, String courseId, String userId) {
|
||||||
|
if (StrUtil.hasBlank(activityId, courseId, userId)) {
|
||||||
|
throw new BaseException("删除报名参数不完整");
|
||||||
|
}
|
||||||
|
lockSignUpUser(userId);
|
||||||
|
lockCourseForSignUp(courseId);
|
||||||
|
Cnd cnd = Cnd.where("activityId", "=", activityId).and("courseId", "=", courseId).and("userId", "=", userId);
|
||||||
|
dao().clear(FamilyUserCourse.class, cnd);
|
||||||
|
dao().clear(FamilyUser.class, cnd);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FamilyApplyPrefill getPrefill(String courseId, String userId) {
|
public FamilyApplyPrefill getPrefill(String courseId, String userId) {
|
||||||
if (StrUtil.isBlank(courseId) || StrUtil.isBlank(userId)) {
|
if (StrUtil.isBlank(courseId) || StrUtil.isBlank(userId)) {
|
||||||
@@ -581,7 +732,13 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
if (currentSignUp == null) {
|
if (currentSignUp == null) {
|
||||||
throw new BaseException("未找到本人在该课程的报名信息");
|
throw new BaseException("未找到本人在该课程的报名信息");
|
||||||
}
|
}
|
||||||
validateUpdateSignUpData(currentSignUp, currentSignUp);
|
// 打开编辑表单只校验归属和报名时间,让历史冲突数据仍可进入表单整改;保存时校验新数据。
|
||||||
|
FamilyCourse course = dao().fetch(FamilyCourse.class, courseId);
|
||||||
|
if (course == null || !Objects.equals(course.getActivityId(), currentSignUp.getActivityId())) {
|
||||||
|
throw new BaseException("报名课程不存在或已变更");
|
||||||
|
}
|
||||||
|
validateSignUpTimeAndScope(dao().fetch(FamilyActivity.class, course.getActivityId()),
|
||||||
|
SecurityUtil.getUserId(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -592,11 +749,31 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
String currentUserId = SecurityUtil.getUserId();
|
String currentUserId = SecurityUtil.getUserId();
|
||||||
|
lockSignUpUser(currentUserId);
|
||||||
|
lockCourseForSignUp(familyUser.getCourseId());
|
||||||
FamilyUser currentSignUp = getSignUpInfo(familyUser.getCourseId(), currentUserId);
|
FamilyUser currentSignUp = getSignUpInfo(familyUser.getCourseId(), currentUserId);
|
||||||
if (currentSignUp == null || !Objects.equals(currentSignUp.getId(), familyUser.getId())) {
|
if (currentSignUp == null || !Objects.equals(currentSignUp.getId(), familyUser.getId())) {
|
||||||
throw new BaseException("未找到本人在该课程的报名信息");
|
throw new BaseException("未找到本人在该课程的报名信息");
|
||||||
}
|
}
|
||||||
validateUpdateSignUpData(familyUser, currentSignUp);
|
FamilyCourse course = dao().fetch(FamilyCourse.class, familyUser.getCourseId());
|
||||||
|
if (course == null || !Objects.equals(course.getActivityId(), currentSignUp.getActivityId())) {
|
||||||
|
throw new BaseException("报名课程不存在或已变更");
|
||||||
|
}
|
||||||
|
FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId());
|
||||||
|
validateSignUpTimeAndScope(activity, currentUserId, true);
|
||||||
|
FamilyType type = dao().fetch(FamilyType.class, course.getCourseType());
|
||||||
|
if (type == null) {
|
||||||
|
throw new BaseException("报名类型不存在或已变更");
|
||||||
|
}
|
||||||
|
validateUpdateSignUpData(familyUser, currentSignUp, course, activity, type);
|
||||||
|
|
||||||
|
// 正式报名和候补转正都占正式名额,按类型计人数以识别本次修改实际释放的名额。
|
||||||
|
int oldFamilyNumber = Lang.isNotEmpty(currentSignUp.getMobileColumnsValue())
|
||||||
|
? currentSignUp.getMobileColumnsValue().size() : 0;
|
||||||
|
boolean releasedFormalSlots = (Objects.equals(currentSignUp.getState(), 1)
|
||||||
|
|| Objects.equals(currentSignUp.getState(), 3))
|
||||||
|
&& getFamilyContribution(type, oldFamilyNumber)
|
||||||
|
> getFamilyContribution(type, familyUser.getMobileColumnsValue().size());
|
||||||
|
|
||||||
String oldActivityCourseId = currentSignUp.getActivityCourseId();
|
String oldActivityCourseId = currentSignUp.getActivityCourseId();
|
||||||
currentSignUp.setMobile(familyUser.getMobile());
|
currentSignUp.setMobile(familyUser.getMobile());
|
||||||
@@ -604,22 +781,20 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
currentSignUp.setActivityCourseId(familyUser.getActivityCourseId());
|
currentSignUp.setActivityCourseId(familyUser.getActivityCourseId());
|
||||||
dao().update(currentSignUp);
|
dao().update(currentSignUp);
|
||||||
syncUpdatedUserCourse(currentSignUp, oldActivityCourseId);
|
syncUpdatedUserCourse(currentSignUp, oldActivityCourseId);
|
||||||
|
// 保存减员及其时段记录后,在同一课程锁内按最新占用人数执行递补。
|
||||||
|
if (releasedFormalSlots && Objects.equals(course.getReserveMode(), 2) && isWithinSignUpTime(activity)) {
|
||||||
|
promoteWaitingUsers(course);
|
||||||
|
}
|
||||||
return currentSignUp;
|
return currentSignUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改按钮预校验与最终保存共用本方法,确保两处业务规则不会产生偏差。
|
* 保存修改前校验家属信息、报名名额与时段,人数及冲突校验须排除本人的原记录。
|
||||||
*/
|
*/
|
||||||
private void validateUpdateSignUpData(FamilyUser familyUser, FamilyUser currentSignUp) {
|
private void validateUpdateSignUpData(FamilyUser familyUser, FamilyUser currentSignUp,
|
||||||
FamilyCourse course = dao().fetch(FamilyCourse.class, familyUser.getCourseId());
|
FamilyCourse course, FamilyActivity activity, FamilyType type) {
|
||||||
if (course == null || !Objects.equals(course.getActivityId(), currentSignUp.getActivityId())) {
|
if (!Boolean.TRUE.equals(course.getCourseIsLimitApply())) {
|
||||||
throw new BaseException("报名课程不存在或已变更");
|
familyUser.setActivityCourseId(null);
|
||||||
}
|
|
||||||
FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId());
|
|
||||||
validateSignUpTimeAndScope(activity, SecurityUtil.getUserId(), true);
|
|
||||||
FamilyType type = dao().fetch(FamilyType.class, course.getCourseType());
|
|
||||||
if (type == null) {
|
|
||||||
throw new BaseException("报名类型不存在或已变更");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Lang.isEmpty(familyUser.getMobileColumnsValue())) {
|
if (Lang.isEmpty(familyUser.getMobileColumnsValue())) {
|
||||||
@@ -651,6 +826,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
: "您选择的类型已达上限,不能再报该类型的了");
|
: "您选择的类型已达上限,不能再报该类型的了");
|
||||||
}
|
}
|
||||||
validateCourseTime(course, familyUser.getActivityCourseId(), currentSignUp);
|
validateCourseTime(course, familyUser.getActivityCourseId(), currentSignUp);
|
||||||
|
validateTimeConflict(familyUser, course, activity, SecurityUtil.getUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -701,6 +877,22 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁定实际报名教职工的账号行,覆盖多节点、PC/H5和快速报名的跨课程并发。
|
||||||
|
* userId 必须由登录态或已校验的后台人员调整参数提供,所有写入口统一先锁账号再锁课程。
|
||||||
|
*/
|
||||||
|
private void lockSignUpUser(String userId) {
|
||||||
|
if (StrUtil.isBlank(userId)) {
|
||||||
|
throw new BaseException("报名人员不能为空");
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("SELECT id FROM sys_user WHERE id = @userId FOR UPDATE");
|
||||||
|
sql.setParam("userId", userId);
|
||||||
|
sql.setCallback(Sqls.callback.str());
|
||||||
|
if (dao().execute(sql).getResult() == null) {
|
||||||
|
throw new BaseException("报名人员不存在或已删除");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预填新增、修改和删除共用时间及人员范围校验。
|
* 预填新增、修改和删除共用时间及人员范围校验。
|
||||||
* 预填只允许发生在报名开始时间之前,到达开始时间后数据立即冻结。
|
* 预填只允许发生在报名开始时间之前,到达开始时间后数据立即冻结。
|
||||||
@@ -775,6 +967,14 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
for (FamilyMobileSignColumn config : columnConfigs) {
|
for (FamilyMobileSignColumn config : columnConfigs) {
|
||||||
NutMap submittedColumn = submittedValueMap.get(config.getColumnCode());
|
NutMap submittedColumn = submittedValueMap.get(config.getColumnCode());
|
||||||
Object columnValue = submittedColumn == null ? null : submittedColumn.get("columnValue");
|
Object columnValue = submittedColumn == null ? null : submittedColumn.get("columnValue");
|
||||||
|
if (Objects.equals(activity.getFamilyCountScope(), 2)
|
||||||
|
&& Objects.equals(activity.getOnlyKey(), config.getColumnCode())) {
|
||||||
|
if (!Boolean.TRUE.equals(config.getIsRequired())) {
|
||||||
|
throw new BaseException("活动累计家属人数需要同编码且必填的唯一标识字段,请联系管理员核对类型配置");
|
||||||
|
}
|
||||||
|
// 姓名等普通字段保留大小写;仅有效身份证号统一末位X,历史统计采用相同规则。
|
||||||
|
columnValue = normalizeFamilyIdentityValue(columnValue == null ? null : String.valueOf(columnValue));
|
||||||
|
}
|
||||||
validateDynamicColumnValue(config, columnValue, familyIndex, course, activity);
|
validateDynamicColumnValue(config, columnValue, familyIndex, course, activity);
|
||||||
|
|
||||||
NutMap normalizedColumn = NutMap.NEW();
|
NutMap normalizedColumn = NutMap.NEW();
|
||||||
@@ -986,13 +1186,133 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提取活动指定字段的值;活动级累计去除首尾空格,普通字段保留大小写,历史按类型规则保持原值。 */
|
||||||
|
private String getFamilyIdentity(List<NutMap> family, FamilyActivity activity) {
|
||||||
|
if (family == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String value = family.stream().filter(Objects::nonNull)
|
||||||
|
.filter(column -> Objects.equals(activity.getOnlyKey(), column.getString("columnCode")))
|
||||||
|
.map(column -> column.getString("columnValue")).filter(Objects::nonNull).findFirst().orElse(null);
|
||||||
|
return value != null && Objects.equals(activity.getFamilyCountScope(), 2)
|
||||||
|
? normalizeFamilyIdentityValue(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一本次提交与历史记录的唯一标识比较值,不要求值必须为身份证。
|
||||||
|
* value 为所选字段的文本值,返回去除首尾空格的文本;有效身份证号末位x统一为X,其他文本保持原大小写。
|
||||||
|
*/
|
||||||
|
private String normalizeFamilyIdentityValue(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String identity = value.trim();
|
||||||
|
return IdcardUtil.isValidCard(identity) ? identity.toUpperCase(Locale.ROOT) : identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一条报名实际参加的课程时段。独立报名仅取所选时段,否则取全部时段。
|
||||||
|
* course 为所属子活动,activityCourseId 为用户所选时段ID;返回完整有效的日期时间区间列表。
|
||||||
|
* 缺失、越权或无效时间不允许跳过冲突检查,避免缺配置成为绕过入口。
|
||||||
|
*/
|
||||||
|
private List<FamilyActivityCourse> getSignUpCourseTimes(FamilyCourse course, String activityCourseId) {
|
||||||
|
Cnd cnd = Cnd.where(FamilyActivityCourse::getCourseId, "=", course.getId());
|
||||||
|
if (Boolean.TRUE.equals(course.getCourseIsLimitApply())) {
|
||||||
|
if (StrUtil.isBlank(activityCourseId)) {
|
||||||
|
throw new BaseException("请选择报名时段:" + course.getCourseName());
|
||||||
|
}
|
||||||
|
cnd.and(FamilyActivityCourse::getId, "=", activityCourseId);
|
||||||
|
}
|
||||||
|
List<FamilyActivityCourse> times = dao().query(FamilyActivityCourse.class, cnd);
|
||||||
|
if (Lang.isEmpty(times)) {
|
||||||
|
throw new BaseException("子活动报名时段不存在或未配置,请联系管理员:" + course.getCourseName());
|
||||||
|
}
|
||||||
|
for (FamilyActivityCourse time : times) {
|
||||||
|
if (time.getCourseStartTime() == null || time.getCourseEndTime() == null
|
||||||
|
|| !time.getCourseStartTime().before(time.getCourseEndTime())) {
|
||||||
|
throw new BaseException("子活动起止时间配置无效,请联系管理员:" + course.getCourseName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return times;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取一条报名中的全部家属唯一标识,用于判断两次报名是否包含同一个孩子。
|
||||||
|
* user 为当前或历史报名,activity.onlyKey 为管理员配置的统一字段编码;返回规范化后的标识集合。
|
||||||
|
* 缺少字段或值为空时提示核对数据,不能将无法识别的历史报名当作不同孩子而跳过冲突校验。
|
||||||
|
*/
|
||||||
|
private Set<String> getTimeConflictFamilyIdentities(FamilyUser user, FamilyActivity activity) {
|
||||||
|
if (StrUtil.isBlank(activity.getOnlyKey())) {
|
||||||
|
throw new BaseException("活动未配置家属唯一标识,请联系管理员");
|
||||||
|
}
|
||||||
|
if (Lang.isEmpty(user.getMobileColumnsValue())) {
|
||||||
|
throw new BaseException("报名记录缺少家属信息,请联系管理员核对历史数据");
|
||||||
|
}
|
||||||
|
Set<String> identities = new HashSet<>();
|
||||||
|
for (List<NutMap> family : user.getMobileColumnsValue()) {
|
||||||
|
String identity = normalizeFamilyIdentityValue(getFamilyIdentity(family, activity));
|
||||||
|
if (StrUtil.isBlank(identity)) {
|
||||||
|
throw new BaseException("报名记录缺少家属唯一标识,请联系管理员核对字段编码和历史数据");
|
||||||
|
}
|
||||||
|
identities.add(identity);
|
||||||
|
}
|
||||||
|
return identities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在同一活动、同一教职工下仅校验同一家属的时间重叠,含候补,修改和调班排除当前报名记录。
|
||||||
|
* 参数为本次报名、所属课程、活动配置和实际报名用户ID;无返回值,同一家属冲突时提示已有课程和时间。
|
||||||
|
* 使用半开区间:一场结束时刻等于另一场开始时刻允许报名;不同日期自然不会冲突。
|
||||||
|
*/
|
||||||
|
private void validateTimeConflict(FamilyUser user, FamilyCourse course, FamilyActivity activity, String userId) {
|
||||||
|
if (!Boolean.TRUE.equals(activity.getTimeConflictCheck())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> currentIdentities = getTimeConflictFamilyIdentities(user, activity);
|
||||||
|
List<FamilyActivityCourse> currentTimes = getSignUpCourseTimes(course, user.getActivityCourseId());
|
||||||
|
Cnd cnd = Cnd.where(FamilyUser::getActivityId, "=", activity.getId())
|
||||||
|
.and(FamilyUser::getUserId, "=", userId).and(FamilyUser::getState, "in", List.of(1, 2, 3));
|
||||||
|
if (StrUtil.isNotBlank(user.getId())) {
|
||||||
|
cnd.and(FamilyUser::getId, "!=", user.getId());
|
||||||
|
}
|
||||||
|
for (FamilyUser existing : dao().query(FamilyUser.class, cnd)) {
|
||||||
|
Set<String> existingIdentities = getTimeConflictFamilyIdentities(existing, activity);
|
||||||
|
// 两次报名没有相同孩子时允许同时段报名;多人报名只要有一个相同孩子,仍需检查时间。
|
||||||
|
if (Collections.disjoint(currentIdentities, existingIdentities)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FamilyCourse oldCourse = dao().fetch(FamilyCourse.class, existing.getCourseId());
|
||||||
|
if (oldCourse == null || !Objects.equals(oldCourse.getActivityId(), activity.getId())) {
|
||||||
|
throw new BaseException("已有报名的子活动不存在或已变更,请联系管理员核对");
|
||||||
|
}
|
||||||
|
for (FamilyActivityCourse oldTime : getSignUpCourseTimes(oldCourse, existing.getActivityCourseId())) {
|
||||||
|
for (FamilyActivityCourse currentTime : currentTimes) {
|
||||||
|
if (currentTime.getCourseStartTime().before(oldTime.getCourseEndTime())
|
||||||
|
&& oldTime.getCourseStartTime().before(currentTime.getCourseEndTime())) {
|
||||||
|
throw new BaseException("同一家属已报名【" + oldCourse.getCourseName() + "】("
|
||||||
|
+ DateUtil.format(oldTime.getCourseStartTime(), "yyyy-MM-dd HH:mm") + "至"
|
||||||
|
+ DateUtil.format(oldTime.getCourseEndTime(), "yyyy-MM-dd HH:mm")
|
||||||
|
+ "),与当前报名时间重叠,不能兼报");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅在报名时段发生变化时重建本人课程时段记录,未变化时保留原签到和领取信息。
|
* 仅在报名时段发生变化时重建本人课程时段记录,未变化时保留原签到和领取信息。
|
||||||
*/
|
*/
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
private void syncUpdatedUserCourse(FamilyUser currentSignUp, String oldActivityCourseId) {
|
private void syncUpdatedUserCourse(FamilyUser currentSignUp, String oldActivityCourseId) {
|
||||||
if (Objects.equals(oldActivityCourseId, currentSignUp.getActivityCourseId())) {
|
if (Objects.equals(oldActivityCourseId, currentSignUp.getActivityCourseId())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
rebuildUserCourseTimes(currentSignUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改所选时段或后台调班后,按本次报名实际参加的时段重建课程记录。 */
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
private void rebuildUserCourseTimes(FamilyUser currentSignUp) {
|
||||||
dao().clear(FamilyUserCourse.class, Cnd.where(FamilyUserCourse::getActivityId, "=", currentSignUp.getActivityId())
|
dao().clear(FamilyUserCourse.class, Cnd.where(FamilyUserCourse::getActivityId, "=", currentSignUp.getActivityId())
|
||||||
.and(FamilyUserCourse::getCourseId, "=", currentSignUp.getCourseId())
|
.and(FamilyUserCourse::getCourseId, "=", currentSignUp.getCourseId())
|
||||||
.and(FamilyUserCourse::getUserId, "=", currentSignUp.getUserId()));
|
.and(FamilyUserCourse::getUserId, "=", currentSignUp.getUserId()));
|
||||||
@@ -1023,6 +1343,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
|
|
||||||
@Async
|
@Async
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void asyncInsertUserCourse(String activityId, String courseId, String userId) {
|
public void asyncInsertUserCourse(String activityId, String courseId, String userId) {
|
||||||
log.info("异步插入{}的上课信息,课程ID为{},活动ID为{}", userId, courseId, activityId);
|
log.info("异步插入{}的上课信息,课程ID为{},活动ID为{}", userId, courseId, activityId);
|
||||||
insertUserCourse(activityId, courseId, userId);
|
insertUserCourse(activityId, courseId, userId);
|
||||||
@@ -1031,6 +1352,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
/**
|
/**
|
||||||
* 同步写入报名人员的全部课程时段,用于保证正式报名主记录与时段记录处于同一事务。
|
* 同步写入报名人员的全部课程时段,用于保证正式报名主记录与时段记录处于同一事务。
|
||||||
*/
|
*/
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
private void insertUserCourse(String activityId, String courseId, String userId) {
|
private void insertUserCourse(String activityId, String courseId, String userId) {
|
||||||
List<FamilyActivityCourse> courseList = dao().query(FamilyActivityCourse.class, Cnd.where("courseId", "=", courseId));
|
List<FamilyActivityCourse> courseList = dao().query(FamilyActivityCourse.class, Cnd.where("courseId", "=", courseId));
|
||||||
List<FamilyUserCourse> list = new ArrayList<>();
|
List<FamilyUserCourse> list = new ArrayList<>();
|
||||||
@@ -1112,6 +1434,11 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Boolean, String> validFamilyCount(FamilyUser user) {
|
public Map<Boolean, String> validFamilyCount(FamilyUser user) {
|
||||||
|
return validFamilyCount(user, SecurityUtil.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按实际报名用户校验家属集合,后台调班不能使用当前管理员身份查询报名。 */
|
||||||
|
private Map<Boolean, String> validFamilyCount(FamilyUser user, String userId) {
|
||||||
String activityId = user.getActivityId();
|
String activityId = user.getActivityId();
|
||||||
FamilyActivity activity = dao().fetch(FamilyActivity.class, activityId);
|
FamilyActivity activity = dao().fetch(FamilyActivity.class, activityId);
|
||||||
if (activity == null) {
|
if (activity == null) {
|
||||||
@@ -1137,7 +1464,11 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
|
|
||||||
// 查询本人本活动的其他报名,修改报名时排除当前记录,后续冲突校验仍覆盖全部子活动类型。
|
// 查询本人本活动的其他报名,修改报名时排除当前记录,后续冲突校验仍覆盖全部子活动类型。
|
||||||
Cnd signUpCnd = Cnd.where(FamilyUser::getActivityId, "=", activityId)
|
Cnd signUpCnd = Cnd.where(FamilyUser::getActivityId, "=", activityId)
|
||||||
.and(FamilyUser::getUserId, "=", SecurityUtil.getUserId());
|
.and(FamilyUser::getUserId, "=", userId);
|
||||||
|
if (Objects.equals(activity.getFamilyCountScope(), 2) || Boolean.TRUE.equals(activity.getTimeConflictCheck())) {
|
||||||
|
// 正常、候补及候补转正占用规则;废弃报名不占用新规则的家属名额。
|
||||||
|
signUpCnd.and(FamilyUser::getState, "in", List.of(1, 2, 3));
|
||||||
|
}
|
||||||
if (StrUtil.isNotBlank(user.getId())) {
|
if (StrUtil.isNotBlank(user.getId())) {
|
||||||
signUpCnd.and(FamilyUser::getId, "!=", user.getId());
|
signUpCnd.and(FamilyUser::getId, "!=", user.getId());
|
||||||
}
|
}
|
||||||
@@ -1146,6 +1477,31 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
Map<String, String> courseMap = courseList.stream()
|
Map<String, String> courseMap = courseList.stream()
|
||||||
.collect(Collectors.toMap(FamilyCourse::getId, FamilyCourse::getCourseType));
|
.collect(Collectors.toMap(FamilyCourse::getId, FamilyCourse::getCourseType));
|
||||||
|
|
||||||
|
if (Objects.equals(activity.getFamilyCountScope(), 2)) {
|
||||||
|
if (activity.getFamilyMaxCount() == null || activity.getFamilyMaxCount() < 1) {
|
||||||
|
return Map.of(false, "活动未配置有效的家属最多数,请联系管理员");
|
||||||
|
}
|
||||||
|
Set<String> identities = new HashSet<>();
|
||||||
|
List<FamilyUser> allSignUps = new ArrayList<>(list);
|
||||||
|
allSignUps.add(user);
|
||||||
|
for (FamilyUser signUp : allSignUps) {
|
||||||
|
if (Lang.isEmpty(signUp.getMobileColumnsValue())) {
|
||||||
|
return Map.of(false, "报名记录缺少家属信息,请联系管理员核对历史数据");
|
||||||
|
}
|
||||||
|
for (List<NutMap> family : signUp.getMobileColumnsValue()) {
|
||||||
|
String identity = getFamilyIdentity(family, activity);
|
||||||
|
if (StrUtil.isBlank(identity)) {
|
||||||
|
return Map.of(false, "报名记录缺少统一的家属唯一标识,请联系管理员核对字段编码和历史数据");
|
||||||
|
}
|
||||||
|
identities.add(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (identities.size() > activity.getFamilyMaxCount()) {
|
||||||
|
return Map.of(false, "本活动累计" + activity.getKeyWord() + "人数最多为" + activity.getFamilyMaxCount()
|
||||||
|
+ ",请使用已报名的家属,不能更换为其他人");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 家属最多数按当前子活动类型独立累计,同类型多个子活动按家属唯一标识去重。
|
// 家属最多数按当前子活动类型独立累计,同类型多个子活动按家属唯一标识去重。
|
||||||
List<List<NutMap>> allMobileColumnsValue = new ArrayList<>(list.stream()
|
List<List<NutMap>> allMobileColumnsValue = new ArrayList<>(list.stream()
|
||||||
.filter(item -> Objects.equals(currentCourse.getCourseType(), courseMap.get(item.getCourseId())))
|
.filter(item -> Objects.equals(currentCourse.getCourseType(), courseMap.get(item.getCourseId())))
|
||||||
@@ -1157,11 +1513,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
allMobileColumnsValue.addAll(user.getMobileColumnsValue());
|
allMobileColumnsValue.addAll(user.getMobileColumnsValue());
|
||||||
|
|
||||||
long count = allMobileColumnsValue.stream()
|
long count = allMobileColumnsValue.stream()
|
||||||
.filter(Objects::nonNull)
|
.map(family -> getFamilyIdentity(family, activity))
|
||||||
.flatMap(List::stream)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.filter(nutMap -> Objects.equals(activity.getOnlyKey(), nutMap.getString("columnCode")))
|
|
||||||
.map(nutMap -> nutMap.getString("columnValue"))
|
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.distinct()
|
.distinct()
|
||||||
.count();
|
.count();
|
||||||
@@ -1170,8 +1522,9 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
return Map.of(false, activity.getKeyWord() + "人数最多为" + currentType.getFamilyMaxCount());
|
return Map.of(false, activity.getKeyWord() + "人数最多为" + currentType.getFamilyMaxCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FamilyType> typeList = dao().query(FamilyType.class, Cnd.NEW());
|
// 冲突提示显示历史报名对应的具体子活动名称,不使用类型名称或本次尝试报名的名称。
|
||||||
Map<String, String> typeMap = typeList.stream().collect(Collectors.toMap(FamilyType::getId, FamilyType::getTypeName));
|
Map<String, String> courseNameMap = courseList.stream().collect(Collectors.toMap(FamilyCourse::getId,
|
||||||
|
course -> StrUtil.isBlank(course.getCourseName()) ? "原子活动" : course.getCourseName()));
|
||||||
|
|
||||||
List<List<NutMap>> currentValue = user.getMobileColumnsValue();
|
List<List<NutMap>> currentValue = user.getMobileColumnsValue();
|
||||||
|
|
||||||
@@ -1190,7 +1543,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 家属匹配唯一标识的值
|
// 家属匹配唯一标识的值
|
||||||
String onlyKeyValue = nutMap.getString("columnValue");
|
String onlyKeyValue = getFamilyIdentity(listMap, activity);
|
||||||
if(Objects.equals(tempValue, onlyKeyValue)) {
|
if(Objects.equals(tempValue, onlyKeyValue)) {
|
||||||
return Map.of(false, activity.getKeyWord() + "不能重复报同一分场");
|
return Map.of(false, activity.getKeyWord() + "不能重复报同一分场");
|
||||||
}
|
}
|
||||||
@@ -1212,18 +1565,21 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
|||||||
if(hisNutMap == null) {
|
if(hisNutMap == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String hisKeyValue = hisNutMap.getString("columnValue");
|
String hisKeyValue = getFamilyIdentity(hisValue, activity);
|
||||||
// 判断是不是同一个孩子
|
// 判断是不是同一个孩子
|
||||||
if(Objects.equals(onlyKeyValue, hisKeyValue)) {
|
if(Objects.equals(onlyKeyValue, hisKeyValue)) {
|
||||||
oneFamilyCount ++;
|
oneFamilyCount ++;
|
||||||
if(Objects.equals(courseMap.get(user.getCourseId()), courseMap.get(hisRecord.getCourseId()))) {
|
if(Objects.equals(courseMap.get(user.getCourseId()), courseMap.get(hisRecord.getCourseId()))) {
|
||||||
return Map.of(false, onlyKeyValue + "已经报名" + typeMap.get(courseMap.get(user.getCourseId())) + "分场");
|
return Map.of(false, onlyKeyValue + "已经报名“" + courseNameMap.getOrDefault(hisRecord.getCourseId(), "原子活动")
|
||||||
|
+ "”,不能重复报名同时间段");
|
||||||
}
|
}
|
||||||
if(aa.contains(user.getCourseId()) && bb.contains(hisRecord.getCourseId())) {
|
if(aa.contains(user.getCourseId()) && bb.contains(hisRecord.getCourseId())) {
|
||||||
return Map.of(false, onlyKeyValue + "已经报名" + typeMap.get(courseMap.get(hisRecord.getCourseId())) + "分场,存在冲突");
|
return Map.of(false, onlyKeyValue + "已经报名“" + courseNameMap.getOrDefault(hisRecord.getCourseId(), "原子活动")
|
||||||
|
+ "”,与当前子活动存在冲突");
|
||||||
}
|
}
|
||||||
if(bb.contains(user.getCourseId()) && aa.contains(hisRecord.getCourseId())) {
|
if(bb.contains(user.getCourseId()) && aa.contains(hisRecord.getCourseId())) {
|
||||||
return Map.of(false, onlyKeyValue + "已经报名" + typeMap.get(courseMap.get(hisRecord.getCourseId())) + "分场,存在冲突");
|
return Map.of(false, onlyKeyValue + "已经报名“" + courseNameMap.getOrDefault(hisRecord.getCourseId(), "原子活动")
|
||||||
|
+ "”,与当前子活动存在冲突");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -12,6 +12,7 @@ import org.nutz.dao.sql.Criteria;
|
|||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
|
import org.nutz.lang.Strings;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -51,9 +52,12 @@ public class TrainSignUpUserServiceImpl extends BaseServiceImpl<TrainSignUpBlack
|
|||||||
LEFT JOIN train_sign_up_course tsuc ON tsuc.id = tsuu.courseId
|
LEFT JOIN train_sign_up_course tsuc ON tsuc.id = tsuu.courseId
|
||||||
LEFT JOIN train_sign_up_black_list tsubl on tsubl.userId = tsuu.userId
|
LEFT JOIN train_sign_up_black_list tsubl on tsubl.userId = tsuu.userId
|
||||||
$condition
|
$condition
|
||||||
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 )
|
|
||||||
""");
|
""");
|
||||||
cnd.groupBy("tsuu.userId");
|
cnd.groupBy("tsuu.userId");
|
||||||
|
// 默认按工号正序,工号相同时用用户 ID 保持分页稳定;手动列排序沿用传入条件。
|
||||||
|
if (Strings.isBlank(pageForm.getPageOrderName()) || Strings.isBlank(pageForm.getPageOrderBy())) {
|
||||||
|
cnd.asc("u.loginname").asc("tsuu.userId");
|
||||||
|
}
|
||||||
Criteria varCnd = Cnd.cri();
|
Criteria varCnd = Cnd.cri();
|
||||||
varCnd.where().setTop(false);
|
varCnd.where().setTop(false);
|
||||||
varCnd.where().andEX("activityId", "=", activityId);
|
varCnd.where().andEX("activityId", "=", activityId);
|
||||||
|
|||||||
+19
@@ -6,6 +6,7 @@ import com.budwk.app.base.result.Result;
|
|||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.zhgh.welfare.model.WelfareMemberAddress;
|
import com.budwk.app.zhgh.welfare.model.WelfareMemberAddress;
|
||||||
|
import com.budwk.app.zhgh.welfare.service.WelfareAddressManageService;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Chain;
|
import org.nutz.dao.Chain;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
@@ -18,6 +19,8 @@ import org.nutz.mvc.annotation.At;
|
|||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName WelfareAddressManageController
|
* @ClassName WelfareAddressManageController
|
||||||
* @Description 福利地址管理
|
* @Description 福利地址管理
|
||||||
@@ -31,12 +34,28 @@ public class WelfareAddressManageController {
|
|||||||
@Inject
|
@Inject
|
||||||
private BaseService baseService;
|
private BaseService baseService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private WelfareAddressManageService welfareAddressManageService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/zhgh/welfare/addressManage/index.html")
|
@Ok("beetl:/platform/zhgh/welfare/addressManage/index.html")
|
||||||
@SaCheckPermission("welfare.addressManage")
|
@SaCheckPermission("welfare.addressManage")
|
||||||
public void index() {
|
public void index() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出全部用户的收货地址,不接收用户或分页筛选参数。
|
||||||
|
* 无返回对象,文件内容直接写入响应。
|
||||||
|
*
|
||||||
|
* @param response 文件下载响应,输出包含工号、姓名、收货地址三列的 XLSX 文件
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SaCheckPermission("welfare.addressManage")
|
||||||
|
public void exportAllAddress(HttpServletResponse response) {
|
||||||
|
welfareAddressManageService.exportAllAddress(response);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 地址列表
|
* 地址列表
|
||||||
*
|
*
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.budwk.app.zhgh.welfare.h5Controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端福利选择情况页面入口;查询和代选沿用现有福利接口。
|
||||||
|
*/
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/h5/welfare/selection/situation")
|
||||||
|
public class H5WelfareSelectionSituationController {
|
||||||
|
|
||||||
|
@At("")
|
||||||
|
@Ok("beetl:/platform/zhghh5/welfare/selectionSituation/index.html")
|
||||||
|
@SaCheckPermission("h5.welfare.selection.situation")
|
||||||
|
public void index() {
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
package com.budwk.app.zhgh.welfare.h5Controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.param.PageForm;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
|
||||||
|
import com.budwk.app.zhgh.welfare.service.WelfareStatisticsService;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
|
||||||
|
/** 移动端福利统计只读入口;统计和人员查询业务统一由现有 service 承担。 */
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/h5/welfare/statistics")
|
||||||
|
@Ok("json:full")
|
||||||
|
public class H5WelfareStatisticsController {
|
||||||
|
@Inject
|
||||||
|
private WelfareStatisticsService welfareStatisticsService;
|
||||||
|
@Inject
|
||||||
|
private WelfareProjectService welfareProjectService;
|
||||||
|
|
||||||
|
@At("")
|
||||||
|
@Ok("beetl:/platform/zhghh5/welfare/statistics/index.html")
|
||||||
|
@SaCheckPermission("h5.welfare.statistics")
|
||||||
|
public void index() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param year 可选年份,省略时返回全部年份的项目,供移动端联动筛选
|
||||||
|
* @return Result.data 为项目数组,使用 id、name、year 生成筛选项
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("h5.welfare.statistics")
|
||||||
|
public Result projects(Integer year) {
|
||||||
|
return Result.success(welfareProjectService.getWelfareList(year));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param projectId 必填福利项目 ID
|
||||||
|
* @param unionId 可选工会 ID,空表示全部工会
|
||||||
|
* @param isSelectByAdmin null 全部方式,true 管理员代选,false 个人选择
|
||||||
|
* @return Result.data 含 tableList 工会人数统计、tableColumn 固定列和动态福利选项列
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("h5.welfare.statistics")
|
||||||
|
public Result pageData(String projectId, String unionId, Boolean isSelectByAdmin) {
|
||||||
|
if (StrUtil.isBlank(projectId)) return Result.error("请选择福利项目");
|
||||||
|
return Result.success(welfareStatisticsService.mobilePageData(projectId, unionId, isSelectByAdmin));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pageForm pageNumber 从 1 开始,pageSize 每页条数,searchKeyword 为工号或姓名
|
||||||
|
* @param projectId 必填福利项目 ID
|
||||||
|
* @param unionId 必填工会 ID
|
||||||
|
* @param isSelectByAdmin null 全部方式,true 管理员代选,false 个人选择
|
||||||
|
* @return Result.data 为 Pagination,list 含人员、单位、所选福利及选择方式,totalCount 为匹配人数
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("h5.welfare.statistics")
|
||||||
|
public Result selectedUnionUserPageData(@Valid PageForm pageForm, String projectId, String unionId, Boolean isSelectByAdmin) {
|
||||||
|
if (StrUtil.isBlank(projectId) || StrUtil.isBlank(unionId)) return Result.error("请选择福利项目和工会");
|
||||||
|
return Result.success(welfareStatisticsService.selectedUnionUserPageData(pageForm, projectId, unionId, isSelectByAdmin));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pageForm pageNumber 从 1 开始,pageSize 每页条数,searchKeyword 为工号或姓名
|
||||||
|
* @param projectId 必填福利项目 ID
|
||||||
|
* @param unionId 必填工会 ID;未选人员不受个人选择/管理员代选条件影响
|
||||||
|
* @return Result.data 为 Pagination,list 含未选人员的工号、姓名、单位,totalCount 为匹配人数
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("h5.welfare.statistics")
|
||||||
|
public Result unSelectedUnionUserPageData(@Valid PageForm pageForm, String projectId, String unionId) {
|
||||||
|
if (StrUtil.isBlank(projectId) || StrUtil.isBlank(unionId)) return Result.error("请选择福利项目和工会");
|
||||||
|
return Result.success(welfareStatisticsService.unSelectedUnionUserPageData(pageForm, projectId, unionId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.budwk.app.zhgh.welfare.service;
|
||||||
|
|
||||||
|
import com.budwk.app.base.service.BaseService;
|
||||||
|
import com.budwk.app.zhgh.welfare.model.WelfareMemberAddress;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 福利收货地址管理服务。
|
||||||
|
*/
|
||||||
|
public interface WelfareAddressManageService extends BaseService<WelfareMemberAddress> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅导出有收货地址的人员,每人一行,多地址优先取默认地址。
|
||||||
|
* 无返回对象,文件内容直接写入响应。
|
||||||
|
*
|
||||||
|
* @param response 文件下载响应,输出工号、姓名、收货地址三列的 XLSX 文件
|
||||||
|
*/
|
||||||
|
void exportAllAddress(HttpServletResponse response);
|
||||||
|
}
|
||||||
@@ -18,6 +18,16 @@ public interface WelfareStatisticsService extends BaseService<WelfareProject> {
|
|||||||
*/
|
*/
|
||||||
NutMap pageData(String projectId, String unionId, Boolean isSelectByAdmin);
|
NutMap pageData(String projectId, String unionId, Boolean isSelectByAdmin);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端工会统计;方式筛选只影响已选人数和各福利选项人数。
|
||||||
|
* @param projectId 必填福利项目 ID
|
||||||
|
* @param unionId 工会 ID,空值表示全部工会
|
||||||
|
* @param isSelectByAdmin null 表示全部方式,true 表示管理员代选,false 表示个人选择
|
||||||
|
* @return tableList 为工会统计行(teacherSum 总人数、selectedNum 已选、unSelectedNum 真正未选);
|
||||||
|
* tableColumn 为列描述(label 名称、prop 字段),其中包含动态福利选项人数列
|
||||||
|
*/
|
||||||
|
NutMap mobilePageData(String projectId, String unionId, Boolean isSelectByAdmin);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询某分工会已选择人员
|
* 查询某分工会已选择人员
|
||||||
* @param pageForm
|
* @param pageForm
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package com.budwk.app.zhgh.welfare.service.impl;
|
||||||
|
|
||||||
|
import cn.afterturn.easypoi.excel.ExcelExportUtil;
|
||||||
|
import cn.afterturn.easypoi.excel.entity.ExportParams;
|
||||||
|
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
|
||||||
|
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
|
||||||
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
|
import com.budwk.app.zhgh.welfare.model.WelfareMemberAddress;
|
||||||
|
import com.budwk.app.zhgh.welfare.service.WelfareAddressManageService;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总用户与福利地址信息,生成每人一行的收货地址表。
|
||||||
|
*/
|
||||||
|
@IocBean(args = {"refer:dao"})
|
||||||
|
public class WelfareAddressManageServiceImpl extends BaseServiceImpl<WelfareMemberAddress> implements WelfareAddressManageService {
|
||||||
|
|
||||||
|
public WelfareAddressManageServiceImpl(Dao dao) {
|
||||||
|
super(dao);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出有收货地址人员的工号、姓名和完整收货地址,按工号排序。
|
||||||
|
* 无返回对象,XLSX 文件直接写入响应。
|
||||||
|
*
|
||||||
|
* @param response 文件下载响应,输出工号、姓名、收货地址三列的 XLSX 文件
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void exportAllAddress(HttpServletResponse response) {
|
||||||
|
// 沿用福利地址导出的排序规则:默认地址优先;未设默认或多个默认时按 ID 稳定选取一条。
|
||||||
|
// 仅关联存在地址记录的人员;逐段处理空值,避免部分地区字段为空导致整条地址丢失。
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
u.loginname,
|
||||||
|
u.username,
|
||||||
|
CONCAT('收货人:', IFNULL(a.userName, ''), ' 电话:', IFNULL(a.tel, ''),
|
||||||
|
'详细地址:', IFNULL(a.province, ''), IFNULL(a.city, ''),
|
||||||
|
IFNULL(a.county, ''), IFNULL(a.addressDetail, '')) AS address
|
||||||
|
FROM sys_user u
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT userId, userName, tel, province, city, county, addressDetail,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY userId ORDER BY isDefault DESC, id) AS rn
|
||||||
|
FROM welfare_member_address
|
||||||
|
) a ON a.userId = u.id AND a.rn = 1
|
||||||
|
ORDER BY u.loginname, u.id
|
||||||
|
""");
|
||||||
|
List<NutMap> list = listMap(sql);
|
||||||
|
|
||||||
|
// 工号按字符串导出,保留前导零;收货地址合并所选地址的收货人、电话及省市区县和详细地址。
|
||||||
|
List<ExcelExportEntity> entities = new ArrayList<>();
|
||||||
|
entities.add(new ExcelExportEntity("工号", "loginname", 20));
|
||||||
|
entities.add(new ExcelExportEntity("姓名", "username", 20));
|
||||||
|
entities.add(new ExcelExportEntity("收货地址", "address", 70));
|
||||||
|
|
||||||
|
ExportParams exportParams = new ExportParams();
|
||||||
|
exportParams.setType(ExcelType.XSSF);
|
||||||
|
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, list);
|
||||||
|
// 全局下载方法负责写入响应并关闭工作簿。
|
||||||
|
CommonDownloadUtil.download("所有人收货地址.xlsx", workbook, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -415,6 +415,8 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
|
|||||||
cnd.andEX("t1.preparedBy", "in", pageForm.getPreparedBys());
|
cnd.andEX("t1.preparedBy", "in", pageForm.getPreparedBys());
|
||||||
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
|
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
|
||||||
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
||||||
|
// 默认先按工号正序分页,工号相同时沿用工会和单位顺序。
|
||||||
|
cnd.asc("t2.loginname");
|
||||||
cnd.asc("t1.welfareUnionName");
|
cnd.asc("t1.welfareUnionName");
|
||||||
cnd.asc("t1.welfareUnitName");
|
cnd.asc("t1.welfareUnitName");
|
||||||
} else {
|
} else {
|
||||||
@@ -644,32 +646,52 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
|
|||||||
LEFT JOIN sys_user t2 ON t2.id = t1.userId
|
LEFT JOIN sys_user t2 ON t2.id = t1.userId
|
||||||
$condition
|
$condition
|
||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
// 只统计当前福利项目名单中的身份证号;同号名单记录至少两条才属于重复组。
|
||||||
cnd.and("t1.projectId", "=", pageForm.getProjectId());
|
Sql repeatIdCardSql = Sqls.create("""
|
||||||
cnd.and("t2.idCard", "in", Sqls.create("select idCard from sys_user where idCard is not null and idCard != '' group by idCard having count(1) > 1"));
|
select u.idCard from welfare_list w
|
||||||
|
inner join sys_user u on u.id = w.userId
|
||||||
|
where w.projectId = @projectId and u.idCard is not null and u.idCard != ''
|
||||||
|
group by u.idCard having count(1) > 1
|
||||||
|
""").setParam("projectId", pageForm.getProjectId());
|
||||||
|
|
||||||
|
// 页面筛选只决定导出哪些重复组,匹配到的身份证号用于补齐同组全部记录。
|
||||||
|
Sql matchedIdCardSql = Sqls.create("""
|
||||||
|
select distinct t2.idCard from welfare_list t1
|
||||||
|
inner join sys_user t2 on t2.id = t1.userId
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
Cnd matchedCnd = Cnd.NEW();
|
||||||
|
matchedCnd.and("t1.projectId", "=", pageForm.getProjectId());
|
||||||
|
matchedCnd.and("t2.idCard", "in", repeatIdCardSql);
|
||||||
if (StrUtil.isNotBlank(pageForm.getUserName())) {
|
if (StrUtil.isNotBlank(pageForm.getUserName())) {
|
||||||
cnd.where().andLike("t2.username", pageForm.getUserName());
|
matchedCnd.where().andLike("t2.username", pageForm.getUserName());
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(pageForm.getLoginName())) {
|
if (StrUtil.isNotBlank(pageForm.getLoginName())) {
|
||||||
cnd.where().andLike("t2.loginname", pageForm.getLoginName());
|
matchedCnd.where().andLike("t2.loginname", pageForm.getLoginName());
|
||||||
}
|
}
|
||||||
cnd.andEX("t2.sex", "=", pageForm.getSex());
|
matchedCnd.andEX("t2.sex", "=", pageForm.getSex());
|
||||||
cnd.andEX("t2.birthday", "=", pageForm.getBirthday());
|
matchedCnd.andEX("t2.birthday", "=", pageForm.getBirthday());
|
||||||
if (pageForm.getBirthMonths() != null && pageForm.getBirthMonths().length > 0) {
|
if (pageForm.getBirthMonths() != null && pageForm.getBirthMonths().length > 0) {
|
||||||
cnd.andEX("MONTH(t2.birthday)", "in", pageForm.getBirthMonths());
|
matchedCnd.andEX("MONTH(t2.birthday)", "in", pageForm.getBirthMonths());
|
||||||
}
|
}
|
||||||
cnd.andEX("t1.welfareUnitId", "in", pageForm.getUnitIds());
|
matchedCnd.andEX("t1.welfareUnitId", "in", pageForm.getUnitIds());
|
||||||
cnd.andEX("t1.welfareUnionId", "=", pageForm.getUnionId());
|
matchedCnd.andEX("t1.welfareUnionId", "=", pageForm.getUnionId());
|
||||||
cnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
|
matchedCnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
|
||||||
cnd.andEX("t1.preparedBy", "in", pageForm.getPreparedBys());
|
matchedCnd.andEX("t1.preparedBy", "in", pageForm.getPreparedBys());
|
||||||
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
|
matchedCnd.andEX("t1.userState", "in", pageForm.getUserStates());
|
||||||
|
matchedIdCardSql.setCondition(matchedCnd);
|
||||||
|
List<NutMap> matchedIdCards = listMap(matchedIdCardSql);
|
||||||
|
List<NutMap> list = new ArrayList<>();
|
||||||
|
if (!matchedIdCards.isEmpty()) {
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
cnd.and("t1.projectId", "=", pageForm.getProjectId());
|
||||||
|
cnd.and("t2.idCard", "in", matchedIdCards.stream().map(item -> item.getString("idCard")).toArray(String[]::new));
|
||||||
cnd.asc("t2.idCard");
|
cnd.asc("t2.idCard");
|
||||||
cnd.asc("t1.welfareUnionName");
|
cnd.asc("t2.loginname");
|
||||||
cnd.asc("t1.welfareUnitName");
|
cnd.asc("t1.id");
|
||||||
|
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
List<NutMap> list = listMap(sql);
|
list = listMap(sql);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<ExcelExportEntity> exportEntities = new ArrayList<>();
|
List<ExcelExportEntity> exportEntities = new ArrayList<>();
|
||||||
|
|||||||
+2
@@ -102,6 +102,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
||||||
|
// 默认先按工号正序分页,工号相同时沿用工会和单位顺序。
|
||||||
|
cnd.asc("t4.loginname");
|
||||||
cnd.asc("welfareUnionName");
|
cnd.asc("welfareUnionName");
|
||||||
cnd.asc("welfareUnitName");
|
cnd.asc("welfareUnitName");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+20
@@ -138,6 +138,26 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
|
|||||||
return NutMap.NEW().addv("tableList", mapUnions).addv("tableColumn", dynamicTableColumns);
|
return NutMap.NEW().addv("tableList", mapUnions).addv("tableColumn", dynamicTableColumns);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 沿用现有统计查询;筛选选择方式时,以不限方式的未选人数匹配未选人员明细口径。
|
||||||
|
* 例如已经个人选择的人,在筛选管理员代选时不会被计为未选择。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public NutMap mobilePageData(String projectId, String unionId, Boolean isSelectByAdmin) {
|
||||||
|
NutMap result = pageData(projectId, unionId, isSelectByAdmin);
|
||||||
|
if (isSelectByAdmin != null) {
|
||||||
|
NutMap allMethods = pageData(projectId, unionId, null);
|
||||||
|
Map<String, Long> unSelectedByUnion = new HashMap<>();
|
||||||
|
for (NutMap row : allMethods.getList("tableList", NutMap.class)) {
|
||||||
|
unSelectedByUnion.put(row.getString("id"), row.getLong("unSelectedNum"));
|
||||||
|
}
|
||||||
|
for (NutMap row : result.getList("tableList", NutMap.class)) {
|
||||||
|
row.put("unSelectedNum", unSelectedByUnion.getOrDefault(row.getString("id"), 0L));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pagination selectedUnionUserPageData(PageForm pageForm, String projectId, String unionId, Boolean isSelectByAdmin) {
|
public Pagination selectedUnionUserPageData(PageForm pageForm, String projectId, String unionId, Boolean isSelectByAdmin) {
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ const h5PopupHistory = (() => {
|
|||||||
popup.closeFromHistory()
|
popup.closeFromHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("popstate", handlePopState)
|
// 捕获阶段优先消费弹框返回事件,避免 PJAX 重载页面并重置筛选条件。
|
||||||
|
window.addEventListener("popstate", handlePopState, true)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
@@ -37,11 +38,13 @@ const h5PopupHistory = (() => {
|
|||||||
/**
|
/**
|
||||||
* 通过取消、关闭按钮关闭指定弹框时,同步回退对应历史记录。
|
* 通过取消、关闭按钮关闭指定弹框时,同步回退对应历史记录。
|
||||||
* @param id 弹框唯一标识,必须是当前最上层弹框
|
* @param id 弹框唯一标识,必须是当前最上层弹框
|
||||||
* @return boolean;true 表示已发起历史回退,false 表示弹框不在栈顶
|
* @return boolean;true 表示已发起历史回退,false 表示弹框不在栈顶或正在关闭
|
||||||
*/
|
*/
|
||||||
close(id) {
|
close(id) {
|
||||||
const top = popupStack[popupStack.length - 1]
|
const top = popupStack[popupStack.length - 1]
|
||||||
if (!top || top.id !== id) return false
|
if (!top || top.id !== id || top.closing) return false
|
||||||
|
// history.back 异步触发 popstate;等待期间重复点击或关闭回调不能再回退一层。
|
||||||
|
top.closing = true
|
||||||
window.history.back()
|
window.history.back()
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
.field .van-field__error-message {
|
.field .van-field__error-message {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 与测量节点共用排版,确保滚轮每一行都能容纳完整选项。 */
|
||||||
|
.train-picker-option-text {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 22px;
|
||||||
|
text-align: center;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
@@ -89,8 +100,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<van-picker
|
<van-picker
|
||||||
|
:key="column.columnCode + '-' + (pickerItemHeights[column.columnCode] || 44)"
|
||||||
:columns="column.selectValues"
|
:columns="column.selectValues"
|
||||||
:title="column.columnName"
|
:title="column.columnName"
|
||||||
|
:item-height="pickerItemHeights[column.columnCode] || 44"
|
||||||
|
:visible-item-count="pickerVisibleItemCounts[column.columnCode] || 6"
|
||||||
show-toolbar
|
show-toolbar
|
||||||
@cancel="ordinaryPickerCancel(column)"
|
@cancel="ordinaryPickerCancel(column)"
|
||||||
@confirm="
|
@confirm="
|
||||||
@@ -98,7 +112,11 @@
|
|||||||
ordinaryPickerConfirm(value, column)
|
ordinaryPickerConfirm(value, column)
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
>
|
||||||
|
<template v-if="fullOptionText" #option="option">
|
||||||
|
<div class="train-picker-option-text">{{ option }}</div>
|
||||||
|
</template>
|
||||||
|
</van-picker>
|
||||||
</template>
|
</template>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
</template>
|
</template>
|
||||||
@@ -140,7 +158,12 @@ module.exports = {
|
|||||||
name: "TrainDynamicForm",
|
name: "TrainDynamicForm",
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
dynamicData: []
|
dynamicData: [],
|
||||||
|
// 由亲子活动报名页单独启用,其余引用页面保留原有选项展示。
|
||||||
|
fullOptionText: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
prop: "dynamicData",
|
prop: "dynamicData",
|
||||||
@@ -160,6 +183,8 @@ module.exports = {
|
|||||||
return {
|
return {
|
||||||
//所有的picker状态
|
//所有的picker状态
|
||||||
pickerStates: {},
|
pickerStates: {},
|
||||||
|
pickerItemHeights: {},
|
||||||
|
pickerVisibleItemCounts: {},
|
||||||
fileList: [],
|
fileList: [],
|
||||||
addressStates: {},
|
addressStates: {},
|
||||||
addressAreaList: window.trainSignUpAreaList || { province_list: {}, city_list: {}, county_list: {} }
|
addressAreaList: window.trainSignUpAreaList || { province_list: {}, city_list: {}, county_list: {} }
|
||||||
@@ -444,7 +469,44 @@ module.exports = {
|
|||||||
const addressValue = (state.displayArea || "") + (state.detail || "")
|
const addressValue = (state.displayArea || "") + (state.detail || "")
|
||||||
this.$set(column, "columnValue", addressValue)
|
this.$set(column, "columnValue", addressValue)
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 按当前屏幕宽度测量最长选项,统一行高以保持滚轮定位准确。
|
||||||
|
* @param column 动态字段配置,selectValues 为后台配置的字符串选项数组
|
||||||
|
* @return 无返回值;将像素行高和屏内可见行数写入对应字段的展示状态
|
||||||
|
*/
|
||||||
|
measurePickerOptions(column) {
|
||||||
|
if (!this.fullOptionText || column.columnFormType !== "SELECT"
|
||||||
|
|| ["DATE", "DATETIME"].includes(column.columnType)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const measureElement = document.createElement("div")
|
||||||
|
measureElement.className = "train-picker-option-text"
|
||||||
|
// Vant 选项左右各有 4px 内边距,测量宽度必须与实际文字区域一致。
|
||||||
|
Object.assign(measureElement.style, {
|
||||||
|
position: "fixed",
|
||||||
|
visibility: "hidden",
|
||||||
|
pointerEvents: "none",
|
||||||
|
width: Math.max(document.documentElement.clientWidth - 8, 1) + "px",
|
||||||
|
fontFamily: window.getComputedStyle(this.$el).fontFamily
|
||||||
|
})
|
||||||
|
document.body.appendChild(measureElement)
|
||||||
|
let itemHeight = 44
|
||||||
|
try {
|
||||||
|
(column.selectValues || []).forEach((option) => {
|
||||||
|
measureElement.textContent = option
|
||||||
|
itemHeight = Math.max(itemHeight, Math.ceil(measureElement.getBoundingClientRect().height) + 16)
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(measureElement)
|
||||||
|
}
|
||||||
|
this.$set(this.pickerItemHeights, column.columnCode, itemHeight)
|
||||||
|
// 多行选项减少同时展示的行数,避免滚轮随行高增加而撑出屏幕。
|
||||||
|
const availableHeight = Math.min(264, Math.max(window.innerHeight - 44, 0))
|
||||||
|
this.$set(this.pickerVisibleItemCounts, column.columnCode,
|
||||||
|
Math.max(1, Math.floor(availableHeight / itemHeight)))
|
||||||
|
},
|
||||||
selectFieldClick(column) {
|
selectFieldClick(column) {
|
||||||
|
this.measurePickerOptions(column)
|
||||||
this.$set(this.pickerStates, column.columnCode + "Picker", true)
|
this.$set(this.pickerStates, column.columnCode + "Picker", true)
|
||||||
if (!["DATE", "DATETIME"].includes(column.columnType)) {
|
if (!["DATE", "DATETIME"].includes(column.columnType)) {
|
||||||
//说明是普通的下拉框
|
//说明是普通的下拉框
|
||||||
|
|||||||
@@ -65,8 +65,8 @@
|
|||||||
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/style.min.css" />-->
|
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/style.min.css" />-->
|
||||||
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-table@3.9.0/lib/style.min.css" />-->
|
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-table@3.9.0/lib/style.min.css" />-->
|
||||||
<script src="${base!}/assets/platform/plugins/vxeui/xe-utils.js"></script>
|
<script src="${base!}/assets/platform/plugins/vxeui/xe-utils.js"></script>
|
||||||
<script src="${base!}/assets/platform/plugins/vxeui/vxe-table.js"></script>
|
|
||||||
<script src="${base!}/assets/platform/plugins/vxeui/vxe-pc-ui.js"></script>
|
<script src="${base!}/assets/platform/plugins/vxeui/vxe-pc-ui.js"></script>
|
||||||
|
<script src="${base!}/assets/platform/plugins/vxeui/vxe-table.js"></script>
|
||||||
|
|
||||||
<!-- 引入 form-create 和 designer -->
|
<!-- 引入 form-create 和 designer -->
|
||||||
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
|
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
|
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
|
||||||
|
|
||||||
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
|
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
|
||||||
|
<!-- 弹框历史必须先于 PJAX 注册,关闭弹框时优先消费返回事件,避免重新加载业务页面。 -->
|
||||||
|
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js?v=20260930-2"></script>
|
||||||
<!-- pjax是异步加载html片段的工具,模拟前端路由机制 -->
|
<!-- pjax是异步加载html片段的工具,模拟前端路由机制 -->
|
||||||
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
|
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
|
||||||
|
|
||||||
@@ -57,7 +59,6 @@
|
|||||||
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
|
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
|
||||||
<script src="${base!}/components/plugins/sysDict/DictData.js"></script>
|
<script src="${base!}/components/plugins/sysDict/DictData.js"></script>
|
||||||
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
|
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
|
||||||
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js"></script>
|
|
||||||
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
|
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
|
||||||
|
|
||||||
<!--富文本编辑器-->
|
<!--富文本编辑器-->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const ACTIVITY_CULTURE_USER_STATISTICS = {
|
const ACTIVITY_CULTURE_USER_STATISTICS = {
|
||||||
template: /*language=HTML*/ `
|
template: /*language=HTML*/ `
|
||||||
<guava ref="guava">
|
<guava ref="guava" @hook:mounted="onPageVisible">
|
||||||
<template>
|
<template>
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
|
|
||||||
@@ -65,7 +65,8 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
|
|||||||
</el-button>
|
</el-button>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
|
|
||||||
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder" row-key="id"
|
<el-table ref="tableRef" class="activity-culture-statistics-table" :data="tableData"
|
||||||
|
:max-height="tableHeight" :size="tableSize" @sort-change="pageOrder" row-key="id"
|
||||||
style="width: 100%;" v-loading="tableLoading">
|
style="width: 100%;" v-loading="tableLoading">
|
||||||
<el-table-column :index="indexMethod" align="center" header-align="center"
|
<el-table-column :index="indexMethod" align="center" header-align="center"
|
||||||
label="序号" type="index"
|
label="序号" type="index"
|
||||||
@@ -110,8 +111,10 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
|
|||||||
pageForm: {
|
pageForm: {
|
||||||
unionId: "",
|
unionId: "",
|
||||||
searchName: "atp.username",
|
searchName: "atp.username",
|
||||||
|
pageSize: 50,
|
||||||
year: new Date().getFullYear() + ""
|
year: new Date().getFullYear() + ""
|
||||||
},
|
},
|
||||||
|
tableHeight: 300,
|
||||||
tableColumns: [
|
tableColumns: [
|
||||||
{ prop: "userName", label: "姓名", width: 180, fixed: "left" },
|
{ prop: "userName", label: "姓名", width: 180, fixed: "left" },
|
||||||
{ prop: "loginName", label: "工号", width: 180, fixed: "left" },
|
{ prop: "loginName", label: "工号", width: 180, fixed: "left" },
|
||||||
@@ -133,6 +136,60 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
|
|||||||
methods: {
|
methods: {
|
||||||
getNestedValue: getNestedValue,
|
getNestedValue: getNestedValue,
|
||||||
|
|
||||||
|
// 页面在 PJAX 显示后再测量,避免首次渲染时隐藏容器给出错误高度。
|
||||||
|
onPageVisible() {
|
||||||
|
this.$nextTick(() => requestAnimationFrame(() => this.updateTableHeight()))
|
||||||
|
},
|
||||||
|
|
||||||
|
// 按当前视口和分页实际位置计算表格最大高度,保留分页及卡片底部空间。
|
||||||
|
updateTableHeight() {
|
||||||
|
if (!this.$refs.tableRef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const table = this.$refs.tableRef.$el
|
||||||
|
const card = table.closest(".el-card")
|
||||||
|
const content = table.closest(".ele-admin-content")
|
||||||
|
const pagination = card && card.querySelector(".el-pagination-container")
|
||||||
|
if (!content || !pagination || !table.isConnected || !table.getBoundingClientRect().width || !pagination.getBoundingClientRect().height) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content.scrollTop = 0
|
||||||
|
const pageBody = table.closest(".ele-body")
|
||||||
|
const pagePadding = pageBody ? parseFloat(getComputedStyle(pageBody).paddingBottom) || 0 : 0
|
||||||
|
// 页签异步出现时内容区可能被下推,以浏览器可视底部为最终边界。
|
||||||
|
const visibleBottom = Math.min(content.getBoundingClientRect().bottom, window.innerHeight) - pagePadding - 4
|
||||||
|
const nextHeight = Math.max(120, Math.floor(table.getBoundingClientRect().height + visibleBottom - card.getBoundingClientRect().bottom))
|
||||||
|
if (nextHeight !== this.tableHeight) {
|
||||||
|
this.tableHeight = nextHeight
|
||||||
|
}
|
||||||
|
// 筛选区和卡片高度变化时重新计算,避免异步内容或窗口变化遮挡分页。
|
||||||
|
if (!this.tableResizeObserver && window.ResizeObserver) {
|
||||||
|
this.tableResizeObserver = new ResizeObserver(() => this.updateTableHeight())
|
||||||
|
const resizeTargets = [content, document.querySelector("#ele-admin-tabs"), this.$el.querySelector(".el-card"), card, card.querySelector(".ele-table-tool"), pagination]
|
||||||
|
resizeTargets.forEach((element) => {
|
||||||
|
if (element) {
|
||||||
|
this.tableResizeObserver.observe(element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 组件离开页面时清理监听,防止旧表格继续响应尺寸变化。
|
||||||
|
stopTableResize() {
|
||||||
|
window.removeEventListener("resize", this.pageVisibleHandler)
|
||||||
|
window.removeEventListener("load", this.pageVisibleHandler)
|
||||||
|
$(document).off("pjax:send", this.pageExitHandler)
|
||||||
|
$(document).off("pjax:complete", this.pageVisibleHandler)
|
||||||
|
if (this.tabsObserver) {
|
||||||
|
this.tabsObserver.disconnect()
|
||||||
|
this.tabsObserver = null
|
||||||
|
}
|
||||||
|
if (this.tableResizeObserver) {
|
||||||
|
this.tableResizeObserver.disconnect()
|
||||||
|
this.tableResizeObserver = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
openMore(dynamicFormData) {
|
openMore(dynamicFormData) {
|
||||||
const formConfig = this.activityList.find((v) => v.id === this.pageForm.activityId)?.formConfig
|
const formConfig = this.activityList.find((v) => v.id === this.pageForm.activityId)?.formConfig
|
||||||
this.$refs.dynamicTableFormEvalRef.onOpen(formConfig, dynamicFormData)
|
this.$refs.dynamicTableFormEvalRef.onOpen(formConfig, dynamicFormData)
|
||||||
@@ -197,8 +254,42 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
// 原生和 jQuery 事件会改变 this,使用稳定的箭头回调以便正确执行和解绑。
|
||||||
|
this.pageVisibleHandler = () => this.onPageVisible()
|
||||||
|
this.pageExitHandler = () => this.stopTableResize()
|
||||||
|
window.addEventListener("resize", this.pageVisibleHandler)
|
||||||
|
window.addEventListener("load", this.pageVisibleHandler)
|
||||||
|
$(document).one("pjax:send", this.pageExitHandler)
|
||||||
|
$(document).on("pjax:complete", this.pageVisibleHandler)
|
||||||
|
// 顶部页签独立异步挂载,只移动内容区时 ResizeObserver 不会收到尺寸变化。
|
||||||
|
const tabs = document.querySelector("#ele-admin-tabs")
|
||||||
|
if (tabs && window.MutationObserver) {
|
||||||
|
this.tabsObserver = new MutationObserver(() => this.onPageVisible())
|
||||||
|
this.tabsObserver.observe(tabs, { childList: true, subtree: true })
|
||||||
|
}
|
||||||
|
this.onPageVisible()
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
// 每次异步渲染后重新测量,首次取得尺寸不代表页面布局已稳定。
|
||||||
|
if (this.$refs.tableRef) {
|
||||||
|
this.onPageVisible()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopTableResize()
|
||||||
|
},
|
||||||
async created() {
|
async created() {
|
||||||
await this.getActivityByYearOrType()
|
await this.getActivityByYearOrType()
|
||||||
this.unionList = await this.$businessTool.listUnion()
|
this.unionList = await this.$businessTool.listUnion()
|
||||||
|
},
|
||||||
|
style: /*language=CSS*/ `
|
||||||
|
.ele-admin-fixed-body .ele-admin-content {
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activity-culture-statistics-table .el-table__body-wrapper {
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
}
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const signForm = {
|
|||||||
<div>
|
<div>
|
||||||
<el-dialog :close-on-click-modal="false" :visible.sync="signDialog"
|
<el-dialog :close-on-click-modal="false" :visible.sync="signDialog"
|
||||||
:title="prefillMode ? (formData.id ? '修改预填信息' : '预填报名信息') : (editMode ? '修改报名' : '信息填写')" width="50%"
|
:title="prefillMode ? (formData.id ? '修改预填信息' : '预填报名信息') : (editMode ? '修改报名' : '信息填写')" width="50%"
|
||||||
append-to-body>
|
class="family-sign-dialog" top="5vh" append-to-body>
|
||||||
<el-form :model="formData" ref="form" label-width="120px">
|
<el-form :model="formData" ref="form" label-width="120px">
|
||||||
<div class="left-span-label">个人信息</div>
|
<div class="left-span-label">个人信息</div>
|
||||||
<el-row>
|
<el-row>
|
||||||
@@ -72,6 +72,9 @@ const signForm = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activity.familyCountScope === 2" style="color: #c64120; margin-bottom: 10px">
|
||||||
|
本活动累计最多报名{{activity.familyMaxCount}}名不同{{activity.keyWord}},含候补;
|
||||||
|
</div>
|
||||||
<div v-if="formData.mobileColumnsValue && formData.mobileColumnsValue.length > 0">
|
<div v-if="formData.mobileColumnsValue && formData.mobileColumnsValue.length > 0">
|
||||||
<el-tabs v-model="active" type="card">
|
<el-tabs v-model="active" type="card">
|
||||||
<el-tab-pane v-for="(item, index) in formData.mobileColumnsValue"
|
<el-tab-pane v-for="(item, index) in formData.mobileColumnsValue"
|
||||||
@@ -137,7 +140,7 @@ const signForm = {
|
|||||||
|
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-row class="mt20" justify="end" type="flex">
|
<el-row slot="footer" justify="end" type="flex">
|
||||||
<el-button @click="signDialog = false">取 消</el-button>
|
<el-button @click="signDialog = false">取 消</el-button>
|
||||||
<el-button :loading="formLoading" @click="onSubmit" type="primary">{{prefillMode ? '保存预填' : '提 交'}}</el-button>
|
<el-button :loading="formLoading" @click="onSubmit" type="primary">{{prefillMode ? '保存预填' : '提 交'}}</el-button>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -158,7 +161,7 @@ const signForm = {
|
|||||||
courseRow: {},
|
courseRow: {},
|
||||||
courseTypeRow: {},
|
courseTypeRow: {},
|
||||||
courseTimeSelectList: [],
|
courseTimeSelectList: [],
|
||||||
active: '',
|
active: '0',
|
||||||
|
|
||||||
activity: {},
|
activity: {},
|
||||||
}
|
}
|
||||||
@@ -173,13 +176,23 @@ const signForm = {
|
|||||||
this.$message.warning('家属最多人数为' + this.courseTypeRow.familyMaxCount)
|
this.$message.warning('家属最多人数为' + this.courseTypeRow.familyMaxCount)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 类型上限与整个活动上限同时生效;跨课程已用家属集合由后端在最终提交时校验。
|
||||||
|
if (this.activity.familyCountScope === 2
|
||||||
|
&& this.formData.mobileColumnsValue.length >= Number(this.activity.familyMaxCount)) {
|
||||||
|
this.$message.warning('本活动累计家属人数最多为' + this.activity.familyMaxCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
const list = clone(this.courseTypeRow.familyMobileSignColumnList)
|
const list = clone(this.courseTypeRow.familyMobileSignColumnList)
|
||||||
this.formData.mobileColumnsValue.push(list)
|
this.formData.mobileColumnsValue.push(list)
|
||||||
|
// 新增儿童后选中对应的字符串索引,确保标签和填写区域同步显示。
|
||||||
|
this.$set(this, "active", String(this.formData.mobileColumnsValue.length - 1))
|
||||||
},
|
},
|
||||||
delFamily() {
|
delFamily() {
|
||||||
|
// 无儿童时不执行删除;删完后保留首项索引,兼容重新添加儿童。
|
||||||
|
if (this.formData.mobileColumnsValue.length === 0) return
|
||||||
this.formData.mobileColumnsValue.splice(this.active, 1)
|
this.formData.mobileColumnsValue.splice(this.active, 1)
|
||||||
const ac = (Number(this.active) - 1)
|
const ac = (Number(this.active) - 1)
|
||||||
this.active = '' + (ac >= 0 ? ac : 0)
|
this.$set(this, "active", String(ac >= 0 ? ac : 0))
|
||||||
},
|
},
|
||||||
validSignUp() {
|
validSignUp() {
|
||||||
//获取家属是多少人
|
//获取家属是多少人
|
||||||
@@ -478,12 +491,33 @@ const signForm = {
|
|||||||
this.mergeMobileColumnsValue(courseType, signUpData.mobileColumnsValue))
|
this.mergeMobileColumnsValue(courseType, signUpData.mobileColumnsValue))
|
||||||
}
|
}
|
||||||
this.$set(this, "formData", formData)
|
this.$set(this, "formData", formData)
|
||||||
|
// 不同活动共用报名组件,每次打开都选中首个儿童,避免沿用上次的标签状态。
|
||||||
|
this.$set(this, "active", "0")
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.form.clearValidate()
|
this.$refs.form.clearValidate()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
style: /*language=CSS*/ `
|
style: /*language=CSS*/ `
|
||||||
|
/* 报名弹框保留上下可视边距,仅正文滚动,标题和操作按钮始终可见。 */
|
||||||
|
.family-sign-dialog {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.family-sign-dialog > .el-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 90vh;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.family-sign-dialog > .el-dialog > .el-dialog__header,
|
||||||
|
.family-sign-dialog > .el-dialog > .el-dialog__footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.family-sign-dialog > .el-dialog > .el-dialog__body {
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
}
|
||||||
::v-deep .el-tabs__header {
|
::v-deep .el-tabs__header {
|
||||||
margin: 0 0 15px !important;
|
margin: 0 0 15px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,24 @@ const basicForm = {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-form-item label="家属人数范围" prop="familyCountScope">
|
||||||
|
<el-radio-group :value="formData.familyCountScope" size="small"
|
||||||
|
@input="(val) => { this.$set(formData, 'familyCountScope', val) }">
|
||||||
|
<el-radio :label="1" border>按子活动类型累计</el-radio>
|
||||||
|
<el-radio :label="2" border>按整个活动累计</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<div style="color: #c64120; font-size: 12px" v-if="formData.familyCountScope === 2">
|
||||||
|
同一教职工在本活动所有子活动中,家属按唯一标识去重后的累计人数不得超过家属最多数;含候补报名。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报名时间冲突" prop="timeConflictCheck">
|
||||||
|
<el-switch :value="formData.timeConflictCheck"
|
||||||
|
@input="(val) => { this.$set(formData, 'timeConflictCheck', val) }"
|
||||||
|
active-text="禁止同一家属时间重叠报名"></el-switch>
|
||||||
|
<div style="color: #c64120; font-size: 12px">
|
||||||
|
按同一教职工下同一家属的子活动实际日期及起止时间判断,含候补报名。同一家属上午与同日全天互斥,上午与下午、不同日期可兼报;不同家属可以报名同时段的不同子活动。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item :rules="{required:true,message: '请输入活动介绍', trigger: 'blur'}" label="活动介绍" prop="introduce">
|
<el-form-item :rules="{required:true,message: '请输入活动介绍', trigger: 'blur'}" label="活动介绍" prop="introduce">
|
||||||
<text-editor v-model="formData.introduce"></text-editor>
|
<text-editor v-model="formData.introduce"></text-editor>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -280,9 +298,14 @@ const basicForm = {
|
|||||||
|
|
||||||
<div class="left-span-label" style="margin-top: 20px">判断家属数量唯一标识</div>
|
<div class="left-span-label" style="margin-top: 20px">判断家属数量唯一标识</div>
|
||||||
<el-form-item label="唯一标识" prop="onlyKey">
|
<el-form-item label="唯一标识" prop="onlyKey">
|
||||||
<el-select @focus="keyFocus" v-model="formData.onlyKey" placeholder="请选择唯一标识">
|
<el-select @focus="keyFocus" :value="formData.onlyKey" placeholder="请选择唯一标识" style="width: 420px"
|
||||||
<el-option v-for="item in typeArray" :key="item.columnCode" :label="item.columnName" :value="item.columnCode"></el-option>
|
@input="(val) => { this.$set(formData, 'onlyKey', val) }">
|
||||||
|
<el-option v-for="item in typeArray" :key="item.columnCode" :label="item.optionLabel"
|
||||||
|
:value="item.columnCode" :disabled="item.legacy"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<div style="color: #c64120; font-size: 12px">
|
||||||
|
请选择本活动所有子活动类型共有且均设为必填的字段,各类型字段编码必须一致。有报名或预填后禁止直接更换。
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -290,8 +313,8 @@ const basicForm = {
|
|||||||
<el-button @click="$emit('back')">取消</el-button>
|
<el-button @click="$emit('back')">取消</el-button>
|
||||||
<el-button v-if="step === 2" type="primary" @click="step = 1">上一步</el-button>
|
<el-button v-if="step === 2" type="primary" @click="step = 1">上一步</el-button>
|
||||||
<el-button v-if="step === 1" type="primary" @click="step = 2">下一步</el-button>
|
<el-button v-if="step === 1" type="primary" @click="step = 2">下一步</el-button>
|
||||||
<el-button type="primary" @click="onSave">保存</el-button>
|
<el-button type="primary" :loading="formLoading" @click="onSave">保存</el-button>
|
||||||
<el-button type="primary" @click="onSubmit">提交</el-button>
|
<el-button type="primary" :loading="formLoading" @click="onSubmit">提交</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog :close-on-click-modal="false" :visible.sync="signUpDialog" title="设置报名限制" append-to-body>
|
<el-dialog :close-on-click-modal="false" :visible.sync="signUpDialog" title="设置报名限制" append-to-body>
|
||||||
@@ -325,6 +348,7 @@ const basicForm = {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
signUpDialog: false,
|
signUpDialog: false,
|
||||||
|
formLoading: false,
|
||||||
step: 1,
|
step: 1,
|
||||||
formData: {
|
formData: {
|
||||||
notice: false,
|
notice: false,
|
||||||
@@ -348,7 +372,9 @@ const basicForm = {
|
|||||||
restrictLimit: 1,
|
restrictLimit: 1,
|
||||||
limitNum: 1,
|
limitNum: 1,
|
||||||
typeLimits: [],
|
typeLimits: [],
|
||||||
keyWord: '家属'
|
keyWord: '家属',
|
||||||
|
familyCountScope: 1,
|
||||||
|
timeConflictCheck: false,
|
||||||
},
|
},
|
||||||
historicalActList: [],
|
historicalActList: [],
|
||||||
trainTypeList: [],
|
trainTypeList: [],
|
||||||
@@ -372,28 +398,47 @@ const basicForm = {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
keyFocus() {
|
keyFocus() {
|
||||||
const types = this.formData.courseList.filter(o => o.courseType !== undefined).map(o => o.courseType)
|
const types = [...new Set(this.formData.courseList.map(o => o.courseType).filter(Boolean))]
|
||||||
if(types.length === 0) {
|
if(types.length === 0) {
|
||||||
|
this.$set(this, 'typeArray', [])
|
||||||
this.$message.warning('请先设置' + this.activityType + '类型')
|
this.$message.warning('请先设置' + this.activityType + '类型')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const typeList = this.courseTypeList.filter(o => types.includes(o.id))
|
const typeList = this.courseTypeList.filter(o => types.includes(o.id))
|
||||||
let array = []
|
// 唯一标识必须在每种类型中各出现一次且均为必填,不限定字段的验证规则。
|
||||||
typeList.forEach(item => {
|
const firstColumns = typeList[0] ? (typeList[0].familyMobileSignColumnList || []) : []
|
||||||
array = array.concat(item.familyMobileSignColumnList)
|
const options = firstColumns.filter(column => column.columnCode && typeList.length === types.length
|
||||||
|
&& typeList.every(type => {
|
||||||
|
const matches = (type.familyMobileSignColumnList || []).filter(item => item.columnCode === column.columnCode)
|
||||||
|
return matches.length === 1 && matches[0].isRequired === true
|
||||||
|
})).map(column => ({
|
||||||
|
columnCode: column.columnCode,
|
||||||
|
optionLabel: column.columnName,
|
||||||
|
legacy: false
|
||||||
|
}))
|
||||||
|
// 历史选择仍可回显,但不能作为新选择;不改写历史编码,也不静默清空原配置。
|
||||||
|
if (this.formData.onlyKey && !options.some(item => item.columnCode === this.formData.onlyKey)) {
|
||||||
|
const oldColumn = typeList.flatMap(type => type.familyMobileSignColumnList || [])
|
||||||
|
.find(column => column.columnCode === this.formData.onlyKey)
|
||||||
|
options.push({
|
||||||
|
columnCode: this.formData.onlyKey,
|
||||||
|
optionLabel: oldColumn ? oldColumn.columnName : '原字段',
|
||||||
|
legacy: true
|
||||||
})
|
})
|
||||||
// 根据 columnCode 去重
|
|
||||||
const uniqueMap = new Map()
|
|
||||||
array.forEach(item => {
|
|
||||||
if (item.columnCode) {
|
|
||||||
uniqueMap.set(item.columnCode, item)
|
|
||||||
}
|
}
|
||||||
})
|
this.$set(this, 'typeArray', options)
|
||||||
// 可选:保留没有 columnCode 的项
|
|
||||||
const withoutCode = array.filter(item => !item.columnCode)
|
|
||||||
this.typeArray = [...uniqueMap.values(), ...withoutCode]
|
|
||||||
},
|
},
|
||||||
async validNumber(row, old) {
|
// 老活动空配置按原来的按类型累计、不检查重叠回显,沿用活动时也保留所选规则。
|
||||||
|
applyRuleDefaults() {
|
||||||
|
this.$set(this.formData, 'familyCountScope', Number(this.formData.familyCountScope) === 2 ? 2 : 1)
|
||||||
|
this.$set(this.formData, 'timeConflictCheck', this.formData.timeConflictCheck === true)
|
||||||
|
if (this.formData.courseList.some(course => course.courseType)) {
|
||||||
|
this.keyFocus()
|
||||||
|
} else {
|
||||||
|
this.$set(this, 'typeArray', [])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validNumber(row, old) {
|
||||||
if (GetQueryString("id") === "") {
|
if (GetQueryString("id") === "") {
|
||||||
if (row.courseReservedNumber > row.coursePeopleNumber && row.reserveMode === 1) {
|
if (row.courseReservedNumber > row.coursePeopleNumber && row.reserveMode === 1) {
|
||||||
this.$alert("预留人数不能大于" + this.activityType + "人数!", "提示", {
|
this.$alert("预留人数不能大于" + this.activityType + "人数!", "提示", {
|
||||||
@@ -402,7 +447,7 @@ const basicForm = {
|
|||||||
row.courseReservedNumber = 0
|
row.courseReservedNumber = 0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const registerUserCount = await this.getRegisterUserCount(row.id)
|
return this.getRegisterUserCount(row.id).then((registerUserCount) => {
|
||||||
if (row.courseReservedNumber + registerUserCount > row.coursePeopleNumber) {
|
if (row.courseReservedNumber + registerUserCount > row.coursePeopleNumber) {
|
||||||
if (row.reserveMode === 1) {
|
if (row.reserveMode === 1) {
|
||||||
let num = row.coursePeopleNumber - registerUserCount
|
let num = row.coursePeopleNumber - registerUserCount
|
||||||
@@ -422,11 +467,12 @@ const basicForm = {
|
|||||||
row.courseReservedNumber = 0
|
row.courseReservedNumber = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getRegisterUserCount(courseId) {
|
getRegisterUserCount(courseId) {
|
||||||
const resp = await this.$axios.post("/platform/family/manage/getRegisterUserCount", { courseId })
|
return this.$axios.post("/platform/family/manage/getRegisterUserCount", { courseId })
|
||||||
return resp.code === 0 ? resp.data : 0
|
.then((resp) => resp.code === 0 ? resp.data : 0)
|
||||||
},
|
},
|
||||||
courseTypeChange(val, row) {
|
courseTypeChange(val, row) {
|
||||||
row.reserveMode = 1
|
row.reserveMode = 1
|
||||||
@@ -497,10 +543,11 @@ const basicForm = {
|
|||||||
addCourse() {
|
addCourse() {
|
||||||
this.formData.courseList.push({ courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false })
|
this.formData.courseList.push({ courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false })
|
||||||
},
|
},
|
||||||
async historicalActChange(val) {
|
historicalActChange(val) {
|
||||||
const resp = await this.$axios.post("/platform/family/manage/findOne", {id: val})
|
return this.$axios.post("/platform/family/manage/findOne", {id: val}).then((resp) => {
|
||||||
if (resp.code === 0) {
|
if (resp.code === 0) {
|
||||||
this.formData = resp.data
|
this.$set(this, "formData", resp.data)
|
||||||
|
this.applyRuleDefaults()
|
||||||
this.typeChange(this.formData.trainType)
|
this.typeChange(this.formData.trainType)
|
||||||
this.formData.id = ""
|
this.formData.id = ""
|
||||||
this.formData.courseList.forEach((course) => {
|
this.formData.courseList.forEach((course) => {
|
||||||
@@ -521,6 +568,7 @@ const basicForm = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
typeChange(val) {
|
typeChange(val) {
|
||||||
const type = this.dict.type.FAMILY_SIGNUP_TYPE.find(o => o.code === val)
|
const type = this.dict.type.FAMILY_SIGNUP_TYPE.find(o => o.code === val)
|
||||||
@@ -534,15 +582,13 @@ const basicForm = {
|
|||||||
this.$set(this.formData, "activityGroupName", "全部教职工")
|
this.$set(this.formData, "activityGroupName", "全部教职工")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getActivityGroup() {
|
getActivityGroup() {
|
||||||
const { data } = await this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup")
|
return this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((resp) => resp.data)
|
||||||
return data
|
|
||||||
},
|
},
|
||||||
async getHistoricalActList() {
|
getHistoricalActList() {
|
||||||
const resp = await this.$axios.post("/platform/family/manage/getHistoricalActList", {})
|
return this.$axios.post("/platform/family/manage/getHistoricalActList", {}).then((resp) => resp.data)
|
||||||
return resp.data
|
|
||||||
},
|
},
|
||||||
async onSave() {
|
onSave() {
|
||||||
let valid = false
|
let valid = false
|
||||||
this.$refs.form.validateField("activityName", (errMsg) => {
|
this.$refs.form.validateField("activityName", (errMsg) => {
|
||||||
valid = errMsg === ""
|
valid = errMsg === ""
|
||||||
@@ -551,10 +597,10 @@ const basicForm = {
|
|||||||
this.$message.warning("请输入活动名称")
|
this.$message.warning("请输入活动名称")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await this.doHandle('保存')
|
return this.doHandle('保存')
|
||||||
},
|
},
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.$refs["form"].validate(async (valid, errMsg) => {
|
this.$refs["form"].validate((valid, errMsg) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
const courseValid = this.formData.courseList.some((v, i) => {
|
const courseValid = this.formData.courseList.some((v, i) => {
|
||||||
const basicValid = v.courseName && v.coursePeopleNumber && v.courseLocation && v.courseInstructor && v.courseType
|
const basicValid = v.courseName && v.coursePeopleNumber && v.courseLocation && v.courseInstructor && v.courseType
|
||||||
@@ -599,7 +645,7 @@ const basicForm = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.formData.isDisabled = false
|
this.formData.isDisabled = false
|
||||||
await this.doHandle('提交')
|
this.doHandle('提交')
|
||||||
} else {
|
} else {
|
||||||
if(Object.keys(errMsg).length > 0) {
|
if(Object.keys(errMsg).length > 0) {
|
||||||
this.$message.warning(errMsg[Object.keys(errMsg)[0]][0].message)
|
this.$message.warning(errMsg[Object.keys(errMsg)[0]][0].message)
|
||||||
@@ -609,7 +655,7 @@ const basicForm = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async doHandle(type) {
|
doHandle(type) {
|
||||||
const cloneData = clone(this.formData)
|
const cloneData = clone(this.formData)
|
||||||
cloneData.activitySignUpStartTime = cloneData.activitySignTime !== undefined ? cloneData.activitySignTime[0] : null
|
cloneData.activitySignUpStartTime = cloneData.activitySignTime !== undefined ? cloneData.activitySignTime[0] : null
|
||||||
cloneData.activitySignUpEndTime = cloneData.activitySignTime !== undefined ? cloneData.activitySignTime[1] : null
|
cloneData.activitySignUpEndTime = cloneData.activitySignTime !== undefined ? cloneData.activitySignTime[1] : null
|
||||||
@@ -620,13 +666,13 @@ const basicForm = {
|
|||||||
}
|
}
|
||||||
cloneData.typeLimits = JSON.stringify(this.formData.typeLimits)
|
cloneData.typeLimits = JSON.stringify(this.formData.typeLimits)
|
||||||
cloneData.courseList = JSON.stringify(cloneData.courseList)
|
cloneData.courseList = JSON.stringify(cloneData.courseList)
|
||||||
const confirm = await this.$confirm("您确定要" + type + "吗?", "提示", {
|
return this.$confirm("您确定要" + type + "吗?", "提示", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
type: "warning"
|
type: "warning"
|
||||||
})
|
}).then(() => {
|
||||||
if (confirm !== "confirm") return
|
this.formLoading = true
|
||||||
const resp = await this.$axios.post("/platform/family/manage/doHandle", cloneData)
|
return this.$axios.post("/platform/family/manage/doHandle", cloneData).then((resp) => {
|
||||||
if (resp.code === 0) {
|
if (resp.code === 0) {
|
||||||
this.$message.success(resp.msg)
|
this.$message.success(resp.msg)
|
||||||
this.step = 1
|
this.step = 1
|
||||||
@@ -635,26 +681,35 @@ const basicForm = {
|
|||||||
} else {
|
} else {
|
||||||
this.$message.warning(resp.msg)
|
this.$message.warning(resp.msg)
|
||||||
}
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.formLoading = false
|
||||||
|
})
|
||||||
|
}).catch(() => {})
|
||||||
},
|
},
|
||||||
async getAllType() {
|
getAllType() {
|
||||||
const resp = await this.$axios.post("/platform/family/type/getAllType", {})
|
return this.$axios.post("/platform/family/type/getAllType", {}).then((resp) => resp.data)
|
||||||
return resp.data
|
|
||||||
},
|
},
|
||||||
async init(row) {
|
init(row) {
|
||||||
if (row && row.id) {
|
if (!row || !row.id) {
|
||||||
const resp = await this.$axios.post("/platform/family/manage/findOne", {id: row.id})
|
this.applyRuleDefaults()
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
return this.$axios.post("/platform/family/manage/findOne", {id: row.id}).then((resp) => {
|
||||||
if (resp.code === 0) {
|
if (resp.code === 0) {
|
||||||
this.formData = resp.data
|
this.$set(this, "formData", resp.data)
|
||||||
this.typeChange(this.formData.trainType)
|
this.typeChange(this.formData.trainType)
|
||||||
if(this.formData.onlyKey) this.keyFocus()
|
this.applyRuleDefaults()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async initData(row) {
|
initData(row) {
|
||||||
this.activityGroupList = await this.getActivityGroup()
|
return Promise.all([this.getActivityGroup(), this.getHistoricalActList(), this.getAllType()])
|
||||||
this.historicalActList = await this.getHistoricalActList()
|
.then(([groups, activities, types]) => {
|
||||||
this.courseTypeList = await this.getAllType()
|
this.$set(this, "activityGroupList", groups)
|
||||||
await this.init(row)
|
this.$set(this, "historicalActList", activities)
|
||||||
|
this.$set(this, "courseTypeList", types)
|
||||||
|
return this.init(row)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -664,9 +719,11 @@ const basicForm = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'formData.activityGroupId': {
|
'formData.activityGroupId': {
|
||||||
async handler(newVal, oldVal) {
|
handler(newVal, oldVal) {
|
||||||
this.activityGroupList = await this.getActivityGroup()
|
this.getActivityGroup().then((groups) => {
|
||||||
this.userScopeDialog = false
|
this.$set(this, 'activityGroupList', groups)
|
||||||
|
this.$set(this, 'userScopeDialog', false)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ const info = {
|
|||||||
<el-descriptions-item label="活动结束时间">{{viewData.activityEndTime}}</el-descriptions-item>
|
<el-descriptions-item label="活动结束时间">{{viewData.activityEndTime}}</el-descriptions-item>
|
||||||
<el-descriptions-item label="报名开始时间">{{viewData.activitySignUpStartTime}}</el-descriptions-item>
|
<el-descriptions-item label="报名开始时间">{{viewData.activitySignUpStartTime}}</el-descriptions-item>
|
||||||
<el-descriptions-item label="报名结束时间">{{viewData.activitySignUpEndTime}}</el-descriptions-item>
|
<el-descriptions-item label="报名结束时间">{{viewData.activitySignUpEndTime}}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="家属人数范围">{{viewData.familyCountScope === 2 ? '按整个活动累计' : '按子活动类型累计'}}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="活动家属最多数">{{viewData.familyMaxCount}}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="禁止同一家属时间重叠">{{viewData.timeConflictCheck === true ? '是(含候补)' : '否'}}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="家属唯一标识编码">{{viewData.onlyKey}}</el-descriptions-item>
|
||||||
<el-descriptions-item label="详细信息" :span="2">
|
<el-descriptions-item label="详细信息" :span="2">
|
||||||
<div v-html="viewData.introduce"></div>
|
<div v-html="viewData.introduce"></div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -54,6 +58,7 @@ const info = {
|
|||||||
<el-dialog :visible.sync="viewMapDialog" title="地点" width="60%">
|
<el-dialog :visible.sync="viewMapDialog" title="地点" width="60%">
|
||||||
<div id="viewMap" style="width: 100%; height: 500px"></div>
|
<div id="viewMap" style="width: 100%; height: 500px"></div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
dicts: ["FAMILY_SIGNUP_TYPE"],
|
dicts: ["FAMILY_SIGNUP_TYPE"],
|
||||||
@@ -72,7 +77,7 @@ const info = {
|
|||||||
this.$axios.post("/platform/family/manage/findOne", { id: id })
|
this.$axios.post("/platform/family/manage/findOne", { id: id })
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
if (resp.code === 0) {
|
if (resp.code === 0) {
|
||||||
this.viewData = resp.data
|
this.$set(this, 'viewData', resp.data)
|
||||||
const type = this.dict.type.FAMILY_SIGNUP_TYPE.find((o) => o.code === resp.data.trainType)
|
const type = this.dict.type.FAMILY_SIGNUP_TYPE.find((o) => o.code === resp.data.trainType)
|
||||||
this.activityType = type?.name || "子活动"
|
this.activityType = type?.name || "子活动"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ layout("/layouts/platform.html"){
|
|||||||
#-->
|
#-->
|
||||||
|
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak>
|
||||||
<guava ref="guava">
|
<guava ref="guava" @hook:mounted="onPageVisible">
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<search @search="doSearch">
|
<search @search="doSearch">
|
||||||
<search-item label="年度:">
|
<search-item label="年度:">
|
||||||
@@ -36,7 +36,8 @@ layout("/layouts/platform.html"){
|
|||||||
<el-button @click="exportSignPerson" type="primary" size="small">导出签到名单</el-button>
|
<el-button @click="exportSignPerson" type="primary" size="small">导出签到名单</el-button>
|
||||||
<el-button @click="exportGiftPerson" type="primary" size="small">导出领取名单</el-button>
|
<el-button @click="exportGiftPerson" type="primary" size="small">导出领取名单</el-button>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
<el-table :data="tableData" :size="tableSize">
|
<el-table ref="tableRef" class="train-signup-user-table" :data="tableData"
|
||||||
|
:max-height="tableHeight" :size="tableSize">
|
||||||
<el-table-column :index="indexMethod" label="序号" type="index" width="80px"></el-table-column>
|
<el-table-column :index="indexMethod" label="序号" type="index" width="80px"></el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="column.prop === 'courseNames' ? trainType : column.label"
|
:label="column.prop === 'courseNames' ? trainType : column.label"
|
||||||
@@ -98,15 +99,42 @@ layout("/layouts/platform.html"){
|
|||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
|
mounted() {
|
||||||
|
// 原生和 jQuery 事件使用稳定的箭头回调,确保尺寸重算及销毁时正确解绑。
|
||||||
|
this.pageVisibleHandler = () => this.onPageVisible()
|
||||||
|
this.pageExitHandler = () => this.stopTableResize()
|
||||||
|
window.addEventListener("resize", this.pageVisibleHandler)
|
||||||
|
window.addEventListener("load", this.pageVisibleHandler)
|
||||||
|
$(document).one("pjax:send", this.pageExitHandler)
|
||||||
|
$(document).on("pjax:complete", this.pageVisibleHandler)
|
||||||
|
// 顶部页签异步挂载可能只移动内容区,不改变已监听元素的尺寸。
|
||||||
|
const tabs = document.querySelector("#ele-admin-tabs")
|
||||||
|
if (tabs && window.MutationObserver) {
|
||||||
|
this.tabsObserver = new MutationObserver(() => this.onPageVisible())
|
||||||
|
this.tabsObserver.observe(tabs, { childList: true, subtree: true })
|
||||||
|
}
|
||||||
|
this.onPageVisible()
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
// 表格及查询数据异步渲染后继续校正,首次取得的高度不一定是最终布局。
|
||||||
|
if (this.$refs.tableRef) {
|
||||||
|
this.onPageVisible()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopTableResize()
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
reserveDialogVisible: false,
|
reserveDialogVisible: false,
|
||||||
reserveTableData: [],
|
reserveTableData: [],
|
||||||
activityList: [],
|
activityList: [],
|
||||||
pageForm: {
|
pageForm: {
|
||||||
|
pageSize: 50,
|
||||||
year: new Date().getFullYear().toString(),
|
year: new Date().getFullYear().toString(),
|
||||||
activityId: null
|
activityId: null
|
||||||
},
|
},
|
||||||
|
tableHeight: 300,
|
||||||
tableColumns: [
|
tableColumns: [
|
||||||
{ label: "姓名", prop: "username" },
|
{ label: "姓名", prop: "username" },
|
||||||
{ label: "工号", prop: "loginname" },
|
{ label: "工号", prop: "loginname" },
|
||||||
@@ -139,6 +167,55 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 页面显示后再读取表格位置,避免刷新或 PJAX 隐藏期间计算出错误高度。
|
||||||
|
onPageVisible() {
|
||||||
|
this.$nextTick(() => requestAnimationFrame(() => this.updateTableHeight()))
|
||||||
|
},
|
||||||
|
// 按浏览器可视底部与卡片实际留白计算表格最大高度,让分页始终留在表格外。
|
||||||
|
updateTableHeight() {
|
||||||
|
if (!this.$refs.tableRef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const table = this.$refs.tableRef.$el
|
||||||
|
const card = table.closest(".el-card")
|
||||||
|
const content = table.closest(".ele-admin-content")
|
||||||
|
const pagination = card && card.querySelector(".el-pagination-container")
|
||||||
|
if (!content || !pagination || !table.isConnected || !table.getBoundingClientRect().width || !pagination.getBoundingClientRect().height) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content.scrollTop = 0
|
||||||
|
const pageBody = table.closest(".ele-body")
|
||||||
|
const pagePadding = pageBody ? parseFloat(getComputedStyle(pageBody).paddingBottom) || 0 : 0
|
||||||
|
const visibleBottom = Math.min(content.getBoundingClientRect().bottom, window.innerHeight) - pagePadding - 4
|
||||||
|
const nextHeight = Math.max(120, Math.floor(table.getBoundingClientRect().height + visibleBottom - card.getBoundingClientRect().bottom))
|
||||||
|
if (nextHeight !== this.tableHeight) {
|
||||||
|
this.tableHeight = nextHeight
|
||||||
|
}
|
||||||
|
if (!this.tableResizeObserver && window.ResizeObserver) {
|
||||||
|
this.tableResizeObserver = new ResizeObserver(() => this.updateTableHeight())
|
||||||
|
const resizeTargets = [content, document.querySelector("#ele-admin-tabs"), this.$el.querySelector(".el-card"), card, card.querySelector(".ele-table-tool"), pagination]
|
||||||
|
resizeTargets.forEach((element) => {
|
||||||
|
if (element) {
|
||||||
|
this.tableResizeObserver.observe(element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// PJAX 切换或组件销毁时清理监听,防止旧页面继续修改表格高度。
|
||||||
|
stopTableResize() {
|
||||||
|
window.removeEventListener("resize", this.pageVisibleHandler)
|
||||||
|
window.removeEventListener("load", this.pageVisibleHandler)
|
||||||
|
$(document).off("pjax:send", this.pageExitHandler)
|
||||||
|
$(document).off("pjax:complete", this.pageVisibleHandler)
|
||||||
|
if (this.tabsObserver) {
|
||||||
|
this.tabsObserver.disconnect()
|
||||||
|
this.tabsObserver = null
|
||||||
|
}
|
||||||
|
if (this.tableResizeObserver) {
|
||||||
|
this.tableResizeObserver.disconnect()
|
||||||
|
this.tableResizeObserver = null
|
||||||
|
}
|
||||||
|
},
|
||||||
handleSelectionChange(val) {
|
handleSelectionChange(val) {
|
||||||
this.multipleSelection = val
|
this.multipleSelection = val
|
||||||
},
|
},
|
||||||
@@ -259,6 +336,16 @@ layout("/layouts/platform.html"){
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ele-admin-fixed-body .ele-admin-content {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app .train-signup-user-table .el-table__body-wrapper {
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!--#
|
<!--#
|
||||||
}
|
}
|
||||||
#-->
|
#-->
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ layout("/layouts/platform.html"){
|
|||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<table-tool :app="this" label="收货地址">
|
<table-tool :app="this" label="收货地址">
|
||||||
<el-button @click="openAddress" size="small" type="primary">新增地址</el-button>
|
<el-button @click="openAddress" size="small" type="primary">新增地址</el-button>
|
||||||
|
<el-button v-if="$auth.hasRole('SYSADMIN')" @click="exportAllAddress" size="small" type="primary">导出所有人收货地址</el-button>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
@@ -74,6 +75,10 @@ layout("/layouts/platform.html"){
|
|||||||
"address-dialog": ADDRESS_DIALOG
|
"address-dialog": ADDRESS_DIALOG
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 导出全部用户的收货地址,不传当前用户或分页条件;下载和 loading 复用全局方法。
|
||||||
|
exportAllAddress() {
|
||||||
|
this.$downLoad("/platform/welfare/addressManage/exportAllAddress", {})
|
||||||
|
},
|
||||||
selectUserAddress(){
|
selectUserAddress(){
|
||||||
this.pageData()
|
this.pageData()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ layout("/layouts/platform.html"){
|
|||||||
#-->
|
#-->
|
||||||
|
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak>
|
||||||
<guava>
|
<guava @hook:mounted="onPageVisible">
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<search @search="doSearch">
|
<search @search="doSearch">
|
||||||
<search-item label="所属年份">
|
<search-item label="所属年份">
|
||||||
@@ -96,7 +96,9 @@ layout("/layouts/platform.html"){
|
|||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
|
:max-height="tableHeight"
|
||||||
:size="tableSize"
|
:size="tableSize"
|
||||||
|
class="welfare-selection-table"
|
||||||
@sort-change="pageOrder"
|
@sort-change="pageOrder"
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@@ -168,6 +170,31 @@ layout("/layouts/platform.html"){
|
|||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
|
mounted() {
|
||||||
|
// 原生和 jQuery 事件会改变 this,保留箭头回调供执行和解绑使用。
|
||||||
|
this.pageVisibleHandler = () => this.onPageVisible()
|
||||||
|
this.pageExitHandler = () => this.stopTableResize()
|
||||||
|
window.addEventListener("resize", this.pageVisibleHandler)
|
||||||
|
window.addEventListener("load", this.pageVisibleHandler)
|
||||||
|
$(document).one("pjax:send", this.pageExitHandler)
|
||||||
|
$(document).on("pjax:complete", this.pageVisibleHandler)
|
||||||
|
// 顶部页签独立异步挂载,只移动内容区时 ResizeObserver 不会收到尺寸变化。
|
||||||
|
const tabs = document.querySelector("#ele-admin-tabs")
|
||||||
|
if (tabs && window.MutationObserver) {
|
||||||
|
this.tabsObserver = new MutationObserver(() => this.onPageVisible())
|
||||||
|
this.tabsObserver.observe(tabs, { childList: true, subtree: true })
|
||||||
|
}
|
||||||
|
this.onPageVisible()
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
// 首次取得尺寸不代表异步布局已稳定,每次渲染后继续校正。
|
||||||
|
if (this.$refs.tableRef) {
|
||||||
|
this.onPageVisible()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopTableResize()
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
"option-select": optionSelect
|
"option-select": optionSelect
|
||||||
},
|
},
|
||||||
@@ -175,9 +202,11 @@ layout("/layouts/platform.html"){
|
|||||||
return {
|
return {
|
||||||
pageForm: {
|
pageForm: {
|
||||||
searchName: "username",
|
searchName: "username",
|
||||||
|
pageSize: 50,
|
||||||
year: new Date().getFullYear().toString(),
|
year: new Date().getFullYear().toString(),
|
||||||
isSelect: null
|
isSelect: null
|
||||||
},
|
},
|
||||||
|
tableHeight: 300,
|
||||||
unionOptions: [],
|
unionOptions: [],
|
||||||
unitOptions: [],
|
unitOptions: [],
|
||||||
projectOptions: [],
|
projectOptions: [],
|
||||||
@@ -216,6 +245,57 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 等页面完成显示后再测量,避免 PJAX 隐藏内容区时得到错误尺寸。
|
||||||
|
onPageVisible() {
|
||||||
|
this.$nextTick(() => requestAnimationFrame(() => this.updateTableHeight()))
|
||||||
|
},
|
||||||
|
// 用卡片实际底部校正表格高度,涵盖分页和卡片留白,避免首次渲染后布局变化遮住分页。
|
||||||
|
updateTableHeight() {
|
||||||
|
if (!this.$refs.tableRef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const table = this.$refs.tableRef.$el
|
||||||
|
const card = table.closest(".el-card")
|
||||||
|
const content = table.closest(".ele-admin-content")
|
||||||
|
const pagination = card.querySelector(".el-pagination-container")
|
||||||
|
if (!pagination || !table.isConnected || !table.getBoundingClientRect().width || !pagination.getBoundingClientRect().height) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content.scrollTop = 0
|
||||||
|
const pageBody = table.closest(".ele-body")
|
||||||
|
const pagePadding = parseFloat(getComputedStyle(pageBody).paddingBottom) || 0
|
||||||
|
// 页签异步出现时内容区可能下移,分页必须留在浏览器可视范围内。
|
||||||
|
const visibleBottom = Math.min(content.getBoundingClientRect().bottom, window.innerHeight) - pagePadding - 4
|
||||||
|
const nextHeight = Math.max(120, Math.floor(table.getBoundingClientRect().height + visibleBottom - card.getBoundingClientRect().bottom))
|
||||||
|
if (nextHeight !== this.tableHeight) {
|
||||||
|
this.tableHeight = nextHeight
|
||||||
|
}
|
||||||
|
// 卡片和筛选区异步变化时重新校正;表格高度稳定后不再触发更新。
|
||||||
|
if (!this.tableResizeObserver && window.ResizeObserver) {
|
||||||
|
this.tableResizeObserver = new ResizeObserver(() => this.updateTableHeight())
|
||||||
|
const resizeTargets = [content, document.querySelector("#ele-admin-tabs"), this.$el.querySelector(".el-card"), card, card.querySelector(".ele-table-tool"), pagination]
|
||||||
|
resizeTargets.forEach((element) => {
|
||||||
|
if (element) {
|
||||||
|
this.tableResizeObserver.observe(element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// PJAX 切换或组件销毁时移除监听,避免旧页面继续响应窗口变化。
|
||||||
|
stopTableResize() {
|
||||||
|
window.removeEventListener("resize", this.pageVisibleHandler)
|
||||||
|
window.removeEventListener("load", this.pageVisibleHandler)
|
||||||
|
$(document).off("pjax:send", this.pageExitHandler)
|
||||||
|
$(document).off("pjax:complete", this.pageVisibleHandler)
|
||||||
|
if (this.tabsObserver) {
|
||||||
|
this.tabsObserver.disconnect()
|
||||||
|
this.tabsObserver = null
|
||||||
|
}
|
||||||
|
if (this.tableResizeObserver) {
|
||||||
|
this.tableResizeObserver.disconnect()
|
||||||
|
this.tableResizeObserver = null
|
||||||
|
}
|
||||||
|
},
|
||||||
// 获取福利列表
|
// 获取福利列表
|
||||||
getWelfareList() {
|
getWelfareList() {
|
||||||
this.$axios.post("/platform/welfare/common/list", { year: this.pageForm.year }).then((res) => {
|
this.$axios.post("/platform/welfare/common/list", { year: this.pageForm.year }).then((res) => {
|
||||||
@@ -355,6 +435,16 @@ layout("/layouts/platform.html"){
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ele-admin-fixed-body .ele-admin-content {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app .welfare-selection-table .el-table__body-wrapper {
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!--#
|
<!--#
|
||||||
}
|
}
|
||||||
#-->
|
#-->
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ layout("/layouts/platform.html"){
|
|||||||
#-->
|
#-->
|
||||||
|
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak>
|
||||||
<guava ref="guava">
|
<guava ref="guava" @hook:mounted="onPageVisible">
|
||||||
<template>
|
<template>
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<search @search="doSearch">
|
<search @search="doSearch">
|
||||||
@@ -194,7 +194,9 @@ layout("/layouts/platform.html"){
|
|||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
|
:max-height="tableHeight"
|
||||||
:size="tableSize"
|
:size="tableSize"
|
||||||
|
class="welfare-list-table"
|
||||||
@sort-change="pageOrder"
|
@sort-change="pageOrder"
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@@ -294,6 +296,29 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.checkedFields = this.tableColumns.filter(c => c.checked !== 0).map(c => c.prop);
|
this.checkedFields = this.tableColumns.filter(c => c.checked !== 0).map(c => c.prop);
|
||||||
|
// 原生和 jQuery 事件会改变 this,保留箭头回调供执行和解绑使用。
|
||||||
|
this.pageVisibleHandler = () => this.onPageVisible()
|
||||||
|
this.pageExitHandler = () => this.stopTableResize()
|
||||||
|
window.addEventListener("resize", this.pageVisibleHandler)
|
||||||
|
window.addEventListener("load", this.pageVisibleHandler)
|
||||||
|
$(document).one("pjax:send", this.pageExitHandler)
|
||||||
|
$(document).on("pjax:complete", this.pageVisibleHandler)
|
||||||
|
// 顶部页签独立异步挂载,只移动内容区时 ResizeObserver 不会收到尺寸变化。
|
||||||
|
const tabs = document.querySelector("#ele-admin-tabs")
|
||||||
|
if (tabs && window.MutationObserver) {
|
||||||
|
this.tabsObserver = new MutationObserver(() => this.onPageVisible())
|
||||||
|
this.tabsObserver.observe(tabs, { childList: true, subtree: true })
|
||||||
|
}
|
||||||
|
this.onPageVisible()
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
// 首次取得尺寸不代表异步布局已稳定,每次渲染后继续校正。
|
||||||
|
if (this.$refs.tableRef) {
|
||||||
|
this.onPageVisible()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopTableResize()
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
add_welfare_list_by_select: ADD_WELFARE_LIST_BY_SELECT,
|
add_welfare_list_by_select: ADD_WELFARE_LIST_BY_SELECT,
|
||||||
@@ -306,8 +331,10 @@ layout("/layouts/platform.html"){
|
|||||||
checkedFields: [],
|
checkedFields: [],
|
||||||
pageForm: {
|
pageForm: {
|
||||||
searchName: "username",
|
searchName: "username",
|
||||||
|
pageSize: 50,
|
||||||
year: new Date().getFullYear().toString()
|
year: new Date().getFullYear().toString()
|
||||||
},
|
},
|
||||||
|
tableHeight: 300,
|
||||||
unionOptions: [],
|
unionOptions: [],
|
||||||
unitOptions: [],
|
unitOptions: [],
|
||||||
sexOptions: ["男", "女"],
|
sexOptions: ["男", "女"],
|
||||||
@@ -335,6 +362,57 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 等页面完成显示后再测量,避免 PJAX 隐藏内容区时得到错误尺寸。
|
||||||
|
onPageVisible() {
|
||||||
|
this.$nextTick(() => requestAnimationFrame(() => this.updateTableHeight()))
|
||||||
|
},
|
||||||
|
// 用卡片实际底部校正表格高度,涵盖分页和卡片留白,避免首次渲染后布局变化遮住分页。
|
||||||
|
updateTableHeight() {
|
||||||
|
if (!this.$refs.tableRef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const table = this.$refs.tableRef.$el
|
||||||
|
const card = table.closest(".el-card")
|
||||||
|
const content = table.closest(".ele-admin-content")
|
||||||
|
const pagination = card.querySelector(".el-pagination-container")
|
||||||
|
if (!pagination || !table.isConnected || !table.getBoundingClientRect().width || !pagination.getBoundingClientRect().height) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content.scrollTop = 0
|
||||||
|
const pageBody = table.closest(".ele-body")
|
||||||
|
const pagePadding = parseFloat(getComputedStyle(pageBody).paddingBottom) || 0
|
||||||
|
// 页签异步出现时内容区可能下移,分页必须留在浏览器可视范围内。
|
||||||
|
const visibleBottom = Math.min(content.getBoundingClientRect().bottom, window.innerHeight) - pagePadding - 4
|
||||||
|
const nextHeight = Math.max(120, Math.floor(table.getBoundingClientRect().height + visibleBottom - card.getBoundingClientRect().bottom))
|
||||||
|
if (nextHeight !== this.tableHeight) {
|
||||||
|
this.tableHeight = nextHeight
|
||||||
|
}
|
||||||
|
// 卡片和筛选区异步变化时重新校正;表格高度稳定后不再触发更新。
|
||||||
|
if (!this.tableResizeObserver && window.ResizeObserver) {
|
||||||
|
this.tableResizeObserver = new ResizeObserver(() => this.updateTableHeight())
|
||||||
|
const resizeTargets = [content, document.querySelector("#ele-admin-tabs"), this.$el.querySelector(".el-card"), card, card.querySelector(".ele-table-tool"), pagination]
|
||||||
|
resizeTargets.forEach((element) => {
|
||||||
|
if (element) {
|
||||||
|
this.tableResizeObserver.observe(element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// PJAX 切换或组件销毁时移除监听,避免旧页面继续响应窗口变化。
|
||||||
|
stopTableResize() {
|
||||||
|
window.removeEventListener("resize", this.pageVisibleHandler)
|
||||||
|
window.removeEventListener("load", this.pageVisibleHandler)
|
||||||
|
$(document).off("pjax:send", this.pageExitHandler)
|
||||||
|
$(document).off("pjax:complete", this.pageVisibleHandler)
|
||||||
|
if (this.tabsObserver) {
|
||||||
|
this.tabsObserver.disconnect()
|
||||||
|
this.tabsObserver = null
|
||||||
|
}
|
||||||
|
if (this.tableResizeObserver) {
|
||||||
|
this.tableResizeObserver.disconnect()
|
||||||
|
this.tableResizeObserver = null
|
||||||
|
}
|
||||||
|
},
|
||||||
// 导出收货地址
|
// 导出收货地址
|
||||||
exportAddress() {
|
exportAddress() {
|
||||||
this.$downLoad("/platform/welfare/list/mange/exportAddress", this.pageForm)
|
this.$downLoad("/platform/welfare/list/mange/exportAddress", this.pageForm)
|
||||||
@@ -422,6 +500,16 @@ layout("/layouts/platform.html"){
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ele-admin-fixed-body .ele-admin-content {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app .welfare-list-table .el-table__body-wrapper {
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!--#
|
<!--#
|
||||||
}
|
}
|
||||||
#-->
|
#-->
|
||||||
|
|||||||
@@ -52,13 +52,16 @@ const applyForm = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div v-if="activity.familyCountScope === 2" style="color: #c64120; padding: 10px 16px">
|
||||||
|
本活动累计最多报名{{activity.familyMaxCount}}名不同{{activity.keyWord}},含候补;
|
||||||
|
</div>
|
||||||
<div v-if="formData.mobileColumnsValue && formData.mobileColumnsValue.length > 0" class="mt10">
|
<div v-if="formData.mobileColumnsValue && formData.mobileColumnsValue.length > 0" class="mt10">
|
||||||
<van-tabs v-model="familyActive" type="card" color="#0e78c5" animated>
|
<van-tabs v-model="familyActive" type="card" color="#0e78c5" animated>
|
||||||
<van-tab v-for="item, index in formData.mobileColumnsValue"
|
<van-tab v-for="item, index in formData.mobileColumnsValue"
|
||||||
:name="index + ''"
|
:name="index + ''"
|
||||||
:title="activity.keyWord + (index + 1)"
|
:title="activity.keyWord + (index + 1)"
|
||||||
:key="index">
|
:key="index">
|
||||||
<train-dynamic-form v-model="formData.mobileColumnsValue[index]" :key="index"></train-dynamic-form>
|
<train-dynamic-form v-model="formData.mobileColumnsValue[index]" :key="index" full-option-text></train-dynamic-form>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,6 +114,12 @@ const applyForm = {
|
|||||||
this.$toast(this.activity.keyWord + '最多人数为' + this.courseType.familyMaxCount)
|
this.$toast(this.activity.keyWord + '最多人数为' + this.courseType.familyMaxCount)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 与PC一致,单次添加同时受类型和活动上限约束,跨课程累计由后端负责。
|
||||||
|
if (this.activity.familyCountScope === 2
|
||||||
|
&& this.formData.mobileColumnsValue.length >= Number(this.activity.familyMaxCount)) {
|
||||||
|
this.$toast('本活动累计家属人数最多为' + this.activity.familyMaxCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
const list = clone(this.courseType.familyMobileSignColumnList)
|
const list = clone(this.courseType.familyMobileSignColumnList)
|
||||||
this.formData.mobileColumnsValue.push(list)
|
this.formData.mobileColumnsValue.push(list)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,11 +16,22 @@ layout("/layouts/platform_h5.html"){
|
|||||||
text-overflow: unset !important;
|
text-overflow: unset !important;
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
}
|
}
|
||||||
.sign-success .label{
|
/* 仅突出当前活动列表的报名结果,姓名过长时完整换行展示。 */
|
||||||
color: #0e9558;
|
.table-list-container .sign-success {
|
||||||
|
align-items: baseline;
|
||||||
|
line-height: 28px;
|
||||||
}
|
}
|
||||||
.sign-success .value{
|
.table-list-container .sign-success .label {
|
||||||
color: #0e9558;
|
color: #087f4f;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.table-list-container .sign-success .value {
|
||||||
|
min-width: 0;
|
||||||
|
color: #087f4f;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ const home = {
|
|||||||
template: /*language=HTML*/ `
|
template: /*language=HTML*/ `
|
||||||
<div>
|
<div>
|
||||||
<van-nav-bar title="首页" class="nav-bar" placeholder fixed></van-nav-bar>
|
<van-nav-bar title="首页" class="nav-bar" placeholder fixed></van-nav-bar>
|
||||||
<van-swipe :autoplay="100000" :height="200" class="home__swipe">
|
<!-- <van-swipe :autoplay="100000" :height="200" class="home__swipe">-->
|
||||||
<van-swipe-item>
|
<!-- <van-swipe-item>-->
|
||||||
<img src="/assets/mobile/img/home/home1.png" alt="">
|
<!-- <img src="/assets/mobile/img/home/home1.png" alt="">-->
|
||||||
</van-swipe-item>
|
<!-- </van-swipe-item>-->
|
||||||
<van-swipe-item>
|
<!-- <van-swipe-item>-->
|
||||||
<img src="/assets/mobile/img/home/home2.png" alt="">
|
<!-- <img src="/assets/mobile/img/home/home2.png" alt="">-->
|
||||||
</van-swipe-item>
|
<!-- </van-swipe-item>-->
|
||||||
<van-swipe-item>
|
<!-- <van-swipe-item>-->
|
||||||
<img src="/assets/mobile/img/home/home3.png" alt="">
|
<!-- <img src="/assets/mobile/img/home/home3.png" alt="">-->
|
||||||
</van-swipe-item>
|
<!-- </van-swipe-item>-->
|
||||||
</van-swipe>
|
<!-- </van-swipe>-->
|
||||||
|
|
||||||
<!--快捷入口-->
|
<!--快捷入口-->
|
||||||
<div class="quick-grid">
|
<div class="quick-grid">
|
||||||
<div class="section-title">快速入口</div>
|
<!-- <div class="section-title">快速入口</div>-->
|
||||||
<van-grid :column-num="4" icon-size="46" square clickable :border="false">
|
<van-grid :column-num="4" icon-size="46" square clickable :border="false">
|
||||||
<van-grid-item v-for="item in quickEntries"
|
<van-grid-item v-for="item in quickEntries"
|
||||||
@click="menuClick(item)"
|
@click="menuClick(item)"
|
||||||
|
|||||||
@@ -0,0 +1,603 @@
|
|||||||
|
<!--#
|
||||||
|
layout("/layouts/platform_h5.html"){
|
||||||
|
#-->
|
||||||
|
<style>
|
||||||
|
/* 页面限定在可用视口内,顶部信息不收缩,剩余高度交给人员列表。 */
|
||||||
|
#app.selection-situation-page { height: 100%; height: 100dvh; min-height: 0; flex-direction: column; overflow: hidden; box-sizing: border-box; background: #f4f6fa; color: #1c293a; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
|
||||||
|
#app.selection-situation-page:not([v-cloak]) { display: flex; }
|
||||||
|
.selection-situation-page > .van-nav-bar,
|
||||||
|
.selection-situation-page > .selection-filters,
|
||||||
|
.selection-situation-page > .selection-result-count { flex-shrink: 0; }
|
||||||
|
/* van-list 会监听此滚动容器;到达边界时不把滚动继续传给外层页面。 */
|
||||||
|
.selection-person-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; padding: 16px 0 calc(20px + env(safe-area-inset-bottom)); }
|
||||||
|
/* 导航栏沿用福利列表的 Vant 默认样式:白底、深色标题、蓝色返回文字和箭头。 */
|
||||||
|
/* 项目独占一行以容纳长名称;两个布尔筛选等宽排列,并分别标明业务含义。 */
|
||||||
|
.selection-filters { padding: 12px 16px; background: #fff; }
|
||||||
|
.selection-filter-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
|
||||||
|
.selection-filter-value { display: flex; align-items: center; justify-content: space-between; flex: 1 1 0; min-width: 0; min-height: 48px; padding: 8px 12px; border: 1px solid #e2e7ef; border-radius: 8px; background: #f8fafc; color: #1c293a; font-family: inherit; font-size: 14px; text-align: left; cursor: pointer; }
|
||||||
|
.selection-filter-project { background: #f0f7ff; border-color: #d0e5fc; }
|
||||||
|
.selection-filter-value.is-active { background: #f0f7ff; border-color: #a9d1f2; }
|
||||||
|
.selection-filter-copy { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 3px; }
|
||||||
|
.selection-filter-label { color: #667085; font-size: 12px; line-height: 16px; }
|
||||||
|
.selection-filter-selected { font-weight: 500; line-height: 20px; }
|
||||||
|
.selection-filter-project .selection-filter-selected,
|
||||||
|
.selection-filter-value.is-active .selection-filter-selected { color: #006dbb; }
|
||||||
|
.selection-filter-value span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
.selection-filter-value .van-icon { flex-shrink: 0; margin-left: 8px; color: #52627a; }
|
||||||
|
.selection-filter-value:focus-visible { outline: 2px solid #006dbb; outline-offset: 2px; }
|
||||||
|
.selection-search-row { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.selection-search-row .van-search { flex: 1; min-width: 0; padding: 0; background: transparent; }
|
||||||
|
.selection-search-row .van-search__content { box-sizing: border-box; height: 40px; align-items: center; border: 1px solid #e2e7ef; border-radius: 8px; background: #f4f6f9; }
|
||||||
|
.selection-search-row .van-field__control { font-size: 15px; }
|
||||||
|
.selection-search-row .van-button { flex-shrink: 0; min-width: 68px; height: 40px; padding: 0 14px; border-radius: 8px; background: #006dbb; border-color: #006dbb; font-size: 14px; font-weight: 500; }
|
||||||
|
.selection-result-count { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 0 16px; border-top: 1px solid #edf0f5; background: #fff; color: #667085; font-size: 13px; }
|
||||||
|
.selection-result-title { color: #1c293a; font-size: 15px; font-weight: 600; }
|
||||||
|
.selection-result-count strong { color: #006dbb; font-size: 16px; font-weight: 600; }
|
||||||
|
/* 卡片按身份、所属组织、福利与操作分为三行;长名称自然换行,避免信息被裁切。 */
|
||||||
|
.selection-person-card { margin: 0 12px 12px; padding: 12px 14px; background: #fff; border-radius: 10px; }
|
||||||
|
.selection-person-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; min-height: 24px; }
|
||||||
|
.selection-person-identity { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 12px; flex: 1; min-width: 0; line-height: 24px; }
|
||||||
|
.selection-person-name { color: #1c293a; font-size: 18px; font-weight: 600; overflow-wrap: anywhere; }
|
||||||
|
.selection-person-number { color: #667085; font-size: 13px; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
||||||
|
.selection-person-status { display: inline-flex; align-items: center; flex-shrink: 0; gap: 6px; color: #138b42; font-size: 13px; line-height: 24px; white-space: nowrap; }
|
||||||
|
.selection-person-status .fa { font-size: 10px; }
|
||||||
|
.selection-person-status.is-unselected { color: #ad7900; }
|
||||||
|
.selection-person-organization { margin-top: 6px; color: #667085; font-size: 13px; line-height: 22px; overflow-wrap: anywhere; }
|
||||||
|
.selection-person-separator { padding: 0 5px; color: #98a2b3; }
|
||||||
|
.selection-person-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 6px; }
|
||||||
|
.selection-person-benefit { display: flex; align-items: baseline; flex: 1; min-width: 0; gap: 8px; font-size: 14px; line-height: 22px; }
|
||||||
|
.selection-person-benefit-label { flex-shrink: 0; color: #8792a3; }
|
||||||
|
.selection-person-benefit-value { min-width: 0; color: #1c293a; font-weight: 500; overflow-wrap: anywhere; }
|
||||||
|
.selection-person-benefit-value.is-empty { color: #98a2b3; font-weight: 400; }
|
||||||
|
.selection-person-footer .van-button { flex-shrink: 0; min-width: 68px; height: 34px; padding: 0 12px; border: none; border-radius: 6px; background: #e7f3ff; color: #006dbb; font-size: 14px; font-weight: 500; }
|
||||||
|
.selection-empty { padding-top: 25px; }
|
||||||
|
/* 弹框随内容撑开并限制最大高度,标题和操作按钮保留在滚动内容之外。 */
|
||||||
|
.selection-situation-page .selection-popup { border-radius: 18px 18px 0 0; }
|
||||||
|
.selection-sheet-head { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; gap: 12px; min-height: 56px; padding: 0 12px 0 20px; border-bottom: 1px solid #edf0f5; background: #fff; color: #1c293a; font-size: 17px; font-weight: 600; }
|
||||||
|
.selection-sheet-close { display: flex; align-items: center; justify-content: center; flex: 0 0 40px; width: 40px; height: 40px; padding: 0; border: none; background: transparent; color: #8792a3; font-size: 20px; cursor: pointer; }
|
||||||
|
.selection-picker { display: flex; flex-direction: column; max-height: 75vh; max-height: 75dvh; background: #fff; }
|
||||||
|
.selection-picker-description { flex-shrink: 0; padding: 16px 20px 8px; color: #8792a3; font-size: 13px; line-height: 20px; }
|
||||||
|
.selection-picker-options { min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; padding: 12px 16px; }
|
||||||
|
.selection-picker-option { display: flex; align-items: center; justify-content: space-between; gap: 14px; box-sizing: border-box; width: 100%; min-height: 52px; padding: 12px; border: none; border-bottom: 1px solid #f0f2f5; background: #fff; color: #1c293a; font-family: inherit; font-size: 14px; text-align: left; cursor: pointer; }
|
||||||
|
.selection-picker-option.is-active { border-bottom-color: transparent; border-radius: 6px; background: #eef7ff; }
|
||||||
|
.selection-picker-option-label { min-width: 0; line-height: 22px; overflow-wrap: anywhere; }
|
||||||
|
.selection-picker-option-label .fa { margin-right: 8px; color: #138b42; font-size: 10px; }
|
||||||
|
.selection-picker-option-label .fa.is-unselected { color: #ad7900; }
|
||||||
|
.selection-picker-option > .van-icon { flex-shrink: 0; color: #c3cbd6; font-size: 20px; }
|
||||||
|
.selection-picker-option.is-active > .van-icon { color: #006dbb; }
|
||||||
|
.selection-picker-footer { display: flex; flex-shrink: 0; gap: 10px; padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid #edf0f5; background: #fff; }
|
||||||
|
.selection-picker-footer .van-button { height: 44px; border-radius: 6px; font-size: 14px; font-weight: 500; }
|
||||||
|
.selection-picker-cancel { flex: 2; color: #006dbb; border-color: #cad7e7; }
|
||||||
|
.selection-picker-confirm { flex: 3; background: #006dbb; border-color: #006dbb; }
|
||||||
|
.selection-proxy { display: flex; flex-direction: column; max-height: 85vh; max-height: 85dvh; background: #f4f6fa; }
|
||||||
|
.selection-proxy-content { flex: 0 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; padding: 14px 16px; }
|
||||||
|
.selection-proxy-person { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 14px; color: #1c293a; line-height: 24px; }
|
||||||
|
.selection-proxy-person-name { font-size: 17px; font-weight: 600; overflow-wrap: anywhere; }
|
||||||
|
.selection-proxy-person-number { color: #667085; font-size: 13px; overflow-wrap: anywhere; }
|
||||||
|
.selection-proxy-section { margin-bottom: 12px; padding: 14px; border-radius: 10px; background: #fff; }
|
||||||
|
.selection-proxy-section:last-child { margin-bottom: 0; }
|
||||||
|
.selection-proxy-title { margin-bottom: 8px; color: #1c293a; font-size: 15px; font-weight: 600; line-height: 22px; overflow-wrap: anywhere; }
|
||||||
|
.selection-proxy-description { margin-bottom: 12px; color: #8792a3; font-size: 13px; line-height: 20px; }
|
||||||
|
.selection-option { display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; min-height: 44px; margin-top: 8px; padding: 10px 12px; border: 1px solid #e2e7ef; border-radius: 6px; font-size: 14px; line-height: 22px; }
|
||||||
|
.selection-option.is-active { border-color: #006dbb; background: #eef7ff; }
|
||||||
|
.selection-option-radio { cursor: pointer; }
|
||||||
|
.selection-option-name { flex: 1; min-width: 0; padding-right: 12px; overflow-wrap: anywhere; }
|
||||||
|
.selection-option .van-stepper { flex-shrink: 0; }
|
||||||
|
.selection-proxy-section .van-cell { align-items: center; padding: 8px 0; }
|
||||||
|
.selection-proxy-section .van-field__label { width: 72px; color: #667085; font-size: 13px; }
|
||||||
|
.selection-proxy-section .van-field__body { padding: 7px 10px; border: 1px solid #e2e7ef; border-radius: 6px; background: #f4f6f9; }
|
||||||
|
.selection-proxy-hint { display: flex; align-items: center; gap: 6px; margin-top: 10px; color: #8792a3; font-size: 12px; line-height: 18px; }
|
||||||
|
.selection-proxy-hint .van-icon { flex-shrink: 0; color: #006dbb; font-size: 14px; }
|
||||||
|
.selection-proxy-loading { min-height: 160px; padding: 48px 16px; box-sizing: border-box; text-align: center; }
|
||||||
|
.selection-proxy-footer { flex-shrink: 0; padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid #edf0f5; background: #fff; }
|
||||||
|
.selection-proxy-footer .van-button { height: 44px; border-radius: 6px; background: #006dbb; border-color: #006dbb; font-size: 15px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="app" v-cloak class="selection-situation-page">
|
||||||
|
<van-nav-bar title="选择情况" left-text="返回" left-arrow @click-left="historyBack"></van-nav-bar>
|
||||||
|
|
||||||
|
<div class="selection-filters">
|
||||||
|
<div class="selection-search-row">
|
||||||
|
<van-search v-model="searchKeyword" placeholder="工号或姓名" @search="doSearch"></van-search>
|
||||||
|
<van-button type="info" @click="doSearch">查询</van-button>
|
||||||
|
</div>
|
||||||
|
<div class="selection-filter-row">
|
||||||
|
<button type="button" class="selection-filter-value selection-filter-project" aria-label="选择福利项目"
|
||||||
|
:title="currentProjectName || '请选择福利项目'" @click="openPicker('project')">
|
||||||
|
<span class="selection-filter-copy">
|
||||||
|
<span class="selection-filter-label">福利项目</span>
|
||||||
|
<span class="selection-filter-selected">{{ currentProjectName || '请选择福利项目' }}</span>
|
||||||
|
</span>
|
||||||
|
<van-icon name="arrow-down"></van-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="selection-filter-row">
|
||||||
|
<button type="button" class="selection-filter-value" :class="{'is-active': pageForm.isSelect !== null}"
|
||||||
|
aria-label="选择状态" @click="openPicker('status')">
|
||||||
|
<span class="selection-filter-copy">
|
||||||
|
<span class="selection-filter-label">选择状态</span>
|
||||||
|
<span class="selection-filter-selected">{{ currentStatusName }}</span>
|
||||||
|
</span>
|
||||||
|
<van-icon name="arrow-down"></van-icon>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="selection-filter-value" :class="{'is-active': pageForm.isSelectByAdmin !== null}"
|
||||||
|
aria-label="管理员代选" @click="openPicker('admin')">
|
||||||
|
<span class="selection-filter-copy">
|
||||||
|
<span class="selection-filter-label">管理员代选</span>
|
||||||
|
<span class="selection-filter-selected">{{ currentAdminName }}</span>
|
||||||
|
</span>
|
||||||
|
<van-icon name="arrow-down"></van-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selection-result-count" v-if="pageForm.projectId">
|
||||||
|
<span class="selection-result-title">人员名单</span>
|
||||||
|
<span>共 <strong>{{ pageForm.totalCount }}</strong> 人</span>
|
||||||
|
</div>
|
||||||
|
<div ref="personScroll" class="selection-person-scroll">
|
||||||
|
<van-list v-if="pageForm.projectId" v-model="tableLoading" :finished="finished" finished-text="没有更多了" @load="pageData">
|
||||||
|
<div class="selection-person-card" v-for="row in tableData" :key="row.id">
|
||||||
|
<div class="selection-person-head">
|
||||||
|
<div class="selection-person-identity">
|
||||||
|
<span class="selection-person-name">{{ row.userName || '-' }}</span>
|
||||||
|
<span class="selection-person-number" :aria-label="'工号 ' + (row.loginName || '-')">{{ row.loginName || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="selection-person-status" :class="{'is-unselected': !row.selectedOptions}">
|
||||||
|
<i class="fa fa-circle" aria-hidden="true"></i>
|
||||||
|
{{ row.selectedOptions ? '已选择' : '未选择' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="selection-person-organization">
|
||||||
|
<span :title="'所属工会:' + (row.welfareUnionName || '-')">{{ row.welfareUnionName || '-' }}</span><span class="selection-person-separator" aria-hidden="true">·</span><span :title="'所属单位:' + (row.welfareUnitName || '-')">{{ row.welfareUnitName || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="selection-person-footer">
|
||||||
|
<div class="selection-person-benefit">
|
||||||
|
<span class="selection-person-benefit-label">福利</span>
|
||||||
|
<span class="selection-person-benefit-value" :class="{'is-empty': !row.selectedOptions}">{{ row.selectedOptions || '暂未选择' }}</span>
|
||||||
|
</div>
|
||||||
|
<van-button type="info" size="small" @click="openProxy(row)">代选</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<van-empty class="selection-empty" v-if="!tableLoading && tableData.length === 0" description="暂无符合条件的人员"></van-empty>
|
||||||
|
</van-list>
|
||||||
|
<div class="selection-empty" v-else-if="projectLoading" style="text-align:center"><van-loading>加载福利项目...</van-loading></div>
|
||||||
|
<van-empty class="selection-empty" v-else description="暂无福利项目"></van-empty>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<van-popup v-model="showProjectPicker" class="selection-popup" position="bottom" round :close-on-click-overlay="false">
|
||||||
|
<div class="selection-picker">
|
||||||
|
<div class="selection-sheet-head">
|
||||||
|
<span>选择福利项目</span>
|
||||||
|
<button type="button" class="selection-sheet-close" aria-label="关闭福利项目选择" @click="closePopup('project')"><van-icon name="cross"></van-icon></button>
|
||||||
|
</div>
|
||||||
|
<div class="selection-picker-description">请选择本次查询的福利项目</div>
|
||||||
|
<div class="selection-picker-options" role="group" aria-label="福利项目">
|
||||||
|
<button v-for="item in projectColumns" :key="item.value" type="button" class="selection-picker-option"
|
||||||
|
:class="{'is-active': draftProjectId === item.value}" :aria-pressed="draftProjectId === item.value"
|
||||||
|
@click="selectPickerOption('project', item.value)">
|
||||||
|
<span class="selection-picker-option-label">{{ item.text }}</span>
|
||||||
|
<van-icon :name="draftProjectId === item.value ? 'checked' : 'circle'" aria-hidden="true"></van-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="selection-picker-footer">
|
||||||
|
<van-button class="selection-picker-cancel" @click="closePopup('project')">取消</van-button>
|
||||||
|
<van-button class="selection-picker-confirm" type="info" :disabled="!draftProjectId" @click="chooseProject">确认选择</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
<van-popup v-model="showStatusPicker" class="selection-popup" position="bottom" round :close-on-click-overlay="false">
|
||||||
|
<div class="selection-picker">
|
||||||
|
<div class="selection-sheet-head">
|
||||||
|
<span>选择状态</span>
|
||||||
|
<button type="button" class="selection-sheet-close" aria-label="关闭选择状态" @click="closePopup('status')"><van-icon name="cross"></van-icon></button>
|
||||||
|
</div>
|
||||||
|
<div class="selection-picker-options" role="group" aria-label="选择状态">
|
||||||
|
<button v-for="item in statusColumns" :key="item.text" type="button" class="selection-picker-option"
|
||||||
|
:class="{'is-active': draftStatus === item.value}" :aria-pressed="draftStatus === item.value"
|
||||||
|
@click="selectPickerOption('status', item.value)">
|
||||||
|
<span class="selection-picker-option-label"><i v-if="item.value !== null" class="fa fa-circle" :class="{'is-unselected': item.value === false}" aria-hidden="true"></i>{{ item.text }}</span>
|
||||||
|
<van-icon :name="draftStatus === item.value ? 'checked' : 'circle'" aria-hidden="true"></van-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="selection-picker-footer">
|
||||||
|
<van-button class="selection-picker-cancel" @click="closePopup('status')">取消</van-button>
|
||||||
|
<van-button class="selection-picker-confirm" type="info" @click="chooseStatus">确认选择</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<van-popup v-model="showAdminPicker" class="selection-popup" position="bottom" round :close-on-click-overlay="false">
|
||||||
|
<div class="selection-picker">
|
||||||
|
<div class="selection-sheet-head">
|
||||||
|
<span>管理员代选</span>
|
||||||
|
<button type="button" class="selection-sheet-close" aria-label="关闭管理员代选筛选" @click="closePopup('admin')"><van-icon name="cross"></van-icon></button>
|
||||||
|
</div>
|
||||||
|
<div class="selection-picker-options" role="group" aria-label="管理员代选">
|
||||||
|
<button v-for="item in adminColumns" :key="item.text" type="button" class="selection-picker-option"
|
||||||
|
:class="{'is-active': draftAdmin === item.value}" :aria-pressed="draftAdmin === item.value"
|
||||||
|
@click="selectPickerOption('admin', item.value)">
|
||||||
|
<span class="selection-picker-option-label">{{ item.text }}</span>
|
||||||
|
<van-icon :name="draftAdmin === item.value ? 'checked' : 'circle'" aria-hidden="true"></van-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="selection-picker-footer">
|
||||||
|
<van-button class="selection-picker-cancel" @click="closePopup('admin')">取消</van-button>
|
||||||
|
<van-button class="selection-picker-confirm" type="info" @click="chooseAdmin">确认选择</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<van-popup v-model="showProxy" class="selection-popup" position="bottom" round :close-on-click-overlay="false">
|
||||||
|
<div class="selection-proxy">
|
||||||
|
<div class="selection-sheet-head">
|
||||||
|
<span>代选福利</span>
|
||||||
|
<button type="button" class="selection-sheet-close" aria-label="关闭代选福利" @click="closePopup('proxy')"><van-icon name="cross"></van-icon></button>
|
||||||
|
</div>
|
||||||
|
<div class="selection-proxy-content" v-if="!proxyLoading">
|
||||||
|
<div class="selection-proxy-person">
|
||||||
|
<span class="selection-proxy-person-name">{{ proxyRow.userName || '-' }}</span>
|
||||||
|
<span class="selection-proxy-person-number">工号 {{ proxyRow.loginName || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="selection-proxy-section">
|
||||||
|
<div class="selection-proxy-title">{{ proxyProject.name || '福利选项' }}</div>
|
||||||
|
<div class="selection-proxy-description">{{ proxyProject.isCheckBox === 'radio' ? '请选择一项福利' : '请选择福利及份数' }}</div>
|
||||||
|
<van-radio-group v-if="proxyProject.isCheckBox === 'radio'" v-model="selectedRadioId">
|
||||||
|
<div class="selection-option selection-option-radio" v-for="option in proxyProject.options" :key="option.id"
|
||||||
|
:class="{'is-active': selectedRadioId === option.id}" @click="chooseRadio(option.id)">
|
||||||
|
<span class="selection-option-name">{{ option.optionName }}</span>
|
||||||
|
<van-radio :name="option.id" checked-color="#006dbb"></van-radio>
|
||||||
|
</div>
|
||||||
|
</van-radio-group>
|
||||||
|
<template v-else>
|
||||||
|
<div class="selection-option" v-for="option in proxyProject.options" :key="option.id" :class="{'is-active': option.selectNum > 0}">
|
||||||
|
<span class="selection-option-name">{{ option.optionName }}</span>
|
||||||
|
<van-stepper :value="option.selectNum" :min="0" :max="optionMax(option)" integer disable-input
|
||||||
|
@change="updateOptionNum(option, $event)"></van-stepper>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="selection-proxy-section">
|
||||||
|
<div class="selection-proxy-title">联系信息</div>
|
||||||
|
<van-field v-model="proxyForm.welfareMobile" type="tel" maxlength="11" label="福利电话" placeholder="请输入11位手机号" :border="false" required></van-field>
|
||||||
|
<van-field v-if="proxyProject.provideMode === 3" v-model="proxyForm.receiveAddress" type="textarea"
|
||||||
|
maxlength="100" rows="2" autosize label="收货地址" placeholder="请输入被代选人的收货地址" :border="false" required></van-field>
|
||||||
|
<div class="selection-proxy-hint"><van-icon name="info-o"></van-icon><span>请核对人员信息后提交</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="selection-proxy-section" v-if="proxyProject.signMode === 2">
|
||||||
|
<div class="selection-proxy-title">签字确认</div>
|
||||||
|
<h5-signature v-model="proxyForm.userSign"></h5-signature>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="selection-proxy-content selection-proxy-loading" v-else><van-loading>加载中...</van-loading></div>
|
||||||
|
<div class="selection-proxy-footer">
|
||||||
|
<van-button block type="info" :loading="formLoading" :disabled="proxyLoading || !hasSelection" @click="submitProxy">
|
||||||
|
{{ hasSubmittedBefore ? '确认修改' : '确认代选' }}
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: "#app",
|
||||||
|
store,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pageForm: {pageNumber: 1, pageSize: 10, projectId: null, isSelect: null, isSelectByAdmin: null, totalCount: 0},
|
||||||
|
searchKeyword: "",
|
||||||
|
projectColumns: [],
|
||||||
|
projectLoading: true,
|
||||||
|
statusColumns: [
|
||||||
|
{text: "全部", value: null},
|
||||||
|
{text: "已选择", value: true},
|
||||||
|
{text: "未选择", value: false}
|
||||||
|
],
|
||||||
|
adminColumns: [
|
||||||
|
{text: "全部", value: null},
|
||||||
|
{text: "是", value: true},
|
||||||
|
{text: "否", value: false}
|
||||||
|
],
|
||||||
|
tableData: [],
|
||||||
|
tableLoading: false,
|
||||||
|
listInFlight: false,
|
||||||
|
finished: false,
|
||||||
|
queryVersion: 0,
|
||||||
|
showProjectPicker: false,
|
||||||
|
showStatusPicker: false,
|
||||||
|
showAdminPicker: false,
|
||||||
|
draftProjectId: null,
|
||||||
|
draftStatus: null,
|
||||||
|
draftAdmin: null,
|
||||||
|
showProxy: false,
|
||||||
|
historyOwner: "",
|
||||||
|
proxyRow: {},
|
||||||
|
proxyProject: {},
|
||||||
|
proxyLoading: false,
|
||||||
|
proxyRequestVersion: 0,
|
||||||
|
selectedRadioId: null,
|
||||||
|
proxyForm: {welfareMobile: "", receiveAddress: "", userSign: ""},
|
||||||
|
hasSubmittedBefore: false,
|
||||||
|
formLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentProjectName() {
|
||||||
|
const current = this.projectColumns.find((item) => item.value === this.pageForm.projectId)
|
||||||
|
return current ? current.text : ""
|
||||||
|
},
|
||||||
|
currentStatusName() {
|
||||||
|
const current = this.statusColumns.find((item) => item.value === this.pageForm.isSelect)
|
||||||
|
return current ? current.text : "全部"
|
||||||
|
},
|
||||||
|
// 将管理员代选的 null/true/false 显示为全部/是/否,保留 false 的有效含义。
|
||||||
|
currentAdminName() {
|
||||||
|
const current = this.adminColumns.find((item) => item.value === this.pageForm.isSelectByAdmin)
|
||||||
|
return current ? current.text : "全部"
|
||||||
|
},
|
||||||
|
selectedOptions() {
|
||||||
|
if (!Array.isArray(this.proxyProject.options)) return []
|
||||||
|
if (this.proxyProject.isCheckBox === "radio") {
|
||||||
|
return this.proxyProject.options.filter((item) => item.id === this.selectedRadioId)
|
||||||
|
}
|
||||||
|
return this.proxyProject.options.filter((item) => Number(item.selectNum) > 0)
|
||||||
|
},
|
||||||
|
hasSelection() {
|
||||||
|
return this.selectedOptions.length > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.historyOwner = "welfare-selection-situation-" + this._uid
|
||||||
|
this.loadProjects()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.queryVersion++
|
||||||
|
this.proxyRequestVersion++
|
||||||
|
h5PopupHistory.unregisterOwner(this.historyOwner)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 项目列表不传年份,供移动端直接选择各年度福利项目。
|
||||||
|
loadProjects() {
|
||||||
|
this.$axios.post("/platform/welfare/common/list", {}).then((res) => {
|
||||||
|
if (res.code !== 0) {
|
||||||
|
this.$toast.fail(res.msg || "福利项目加载失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const columns = (res.data || []).map((item) => ({text: item.name, value: item.id}))
|
||||||
|
this.$set(this, "projectColumns", columns)
|
||||||
|
if (columns.length > 0) {
|
||||||
|
this.$set(this.pageForm, "projectId", columns[0].value)
|
||||||
|
this.doSearch()
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.$toast.fail("福利项目加载失败")
|
||||||
|
}).finally(() => {
|
||||||
|
this.$set(this, "projectLoading", false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// type 为 project/status/admin;打开对应筛选并注册全局历史回调,无返回值。
|
||||||
|
openPicker(type) {
|
||||||
|
if (type === "project" && this.projectColumns.length === 0) return
|
||||||
|
const picker = {
|
||||||
|
project: {visible: "showProjectPicker", draft: "draftProjectId", field: "projectId"},
|
||||||
|
status: {visible: "showStatusPicker", draft: "draftStatus", field: "isSelect"},
|
||||||
|
admin: {visible: "showAdminPicker", draft: "draftAdmin", field: "isSelectByAdmin"}
|
||||||
|
}[type]
|
||||||
|
if (!picker) return
|
||||||
|
// 点选仅更新弹框草稿;重新打开时还原已确认条件,取消和返回均不发起查询。
|
||||||
|
this.$set(this, picker.draft, this.pageForm[picker.field])
|
||||||
|
h5PopupHistory.open(this.historyOwner + "-" + type, this.historyOwner, () => {
|
||||||
|
this.$set(this, picker.visible, false)
|
||||||
|
})
|
||||||
|
this.$set(this, picker.visible, true)
|
||||||
|
},
|
||||||
|
// 仅由按钮发起一次历史回退,弹框显隐由全局历史回调同步,取消不触发查询。
|
||||||
|
closePopup(type) {
|
||||||
|
return h5PopupHistory.close(this.historyOwner + "-" + type)
|
||||||
|
},
|
||||||
|
// type 为 project/status/admin,value 为项目 ID 或 null/true/false;仅修改草稿,无返回值、不查询。
|
||||||
|
selectPickerOption(type, value) {
|
||||||
|
const draft = {project: "draftProjectId", status: "draftStatus", admin: "draftAdmin"}[type]
|
||||||
|
if (draft) this.$set(this, draft, value)
|
||||||
|
},
|
||||||
|
// 确认时从项目列表校验草稿值,仅变更有效项目后查询;重复点击由历史栈拦截。
|
||||||
|
chooseProject() {
|
||||||
|
const item = this.projectColumns.find((option) => option.value === this.draftProjectId)
|
||||||
|
if (!item || !this.closePopup("project")) return
|
||||||
|
if (this.pageForm.projectId === item.value) return
|
||||||
|
this.$set(this.pageForm, "projectId", item.value)
|
||||||
|
this.doSearch()
|
||||||
|
},
|
||||||
|
// 状态值 null/true/false 分别代表全部、已选择、未选择,false 也是有效筛选值。
|
||||||
|
chooseStatus() {
|
||||||
|
const item = this.statusColumns.find((option) => option.value === this.draftStatus)
|
||||||
|
if (!item || !this.closePopup("status")) return
|
||||||
|
if (this.pageForm.isSelect === item.value) return
|
||||||
|
this.$set(this.pageForm, "isSelect", item.value)
|
||||||
|
this.doSearch()
|
||||||
|
},
|
||||||
|
// 全部不限制代选来源;是/否分别传 true/false,确认变更后复用现有分页重置逻辑。
|
||||||
|
chooseAdmin() {
|
||||||
|
const item = this.adminColumns.find((option) => option.value === this.draftAdmin)
|
||||||
|
if (!item || !this.closePopup("admin")) return
|
||||||
|
if (this.pageForm.isSelectByAdmin === item.value) return
|
||||||
|
this.$set(this.pageForm, "isSelectByAdmin", item.value)
|
||||||
|
this.doSearch()
|
||||||
|
},
|
||||||
|
// 现有接口分别按姓名或工号过滤;含中文的关键词作为姓名,其余作为工号。
|
||||||
|
doSearch() {
|
||||||
|
this.queryVersion++
|
||||||
|
this.$set(this.pageForm, "pageNumber", 1)
|
||||||
|
this.$set(this.pageForm, "totalCount", 0)
|
||||||
|
this.$set(this, "tableData", [])
|
||||||
|
this.$set(this, "finished", false)
|
||||||
|
this.$set(this, "listInFlight", false)
|
||||||
|
this.$set(this, "tableLoading", false)
|
||||||
|
// 切换筛选条件或重新查询时从第一张卡片开始,加载下一页时保持当前位置。
|
||||||
|
if (this.$refs.personScroll) this.$refs.personScroll.scrollTop = 0
|
||||||
|
if (this.pageForm.projectId) this.pageData()
|
||||||
|
},
|
||||||
|
// pageForm 传项目 ID、分页信息及选择状态;管理员代选为 true/false,“全部”省略该字段。
|
||||||
|
// 关键词按姓名或工号提交;响应 data 为 {list, totalCount},分别用于追加人员和判断分页结束。
|
||||||
|
pageData() {
|
||||||
|
if (!this.pageForm.projectId || this.finished || this.listInFlight) return
|
||||||
|
const version = this.queryVersion
|
||||||
|
const pageForm = Object.assign({}, this.pageForm)
|
||||||
|
// “全部”省略该查询参数;false 必须原样传递,表示已有选择且非管理员代选。
|
||||||
|
if (pageForm.isSelectByAdmin === null || pageForm.isSelectByAdmin === undefined || pageForm.isSelectByAdmin === "") {
|
||||||
|
delete pageForm.isSelectByAdmin
|
||||||
|
}
|
||||||
|
const keyword = this.searchKeyword.trim()
|
||||||
|
if (keyword) {
|
||||||
|
if (/[\u3400-\u9fff]/.test(keyword)) pageForm.userName = keyword
|
||||||
|
else pageForm.loginName = keyword
|
||||||
|
}
|
||||||
|
this.$set(this, "listInFlight", true)
|
||||||
|
this.$set(this, "tableLoading", true)
|
||||||
|
this.$axios.post("/platform/welfare/selection/situation/pageData", {pageForm: JSON.stringify(pageForm)})
|
||||||
|
.then((res) => {
|
||||||
|
if (version !== this.queryVersion) return
|
||||||
|
if (res.code !== 0) {
|
||||||
|
this.$toast.fail(res.msg || "查询失败")
|
||||||
|
this.$set(this, "finished", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = res.data || {}
|
||||||
|
const rows = data.list || []
|
||||||
|
this.$set(this, "tableData", this.tableData.concat(rows))
|
||||||
|
this.$set(this.pageForm, "totalCount", data.totalCount || 0)
|
||||||
|
this.$set(this.pageForm, "pageNumber", pageForm.pageNumber + 1)
|
||||||
|
this.$set(this, "finished", rows.length === 0 || this.tableData.length >= this.pageForm.totalCount)
|
||||||
|
}).catch(() => {
|
||||||
|
if (version === this.queryVersion) {
|
||||||
|
this.$toast.fail("查询失败,请重试")
|
||||||
|
this.$set(this, "finished", true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (version !== this.queryVersion) return
|
||||||
|
this.$set(this, "listInFlight", false)
|
||||||
|
this.$set(this, "tableLoading", false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 打开单人代选,回显当前项目配置、已有选择和被代选人的福利电话。
|
||||||
|
openProxy(row) {
|
||||||
|
this.proxyRequestVersion++
|
||||||
|
const version = this.proxyRequestVersion
|
||||||
|
this.$set(this, "proxyRow", row)
|
||||||
|
this.$set(this, "proxyProject", {})
|
||||||
|
this.$set(this, "selectedRadioId", null)
|
||||||
|
this.$set(this, "proxyForm", {welfareMobile: "", receiveAddress: "", userSign: ""})
|
||||||
|
this.$set(this, "proxyLoading", true)
|
||||||
|
this.$set(this, "hasSubmittedBefore", false)
|
||||||
|
h5PopupHistory.open(this.historyOwner + "-proxy", this.historyOwner, () => {
|
||||||
|
this.proxyRequestVersion++
|
||||||
|
this.$set(this, "showProxy", false)
|
||||||
|
})
|
||||||
|
this.$set(this, "showProxy", true)
|
||||||
|
Promise.all([
|
||||||
|
this.$axios.post("/platform/welfare/common/projectInfo", {id: row.projectId}),
|
||||||
|
this.$axios.post("/platform/welfare/selection/situation/getUserSelection", {projectId: row.projectId, userId: row.userId}),
|
||||||
|
this.$axios.post("/platform/welfare/selection/situation/getWelfareMobileByUserId", {userId: row.userId})
|
||||||
|
]).then((results) => {
|
||||||
|
if (version !== this.proxyRequestVersion) return
|
||||||
|
if (results.some((item) => item.code !== 0)) {
|
||||||
|
this.$toast.fail("代选信息加载失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const project = results[0].data || {}
|
||||||
|
const selections = results[1].data || []
|
||||||
|
const options = project.options || []
|
||||||
|
options.forEach((option) => this.$set(option, "selectNum", 0))
|
||||||
|
this.$set(this, "proxyProject", project)
|
||||||
|
this.$set(this, "hasSubmittedBefore", selections.length > 0)
|
||||||
|
this.$set(this.proxyForm, "welfareMobile", results[2].data || "")
|
||||||
|
if (selections.length > 0) {
|
||||||
|
this.$set(this.proxyForm, "receiveAddress", selections[0].receiveAddress || "")
|
||||||
|
this.$set(this.proxyForm, "userSign", selections[0].userSign || "")
|
||||||
|
}
|
||||||
|
if (project.isCheckBox === "radio" && selections.length > 0) {
|
||||||
|
this.$set(this, "selectedRadioId", selections[0].selectOptionId)
|
||||||
|
} else {
|
||||||
|
selections.forEach((selection) => {
|
||||||
|
const option = options.find((item) => item.id === selection.selectOptionId)
|
||||||
|
if (option) this.$set(option, "selectNum", selection.selectNum || 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
if (version === this.proxyRequestVersion) this.$toast.fail("代选信息加载失败")
|
||||||
|
}).finally(() => {
|
||||||
|
if (version === this.proxyRequestVersion) this.$set(this, "proxyLoading", false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
chooseRadio(id) {
|
||||||
|
this.$set(this, "selectedRadioId", id)
|
||||||
|
},
|
||||||
|
optionMax(option) {
|
||||||
|
if (option.maxNum != null) return Number(option.maxNum)
|
||||||
|
if (this.proxyProject.singleOptionSupportMultipleSelection) return Number(this.proxyProject.multiSelectNum) || 1
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
// 单项上限和项目总份数共同限制代选数量。
|
||||||
|
updateOptionNum(option, value) {
|
||||||
|
const count = Number(value) || 0
|
||||||
|
const others = (this.proxyProject.options || []).reduce((sum, item) => {
|
||||||
|
return sum + (item.id === option.id ? 0 : Number(item.selectNum || 0))
|
||||||
|
}, 0)
|
||||||
|
const maxTotal = Number(this.proxyProject.multiSelectNum) || 1
|
||||||
|
if (others + count > maxTotal) {
|
||||||
|
this.$toast("此项目最多选择" + maxTotal + "份")
|
||||||
|
this.$set(option, "selectNum", Math.max(0, maxTotal - others))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(option, "selectNum", count)
|
||||||
|
},
|
||||||
|
// 仅提交被代选人、已选选项及必要联系信息,不改变现有后端规则。
|
||||||
|
submitProxy() {
|
||||||
|
if (!this.hasSelection || this.formLoading) return
|
||||||
|
const mobile = (this.proxyForm.welfareMobile || "").trim()
|
||||||
|
if (!/^1[3456789]\d{9}$/.test(mobile)) {
|
||||||
|
this.$toast.fail("请输入正确的福利电话")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.proxyProject.provideMode === 3 && !(this.proxyForm.receiveAddress || "").trim()) {
|
||||||
|
this.$toast.fail("请输入收货地址")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.proxyProject.signMode === 2 && !this.proxyForm.userSign) {
|
||||||
|
this.$toast.fail("请完成签字确认")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selections = this.selectedOptions.map((option) => ({
|
||||||
|
selectOptionId: option.id,
|
||||||
|
selectNum: this.proxyProject.isCheckBox === "radio" ? 1 : Number(option.selectNum),
|
||||||
|
receiveAddress: this.proxyForm.receiveAddress,
|
||||||
|
userSign: this.proxyForm.userSign
|
||||||
|
}))
|
||||||
|
this.$set(this, "formLoading", true)
|
||||||
|
this.$axios.post("/platform/welfare/selection/situation/confirmSelect", {
|
||||||
|
projectId: this.proxyRow.projectId,
|
||||||
|
userId: this.proxyRow.userId,
|
||||||
|
welfareMobile: mobile,
|
||||||
|
selections: JSON.stringify(selections)
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code !== 0) {
|
||||||
|
this.$toast.fail(res.msg || "代选失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$toast.success("代选成功")
|
||||||
|
h5PopupHistory.clearOwner(this.historyOwner)
|
||||||
|
this.doSearch()
|
||||||
|
}).catch(() => {
|
||||||
|
this.$toast.fail("代选失败,请重试")
|
||||||
|
}).finally(() => {
|
||||||
|
this.$set(this, "formLoading", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<!--#
|
||||||
|
}
|
||||||
|
#-->
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
<!--#
|
||||||
|
layout("/layouts/platform_h5.html"){
|
||||||
|
#-->
|
||||||
|
<style>
|
||||||
|
/* 固定视口使用 flex 分配剩余空间,首次进入和刷新均不依赖延时计算高度。 */
|
||||||
|
#app.welfare-statistics-page { height: 100%; height: 100dvh; min-height: 0; overflow: hidden; flex-direction: column; background: #f4f7fb; color: #182538; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
|
||||||
|
#app.welfare-statistics-page:not([v-cloak]) { display: flex; }
|
||||||
|
.welfare-statistics-page * { box-sizing: border-box; }
|
||||||
|
.welfare-statistics-page button { font-family: inherit; cursor: pointer; }
|
||||||
|
.welfare-statistics-page button:focus-visible { outline: 2px solid #0074d9; outline-offset: 2px; }
|
||||||
|
.welfare-statistics-page > .van-nav-bar, .ws-filters { flex-shrink: 0; background: #fff; }
|
||||||
|
.welfare-statistics-page .van-nav-bar__title { color: #182538; font-weight: 600; }
|
||||||
|
.welfare-statistics-page .van-nav-bar__text, .welfare-statistics-page .van-nav-bar .van-icon { color: #0074d9; }
|
||||||
|
.ws-filters { padding: 10px 12px 12px; border-bottom: 1px solid #e7edf5; }
|
||||||
|
.ws-filter-row { display: flex; gap: 8px; }
|
||||||
|
.ws-filter-row + .ws-filter-row { margin-top: 8px; }
|
||||||
|
.ws-filter { display: flex; align-items: center; justify-content: space-between; flex: 1; min-width: 0; height: 38px; padding: 0 10px; border: 1px solid #dde6f0; border-radius: 7px; background: #fff; color: #24364d; font-size: 14px; text-align: left; }
|
||||||
|
.ws-filter span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.ws-filter .van-icon { flex-shrink: 0; margin-left: 8px; color: #607895; }
|
||||||
|
.ws-filter-year { flex: 0 0 94px; }
|
||||||
|
.ws-filter-project { background: #f0f7ff; border-color: #d9e9fa; }
|
||||||
|
.ws-filter:disabled { opacity: .55; cursor: default; }
|
||||||
|
/* 顶部之外仅此容器滚动,边界滚动不传递给页面外层。 */
|
||||||
|
.ws-content { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; padding: 12px 12px calc(20px + env(safe-area-inset-bottom)); }
|
||||||
|
.ws-summary, .ws-union { padding: 16px 12px; background: #fff; border-radius: 10px; }
|
||||||
|
.ws-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); text-align: center; }
|
||||||
|
.ws-metric { min-width: 0; }
|
||||||
|
.ws-metric + .ws-metric { border-left: 1px solid #e8eef6; }
|
||||||
|
.ws-metric-label { display: block; margin-bottom: 5px; color: #718198; font-size: 12px; }
|
||||||
|
.ws-metric strong { display: block; font-size: 23px; line-height: 28px; font-variant-numeric: tabular-nums; }
|
||||||
|
.ws-blue { color: #0074d9; }
|
||||||
|
.ws-orange { color: #dc7600; }
|
||||||
|
.ws-summary .van-progress { margin-top: 18px; }
|
||||||
|
.ws-rate { margin-top: 8px; text-align: right; color: #718198; font-size: 12px; }
|
||||||
|
.ws-rate strong { margin-left: 6px; color: #0074d9; }
|
||||||
|
.ws-note { margin: 10px 0 0; color: #718198; font-size: 12px; line-height: 18px; }
|
||||||
|
.ws-section-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 18px 2px 10px; }
|
||||||
|
.ws-section-title h2 { margin: 0; font-size: 17px; }
|
||||||
|
.ws-section-title span { color: #718198; font-size: 12px; }
|
||||||
|
.ws-union + .ws-union { margin-top: 10px; }
|
||||||
|
.ws-union-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 13px; }
|
||||||
|
.ws-union-head h3 { margin: 0; font-size: 16px; line-height: 23px; overflow-wrap: anywhere; }
|
||||||
|
.ws-union-head span { flex-shrink: 0; padding-top: 2px; color: #0074d9; font-size: 14px; }
|
||||||
|
.ws-count-button { width: 100%; min-height: 48px; padding: 0; border: 0; background: transparent; }
|
||||||
|
.ws-count-button strong { display: inline; font-size: 20px; }
|
||||||
|
.ws-count-button .van-icon { font-size: 13px; vertical-align: 1px; }
|
||||||
|
.ws-union .ws-metric > strong { font-size: 20px; }
|
||||||
|
.ws-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 0; margin-top: 13px; padding: 10px 0; background: #f4f8fc; border-radius: 7px; text-align: center; }
|
||||||
|
.ws-option { padding: 0 6px; min-width: 0; font-size: 13px; line-height: 19px; overflow-wrap: anywhere; }
|
||||||
|
.ws-option:not(:nth-child(3n+1)) { border-left: 1px solid #dde7f3; }
|
||||||
|
.ws-option span { display: block; color: #718198; }
|
||||||
|
.ws-option strong { font-weight: 500; }
|
||||||
|
.ws-loading { padding: 60px 0; text-align: center; }
|
||||||
|
.ws-error { padding: 35px 15px; text-align: center; color: #718198; font-size: 14px; }
|
||||||
|
.ws-error .van-button { margin-top: 12px; }
|
||||||
|
/* 弹框也使用 flex;标题、筛选和查询固定,名单与选项列表分别独立滚动。 */
|
||||||
|
.welfare-statistics-page .ws-sheet { display: flex; flex-direction: column; overflow: hidden; padding-bottom: env(safe-area-inset-bottom); background: #fff; }
|
||||||
|
.ws-filter-sheet { max-height: 75%; max-height: 75dvh; }
|
||||||
|
.ws-sheet-toolbar { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; min-height: 54px; padding: 0 10px; border-bottom: 1px solid #edf1f7; }
|
||||||
|
.ws-sheet-toolbar strong { font-size: 16px; }
|
||||||
|
.ws-sheet-toolbar button { min-height: 44px; padding: 0 10px; border: 0; background: transparent; color: #0074d9; font-size: 14px; }
|
||||||
|
.ws-sheet-toolbar button:first-child { color: #718198; }
|
||||||
|
.ws-choices { min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; padding: 8px 16px 16px; }
|
||||||
|
.ws-choice { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; min-height: 48px; padding: 12px; border: 0; border-radius: 7px; background: #fff; color: #24364d; text-align: left; font-size: 14px; line-height: 21px; }
|
||||||
|
.ws-choice.is-selected { background: #edf6ff; color: #0074d9; }
|
||||||
|
.ws-choice .van-icon { flex-shrink: 0; font-size: 18px; }
|
||||||
|
.ws-detail-sheet { height: 82%; height: 82dvh; }
|
||||||
|
.ws-detail-head { flex-shrink: 0; display: flex; justify-content: space-between; gap: 12px; padding: 20px 16px 14px; }
|
||||||
|
.ws-detail-head h2 { margin: 0 0 5px; font-size: 20px; line-height: 27px; overflow-wrap: anywhere; }
|
||||||
|
.ws-detail-head p { margin: 0; color: #718198; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
|
||||||
|
.ws-close { flex-shrink: 0; align-self: flex-start; width: 36px; height: 36px; margin: -4px -8px 0 0; border: 0; background: transparent; color: #718198; font-size: 22px; }
|
||||||
|
.ws-tabs { flex-shrink: 0; display: flex; border-bottom: 1px solid #e7edf5; }
|
||||||
|
.ws-tab { position: relative; flex: 1; min-height: 44px; padding: 8px 4px; border: 0; background: #fff; color: #718198; font-size: 15px; }
|
||||||
|
.ws-tab.is-selected { color: #0074d9; font-weight: 600; }
|
||||||
|
.ws-tab.ws-tab-unselected.is-selected { color: #dc7600; }
|
||||||
|
.ws-tab.is-selected:after { position: absolute; right: 25%; bottom: 0; left: 25%; height: 3px; border-radius: 3px; background: currentColor; content: ""; }
|
||||||
|
.ws-search { display: flex; align-items: center; gap: 8px; flex-shrink: 0; padding: 12px 16px 6px; }
|
||||||
|
.ws-search .van-search { flex: 1; min-width: 0; padding: 0; }
|
||||||
|
.ws-search .van-search__content { border: 1px solid #e1e9f3; border-radius: 7px; background: #f4f7fb; }
|
||||||
|
.ws-search .van-button { height: 36px; border-radius: 6px; }
|
||||||
|
.ws-detail-count { flex-shrink: 0; padding: 5px 16px 10px; color: #718198; font-size: 12px; text-align: right; }
|
||||||
|
.ws-person-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; padding: 0 16px 16px; }
|
||||||
|
.ws-person { margin-bottom: 8px; padding: 12px; border: 1px solid #e7edf5; border-radius: 8px; }
|
||||||
|
.ws-person-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
|
||||||
|
.ws-person-head > div { min-width: 0; overflow-wrap: anywhere; }
|
||||||
|
.ws-person-name { font-size: 16px; font-weight: 600; line-height: 24px; }
|
||||||
|
.ws-person-number { display: inline-block; margin-left: 6px; color: #718198; font-size: 13px; }
|
||||||
|
.ws-person-status { flex-shrink: 0; padding-top: 3px; font-size: 12px; }
|
||||||
|
.ws-person p { margin: 5px 0 0; color: #718198; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
|
||||||
|
.ws-person .ws-person-option { color: #395570; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="app" class="welfare-statistics-page" v-cloak>
|
||||||
|
<van-nav-bar title="福利统计" left-text="返回" left-arrow @click-left="historyBack"></van-nav-bar>
|
||||||
|
<div class="ws-filters">
|
||||||
|
<div class="ws-filter-row">
|
||||||
|
<button class="ws-filter ws-filter-year" :disabled="bootLoading || !!bootError" @click="openFilter('year')" aria-label="选择年份"><span>{{ filters.year }}年</span><van-icon name="arrow-down"></van-icon></button>
|
||||||
|
<button class="ws-filter ws-filter-project" :disabled="bootLoading || !!bootError" @click="openFilter('projectId')" aria-label="选择福利项目"><span>{{ projectName || '选择福利项目' }}</span><van-icon name="arrow-down"></van-icon></button>
|
||||||
|
</div>
|
||||||
|
<div class="ws-filter-row">
|
||||||
|
<button class="ws-filter" :disabled="bootLoading || !!bootError || branchOnly" @click="openFilter('unionId')" aria-label="选择工会"><span>{{ unionName }}</span><van-icon name="arrow-down"></van-icon></button>
|
||||||
|
<button class="ws-filter" :disabled="bootLoading || !!bootError" @click="openFilter('method')" aria-label="选择统计方式"><span>{{ methodName }}</span><van-icon name="arrow-down"></van-icon></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main class="ws-content" ref="contentScroll" aria-label="福利统计内容">
|
||||||
|
<div v-if="bootLoading || tableLoading" class="ws-loading"><van-loading size="24px" vertical>正在加载统计</van-loading></div>
|
||||||
|
<div v-else-if="bootError || tableError" class="ws-error"><div>{{ bootError || tableError }}</div><van-button size="small" plain type="info" @click="retryStatistics">重新加载</van-button></div>
|
||||||
|
<van-empty v-else-if="!filters.projectId" description="该年份暂无福利项目"></van-empty>
|
||||||
|
<template v-else>
|
||||||
|
<section class="ws-summary" aria-label="人数汇总">
|
||||||
|
<div class="ws-metrics">
|
||||||
|
<div class="ws-metric"><span class="ws-metric-label">福利人数</span><strong>{{ totals.teacherSum }}</strong></div>
|
||||||
|
<div class="ws-metric"><span class="ws-metric-label">已选择</span><strong class="ws-blue">{{ totals.selectedNum }}</strong></div>
|
||||||
|
<div class="ws-metric"><span class="ws-metric-label">未选择</span><strong class="ws-orange">{{ totals.unSelectedNum }}</strong></div>
|
||||||
|
</div>
|
||||||
|
<van-progress :percentage="percentage(totals)" :show-pivot="false" stroke-width="8" color="#0074d9" track-color="#e7f1fa"></van-progress>
|
||||||
|
<div class="ws-rate">已选比例<strong>{{ percentage(totals) }}%</strong></div>
|
||||||
|
<p v-if="filters.method !== ''" class="ws-note">已选按{{ methodName }}统计,未选为尚未选择人员。</p>
|
||||||
|
</section>
|
||||||
|
<div class="ws-section-title"><h2>分工会统计</h2><span>点击已选、未选人数查看名单</span></div>
|
||||||
|
<van-empty v-if="!tableData.length" description="暂无工会统计"></van-empty>
|
||||||
|
<article v-for="row in tableData" :key="row.id" class="ws-union">
|
||||||
|
<div class="ws-union-head"><h3>{{ row.name }}</h3><span>{{ percentage(row) }}%</span></div>
|
||||||
|
<div class="ws-metrics">
|
||||||
|
<div class="ws-metric"><span class="ws-metric-label">总人数</span><strong>{{ row.teacherSum || 0 }}</strong></div>
|
||||||
|
<div class="ws-metric"><button class="ws-count-button ws-blue" @click="openDetail(row, 'selected')" :aria-label="row.name + '已选择人员'"><span class="ws-metric-label">已选</span><strong>{{ row.selectedNum || 0 }}</strong><van-icon name="arrow"></van-icon></button></div>
|
||||||
|
<div class="ws-metric"><button class="ws-count-button ws-orange" @click="openDetail(row, 'unselected')" :aria-label="row.name + '未选择人员'"><span class="ws-metric-label">未选</span><strong>{{ row.unSelectedNum || 0 }}</strong><van-icon name="arrow"></van-icon></button></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="optionColumns.length" class="ws-options">
|
||||||
|
<div v-for="column in optionColumns" :key="column.prop" class="ws-option"><span>{{ column.label }}</span><strong>{{ row[column.prop] || 0 }}人</strong></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<van-popup :value="showFilter" position="bottom" round class="ws-sheet ws-filter-sheet" :close-on-click-overlay="false" @click-overlay="closeFilter">
|
||||||
|
<div class="ws-sheet-toolbar"><button @click="closeFilter">取消</button><strong>{{ filterTitle }}</strong><button @click="confirmFilter">确认</button></div>
|
||||||
|
<div class="ws-choices" role="radiogroup" :aria-label="filterTitle">
|
||||||
|
<button v-for="option in filterOptions" :key="String(option.value)" class="ws-choice" :class="{'is-selected': draftValue === option.value}" role="radio" :aria-checked="draftValue === option.value" @click="chooseDraft(option.value)"><span>{{ option.text }}</span><van-icon :name="draftValue === option.value ? 'checked' : 'circle'"></van-icon></button>
|
||||||
|
<van-empty v-if="!filterOptions.length" description="暂无可选项目"></van-empty>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<van-popup :value="showDetail" position="bottom" round class="ws-sheet ws-detail-sheet" :close-on-click-overlay="false" @click-overlay="closeDetail" @opened="checkDetailList">
|
||||||
|
<div class="ws-detail-head"><div><h2>{{ detailUnion.name }}</h2><p>{{ projectName }}</p></div><button class="ws-close" aria-label="关闭人员名单" @click="closeDetail"><van-icon name="cross"></van-icon></button></div>
|
||||||
|
<div class="ws-tabs" role="tablist" aria-label="人员选择状态">
|
||||||
|
<button class="ws-tab" :class="{'is-selected': detailTab === 'selected'}" role="tab" :aria-selected="detailTab === 'selected'" @click="changeDetailTab('selected')">已选择 {{ detailUnion.selectedNum || 0 }}</button>
|
||||||
|
<button class="ws-tab ws-tab-unselected" :class="{'is-selected': detailTab === 'unselected'}" role="tab" :aria-selected="detailTab === 'unselected'" @click="changeDetailTab('unselected')">未选择 {{ detailUnion.unSelectedNum || 0 }}</button>
|
||||||
|
</div>
|
||||||
|
<div class="ws-search"><van-search :value="searchInput" placeholder="工号或姓名" @input="setSearchInput" @search="searchDetail" @clear="clearSearch"></van-search><van-button type="info" @click="searchDetail">查询</van-button></div>
|
||||||
|
<div class="ws-detail-count">{{ detailLoading && !detailRows.length ? '正在查询…' : '共 ' + detailTotal + ' 人' }}</div>
|
||||||
|
<div class="ws-person-scroll" ref="personScroll">
|
||||||
|
<van-list ref="personList" :loading="detailLoading" :finished="detailFinished" :immediate-check="false" :error="!!detailError" finished-text="没有更多了" @load="loadDetail">
|
||||||
|
<article v-for="(person, index) in detailRows" :key="String(person.loginName) + '-' + index" class="ws-person">
|
||||||
|
<div class="ws-person-head"><div><span class="ws-person-name">{{ person.userName || '未填写姓名' }}</span><span class="ws-person-number">{{ person.loginName }}</span></div><span class="ws-person-status" :class="detailTab === 'selected' ? 'ws-blue' : 'ws-orange'">● {{ detailTab === 'selected' ? '已选择' : '未选择' }}</span></div>
|
||||||
|
<p>{{ person.welfareUnitName || '未填写单位' }}</p>
|
||||||
|
<p v-if="detailTab === 'selected'" class="ws-person-option">{{ person.selectOptionName || '—' }}<span v-if="person.selectByAdminName"> · {{ person.selectByAdminName }}</span></p>
|
||||||
|
</article>
|
||||||
|
<template #error><div @click.stop="loadDetail">{{ detailError }},点击重试</div></template>
|
||||||
|
</van-list>
|
||||||
|
<van-empty v-if="!detailLoading && !detailError && !detailRows.length" description="暂无符合条件的人员"></van-empty>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
store: store,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
filters: {year: new Date().getFullYear(), projectId: '', unionId: '', method: ''},
|
||||||
|
projects: [], unions: [], branchOnly: false,
|
||||||
|
bootLoading: false, bootError: '', bootVersion: 0,
|
||||||
|
tableData: [], optionColumns: [], tableLoading: false, tableError: '', tableVersion: 0,
|
||||||
|
popupOwner: 'h5-welfare-statistics', showFilter: false, filterType: '', draftValue: '',
|
||||||
|
showDetail: false, detailUnion: {}, detailTab: 'selected', searchInput: '', searchKeyword: '',
|
||||||
|
detailRows: [], detailTotal: 0, detailPage: 1, detailLoading: false, detailFinished: false, detailError: '', detailVersion: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
yearProjects() { return this.projects.filter((item) => Number(item.year) === this.filters.year) },
|
||||||
|
projectName() { return (this.projects.find((item) => item.id === this.filters.projectId) || {}).name || '' },
|
||||||
|
unionName() { return (this.unions.find((item) => item.id === this.filters.unionId) || {}).name || '全部工会' },
|
||||||
|
methodName() { return this.filters.method === '' ? '全部方式' : (this.filters.method ? '管理员代选' : '个人选择') },
|
||||||
|
filterTitle() { return {year: '选择年份', projectId: '选择福利项目', unionId: '选择工会', method: '选择方式'}[this.filterType] || '' },
|
||||||
|
filterOptions() {
|
||||||
|
if (this.filterType === 'year') {
|
||||||
|
const years = Array.from(new Set([new Date().getFullYear()].concat(this.projects.map((item) => Number(item.year)).filter((year) => year > 0))))
|
||||||
|
return years.sort((a, b) => b - a).map((year) => ({value: year, text: year + '年'}))
|
||||||
|
}
|
||||||
|
if (this.filterType === 'projectId') return this.yearProjects.map((item) => ({value: item.id, text: item.name}))
|
||||||
|
if (this.filterType === 'unionId') return (this.branchOnly ? [] : [{value: '', text: '全部工会'}]).concat(this.unions.map((item) => ({value: item.id, text: item.name})))
|
||||||
|
return [{value: '', text: '全部方式'}, {value: false, text: '个人选择'}, {value: true, text: '管理员代选'}]
|
||||||
|
},
|
||||||
|
totals() {
|
||||||
|
return this.tableData.reduce((total, row) => {
|
||||||
|
Object.keys(total).forEach((key) => { total[key] += Number(row[key]) || 0 })
|
||||||
|
return total
|
||||||
|
}, {teacherSum: 0, selectedNum: 0, unSelectedNum: 0})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() { this.init() },
|
||||||
|
beforeDestroy() {
|
||||||
|
// 失效所有未完成请求,并移除本页面的弹框历史回调,防止离页后写回旧组件。
|
||||||
|
this.$set(this, 'bootVersion', this.bootVersion + 1)
|
||||||
|
this.$set(this, 'tableVersion', this.tableVersion + 1)
|
||||||
|
this.$set(this, 'detailVersion', this.detailVersion + 1)
|
||||||
|
h5PopupHistory.unregisterOwner(this.popupOwner)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
percentage(row) {
|
||||||
|
const total = Number(row.teacherSum) || 0
|
||||||
|
return total > 0 ? Math.min(100, Math.max(0, Math.round((Number(row.selectedNum) || 0) / total * 1000) / 10)) : 0
|
||||||
|
},
|
||||||
|
// 项目接口返回 id/name/year 数组;工会接口沿用 PC 分工会主席的默认范围。
|
||||||
|
init() {
|
||||||
|
const version = this.bootVersion + 1
|
||||||
|
this.$set(this, 'bootVersion', version)
|
||||||
|
this.$set(this, 'bootLoading', true)
|
||||||
|
this.$set(this, 'bootError', '')
|
||||||
|
const branchOnly = !this.$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN', 'SCHOOL_UNION_WELFARE_ADMIN']) && this.$auth.hasRoleOr(['BRANCH_UNION_CHAIRMAN'])
|
||||||
|
const userUnion = this.$store.state.user.union || {}
|
||||||
|
this.$set(this, 'branchOnly', branchOnly)
|
||||||
|
// 无所属工会时不能退化为全工会查询,显示可重试提示。
|
||||||
|
if (branchOnly && !userUnion.id) {
|
||||||
|
this.$set(this, 'bootError', '当前账号未配置所属工会,请联系管理员')
|
||||||
|
this.$set(this, 'bootLoading', false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Promise.all([
|
||||||
|
this.$axios.post('/platform/h5/welfare/statistics/projects', {}),
|
||||||
|
this.$axios.post('/open/common/unionOptions', branchOnly ? {unionId: userUnion.id} : {})
|
||||||
|
]).then((results) => {
|
||||||
|
if (version !== this.bootVersion) return
|
||||||
|
if (results.some((res) => res.code !== 0)) throw new Error('筛选条件加载失败,请重试')
|
||||||
|
this.$set(this, 'projects', results[0].data || [])
|
||||||
|
this.$set(this, 'unions', results[1].data || [])
|
||||||
|
this.$set(this.filters, 'unionId', branchOnly ? userUnion.id : '')
|
||||||
|
this.$set(this.filters, 'projectId', this.yearProjects.length ? this.yearProjects[0].id : '')
|
||||||
|
this.loadStatistics()
|
||||||
|
}).catch((error) => {
|
||||||
|
if (version === this.bootVersion) this.$set(this, 'bootError', error.message || '筛选条件加载失败,请重试')
|
||||||
|
}).finally(() => {
|
||||||
|
if (version === this.bootVersion) this.$set(this, 'bootLoading', false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
retryStatistics() { if (this.bootError) this.init(); else this.loadStatistics() },
|
||||||
|
// 筛选使用独立草稿;取消、遮罩和返回键只关闭弹框,不改变查询条件。
|
||||||
|
openFilter(type) {
|
||||||
|
if (this.showFilter || this.showDetail || this.bootLoading || this.bootError) return
|
||||||
|
this.$set(this, 'filterType', type)
|
||||||
|
this.$set(this, 'draftValue', this.filters[type])
|
||||||
|
this.$set(this, 'showFilter', true)
|
||||||
|
h5PopupHistory.open(this.popupOwner + '-filter', this.popupOwner, () => { this.$set(this, 'showFilter', false) })
|
||||||
|
},
|
||||||
|
chooseDraft(value) { this.$set(this, 'draftValue', value) },
|
||||||
|
closeFilter() { h5PopupHistory.close(this.popupOwner + '-filter') },
|
||||||
|
confirmFilter() {
|
||||||
|
if (!this.filterOptions.some((item) => item.value === this.draftValue)) { this.closeFilter(); return }
|
||||||
|
if (!h5PopupHistory.close(this.popupOwner + '-filter')) return
|
||||||
|
if (this.filters[this.filterType] === this.draftValue) return
|
||||||
|
this.$set(this.filters, this.filterType, this.draftValue)
|
||||||
|
// 年份变化后只选该年份的第一个项目;普通项目切换绝不重新覆盖为默认值。
|
||||||
|
if (this.filterType === 'year') this.$set(this.filters, 'projectId', this.yearProjects.length ? this.yearProjects[0].id : '')
|
||||||
|
this.loadStatistics()
|
||||||
|
},
|
||||||
|
// 参数 projectId 必填、unionId 可选;method 空时省略,false 必须保留为个人选择。
|
||||||
|
statisticsParams() {
|
||||||
|
const params = {projectId: this.filters.projectId, unionId: this.filters.unionId}
|
||||||
|
if (this.filters.method !== '') params.isSelectByAdmin = this.filters.method
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
loadStatistics() {
|
||||||
|
const version = this.tableVersion + 1
|
||||||
|
this.$set(this, 'tableVersion', version)
|
||||||
|
this.$set(this, 'tableData', [])
|
||||||
|
this.$set(this, 'optionColumns', [])
|
||||||
|
this.$set(this, 'tableError', '')
|
||||||
|
if (this.$refs.contentScroll) this.$refs.contentScroll.scrollTop = 0
|
||||||
|
if (!this.filters.projectId) { this.$set(this, 'tableLoading', false); return }
|
||||||
|
this.$set(this, 'tableLoading', true)
|
||||||
|
this.$axios.post('/platform/h5/welfare/statistics/pageData', this.statisticsParams()).then((res) => {
|
||||||
|
if (version !== this.tableVersion) return
|
||||||
|
if (res.code !== 0) throw new Error(res.msg || '统计加载失败')
|
||||||
|
this.$set(this, 'tableData', res.data.tableList || [])
|
||||||
|
const fixed = ['name', 'teacherSum', 'selectedNum', 'unSelectedNum']
|
||||||
|
this.$set(this, 'optionColumns', (res.data.tableColumn || []).filter((column) => fixed.indexOf(column.prop) === -1))
|
||||||
|
}).catch((error) => {
|
||||||
|
if (version === this.tableVersion) this.$set(this, 'tableError', error.message || '统计加载失败,请重试')
|
||||||
|
}).finally(() => {
|
||||||
|
if (version === this.tableVersion) this.$set(this, 'tableLoading', false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openDetail(row, tab) {
|
||||||
|
if (this.showDetail || this.showFilter) return
|
||||||
|
this.$set(this, 'detailUnion', Object.assign({}, row))
|
||||||
|
this.$set(this, 'detailTab', tab)
|
||||||
|
this.$set(this, 'searchInput', '')
|
||||||
|
this.$set(this, 'searchKeyword', '')
|
||||||
|
this.$set(this, 'showDetail', true)
|
||||||
|
h5PopupHistory.open(this.popupOwner + '-detail', this.popupOwner, () => {
|
||||||
|
this.$set(this, 'showDetail', false)
|
||||||
|
this.$set(this, 'detailVersion', this.detailVersion + 1)
|
||||||
|
this.$set(this, 'detailLoading', false)
|
||||||
|
})
|
||||||
|
this.resetDetail()
|
||||||
|
},
|
||||||
|
closeDetail() { h5PopupHistory.close(this.popupOwner + '-detail') },
|
||||||
|
changeDetailTab(tab) {
|
||||||
|
if (tab === this.detailTab) return
|
||||||
|
this.$set(this, 'detailTab', tab)
|
||||||
|
this.resetDetail()
|
||||||
|
},
|
||||||
|
setSearchInput(value) { this.$set(this, 'searchInput', value) },
|
||||||
|
searchDetail() {
|
||||||
|
this.$set(this, 'searchKeyword', this.searchInput.trim())
|
||||||
|
this.resetDetail()
|
||||||
|
},
|
||||||
|
clearSearch() { this.$set(this, 'searchInput', ''); this.searchDetail() },
|
||||||
|
resetDetail() {
|
||||||
|
// 切换工会、状态或查询词时使旧请求失效,并从第一页重新读取。
|
||||||
|
this.$set(this, 'detailVersion', this.detailVersion + 1)
|
||||||
|
this.$set(this, 'detailRows', [])
|
||||||
|
this.$set(this, 'detailTotal', 0)
|
||||||
|
this.$set(this, 'detailPage', 1)
|
||||||
|
this.$set(this, 'detailFinished', false)
|
||||||
|
this.$set(this, 'detailLoading', false)
|
||||||
|
this.$set(this, 'detailError', '')
|
||||||
|
if (this.$refs.personScroll) this.$refs.personScroll.scrollTop = 0
|
||||||
|
this.loadDetail()
|
||||||
|
},
|
||||||
|
checkDetailList() { if (this.showDetail && this.$refs.personList) this.$refs.personList.check() },
|
||||||
|
// 两个接口均返回 Pagination 的 list/totalCount;未选接口不传选择方式。
|
||||||
|
loadDetail() {
|
||||||
|
if (!this.showDetail || this.detailLoading || this.detailFinished) return
|
||||||
|
const version = this.detailVersion
|
||||||
|
const params = {projectId: this.filters.projectId, unionId: this.detailUnion.id, pageNumber: this.detailPage, pageSize: 50, searchKeyword: this.searchKeyword}
|
||||||
|
const selected = this.detailTab === 'selected'
|
||||||
|
if (selected && this.filters.method !== '') params.isSelectByAdmin = this.filters.method
|
||||||
|
this.$set(this, 'detailLoading', true)
|
||||||
|
this.$set(this, 'detailError', '')
|
||||||
|
this.$axios.post('/platform/h5/welfare/statistics/' + (selected ? 'selectedUnionUserPageData' : 'unSelectedUnionUserPageData'), params).then((res) => {
|
||||||
|
if (version !== this.detailVersion) return
|
||||||
|
if (res.code !== 0) throw new Error(res.msg || '人员加载失败')
|
||||||
|
const rows = res.data.list || []
|
||||||
|
this.$set(this, 'detailRows', this.detailRows.concat(rows))
|
||||||
|
this.$set(this, 'detailTotal', Number(res.data.totalCount) || 0)
|
||||||
|
this.$set(this, 'detailFinished', rows.length === 0 || this.detailRows.length >= this.detailTotal)
|
||||||
|
this.$set(this, 'detailPage', this.detailPage + 1)
|
||||||
|
}).catch((error) => {
|
||||||
|
if (version === this.detailVersion) this.$set(this, 'detailError', error.message || '人员加载失败')
|
||||||
|
}).finally(() => {
|
||||||
|
if (version !== this.detailVersion) return
|
||||||
|
this.$set(this, 'detailLoading', false)
|
||||||
|
// 首屏不足一屏时继续检查;失败等待手动重试,避免自动请求循环。
|
||||||
|
if (!this.detailError) this.$nextTick(() => { this.checkDetailList() })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<!--#
|
||||||
|
}
|
||||||
|
#-->
|
||||||
Reference in New Issue
Block a user