Compare commits
16
Commits
50f82e05e5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
743e6c5428 | ||
|
|
09e955fb38 | ||
|
|
7f578e1f9e | ||
|
|
d8f6298dd8 | ||
|
|
6b0586b638 | ||
|
|
15aef647d1 | ||
|
|
f3021514fd | ||
|
|
4856cd5ddb | ||
|
|
a09c6ae2c9 | ||
|
|
d197c78b9c | ||
|
|
dbca87f09c | ||
|
|
38b1a09e11 | ||
|
|
5cecb3b185 | ||
|
|
fc727c690a | ||
|
|
063e22491a | ||
|
|
48ebcc148d |
@@ -166,7 +166,15 @@ public class SysHomeController {
|
||||
@Ok("json")
|
||||
public Result listHomeActivity() {
|
||||
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();
|
||||
List<Sys_home_activity> allowActivityList = new ArrayList<>();
|
||||
// 今天的时间
|
||||
|
||||
@@ -97,8 +97,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
||||
Map<String, Date> doctoralData = pullPostDoctoral();
|
||||
|
||||
Date nowDate = new Date();
|
||||
String appId = "1926887238437818370";
|
||||
String secret = "32df31fcf4fc43f9a647ee1f47134f36";
|
||||
String appId = "XXXXXXXXXXXXXXXXXXXXX";
|
||||
String secret = "XXXXXXXXXXXXXXXXXXXX";
|
||||
|
||||
List<JSONObject> rawDataList = new ArrayList<>();
|
||||
int skip = 0;
|
||||
@@ -280,8 +280,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
||||
|
||||
@Override
|
||||
public Map<String, Date> pullPostDoctoral() {
|
||||
String appId = "1926887238437818370";
|
||||
String secret = "32df31fcf4fc43f9a647ee1f47134f36";
|
||||
String appId = "XXXXXXXXXXXXXXXXXXXXXX";
|
||||
String secret = "XXXXXXXXXXXXXXXXXXXXXX";
|
||||
|
||||
List<JSONObject> allUsers = new ArrayList<>();
|
||||
int skip = 0;
|
||||
|
||||
+1
-5
@@ -112,11 +112,7 @@ public class ActivityCultureUserStatisticsController {
|
||||
if (Strings.isNotBlank(page.getSearchName()) && Strings.isNotBlank(page.getSearchKeyword())) {
|
||||
cnd.where().andLike(page.getSearchName(), page.getSearchKeyword());
|
||||
}
|
||||
if (StrUtil.isNotBlank(page.getPageOrderName()) && StrUtil.isNotBlank(page.getPageOrderBy())) {
|
||||
cnd.orderBy(page.getPageOrderName(), page.getPageOrderBy().equalsIgnoreCase("ascending") ? "ASC" : "DESC");
|
||||
} else {
|
||||
cnd.desc("atp.applyDateTime");
|
||||
}
|
||||
activityCultureApplyUserService.applyUserStatisticsSort(cnd, page);
|
||||
sql.setCondition(cnd);
|
||||
Pagination pagination = activityCultureApplyUserService.listPageMap(page.getPageNumber(), page.getPageSize(), sql);
|
||||
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.service.BaseService;
|
||||
import com.budwk.app.zhgh.activity.culture.models.ActivityTissuePerson;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
@@ -19,6 +20,14 @@ public interface ActivityCultureApplyUserService extends BaseService<ActivityTis
|
||||
boolean isEnrolled,
|
||||
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;
|
||||
}
|
||||
|
||||
@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
|
||||
public void signUp(String activityId, List<String> personIds, JSONObject dynamicFormParam) {
|
||||
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
|
||||
@ApiOperation("活动报名")
|
||||
@SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR)
|
||||
@@ -529,37 +535,7 @@ public class FamilyActivityApplyController {
|
||||
public Result doSignUp(FamilyUser familyUser) {
|
||||
try {
|
||||
lock.lock();
|
||||
FamilyActivity activity = dao.fetch(FamilyActivity.class, familyUser.getActivityId());
|
||||
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,"该活动您所在的分工会名额不足");
|
||||
}
|
||||
// 家属、名额和时间冲突由service在数据库锁及事务内统一校验,避免普通报名与快速报名规则分叉。
|
||||
FamilyUser finishUser = familyActivityService.doSignUp(familyUser);
|
||||
if(finishUser.getState() == 2) {
|
||||
return Result.success("正式报名已满,您已列为替补,如有老师放弃报名,将会按先后顺序依次替补。请您自行关注后续结果。谢谢!");
|
||||
|
||||
+9
@@ -13,6 +13,8 @@ import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
@@ -144,11 +146,18 @@ public class FamilyActivityController {
|
||||
return Result.success().addData(dataMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存或发布活动配置,业务校验由service执行。
|
||||
* @param activity 活动及子活动列表;familyCountScope为1按类型、2按活动,timeConflictCheck为重叠校验开关,
|
||||
* onlyKey为各类型共有且必填的字段编码,familyMaxCount为活动累计上限
|
||||
* @return Result JSON,code为0表示保存成功,配置不完整时返回具体业务错误
|
||||
*/
|
||||
@At
|
||||
@Ok("json:full")
|
||||
@ApiOperation("亲子活动新增/修改")
|
||||
@SaCheckPermission("family.manage")
|
||||
@SLog(tag = "亲子活动-活动管理", msg = "新增/修改活动")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doHandle(FamilyActivity activity) {
|
||||
if (StrUtil.isBlank(activity.getId())) {
|
||||
familyActivityManageService.add(activity, null);
|
||||
|
||||
+30
-44
@@ -1,8 +1,10 @@
|
||||
package com.budwk.app.zhgh.activity.family.controller.manage;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
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.param.PageForm;
|
||||
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.Param;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -118,59 +119,44 @@ public class FamilyAdjustController {
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param activityId 当前活动ID
|
||||
* @param oldCourseId 人员原报名子活动ID
|
||||
* @param newCourseId 同活动内的目标子活动ID
|
||||
* @param userId 被调整教职工ID,按该人员而非管理员校验家属和时间规则
|
||||
* @return Result JSON;code为0表示调整成功,失败时msg说明原因且保留原报名
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("人员调整")
|
||||
@SaCheckPermission("family.adjust")
|
||||
@SLog(tag = "亲子活动-人员调整", msg = "人员调整")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result adjust(String activityId, String oldCourseId, String newCourseId, String userId) {
|
||||
|
||||
Cnd oldCnd = Cnd.where("activityId", "=", activityId)
|
||||
.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();
|
||||
try {
|
||||
familyActivityManageService.adjustSignUp(activityId, oldCourseId, newCourseId, userId);
|
||||
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
|
||||
@ApiOperation("删除报名人员")
|
||||
@SaCheckPermission("family.adjust")
|
||||
@SLog(tag = "亲子活动-人员调整", msg = "删除报名人员")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result deleteSignUser(String activityId, String courseId, String 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();
|
||||
try {
|
||||
familyActivityManageService.deleteSignUser(activityId, courseId, userId);
|
||||
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)
|
||||
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
|
||||
@Comment("判断家属数量的唯一标识")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||
|
||||
+22
-2
@@ -76,6 +76,25 @@ public interface FamilyActivityService extends BaseService<FamilyActivity> {
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 校验当前用户是否可以修改指定课程的报名信息。
|
||||
* 打开编辑表单前校验当前用户报名归属、报名时间和人员范围。
|
||||
* 家属、人数和时间冲突在保存新数据时重新校验,允许历史冲突记录进入表单整改。
|
||||
*
|
||||
* @param courseId 课程ID;当前用户必须已经报名该课程
|
||||
* @return 无返回值;校验不通过时抛出包含具体原因的业务异常
|
||||
@@ -166,7 +186,7 @@ public interface FamilyActivityService extends BaseService<FamilyActivity> {
|
||||
boolean isSignFull(FamilyCourse course, Integer currentFamilyNumber);
|
||||
|
||||
/**
|
||||
* 校验本次提交的家属数量、唯一标识及分场冲突。
|
||||
* 校验本次提交的类型家属上限、活动累计家属上限、唯一标识及分场冲突。
|
||||
* 当参数包含报名记录ID时,会排除该记录后再校验,用于修改报名。
|
||||
*
|
||||
* @param user 待校验的报名信息
|
||||
|
||||
+385
-29
@@ -48,10 +48,74 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
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
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void add(FamilyActivity activity, FamilyCourse course) {
|
||||
|
||||
validateActivityRuleConfig(activity, null);
|
||||
dao().insert(activity);
|
||||
|
||||
//插入类型限制
|
||||
@@ -81,7 +145,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void edit(FamilyActivity activity) {
|
||||
|
||||
validateActivityRuleConfig(activity, dao().fetch(FamilyActivity.class, activity.getId()));
|
||||
//修改活动
|
||||
update(activity);
|
||||
|
||||
@@ -155,6 +219,8 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
}
|
||||
}
|
||||
|
||||
/** 将页面的课程日期与时分合成为实际上课时间,保存到同一事务的课程时段表。 */
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
private void setCourseTimeAndInsert(FamilyCourse course) {
|
||||
course.getCourseTimeList().forEach(courseTime -> {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
@@ -254,7 +320,13 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public FamilyUser doSignUp(FamilyUser familyUser) throws Exception {
|
||||
String userId = SecurityUtil.getUserId();
|
||||
|
||||
if (familyUser == null) {
|
||||
throw new BaseException("报名信息不能为空");
|
||||
}
|
||||
// 先锁教职工,再锁课程;跨课程报名也必须串行校验同一活动的家属总数和时间。
|
||||
lockSignUpUser(userId);
|
||||
familyUser.setId(null);
|
||||
familyUser.setUserId(userId);
|
||||
// 对课程行加数据库锁,使名额校验和正式写入在多节点部署时仍按顺序执行。
|
||||
lockCourseForSignUp(familyUser.getCourseId());
|
||||
//查询课程
|
||||
@@ -272,6 +344,10 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId());
|
||||
validateSignUpTimeAndScope(activity, userId, false);
|
||||
familyUser.setActivityId(course.getActivityId());
|
||||
if (!Boolean.TRUE.equals(course.getCourseIsLimitApply())) {
|
||||
// 非独立时段课程必须参加全部时段,忽略客户端多传的单个时段ID。
|
||||
familyUser.setActivityCourseId(null);
|
||||
}
|
||||
if (getSignUpInfo(course.getId(), userId) != null) {
|
||||
throw new BaseException("抱歉,您已经报名");
|
||||
}
|
||||
@@ -298,6 +374,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
: "您选择的类型已达上限,不能再报该类型的了");
|
||||
}
|
||||
validateCourseTime(course, familyUser.getActivityCourseId(), null);
|
||||
validateTimeConflict(familyUser, course, activity, userId);
|
||||
//如果这个课程的预留名额方式为报名人数不变
|
||||
if (course.getReserveMode() == 2) {
|
||||
//如果当前报名+已报小于这个课程限制人数
|
||||
@@ -345,6 +422,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
throw new BaseException("取消报名参数不完整");
|
||||
}
|
||||
|
||||
lockSignUpUser(SecurityUtil.getUserId());
|
||||
// 与正式报名共用课程行锁,保证删除、重新统计和候补转正在同一串行区间内完成。
|
||||
lockCourseForSignUp(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) {
|
||||
FamilyType type = dao().fetch(FamilyType.class, course.getCourseType());
|
||||
if (type == null) {
|
||||
@@ -459,6 +538,78 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
&& 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
|
||||
public FamilyApplyPrefill getPrefill(String courseId, String userId) {
|
||||
if (StrUtil.isBlank(courseId) || StrUtil.isBlank(userId)) {
|
||||
@@ -581,7 +732,13 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
if (currentSignUp == null) {
|
||||
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
|
||||
@@ -592,11 +749,31 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
}
|
||||
|
||||
String currentUserId = SecurityUtil.getUserId();
|
||||
lockSignUpUser(currentUserId);
|
||||
lockCourseForSignUp(familyUser.getCourseId());
|
||||
FamilyUser currentSignUp = getSignUpInfo(familyUser.getCourseId(), currentUserId);
|
||||
if (currentSignUp == null || !Objects.equals(currentSignUp.getId(), familyUser.getId())) {
|
||||
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();
|
||||
currentSignUp.setMobile(familyUser.getMobile());
|
||||
@@ -604,22 +781,20 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
currentSignUp.setActivityCourseId(familyUser.getActivityCourseId());
|
||||
dao().update(currentSignUp);
|
||||
syncUpdatedUserCourse(currentSignUp, oldActivityCourseId);
|
||||
// 保存减员及其时段记录后,在同一课程锁内按最新占用人数执行递补。
|
||||
if (releasedFormalSlots && Objects.equals(course.getReserveMode(), 2) && isWithinSignUpTime(activity)) {
|
||||
promoteWaitingUsers(course);
|
||||
}
|
||||
return currentSignUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改按钮预校验与最终保存共用本方法,确保两处业务规则不会产生偏差。
|
||||
* 保存修改前校验家属信息、报名名额与时段,人数及冲突校验须排除本人的原记录。
|
||||
*/
|
||||
private void validateUpdateSignUpData(FamilyUser familyUser, 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, SecurityUtil.getUserId(), true);
|
||||
FamilyType type = dao().fetch(FamilyType.class, course.getCourseType());
|
||||
if (type == null) {
|
||||
throw new BaseException("报名类型不存在或已变更");
|
||||
private void validateUpdateSignUpData(FamilyUser familyUser, FamilyUser currentSignUp,
|
||||
FamilyCourse course, FamilyActivity activity, FamilyType type) {
|
||||
if (!Boolean.TRUE.equals(course.getCourseIsLimitApply())) {
|
||||
familyUser.setActivityCourseId(null);
|
||||
}
|
||||
|
||||
if (Lang.isEmpty(familyUser.getMobileColumnsValue())) {
|
||||
@@ -651,6 +826,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
: "您选择的类型已达上限,不能再报该类型的了");
|
||||
}
|
||||
validateCourseTime(course, familyUser.getActivityCourseId(), currentSignUp);
|
||||
validateTimeConflict(familyUser, course, activity, SecurityUtil.getUserId());
|
||||
}
|
||||
|
||||
@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) {
|
||||
NutMap submittedColumn = submittedValueMap.get(config.getColumnCode());
|
||||
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);
|
||||
|
||||
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) {
|
||||
if (Objects.equals(oldActivityCourseId, currentSignUp.getActivityCourseId())) {
|
||||
return;
|
||||
}
|
||||
rebuildUserCourseTimes(currentSignUp);
|
||||
}
|
||||
|
||||
/** 修改所选时段或后台调班后,按本次报名实际参加的时段重建课程记录。 */
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
private void rebuildUserCourseTimes(FamilyUser currentSignUp) {
|
||||
dao().clear(FamilyUserCourse.class, Cnd.where(FamilyUserCourse::getActivityId, "=", currentSignUp.getActivityId())
|
||||
.and(FamilyUserCourse::getCourseId, "=", currentSignUp.getCourseId())
|
||||
.and(FamilyUserCourse::getUserId, "=", currentSignUp.getUserId()));
|
||||
@@ -1023,6 +1343,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
|
||||
@Async
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void asyncInsertUserCourse(String activityId, String courseId, String userId) {
|
||||
log.info("异步插入{}的上课信息,课程ID为{},活动ID为{}", userId, courseId, activityId);
|
||||
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) {
|
||||
List<FamilyActivityCourse> courseList = dao().query(FamilyActivityCourse.class, Cnd.where("courseId", "=", courseId));
|
||||
List<FamilyUserCourse> list = new ArrayList<>();
|
||||
@@ -1112,6 +1434,11 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
|
||||
@Override
|
||||
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();
|
||||
FamilyActivity activity = dao().fetch(FamilyActivity.class, activityId);
|
||||
if (activity == null) {
|
||||
@@ -1137,7 +1464,11 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
|
||||
// 查询本人本活动的其他报名,修改报名时排除当前记录,后续冲突校验仍覆盖全部子活动类型。
|
||||
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())) {
|
||||
signUpCnd.and(FamilyUser::getId, "!=", user.getId());
|
||||
}
|
||||
@@ -1146,6 +1477,31 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
Map<String, String> courseMap = courseList.stream()
|
||||
.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()
|
||||
.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());
|
||||
|
||||
long count = allMobileColumnsValue.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(List::stream)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(nutMap -> Objects.equals(activity.getOnlyKey(), nutMap.getString("columnCode")))
|
||||
.map(nutMap -> nutMap.getString("columnValue"))
|
||||
.map(family -> getFamilyIdentity(family, activity))
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.count();
|
||||
@@ -1170,8 +1522,9 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
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();
|
||||
|
||||
@@ -1190,7 +1543,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
continue;
|
||||
}
|
||||
// 家属匹配唯一标识的值
|
||||
String onlyKeyValue = nutMap.getString("columnValue");
|
||||
String onlyKeyValue = getFamilyIdentity(listMap, activity);
|
||||
if(Objects.equals(tempValue, onlyKeyValue)) {
|
||||
return Map.of(false, activity.getKeyWord() + "不能重复报同一分场");
|
||||
}
|
||||
@@ -1212,18 +1565,21 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl<FamilyActivity> i
|
||||
if(hisNutMap == null) {
|
||||
continue;
|
||||
}
|
||||
String hisKeyValue = hisNutMap.getString("columnValue");
|
||||
String hisKeyValue = getFamilyIdentity(hisValue, activity);
|
||||
// 判断是不是同一个孩子
|
||||
if(Objects.equals(onlyKeyValue, hisKeyValue)) {
|
||||
oneFamilyCount ++;
|
||||
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())) {
|
||||
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())) {
|
||||
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.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Lang;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
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_black_list tsubl on tsubl.userId = tsuu.userId
|
||||
$condition
|
||||
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 )
|
||||
""");
|
||||
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();
|
||||
varCnd.where().setTop(false);
|
||||
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.web.commons.auth.utils.SecurityUtil;
|
||||
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.dao.Chain;
|
||||
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.Param;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @ClassName WelfareAddressManageController
|
||||
* @Description 福利地址管理
|
||||
@@ -31,12 +34,28 @@ public class WelfareAddressManageController {
|
||||
@Inject
|
||||
private BaseService baseService;
|
||||
|
||||
@Inject
|
||||
private WelfareAddressManageService welfareAddressManageService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/welfare/addressManage/index.html")
|
||||
@SaCheckPermission("welfare.addressManage")
|
||||
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);
|
||||
|
||||
/**
|
||||
* 移动端工会统计;方式筛选只影响已选人数和各福利选项人数。
|
||||
* @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
|
||||
|
||||
+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.userState", "in", pageForm.getUserStates());
|
||||
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
||||
// 默认先按工号正序分页,工号相同时沿用工会和单位顺序。
|
||||
cnd.asc("t2.loginname");
|
||||
cnd.asc("t1.welfareUnionName");
|
||||
cnd.asc("t1.welfareUnitName");
|
||||
} else {
|
||||
@@ -644,32 +646,52 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
|
||||
LEFT JOIN sys_user t2 ON t2.id = t1.userId
|
||||
$condition
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and("t1.projectId", "=", pageForm.getProjectId());
|
||||
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"));
|
||||
// 只统计当前福利项目名单中的身份证号;同号名单记录至少两条才属于重复组。
|
||||
Sql repeatIdCardSql = Sqls.create("""
|
||||
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())) {
|
||||
cnd.where().andLike("t2.username", pageForm.getUserName());
|
||||
matchedCnd.where().andLike("t2.username", pageForm.getUserName());
|
||||
}
|
||||
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());
|
||||
cnd.andEX("t2.birthday", "=", pageForm.getBirthday());
|
||||
matchedCnd.andEX("t2.sex", "=", pageForm.getSex());
|
||||
matchedCnd.andEX("t2.birthday", "=", pageForm.getBirthday());
|
||||
if (pageForm.getBirthMonths() != null && pageForm.getBirthMonths().length > 0) {
|
||||
cnd.andEX("MONTH(t2.birthday)", "in", pageForm.getBirthMonths());
|
||||
matchedCnd.andEX("MONTH(t2.birthday)", "in", pageForm.getBirthMonths());
|
||||
}
|
||||
matchedCnd.andEX("t1.welfareUnitId", "in", pageForm.getUnitIds());
|
||||
matchedCnd.andEX("t1.welfareUnionId", "=", pageForm.getUnionId());
|
||||
matchedCnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
|
||||
matchedCnd.andEX("t1.preparedBy", "in", pageForm.getPreparedBys());
|
||||
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.loginname");
|
||||
cnd.asc("t1.id");
|
||||
sql.setCondition(cnd);
|
||||
list = listMap(sql);
|
||||
}
|
||||
cnd.andEX("t1.welfareUnitId", "in", pageForm.getUnitIds());
|
||||
cnd.andEX("t1.welfareUnionId", "=", pageForm.getUnionId());
|
||||
cnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
|
||||
cnd.andEX("t1.preparedBy", "in", pageForm.getPreparedBys());
|
||||
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
|
||||
|
||||
cnd.asc("t2.idCard");
|
||||
cnd.asc("t1.welfareUnionName");
|
||||
cnd.asc("t1.welfareUnitName");
|
||||
|
||||
sql.setCondition(cnd);
|
||||
List<NutMap> list = listMap(sql);
|
||||
|
||||
try {
|
||||
List<ExcelExportEntity> exportEntities = new ArrayList<>();
|
||||
|
||||
+2
@@ -102,6 +102,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
|
||||
}
|
||||
|
||||
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
||||
// 默认先按工号正序分页,工号相同时沿用工会和单位顺序。
|
||||
cnd.asc("t4.loginname");
|
||||
cnd.asc("welfareUnionName");
|
||||
cnd.asc("welfareUnitName");
|
||||
} else {
|
||||
|
||||
+20
@@ -138,6 +138,26 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
|
||||
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
|
||||
public Pagination selectedUnionUserPageData(PageForm pageForm, String projectId, String unionId, Boolean isSelectByAdmin) {
|
||||
Sql sql = Sqls.create("""
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, processKey, platform, moduleId, isQuickEntry) VALUES ('56e0941221324d04bea7329cb8f549c4', '', '0079', '协会审核', null, 'menu', '/platform/h5/club/audit', null, '/platform/sys/file/download?id=19cl7j298kj3logm5qlb0vslhi', 1, 0, 'h5.club.audit', null, 223, 0, '17a7f8ad3ee947b4a26175049a9c253d', 1790154555839, '17a7f8ad3ee947b4a26175049a9c253d', 1790155973918, 0, null, 'H5', '5710401a805645d59060f888eb863b60', 0);
|
||||
|
||||
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '56e0941221324d04bea7329cb8f549c4');
|
||||
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('53650479ee954e748fda6cceabdd2918', '56e0941221324d04bea7329cb8f549c4');
|
||||
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('419844ad90f34ca58807e84224905f27', '56e0941221324d04bea7329cb8f549c4');
|
||||
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('d8d30588c356492ebb8645bc2b593270', '56e0941221324d04bea7329cb8f549c4');
|
||||
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('cf0a07b773f444d3800bc18216d8cffb', '56e0941221324d04bea7329cb8f549c4');
|
||||
@@ -14,7 +14,8 @@ const h5PopupHistory = (() => {
|
||||
popup.closeFromHistory()
|
||||
}
|
||||
|
||||
window.addEventListener("popstate", handlePopState)
|
||||
// 捕获阶段优先消费弹框返回事件,避免 PJAX 重载页面并重置筛选条件。
|
||||
window.addEventListener("popstate", handlePopState, true)
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -37,11 +38,13 @@ const h5PopupHistory = (() => {
|
||||
/**
|
||||
* 通过取消、关闭按钮关闭指定弹框时,同步回退对应历史记录。
|
||||
* @param id 弹框唯一标识,必须是当前最上层弹框
|
||||
* @return boolean;true 表示已发起历史回退,false 表示弹框不在栈顶
|
||||
* @return boolean;true 表示已发起历史回退,false 表示弹框不在栈顶或正在关闭
|
||||
*/
|
||||
close(id) {
|
||||
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()
|
||||
return true
|
||||
},
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
.field .van-field__error-message {
|
||||
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>
|
||||
<template>
|
||||
<div>
|
||||
@@ -89,8 +100,11 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<van-picker
|
||||
:key="column.columnCode + '-' + (pickerItemHeights[column.columnCode] || 44)"
|
||||
:columns="column.selectValues"
|
||||
:title="column.columnName"
|
||||
:item-height="pickerItemHeights[column.columnCode] || 44"
|
||||
:visible-item-count="pickerVisibleItemCounts[column.columnCode] || 6"
|
||||
show-toolbar
|
||||
@cancel="ordinaryPickerCancel(column)"
|
||||
@confirm="
|
||||
@@ -98,7 +112,11 @@
|
||||
ordinaryPickerConfirm(value, column)
|
||||
}
|
||||
"
|
||||
/>
|
||||
>
|
||||
<template v-if="fullOptionText" #option="option">
|
||||
<div class="train-picker-option-text">{{ option }}</div>
|
||||
</template>
|
||||
</van-picker>
|
||||
</template>
|
||||
</van-popup>
|
||||
</template>
|
||||
@@ -140,7 +158,12 @@ module.exports = {
|
||||
name: "TrainDynamicForm",
|
||||
components: {},
|
||||
props: {
|
||||
dynamicData: []
|
||||
dynamicData: [],
|
||||
// 由亲子活动报名页单独启用,其余引用页面保留原有选项展示。
|
||||
fullOptionText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
model: {
|
||||
prop: "dynamicData",
|
||||
@@ -160,6 +183,8 @@ module.exports = {
|
||||
return {
|
||||
//所有的picker状态
|
||||
pickerStates: {},
|
||||
pickerItemHeights: {},
|
||||
pickerVisibleItemCounts: {},
|
||||
fileList: [],
|
||||
addressStates: {},
|
||||
addressAreaList: window.trainSignUpAreaList || { province_list: {}, city_list: {}, county_list: {} }
|
||||
@@ -444,7 +469,44 @@ module.exports = {
|
||||
const addressValue = (state.displayArea || "") + (state.detail || "")
|
||||
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) {
|
||||
this.measurePickerOptions(column)
|
||||
this.$set(this.pickerStates, column.columnCode + "Picker", true)
|
||||
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-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/vxe-table.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 -->
|
||||
<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/jquery/jquery.js"></script>
|
||||
<!-- 弹框历史必须先于 PJAX 注册,关闭弹框时优先消费返回事件,避免重新加载业务页面。 -->
|
||||
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js?v=20260930-2"></script>
|
||||
<!-- pjax是异步加载html片段的工具,模拟前端路由机制 -->
|
||||
<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!}/components/plugins/sysDict/DictData.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>
|
||||
|
||||
<!--富文本编辑器-->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const ACTIVITY_CULTURE_USER_STATISTICS = {
|
||||
template: /*language=HTML*/ `
|
||||
<guava ref="guava">
|
||||
<guava ref="guava" @hook:mounted="onPageVisible">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
|
||||
@@ -65,7 +65,8 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
|
||||
</el-button>
|
||||
</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">
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center"
|
||||
label="序号" type="index"
|
||||
@@ -110,8 +111,10 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
|
||||
pageForm: {
|
||||
unionId: "",
|
||||
searchName: "atp.username",
|
||||
pageSize: 50,
|
||||
year: new Date().getFullYear() + ""
|
||||
},
|
||||
tableHeight: 300,
|
||||
tableColumns: [
|
||||
{ prop: "userName", label: "姓名", width: 180, fixed: "left" },
|
||||
{ prop: "loginName", label: "工号", width: 180, fixed: "left" },
|
||||
@@ -133,6 +136,60 @@ const ACTIVITY_CULTURE_USER_STATISTICS = {
|
||||
methods: {
|
||||
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) {
|
||||
const formConfig = this.activityList.find((v) => v.id === this.pageForm.activityId)?.formConfig
|
||||
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() {
|
||||
await this.getActivityByYearOrType()
|
||||
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>
|
||||
<el-dialog :close-on-click-modal="false" :visible.sync="signDialog"
|
||||
: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">
|
||||
<div class="left-span-label">个人信息</div>
|
||||
<el-row>
|
||||
@@ -72,6 +72,9 @@ const signForm = {
|
||||
</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">
|
||||
<el-tabs v-model="active" type="card">
|
||||
<el-tab-pane v-for="(item, index) in formData.mobileColumnsValue"
|
||||
@@ -137,7 +140,7 @@ const signForm = {
|
||||
|
||||
</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 :loading="formLoading" @click="onSubmit" type="primary">{{prefillMode ? '保存预填' : '提 交'}}</el-button>
|
||||
</el-row>
|
||||
@@ -158,7 +161,7 @@ const signForm = {
|
||||
courseRow: {},
|
||||
courseTypeRow: {},
|
||||
courseTimeSelectList: [],
|
||||
active: '',
|
||||
active: '0',
|
||||
|
||||
activity: {},
|
||||
}
|
||||
@@ -173,13 +176,23 @@ const signForm = {
|
||||
this.$message.warning('家属最多人数为' + this.courseTypeRow.familyMaxCount)
|
||||
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)
|
||||
this.formData.mobileColumnsValue.push(list)
|
||||
// 新增儿童后选中对应的字符串索引,确保标签和填写区域同步显示。
|
||||
this.$set(this, "active", String(this.formData.mobileColumnsValue.length - 1))
|
||||
},
|
||||
delFamily() {
|
||||
// 无儿童时不执行删除;删完后保留首项索引,兼容重新添加儿童。
|
||||
if (this.formData.mobileColumnsValue.length === 0) return
|
||||
this.formData.mobileColumnsValue.splice(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() {
|
||||
//获取家属是多少人
|
||||
@@ -478,12 +491,33 @@ const signForm = {
|
||||
this.mergeMobileColumnsValue(courseType, signUpData.mobileColumnsValue))
|
||||
}
|
||||
this.$set(this, "formData", formData)
|
||||
// 不同活动共用报名组件,每次打开都选中首个儿童,避免沿用上次的标签状态。
|
||||
this.$set(this, "active", "0")
|
||||
this.$nextTick(() => {
|
||||
this.$refs.form.clearValidate()
|
||||
})
|
||||
},
|
||||
},
|
||||
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 {
|
||||
margin: 0 0 15px !important;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,24 @@ const basicForm = {
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</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">
|
||||
<text-editor v-model="formData.introduce"></text-editor>
|
||||
</el-form-item>
|
||||
@@ -280,9 +298,14 @@ const basicForm = {
|
||||
|
||||
<div class="left-span-label" style="margin-top: 20px">判断家属数量唯一标识</div>
|
||||
<el-form-item label="唯一标识" prop="onlyKey">
|
||||
<el-select @focus="keyFocus" v-model="formData.onlyKey" placeholder="请选择唯一标识">
|
||||
<el-option v-for="item in typeArray" :key="item.columnCode" :label="item.columnName" :value="item.columnCode"></el-option>
|
||||
<el-select @focus="keyFocus" :value="formData.onlyKey" placeholder="请选择唯一标识" style="width: 420px"
|
||||
@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>
|
||||
<div style="color: #c64120; font-size: 12px">
|
||||
请选择本活动所有子活动类型共有且均设为必填的字段,各类型字段编码必须一致。有报名或预填后禁止直接更换。
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -290,8 +313,8 @@ const basicForm = {
|
||||
<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 === 1" type="primary" @click="step = 2">下一步</el-button>
|
||||
<el-button type="primary" @click="onSave">保存</el-button>
|
||||
<el-button type="primary" @click="onSubmit">提交</el-button>
|
||||
<el-button type="primary" :loading="formLoading" @click="onSave">保存</el-button>
|
||||
<el-button type="primary" :loading="formLoading" @click="onSubmit">提交</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog :close-on-click-modal="false" :visible.sync="signUpDialog" title="设置报名限制" append-to-body>
|
||||
@@ -325,6 +348,7 @@ const basicForm = {
|
||||
data() {
|
||||
return {
|
||||
signUpDialog: false,
|
||||
formLoading: false,
|
||||
step: 1,
|
||||
formData: {
|
||||
notice: false,
|
||||
@@ -348,7 +372,9 @@ const basicForm = {
|
||||
restrictLimit: 1,
|
||||
limitNum: 1,
|
||||
typeLimits: [],
|
||||
keyWord: '家属'
|
||||
keyWord: '家属',
|
||||
familyCountScope: 1,
|
||||
timeConflictCheck: false,
|
||||
},
|
||||
historicalActList: [],
|
||||
trainTypeList: [],
|
||||
@@ -372,28 +398,47 @@ const basicForm = {
|
||||
},
|
||||
methods: {
|
||||
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) {
|
||||
this.$set(this, 'typeArray', [])
|
||||
this.$message.warning('请先设置' + this.activityType + '类型')
|
||||
return
|
||||
}
|
||||
const typeList = this.courseTypeList.filter(o => types.includes(o.id))
|
||||
let array = []
|
||||
typeList.forEach(item => {
|
||||
array = array.concat(item.familyMobileSignColumnList)
|
||||
})
|
||||
// 根据 columnCode 去重
|
||||
const uniqueMap = new Map()
|
||||
array.forEach(item => {
|
||||
if (item.columnCode) {
|
||||
uniqueMap.set(item.columnCode, item)
|
||||
}
|
||||
})
|
||||
// 可选:保留没有 columnCode 的项
|
||||
const withoutCode = array.filter(item => !item.columnCode)
|
||||
this.typeArray = [...uniqueMap.values(), ...withoutCode]
|
||||
// 唯一标识必须在每种类型中各出现一次且均为必填,不限定字段的验证规则。
|
||||
const firstColumns = typeList[0] ? (typeList[0].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
|
||||
})
|
||||
}
|
||||
this.$set(this, 'typeArray', options)
|
||||
},
|
||||
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 (row.courseReservedNumber > row.coursePeopleNumber && row.reserveMode === 1) {
|
||||
this.$alert("预留人数不能大于" + this.activityType + "人数!", "提示", {
|
||||
@@ -402,31 +447,32 @@ const basicForm = {
|
||||
row.courseReservedNumber = 0
|
||||
}
|
||||
} else {
|
||||
const registerUserCount = await this.getRegisterUserCount(row.id)
|
||||
if (row.courseReservedNumber + registerUserCount > row.coursePeopleNumber) {
|
||||
if (row.reserveMode === 1) {
|
||||
let num = row.coursePeopleNumber - registerUserCount
|
||||
let str =
|
||||
"您设置的" +
|
||||
this.activityType +
|
||||
"人数为" +
|
||||
row.coursePeopleNumber +
|
||||
"人,当前报名人数为" +
|
||||
registerUserCount +
|
||||
"人,预留名额上限为" +
|
||||
num +
|
||||
"人!"
|
||||
this.$alert(str, "提示", {
|
||||
confirmButtonText: "确定"
|
||||
})
|
||||
row.courseReservedNumber = 0
|
||||
return this.getRegisterUserCount(row.id).then((registerUserCount) => {
|
||||
if (row.courseReservedNumber + registerUserCount > row.coursePeopleNumber) {
|
||||
if (row.reserveMode === 1) {
|
||||
let num = row.coursePeopleNumber - registerUserCount
|
||||
let str =
|
||||
"您设置的" +
|
||||
this.activityType +
|
||||
"人数为" +
|
||||
row.coursePeopleNumber +
|
||||
"人,当前报名人数为" +
|
||||
registerUserCount +
|
||||
"人,预留名额上限为" +
|
||||
num +
|
||||
"人!"
|
||||
this.$alert(str, "提示", {
|
||||
confirmButtonText: "确定"
|
||||
})
|
||||
row.courseReservedNumber = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
async getRegisterUserCount(courseId) {
|
||||
const resp = await this.$axios.post("/platform/family/manage/getRegisterUserCount", { courseId })
|
||||
return resp.code === 0 ? resp.data : 0
|
||||
getRegisterUserCount(courseId) {
|
||||
return this.$axios.post("/platform/family/manage/getRegisterUserCount", { courseId })
|
||||
.then((resp) => resp.code === 0 ? resp.data : 0)
|
||||
},
|
||||
courseTypeChange(val, row) {
|
||||
row.reserveMode = 1
|
||||
@@ -497,30 +543,32 @@ const basicForm = {
|
||||
addCourse() {
|
||||
this.formData.courseList.push({ courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false })
|
||||
},
|
||||
async historicalActChange(val) {
|
||||
const resp = await this.$axios.post("/platform/family/manage/findOne", {id: val})
|
||||
if (resp.code === 0) {
|
||||
this.formData = resp.data
|
||||
this.typeChange(this.formData.trainType)
|
||||
this.formData.id = ""
|
||||
this.formData.courseList.forEach((course) => {
|
||||
course.id = ""
|
||||
course.activityId = ""
|
||||
if (course.courseTimeList) {
|
||||
course.courseTimeList.forEach((courseTime) => {
|
||||
courseTime.id = ""
|
||||
courseTime.activityId = ""
|
||||
courseTime.courseId = ""
|
||||
historicalActChange(val) {
|
||||
return this.$axios.post("/platform/family/manage/findOne", {id: val}).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$set(this, "formData", resp.data)
|
||||
this.applyRuleDefaults()
|
||||
this.typeChange(this.formData.trainType)
|
||||
this.formData.id = ""
|
||||
this.formData.courseList.forEach((course) => {
|
||||
course.id = ""
|
||||
course.activityId = ""
|
||||
if (course.courseTimeList) {
|
||||
course.courseTimeList.forEach((courseTime) => {
|
||||
courseTime.id = ""
|
||||
courseTime.activityId = ""
|
||||
courseTime.courseId = ""
|
||||
})
|
||||
}
|
||||
})
|
||||
if (this.formData.typeLimits) {
|
||||
this.formData.typeLimits.forEach((typeLimit) => {
|
||||
typeLimit.id = ""
|
||||
typeLimit.activityId = ""
|
||||
})
|
||||
}
|
||||
})
|
||||
if (this.formData.typeLimits) {
|
||||
this.formData.typeLimits.forEach((typeLimit) => {
|
||||
typeLimit.id = ""
|
||||
typeLimit.activityId = ""
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
typeChange(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", "全部教职工")
|
||||
}
|
||||
},
|
||||
async getActivityGroup() {
|
||||
const { data } = await this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup")
|
||||
return data
|
||||
getActivityGroup() {
|
||||
return this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((resp) => resp.data)
|
||||
},
|
||||
async getHistoricalActList() {
|
||||
const resp = await this.$axios.post("/platform/family/manage/getHistoricalActList", {})
|
||||
return resp.data
|
||||
getHistoricalActList() {
|
||||
return this.$axios.post("/platform/family/manage/getHistoricalActList", {}).then((resp) => resp.data)
|
||||
},
|
||||
async onSave() {
|
||||
onSave() {
|
||||
let valid = false
|
||||
this.$refs.form.validateField("activityName", (errMsg) => {
|
||||
valid = errMsg === ""
|
||||
@@ -551,10 +597,10 @@ const basicForm = {
|
||||
this.$message.warning("请输入活动名称")
|
||||
return
|
||||
}
|
||||
await this.doHandle('保存')
|
||||
return this.doHandle('保存')
|
||||
},
|
||||
onSubmit() {
|
||||
this.$refs["form"].validate(async (valid, errMsg) => {
|
||||
this.$refs["form"].validate((valid, errMsg) => {
|
||||
if (valid) {
|
||||
const courseValid = this.formData.courseList.some((v, i) => {
|
||||
const basicValid = v.courseName && v.coursePeopleNumber && v.courseLocation && v.courseInstructor && v.courseType
|
||||
@@ -599,7 +645,7 @@ const basicForm = {
|
||||
return
|
||||
}
|
||||
this.formData.isDisabled = false
|
||||
await this.doHandle('提交')
|
||||
this.doHandle('提交')
|
||||
} else {
|
||||
if(Object.keys(errMsg).length > 0) {
|
||||
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)
|
||||
cloneData.activitySignUpStartTime = cloneData.activitySignTime !== undefined ? cloneData.activitySignTime[0] : null
|
||||
cloneData.activitySignUpEndTime = cloneData.activitySignTime !== undefined ? cloneData.activitySignTime[1] : null
|
||||
@@ -620,41 +666,50 @@ const basicForm = {
|
||||
}
|
||||
cloneData.typeLimits = JSON.stringify(this.formData.typeLimits)
|
||||
cloneData.courseList = JSON.stringify(cloneData.courseList)
|
||||
const confirm = await this.$confirm("您确定要" + type + "吗?", "提示", {
|
||||
return this.$confirm("您确定要" + type + "吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
})
|
||||
if (confirm !== "confirm") return
|
||||
const resp = await this.$axios.post("/platform/family/manage/doHandle", cloneData)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.step = 1
|
||||
this.$emit('refresh')
|
||||
this.$emit('back')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}).then(() => {
|
||||
this.formLoading = true
|
||||
return this.$axios.post("/platform/family/manage/doHandle", cloneData).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.step = 1
|
||||
this.$emit('refresh')
|
||||
this.$emit('back')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
getAllType() {
|
||||
return this.$axios.post("/platform/family/type/getAllType", {}).then((resp) => resp.data)
|
||||
},
|
||||
init(row) {
|
||||
if (!row || !row.id) {
|
||||
this.applyRuleDefaults()
|
||||
return Promise.resolve()
|
||||
}
|
||||
},
|
||||
async getAllType() {
|
||||
const resp = await this.$axios.post("/platform/family/type/getAllType", {})
|
||||
return resp.data
|
||||
},
|
||||
async init(row) {
|
||||
if (row && row.id) {
|
||||
const resp = await this.$axios.post("/platform/family/manage/findOne", {id: row.id})
|
||||
return this.$axios.post("/platform/family/manage/findOne", {id: row.id}).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.formData = resp.data
|
||||
this.$set(this, "formData", resp.data)
|
||||
this.typeChange(this.formData.trainType)
|
||||
if(this.formData.onlyKey) this.keyFocus()
|
||||
this.applyRuleDefaults()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async initData(row) {
|
||||
this.activityGroupList = await this.getActivityGroup()
|
||||
this.historicalActList = await this.getHistoricalActList()
|
||||
this.courseTypeList = await this.getAllType()
|
||||
await this.init(row)
|
||||
initData(row) {
|
||||
return Promise.all([this.getActivityGroup(), this.getHistoricalActList(), this.getAllType()])
|
||||
.then(([groups, activities, types]) => {
|
||||
this.$set(this, "activityGroupList", groups)
|
||||
this.$set(this, "historicalActList", activities)
|
||||
this.$set(this, "courseTypeList", types)
|
||||
return this.init(row)
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -664,9 +719,11 @@ const basicForm = {
|
||||
}
|
||||
},
|
||||
'formData.activityGroupId': {
|
||||
async handler(newVal, oldVal) {
|
||||
this.activityGroupList = await this.getActivityGroup()
|
||||
this.userScopeDialog = false
|
||||
handler(newVal, oldVal) {
|
||||
this.getActivityGroup().then((groups) => {
|
||||
this.$set(this, 'activityGroupList', groups)
|
||||
this.$set(this, 'userScopeDialog', false)
|
||||
})
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
|
||||
@@ -11,6 +11,10 @@ const info = {
|
||||
<el-descriptions-item label="活动结束时间">{{viewData.activityEndTime}}</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.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">
|
||||
<div v-html="viewData.introduce"></div>
|
||||
</el-descriptions-item>
|
||||
@@ -54,6 +58,7 @@ const info = {
|
||||
<el-dialog :visible.sync="viewMapDialog" title="地点" width="60%">
|
||||
<div id="viewMap" style="width: 100%; height: 500px"></div>
|
||||
</el-dialog>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
dicts: ["FAMILY_SIGNUP_TYPE"],
|
||||
@@ -72,7 +77,7 @@ const info = {
|
||||
this.$axios.post("/platform/family/manage/findOne", { id: id })
|
||||
.then((resp) => {
|
||||
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)
|
||||
this.activityType = type?.name || "子活动"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<guava ref="guava" @hook:mounted="onPageVisible">
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度:">
|
||||
@@ -36,7 +36,8 @@ layout("/layouts/platform.html"){
|
||||
<el-button @click="exportSignPerson" type="primary" size="small">导出签到名单</el-button>
|
||||
<el-button @click="exportGiftPerson" type="primary" size="small">导出领取名单</el-button>
|
||||
</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
|
||||
:label="column.prop === 'courseNames' ? trainType : column.label"
|
||||
@@ -98,15 +99,42 @@ layout("/layouts/platform.html"){
|
||||
new Vue({
|
||||
el: "#app",
|
||||
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() {
|
||||
return {
|
||||
reserveDialogVisible: false,
|
||||
reserveTableData: [],
|
||||
activityList: [],
|
||||
pageForm: {
|
||||
pageSize: 50,
|
||||
year: new Date().getFullYear().toString(),
|
||||
activityId: null
|
||||
},
|
||||
tableHeight: 300,
|
||||
tableColumns: [
|
||||
{ label: "姓名", prop: "username" },
|
||||
{ label: "工号", prop: "loginname" },
|
||||
@@ -139,6 +167,55 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
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) {
|
||||
this.multipleSelection = val
|
||||
},
|
||||
@@ -259,6 +336,16 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
</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">
|
||||
<table-tool :app="this" label="收货地址">
|
||||
<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>
|
||||
|
||||
<el-table
|
||||
@@ -74,6 +75,10 @@ layout("/layouts/platform.html"){
|
||||
"address-dialog": ADDRESS_DIALOG
|
||||
},
|
||||
methods: {
|
||||
// 导出全部用户的收货地址,不传当前用户或分页条件;下载和 loading 复用全局方法。
|
||||
exportAllAddress() {
|
||||
this.$downLoad("/platform/welfare/addressManage/exportAllAddress", {})
|
||||
},
|
||||
selectUserAddress(){
|
||||
this.pageData()
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava>
|
||||
<guava @hook:mounted="onPageVisible">
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="所属年份">
|
||||
@@ -96,7 +96,9 @@ layout("/layouts/platform.html"){
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
:max-height="tableHeight"
|
||||
:size="tableSize"
|
||||
class="welfare-selection-table"
|
||||
@sort-change="pageOrder"
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
@@ -168,6 +170,31 @@ layout("/layouts/platform.html"){
|
||||
new Vue({
|
||||
el: "#app",
|
||||
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: {
|
||||
"option-select": optionSelect
|
||||
},
|
||||
@@ -175,9 +202,11 @@ layout("/layouts/platform.html"){
|
||||
return {
|
||||
pageForm: {
|
||||
searchName: "username",
|
||||
pageSize: 50,
|
||||
year: new Date().getFullYear().toString(),
|
||||
isSelect: null
|
||||
},
|
||||
tableHeight: 300,
|
||||
unionOptions: [],
|
||||
unitOptions: [],
|
||||
projectOptions: [],
|
||||
@@ -216,6 +245,57 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.$axios.post("/platform/welfare/common/list", { year: this.pageForm.year }).then((res) => {
|
||||
@@ -355,6 +435,16 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
</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>
|
||||
<guava ref="guava">
|
||||
<guava ref="guava" @hook:mounted="onPageVisible">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
@@ -194,7 +194,9 @@ layout("/layouts/platform.html"){
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
:max-height="tableHeight"
|
||||
:size="tableSize"
|
||||
class="welfare-list-table"
|
||||
@sort-change="pageOrder"
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
@@ -294,6 +296,29 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
mounted() {
|
||||
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: {
|
||||
add_welfare_list_by_select: ADD_WELFARE_LIST_BY_SELECT,
|
||||
@@ -306,8 +331,10 @@ layout("/layouts/platform.html"){
|
||||
checkedFields: [],
|
||||
pageForm: {
|
||||
searchName: "username",
|
||||
pageSize: 50,
|
||||
year: new Date().getFullYear().toString()
|
||||
},
|
||||
tableHeight: 300,
|
||||
unionOptions: [],
|
||||
unitOptions: [],
|
||||
sexOptions: ["男", "女"],
|
||||
@@ -335,6 +362,57 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.$downLoad("/platform/welfare/list/mange/exportAddress", this.pageForm)
|
||||
@@ -422,6 +500,16 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
</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>
|
||||
</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">
|
||||
<van-tabs v-model="familyActive" type="card" color="#0e78c5" animated>
|
||||
<van-tab v-for="item, index in formData.mobileColumnsValue"
|
||||
:name="index + ''"
|
||||
:title="activity.keyWord + (index + 1)"
|
||||
: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-tabs>
|
||||
</div>
|
||||
@@ -111,6 +114,12 @@ const applyForm = {
|
||||
this.$toast(this.activity.keyWord + '最多人数为' + this.courseType.familyMaxCount)
|
||||
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)
|
||||
this.formData.mobileColumnsValue.push(list)
|
||||
},
|
||||
|
||||
@@ -16,11 +16,22 @@ layout("/layouts/platform_h5.html"){
|
||||
text-overflow: unset !important;
|
||||
white-space: normal !important;
|
||||
}
|
||||
.sign-success .label{
|
||||
color: #0e9558;
|
||||
/* 仅突出当前活动列表的报名结果,姓名过长时完整换行展示。 */
|
||||
.table-list-container .sign-success {
|
||||
align-items: baseline;
|
||||
line-height: 28px;
|
||||
}
|
||||
.sign-success .value{
|
||||
color: #0e9558;
|
||||
.table-list-container .sign-success .label {
|
||||
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>
|
||||
|
||||
|
||||
@@ -2,21 +2,21 @@ const home = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<van-nav-bar title="首页" class="nav-bar" placeholder fixed></van-nav-bar>
|
||||
<van-swipe :autoplay="100000" :height="200" class="home__swipe">
|
||||
<van-swipe-item>
|
||||
<img src="/assets/mobile/img/home/home1.png" alt="">
|
||||
</van-swipe-item>
|
||||
<van-swipe-item>
|
||||
<img src="/assets/mobile/img/home/home2.png" alt="">
|
||||
</van-swipe-item>
|
||||
<van-swipe-item>
|
||||
<img src="/assets/mobile/img/home/home3.png" alt="">
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
<!-- <van-swipe :autoplay="100000" :height="200" class="home__swipe">-->
|
||||
<!-- <van-swipe-item>-->
|
||||
<!-- <img src="/assets/mobile/img/home/home1.png" alt="">-->
|
||||
<!-- </van-swipe-item>-->
|
||||
<!-- <van-swipe-item>-->
|
||||
<!-- <img src="/assets/mobile/img/home/home2.png" alt="">-->
|
||||
<!-- </van-swipe-item>-->
|
||||
<!-- <van-swipe-item>-->
|
||||
<!-- <img src="/assets/mobile/img/home/home3.png" alt="">-->
|
||||
<!-- </van-swipe-item>-->
|
||||
<!-- </van-swipe>-->
|
||||
|
||||
<!--快捷入口-->
|
||||
<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-item v-for="item in quickEntries"
|
||||
@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