This commit is contained in:
2026-06-08 08:56:48 +08:00
parent 67a46212e2
commit 5cf398f2b1
53 changed files with 5613 additions and 94 deletions
@@ -193,13 +193,23 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
// 判断财务信息有没有值,
if (financeUserMap.containsKey(sysUser.getLoginname())) {
NutMap nutMap = financeUserMap.get(sysUser.getLoginname());
sysUser.setMember(true);
sysUser.setWelfareMember(true);
sysUser.setPreparationMemberFee(BigDecimal.valueOf(nutMap.getDouble("preparationMemberFee")));
sysUser.setContractMemberFee(BigDecimal.valueOf(nutMap.getDouble("contractMemberFee")));
if(BigDecimal.valueOf(nutMap.getDouble("preparationMemberFee")).doubleValue()>0
|| BigDecimal.valueOf(nutMap.getDouble("contractMemberFee")).doubleValue()>0){
sysUser.setMember(true);
sysUser.setWelfareMember(true);
sysUser.setPreparationMemberFee(BigDecimal.valueOf(nutMap.getDouble("preparationMemberFee")));
sysUser.setContractMemberFee(BigDecimal.valueOf(nutMap.getDouble("contractMemberFee")));
} else {
sysUser.setMember(false);
sysUser.setWelfareMember(false);
sysUser.setPreparationMemberFee(new BigDecimal(0));
sysUser.setContractMemberFee(new BigDecimal(0));
}
} else {
sysUser.setMember(false);
sysUser.setWelfareMember(false);
sysUser.setPreparationMemberFee(new BigDecimal(0));
sysUser.setContractMemberFee(new BigDecimal(0));
}
return sysUser;
@@ -127,7 +127,7 @@ public class ActivityCultureInfoManageController {
@At
@Ok("json:full")
@ApiOperation("通知报名人员")
@SaCheckPermission("trainSignUp.manage")
@SaCheckPermission("infoManage.school.manage")
public Result sendNotify(@Param("activityId") String activityId, @Param("content") String content) {
if (StrUtil.isBlank(activityId) || StrUtil.isBlank(content)) {
return Result.error("参数不完整");
@@ -0,0 +1,176 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Collections;
import java.util.List;
@IocBean
@Ok("json:full")
@At("/platform/tour/branchUserAssignment")
public class TourBranchUserAssignmentController {
@Inject
private TourUserAssignmentService tourUserAssignmentService;
/**
* 分工会人员分配列表入口。
*/
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/branchUserAssignment/index.html")
@SaCheckPermission("tour.branchUserAssignment")
public void index() {
}
/**
* 分页查询当前登录人所在分工会的分配记录,列表数据只来自人员分配表。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result pageData(PageForm pageForm, Integer year, String settingId, String matterId,
String personType, String keyword) {
return Result.success(tourUserAssignmentService.branchAssignmentPage(pageForm, year, settingId, matterId,
personType, keyword));
}
/**
* 分页查询候选人员,候选范围由疗休养配置可参加人员范围和当前登录人所在分工会共同决定。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result candidatePageData(PageForm pageForm, String settingId, String keyword) {
return Result.success(tourUserAssignmentService.branchCandidatePage(pageForm, settingId, keyword));
}
/**
* 查询可用于分配的疗休养配置。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result settingOptions(Integer year) {
return Result.success(tourUserAssignmentService.listEnabledSettingOptions(year));
}
/**
* 查询分工会可分配线路选项,选项携带事项、线路和旅行社快照信息。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result matterOptions(String settingId) {
return Result.success(tourUserAssignmentService.listMatterOptions(settingId));
}
/**
* 查询当前登录人所在分工会在指定配置下的名额使用情况。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result quotaInfo(String settingId) {
return Result.success(tourUserAssignmentService.branchQuotaInfo(settingId));
}
/**
* 保存分工会人员分配,保存时数据来源固定为 BRANCH_UNION。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "保存分工会人员分配")
public Result doAssign(String settingId, String matterId, String personType, @Param("userIds") String userIds) {
List<String> parsedUserIds;
try {
parsedUserIds = parseUserIds(userIds);
} catch (Exception e) {
return Result.error("人员参数错误");
}
try {
return Result.success(tourUserAssignmentService.assignBranchUsers(settingId, matterId, personType, parsedUserIds));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 切换当前分工会人员分配记录的正式/替补状态,具体名额和台账保护规则由 service 统一处理。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "切换分工会人员类型")
public Result switchPersonType(String id, String personType) {
try {
return Result.success(tourUserAssignmentService.switchCurrentBranchPersonType(id, personType));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 恢复当前分工会已取消退出的人员分配记录,只恢复状态,不恢复已删除台账。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment.cancelRestore")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "取消退出分工会人员分配")
public Result restoreCancel(String id) {
try {
tourUserAssignmentService.restoreCurrentBranchCancelledAssignment(id);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 删除前检查该分配记录是否已经存在对应报名台账。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result deleteInfo(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
return Result.success(tourUserAssignmentService.branchDeleteInfo(id));
}
/**
* 删除分工会人员分配记录,只删除 BRANCH_UNION 来源的数据。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "删除分工会人员分配")
public Result doDelete(String id, Boolean deleteLedger) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
try {
tourUserAssignmentService.deleteCurrentBranchAssignment(id, Boolean.TRUE.equals(deleteLedger));
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
private List<String> parseUserIds(String userIds) {
if (StrUtil.isBlank(userIds)) {
return Collections.emptyList();
}
List<String> list = Json.fromJsonAsList(String.class, userIds);
return Lang.isEmpty(list) ? Collections.emptyList() : list;
}
}
@@ -213,6 +213,7 @@ public class TourGroupController {
List<NutMap> list = listSql.getList(NutMap.class);
fillSignupMobile(list);
fillSignupFamilies(list);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@@ -362,6 +363,50 @@ public class TourGroupController {
}
}
/**
* 给报名人员列表挂载家属子列表;当前家属台账未保存手机号,mobile 字段先返回空值供前端占位。
*/
private void fillSignupFamilies(List<NutMap> list) {
if (list == null || list.isEmpty()) {
return;
}
List<String> ledgerIds = new ArrayList<>();
for (NutMap item : list) {
String ledgerId = item.getString("id", "");
if (StrUtil.isNotBlank(ledgerId)) {
ledgerIds.add(ledgerId);
}
}
if (ledgerIds.isEmpty()) {
return;
}
Sql sql = Sqls.create("""
SELECT
ledgerId,
familyName,
age,
gender,
'' AS mobile,
idCard,
relationship
FROM tour_ledger_family
WHERE delFlag = 0
AND ledgerId IN (@ledgerIds)
ORDER BY createdAt ASC
""");
sql.setParam("ledgerIds", ledgerIds.toArray(new String[0]));
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
Map<String, List<NutMap>> familyMap = new HashMap<>();
for (NutMap family : sql.getList(NutMap.class)) {
familyMap.computeIfAbsent(family.getString("ledgerId", ""), key -> new ArrayList<>()).add(family);
}
for (NutMap item : list) {
item.put("families", familyMap.getOrDefault(item.getString("id", ""), List.of()));
}
}
private Workbook buildParticipantsWorkbook(NutMap matter, List<NutMap> list) {
Workbook workbook = new HSSFWorkbook();
Sheet sheet = workbook.createSheet("参加人员名单");
@@ -0,0 +1,78 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLeaveApplyService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
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;
@IocBean
@Ok("json:full")
@At("/platform/tour/leaveApply")
public class TourLeaveApplyController {
@Inject
private TourLeaveApplyService leaveApplyService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/leaveApply/index.html")
@SaCheckPermission("tour.leaveApply")
public void index() {
}
/**
* 查询人员分配表中的退出取消数据,旧退出申请表不再作为业务来源。
*/
@At
@SaCheckPermission("tour.leaveApply")
public Result pageData(PageForm pageForm, String keyword, String unionName, String status) {
return Result.success(leaveApplyService.pageData(pageForm, keyword, unionName, status));
}
/**
* 查询当前登录人在取消管理页的可操作权限,供前端控制按钮显示。
*/
@At
@SaCheckPermission("tour.leaveApply")
public Result permissionInfo() {
return Result.success(leaveApplyService.permissionInfo());
}
/**
* 取消人员分配记录:标记已退出,并同步删除该人员对应路线的报名台账。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.leaveApply")
@SLog(type = "tour", tag = "疗休养退出取消", msg = "取消疗休养人员分配")
public Result doCancel(String id) {
try {
leaveApplyService.cancelAssignment(id);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 恢复人员分配记录退出状态,只恢复人员分配表状态,不恢复已删除台账。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment.cancelRestore")
@SLog(type = "tour", tag = "疗休养退出取消", msg = "恢复疗休养人员分配退出状态")
public Result doRestore(String id) {
try {
leaveApplyService.restoreAssignment(id);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
}
@@ -124,6 +124,10 @@ public class TourLedgerController {
IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0) AS directFamilyUnitLine,
dr.id AS directRelativeId,
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName,
COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS currentGender,
vu.age AS age,
COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS currentIdCard,
COALESCE(NULLIF(vu.mobile, ''), '') AS mobile,
IFNULL(f.familyCount, 0) AS familyCount,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
@@ -141,6 +145,7 @@ public class TourLedgerController {
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
LEFT JOIN (
SELECT
`year`,
@@ -172,7 +177,11 @@ public class TourLedgerController {
tourLedgerService.dao().execute(listSql);
var list = listSql.getList(NutMap.class);
list.forEach(item -> item.put("lineName", item.getString("currentLineName")));
list.forEach(item -> {
item.put("lineName", item.getString("currentLineName"));
item.put("gender", item.getString("currentGender"));
item.put("idCard", item.getString("currentIdCard"));
});
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@@ -313,6 +322,114 @@ public class TourLedgerController {
CommonDownloadUtil.download("参加人员导入模板.xlsx", workbook, response);
}
/**
* 按当前查询条件导出台账列表,导出字段与页面列表显示字样保持一致。
*/
@At
@SaCheckPermission("tour.ledger")
@Ok("void")
public void exportLedgerData(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly,
HttpServletResponse response) {
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("工号", "jobNo", 16));
entities.add(new ExcelExportEntity("姓名", "userName", 14));
entities.add(new ExcelExportEntity("性别", "gender", 10));
entities.add(new ExcelExportEntity("年龄", "age", 10));
entities.add(new ExcelExportEntity("身份证号", "idCard", 24));
entities.add(new ExcelExportEntity("手机号", "mobile", 18));
entities.add(new ExcelExportEntity("所在单位", "unitName", 26));
entities.add(new ExcelExportEntity("乘车地点", "boardingPlace", 18));
entities.add(new ExcelExportEntity("报名线路", "lineName", 30));
entities.add(new ExcelExportEntity("出行时段", "travelPeriod", 30));
entities.add(new ExcelExportEntity("线路类型", "lineType", 16));
entities.add(new ExcelExportEntity("报名时间", "signupTime", 22));
entities.add(new ExcelExportEntity("携带家属", "familyCountText", 14));
entities.add(new ExcelExportEntity("是否参加", "joinedText", 14));
entities.add(new ExcelExportEntity("报销超出费用", "overCostReimbursedText", 18));
List<NutMap> rows = queryLedgerExportRows(startYear, endYear, keyword, unionId, lineId, travelPeriod,
lineType, directFamilyOnly, overCostOnly);
rows.forEach(row -> {
Integer familyCount = row.getInt("familyCount");
row.put("familyCountText", (familyCount == null ? 0 : familyCount) + "");
row.put("joinedText", Boolean.TRUE.equals(row.getBoolean("joined")) ? "" : "");
row.put("overCostReimbursedText", Boolean.TRUE.equals(row.getBoolean("overCostReimbursed")) ? "" : "");
});
ExportParams exportParams = new ExportParams();
exportParams.setSheetName("疗休养台账");
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, rows);
CommonDownloadUtil.download("疗休养台账数据.xlsx", workbook, response);
}
private List<NutMap> queryLedgerExportRows(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly);
Sql sql = Sqls.create("""
SELECT
t.id,
t.jobNo,
t.userName,
COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS gender,
vu.age AS age,
COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS idCard,
COALESCE(NULLIF(vu.mobile, ''), '') AS mobile,
t.unitName,
t.boardingPlace,
COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE tp.travelPeriod
END AS travelPeriod,
t.lineType,
t.signupTime,
IFNULL(f.familyCount, 0) AS familyCount,
t.joined,
t.overCostReimbursed
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
GROUP BY t.id
ORDER BY t.signupTime DESC, t.createdAt DESC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return sql.getList(NutMap.class);
}
@At
@SaCheckPermission("tour.ledger")
@Ok("void")
@@ -263,6 +263,19 @@ public class TourMatterController {
.addv("maxGroupPeople", setting.getMaxGroupPeople()));
}
@At
@SaCheckPermission("tour.matter")
public Result settingBoardingPlaces(String settingId) {
if (StrUtil.isBlank(settingId)) {
return Result.error("参数错误");
}
TourSetting setting = tourMatterService.dao().fetch(TourSetting.class, settingId);
if (setting == null) {
return Result.error("疗休养配置不存在");
}
return Result.success(NutMap.NEW().addv("boardingPlace", StrUtil.blankToDefault(setting.getBoardingPlace(), "[]")));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.matter")
@@ -277,6 +290,8 @@ public class TourMatterController {
return Result.error("事项不存在");
}
oldMatter.setLineId(matter.getLineId());
// 默认乘车地点来自疗休养配置中的乘车地点列表,后续代报名和自主报名会优先带出该值。
oldMatter.setDefaultBoardingPlace(matter.getDefaultBoardingPlace());
oldMatter.setSignupStartTime(matter.getSignupStartTime());
oldMatter.setSignupEndTime(matter.getSignupEndTime());
oldMatter.setTravelStartTime(matter.getTravelStartTime());
@@ -31,6 +31,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
@@ -84,6 +85,9 @@ public class TourMySignupController {
@Inject
private TourMatterService tourMatterService;
@Inject
private TourUserAssignmentService tourUserAssignmentService;
@Inject
private SysDictService sysDictService;
@@ -284,6 +288,7 @@ public class TourMySignupController {
m.`year`,
m.matterName,
m.unionId AS matterUnionId,
m.defaultBoardingPlace,
m.travelStartTime,
m.travelEndTime,
CASE
@@ -299,7 +304,8 @@ public class TourMySignupController {
a.agencyName AS travelAgencyName,
IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement,
s.allowFamily,
IFNULL(s.fillBedInfo, 1) AS fillBedInfo
IFNULL(s.fillBedInfo, 1) AS fillBedInfo,
s.boardingPlace AS boardingPlaceOptions
FROM tour_matter m
INNER JOIN tour_line l ON l.id = m.lineId
LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId
@@ -408,6 +414,15 @@ public class TourMySignupController {
return Result.success(sql.getList(Sys_dict.class));
}
/**
* 我的报名修改接口,PC 和 H5 共用。
* 修改成功后更新疗休养台账,并回填当前用户已存在的人员分配记录事项快照。
*
* @param ledger 报名台账主信息
* @param families 家属信息 JSON 数组
* @param directRelative 直系亲属线路报名信息 JSON
* @return 修改结果;直系亲属线路、超额报销等场景会返回待审核提示
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR)
@@ -440,6 +455,10 @@ public class TourMySignupController {
TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId());
boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine());
TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
Result boardingPlaceResult = normalizeBoardingPlace(ledger, matter, setting);
if (boardingPlaceResult != null) {
return boardingPlaceResult;
}
if (setting != null && Boolean.FALSE.equals(setting.getFillBedInfo())) {
clearBedInfo(ledger, familyList);
}
@@ -468,6 +487,8 @@ public class TourMySignupController {
fillStaffInfo(ledger);
tourLedgerService.updateIgnoreNull(ledger);
// 报名台账更新后,仅回填已存在的人员分配记录事项信息,不新增记录、不改变分配来源。
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), ledger.getBoardingPlace());
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
if (Lang.isNotEmpty(familyList)) {
@@ -511,6 +532,7 @@ public class TourMySignupController {
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
clearAssignmentMatterAfterCancel(ledger);
return Result.success().addMsg("取消报名成功");
}
@@ -526,9 +548,21 @@ public class TourMySignupController {
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
clearAssignmentMatterAfterCancel(ledger);
return Result.success().addMsg("取消报名成功");
}
private void clearAssignmentMatterAfterCancel(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) {
return;
}
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
if (matter == null || StrUtil.isBlank(matter.getSettingId())) {
return;
}
tourUserAssignmentService.clearExistingAssignmentMatterAfterCancel(matter.getSettingId(), SecurityUtil.getUserId());
}
@At
@SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR)
public Result lineOptions(Integer startYear, Integer endYear) {
@@ -590,6 +624,60 @@ public class TourMySignupController {
return null;
}
/**
* 修改报名时重新校验乘车地点,确保提交值仍来自当前疗休养配置的乘车地点列表。
*/
private Result normalizeBoardingPlace(TourLedger ledger, TourMatter matter, TourSetting setting) {
if (ledger == null || setting == null) {
return null;
}
List<String> options = parseBoardingPlaceOptions(setting.getBoardingPlace());
if (Lang.isEmpty(options)) {
ledger.setBoardingPlace("");
return null;
}
String boardingPlace = StrUtil.blankToDefault(ledger.getBoardingPlace(), matter == null ? "" : matter.getDefaultBoardingPlace());
if (StrUtil.isBlank(boardingPlace)) {
return Result.error("请选择乘车地点");
}
if (!options.contains(boardingPlace)) {
return Result.error("乘车地点不在当前疗休养配置范围内");
}
ledger.setBoardingPlace(boardingPlace);
return null;
}
private List<String> parseBoardingPlaceOptions(String value) {
if (StrUtil.isBlank(value)) {
return Collections.emptyList();
}
try {
List<?> list = Json.fromJson(List.class, value);
if (Lang.isEmpty(list)) {
return Collections.emptyList();
}
return list.stream().map(item -> {
if (item instanceof String) {
return ((String) item).trim();
}
if (item instanceof java.util.Map) {
Object name = ((java.util.Map<?, ?>) item).get("name");
return name == null ? "" : String.valueOf(name).trim();
}
return "";
})
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
} catch (Exception e) {
return java.util.Arrays.stream(value.split(","))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
}
}
private Result checkSignupRule(TourLedger ledger, TourMatter matter, TourLedger oldLedger) {
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) {
return Result.error("报名出行时段不存在或已停用");
@@ -0,0 +1,207 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Collections;
import java.util.List;
@IocBean
@Ok("json:full")
@At("/platform/tour/schoolUserAssignment")
public class TourSchoolUserAssignmentController {
@Inject
private TourUserAssignmentService tourUserAssignmentService;
/**
* 校工会人员分配列表入口。
*/
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/schoolUserAssignment/index.html")
@SaCheckPermission("tour.schoolUserAssignment")
public void index() {
}
/**
* 分页查询校工会分配记录,列表数据只来自人员分配表。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result pageData(PageForm pageForm, Integer year, String settingId, String matterId, String unionId,
String personType, String keyword) {
return Result.success(tourUserAssignmentService.schoolAssignmentPage(pageForm, year, settingId, matterId,
unionId, personType, keyword));
}
/**
* 分页查询候选人员,候选范围由疗休养配置的可参加人员范围决定。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result candidatePageData(PageForm pageForm, String settingId, String unionId, String keyword) {
return Result.success(tourUserAssignmentService.schoolCandidatePage(pageForm, settingId, unionId, keyword));
}
/**
* 查询可用于分配的疗休养配置。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result settingOptions(Integer year) {
return Result.success(tourUserAssignmentService.listEnabledSettingOptions(year));
}
/**
* 查询校工会可分配线路选项,选项携带事项、线路和旅行社快照信息。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result matterOptions(String settingId) {
return Result.success(tourUserAssignmentService.listMatterOptions(settingId));
}
/**
* 查询分工会选项,供筛选和候选人员过滤使用。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result unionOptions() {
return Result.success(tourUserAssignmentService.listUnionOptions());
}
/**
* 保存校工会人员分配,保存时数据来源固定为 SCHOOL_UNION。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "保存校工会人员分配")
public Result doAssign(String settingId, String matterId, String personType, @Param("userIds") String userIds) {
List<String> parsedUserIds;
try {
parsedUserIds = parseUserIds(userIds);
} catch (Exception e) {
return Result.error("人员参数错误");
}
try {
return Result.success(tourUserAssignmentService.assignSchoolUsers(settingId, matterId, personType, parsedUserIds));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 保存校工会人员分配明细,支持弹窗候选列表中每个人单独选择分配线路。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "保存校工会人员分配明细")
public Result doAssignItems(String settingId, @Param("assignItems") String assignItems) {
List<NutMap> parsedItems;
try {
parsedItems = parseAssignItems(assignItems);
} catch (Exception e) {
return Result.error("人员分配明细参数错误");
}
try {
return Result.success(tourUserAssignmentService.assignSchoolUsers(settingId, parsedItems));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 给校工会已分配人员补选分配线路,具体回写分配表和写台账逻辑由 service 统一处理。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "选择校工会分配事项")
public Result selectMatter(String id, String matterId) {
try {
return Result.success(tourUserAssignmentService.selectSchoolAssignmentMatter(id, matterId));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 恢复校工会已取消退出的人员分配记录,只恢复状态,不恢复已删除台账。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment.cancelRestore")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "取消退出校工会人员分配")
public Result restoreCancel(String id) {
try {
tourUserAssignmentService.restoreCancelledAssignmentBySource(id, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 删除前检查该分配记录是否已经存在对应报名台账。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result deleteInfo(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
return Result.success(tourUserAssignmentService.schoolDeleteInfo(id));
}
/**
* 删除校工会人员分配记录,只删除 SCHOOL_UNION 来源的数据。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "删除校工会人员分配")
public Result doDelete(String id, Boolean deleteLedger) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
try {
tourUserAssignmentService.deleteBySource(id, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION, Boolean.TRUE.equals(deleteLedger));
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
private List<String> parseUserIds(String userIds) {
if (StrUtil.isBlank(userIds)) {
return Collections.emptyList();
}
List<String> list = Json.fromJsonAsList(String.class, userIds);
return Lang.isEmpty(list) ? Collections.emptyList() : list;
}
private List<NutMap> parseAssignItems(String assignItems) {
if (StrUtil.isBlank(assignItems)) {
return Collections.emptyList();
}
List<NutMap> list = Json.fromJsonAsList(NutMap.class, assignItems);
return Lang.isEmpty(list) ? Collections.emptyList() : list;
}
}
@@ -9,6 +9,7 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingLot;
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.ioc.aop.Aop;
@@ -16,6 +17,7 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
@@ -29,9 +31,15 @@ import java.util.stream.Collectors;
@At("/platform/tour/setting")
public class TourSettingController {
private static final String SIGNUP_ELIGIBILITY_MODE_SCOPE_GROUP = "SCOPE_GROUP";
private static final String SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER = "ASSIGNED_USER";
@Inject
private TourSettingService tourSettingService;
@Inject
private TourUserAssignmentService tourUserAssignmentService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/setting/index.html")
@SaCheckPermission("tour.setting")
@@ -69,6 +77,7 @@ public class TourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(id));
return Result.success(tourSetting);
}
@@ -89,15 +98,31 @@ public class TourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(tourSetting.getId()));
return Result.success(tourSetting);
}
@At
@SaCheckPermission("tour.setting")
public Result unionQuotaRows(String settingId) {
// 新增配置时 settingId 为空,service 会返回所有分工会的空名额行;编辑时合并已保存名额。
return Result.success(tourSettingService.listUnionQuotaRows(settingId));
}
@At
@SaCheckPermission("tour.setting")
public Result unionQuotaOverview(String settingId) {
int schoolFormalAssignedCount = tourUserAssignmentService.countSchoolFormalAssignedUsers(settingId);
return Result.success(NutMap.NEW().addv("schoolFormalAssignedCount", schoolFormalAssignedCount));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.setting")
@SLog(type = "tour", tag = "疗休养设置", msg = "保存疗休养配置")
public Result doSubmit(TourSetting tourSetting,
@Param(value = "lots") String lots,
@Param(value = "unionQuotas") String unionQuotas,
@Param(value = "lotDeleteList") String[] lotDeleteList) {
Result checkResult = check(tourSetting);
if (checkResult != null) {
@@ -114,6 +139,13 @@ public class TourSettingController {
}
// 布尔值给默认值,避免前端未传时出现空状态。
int schoolFormalAssignedCount = tourUserAssignmentService.countSchoolFormalAssignedUsers(tourSetting.getId());
int branchTotalQuota = Math.max(defaultInt(tourSetting.getTravelPeopleQuota()) - schoolFormalAssignedCount, 0);
String quotaLimitMessage = tourSettingService.checkBranchFormalQuotaLimit(unionQuotas, branchTotalQuota);
if (StrUtil.isNotBlank(quotaLimitMessage)) {
return Result.error(quotaLimitMessage);
}
if (tourSetting.getEnabled() == null) {
tourSetting.setEnabled(true);
}
@@ -123,6 +155,10 @@ public class TourSettingController {
if (tourSetting.getFillBedInfo() == null) {
tourSetting.setFillBedInfo(true);
}
// 报名资格校验方式默认按人员分配表校验,后续报名校验切换会读取该配置。
if (StrUtil.isBlank(tourSetting.getSignupEligibilityMode())) {
tourSetting.setSignupEligibilityMode(SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER);
}
if (StrUtil.isBlank(tourSetting.getOutProvinceRatioType())) {
tourSetting.setOutProvinceRatioType("当年报名人数");
}
@@ -148,6 +184,7 @@ public class TourSettingController {
} else {
tourSettingService.insertWith(tourSetting, "lots");
}
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
} else {
// 编辑时先处理页面删除的标段,再保存配置和当前标段行。
if (Lang.isNotEmpty(lotDeleteList)) {
@@ -155,6 +192,7 @@ public class TourSettingController {
}
tourSettingService.updateIgnoreNull(tourSetting);
saveLots(tourSetting);
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
}
return Result.success();
}
@@ -168,6 +206,8 @@ public class TourSettingController {
return Result.error("参数错误");
}
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id));
tourSettingService.clearUnionQuotas(id);
tourUserAssignmentService.clearBySettingId(id);
tourSettingService.delete(id);
return Result.success();
}
@@ -205,6 +245,18 @@ public class TourSettingController {
if (StrUtil.isBlank(tourSetting.getConfigName())) {
return Result.error("配置名称不能为空");
}
Result boardingPlaceCheck = normalizeBoardingPlace(tourSetting);
if (boardingPlaceCheck != null) {
return boardingPlaceCheck;
}
if (tourSetting.getTravelPeopleQuota() != null && tourSetting.getTravelPeopleQuota() < 0) {
return Result.error("出行人数指标不能小于0");
}
if (StrUtil.isNotBlank(tourSetting.getSignupEligibilityMode())
&& !SIGNUP_ELIGIBILITY_MODE_SCOPE_GROUP.equals(tourSetting.getSignupEligibilityMode())
&& !SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER.equals(tourSetting.getSignupEligibilityMode())) {
return Result.error("报名资格校验方式不正确");
}
if (tourSetting.getMinGroupPeople() != null && tourSetting.getMaxGroupPeople() != null
&& tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) {
return Result.error("最少成团人数不能大于最多成团人数");
@@ -227,6 +279,33 @@ public class TourSettingController {
return null;
}
private int defaultInt(Integer value) {
return value == null || value < 0 ? 0 : value;
}
private Result normalizeBoardingPlace(TourSetting tourSetting) {
if (StrUtil.isBlank(tourSetting.getBoardingPlace())) {
tourSetting.setBoardingPlace("[]");
return null;
}
try {
List<NutMap> rows = Json.fromJsonAsList(NutMap.class, tourSetting.getBoardingPlace());
if (Lang.isEmpty(rows)) {
tourSetting.setBoardingPlace("[]");
return null;
}
// 乘车路线以 JSON 数组保存,只保留有效路线名称,避免页面空行写入配置。
List<NutMap> normalizedRows = rows.stream()
.filter(item -> item != null && StrUtil.isNotBlank(item.getString("name")))
.map(item -> NutMap.NEW().addv("name", item.getString("name").trim()))
.collect(Collectors.toList());
tourSetting.setBoardingPlace(Json.toJson(normalizedRows));
return null;
} catch (Exception e) {
return Result.error("乘车路线格式不正确");
}
}
private Result checkLots(List<TourSettingLot> lots) {
if (Lang.isEmpty(lots)) {
return null;
@@ -30,6 +30,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
@@ -63,6 +64,9 @@ public class TourSignupController {
@Inject
private TourMatterService tourMatterService;
@Inject
private TourUserAssignmentService tourUserAssignmentService;
@Inject
private TourLedgerService tourLedgerService;
@@ -405,6 +409,7 @@ public class TourSignupController {
m.`year`,
m.matterName,
m.unionId AS matterUnionId,
m.defaultBoardingPlace,
m.travelStartTime,
m.travelEndTime,
m.contactName,
@@ -421,7 +426,8 @@ public class TourSignupController {
a.agencyName AS travelAgencyName,
IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement,
s.allowFamily,
IFNULL(s.fillBedInfo, 1) AS fillBedInfo
IFNULL(s.fillBedInfo, 1) AS fillBedInfo,
s.boardingPlace AS boardingPlaceOptions
FROM tour_matter m
INNER JOIN tour_line l ON l.id = m.lineId
LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId
@@ -484,6 +490,13 @@ public class TourSignupController {
.addv("directRelative", directRelative));
}
/**
* 报名前校验接口PC H5 共用
* 用于在进入报名表单前校验事项线路配置状态人员范围重复报名和线路报名规则
*
* @param matterId 疗休养事项ID
* @return 可报名返回成功不可报名返回对应业务提示
*/
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result signupEligibilityNotice(String matterId) {
@@ -502,9 +515,13 @@ public class TourSignupController {
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
return Result.error("报名事项配置不存在或已停用");
}
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
Result timeResult = checkSignupTime(matter);
if (timeResult != null) {
return timeResult;
}
Result scopeResult = checkSignupEligibilityScope(setting);
if (scopeResult != null) {
return scopeResult;
}
TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId());
CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger);
@@ -532,7 +549,7 @@ public class TourSignupController {
.addv("currentLineCost", cycleTotalCost.currentCost())
.addv("message", buildCycleTotalCostNotice(cycleTotalCost)));
}
if (!isOutProvinceLine(line.getLineType())) {
if (isAssignedUserEligibility(setting) || !isOutProvinceLine(line.getLineType())) {
return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false));
}
OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger);
@@ -570,9 +587,13 @@ public class TourSignupController {
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
return Result.error("报名事项配置不存在或已停用");
}
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
Result timeResult = checkSignupTime(matter);
if (timeResult != null) {
return timeResult;
}
Result scopeResult = checkSignupEligibilityScope(setting);
if (scopeResult != null) {
return scopeResult;
}
TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId());
OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger);
@@ -627,6 +648,15 @@ public class TourSignupController {
return Result.success(sql.getList(Sys_dict.class));
}
/**
* 报名提交接口PC H5 共用
* 提交成功后写入或更新疗休养台账并回填当前用户已存在的人员分配记录事项快照
*
* @param ledger 报名台账主信息
* @param families 家属信息 JSON 数组
* @param directRelative 直系亲属线路报名信息 JSON
* @return 报名或修改结果直系亲属线路超额报销等场景会返回待审核提示
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
@@ -668,6 +698,10 @@ public class TourSignupController {
TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId());
boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine());
TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
Result boardingPlaceResult = normalizeBoardingPlace(ledger, matter, setting);
if (boardingPlaceResult != null) {
return boardingPlaceResult;
}
if (setting != null && Boolean.FALSE.equals(setting.getFillBedInfo())) {
clearBedInfo(ledger, familyList);
}
@@ -707,6 +741,8 @@ public class TourSignupController {
} else {
tourLedgerService.insert(ledger);
}
// 报名台账落库后仅回填已存在的人员分配记录事项信息不新增记录不改变分配来源
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), ledger.getBoardingPlace());
if (Lang.isNotEmpty(familyList)) {
familyList.forEach(item -> {
item.setLedgerId(ledger.getId());
@@ -754,9 +790,21 @@ public class TourSignupController {
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
clearAssignmentMatterAfterCancel(ledger);
return Result.success().addMsg("取消报名成功");
}
private void clearAssignmentMatterAfterCancel(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) {
return;
}
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
if (matter == null || StrUtil.isBlank(matter.getSettingId())) {
return;
}
tourUserAssignmentService.clearExistingAssignmentMatterAfterCancel(matter.getSettingId(), SecurityUtil.getUserId());
}
private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId) {
Cnd cnd = Cnd.NEW();
cnd.and("m.delFlag", "=", false);
@@ -883,19 +931,13 @@ public class TourSignupController {
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
return Result.error("报名事项配置不存在或已停用");
}
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
Result timeResult = checkSignupTime(matter);
if (timeResult != null) {
return timeResult;
}
if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) {
return Result.error("报名时间未配置");
}
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) {
return Result.error("报名未开始");
}
if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) {
return Result.error("报名已结束");
Result scopeResult = checkSignupEligibilityScope(setting);
if (scopeResult != null) {
return scopeResult;
}
CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger);
if (!cycleAllowedTimes.canApply()) {
@@ -906,7 +948,7 @@ public class TourSignupController {
return Result.error(buildCycleTotalCostNotice(cycleTotalCost));
}
String jobNo = currentJobNo();
if (isOutProvinceLine(line.getLineType())) {
if (!isAssignedUserEligibility(setting) && isOutProvinceLine(line.getLineType())) {
int startYear = setting.getCycleStartYear() == null ? matter.getYear() : setting.getCycleStartYear();
int endYear = LocalDate.now().getYear();
Cnd outProvinceCnd = Cnd.where(TourLedger::getDelFlag, "=", false)
@@ -1111,6 +1153,101 @@ public class TourSignupController {
return "当年报名人数";
}
private boolean isAssignedUserEligibility(TourSetting setting) {
return setting != null && "ASSIGNED_USER".equals(setting.getSignupEligibilityMode());
}
/**
* 统一校验疗休养事项报名时间确保资格名单校验在时间校验通过后再执行
*/
private Result checkSignupTime(TourMatter matter) {
if (matter == null || StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) {
return Result.error("报名时间未配置");
}
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) {
return Result.error("报名未开始");
}
if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) {
return Result.error("报名已结束");
}
return null;
}
/**
* 报名乘车地点以疗休养配置中的乘车地点列表为准
* 页面未传值时优先使用事项默认乘车地点避免历史页面或代入默认值时丢失乘车地点
*/
private Result normalizeBoardingPlace(TourLedger ledger, TourMatter matter, TourSetting setting) {
if (ledger == null || setting == null) {
return null;
}
List<String> options = parseBoardingPlaceOptions(setting.getBoardingPlace());
if (Lang.isEmpty(options)) {
ledger.setBoardingPlace("");
return null;
}
String boardingPlace = StrUtil.blankToDefault(ledger.getBoardingPlace(), matter == null ? "" : matter.getDefaultBoardingPlace());
if (StrUtil.isBlank(boardingPlace)) {
return Result.error("请选择乘车地点");
}
if (!options.contains(boardingPlace)) {
return Result.error("乘车地点不在当前疗休养配置范围内");
}
ledger.setBoardingPlace(boardingPlace);
return null;
}
private List<String> parseBoardingPlaceOptions(String value) {
if (StrUtil.isBlank(value)) {
return Collections.emptyList();
}
try {
List<?> list = Json.fromJson(List.class, value);
if (Lang.isEmpty(list)) {
return Collections.emptyList();
}
return list.stream().map(item -> {
if (item instanceof String) {
return ((String) item).trim();
}
if (item instanceof java.util.Map) {
Object name = ((java.util.Map<?, ?>) item).get("name");
return name == null ? "" : String.valueOf(name).trim();
}
return "";
})
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
} catch (Exception e) {
return java.util.Arrays.stream(value.split(","))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
}
}
/**
* 根据疗休养配置的报名资格方式切换校验来源
* ASSIGNED_USER 模式读取人员分配表并要求当前用户为正式人员否则沿用可参加人员范围校验
*/
private Result checkSignupEligibilityScope(TourSetting setting) {
if (isAssignedUserEligibility(setting)) {
NutMap assignmentResult = tourUserAssignmentService.checkFormalAssignedUser(setting.getId(), SecurityUtil.getUserId());
if (!assignmentResult.getBoolean("canApply")) {
return Result.error(defaultIfBlank(assignmentResult.getString("message"), "您不在本次疗休养人员分配名单中,无法报名"));
}
return null;
}
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
}
return null;
}
private String buildOutProvinceQuotaNotice(OutProvinceQuota quota) {
String suffix = quota.canApply() ? "您可以报名,或您稍后报名" : "请稍后报名";
if ("当年报名人数".equals(quota.ratioType())) {
@@ -0,0 +1,103 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养退出申请
* 仅作为不能参加人员的维护台账不直接关联报名疗休养台账人员分配等业务流程
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_leave_apply")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("疗休养退出申请")
public class TourLeaveApply extends BaseModel implements Serializable {
public static final String STATUS_PENDING = "PENDING";
public static final String STATUS_APPROVED = "APPROVED";
public static final String STATUS_REJECTED = "REJECTED";
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("疗休养事项ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String matterId;
@Column
@Comment("线路ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lineId;
@Column
@Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String jobNo;
@Column
@Comment("姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String userName;
@Column
@Comment("用户ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("所属工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("所属工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("不能参加原因")
@ColDefine(type = ColType.TEXT)
private String reason;
@Column
@Comment("状态")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String status;
@Column
@Comment("审核人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String auditBy;
@Column
@Comment("审核人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String auditName;
@Column
@Comment("审核时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String auditTime;
@Column
@Comment("审核备注")
@ColDefine(type = ColType.TEXT)
private String auditRemark;
}
@@ -111,6 +111,11 @@ public class TourLedger extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 100)
private String travelAgencyName;
@Column
@Comment("乘车地点")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String boardingPlace;
@Column
@Comment("是否携带家属")
@ColDefine(type = ColType.BOOLEAN)
@@ -71,6 +71,11 @@ public class TourMatter extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lineId;
@Column
@Comment("默认乘车地点")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String defaultBoardingPlace;
@Column
@Comment("报名开始时间")
@ColDefine(type = ColType.VARCHAR, width = 20)
@@ -43,11 +43,28 @@ public class TourSetting extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 50)
private String tourType;
@Column
@Comment("乘车地点列表JSON")
@ColDefine(type = ColType.TEXT)
private String boardingPlace;
@Column
@Comment("出行人数指标")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer travelPeopleQuota;
@Column
@Comment("可参加人员范围ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String activityGroupId;
@Column
@Comment("报名资格校验方式")
@ColDefine(type = ColType.VARCHAR, width = 30)
@Default("'ASSIGNED_USER'")
private String signupEligibilityMode;
@Column
@Comment("排序编号")
@ColDefine(type = ColType.INT)
@@ -127,6 +144,11 @@ public class TourSetting extends BaseModel implements Serializable {
@Many(field = "settingId")
private List<TourSettingLot> lots;
/**
* 分工会名额分配仅用于配置弹窗回显与提交不作为 tour_setting 表字段保存
*/
private List<TourSettingUnionQuota> unionQuotas;
@Column
@Comment("服务须知")
@ColDefine(type = ColType.TEXT)
@@ -0,0 +1,66 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Default;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养配置下的分工会名额分配
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_setting_union_quota")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("疗休养配置分工会名额分配")
public class TourSettingUnionQuota extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("所属疗休养配置ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String settingId;
@Column
@Comment("分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("分工会名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("正式人员名额")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer formalQuota;
@Column
@Comment("替补人员名额")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer backupQuota;
/**
* 页面展示用实时会员数不落库
*/
private Integer memberCount;
}
@@ -0,0 +1,155 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Default;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养人员分配表
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_user_assignment")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("疗休养人员分配")
public class TourUserAssignment extends BaseModel implements Serializable {
public static final String ASSIGN_SOURCE_SCHOOL_UNION = "SCHOOL_UNION";
public static final String ASSIGN_SOURCE_BRANCH_UNION = "BRANCH_UNION";
public static final String PERSON_TYPE_FORMAL = "FORMAL";
public static final String PERSON_TYPE_BACKUP = "BACKUP";
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("疗休养配置ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String settingId;
@Column
@Comment("疗休养事项ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String matterId;
@Column
@Comment("疗休养事项名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String matterName;
@Column
@Comment("旅行社ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String travelAgencyId;
@Column
@Comment("旅行社名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String travelAgencyName;
@Column
@Comment("线路ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lineId;
@Column
@Comment("线路名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String lineName;
@Column
@Comment("乘车地点")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String boardingPlace;
@Column
@Comment("人员ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String loginName;
@Column
@Comment("姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String userName;
@Column
@Comment("性别")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String gender;
@Column
@Comment("手机号")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String mobile;
@Column
@Comment("所在单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unitId;
@Column
@Comment("所在单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unitName;
@Column
@Comment("所在分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("所在分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("分配来源")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String assignSource;
@Column
@Comment("人员类型")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String personType;
@Column
@Comment("是否已退出")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean cancelled;
@Column
@Comment("分配人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String assignedBy;
@Column
@Comment("分配人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String assignedName;
@Column
@Comment("分配时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String assignedAt;
}
@@ -0,0 +1,31 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLeaveApply;
import org.nutz.lang.util.NutMap;
public interface TourLeaveApplyService extends BaseService<TourLeaveApply> {
/**
* 分页查询人员分配表中的退出/取消管理数据关键词同时匹配工号和姓名
* 数据权限系统管理员校工会主席看全部分工会主席看本分工会普通用户看本人
*/
Pagination<NutMap> pageData(PageForm pageForm, String keyword, String unionName, String status);
/**
* 查询当前登录人在退出取消管理页的按钮权限
*/
NutMap permissionInfo();
/**
* 取消指定人员分配记录同时删除对应报名台账并标记已退出
*/
void cancelAssignment(String id);
/**
* 恢复指定人员分配记录的退出状态不恢复已删除台账
*/
void restoreAssignment(String id);
}
@@ -2,6 +2,41 @@ package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota;
import java.util.List;
public interface TourSettingService extends BaseService<TourSetting> {
/**
* 查询所有分工会在指定疗休养配置下的名额实时补充当前工会会员数供页面分配时参考
*
* @param settingId 疗休养配置ID新增时可为空
* @return 按分工会编码排序后的名额列表
*/
List<TourSettingUnionQuota> listUnionQuotaRows(String settingId);
/**
* 保存指定疗休养配置下的分工会名额前端传入的是 JSON 数组字符串
*
* @param settingId 疗休养配置ID
* @param unionQuotas 分工会名额 JSON
*/
void saveUnionQuotas(String settingId, String unionQuotas);
/**
* 校验分工会正式人员名额合计是否超过分工会总名额
*
* @param unionQuotas 分工会名额 JSON
* @param branchTotalQuota 分工会总名额
* @return 通过返回空字符串不通过返回业务提示
*/
String checkBranchFormalQuotaLimit(String unionQuotas, int branchTotalQuota);
/**
* 清理指定疗休养配置下的分工会名额
*
* @param settingId 疗休养配置ID
*/
void clearUnionQuotas(String settingId);
}
@@ -0,0 +1,285 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment;
import org.nutz.lang.util.NutMap;
import java.util.List;
public interface TourUserAssignmentService extends BaseService<TourUserAssignment> {
/**
* 分页查询校工会人员分配记录列表只读取人员分配表不读取报名台账
*
* @param pageForm 分页排序参数
* @param year 疗休养年度
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID
* @param unionId 所属分工会ID
* @param personType 人员类型FORMAL 正式人员BACKUP 替补人员
* @param keyword 姓名或工号关键字
* @return 校工会分配记录分页数据
*/
Pagination<NutMap> schoolAssignmentPage(PageForm pageForm, Integer year, String settingId, String matterId,
String unionId, String personType, String keyword);
/**
* 分页查询校工会可分配候选人候选人来自疗休养配置的可参加人员范围并排除同一配置下已分配人员
*
* @param pageForm 分页排序参数
* @param settingId 疗休养配置ID
* @param unionId 所属分工会ID
* @param keyword 姓名或工号关键字
* @return 可分配候选人分页数据
*/
Pagination<NutMap> schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword);
/**
* 分页查询当前登录人所在分工会的人员分配记录列表只读取人员分配表不读取报名台账
*
* @param pageForm 分页排序参数
* @param year 疗休养年度
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID
* @param personType 人员类型FORMAL 正式人员BACKUP 替补人员
* @param keyword 姓名或工号关键字
* @return 分工会分配记录分页数据
*/
Pagination<NutMap> branchAssignmentPage(PageForm pageForm, Integer year, String settingId, String matterId,
String personType, String keyword);
/**
* 分页查询当前登录人所在分工会可分配候选人候选人来自疗休养配置的可参加人员范围
*
* @param pageForm 分页排序参数
* @param settingId 疗休养配置ID
* @param keyword 姓名或工号关键字
* @return 当前分工会可分配候选人分页数据
*/
Pagination<NutMap> branchCandidatePage(PageForm pageForm, String settingId, String keyword);
/**
* 查询启用的疗休养配置选项供人员分配列表筛选和分配弹窗复用
*
* @param year 疗休养年度
* @return 配置选项列表
*/
List<NutMap> listEnabledSettingOptions(Integer year);
/**
* 查询指定配置下已配置线路的分配线路选项用于人员分配时指定事项线路和旅行社
*
* @param settingId 疗休养配置ID
* @return 事项选项列表
*/
List<NutMap> listMatterOptions(String settingId);
/**
* 查询分工会选项供校工会分配候选人和列表筛选使用
*
* @return 分工会选项列表
*/
List<NutMap> listUnionOptions();
/**
* 保存校工会人员分配保存前会重新按配置可参加人员范围过滤防止写入范围外人员
* 分配线路为可选正式人员选择线路时视为代报名并同步写入疗休养台账替补人员不能分配线路
*
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID可为空
* @param personType 人员类型FORMAL 正式人员BACKUP 替补人员
* @param userIds 待分配人员ID列表
* @return 保存结果包含实际分配数量跳过数量和代报名台账写入数量
*/
NutMap assignSchoolUsers(String settingId, String matterId, String personType, List<String> userIds);
/**
* 保存校工会人员分配支持每个候选人员单独选择分配线路
* assignItems 中每项包含 userIdmatterIdmatterId 为空时只保存人员分配不写台账
*
* @param settingId 疗休养配置ID
* @param assignItems 人员和分配线路明细
* @return 保存结果包含实际分配数量跳过数量和代报名台账写入数量
*/
NutMap assignSchoolUsers(String settingId, List<NutMap> assignItems);
/**
* 给校工会已分配的正式人员补选分配线路并按代报名写入疗休养台账
* 只处理 SCHOOL_UNION 来源且尚未选择线路的分配记录避免覆盖既有台账
*
* @param id 人员分配记录ID
* @param matterId 疗休养事项ID
* @return 处理结果包含台账写入数量
*/
NutMap selectSchoolAssignmentMatter(String id, String matterId);
/**
* 保存分工会人员分配保存前会重新按配置可参加人员范围和当前登录人所在分工会过滤并校验正式/替补名额
* 疗休养事项为可选正式人员选择事项时视为代报名并同步写入疗休养台账替补人员不能分配事项
*
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID可为空
* @param personType 人员类型FORMAL 正式人员BACKUP 替补人员
* @param userIds 待分配人员ID列表
* @return 保存结果包含实际分配数量跳过数量代报名台账写入数量和剩余名额
*/
NutMap assignBranchUsers(String settingId, String matterId, String personType, List<String> userIds);
/**
* 切换当前分工会人员分配记录的正式/替补状态
* 切换时会校验当前登录人所在分工会分配来源和目标类型剩余名额已分配事项的正式人员不能切换为替补
*
* @param id 人员分配记录ID
* @param personType 目标人员类型FORMAL 正式人员BACKUP 替补人员
* @return 切换后的人员类型和最新名额信息
*/
NutMap switchCurrentBranchPersonType(String id, String personType);
/**
* 用户报名写入台账后回填该用户在同一疗休养配置下已存在的人员分配记录
* 仅更新事项线路旅行社快照字段不新增记录不修改 assignSource 和人员类型
*
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID
* @param userId 报名用户ID
* @param boardingPlace 用户报名时最终选择的乘车地点
* @return 更新记录数
*/
int backfillExistingAssignmentAfterSignup(String settingId, String matterId, String userId, String boardingPlace);
/**
* 用户取消报名删除台账后清空该用户在同一疗休养配置下已存在人员分配记录的事项快照
* 仅清空事项线路旅行社字段不删除记录不修改 assignSource 和人员类型
*
* @param settingId 疗休养配置ID
* @param userId 报名用户ID
* @return 更新记录数
*/
int clearExistingAssignmentMatterAfterCancel(String settingId, String userId);
/**
* 查询当前登录人所在分工会在指定疗休养配置下的名额使用情况
*
* @param settingId 疗休养配置ID
* @return 正式/替补名额已用名额和剩余名额
*/
NutMap branchQuotaInfo(String settingId);
/**
* 查询校工会人员分配记录是否已存在对应报名台账用于删除前二次确认
*
* @param id 人员分配记录ID
* @return hasLedger 表示是否存在台账ledgerCount 表示对应台账数量
*/
NutMap schoolDeleteInfo(String id);
/**
* 查询当前分工会人员分配记录是否已存在对应报名台账用于删除前二次确认
*
* @param id 人员分配记录ID
* @return hasLedger 表示是否存在台账ledgerCount 表示对应台账数量
*/
NutMap branchDeleteInfo(String id);
/**
* 删除指定来源的人员分配记录避免校工会页面误删分工会分配的数据
*
* @param id 人员分配记录ID
* @param assignSource 分配来源
*/
void deleteBySource(String id, String assignSource);
/**
* 删除指定来源的人员分配记录可选择是否同步删除该分配记录对应的报名台账
*
* @param id 人员分配记录ID
* @param assignSource 分配来源
* @param deleteLedger 是否同步删除 matterId + loginName 对应的台账及明细
*/
void deleteBySource(String id, String assignSource, boolean deleteLedger);
/**
* 删除当前登录人所在分工会的人员分配记录避免分工会页面删除其它分工会的数据
*
* @param id 人员分配记录ID
*/
void deleteCurrentBranchAssignment(String id);
/**
* 删除当前登录人所在分工会的人员分配记录可选择是否同步删除该分配记录对应的报名台账
*
* @param id 人员分配记录ID
* @param deleteLedger 是否同步删除 matterId + loginName 对应的台账及明细
*/
void deleteCurrentBranchAssignment(String id, boolean deleteLedger);
/**
* 判断用户是否已存在于指定疗休养配置的人员分配表中
*
* @param settingId 疗休养配置ID
* @param userId 用户ID
* @return 存在返回 true不存在返回 false
*/
boolean existsAssignedUser(String settingId, String userId);
/**
* 校验用户是否为指定疗休养配置下的正式分配人员
* 用于报名资格方式为 ASSIGNED_USER 拦截未分配人员和替补人员
* 已退出人员也会被拦截避免取消后再次报名
*
* @param settingId 疗休养配置ID
* @param userId 用户ID
* @return canApply 表示是否可报名message 表示不可报名原因
*/
NutMap checkFormalAssignedUser(String settingId, String userId);
/**
* 实时统计指定疗休养配置下校工会来源的正式分配人数
* 已退出人员不再占用工会名额统计时需要排除
*
* @param settingId 疗休养配置ID
* @return 当前配置下校工会正式人员分配数量
*/
int countSchoolFormalAssignedUsers(String settingId);
/**
* 将人员分配记录标记为已退出并删除该记录对应事项下的报名台账
* 取消前会校验路线出行开始时间只有出行开始前指定天数之前允许取消
*
* @param id 人员分配记录ID
*/
void cancelAssignment(String id);
/**
* 将人员分配记录从已退出恢复为未退出
* 只恢复人员分配表状态不自动恢复已删除的报名台账
*
* @param id 人员分配记录ID
*/
void restoreCancelledAssignment(String id);
/**
* 按分配来源恢复已退出人员避免校工会和分工会页面互相恢复对方数据
*
* @param id 人员分配记录ID
* @param assignSource 分配来源
*/
void restoreCancelledAssignmentBySource(String id, String assignSource);
/**
* 恢复当前登录人所在分工会的已退出人员分配记录
* 用于分工会人员分配列表的取消退出按钮防止跨分工会恢复数据
*
* @param id 人员分配记录ID
*/
void restoreCurrentBranchCancelledAssignment(String id);
/**
* 按疗休养配置清理人员分配数据供删除配置或后续重置分配时复用
*
* @param settingId 疗休养配置ID
*/
void clearBySettingId(String settingId);
}
@@ -0,0 +1,169 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLeaveApply;
import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLeaveApplyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@IocBean(args = {"refer:dao"})
public class TourLeaveApplyServiceImpl extends BaseServiceImpl<TourLeaveApply> implements TourLeaveApplyService {
@Inject
private TourUserAssignmentService tourUserAssignmentService;
public TourLeaveApplyServiceImpl(Dao dao) {
super(dao);
}
@Override
public Pagination<NutMap> pageData(PageForm pageForm, String keyword, String unionName, String status) {
Sql sql = Sqls.create("""
SELECT
a.*,
s.`year` AS `year`,
s.configName AS settingName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' ', m.travelEndTime)
ELSE ''
END AS travelPeriod,
m.travelStartTime AS travelStartTime,
IF(IFNULL(a.cancelled, 0) = 1, 'CANCELLED', 'NORMAL') AS cancelStatus
FROM tour_user_assignment a
LEFT JOIN tour_setting s ON s.id = a.settingId
LEFT JOIN tour_matter m ON m.id = a.matterId AND m.delFlag = 0
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("a.delFlag", "=", false);
// 退出取消只针对已选路线的数据未分配路线的人员没有出行时间和台账可处理
cnd.and("a.matterId", "is not", null);
cnd.and("a.matterId", "<>", "");
cnd.and("a.lineId", "is not", null);
cnd.and("a.lineId", "<>", "");
cnd.and("a.personType", "=", TourUserAssignment.PERSON_TYPE_FORMAL);
applyDataPermission(cnd);
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup keywordGroup = new SqlExpressionGroup();
keywordGroup.orLike("a.loginName", keyword);
keywordGroup.orLike("a.userName", keyword);
cnd.and(keywordGroup);
}
cnd.and(Cnd.likeEX("a.unionName", unionName));
if ("CANCELLED".equals(status)) {
cnd.and("a.cancelled", "=", true);
} else if ("NORMAL".equals(status)) {
cnd.and(Cnd.exps("a.cancelled", "=", false).or("a.cancelled", "is", null));
}
applyOrder(cnd, pageForm);
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public NutMap permissionInfo() {
return NutMap.NEW()
.addv("isAllDataRole", isAllDataRole())
.addv("isBranchUnionChairman", AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name()))
.addv("canRestore", StpUtil.hasPermission("tour.branchUserAssignment.cancelRestore"));
}
@Override
public void cancelAssignment(String id) {
checkCanOperateAssignment(id);
tourUserAssignmentService.cancelAssignment(id);
}
@Override
public void restoreAssignment(String id) {
checkCanOperateAssignment(id);
tourUserAssignmentService.restoreCancelledAssignment(id);
}
/**
* 退出取消管理读取人员分配表按用户角色收窄可见范围
*/
private void applyDataPermission(Cnd cnd) {
if (isAllDataRole()) {
return;
}
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
cnd.andEX("a.unionId", "=", SecurityUtil.getUnionId());
return;
}
cnd.and("a.userId", "=", SecurityUtil.getUserId());
}
private boolean isAllDataRole() {
return AuthUtil.hasRole(RoleConstant.SYSADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CHAIRMAN.name());
}
/**
* 操作前按同一数据权限校验避免前端绕过列表直接提交其它人员记录
*/
private void checkCanOperateAssignment(String id) {
if (StrUtil.isBlank(id)) {
throw new IllegalArgumentException("参数错误");
}
Cnd cnd = Cnd.where(TourUserAssignment::getId, "=", id)
.and(TourUserAssignment::getDelFlag, "=", false);
if (!isAllDataRole()) {
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
cnd.and(TourUserAssignment::getUnionId, "=", SecurityUtil.getUnionId());
} else {
cnd.and(TourUserAssignment::getUserId, "=", SecurityUtil.getUserId());
}
}
if (dao().count(TourUserAssignment.class, cnd) <= 0) {
throw new IllegalArgumentException("人员分配记录不存在或无权操作");
}
}
/**
* 仅开放页面使用到的排序字段避免前端传入任意字段名影响查询
*/
private void applyOrder(Cnd cnd, PageForm pageForm) {
String orderName = pageForm.getPageOrderName();
String orderBy = pageForm.getPageOrderBy();
boolean descending = "descending".equals(orderBy);
if ("loginName".equals(orderName)) {
order(cnd, "a.loginName", descending);
} else if ("userName".equals(orderName)) {
order(cnd, "a.userName", descending);
} else if ("unionName".equals(orderName)) {
order(cnd, "a.unionName", descending);
} else if ("cancelStatus".equals(orderName)) {
order(cnd, "a.cancelled", descending);
} else if ("assignedAt".equals(orderName)) {
order(cnd, "a.assignedAt", descending);
} else {
cnd.desc("a.assignedAt");
cnd.desc("a.createdAt");
}
}
private void order(Cnd cnd, String field, boolean descending) {
if (descending) {
cnd.desc(field);
} else {
cnd.asc(field);
}
}
}
@@ -1,10 +1,23 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota;
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
import org.nutz.dao.Cnd;
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.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class TourSettingServiceImpl extends BaseServiceImpl<TourSetting> implements TourSettingService {
@@ -12,4 +25,109 @@ public class TourSettingServiceImpl extends BaseServiceImpl<TourSetting> impleme
public TourSettingServiceImpl(Dao dao) {
super(dao);
}
@Override
public List<TourSettingUnionQuota> listUnionQuotaRows(String settingId) {
// 一次性查出所有分工会已保存名额和实时会员数避免页面打开时按分工会循环统计造成 N+1 查询
Sql sql = Sqls.create("""
SELECT
quota.id AS id,
@settingId AS settingId,
un.id AS unionId,
un.name AS unionName,
COALESCE(quota.formalQuota, 0) AS formalQuota,
COALESCE(quota.backupQuota, 0) AS backupQuota,
COALESCE(user_count.memberCount, 0) AS memberCount
FROM sys_union un
LEFT JOIN tour_setting_union_quota quota
ON quota.unionId = un.id
AND quota.settingId = @settingId
LEFT JOIN (
SELECT unionId, COUNT(1) AS memberCount
FROM vw_user
WHERE member = 1
GROUP BY unionId
) user_count ON user_count.unionId = un.id
ORDER BY un.unionCode ASC
""");
sql.setParam("settingId", settingId == null ? "" : settingId);
List<NutMap> rows = listMap(sql);
return rows.stream().map(row -> {
TourSettingUnionQuota quota = new TourSettingUnionQuota();
quota.setId(row.getString("id"));
quota.setSettingId(row.getString("settingId"));
quota.setUnionId(row.getString("unionId"));
quota.setUnionName(row.getString("unionName"));
quota.setFormalQuota(defaultInt(row.getInt("formalQuota")));
quota.setBackupQuota(defaultInt(row.getInt("backupQuota")));
quota.setMemberCount(defaultInt(row.getInt("memberCount")));
return quota;
}).collect(Collectors.toList());
}
@Override
public void saveUnionQuotas(String settingId, String unionQuotas) {
if (StrUtil.isBlank(settingId)) {
return;
}
clearUnionQuotas(settingId);
if (StrUtil.isBlank(unionQuotas)) {
return;
}
List<TourSettingUnionQuota> quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas);
if (Lang.isEmpty(quotaList)) {
return;
}
Map<String, Sys_union> unionMap = dao().query(Sys_union.class, Cnd.NEW()).stream()
.collect(Collectors.toMap(Sys_union::getId, item -> item, (a, b) -> a));
List<TourSettingUnionQuota> saveList = quotaList.stream()
.filter(item -> item != null && StrUtil.isNotBlank(item.getUnionId()))
.map(item -> normalizeUnionQuota(settingId, item, unionMap))
.filter(item -> item.getFormalQuota() > 0 || item.getBackupQuota() > 0)
.collect(Collectors.toList());
if (Lang.isNotEmpty(saveList)) {
dao().insert(saveList);
}
}
@Override
public String checkBranchFormalQuotaLimit(String unionQuotas, int branchTotalQuota) {
if (StrUtil.isBlank(unionQuotas)) {
return "";
}
List<TourSettingUnionQuota> quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas);
if (Lang.isEmpty(quotaList)) {
return "";
}
int formalTotal = quotaList.stream()
.filter(item -> item != null)
.mapToInt(item -> defaultInt(item.getFormalQuota()))
.sum();
if (formalTotal > branchTotalQuota) {
return "当前正式人员总数 " + formalTotal + ",分工会总名额 " + branchTotalQuota + ",分配后总人数不能超过分工会总名额";
}
return "";
}
@Override
public void clearUnionQuotas(String settingId) {
if (StrUtil.isNotBlank(settingId)) {
dao().clear(TourSettingUnionQuota.class, Cnd.where(TourSettingUnionQuota::getSettingId, "=", settingId));
}
}
private TourSettingUnionQuota normalizeUnionQuota(String settingId, TourSettingUnionQuota item, Map<String, Sys_union> unionMap) {
Sys_union union = unionMap.get(item.getUnionId());
TourSettingUnionQuota quota = new TourSettingUnionQuota();
quota.setSettingId(settingId);
quota.setUnionId(item.getUnionId());
quota.setUnionName(union == null ? item.getUnionName() : union.getName());
quota.setFormalQuota(defaultInt(item.getFormalQuota()));
quota.setBackupQuota(defaultInt(item.getBackupQuota()));
return quota;
}
private Integer defaultInt(Integer value) {
return value == null || value < 0 ? 0 : value;
}
}
@@ -0,0 +1,13 @@
-- 疗休养乘车地点字段升级脚本。
-- 配置表保存乘车地点列表 JSON;事项、人员分配、台账保存最终/默认乘车地点。
ALTER TABLE `tour_setting`
MODIFY COLUMN `boardingPlace` text COMMENT '乘车地点列表JSON';
ALTER TABLE `tour_matter`
ADD COLUMN `defaultBoardingPlace` varchar(100) DEFAULT NULL COMMENT '默认乘车地点' AFTER `lineId`;
ALTER TABLE `tour_user_assignment`
ADD COLUMN `boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点' AFTER `lineName`;
ALTER TABLE `tour_ledger`
ADD COLUMN `boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点' AFTER `travelAgencyName`;
@@ -0,0 +1,29 @@
-- 疗休养退出申请表。
CREATE TABLE IF NOT EXISTS `tour_leave_apply` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`matterId` varchar(32) DEFAULT NULL COMMENT '疗休养事项ID',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`jobNo` varchar(50) DEFAULT NULL COMMENT '工号',
`userName` varchar(100) DEFAULT NULL COMMENT '姓名',
`userId` varchar(32) DEFAULT NULL COMMENT '用户ID',
`unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID',
`unionName` varchar(100) DEFAULT NULL COMMENT '所属工会',
`reason` text COMMENT '不能参加原因',
`status` varchar(30) DEFAULT NULL COMMENT '状态',
`auditBy` varchar(32) DEFAULT NULL COMMENT '审核人ID',
`auditName` varchar(100) DEFAULT NULL COMMENT '审核人姓名',
`auditTime` varchar(30) DEFAULT NULL COMMENT '审核时间',
`auditRemark` text COMMENT '审核备注',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_leave_apply_user` (`userId`),
KEY `idx_tour_leave_apply_job_no` (`jobNo`),
KEY `idx_tour_leave_apply_union` (`unionId`),
KEY `idx_tour_leave_apply_status` (`status`),
KEY `idx_tour_leave_apply_matter` (`matterId`),
KEY `idx_tour_leave_apply_line` (`lineId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养退出申请';
+13 -1
View File
@@ -9,6 +9,17 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
`unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID',
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`defaultBoardingPlace` varchar(100) DEFAULT NULL COMMENT '默认乘车地点',
`signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间',
`signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间',
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间',
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间',
`contactName` varchar(30) DEFAULT NULL COMMENT '联系人',
`contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式',
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
`estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用',
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
@@ -21,5 +32,6 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
KEY `idx_tour_matter_creator` (`creatorUserId`),
KEY `idx_tour_matter_setting` (`settingId`),
KEY `idx_tour_matter_union` (`unionId`),
KEY `idx_tour_matter_org_type` (`organizationType`)
KEY `idx_tour_matter_org_type` (`organizationType`),
KEY `idx_tour_matter_line` (`lineId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项';
@@ -0,0 +1,4 @@
-- 疗休养配置新增乘车地点、出行人数指标。
ALTER TABLE `tour_setting`
ADD COLUMN `boardingPlace` text COMMENT '乘车地点列表JSON' AFTER `tourType`,
ADD COLUMN `travelPeopleQuota` int DEFAULT 0 COMMENT '出行人数指标' AFTER `boardingPlace`;
@@ -0,0 +1,2 @@
ALTER TABLE `tour_setting`
ADD COLUMN `signupEligibilityMode` varchar(30) DEFAULT 'ASSIGNED_USER' COMMENT '报名资格校验方式' AFTER `activityGroupId`;
+75 -1
View File
@@ -5,7 +5,10 @@ CREATE TABLE IF NOT EXISTS `tour_setting` (
`year` int DEFAULT NULL COMMENT '年度',
`configName` varchar(100) DEFAULT NULL COMMENT '疗休养配置名称',
`tourType` varchar(50) DEFAULT NULL COMMENT '疗休养类型',
`boardingPlace` text COMMENT '乘车地点列表JSON',
`travelPeopleQuota` int DEFAULT 0 COMMENT '出行人数指标',
`activityGroupId` varchar(32) DEFAULT NULL COMMENT '可参加人员范围ID',
`signupEligibilityMode` varchar(30) DEFAULT 'ASSIGNED_USER' COMMENT '报名资格校验方式',
`sortNo` int DEFAULT NULL COMMENT '排序编号',
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
@@ -51,6 +54,65 @@ CREATE TABLE IF NOT EXISTS `tour_setting_lot` (
KEY `idx_tour_setting_lot_value` (`lotValue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置标段';
-- 疗休养配置分工会名额分配表。
CREATE TABLE IF NOT EXISTS `tour_setting_union_quota` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID',
`unionId` varchar(32) DEFAULT NULL COMMENT '分工会ID',
`unionName` varchar(100) DEFAULT NULL COMMENT '分工会名称',
`formalQuota` int DEFAULT 0 COMMENT '正式人员名额',
`backupQuota` int DEFAULT 0 COMMENT '替补人员名额',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_setting_union_quota_setting` (`settingId`),
KEY `idx_tour_setting_union_quota_union` (`unionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养配置分工会名额分配';
-- 疗休养人员分配表。
CREATE TABLE IF NOT EXISTS `tour_user_assignment` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID',
`matterId` varchar(32) DEFAULT NULL COMMENT '疗休养事项ID',
`matterName` varchar(100) DEFAULT NULL COMMENT '疗休养事项名称',
`travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID',
`travelAgencyName` varchar(100) DEFAULT NULL COMMENT '旅行社名称',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`lineName` varchar(100) DEFAULT NULL COMMENT '线路名称',
`boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点',
`userId` varchar(32) DEFAULT NULL COMMENT '人员ID',
`loginName` varchar(50) DEFAULT NULL COMMENT '工号',
`userName` varchar(100) DEFAULT NULL COMMENT '姓名',
`gender` varchar(20) DEFAULT NULL COMMENT '性别',
`mobile` varchar(30) DEFAULT NULL COMMENT '手机号',
`unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID',
`unitName` varchar(100) DEFAULT NULL COMMENT '所在单位',
`unionId` varchar(32) DEFAULT NULL COMMENT '所在分工会ID',
`unionName` varchar(100) DEFAULT NULL COMMENT '所在分工会',
`assignSource` varchar(30) DEFAULT NULL COMMENT '分配来源',
`personType` varchar(30) DEFAULT NULL COMMENT '人员类型',
`cancelled` tinyint(1) DEFAULT 0 COMMENT '是否已退出',
`assignedBy` varchar(32) DEFAULT NULL COMMENT '分配人ID',
`assignedName` varchar(100) DEFAULT NULL COMMENT '分配人姓名',
`assignedAt` varchar(30) DEFAULT NULL COMMENT '分配时间',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_user_assignment_setting_user` (`settingId`, `userId`),
KEY `idx_tour_user_assignment_setting` (`settingId`),
KEY `idx_tour_user_assignment_user` (`userId`),
KEY `idx_tour_user_assignment_union` (`unionId`),
KEY `idx_tour_user_assignment_source` (`assignSource`),
KEY `idx_tour_user_assignment_person_type` (`personType`),
KEY `idx_tour_user_assignment_cancelled` (`cancelled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养人员分配';
-- 旅行社管理表。
CREATE TABLE IF NOT EXISTS `tour_travel_agency` (
`id` varchar(32) NOT NULL COMMENT 'ID',
@@ -118,6 +180,17 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
`unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID',
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`defaultBoardingPlace` varchar(100) DEFAULT NULL COMMENT '默认乘车地点',
`signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间',
`signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间',
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间',
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间',
`contactName` varchar(30) DEFAULT NULL COMMENT '联系人',
`contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式',
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
`estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用',
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
@@ -130,7 +203,8 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
KEY `idx_tour_matter_creator` (`creatorUserId`),
KEY `idx_tour_matter_setting` (`settingId`),
KEY `idx_tour_matter_union` (`unionId`),
KEY `idx_tour_matter_org_type` (`organizationType`)
KEY `idx_tour_matter_org_type` (`organizationType`),
KEY `idx_tour_matter_line` (`lineId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项';
-- 疗休养事项批次表。
@@ -0,0 +1,17 @@
-- 疗休养配置分工会名额分配表。
CREATE TABLE IF NOT EXISTS `tour_setting_union_quota` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID',
`unionId` varchar(32) DEFAULT NULL COMMENT '分工会ID',
`unionName` varchar(100) DEFAULT NULL COMMENT '分工会名称',
`formalQuota` int DEFAULT 0 COMMENT '正式人员名额',
`backupQuota` int DEFAULT 0 COMMENT '替补人员名额',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_setting_union_quota_setting` (`settingId`),
KEY `idx_tour_setting_union_quota_union` (`unionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养配置分工会名额分配';
@@ -0,0 +1,4 @@
-- 疗休养人员分配新增退出状态。
ALTER TABLE `tour_user_assignment`
ADD COLUMN `cancelled` tinyint(1) DEFAULT 0 COMMENT '是否已退出' AFTER `personType`,
ADD KEY `idx_tour_user_assignment_cancelled` (`cancelled`);
@@ -0,0 +1,40 @@
-- 疗休养人员分配表。
CREATE TABLE IF NOT EXISTS `tour_user_assignment` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID',
`matterId` varchar(32) DEFAULT NULL COMMENT '疗休养事项ID',
`matterName` varchar(100) DEFAULT NULL COMMENT '疗休养事项名称',
`travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID',
`travelAgencyName` varchar(100) DEFAULT NULL COMMENT '旅行社名称',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`lineName` varchar(100) DEFAULT NULL COMMENT '线路名称',
`boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点',
`userId` varchar(32) DEFAULT NULL COMMENT '人员ID',
`loginName` varchar(50) DEFAULT NULL COMMENT '工号',
`userName` varchar(100) DEFAULT NULL COMMENT '姓名',
`gender` varchar(20) DEFAULT NULL COMMENT '性别',
`mobile` varchar(30) DEFAULT NULL COMMENT '手机号',
`unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID',
`unitName` varchar(100) DEFAULT NULL COMMENT '所在单位',
`unionId` varchar(32) DEFAULT NULL COMMENT '所在分工会ID',
`unionName` varchar(100) DEFAULT NULL COMMENT '所在分工会',
`assignSource` varchar(30) DEFAULT NULL COMMENT '分配来源',
`personType` varchar(30) DEFAULT NULL COMMENT '人员类型',
`cancelled` tinyint(1) DEFAULT 0 COMMENT '是否已退出',
`assignedBy` varchar(32) DEFAULT NULL COMMENT '分配人ID',
`assignedName` varchar(100) DEFAULT NULL COMMENT '分配人姓名',
`assignedAt` varchar(30) DEFAULT NULL COMMENT '分配时间',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_user_assignment_setting_user` (`settingId`, `userId`),
KEY `idx_tour_user_assignment_setting` (`settingId`),
KEY `idx_tour_user_assignment_user` (`userId`),
KEY `idx_tour_user_assignment_union` (`unionId`),
KEY `idx_tour_user_assignment_source` (`assignSource`),
KEY `idx_tour_user_assignment_person_type` (`personType`),
KEY `idx_tour_user_assignment_cancelled` (`cancelled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养人员分配';
@@ -1,7 +1,7 @@
<template>
<div class="search">
<slot></slot>
<div class="search-query">
<div v-if="isSearchButton" class="search-query">
<el-button type="primary" icon="el-icon-search" @click="$emit('search', null)">搜索</el-button>
</div>
</div>
@@ -9,7 +9,14 @@
<script>
module.exports = {
name: "pageFormSearch"
name: "pageFormSearch",
props: {
// /
isSearchButton: {
type: Boolean,
default: true
}
}
}
</script>
@@ -300,7 +300,7 @@ const ACTIVITY_CULTURE_INFO_MANAGE = {
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post(loc() + "/sendNotify", {
const resp = await this.$axios.post("/platform/activity/culture/infoManage/sendNotify", {
activityId: this.notifyActivityId,
content: this.notifyContent
})
@@ -0,0 +1,649 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%"
@change="pageYearChange">
</el-date-picker>
</search-item>
<search-item label="疗休养配置">
<el-select v-model="pageForm.settingId" clearable filterable placeholder="请选择配置" style="width: 100%" @change="pageSettingChange">
<el-option v-for="item in settingOptions" :key="item.id" :label="item.configName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="分配路线">
<el-select v-model="pageForm.matterId" clearable filterable placeholder="请选择分配路线" style="width: 100%">
<el-option v-for="item in pageMatterOptions" :key="item.matterId" :label="matterOptionLabel(item)" :value="item.matterId"></el-option>
</el-select>
</search-item>
<search-item label="人员类型">
<el-select v-model="pageForm.personType" clearable placeholder="请选择人员类型" style="width: 100%">
<el-option label="正式人员" value="FORMAL"></el-option>
<el-option label="替补人员" value="BACKUP"></el-option>
</el-select>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="分工会人员分配列表">
<el-button type="primary" size="medium" @click="openAssign">
<i class="el-icon-user"></i>
人员分配
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
class="vi-table"
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="姓名" prop="userName" width="110" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="工号" prop="loginName" width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="人员类型" prop="personType" width="110" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.personType === 'BACKUP' ? 'warning' : 'success'">{{ personTypeText(row.personType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="是否退出" prop="cancelled" width="110" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="isCancelled(row) ? 'danger' : 'success'">{{ isCancelled(row) ? '已退出' : '未退出' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="分配时间" prop="assignedAt" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="320" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button size="mini" type="primary" :loading="personTypeSwitching === row.id" @click="switchPersonType(row, row.personType === 'BACKUP' ? 'FORMAL' : 'BACKUP')">{{ row.personType === 'BACKUP' ? '转为正式' : '转为替补' }}</el-button>
<el-button v-if="isCancelled(row) && $auth.hasPermission('tour.branchUserAssignment.cancelRestore')" size="mini" type="warning" @click="restoreCancel(row)">取消退出</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
custom-class="tour-user-assignment-dialog"
title="人员分配"
:visible.sync="assignDialogVisible"
:close-on-click-modal="false"
width="76%"
@closed="resetAssignDialog">
<el-form :model="assignForm" :rules="assignRules" ref="assignFormRef" label-width="110px">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="年度" prop="year">
<el-date-picker
v-model="assignForm.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%"
@change="assignYearChange">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="疗休养配置" prop="settingId">
<el-select v-model="assignForm.settingId" clearable filterable placeholder="请选择配置" style="width: 100%" @change="assignSettingChange">
<el-option v-for="item in assignSettingOptions" :key="item.id" :label="item.configName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="分配路线" prop="matterId">
<el-select v-model="assignForm.matterId" clearable filterable placeholder="可选,选择后代报名" style="width: 100%" :disabled="assignForm.personType === 'BACKUP'">
<el-option v-for="item in assignMatterOptions" :key="item.matterId" :label="matterOptionLabel(item)" :value="item.matterId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="人员类型" prop="personType">
<el-radio-group v-model="assignForm.personType" @change="assignPersonTypeChange">
<el-radio-button label="FORMAL">正式人员</el-radio-button>
<el-radio-button label="BACKUP">替补人员</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="quota-bar">
<div class="quota-item">
<span class="quota-label">正式名额</span>
<span>{{ quotaInfo.formalQuota }}</span>
<span class="quota-muted">已分配 {{ quotaInfo.formalUsed }},剩余 {{ quotaInfo.formalRemaining }}</span>
</div>
<div class="quota-item">
<span class="quota-label">替补名额</span>
<span>{{ quotaInfo.backupQuota }}</span>
<span class="quota-muted">已分配 {{ quotaInfo.backupUsed }},剩余 {{ quotaInfo.backupRemaining }}</span>
</div>
</div>
<div class="candidate-toolbar">
<el-input
v-model="candidateForm.keyword"
clearable
placeholder="姓名/工号"
style="width: 240px"
@keyup.enter.native="candidateSearch">
</el-input>
<el-button type="primary" icon="el-icon-search" @click="candidateSearch">查询</el-button>
<el-button @click="resetCandidateSearch">重置</el-button>
<span class="candidate-selected">已选 {{ selectedCandidates.length }} 人</span>
</div>
<el-table
ref="candidateTable"
v-loading="candidateLoading"
:data="candidateData"
row-key="userId"
border
size="mini"
height="420"
@selection-change="candidateSelectionChange">
<el-table-column type="selection" width="48" :reserve-selection="true" align="center" header-align="center"></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="工号" prop="loginName" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="手机号" prop="mobile" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属分工会" prop="unionName" min-width="160" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
<el-row class="el-pagination-container candidate-pagination">
<el-pagination
background
:current-page="candidateForm.pageNumber"
:page-sizes="[10, 20, 50, 100]"
:page-size="candidateForm.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="candidateForm.totalCount"
@size-change="candidateSizeChange"
@current-change="candidatePageChange">
</el-pagination>
</el-row>
<span slot="footer" class="dialog-footer">
<el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="assignSubmitting" @click="doAssign">保存分配</el-button>
</span>
</el-dialog>
</div>
<style>
#app .search .search-item {
width: calc((100% - 150px) / 4);
}
.tour-user-assignment-dialog .el-dialog__body {
max-height: 72vh;
overflow-y: auto;
}
.quota-bar {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
.quota-item {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px 12px;
min-width: 220px;
color: #303133;
background: #fafafa;
}
.quota-label {
font-weight: 600;
margin-right: 10px;
}
.quota-muted {
margin-left: 10px;
color: #909399;
}
.candidate-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.candidate-selected {
color: #606266;
white-space: nowrap;
}
.candidate-pagination {
margin-top: 12px;
margin-bottom: 0;
text-align: right;
}
@media screen and (max-width: 1350px) {
#app .search .search-item {
width: calc((100% - 100px) / 3);
}
}
@media screen and (max-width: 1200px) {
#app .search .search-item {
width: calc((100% - 50px) / 2);
}
.quota-bar {
flex-direction: column;
}
}
@media screen and (max-width: 992px) {
#app .search .search-item {
width: 100%;
}
.candidate-selected {
margin-left: 0;
}
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
settingOptions: [],
pageMatterOptions: [],
assignSettingOptions: [],
assignMatterOptions: [],
quotaInfo: {
formalQuota: 0,
backupQuota: 0,
formalUsed: 0,
backupUsed: 0,
formalRemaining: 0,
backupRemaining: 0
},
assignDialogVisible: false,
candidateLoading: false,
candidateData: [],
selectedCandidates: [],
assignSubmitting: false,
personTypeSwitching: "",
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "assignedAt",
pageOrderBy: "descending",
year: currentYear,
settingId: "",
matterId: "",
personType: "",
keyword: ""
},
assignForm: {
year: currentYear,
settingId: "",
matterId: "",
personType: "FORMAL"
},
candidateForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
keyword: ""
},
assignRules: {
year: [{required: true, message: "请选择年度", trigger: ["change", "blur"]}],
settingId: [{required: true, message: "请选择疗休养配置", trigger: ["change", "blur"]}],
personType: [{required: true, message: "请选择人员类型", trigger: ["change", "blur"]}]
}
}
},
methods: {
defaultAssignForm(currentYear) {
return {
year: currentYear || moment().format("YYYY"),
settingId: "",
matterId: "",
personType: "FORMAL"
}
},
defaultCandidateForm() {
return {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
keyword: ""
}
},
defaultQuotaInfo() {
return {
formalQuota: 0,
backupQuota: 0,
formalUsed: 0,
backupUsed: 0,
formalRemaining: 0,
backupRemaining: 0
}
},
resetSearch() {
const currentYear = moment().format("YYYY")
this.pageForm.year = currentYear
this.pageForm.settingId = ""
this.pageForm.matterId = ""
this.pageForm.personType = ""
this.pageForm.keyword = ""
this.loadSettingOptions()
},
pageYearChange() {
this.pageForm.settingId = ""
this.pageForm.matterId = ""
this.pageMatterOptions = []
this.loadSettingOptions()
},
pageSettingChange() {
this.pageForm.matterId = ""
this.loadPageMatterOptions()
},
loadSettingOptions() {
this.$axios.post(loc() + "/settingOptions", {year: this.pageForm.year}).then((res) => {
if (res.code === 0) {
this.settingOptions = res.data || []
// 主页面默认选择当前年度第一条配置,避免进入页面后查询范围为空。
if (!this.pageForm.settingId && this.settingOptions.length > 0) {
this.pageForm.settingId = this.settingOptions[0].id
this.loadPageMatterOptions()
this.doSearch()
} else {
this.pageMatterOptions = []
this.doSearch()
}
}
})
},
loadPageMatterOptions() {
if (!this.pageForm.settingId) {
this.pageMatterOptions = []
return
}
this.$axios.post(loc() + "/matterOptions", {settingId: this.pageForm.settingId}).then((res) => {
if (res.code === 0) {
this.pageMatterOptions = res.data || []
}
})
},
openAssign() {
const year = this.pageForm.year || moment().format("YYYY")
this.assignForm = this.defaultAssignForm(year)
this.candidateForm = this.defaultCandidateForm()
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.selectedCandidates = []
this.assignDialogVisible = true
this.loadAssignSettingOptions()
},
resetAssignDialog() {
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignMatterOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.selectedCandidates = []
this.assignSubmitting = false
},
assignYearChange() {
this.assignForm.settingId = ""
this.assignForm.matterId = ""
this.assignMatterOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.clearCandidateSelection()
this.loadAssignSettingOptions()
},
assignSettingChange() {
this.assignForm.matterId = ""
this.assignMatterOptions = []
this.clearCandidateSelection()
this.loadAssignMatterOptions()
this.loadQuotaInfo()
this.loadCandidatePageData()
},
assignPersonTypeChange() {
if (this.assignForm.personType === "BACKUP") {
this.assignForm.matterId = ""
}
},
loadAssignSettingOptions() {
this.$axios.post(loc() + "/settingOptions", {year: this.assignForm.year}).then((res) => {
if (res.code === 0) {
this.assignSettingOptions = res.data || []
// 人员分配弹窗独立默认取当前年度第一条配置,再加载名额、线路和候选人员。
if (!this.assignForm.settingId && this.assignSettingOptions.length > 0) {
this.assignForm.settingId = this.assignSettingOptions[0].id
this.loadAssignMatterOptions()
this.loadQuotaInfo()
this.loadCandidatePageData()
} else if (this.assignForm.settingId) {
this.loadAssignMatterOptions()
this.loadQuotaInfo()
this.loadCandidatePageData()
} else {
this.assignMatterOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.candidateForm.totalCount = 0
}
}
})
},
loadAssignMatterOptions() {
if (!this.assignForm.settingId) {
this.assignMatterOptions = []
return
}
this.$axios.post(loc() + "/matterOptions", {settingId: this.assignForm.settingId}).then((res) => {
if (res.code === 0) {
this.assignMatterOptions = res.data || []
}
})
},
loadQuotaInfo() {
if (!this.assignForm.settingId) {
this.quotaInfo = this.defaultQuotaInfo()
return
}
this.$axios.post(loc() + "/quotaInfo", {settingId: this.assignForm.settingId}).then((res) => {
if (res.code === 0) {
this.quotaInfo = Object.assign(this.defaultQuotaInfo(), res.data || {})
}
})
},
loadCandidatePageData() {
if (!this.assignForm.settingId) {
this.candidateData = []
this.candidateForm.totalCount = 0
return
}
this.candidateLoading = true
this.$axios.post(loc() + "/candidatePageData", {
pageNumber: this.candidateForm.pageNumber,
pageSize: this.candidateForm.pageSize,
settingId: this.assignForm.settingId,
keyword: this.candidateForm.keyword
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.candidateData = data.list || []
this.candidateForm.totalCount = data.totalCount || 0
} else {
this.$message.warning(res.msg || "候选人员查询失败")
}
}).finally(() => {
this.candidateLoading = false
})
},
candidateSearch() {
this.candidateForm.pageNumber = 1
this.loadCandidatePageData()
},
resetCandidateSearch() {
this.candidateForm.keyword = ""
this.candidateForm.pageNumber = 1
this.loadCandidatePageData()
},
candidateSizeChange(size) {
this.candidateForm.pageSize = size
this.candidateForm.pageNumber = 1
this.loadCandidatePageData()
},
candidatePageChange(pageNumber) {
this.candidateForm.pageNumber = pageNumber
this.loadCandidatePageData()
},
candidateSelectionChange(rows) {
this.selectedCandidates = rows || []
},
clearCandidateSelection() {
this.selectedCandidates = []
if (this.$refs.candidateTable) {
this.$refs.candidateTable.clearSelection()
}
},
doAssign() {
this.$refs.assignFormRef.validate((valid) => {
if (!valid) return
if (this.selectedCandidates.length <= 0) {
this.$message.warning("请选择需要分配的人员")
return
}
this.$confirm("确定保存当前人员分配吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const userIds = this.selectedCandidates.map(item => item.userId)
this.assignSubmitting = true
this.$axios.post(loc() + "/doAssign", {
settingId: this.assignForm.settingId,
matterId: this.assignForm.personType === "BACKUP" ? "" : this.assignForm.matterId,
personType: this.assignForm.personType,
userIds: JSON.stringify(userIds)
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.$message.success("分配成功" + (data.ledgerCount > 0 ? ",已代报名" + data.ledgerCount + "人" : "") + (data.skipCount > 0 ? ",已跳过" + data.skipCount + "人" : ""))
this.assignDialogVisible = false
this.doSearch()
} else {
this.$message.warning(res.msg || "分配失败")
}
}).finally(() => {
this.assignSubmitting = false
})
}).catch(() => {})
})
},
switchPersonType(row, targetType) {
this.personTypeSwitching = row.id
this.$axios.post(loc() + "/switchPersonType", {
id: row.id,
personType: targetType
}).then((res) => {
if (res.code === 0) {
this.$message.success("切换成功")
this.doSearch()
} else {
this.$message.warning(res.msg || "切换失败")
}
}).finally(() => {
this.personTypeSwitching = ""
})
},
doDelete(row) {
this.$axios.post(loc() + "/deleteInfo", {id: row.id}).then((infoRes) => {
if (infoRes.code !== 0) {
this.$message.warning(infoRes.msg || "删除校验失败")
return
}
const info = infoRes.data || {}
const hasLedger = !!info.hasLedger
const message = hasLedger ? "该人员已报名,删除分配记录将同时删除台账数据,是否继续?" : "确定删除该人员分配记录吗?"
this.$confirm(message, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", {
id: row.id,
deleteLedger: hasLedger
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg || "删除成功")
this.doSearch()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
}).catch(() => {})
})
},
personTypeText(personType) {
return personType === "BACKUP" ? "替补人员" : "正式人员"
},
isCancelled(row) {
return row && (row.cancelled === true || row.cancelled === 1)
},
restoreCancel(row) {
this.$confirm("确定将【" + row.userName + "】恢复为未退出状态吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/restoreCancel", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("取消退出成功")
this.doSearch()
} else {
this.$message.warning(res.msg || "取消退出失败")
}
})
}).catch(() => {})
},
matterOptionLabel(item) {
const lineName = item.lineName || ""
return lineName || item.matterName || ""
}
},
mounted() {
this.loadSettingOptions()
}
})
</script>
<!--#
}
#-->
@@ -34,7 +34,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -112,7 +112,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="viewSearch">搜索</el-button>
<el-button size="medium" @click="viewReset">重置</el-button>
</div>
@@ -127,6 +127,23 @@ layout("/layouts/platform.html"){
:size="tableSize"
border
@sort-change="viewPageOrder">
<el-table-column type="expand" width="50">
<template slot-scope="{row}">
<el-table
:data="row.families || []"
border
size="mini"
class="tour-family-sub-table"
empty-text="暂无家属信息">
<el-table-column label="姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="手机号" prop="mobile" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="关系" prop="relationship" width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</template>
</el-table-column>
<el-table-column label="序号" type="index" :index="viewIndexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -342,6 +359,10 @@ layout("/layouts/platform.html"){
margin-bottom: 12px;
padding-left: 10px;
}
.tour-family-sub-table {
margin: 8px 24px;
width: calc(100% - 48px);
}
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
@@ -0,0 +1,192 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-input
v-model="pageForm.unionName"
clearable
placeholder="请输入所属工会"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="状态">
<el-select v-model="pageForm.status" clearable placeholder="请选择状态" style="width: 100%">
<el-option label="未取消" value="NORMAL"></el-option>
<el-option label="已取消" value="CANCELLED"></el-option>
</el-select>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="疗休养退出取消列表"></table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="loginName" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属工会" prop="unionName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="疗休养配置" prop="settingName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="路线" prop="lineName" min-width="200" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="人员类型" prop="personType" width="110" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="small" :type="row.personType === 'BACKUP' ? 'warning' : 'success'">{{ personTypeText(row.personType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" prop="cancelStatus" width="110" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="small" :type="isCancelled(row) ? 'danger' : 'success'">{{ isCancelled(row) ? '已取消' : '未取消' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="分配来源" prop="assignSource" width="120" align="center" header-align="center">
<template slot-scope="{row}">
{{ assignSourceText(row.assignSource) }}
</template>
</el-table-column>
<el-table-column label="分配时间" prop="assignedAt" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="170" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button v-if="!isCancelled(row)" size="mini" type="danger" @click="doCancel(row)">退出</el-button>
<el-button v-if="isCancelled(row) && permissionInfo.canRestore" size="mini" type="primary" @click="doRestore(row)">取消退出</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
</div>
<style>
#app .search .search-item {
width: calc((100% - 150px) / 4);
}
@media screen and (max-width: 1200px) {
#app .search .search-item {
width: calc((100% - 50px) / 2);
}
}
@media screen and (max-width: 992px) {
#app .search .search-item {
width: 100%;
}
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
permissionInfo: {
canRestore: false
},
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
keyword: "",
unionName: "",
status: "",
pageOrderName: "assignedAt",
pageOrderBy: "descending"
}
}
},
methods: {
resetSearch() {
this.pageForm.keyword = ""
this.pageForm.unionName = ""
this.pageForm.status = ""
this.doSearch()
},
loadPermissionInfo() {
this.$axios.post(loc() + "/permissionInfo").then((res) => {
if (res.code === 0) {
this.permissionInfo = Object.assign({ canRestore: false }, res.data || {})
}
})
},
isCancelled(row) {
return row && (row.cancelStatus === "CANCELLED" || row.cancelled === true || row.cancelled === 1)
},
personTypeText(personType) {
return personType === "BACKUP" ? "替补人员" : "正式人员"
},
assignSourceText(assignSource) {
if (assignSource === "SCHOOL_UNION") {
return "校工会分配"
}
if (assignSource === "BRANCH_UNION") {
return "分工会分配"
}
return assignSource || ""
},
doCancel(row) {
this.$confirm("确定取消【" + row.userName + "】的疗休养资格吗?取消后将同步删除该人员对应台账数据。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doCancel", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("取消成功")
this.pageData()
} else {
this.$message.warning(res.msg || "取消失败")
}
})
}).catch(() => {})
},
doRestore(row) {
this.$confirm("确定将【" + row.userName + "】恢复为未取消状态吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doRestore", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("取消退出成功")
this.pageData()
} else {
this.$message.warning(res.msg || "取消退出失败")
}
})
}).catch(() => {})
}
},
mounted() {
this.loadPermissionInfo()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -56,7 +56,7 @@ layout("/layouts/platform.html"){
placeholder="请选择线路类型">
</dict-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -71,9 +71,10 @@ layout("/layouts/platform.html"){
<div class="tour-ledger-actions">
<el-button size="medium" type="primary" icon="el-icon-upload2" @click="showImportDialog = true">参加人员导入</el-button>
<el-button size="medium" type="primary" icon="el-icon-check" @click="setParticipants">设置参加人员</el-button>
<el-button size="medium" type="primary" icon="el-icon-download" @click="exportLedgerData">导出台账数据</el-button>
<el-button size="medium" type="primary" icon="el-icon-download" @click="exportUnionSignupZip">导出分工会报名压缩包</el-button>
</div>
<div class="tour-ledger-scope">
<div v-if="false" class="tour-ledger-scope">
<el-button
class="tour-scope-btn"
size="medium"
@@ -104,7 +105,12 @@ layout("/layouts/platform.html"){
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="身份证号" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="手机号" prop="mobile" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" min-width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -437,6 +443,16 @@ layout("/layouts/platform.html"){
}
window.location.href = loc() + "/exportUnionSignupZip?" + params.toString()
},
exportLedgerData() {
const params = new URLSearchParams()
;["startYear", "endYear", "keyword", "unionId", "lineId", "travelPeriod", "lineType"].forEach(key => {
const value = this.pageForm[key]
if (value !== undefined && value !== null && value !== "") {
params.append(key, value)
}
})
window.location.href = loc() + "/exportLedgerData?" + params.toString()
},
afterImport() {
this.showImportDialog = false
this.clearTableSelection()
@@ -34,7 +34,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in organizationTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -154,6 +154,11 @@ layout("/layouts/platform.html"){
<el-option v-for="item in lineOptions" :key="item.id" :label="lineOptionLabel(item)" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="默认乘车地点" prop="defaultBoardingPlace">
<el-select v-model="batchForm.defaultBoardingPlace" clearable filterable placeholder="请选择默认乘车地点" style="width: 100%">
<el-option v-for="item in boardingPlaceOptions" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="报名开始时间" prop="signupStartTime">
@@ -295,6 +300,7 @@ layout("/layouts/platform.html"){
currentMatter: {},
batchForm: {},
lineOptions: [],
boardingPlaceOptions: [],
settingOptions: [],
unionOptions: [],
organizationTypeOptions: [],
@@ -486,6 +492,7 @@ layout("/layouts/platform.html"){
signupEndTime: "",
travelStartTime: "",
travelEndTime: "",
defaultBoardingPlace: "",
contactName: "",
contactPhone: "",
minGroupPeople: null,
@@ -496,6 +503,7 @@ layout("/layouts/platform.html"){
openBatchForm(row) {
this.currentMatter = row
this.loadLines(row.year)
this.loadBoardingPlaceOptions(row.settingId)
this.batchTitle = "选择线路"
this.batchForm = this.emptyBatchForm()
this.$axios.post(loc() + "/lineConfig", { matterId: row.id }).then((res) => {
@@ -503,6 +511,7 @@ layout("/layouts/platform.html"){
this.batchForm = Object.assign(this.emptyBatchForm(), res.data || {})
this.batchForm.travelStartTime = this.dateOnly(this.batchForm.travelStartTime)
this.batchForm.travelEndTime = this.dateOnly(this.batchForm.travelEndTime)
this.ensureBatchBoardingPlace()
this.applySelectedLineDefaults(false)
if (!this.batchForm.minGroupPeople || !this.batchForm.maxGroupPeople) {
this.loadSettingPeople((data) => {
@@ -528,6 +537,44 @@ layout("/layouts/platform.html"){
}
})
},
loadBoardingPlaceOptions(settingId) {
this.boardingPlaceOptions = []
if (!settingId) {
return
}
this.$axios.post(loc() + "/settingBoardingPlaces", { settingId: settingId }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.boardingPlaceOptions = this.parseBoardingPlaceOptions(data.boardingPlace || "")
this.ensureBatchBoardingPlace()
}
})
},
parseBoardingPlaceOptions(value) {
if (!value) {
return []
}
try {
const list = JSON.parse(value)
if (!Array.isArray(list)) {
return []
}
return list.map((item) => {
if (typeof item === "string") {
return item
}
return item && item.name ? item.name : ""
}).filter((item) => item)
} catch (e) {
return String(value).split(",").map((item) => item.trim()).filter((item) => item)
}
},
ensureBatchBoardingPlace() {
if (!this.batchForm || !this.boardingPlaceOptions.length || this.batchForm.defaultBoardingPlace) {
return
}
this.$set(this.batchForm, "defaultBoardingPlace", this.boardingPlaceOptions[0])
},
lineOptionLabel(item) {
if (!item) return ""
const lineName = this.lineField(item, "lineName") || ""
@@ -29,7 +29,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in lineOptions" :key="item.lineName" :label="item.lineName" :value="item.lineName"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -254,11 +254,20 @@ layout("/layouts/platform.html"){
<el-input v-model="signupForm.hotelName" :disabled="isDirectFamilyLine(signupForm)" maxlength="100" placeholder="请输入报名酒店"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="乘车地点">
<el-select v-model="signupForm.boardingPlace" clearable filterable placeholder="请选择乘车地点" style="width: 100%">
<el-option v-for="item in boardingPlaceOptions" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="报名旅行社">
<el-input v-model="signupForm.travelAgencyName" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="出行时间">
<el-input v-model="signupForm.travelPeriod" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
@@ -748,6 +757,7 @@ layout("/layouts/platform.html"){
detailDoneTasks: [],
fillBedInfo: true,
signupForm: {},
boardingPlaceOptions: [],
familyData: [],
bedTypeOptions: [],
familyRelationshipOptions: [],
@@ -1091,6 +1101,7 @@ layout("/layouts/platform.html"){
const matter = data.matter || {}
const ledger = data.ledger || {}
const directRelative = data.directRelative || {}
this.boardingPlaceOptions = this.parseBoardingPlaceOptions(matter.boardingPlaceOptions || "")
this.allowFamily = matter.allowFamily === true || matter.allowFamily === 1 || matter.allowFamily === "1"
this.fillBedInfo = matter.fillBedInfo === undefined || matter.fillBedInfo === null || matter.fillBedInfo === true || matter.fillBedInfo === 1 || matter.fillBedInfo === "1"
this.signupForm = Object.assign({
@@ -1113,6 +1124,7 @@ layout("/layouts/platform.html"){
hotelName: "",
travelAgencyId: matter.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || "",
boardingPlace: matter.defaultBoardingPlace || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -1131,6 +1143,7 @@ layout("/layouts/platform.html"){
directFamilyUnitLine: matter.directFamilyUnitLine,
travelAgencyId: matter.travelAgencyId || ledger.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || ledger.travelAgencyName || "",
boardingPlace: ledger.boardingPlace || matter.defaultBoardingPlace || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -1148,12 +1161,32 @@ layout("/layouts/platform.html"){
if (this.familyData.length > 0) {
this.signupForm.hasFamily = true
}
this.ensureBoardingPlace()
this.signupVisible = true
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
parseBoardingPlaceOptions(value) {
if (!value) return []
try {
const list = JSON.parse(value)
if (!Array.isArray(list)) return []
return list.map((item) => {
if (typeof item === "string") return item
return item && item.name ? item.name : ""
}).filter((item) => item)
} catch (e) {
return String(value).split(",").map((item) => item.trim()).filter((item) => item)
}
},
ensureBoardingPlace() {
if (!this.signupForm) return
if (!this.signupForm.boardingPlace && this.boardingPlaceOptions.length === 1) {
this.$set(this.signupForm, "boardingPlace", this.boardingPlaceOptions[0])
}
},
isDirectFamilyLine(row) {
if (!row) return false
return row.directFamilyUnitLine === true
@@ -1274,6 +1307,10 @@ layout("/layouts/platform.html"){
return true
},
submitSignup() {
if (this.boardingPlaceOptions.length > 0 && !this.signupForm.boardingPlace) {
this.$message.warning("请选择乘车地点")
return
}
if (!this.validateFamilies()) return
if (!this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
@@ -1327,6 +1364,7 @@ layout("/layouts/platform.html"){
},
destroySignup() {
this.signupForm = {}
this.boardingPlaceOptions = []
this.familyData = []
this.directRelativeForm = {}
this.allowFamily = false
@@ -29,7 +29,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in lotOptions" :key="item.id" :label="item.lotName" :value="item.id"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -53,7 +53,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -142,9 +142,6 @@ layout("/layouts/platform.html"){
#app .search .search-item {
width: calc((100% - 150px) / 4);
}
#app .search .search-query {
margin-left: auto;
}
@media screen and (max-width: 1350px) {
#app .search .search-item {
width: calc((100% - 100px) / 3);
@@ -0,0 +1,703 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="年度">
<el-date-picker
v-model="pageForm.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%"
@change="pageYearChange">
</el-date-picker>
</search-item>
<search-item label="疗休养配置">
<el-select v-model="pageForm.settingId" clearable filterable placeholder="请选择配置" style="width: 100%" @change="pageSettingChange">
<el-option v-for="item in settingOptions" :key="item.id" :label="item.configName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="分配线路">
<el-select v-model="pageForm.matterId" clearable filterable placeholder="请选择分配线路" style="width: 100%">
<el-option v-for="item in pageMatterOptions" :key="item.matterId" :label="matterOptionLabel(item)" :value="item.matterId"></el-option>
</el-select>
</search-item>
<search-item label="所属分工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择分工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="人员类型">
<el-select v-model="pageForm.personType" clearable placeholder="请选择人员类型" style="width: 100%">
<el-option label="正式人员" value="FORMAL"></el-option>
<el-option label="替补人员" value="BACKUP"></el-option>
</el-select>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="校工会人员分配列表">
<el-button type="primary" size="medium" @click="openAssign">
<i class="el-icon-user"></i>
人员分配
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
class="vi-table"
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="姓名" prop="userName" width="110" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="工号" prop="loginName" width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属分工会" prop="unionName" min-width="160" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="人员类型" prop="personType" width="110" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.personType === 'BACKUP' ? 'warning' : 'success'">{{ personTypeText(row.personType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="是否退出" prop="cancelled" width="110" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="isCancelled(row) ? 'danger' : 'success'">{{ isCancelled(row) ? '已退出' : '未退出' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="分配时间" prop="assignedAt" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="320" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button v-if="!row.matterId && row.personType !== 'BACKUP'" size="mini" type="primary" @click="openSelectMatter(row)">选择线路</el-button>
<el-button v-if="isCancelled(row) && $auth.hasPermission('tour.schoolUserAssignment.cancelRestore')" size="mini" type="warning" @click="restoreCancel(row)">取消退出</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
custom-class="tour-user-assignment-dialog"
title="人员分配"
:visible.sync="assignDialogVisible"
:close-on-click-modal="false"
width="78%"
@closed="resetAssignDialog">
<el-form :model="assignForm" :rules="assignRules" ref="assignFormRef" label-width="110px">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="年度" prop="year">
<el-date-picker
v-model="assignForm.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%"
@change="assignYearChange">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="疗休养配置" prop="settingId">
<el-select v-model="assignForm.settingId" clearable filterable placeholder="请选择配置" style="width: 100%" @change="assignSettingChange">
<el-option v-for="item in assignSettingOptions" :key="item.id" :label="item.configName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="分配线路" prop="matterId">
<el-select
v-model="assignForm.matterId"
clearable
filterable
:disabled="selectedCandidates.length <= 0"
placeholder="请先勾选人员"
style="width: 100%"
@change="assignMatterChange">
<el-option v-for="item in assignMatterOptions" :key="item.matterId" :label="matterOptionLabel(item)" :value="item.matterId"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="candidate-toolbar">
<el-select v-model="candidateForm.unionId" clearable filterable placeholder="所属分工会" style="width: 220px" @change="candidateSearch">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-input
v-model="candidateForm.keyword"
clearable
placeholder="姓名/工号"
style="width: 220px"
@keyup.enter.native="candidateSearch">
</el-input>
<el-button type="primary" icon="el-icon-search" @click="candidateSearch">查询</el-button>
<el-button @click="resetCandidateSearch">重置</el-button>
<span class="candidate-selected">已选 {{ selectedCandidates.length }} 人</span>
</div>
<el-table
ref="candidateTable"
v-loading="candidateLoading"
:data="candidateData"
row-key="userId"
border
size="mini"
height="420"
@selection-change="candidateSelectionChange">
<el-table-column type="selection" width="48" :reserve-selection="true" align="center" header-align="center"></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="工号" prop="loginName" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="手机号" prop="mobile" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属分工会" prop="unionName" min-width="160" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="分配线路" min-width="260" align="center" header-align="center">
<template slot-scope="{row}">
<el-select
v-model="row.assignmentMatterId"
clearable
filterable
:disabled="!isCandidateSelected(row)"
placeholder="请先勾选人员"
style="width: 100%">
<el-option v-for="item in assignMatterOptions" :key="item.matterId" :label="matterOptionLabel(item)" :value="item.matterId"></el-option>
</el-select>
</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container candidate-pagination">
<el-pagination
background
:current-page="candidateForm.pageNumber"
:page-sizes="[10, 20, 50, 100]"
:page-size="candidateForm.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="candidateForm.totalCount"
@size-change="candidateSizeChange"
@current-change="candidatePageChange">
</el-pagination>
</el-row>
<span slot="footer" class="dialog-footer">
<el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="assignSubmitting" @click="doAssign">保存分配</el-button>
</span>
</el-dialog>
<el-dialog
title="选择分配线路"
:visible.sync="selectMatterDialogVisible"
:close-on-click-modal="false"
width="520px"
@closed="resetSelectMatterDialog">
<el-form :model="selectMatterForm" :rules="selectMatterRules" ref="selectMatterFormRef" label-width="110px">
<el-form-item label="分配人员">
<el-input v-model="selectMatterForm.userName" disabled></el-input>
</el-form-item>
<el-form-item label="分配线路" prop="matterId">
<el-select v-model="selectMatterForm.matterId" clearable filterable placeholder="请选择分配线路" style="width: 100%">
<el-option v-for="item in selectMatterOptions" :key="item.matterId" :label="matterOptionLabel(item)" :value="item.matterId"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="selectMatterDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="selectMatterSubmitting" @click="doSelectMatter">保存</el-button>
</span>
</el-dialog>
</div>
<style>
#app .search .search-item {
width: calc((100% - 150px) / 4);
}
.tour-user-assignment-dialog .el-dialog__body {
max-height: 72vh;
overflow-y: auto;
}
.candidate-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.candidate-selected {
color: #606266;
white-space: nowrap;
}
.candidate-pagination {
margin-top: 12px;
margin-bottom: 0;
text-align: right;
}
@media screen and (max-width: 1350px) {
#app .search .search-item {
width: calc((100% - 100px) / 3);
}
}
@media screen and (max-width: 1200px) {
#app .search .search-item {
width: calc((100% - 50px) / 2);
}
}
@media screen and (max-width: 992px) {
#app .search .search-item {
width: 100%;
}
.candidate-selected {
margin-left: 0;
}
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
settingOptions: [],
pageMatterOptions: [],
assignSettingOptions: [],
assignMatterOptions: [],
unionOptions: [],
assignDialogVisible: false,
candidateLoading: false,
candidateData: [],
selectedCandidates: [],
assignSubmitting: false,
selectMatterDialogVisible: false,
selectMatterSubmitting: false,
selectMatterOptions: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "assignedAt",
pageOrderBy: "descending",
year: currentYear,
settingId: "",
matterId: "",
unionId: "",
personType: "",
keyword: ""
},
assignForm: {
year: currentYear,
settingId: "",
matterId: ""
},
candidateForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
unionId: "",
keyword: ""
},
selectMatterForm: {
id: "",
settingId: "",
matterId: "",
userName: ""
},
assignRules: {
year: [{required: true, message: "请选择年度", trigger: ["change", "blur"]}],
settingId: [{required: true, message: "请选择疗休养配置", trigger: ["change", "blur"]}]
},
selectMatterRules: {
matterId: [{required: true, message: "请选择分配线路", trigger: ["change", "blur"]}]
}
}
},
methods: {
defaultAssignForm(currentYear) {
return {
year: currentYear || moment().format("YYYY"),
settingId: "",
matterId: ""
}
},
defaultCandidateForm() {
return {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
unionId: "",
keyword: ""
}
},
defaultSelectMatterForm() {
return {
id: "",
settingId: "",
matterId: "",
userName: ""
}
},
resetSearch() {
const currentYear = moment().format("YYYY")
this.pageForm.year = currentYear
this.pageForm.settingId = ""
this.pageForm.matterId = ""
this.pageForm.unionId = ""
this.pageForm.personType = ""
this.pageForm.keyword = ""
this.loadSettingOptions()
},
pageYearChange() {
this.pageForm.settingId = ""
this.pageForm.matterId = ""
this.pageMatterOptions = []
this.loadSettingOptions()
},
pageSettingChange() {
this.pageForm.matterId = ""
this.loadPageMatterOptions()
},
loadSettingOptions() {
this.$axios.post(loc() + "/settingOptions", {year: this.pageForm.year}).then((res) => {
if (res.code === 0) {
this.settingOptions = res.data || []
// 主页面查询条件默认选择当前年度第一条配置,并以该配置刷新列表。
if (!this.pageForm.settingId && this.settingOptions.length > 0) {
this.pageForm.settingId = this.settingOptions[0].id
this.loadPageMatterOptions()
this.doSearch()
} else {
this.pageMatterOptions = []
this.doSearch()
}
}
})
},
loadPageMatterOptions() {
if (!this.pageForm.settingId) {
this.pageMatterOptions = []
return
}
this.$axios.post(loc() + "/matterOptions", {settingId: this.pageForm.settingId}).then((res) => {
if (res.code === 0) {
this.pageMatterOptions = res.data || []
}
})
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions").then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
}
})
},
openAssign() {
const year = this.pageForm.year || moment().format("YYYY")
this.assignForm = this.defaultAssignForm(year)
this.assignForm.matterId = ""
this.candidateForm = this.defaultCandidateForm()
this.candidateData = []
this.selectedCandidates = []
this.assignDialogVisible = true
this.loadAssignSettingOptions()
},
resetAssignDialog() {
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignMatterOptions = []
this.candidateData = []
this.selectedCandidates = []
this.assignSubmitting = false
},
assignYearChange() {
this.assignForm.settingId = ""
this.assignForm.matterId = ""
this.assignMatterOptions = []
this.clearCandidateSelection()
this.loadAssignSettingOptions()
this.loadCandidatePageData()
},
assignSettingChange() {
this.assignForm.matterId = ""
this.assignMatterOptions = []
this.clearCandidateSelection()
this.loadAssignMatterOptions()
this.loadCandidatePageData()
},
loadAssignSettingOptions() {
this.$axios.post(loc() + "/settingOptions", {year: this.assignForm.year}).then((res) => {
if (res.code === 0) {
this.assignSettingOptions = res.data || []
// 人员分配弹窗独立于主列表筛选,默认取当前年度第一条可用配置。
if (!this.assignForm.settingId && this.assignSettingOptions.length > 0) {
this.assignForm.settingId = this.assignSettingOptions[0].id
this.loadAssignMatterOptions()
this.loadCandidatePageData()
} else if (this.assignForm.settingId) {
this.loadAssignMatterOptions()
this.loadCandidatePageData()
} else {
this.assignMatterOptions = []
this.candidateData = []
this.candidateForm.totalCount = 0
}
}
})
},
loadAssignMatterOptions() {
if (!this.assignForm.settingId) {
this.assignMatterOptions = []
return
}
this.$axios.post(loc() + "/matterOptions", {settingId: this.assignForm.settingId}).then((res) => {
if (res.code === 0) {
this.assignMatterOptions = res.data || []
}
})
},
loadCandidatePageData() {
if (!this.assignForm.settingId) {
this.candidateData = []
this.candidateForm.totalCount = 0
return
}
this.candidateLoading = true
this.$axios.post(loc() + "/candidatePageData", {
pageNumber: this.candidateForm.pageNumber,
pageSize: this.candidateForm.pageSize,
settingId: this.assignForm.settingId,
unionId: this.candidateForm.unionId,
keyword: this.candidateForm.keyword
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.candidateData = (data.list || []).map(item => Object.assign({}, item, {
assignmentMatterId: ""
}))
this.candidateForm.totalCount = data.totalCount || 0
} else {
this.$message.warning(res.msg || "候选人员查询失败")
}
}).finally(() => {
this.candidateLoading = false
})
},
assignMatterChange(value) {
;(this.selectedCandidates || []).forEach(row => {
this.$set(row, "assignmentMatterId", value || "")
})
},
candidateSearch() {
this.candidateForm.pageNumber = 1
this.loadCandidatePageData()
},
resetCandidateSearch() {
this.candidateForm.unionId = ""
this.candidateForm.keyword = ""
this.candidateForm.pageNumber = 1
this.loadCandidatePageData()
},
candidateSizeChange(size) {
this.candidateForm.pageSize = size
this.candidateForm.pageNumber = 1
this.loadCandidatePageData()
},
candidatePageChange(pageNumber) {
this.candidateForm.pageNumber = pageNumber
this.loadCandidatePageData()
},
candidateSelectionChange(rows) {
const selectedIds = (rows || []).map(item => item.userId)
;(this.candidateData || []).forEach(row => {
if (!selectedIds.includes(row.userId)) {
this.$set(row, "assignmentMatterId", "")
} else if (!row.assignmentMatterId && this.assignForm.matterId) {
this.$set(row, "assignmentMatterId", this.assignForm.matterId)
}
})
this.selectedCandidates = rows || []
if (this.selectedCandidates.length <= 0) {
this.assignForm.matterId = ""
}
},
isCandidateSelected(row) {
return !!row && this.selectedCandidates.some(item => item.userId === row.userId)
},
clearCandidateSelection() {
this.selectedCandidates = []
if (this.$refs.candidateTable) {
this.$refs.candidateTable.clearSelection()
}
},
doAssign() {
this.$refs.assignFormRef.validate((valid) => {
if (!valid) return
if (this.selectedCandidates.length <= 0) {
this.$message.warning("请选择需要分配的人员")
return
}
this.$confirm("确定保存当前人员分配吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const assignItems = this.selectedCandidates.map(item => {
return {
userId: item.userId,
matterId: item.assignmentMatterId || this.assignForm.matterId || ""
}
})
this.assignSubmitting = true
this.$axios.post(loc() + "/doAssignItems", {
settingId: this.assignForm.settingId,
assignItems: JSON.stringify(assignItems)
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.$message.success("分配成功" + (data.ledgerCount > 0 ? ",已代报名" + data.ledgerCount + "人" : "") + (data.skipCount > 0 ? ",已跳过" + data.skipCount + "人" : ""))
this.assignDialogVisible = false
this.doSearch()
} else {
this.$message.warning(res.msg || "分配失败")
}
}).finally(() => {
this.assignSubmitting = false
})
}).catch(() => {})
})
},
openSelectMatter(row) {
this.selectMatterForm = {
id: row.id,
settingId: row.settingId,
matterId: "",
userName: row.userName || ""
}
this.selectMatterOptions = []
this.selectMatterDialogVisible = true
this.loadSelectMatterOptions()
},
resetSelectMatterDialog() {
this.selectMatterForm = this.defaultSelectMatterForm()
this.selectMatterOptions = []
this.selectMatterSubmitting = false
},
loadSelectMatterOptions() {
if (!this.selectMatterForm.settingId) {
this.selectMatterOptions = []
return
}
this.$axios.post(loc() + "/matterOptions", {settingId: this.selectMatterForm.settingId}).then((res) => {
if (res.code === 0) {
this.selectMatterOptions = res.data || []
}
})
},
doSelectMatter() {
this.$refs.selectMatterFormRef.validate((valid) => {
if (!valid) return
this.selectMatterSubmitting = true
this.$axios.post(loc() + "/selectMatter", {
id: this.selectMatterForm.id,
matterId: this.selectMatterForm.matterId
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.$message.success("分配线路选择成功" + (data.ledgerCount > 0 ? ",已代报名" + data.ledgerCount + "人" : ""))
this.selectMatterDialogVisible = false
this.doSearch()
} else {
this.$message.warning(res.msg || "分配线路选择失败")
}
}).finally(() => {
this.selectMatterSubmitting = false
})
})
},
doDelete(row) {
this.$axios.post(loc() + "/deleteInfo", {id: row.id}).then((infoRes) => {
if (infoRes.code !== 0) {
this.$message.warning(infoRes.msg || "删除校验失败")
return
}
const info = infoRes.data || {}
const hasLedger = !!info.hasLedger
const message = hasLedger ? "该人员已报名,删除分配记录将同时删除台账数据,是否继续?" : "确定删除该人员分配记录吗?"
this.$confirm(message, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", {
id: row.id,
deleteLedger: hasLedger
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg || "删除成功")
this.doSearch()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
}).catch(() => {})
})
},
personTypeText(personType) {
return personType === "BACKUP" ? "替补人员" : "正式人员"
},
isCancelled(row) {
return row && (row.cancelled === true || row.cancelled === 1)
},
restoreCancel(row) {
this.$confirm("确定将【" + row.userName + "】恢复为未退出状态吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/restoreCancel", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("取消退出成功")
this.doSearch()
} else {
this.$message.warning(res.msg || "取消退出失败")
}
})
}).catch(() => {})
},
matterOptionLabel(item) {
const lineName = item.lineName || ""
return lineName || item.matterName || ""
}
},
mounted() {
this.loadSettingOptions()
this.loadUnionOptions()
}
})
</script>
<!--#
}
#-->
@@ -79,6 +79,7 @@ layout("/layouts/platform.html"){
</guava>
<el-dialog
custom-class="tour-setting-dialog"
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
@@ -145,20 +146,23 @@ layout("/layouts/platform.html"){
<el-input-number v-model="formData.maxGroupPeople" :controls="false" :min="0" :precision="0" placeholder="请输入最多成团人数" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出行人数指标" prop="travelPeopleQuota">
<el-input-number v-model="formData.travelPeopleQuota" :controls="false" :min="0" :precision="0" placeholder="请输入出行人数指标" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="tour-setting-basic-divider"></div>
</el-col>
<el-col :span="12">
<el-form-item label="省外几年去一次" prop="outProvinceYears">
<el-input-number v-model="formData.outProvinceYears" :controls="false" :min="0" :precision="0" placeholder="请输入省外间隔年限" style="width: 100%"></el-input-number>
<el-form-item label="控制省外比例" prop="signupEligibilityMode">
<el-select v-model="formData.signupEligibilityMode" placeholder="请选择控制省外比例" style="width: 100%" @change="signupEligibilityModeChange">
<el-option label="是" value="SCOPE_GROUP"></el-option>
<el-option label="否" value="ASSIGNED_USER"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="省外人数占比" prop="outProvinceRatio">
<el-input-number v-model="formData.outProvinceRatio" :controls="false" :min="0" :max="100" :precision="2" placeholder="请输入省外人数占比" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col v-if="showOutProvinceRatioFields()" :span="12">
<el-form-item label="省外占比类型" prop="outProvinceRatioType">
<el-select v-model="formData.outProvinceRatioType" placeholder="请选择省外占比类型" style="width: 100%" @change="outProvinceRatioTypeChange">
<el-option label="当年报名人数" value="当年报名人数"></el-option>
@@ -167,7 +171,17 @@ layout("/layouts/platform.html"){
</el-select>
</el-form-item>
</el-col>
<el-col v-if="formData.outProvinceRatioType === '固定人数'" :span="12">
<el-col v-if="showOutProvinceRatioFields()" :span="12">
<el-form-item label="省外几年去一次" prop="outProvinceYears">
<el-input-number v-model="formData.outProvinceYears" :controls="false" :min="0" :precision="0" placeholder="请输入省外间隔年限" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col v-if="showOutProvinceRatioFields()" :span="12">
<el-form-item label="省外人数占比" prop="outProvinceRatio">
<el-input-number v-model="formData.outProvinceRatio" :controls="false" :min="0" :max="100" :precision="2" placeholder="请输入省外人数占比" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col v-if="showOutProvinceRatioFields() && formData.outProvinceRatioType === '固定人数'" :span="12">
<el-form-item label="固定人数" prop="outProvinceFixedPeople">
<el-input-number v-model="formData.outProvinceFixedPeople" :controls="false" :min="0" :precision="0" placeholder="请输入固定人数" style="width: 100%"></el-input-number>
</el-form-item>
@@ -227,45 +241,129 @@ layout("/layouts/platform.html"){
<el-switch v-model="formData.enabled" active-text="启用" inactive-text="停用"></el-switch>
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="标段管理" name="lots">
<div style="text-align: right; margin-bottom: 10px;">
<el-button type="primary" size="medium" icon="el-icon-plus" @click="addLot">增加一条</el-button>
<el-col :span="24">
<div class="tour-setting-basic-divider"></div>
<div class="tour-boarding-panel">
<div class="tour-boarding-toolbar">
<span class="tour-boarding-title">乘车地点</span>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="addBoardingPlace">新增</el-button>
</div>
<el-table
:data="formData.lots"
:data="boardingPlaceRows"
border
class="vi-table"
empty-text="暂无标段"
empty-text="暂无乘车地点"
style="width: 100%">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="标段名称" min-width="180" align="center" header-align="center">
<el-table-column label="乘车地点" min-width="260" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.lotName" maxlength="50" placeholder="请输入标段名称"></el-input>
<el-input v-model="row.name" maxlength="100" placeholder="请输入乘车地点"></el-input>
</template>
</el-table-column>
<el-table-column label="标段值" min-width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.lotValue" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'lotValue', $event)"></el-input>
</template>
</el-table-column>
<el-table-column label="标段费用" min-width="150" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.activityCost" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'activityCost', $event)"></el-input>
</template>
</el-table-column>
<el-table-column label="允许超出报销" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-checkbox v-model="row.allowOverReimbursement"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" header-align="center">
<el-table-column label="操作" width="100" align="center" header-align="center">
<template slot-scope="scope">
<el-button type="danger" size="mini" icon="el-icon-delete" @click="deleteLot(scope.$index, scope.row)"></el-button>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="deleteBoardingPlace(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="标段管理" name="lots">
<div class="tour-tab-fill">
<div class="tour-tab-toolbar">
<el-button type="primary" size="medium" icon="el-icon-plus" @click="addLot">增加一条</el-button>
</div>
<el-table
:data="formData.lots"
border
class="vi-table tour-tab-table"
empty-text="暂无标段"
height="100%"
style="width: 100%">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="标段名称" min-width="180" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.lotName" maxlength="50" placeholder="请输入标段名称"></el-input>
</template>
</el-table-column>
<el-table-column label="标段值" min-width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.lotValue" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'lotValue', $event)"></el-input>
</template>
</el-table-column>
<el-table-column label="标段费用" min-width="150" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.activityCost" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'activityCost', $event)"></el-input>
</template>
</el-table-column>
<el-table-column label="允许超出报销" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-checkbox v-model="row.allowOverReimbursement"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" header-align="center">
<template slot-scope="scope">
<el-button type="danger" size="mini" icon="el-icon-delete" @click="deleteLot(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="工会名额分配" name="unionQuota">
<div class="tour-tab-fill">
<div class="tour-quota-toolbar">
<div class="tour-quota-stat">
<span>出行人数指标</span>
<strong>{{ travelPeopleQuota }}</strong>
</div>
<div class="tour-quota-stat">
<span>校工会正式分配数量</span>
<strong>{{ quotaOverview.schoolFormalAssignedCount }}</strong>
</div>
<div class="tour-quota-stat">
<span>分工会总名额</span>
<strong>{{ branchTotalQuota }}</strong>
</div>
<div class="tour-quota-stat">
<span>分工会总会员数</span>
<strong>{{ branchTotalMemberCount }}</strong>
</div>
<el-input-number
v-model="quotaAllocate.ratio"
:controls="false"
:min="0"
:precision="2"
placeholder="比例"
class="tour-quota-input">
</el-input-number>
<el-button type="primary" size="medium" @click="allocateQuotaByRatio">按比例分配</el-button>
</div>
<el-table
:data="formData.unionQuotas"
border
class="vi-table tour-quota-table tour-tab-table"
empty-text="暂无分工会"
height="100%"
show-summary
:summary-method="unionQuotaSummary"
style="width: 100%">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="分工会" prop="unionName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="当前会员数" prop="memberCount" width="120" align="center" header-align="center"></el-table-column>
<el-table-column label="正式人员数量" width="170" align="center" header-align="center">
<template slot-scope="{row}">
<el-input-number v-model="row.formalQuota" :controls="false" :min="0" :precision="0" style="width: 100%"></el-input-number>
</template>
</el-table-column>
<el-table-column label="替补人员数量" width="170" align="center" header-align="center">
<template slot-scope="{row}">
<el-input-number v-model="row.backupQuota" :controls="false" :min="0" :precision="0" style="width: 100%"></el-input-number>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="服务须知" name="notice">
<el-form-item prop="serviceNotice" label-width="0">
@@ -329,8 +427,15 @@ layout("/layouts/platform.html"){
configName: ""
},
activityGroupList: [],
boardingPlaceRows: [],
formData: {},
lotDeleteList: [],
quotaOverview: {
schoolFormalAssignedCount: 0
},
quotaAllocate: {
ratio: null
},
inheritLoading: false,
formRules: {
year: [{required: true, message: "必填", trigger: ["blur", "change"]}],
@@ -347,6 +452,24 @@ layout("/layouts/platform.html"){
components: {
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue")
},
computed: {
travelPeopleQuota() {
return this.toNonNegativeInteger(this.formData.travelPeopleQuota)
},
branchTotalMemberCount() {
const rows = this.formData.unionQuotas || []
return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.memberCount), 0)
},
branchTotalQuota() {
const quota = this.travelPeopleQuota - this.toNonNegativeInteger(this.quotaOverview.schoolFormalAssignedCount)
return quota > 0 ? quota : 0
}
},
watch: {
"formData.travelPeopleQuota": function() {
this.refreshQuotaRatio()
}
},
methods: {
getActivityGroup() {
return this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((res) => {
@@ -358,6 +481,106 @@ layout("/layouts/platform.html"){
})
})
},
loadUnionQuotaRows(settingId) {
this.$axios.post(loc() + "/unionQuotaRows", {settingId: settingId || ""}).then((res) => {
if (res.code === 0) {
this.$set(this.formData, "unionQuotas", this.normalizeUnionQuotaRows(res.data || []))
this.refreshQuotaRatio()
} else {
this.$message.warning(res.msg || "查询分工会名额失败")
}
})
},
loadUnionQuotaOverview(settingId) {
this.$axios.post(loc() + "/unionQuotaOverview", {settingId: settingId || ""}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.quotaOverview.schoolFormalAssignedCount = this.toNonNegativeInteger(data.schoolFormalAssignedCount)
this.refreshQuotaRatio()
} else {
this.$message.warning(res.msg || "查询工会名额统计失败")
}
})
},
normalizeUnionQuotaRows(rows) {
return (rows || []).map(item => {
return Object.assign({}, item, {
formalQuota: this.toNonNegativeInteger(item.formalQuota),
backupQuota: this.toNonNegativeInteger(item.backupQuota),
memberCount: this.toNonNegativeInteger(item.memberCount)
})
})
},
toNonNegativeInteger(value) {
const numberValue = parseInt(value, 10)
if (isNaN(numberValue) || numberValue < 0) {
return 0
}
return numberValue
},
toNumber(value) {
const numberValue = parseFloat(value)
return isNaN(numberValue) ? null : numberValue
},
truncateDecimal(value, precision) {
const times = Math.pow(10, precision)
return Math.floor(value * times) / times
},
refreshQuotaRatio() {
if (this.branchTotalMemberCount <= 0 || this.branchTotalQuota <= 0) {
this.quotaAllocate.ratio = null
return
}
this.quotaAllocate.ratio = this.truncateDecimal(this.branchTotalQuota / this.branchTotalMemberCount, 2)
},
unionQuotaSummary(param) {
const columns = param.columns || []
const rows = this.formData.unionQuotas || []
return columns.map((column, index) => {
if (index === 0) {
return "合计"
}
if (column.property === "memberCount") {
return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.memberCount), 0)
}
if (column.label === "正式人员数量") {
return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.formalQuota), 0)
}
if (column.label === "替补人员数量") {
return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.backupQuota), 0)
}
return ""
})
},
allocateQuotaByRatio() {
const rows = this.formData.unionQuotas || []
const ratio = this.toNumber(this.quotaAllocate.ratio)
if (!rows.length) {
this.$message.warning("暂无分工会数据")
return
}
if (ratio === null || this.quotaAllocate.ratio === "" || this.quotaAllocate.ratio === undefined) {
this.$message.warning("比例为空,未分配")
return
}
if (ratio < 0) {
this.$message.warning("比例不能为负数")
return
}
// 按比例分配:比例 * 分工会会员数,小数直接舍去,不补余数。
const nextFormalQuotas = rows.map(row => {
const memberCount = this.toNonNegativeInteger(row.memberCount)
return Math.floor(ratio * memberCount)
})
const formalTotal = nextFormalQuotas.reduce((sum, value) => sum + value, 0)
if (formalTotal > this.branchTotalQuota) {
this.$message.warning("当前正式人员总数 " + formalTotal + ",分工会总名额 " + this.branchTotalQuota + ",分配后总人数不能超过分工会总名额")
return
}
nextFormalQuotas.forEach((formalQuota, index) => {
this.$set(rows[index], "formalQuota", formalQuota)
})
},
openUserScope() {
if (this.$refs.drawerUserScope) {
this.$refs.drawerUserScope.groupId = this.formData.activityGroupId || ""
@@ -380,6 +603,8 @@ layout("/layouts/platform.html"){
year: moment().format("YYYY"),
configName: "",
tourType: "",
boardingPlace: "[]",
travelPeopleQuota: 0,
activityGroupId: "",
sortNo: 0,
minGroupPeople: 0,
@@ -388,6 +613,7 @@ layout("/layouts/platform.html"){
outProvinceRatio: 0,
outProvinceRatioType: "当年报名人数",
outProvinceFixedPeople: 0,
signupEligibilityMode: "ASSIGNED_USER",
cycleStartYear: "",
cycleEndYear: "",
cycleTotalCost: null,
@@ -396,6 +622,7 @@ layout("/layouts/platform.html"){
fillBedInfo: true,
enabled: true,
lots: [],
unionQuotas: [],
serviceNotice: ""
}
},
@@ -405,7 +632,11 @@ layout("/layouts/platform.html"){
this.activeTab = "basic"
// 新建时给出默认年度和开关值,减少校工会管理员录入成本。
this.formData = this.defaultFormData()
this.boardingPlaceRows = []
this.quotaOverview.schoolFormalAssignedCount = 0
this.dialogVisible = true
this.loadUnionQuotaRows("")
this.loadUnionQuotaOverview("")
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
inheritPreviousYearInfo() {
@@ -430,12 +661,20 @@ layout("/layouts/platform.html"){
allowOverReimbursement: !!item.allowOverReimbursement
}
})
const inheritedUnionQuotas = this.normalizeUnionQuotaRows(previous.unionQuotas || []).map(item => {
return Object.assign({}, item, {
id: "",
settingId: ""
})
})
this.formData = Object.assign(this.defaultFormData(), previous, {
year: String(currentYear),
cycleStartYear: previous.cycleStartYear ? String(previous.cycleStartYear) : "",
cycleEndYear: previous.cycleEndYear ? String(previous.cycleEndYear) : "",
lots: inheritedLots
lots: inheritedLots,
unionQuotas: inheritedUnionQuotas
})
this.boardingPlaceRows = this.parseBoardingPlaceRows(this.formData.boardingPlace)
delete this.formData.id
delete this.formData.createdAt
delete this.formData.createdBy
@@ -445,6 +684,9 @@ layout("/layouts/platform.html"){
if (!this.formData.outProvinceRatioType) {
this.$set(this.formData, "outProvinceRatioType", "当年报名人数")
}
if (!this.formData.signupEligibilityMode) {
this.$set(this.formData, "signupEligibilityMode", "ASSIGNED_USER")
}
if (this.formData.outProvinceFixedPeople === null || this.formData.outProvinceFixedPeople === undefined) {
this.$set(this.formData, "outProvinceFixedPeople", 0)
}
@@ -465,9 +707,12 @@ layout("/layouts/platform.html"){
this.$axios.post(loc() + "/detail", {id: row.id}).then((res) => {
if (res.code === 0) {
this.formData = Object.assign({
boardingPlace: "[]",
travelPeopleQuota: 0,
activityGroupId: "",
outProvinceRatioType: "当年报名人数",
outProvinceFixedPeople: 0,
signupEligibilityMode: "ASSIGNED_USER",
cycleStartYear: "",
cycleEndYear: "",
cycleTotalCost: null,
@@ -476,12 +721,17 @@ layout("/layouts/platform.html"){
fillBedInfo: true,
enabled: true,
lots: [],
unionQuotas: [],
serviceNotice: ""
}, res.data || {})
this.boardingPlaceRows = this.parseBoardingPlaceRows(this.formData.boardingPlace)
this.formData.year = this.formData.year ? String(this.formData.year) : ""
if (!this.formData.outProvinceRatioType) {
this.$set(this.formData, "outProvinceRatioType", "当年报名人数")
}
if (!this.formData.signupEligibilityMode) {
this.$set(this.formData, "signupEligibilityMode", "ASSIGNED_USER")
}
if (this.formData.outProvinceFixedPeople === null || this.formData.outProvinceFixedPeople === undefined) {
this.$set(this.formData, "outProvinceFixedPeople", 0)
}
@@ -493,6 +743,8 @@ layout("/layouts/platform.html"){
this.formData.lots = (this.formData.lots || []).map(item => Object.assign({}, item, {
allowOverReimbursement: !!item.allowOverReimbursement
}))
this.formData.unionQuotas = this.normalizeUnionQuotaRows(this.formData.unionQuotas || [])
this.loadUnionQuotaOverview(row.id)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
@@ -504,13 +756,17 @@ layout("/layouts/platform.html"){
this.$refs.form.validate((valid) => {
if (!valid) return
if (!this.validateLots()) return
if (!this.validateUnionQuotas()) return
this.submitLoading = true
// Nutz 对子表集合按字符串化 JSON 绑定更稳定,和体检项目维护的提交方式保持一致。
const submitData = JSON.parse(JSON.stringify(this.formData))
// 乘车地点用配置表 JSON 字段保存,避免新增表影响原疗休养配置结构。
submitData.boardingPlace = this.stringifyBoardingPlaceRows()
if (submitData.outProvinceRatioType !== "固定人数") {
submitData.outProvinceFixedPeople = 0
}
submitData.lots = JSON.stringify(this.formData.lots || [])
submitData.unionQuotas = JSON.stringify(this.formData.unionQuotas || [])
submitData.lotDeleteList = JSON.stringify(this.lotDeleteList)
this.$axios.post(loc() + "/doSubmit", submitData).then((res) => {
this.submitLoading = false
@@ -537,6 +793,39 @@ layout("/layouts/platform.html"){
allowOverReimbursement: false
})
},
addBoardingPlace() {
this.boardingPlaceRows.push({ name: "" })
},
deleteBoardingPlace(index) {
this.boardingPlaceRows.splice(index, 1)
},
parseBoardingPlaceRows(value) {
if (!value) {
return []
}
try {
const list = JSON.parse(value)
if (!Array.isArray(list)) {
return []
}
return list.map((item) => {
if (typeof item === "string") {
return { name: item }
}
return { name: item && item.name ? item.name : "" }
}).filter((item) => item.name)
} catch (e) {
return String(value).split(",").map((item) => {
return { name: item.trim() }
}).filter((item) => item.name)
}
},
stringifyBoardingPlaceRows() {
const rows = (this.boardingPlaceRows || []).map((item) => {
return { name: item && item.name ? String(item.name).trim() : "" }
}).filter((item) => item.name)
return JSON.stringify(rows)
},
integerInput(row, field, value) {
const nextValue = String(value || "").replace(/[^\d]/g, "")
this.$set(row, field, nextValue)
@@ -547,6 +836,20 @@ layout("/layouts/platform.html"){
}
this.$nextTick(() => this.$refs.form && this.$refs.form.validateField("outProvinceFixedPeople"))
},
showOutProvinceRatioFields() {
return this.formData.signupEligibilityMode === "SCOPE_GROUP"
},
signupEligibilityModeChange(value) {
// 省内外比例控制关闭时,隐藏字段不再参与当前表单校验。
if (value !== "SCOPE_GROUP") {
this.$set(this.formData, "outProvinceFixedPeople", 0)
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(["outProvinceYears", "outProvinceRatio", "outProvinceRatioType", "outProvinceFixedPeople"])
}
})
}
},
validateLots() {
const lots = this.formData.lots || []
for (let i = 0; i < lots.length; i++) {
@@ -564,6 +867,38 @@ layout("/layouts/platform.html"){
}
return true
},
validateUnionQuotas() {
const rows = this.formData.unionQuotas || []
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
if (row.formalQuota === null || row.formalQuota === undefined || row.formalQuota === "") {
row.formalQuota = 0
}
if (row.backupQuota === null || row.backupQuota === undefined || row.backupQuota === "") {
row.backupQuota = 0
}
const formalQuota = this.toNonNegativeInteger(row.formalQuota)
const backupQuota = this.toNonNegativeInteger(row.backupQuota)
if (!/^\d+$/.test(String(row.formalQuota))) {
this.$message.warning("第" + (i + 1) + "行正式人员数量必须为非负整数")
return false
}
if (!/^\d+$/.test(String(row.backupQuota))) {
this.$message.warning("第" + (i + 1) + "行替补人员数量必须为非负整数")
return false
}
this.$set(row, "formalQuota", formalQuota)
this.$set(row, "backupQuota", backupQuota)
}
const formalTotal = rows.reduce((sum, row) => {
return sum + this.toNonNegativeInteger(row.formalQuota)
}, 0)
if (formalTotal > this.branchTotalQuota) {
this.$message.warning("当前正式人员总数 " + formalTotal + ",分工会总名额 " + this.branchTotalQuota + ",分配后总人数不能超过分工会总名额")
return false
}
return true
},
deleteLot(index, row) {
this.$confirm("确定删除该标段吗?", "提示", {
confirmButtonText: "确定",
@@ -594,7 +929,14 @@ layout("/layouts/platform.html"){
},
destroyEditor() {
this.formData = {}
this.boardingPlaceRows = []
this.lotDeleteList = []
this.quotaAllocate = {
ratio: null
}
this.quotaOverview = {
schoolFormalAssignedCount: 0
}
this.inheritLoading = false
this.activeTab = "basic"
}
@@ -618,6 +960,7 @@ layout("/layouts/platform.html"){
}
.tour-setting-inherit {
flex-shrink: 0;
margin-bottom: -40px;
padding-right: 96px;
position: relative;
@@ -629,6 +972,129 @@ layout("/layouts/platform.html"){
border-top: 1px dashed #dcdfe6;
margin: 2px 0 18px;
}
.tour-boarding-toolbar {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.tour-boarding-panel {
max-width: 560px;
}
.tour-boarding-title {
color: #303133;
font-weight: 600;
}
.tour-quota-toolbar {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.tour-quota-input {
width: 160px;
}
.tour-quota-stat {
align-items: center;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: inline-flex;
gap: 8px;
height: 36px;
padding: 0 10px;
}
.tour-quota-stat span {
color: #606266;
}
.tour-quota-stat strong {
color: #303133;
font-weight: 600;
}
.tour-quota-table {
min-height: 0;
}
.tour-tab-fill {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.tour-tab-toolbar {
flex-shrink: 0;
margin-bottom: 10px;
text-align: right;
}
.tour-tab-table {
flex: 1;
min-height: 0;
}
.tour-setting-dialog {
height: 85vh;
max-height: 85vh;
display: flex;
flex-direction: column;
}
.tour-setting-dialog .el-dialog__header {
flex-shrink: 0;
}
.tour-setting-dialog .el-dialog__body {
flex: 1;
min-height: 0;
overflow: hidden;
padding-bottom: 0;
}
.tour-setting-dialog .el-dialog__footer {
flex-shrink: 0;
}
.tour-setting-dialog .el-form {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.tour-setting-dialog .el-tabs {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
min-height: 0;
}
.tour-setting-dialog .el-tabs__header {
flex-shrink: 0;
}
.tour-setting-dialog .el-tabs__content {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 0 6px 18px 0;
}
.tour-setting-dialog .el-tab-pane {
height: 100%;
}
</style>
<!--#
@@ -34,7 +34,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -153,11 +153,20 @@ layout("/layouts/platform.html"){
<el-input v-model="signupForm.hotelName" :disabled="isDirectFamilyLine(signupForm)" maxlength="100" placeholder="请输入报名酒店"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="乘车地点">
<el-select v-model="signupForm.boardingPlace" clearable filterable placeholder="请选择乘车地点" style="width: 100%">
<el-option v-for="item in boardingPlaceOptions" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="报名旅行社">
<el-input v-model="signupForm.travelAgencyName" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="出行时间">
<el-input v-model="signupForm.travelPeriod" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
@@ -477,6 +486,7 @@ layout("/layouts/platform.html"){
fillBedInfo: true,
signupForm: {},
familyData: [],
boardingPlaceOptions: [],
bedTypeOptions: [],
familyRelationshipOptions: [],
directRelativeOptions: [],
@@ -721,6 +731,7 @@ layout("/layouts/platform.html"){
const staff = data.staff || {}
const ledger = data.ledger || {}
const directRelative = data.directRelative || {}
this.boardingPlaceOptions = this.parseBoardingPlaceOptions(matter.boardingPlaceOptions || "")
this.allowFamily = matter.allowFamily === true || matter.allowFamily === 1 || matter.allowFamily === "1"
this.fillBedInfo = matter.fillBedInfo === undefined || matter.fillBedInfo === null || matter.fillBedInfo === true || matter.fillBedInfo === 1 || matter.fillBedInfo === "1"
this.signupForm = Object.assign({
@@ -743,6 +754,7 @@ layout("/layouts/platform.html"){
hotelName: "",
travelAgencyId: matter.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || "",
boardingPlace: matter.defaultBoardingPlace || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -761,6 +773,7 @@ layout("/layouts/platform.html"){
directFamilyUnitLine: matter.directFamilyUnitLine,
travelAgencyId: matter.travelAgencyId || ledger.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || ledger.travelAgencyName || "",
boardingPlace: ledger.boardingPlace || matter.defaultBoardingPlace || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -778,12 +791,32 @@ layout("/layouts/platform.html"){
if (this.familyData.length > 0) {
this.signupForm.hasFamily = true
}
this.ensureBoardingPlace()
this.signupVisible = true
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
parseBoardingPlaceOptions(value) {
if (!value) return []
try {
const list = JSON.parse(value)
if (!Array.isArray(list)) return []
return list.map((item) => {
if (typeof item === "string") return item
return item && item.name ? item.name : ""
}).filter((item) => item)
} catch (e) {
return String(value).split(",").map((item) => item.trim()).filter((item) => item)
}
},
ensureBoardingPlace() {
if (!this.signupForm) return
if (!this.signupForm.boardingPlace && this.boardingPlaceOptions.length === 1) {
this.$set(this.signupForm, "boardingPlace", this.boardingPlaceOptions[0])
}
},
isOutProvinceLine(lineType) {
return lineType === "省外线路" || lineType === "省外"
},
@@ -890,6 +923,10 @@ layout("/layouts/platform.html"){
return true
},
submitSignup() {
if (this.boardingPlaceOptions.length > 0 && !this.signupForm.boardingPlace) {
this.$message.warning("请选择乘车地点")
return
}
if (!this.validateFamilies()) return
if (!this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
@@ -942,6 +979,7 @@ layout("/layouts/platform.html"){
destroySignup() {
this.signupForm = {}
this.familyData = []
this.boardingPlaceOptions = []
this.directRelativeForm = {}
this.allowFamily = false
this.fillBedInfo = true
@@ -42,7 +42,7 @@ layout("/layouts/platform.html"){
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -221,8 +221,8 @@ layout("/layouts/platform.html"){
agencyName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
agencyCode: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactPhone: [{ validator: validateMobile, trigger: ["blur", "change"] }],
email: [{ validator: validateEmail, trigger: ["blur", "change"] }]
contactPhone: [{ validator: validateMobile, required: true, trigger: ["blur", "change"] }],
email: [{ validator: validateEmail, required: true, trigger: ["blur", "change"] }]
}
}
},
@@ -53,7 +53,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -142,9 +142,6 @@ layout("/layouts/platform.html"){
#app .search .search-item {
width: calc((100% - 150px) / 4);
}
#app .search .search-query {
margin-left: auto;
}
@media screen and (max-width: 1350px) {
#app .search .search-item {
width: calc((100% - 100px) / 3);
@@ -53,7 +53,7 @@ layout("/layouts/platform.html"){
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
@@ -479,6 +479,7 @@ layout("/layouts/platform_h5.html"){
<van-field label="线路类型" readonly v-model="signupForm.lineType"></van-field>
<van-field label="出行时间" readonly v-model="signupForm.travelPeriod"></van-field>
<van-field label="报名酒店" v-model="signupForm.hotelName" maxlength="100" placeholder="请输入报名酒店"></van-field>
<van-field readonly clickable is-link label="乘车地点" v-model="signupForm.boardingPlace" placeholder="请选择乘车地点" @click="openBoardingPlacePicker"></van-field>
<van-field v-if="fillBedInfo" readonly clickable is-link label="床型" v-model="signupForm.bedType" placeholder="请选择床型" @click="openBedPicker('self')"></van-field>
<van-field v-if="fillBedInfo" label="床位信息" v-model="signupForm.bedInfo" maxlength="100" placeholder="请输入床位信息"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="signupForm.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
@@ -546,6 +547,9 @@ layout("/layouts/platform_h5.html"){
<van-popup v-model="directRelativePickerVisible" position="bottom">
<van-picker show-toolbar :columns="directRelativeColumns" @confirm="confirmDirectRelative" @cancel="directRelativePickerVisible=false"></van-picker>
</van-popup>
<van-popup v-model="boardingPlacePickerVisible" position="bottom">
<van-picker show-toolbar :columns="boardingPlaceColumns" @confirm="confirmBoardingPlace" @cancel="boardingPlacePickerVisible=false"></van-picker>
</van-popup>
</div>
<script nonce="${cspNonce!}">
@@ -583,6 +587,7 @@ layout("/layouts/platform_h5.html"){
allowFamily: false,
fillBedInfo: true,
signupForm: {},
boardingPlaceOptions: [],
familyData: [],
directRelativeForm: {},
bedTypeOptions: [],
@@ -593,7 +598,8 @@ layout("/layouts/platform_h5.html"){
bedPickerFamilyIndex: -1,
relationshipPickerVisible: false,
relationshipFamilyIndex: -1,
directRelativePickerVisible: false
directRelativePickerVisible: false,
boardingPlacePickerVisible: false
}
},
computed: {
@@ -613,6 +619,9 @@ layout("/layouts/platform_h5.html"){
},
directRelativeColumns() {
return this.directRelativeOptions.map(item => ({ text: item.name || item.code, item }))
},
boardingPlaceColumns() {
return this.boardingPlaceOptions
}
},
methods: {
@@ -774,6 +783,7 @@ layout("/layouts/platform_h5.html"){
const matter = data.matter || {}
const ledger = data.ledger || {}
const directRelative = data.directRelative || {}
this.boardingPlaceOptions = this.parseBoardingPlaceOptions(matter.boardingPlaceOptions || "")
this.allowFamily = matter.allowFamily === true || matter.allowFamily === 1 || matter.allowFamily === "1"
this.fillBedInfo = matter.fillBedInfo === undefined || matter.fillBedInfo === null || matter.fillBedInfo === true || matter.fillBedInfo === 1 || matter.fillBedInfo === "1"
this.signupForm = Object.assign({
@@ -796,6 +806,7 @@ layout("/layouts/platform_h5.html"){
hotelName: "",
travelAgencyId: matter.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || "",
boardingPlace: matter.defaultBoardingPlace || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -814,6 +825,7 @@ layout("/layouts/platform_h5.html"){
directFamilyUnitLine: matter.directFamilyUnitLine,
travelAgencyId: matter.travelAgencyId || ledger.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || ledger.travelAgencyName || "",
boardingPlace: ledger.boardingPlace || matter.defaultBoardingPlace || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -831,9 +843,29 @@ layout("/layouts/platform_h5.html"){
if (this.familyData.length > 0) {
this.signupForm.hasFamily = true
}
this.ensureBoardingPlace()
this.signupVisible = true
})
},
parseBoardingPlaceOptions(value) {
if (!value) return []
try {
const list = JSON.parse(value)
if (!Array.isArray(list)) return []
return list.map((item) => {
if (typeof item === "string") return item
return item && item.name ? item.name : ""
}).filter((item) => item)
} catch (e) {
return String(value).split(",").map((item) => item.trim()).filter((item) => item)
}
},
ensureBoardingPlace() {
if (!this.signupForm) return
if (!this.signupForm.boardingPlace && this.boardingPlaceOptions.length === 1) {
this.$set(this.signupForm, "boardingPlace", this.boardingPlaceOptions[0])
}
},
isDirectFamilyLine(row) {
return row && (row.directFamilyUnitLine === true || row.directFamilyUnitLine === 1 || row.directFamilyUnitLine === "1" || row.directFamilyUnitLine === "true" || row.directFamilyUnitLine === "TRUE" || !!row.directRelativeId)
},
@@ -918,6 +950,17 @@ layout("/layouts/platform_h5.html"){
this.directRelativeForm.relationshipName = item.name || value.text || ""
this.directRelativePickerVisible = false
},
openBoardingPlacePicker() {
if (this.boardingPlaceColumns.length === 0) {
vant.Toast("暂无乘车地点")
return
}
this.boardingPlacePickerVisible = true
},
confirmBoardingPlace(value) {
this.signupForm.boardingPlace = value
this.boardingPlacePickerVisible = false
},
normalizeFamilyIdCard(row) {
if (!row || !row.idCard) return
row.idCard = String(row.idCard).trim().toUpperCase()
@@ -957,6 +1000,10 @@ layout("/layouts/platform_h5.html"){
return true
},
submitSignup() {
if (this.boardingPlaceOptions.length > 0 && !this.signupForm.boardingPlace) {
vant.Toast("请选择乘车地点")
return
}
if (!this.validateFamilies() || !this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
const form = Object.assign({}, this.signupForm, {
@@ -1034,6 +1081,7 @@ layout("/layouts/platform_h5.html"){
},
destroySignup() {
this.signupForm = {}
this.boardingPlaceOptions = []
this.familyData = []
this.directRelativeForm = {}
this.allowFamily = false
@@ -1,5 +1,5 @@
<!--#
layout("/layouts/platform_tour_signup_h5.html"){
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
@@ -120,6 +120,15 @@ layout("/layouts/platform_tour_signup_h5.html"){
<van-field label="线路类型" readonly v-model="signupForm.lineType"></van-field>
<van-field v-if="signupForm.travelPeriod" label="出行时间" readonly v-model="signupForm.travelPeriod"></van-field>
<van-field label="报名酒店" v-model="signupForm.hotelName" maxlength="100" placeholder="请输入报名酒店"></van-field>
<van-field
label="乘车地点"
readonly
clickable
is-link
placeholder="请选择乘车地点"
v-model="signupForm.boardingPlace"
@click="openBoardingPlacePicker">
</van-field>
<van-field
v-if="fillBedInfo"
label="床型"
@@ -233,6 +242,10 @@ layout("/layouts/platform_tour_signup_h5.html"){
<van-popup v-model="directRelativePickerVisible" position="bottom">
<van-picker show-toolbar :columns="directRelativeColumns" @confirm="confirmDirectRelative" @cancel="directRelativePickerVisible=false"></van-picker>
</van-popup>
<van-popup v-model="boardingPlacePickerVisible" position="bottom">
<van-picker show-toolbar :columns="boardingPlaceColumns" @confirm="confirmBoardingPlace" @cancel="boardingPlacePickerVisible=false"></van-picker>
</van-popup>
</div>
<script nonce="${cspNonce!}">
@@ -252,9 +265,11 @@ layout("/layouts/platform_tour_signup_h5.html"){
bedTypeOptions: [],
familyRelationshipOptions: [],
directRelativeOptions: [],
boardingPlaceOptions: [],
bedPickerVisible: false,
relationshipPickerVisible: false,
directRelativePickerVisible: false,
boardingPlacePickerVisible: false,
bedPickerTarget: "self",
matterId: GetQueryString("matterId") || ""
}
@@ -271,6 +286,9 @@ layout("/layouts/platform_tour_signup_h5.html"){
},
directRelativeColumns() {
return this.directRelativeOptions.map(item => ({ text: item.name || item.code, item }))
},
boardingPlaceColumns() {
return this.boardingPlaceOptions
}
},
methods: {
@@ -326,6 +344,7 @@ layout("/layouts/platform_tour_signup_h5.html"){
const staff = data.staff || {}
const ledger = data.ledger || {}
const directRelative = data.directRelative || {}
this.boardingPlaceOptions = this.parseBoardingPlaceOptions(matter.boardingPlaceOptions || "")
this.allowFamily = matter.allowFamily === true || matter.allowFamily === 1 || matter.allowFamily === "1"
this.fillBedInfo = matter.fillBedInfo === undefined || matter.fillBedInfo === null || matter.fillBedInfo === true || matter.fillBedInfo === 1 || matter.fillBedInfo === "1"
this.signupForm = Object.assign({
@@ -349,6 +368,7 @@ layout("/layouts/platform_tour_signup_h5.html"){
hotelName: "",
travelAgencyId: matter.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || "",
boardingPlace: matter.defaultBoardingPlace || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -367,6 +387,7 @@ layout("/layouts/platform_tour_signup_h5.html"){
directFamilyUnitLine: matter.directFamilyUnitLine,
travelAgencyId: matter.travelAgencyId || ledger.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || ledger.travelAgencyName || "",
boardingPlace: ledger.boardingPlace || matter.defaultBoardingPlace || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -388,8 +409,28 @@ layout("/layouts/platform_tour_signup_h5.html"){
this.familyData = []
this.signupForm.hasFamily = false
}
this.ensureBoardingPlace()
})
},
parseBoardingPlaceOptions(value) {
if (!value) return []
try {
const list = JSON.parse(value)
if (!Array.isArray(list)) return []
return list.map((item) => {
if (typeof item === "string") return item
return item && item.name ? item.name : ""
}).filter((item) => item)
} catch (e) {
return String(value).split(",").map((item) => item.trim()).filter((item) => item)
}
},
ensureBoardingPlace() {
if (!this.signupForm) return
if (!this.signupForm.boardingPlace && this.boardingPlaceOptions.length === 1) {
this.$set(this.signupForm, "boardingPlace", this.boardingPlaceOptions[0])
}
},
emptyFamily() {
return {
familyName: "",
@@ -475,6 +516,17 @@ layout("/layouts/platform_tour_signup_h5.html"){
this.directRelativeForm.relationshipName = item.name || value.text || ""
this.directRelativePickerVisible = false
},
openBoardingPlacePicker() {
if (this.boardingPlaceColumns.length === 0) {
vant.Toast("暂无乘车地点")
return
}
this.boardingPlacePickerVisible = true
},
confirmBoardingPlace(value) {
this.signupForm.boardingPlace = value
this.boardingPlacePickerVisible = false
},
normalizeFamilyIdCard(row) {
if (!row || !row.idCard) return
row.idCard = String(row.idCard).trim().toUpperCase()
@@ -528,6 +580,10 @@ layout("/layouts/platform_tour_signup_h5.html"){
},
submitSignup() {
if (this.pageLoading || this.submitLoading) return
if (this.boardingPlaceOptions.length > 0 && !this.signupForm.boardingPlace) {
vant.Toast("请选择乘车地点")
return
}
if (!this.validateFamilies()) return
if (!this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
@@ -1,5 +1,5 @@
<!--#
layout("/layouts/platform_tour_signup_h5.html"){
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
@@ -1,5 +1,5 @@
<!--#
layout("/layouts/platform_tour_signup_h5.html"){
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
@@ -1,5 +1,5 @@
<!--#
layout("/layouts/platform_tour_signup_h5.html"){
layout("/layouts/platform_h5.html"){
#-->
<style scoped>