Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/main/resources/db/menu/init_tour_menu.sql
This commit is contained in:
2026-06-09 15:18:43 +08:00
40 changed files with 3264 additions and 502 deletions
@@ -83,7 +83,10 @@ public class TourGroupController {
m.`year`,
m.matterName,
m.unionId,
COALESCE(u.name, '校工会') AS unionName,
CASE
WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN COALESCE(u.name, '校工会')
ELSE ''
END AS unionName,
m.lineId,
l.lineName,
l.lineType,
@@ -632,7 +635,7 @@ public class TourGroupController {
return "m.matterName";
}
if ("unionName".equals(orderName)) {
return "u.name";
return "CASE WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN COALESCE(u.name, '校工会') ELSE '' END";
}
if ("lineType".equals(orderName)) {
return "l.lineType";
@@ -36,6 +36,8 @@ import java.util.stream.Collectors;
@At("/platform/tour/route")
public class TourLineController {
private static final String SORT_CODE_PATTERN = "^\\d+$";
@Inject
private TourLineService tourLineService;
@@ -52,7 +54,7 @@ public class TourLineController {
@SaCheckPermission("tour.route")
public Result pageData(PageForm pageForm, Integer year, String lineName, String lotId) {
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
pageForm.defaultSortDesc("enabled");
pageForm.defaultSortAsc("lineCode");
}
Cnd cnd = Cnd.NEW();
@@ -91,7 +93,7 @@ public class TourLineController {
LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId
$creatorJoin
$condition
ORDER BY $orderColumn $orderBy, l.`year` DESC, l.lineCode ASC, l.createdAt DESC
ORDER BY $orderColumn $orderBy, l.`year` DESC, CAST(l.lineCode AS UNSIGNED) ASC, l.createdAt DESC
""");
listSql.setVar("creatorJoin", creatorJoin);
listSql.setCondition(cnd);
@@ -168,7 +170,7 @@ public class TourLineController {
sameCodeCnd.and(TourLine::getId, "<>", line.getId());
}
if (tourLineService.count(sameCodeCnd) > 0) {
return Result.error("同年度下线路编号已存在");
return Result.error("同年度下排序编号已存在");
}
if (line.getEnabled() == null) {
line.setEnabled(true);
@@ -197,10 +199,47 @@ public class TourLineController {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
TourLine line = tourLineService.fetch(id);
if (line == null) {
return Result.error("线路不存在");
}
if (countLineSignup(id) > 0) {
return Result.error("当前线路已有人员报名,不能删除");
}
if (countLineMatter(id) > 0) {
return Result.error("当前线路已关联事项,不能删除");
}
tourLineService.delete(id);
return Result.success();
}
private int countLineSignup(String lineId) {
Sql sql = Sqls.create("""
SELECT COUNT(1)
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId
WHERE t.delFlag = 0
AND (t.lineId = @lineId OR m.lineId = @lineId)
""");
sql.setParam("lineId", lineId);
sql.setCallback(Sqls.callback.integer());
tourLineService.dao().execute(sql);
return sql.getInt();
}
private int countLineMatter(String lineId) {
Sql sql = Sqls.create("""
SELECT COUNT(1)
FROM tour_matter
WHERE delFlag = 0
AND lineId = @lineId
""");
sql.setParam("lineId", lineId);
sql.setCallback(Sqls.callback.integer());
tourLineService.dao().execute(sql);
return sql.getInt();
}
private Result check(TourLine line) {
if (line == null) {
return Result.error("参数错误");
@@ -215,7 +254,10 @@ public class TourLineController {
return Result.error("线路名称不能为空");
}
if (StrUtil.isBlank(line.getLineCode())) {
return Result.error("线路编号不能为空");
return Result.error("排序编号不能为空");
}
if (!line.getLineCode().matches(SORT_CODE_PATTERN)) {
return Result.error("排序编号必须为整数");
}
if (StrUtil.isBlank(line.getLineType())) {
return Result.error("线路类型不能为空");
@@ -281,7 +323,7 @@ public class TourLineController {
return "l.`year`";
}
if ("lineCode".equals(orderName)) {
return "l.lineCode";
return "CAST(l.lineCode AS UNSIGNED)";
}
if ("lotName".equals(orderName)) {
return "lot.lotName";
@@ -76,7 +76,10 @@ public class TourMatterController {
SELECT
m.*,
s.configName AS settingName,
u.name AS unionName,
CASE
WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN COALESCE(u.name, '校工会')
ELSE ''
END AS unionName,
d.name AS organizationTypeName,
l.lineName AS lineName,
l.lineType AS lineType,
@@ -157,6 +160,35 @@ public class TourMatterController {
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.matter")
public Result lineInfo(String lineId) {
if (StrUtil.isBlank(lineId)) {
return Result.error("参数错误");
}
Cnd cnd = Cnd.NEW();
cnd.and("l.delFlag", "=", false);
cnd.and("l.enabled", "=", true);
cnd.and("l.id", "=", lineId);
Sql sql = Sqls.create("""
SELECT
l.*,
lot.lotName AS lotName,
lot.lotValue AS lotValue,
lot.lotValue AS lotDays,
lot.activityCost AS activityCost
FROM tour_line l
LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId
$condition
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.map());
tourMatterService.dao().execute(sql);
NutMap line = sql.getObject(NutMap.class);
return line == null || line.isEmpty() ? Result.error("线路不存在") : Result.success(line);
}
@At
@SaCheckPermission("tour.matter")
public Result organizationTypeOptions() {
@@ -194,7 +226,7 @@ public class TourMatterController {
} else {
tourMatterService.updateIgnoreNull(matter);
}
return Result.success();
return Result.success(NutMap.NEW().addv("id", matter.getId()));
}
@At
@@ -246,21 +278,31 @@ public class TourMatterController {
@At
@SaCheckPermission("tour.matter")
public Result settingPeople(String matterId) {
if (StrUtil.isBlank(matterId)) {
public Result settingPeople(String matterId, Integer year) {
if (StrUtil.isBlank(matterId) && year == null) {
return Result.error("参数错误");
}
TourMatter matter = tourMatterService.fetch(matterId);
if (matter == null || StrUtil.isBlank(matter.getSettingId())) {
return Result.success(NutMap.NEW());
return Result.success(settingDefaults(year, matterId));
}
private NutMap settingDefaults(Integer year, String matterId) {
TourMatter matter = null;
if (StrUtil.isNotBlank(matterId)) {
matter = tourMatterService.fetch(matterId);
}
Integer settingYear = year != null ? year : (matter == null ? null : matter.getYear());
TourSetting setting = fetchSettingByYear(settingYear);
if (setting == null && matter != null && StrUtil.isNotBlank(matter.getSettingId())) {
setting = tourMatterService.dao().fetch(TourSetting.class, matter.getSettingId());
}
TourSetting setting = tourMatterService.dao().fetch(TourSetting.class, matter.getSettingId());
if (setting == null) {
return Result.success(NutMap.NEW());
return NutMap.NEW();
}
return Result.success(NutMap.NEW()
return NutMap.NEW()
.addv("minGroupPeople", setting.getMinGroupPeople())
.addv("maxGroupPeople", setting.getMaxGroupPeople()));
.addv("maxGroupPeople", setting.getMaxGroupPeople())
.addv("signupStartTime", setting.getSignupStartTime())
.addv("signupEndTime", setting.getSignupEndTime());
}
@At
@@ -268,6 +310,7 @@ public class TourMatterController {
@SaCheckPermission("tour.matter")
@SLog(type = "tour", tag = "疗休养事项", msg = "保存事项线路配置")
public Result lineConfigDoSubmit(TourMatter matter) {
fillLineConfigFromSetting(matter);
Result checkResult = checkLineConfig(matter);
if (checkResult != null) {
return checkResult;
@@ -290,6 +333,48 @@ public class TourMatterController {
return Result.success();
}
private void fillLineConfigFromSetting(TourMatter matter) {
if (matter == null || StrUtil.isBlank(matter.getId())) {
return;
}
TourMatter oldMatter = tourMatterService.fetch(matter.getId());
if (oldMatter == null) {
return;
}
TourSetting setting = fetchSettingByYear(oldMatter.getYear());
if (setting == null && StrUtil.isNotBlank(oldMatter.getSettingId())) {
setting = tourMatterService.dao().fetch(TourSetting.class, oldMatter.getSettingId());
}
if (setting == null) {
return;
}
if (setting.getMinGroupPeople() != null) {
matter.setMinGroupPeople(setting.getMinGroupPeople());
}
if (setting.getMaxGroupPeople() != null) {
matter.setMaxGroupPeople(setting.getMaxGroupPeople());
}
if (StrUtil.isNotBlank(setting.getSignupStartTime())) {
matter.setSignupStartTime(setting.getSignupStartTime());
}
if (StrUtil.isNotBlank(setting.getSignupEndTime())) {
matter.setSignupEndTime(setting.getSignupEndTime());
}
}
private TourSetting fetchSettingByYear(Integer year) {
if (year == null) {
return null;
}
Cnd cnd = Cnd.where(TourSetting::getEnabled, "=", true);
cnd.and("delFlag", "=", false);
cnd.and(TourSetting::getYear, "=", year);
cnd.desc(TourSetting::getCreatedAt);
cnd.desc(TourSetting::getUpdatedAt);
cnd.asc(TourSetting::getConfigName);
return tourMatterService.dao().fetch(TourSetting.class, cnd);
}
private Result check(TourMatter matter) {
if (matter == null) {
return Result.error("参数错误");
@@ -422,7 +507,7 @@ public class TourMatterController {
return "m.matterName";
}
if ("unionName".equals(orderName)) {
return "u.name";
return "CASE WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN COALESCE(u.name, '校工会') ELSE '' END";
}
if ("lineName".equals(orderName)) {
return "l.lineName";
@@ -55,7 +55,6 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -146,6 +145,10 @@ public class TourMySignupController {
IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName,
IFNULL(f.familyCount, 0) AS familyCount,
COALESCE(m.travelEndTime, tp.travelEndTime) AS travelEndTime,
CASE
WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN COALESCE(u.name, '校工会')
ELSE ''
END AS matterUnionName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
@@ -163,6 +166,7 @@ public class TourMySignupController {
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 sys_union u ON u.id = m.unionId
LEFT JOIN (
SELECT
`year`,
@@ -213,7 +217,7 @@ public class TourMySignupController {
.and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
NutMap signupConfig = getSignupConfig(ledger);
return Result.success(NutMap.NEW()
.addv("ledger", ledger)
.addv("ledger", buildLedgerView(ledger))
.addv("travelPeriod", getTravelPeriod(ledger))
.addv("families", tourLedgerFamilyService.query(familyCnd))
.addv("directRelative", directRelative)
@@ -326,11 +330,18 @@ public class TourMySignupController {
.and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
return Result.success(NutMap.NEW()
.addv("matter", matter)
.addv("ledger", ledger)
.addv("ledger", buildLedgerView(ledger))
.addv("families", tourLedgerFamilyService.query(familyCnd))
.addv("directRelative", directRelative));
}
private NutMap buildLedgerView(TourLedger ledger) {
NutMap map = Lang.obj2nutmap(ledger);
View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where("loginname", "=", ledger.getJobNo()));
map.addv("mobile", defaultIfBlank(ledger.getMobile(), user == null ? "" : defaultIfBlank(user.getMobile(), "")));
return map;
}
@At
@SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR)
public Result directRelativeOptions() {
@@ -587,6 +598,9 @@ public class TourMySignupController {
if (StrUtil.isBlank(ledger.getLineName())) {
return Result.error("报名线路不能为空");
}
if (StrUtil.isBlank(ledger.getIdCard())) {
return Result.error("身份证号不能为空");
}
return null;
}
@@ -612,14 +626,16 @@ public class TourMySignupController {
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
}
if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) {
String signupStartTime = defaultIfBlank(setting.getSignupStartTime(), matter.getSignupStartTime());
String signupEndTime = defaultIfBlank(setting.getSignupEndTime(), matter.getSignupEndTime());
if (StrUtil.isBlank(signupStartTime) || StrUtil.isBlank(signupEndTime)) {
return Result.error("报名时间未配置");
}
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) {
if (now.compareTo(normalizeDateTime(signupStartTime, false)) < 0) {
return Result.error("报名未开始");
}
if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) {
if (now.compareTo(normalizeDateTime(signupEndTime, true)) > 0) {
return Result.error("报名已结束");
}
CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger);
@@ -794,76 +810,35 @@ public class TourMySignupController {
}
private OutProvinceQuota calculateOutProvinceQuota(TourMatter matter, TourSetting setting, TourLedger oldLedger) {
String ratioType = normalizeOutProvinceRatioType(setting == null ? null : setting.getOutProvinceRatioType());
BigDecimal originalRatio = setting == null ? null : setting.getOutProvinceRatio();
boolean oneThirdRatio = isOneThirdRatio(originalRatio);
BigDecimal ratio = oneThirdRatio ? BigDecimal.ONE.divide(BigDecimal.valueOf(3), 12, RoundingMode.HALF_UP)
: (originalRatio == null ? BigDecimal.ZERO : originalRatio);
int year = matter == null || matter.getYear() == null ? LocalDate.now().getYear() : matter.getYear();
String excludeLedgerId = oldLedger == null ? null : oldLedger.getId();
int totalSignupPeople = countYearSignupPeople(year, excludeLedgerId);
int domesticSignupPeople = countYearDomesticSignupPeople(year, excludeLedgerId);
int outProvinceSignupPeople = countYearOutProvinceSignupPeople(year, excludeLedgerId);
int basePeople;
if ("可参加教职工人数".equals(ratioType)) {
Integer groupId = parseInteger(setting == null ? null : setting.getActivityGroupId());
basePeople = countActivityGroupPeople(groupId);
} else if ("固定人数".equals(ratioType)) {
basePeople = setting == null || setting.getOutProvinceFixedPeople() == null ? 0 : setting.getOutProvinceFixedPeople();
} else {
basePeople = totalSignupPeople;
}
int quotaPeople = oneThirdRatio
? basePeople / 3
: ratio.multiply(BigDecimal.valueOf(basePeople)).setScale(0, RoundingMode.FLOOR).intValue();
int allowPeople = quotaPeople - outProvinceSignupPeople;
return new OutProvinceQuota(ratioType, ratio, basePeople, totalSignupPeople, outProvinceSignupPeople, allowPeople);
}
private boolean isOneThirdRatio(BigDecimal ratio) {
if (ratio == null) {
return false;
}
return ratio.compareTo(BigDecimal.valueOf(0.33)) >= 0 && ratio.compareTo(BigDecimal.valueOf(0.3333)) <= 0;
}
private String normalizeOutProvinceRatioType(String ratioType) {
if ("当年参加人数".equals(ratioType) || StrUtil.isBlank(ratioType)) {
return "当年报名人数";
}
if ("可参加教职工人数".equals(ratioType) || "固定人数".equals(ratioType)) {
return ratioType;
}
return "当年报名人数";
int allowPeople = domesticSignupPeople / 2 - outProvinceSignupPeople;
return new OutProvinceQuota("当年省内报名人数", domesticSignupPeople,
domesticSignupPeople + outProvinceSignupPeople, outProvinceSignupPeople, allowPeople);
}
private String buildOutProvinceQuotaNotice(OutProvinceQuota quota) {
String suffix = quota.canApply() ? "您可以报名,或您稍后报名" : "请稍后报名";
if ("当年报名人数".equals(quota.ratioType())) {
return "当前报名人数是" + quota.totalSignupPeople() + "人,已经报名省外线路人数是"
+ quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText()
+ "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix;
}
if ("可参加教职工人数".equals(quota.ratioType())) {
return "今年有权限报名人数是" + quota.basePeople() + "人,已经报名省外线路人数是"
+ quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText()
+ "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix;
}
return "按照省外人数不超过" + quota.ratioText()
return "当前报省内线路教职工有效人数是" + quota.basePeople() + "人,已经报名省外线路教职工有效人数是"
+ quota.outProvinceSignupPeople() + "人,按照省外人数不超过出行人数的1/3规则"
+ "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix;
}
private int countYearSignupPeople(int year, String excludeLedgerId) {
private int countYearDomesticSignupPeople(int year, String excludeLedgerId) {
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId";
Sql sql = Sqls.create("""
SELECT COUNT(1)
SELECT COUNT(DISTINCT t.id)
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
WHERE t.delFlag = 0
AND t.`year` = @year
AND (ins.id IS NULL OR ins.state = @finishedState)
AND t.lineType IN ('省内线路', '省内', '国内线路', '国内')
AND (ins.id IS NULL OR ins.state <> @abandonState)
""" + excludeSql);
sql.setParam("year", year);
sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode());
sql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode());
if (StrUtil.isNotBlank(excludeLedgerId)) {
sql.setParam("excludeLedgerId", excludeLedgerId);
}
@@ -875,16 +850,16 @@ public class TourMySignupController {
private int countYearOutProvinceSignupPeople(int year, String excludeLedgerId) {
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId";
Sql sql = Sqls.create("""
SELECT COUNT(1)
SELECT COUNT(DISTINCT t.id)
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
WHERE t.delFlag = 0
AND t.`year` = @year
AND t.lineType IN ('省外线路', '省外')
AND (ins.id IS NULL OR ins.state = @finishedState)
AND (ins.id IS NULL OR ins.state <> @abandonState)
""" + excludeSql);
sql.setParam("year", year);
sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode());
sql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode());
if (StrUtil.isNotBlank(excludeLedgerId)) {
sql.setParam("excludeLedgerId", excludeLedgerId);
}
@@ -1089,7 +1064,6 @@ public class TourMySignupController {
}
private record OutProvinceQuota(String ratioType,
BigDecimal ratio,
int basePeople,
int totalSignupPeople,
int outProvinceSignupPeople,
@@ -1099,14 +1073,7 @@ public class TourMySignupController {
}
String ratioText() {
if (ratio == null || BigDecimal.ZERO.compareTo(ratio) == 0) {
return "0";
}
BigDecimal oneThird = BigDecimal.ONE.divide(BigDecimal.valueOf(3), 8, RoundingMode.HALF_UP);
if (ratio.subtract(oneThird).abs().compareTo(BigDecimal.valueOf(0.01)) <= 0) {
return "1/3";
}
return ratio.stripTrailingZeros().toPlainString();
return "floor(省内有效人数/2)-省外有效人数";
}
}
@@ -1338,6 +1305,7 @@ public class TourMySignupController {
.addv("applyDate", firstNotBlank(approvalTimes.getString("applyDate", ""), ledger.getSignupTime()))
.addv("unionAuditorName", approvalTimes.getString("unionAuditorName", ""))
.addv("unionAuditDate", approvalTimes.getString("unionAuditDate", ""))
.addv("schoolUnionAuditorName", approvalTimes.getString("schoolUnionAuditorName", ""))
.addv("schoolUnionAuditDate", approvalTimes.getString("schoolUnionAuditDate", ""));
}
@@ -1389,6 +1357,7 @@ public class TourMySignupController {
.addv("applyDate", firstNotBlank(approvalTimes.getString("applyDate", ""), ledger.getSignupTime()))
.addv("unionAuditorName", approvalTimes.getString("unionAuditorName", ""))
.addv("unionAuditDate", approvalTimes.getString("unionAuditDate", ""))
.addv("schoolUnionAuditorName", approvalTimes.getString("schoolUnionAuditorName", ""))
.addv("schoolUnionAuditDate", approvalTimes.getString("schoolUnionAuditDate", ""))
.addv("relativeUnitAuditDate", "");
}
@@ -1444,6 +1413,7 @@ public class TourMySignupController {
result.put("unionAuditorName", parseTaskUserName(item.getString("variable", "")));
} else if ("校工会审核".equals(displayName)) {
result.put("schoolUnionAuditDate", finishDate);
result.put("schoolUnionAuditorName", parseTaskUserName(item.getString("variable", "")));
}
}
return result;
@@ -1580,6 +1550,7 @@ public class TourMySignupController {
drawPlainDateLine(writer, data.getString("unionAuditDate", ""), 330, paragraphEndY - 163, 18);
writer.text("学校工会:盖章", 18, 300, paragraphEndY - 190, false);
writer.text(data.getString("schoolUnionAuditorName", ""), 18, 410, paragraphEndY - 190, false);
drawPlainDateLine(writer, data.getString("schoolUnionAuditDate", ""), 330, paragraphEndY - 217, 18);
}
document.save(out);
@@ -1644,6 +1615,7 @@ public class TourMySignupController {
signY -= 78;
writer.text("学校工会:盖章", 15, signX + 20, signY, false);
writer.text(data.getString("schoolUnionAuditorName", ""), 15, signX + 135, signY, false);
drawPlainDateLine(writer, data.getString("schoolUnionAuditDate", ""), signX + 45, signY - 31, 15);
signY -= 78;
@@ -1823,6 +1795,9 @@ public class TourMySignupController {
if ("travelPeriod".equals(orderName)) {
return "COALESCE(m.travelStartTime, tp.travelPeriod)";
}
if ("matterUnionName".equals(orderName)) {
return "matterUnionName";
}
if ("lineType".equals(orderName)) {
return "t.lineType";
}
@@ -7,6 +7,7 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative;
@@ -20,6 +21,7 @@ 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.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
@@ -180,7 +182,7 @@ public class TourSchoolUnionApprovalController {
.and(TourLedgerDirectRelative::getDelFlag, "=", false));
NutMap signupConfig = getSignupConfig(ledger);
return Result.success(NutMap.NEW()
.addv("ledger", ledger)
.addv("ledger", buildLedgerView(ledger))
.addv("travelPeriod", getTravelPeriod(ledger))
.addv("families", tourLedgerFamilyService.query(familyCnd))
.addv("directRelative", directRelative)
@@ -189,6 +191,13 @@ public class TourSchoolUnionApprovalController {
.addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false)));
}
private NutMap buildLedgerView(TourLedger ledger) {
NutMap map = Lang.obj2nutmap(ledger);
View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where("loginname", "=", ledger.getJobNo()));
map.addv("mobile", StrUtil.blankToDefault(ledger.getMobile(), user == null ? "" : StrUtil.blankToDefault(user.getMobile(), "")));
return map;
}
@At
@SaCheckPermission(value = {"tour.schoolUnionApproval", "h5.tour.schoolUnionApproval"}, mode = SaMode.OR)
public Result unionOptions(Boolean audit, Integer startYear, Integer endYear, String keyword, String lineId, String travelPeriod, String lineType) {
@@ -209,6 +209,12 @@ public class TourSettingController {
&& tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) {
return Result.error("最少成团人数不能大于最多成团人数");
}
if (StrUtil.isBlank(tourSetting.getSignupStartTime()) || StrUtil.isBlank(tourSetting.getSignupEndTime())) {
return Result.error("报名时间不能为空");
}
if (tourSetting.getSignupStartTime().compareTo(tourSetting.getSignupEndTime()) >= 0) {
return Result.error("开始报名时间必须小于结束报名时间");
}
if (tourSetting.getCycleStartYear() != null && tourSetting.getCycleEndYear() != null
&& tourSetting.getCycleStartYear() > tourSetting.getCycleEndYear()) {
return Result.error("周期开始年度不能大于周期结束年度");
@@ -34,6 +34,7 @@ import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -44,8 +45,6 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -368,6 +367,63 @@ public class TourSignupController {
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.signup")
public Result signupPageData(PageForm pageForm, String matterId, String keyword, String unionId) {
return querySignupPageData(pageForm, matterId, keyword, unionId);
}
@At("/h5/signupPageData")
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result h5SignupPageData(PageForm pageForm, String matterId, String keyword, String unionId) {
return querySignupPageData(pageForm, matterId, keyword, unionId);
}
private Result querySignupPageData(PageForm pageForm, String matterId, String keyword, String unionId) {
if (StrUtil.isBlank(matterId)) {
return Result.error("参数错误");
}
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
pageForm.defaultSortDesc("signupTime");
}
Cnd cnd = buildSignupQueryCnd(matterId, keyword, unionId);
Sql countSql = Sqls.create("""
SELECT COUNT(DISTINCT t.id)
FROM tour_ledger t
INNER JOIN tour_matter m ON m.id = t.matterId
INNER JOIN tour_line l ON l.id = m.lineId
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT
t.id,
t.jobNo,
t.userName,
t.unionId,
t.unionName,
t.signupTime
FROM tour_ledger t
INNER JOIN tour_matter m ON m.id = t.matterId
INNER JOIN tour_line l ON l.id = m.lineId
$condition
ORDER BY $orderColumn $orderBy, t.signupTime DESC, t.createdAt DESC
""");
listSql.setCondition(cnd);
listSql.setVar("orderColumn", getSignupOrderColumn(pageForm.getPageOrderName()));
listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc");
listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(listSql);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class));
return Result.success(pagination);
}
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result lineDetail(String lineId) {
@@ -395,7 +451,7 @@ public class TourSignupController {
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result signupDetail(String matterId) {
public Result signupDetail(String matterId, Boolean noticeChecked) {
if (StrUtil.isBlank(matterId)) {
return Result.error("参数错误");
}
@@ -420,6 +476,8 @@ public class TourSignupController {
l.travelAgencyId,
a.agencyName AS travelAgencyName,
IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement,
IFNULL(sc.signupCount, 0) AS signupCount,
IFNULL(sc.familyCount, 0) AS familyCount,
s.allowFamily,
IFNULL(s.fillBedInfo, 1) AS fillBedInfo
FROM tour_matter m
@@ -427,6 +485,23 @@ public class TourSignupController {
LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId
LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId
LEFT JOIN tour_setting s ON s.id = m.settingId
LEFT JOIN (
SELECT
t.matterId,
COUNT(1) + SUM(IFNULL(f.familyCount, 0)) AS signupCount,
SUM(IFNULL(f.familyCount, 0)) AS familyCount
FROM tour_ledger t
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
WHERE t.delFlag = 0
AND t.matterId IS NOT NULL
AND t.matterId <> ''
GROUP BY t.matterId
) sc ON sc.matterId = m.id
WHERE m.delFlag = 0
AND m.enabled = 1
AND m.id = @matterId
@@ -461,9 +536,11 @@ public class TourSignupController {
ruleLedger.setMatterId(matter.getString("matterId"));
ruleLedger.setLineId(matter.getString("lineId"));
ruleLedger.setLineName(matter.getString("lineName"));
Result ruleResult = checkSignupRule(ruleLedger, matterEntity, ledger);
if (ruleResult != null) {
return ruleResult;
if (!Boolean.TRUE.equals(noticeChecked)) {
Result ruleResult = checkSignupRule(ruleLedger, matterEntity, ledger);
if (ruleResult != null) {
return ruleResult;
}
}
List<TourLedgerFamily> families = Collections.emptyList();
TourLedgerDirectRelative directRelative = null;
@@ -591,26 +668,39 @@ public class TourSignupController {
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result directRelativeOptions() {
List<Sys_dict> list = sysDictService.getSubListByCode("directRelative");
if (list == null) {
return Result.success(Collections.emptyList());
}
return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList()));
return Result.success(getEnabledDictOptions("directRelative"));
}
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result signupOptions() {
return Result.success(NutMap.NEW()
.addv("bedTypeOptions", getEnabledDictOptions("bedType"))
.addv("familyRelationshipOptions", getFamilyRelationshipOptions())
.addv("directRelativeOptions", getEnabledDictOptions("directRelative")));
}
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result bedTypeOptions() {
List<Sys_dict> list = sysDictService.getSubListByCode("bedType");
if (list == null) {
return Result.success(Collections.emptyList());
}
return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList()));
return Result.success(getEnabledDictOptions("bedType"));
}
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result familyRelationshipOptions() {
return Result.success(getFamilyRelationshipOptions());
}
private List<Sys_dict> getEnabledDictOptions(String code) {
List<Sys_dict> list = sysDictService.getSubListByCode(code);
if (list == null) {
return Collections.emptyList();
}
return list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList());
}
private List<Sys_dict> getFamilyRelationshipOptions() {
Sql sql = Sqls.create("""
SELECT d.*
FROM sys_dict p
@@ -624,7 +714,7 @@ public class TourSignupController {
sql.setCallback(Sqls.callback.entities());
sql.setEntity(tourLedgerService.dao().getEntity(Sys_dict.class));
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(Sys_dict.class));
return sql.getList(Sys_dict.class);
}
@At
@@ -772,6 +862,26 @@ public class TourSignupController {
return cnd;
}
private Cnd buildSignupQueryCnd(String matterId, String keyword, String unionId) {
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.and("t.matterId", "=", matterId);
cnd.and("m.delFlag", "=", false);
cnd.and("m.enabled", "=", true);
cnd.and("m.lineId", "IS NOT", null);
cnd.and("m.lineId", "<>", "");
cnd.and("l.delFlag", "=", false);
cnd.and("l.enabled", "=", true);
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup keywordGroup = new SqlExpressionGroup();
keywordGroup.orLike("t.userName", keyword.trim());
keywordGroup.orLike("t.jobNo", keyword.trim());
cnd.and(keywordGroup);
}
cnd.andEX("t.unionId", "=", unionId);
return cnd;
}
private Result checkSignup(TourLedger ledger) {
if (ledger == null) {
return Result.error("参数错误");
@@ -788,6 +898,9 @@ public class TourSignupController {
if (StrUtil.isBlank(ledger.getLineName())) {
return Result.error("报名线路不能为空");
}
if (StrUtil.isBlank(ledger.getIdCard())) {
return Result.error("身份证号不能为空");
}
return null;
}
@@ -887,14 +1000,16 @@ public class TourSignupController {
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
}
if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) {
String signupStartTime = defaultIfBlank(setting.getSignupStartTime(), matter.getSignupStartTime());
String signupEndTime = defaultIfBlank(setting.getSignupEndTime(), matter.getSignupEndTime());
if (StrUtil.isBlank(signupStartTime) || StrUtil.isBlank(signupEndTime)) {
return Result.error("报名时间未配置");
}
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) {
if (now.compareTo(normalizeDateTime(signupStartTime, false)) < 0) {
return Result.error("报名未开始");
}
if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) {
if (now.compareTo(normalizeDateTime(signupEndTime, true)) > 0) {
return Result.error("报名已结束");
}
CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger);
@@ -1069,76 +1184,35 @@ public class TourSignupController {
}
private OutProvinceQuota calculateOutProvinceQuota(TourMatter matter, TourSetting setting, TourLedger oldLedger) {
String ratioType = normalizeOutProvinceRatioType(setting == null ? null : setting.getOutProvinceRatioType());
BigDecimal originalRatio = setting == null ? null : setting.getOutProvinceRatio();
boolean oneThirdRatio = isOneThirdRatio(originalRatio);
BigDecimal ratio = oneThirdRatio ? BigDecimal.ONE.divide(BigDecimal.valueOf(3), 12, RoundingMode.HALF_UP)
: (originalRatio == null ? BigDecimal.ZERO : originalRatio);
int year = matter == null || matter.getYear() == null ? LocalDate.now().getYear() : matter.getYear();
String excludeLedgerId = oldLedger == null ? null : oldLedger.getId();
int totalSignupPeople = countYearSignupPeople(year, excludeLedgerId);
int domesticSignupPeople = countYearDomesticSignupPeople(year, excludeLedgerId);
int outProvinceSignupPeople = countYearOutProvinceSignupPeople(year, excludeLedgerId);
int basePeople;
if ("可参加教职工人数".equals(ratioType)) {
Integer groupId = parseInteger(setting == null ? null : setting.getActivityGroupId());
basePeople = countActivityGroupPeople(groupId);
} else if ("固定人数".equals(ratioType)) {
basePeople = setting == null || setting.getOutProvinceFixedPeople() == null ? 0 : setting.getOutProvinceFixedPeople();
} else {
basePeople = totalSignupPeople;
}
int quotaPeople = oneThirdRatio
? basePeople / 3
: ratio.multiply(BigDecimal.valueOf(basePeople)).setScale(0, RoundingMode.FLOOR).intValue();
int allowPeople = quotaPeople - outProvinceSignupPeople;
return new OutProvinceQuota(ratioType, ratio, basePeople, totalSignupPeople, outProvinceSignupPeople, allowPeople);
}
private boolean isOneThirdRatio(BigDecimal ratio) {
if (ratio == null) {
return false;
}
return ratio.compareTo(BigDecimal.valueOf(0.33)) >= 0 && ratio.compareTo(BigDecimal.valueOf(0.3333)) <= 0;
}
private String normalizeOutProvinceRatioType(String ratioType) {
if ("当年参加人数".equals(ratioType) || StrUtil.isBlank(ratioType)) {
return "当年报名人数";
}
if ("可参加教职工人数".equals(ratioType) || "固定人数".equals(ratioType)) {
return ratioType;
}
return "当年报名人数";
int allowPeople = domesticSignupPeople / 2 - outProvinceSignupPeople;
return new OutProvinceQuota("当年省内报名人数", domesticSignupPeople,
domesticSignupPeople + outProvinceSignupPeople, outProvinceSignupPeople, allowPeople);
}
private String buildOutProvinceQuotaNotice(OutProvinceQuota quota) {
String suffix = quota.canApply() ? "您可以报名,或您稍后报名" : "请稍后报名";
if ("当年报名人数".equals(quota.ratioType())) {
return "当前报名人数是" + quota.totalSignupPeople() + "人,已经报名省外线路人数是"
+ quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText()
+ "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix;
}
if ("可参加教职工人数".equals(quota.ratioType())) {
return "今年有权限报名人数是" + quota.basePeople() + "人,已经报名省外线路人数是"
+ quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText()
+ "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix;
}
return "按照省外人数不超过" + quota.ratioText()
return "当前报省内线路教职工有效人数是" + quota.basePeople() + "人,已经报名省外线路教职工有效人数是"
+ quota.outProvinceSignupPeople() + "人,按照省外人数不超过出行人数的1/3规则"
+ "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix;
}
private int countYearSignupPeople(int year, String excludeLedgerId) {
private int countYearDomesticSignupPeople(int year, String excludeLedgerId) {
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId";
Sql sql = Sqls.create("""
SELECT COUNT(1)
SELECT COUNT(DISTINCT t.id)
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
WHERE t.delFlag = 0
AND t.`year` = @year
AND (ins.id IS NULL OR ins.state = @finishedState)
AND t.lineType IN ('省内线路', '省内', '国内线路', '国内')
AND (ins.id IS NULL OR ins.state <> @abandonState)
""" + excludeSql);
sql.setParam("year", year);
sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode());
sql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode());
if (StrUtil.isNotBlank(excludeLedgerId)) {
sql.setParam("excludeLedgerId", excludeLedgerId);
}
@@ -1150,16 +1224,16 @@ public class TourSignupController {
private int countYearOutProvinceSignupPeople(int year, String excludeLedgerId) {
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId";
Sql sql = Sqls.create("""
SELECT COUNT(1)
SELECT COUNT(DISTINCT t.id)
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
WHERE t.delFlag = 0
AND t.`year` = @year
AND t.lineType IN ('省外线路', '省外')
AND (ins.id IS NULL OR ins.state = @finishedState)
AND (ins.id IS NULL OR ins.state <> @abandonState)
""" + excludeSql);
sql.setParam("year", year);
sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode());
sql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode());
if (StrUtil.isNotBlank(excludeLedgerId)) {
sql.setParam("excludeLedgerId", excludeLedgerId);
}
@@ -1267,7 +1341,6 @@ public class TourSignupController {
}
private record OutProvinceQuota(String ratioType,
BigDecimal ratio,
int basePeople,
int totalSignupPeople,
int outProvinceSignupPeople,
@@ -1277,14 +1350,7 @@ public class TourSignupController {
}
String ratioText() {
if (ratio == null || BigDecimal.ZERO.compareTo(ratio) == 0) {
return "0";
}
BigDecimal oneThird = BigDecimal.ONE.divide(BigDecimal.valueOf(3), 8, RoundingMode.HALF_UP);
if (ratio.subtract(oneThird).abs().compareTo(BigDecimal.valueOf(0.01)) <= 0) {
return "1/3";
}
return ratio.stripTrailingZeros().toPlainString();
return "floor(省内有效人数/2)-省外有效人数";
}
}
@@ -1457,6 +1523,19 @@ public class TourSignupController {
return user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname());
}
private String getSignupOrderColumn(String orderName) {
if ("jobNo".equals(orderName)) {
return "t.jobNo";
}
if ("userName".equals(orderName)) {
return "t.userName";
}
if ("unionName".equals(orderName)) {
return "t.unionName";
}
return "t.signupTime";
}
private String getOrderColumn(String orderName) {
if ("lineName".equals(orderName)) {
return "l.lineName";
@@ -23,6 +23,7 @@ public class TourTravelAgencyController {
private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$";
private static final String EMAIL_PATTERN = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$";
private static final String SORT_CODE_PATTERN = "^\\d+$";
@Inject
private TourTravelAgencyService travelAgencyService;
@@ -77,7 +78,7 @@ public class TourTravelAgencyController {
sameCodeCnd.and(TourTravelAgency::getId, "<>", agency.getId());
}
if (travelAgencyService.count(sameCodeCnd) > 0) {
return Result.error("同年度下旅行社编号已存在");
return Result.error("同年度下排序编号已存在");
}
if (agency.getEnabled() == null) {
agency.setEnabled(true);
@@ -113,7 +114,10 @@ public class TourTravelAgencyController {
return Result.error("旅行社名称不能为空");
}
if (StrUtil.isBlank(agency.getAgencyCode())) {
return Result.error("旅行社编号不能为空");
return Result.error("排序编号不能为空");
}
if (!agency.getAgencyCode().matches(SORT_CODE_PATTERN)) {
return Result.error("排序编号必须为整数");
}
if (StrUtil.isBlank(agency.getContactName())) {
return Result.error("联系人不能为空");
@@ -0,0 +1,530 @@
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.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.sys.models.Sys_union;
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.TourMatter;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
import org.nutz.dao.Cnd;
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;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@IocBean
@Ok("json:full")
@At("/platform/tour/undertake")
public class TourUndertakeController {
@Inject
private TourMatterService tourMatterService;
@Inject
private TourLedgerService tourLedgerService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/undertake/index.html")
@SaCheckPermission("tour.undertake")
public void index() {
}
@At
@SaCheckPermission("tour.undertake")
public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId, String undertakeStatus) {
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
pageForm.defaultSortAsc("lineName");
}
Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId, undertakeStatus);
boolean sysAdmin = AuthUtil.hasRole(RoleConstant.SYSADMIN.name());
boolean showCurrentUnionSignupCount = !AuthUtil.hasRole(RoleConstant.SYSADMIN.name())
&& StrUtil.isNotBlank(SecurityUtil.getUnionId());
String currentUnionId = showCurrentUnionSignupCount ? SecurityUtil.getUnionId() : "";
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_matter m
INNER JOIN tour_line l ON l.id = m.lineId
LEFT JOIN sys_union u ON u.id = m.unionId
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourMatterService.dao().execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT
m.id AS matterId,
m.`year`,
m.matterName,
m.unionId,
CASE
WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN COALESCE(u.name, '校工会')
ELSE ''
END AS unionName,
IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') AS undertakeStatus,
CASE
WHEN IFNULL(NULLIF(m.undertakeStatus, ''), 'unclaimed') = 'claimed' THEN '已领'
ELSE '未领'
END AS undertakeStatusName,
m.lineId,
l.lineName,
l.lineType,
l.directFamilyUnitLine,
m.travelStartTime,
m.travelEndTime,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE ''
END AS travelPeriod,
m.minGroupPeople,
m.maxGroupPeople,
IFNULL(sc.signupCount, 0) AS signupCount,
IFNULL(usc.currentUnionSignupCount, 0) AS currentUnionSignupCount,
CASE
WHEN m.maxGroupPeople IS NOT NULL AND m.maxGroupPeople > 0 AND IFNULL(sc.signupCount, 0) > m.maxGroupPeople THEN 'over'
WHEN m.minGroupPeople IS NOT NULL AND m.minGroupPeople > 0 AND IFNULL(sc.signupCount, 0) >= m.minGroupPeople THEN 'formed'
ELSE 'unformed'
END AS groupStatus,
CASE
WHEN m.maxGroupPeople IS NOT NULL AND m.maxGroupPeople > 0 AND IFNULL(sc.signupCount, 0) > m.maxGroupPeople THEN '超员'
WHEN m.minGroupPeople IS NOT NULL AND m.minGroupPeople > 0 AND IFNULL(sc.signupCount, 0) >= m.minGroupPeople THEN '已成团'
ELSE '未成团'
END AS groupStatusName
FROM tour_matter m
INNER JOIN tour_line l ON l.id = m.lineId
LEFT JOIN sys_union u ON u.id = m.unionId
LEFT JOIN (
SELECT t.matterId, SUM(1 + IFNULL(f.familyCount, 0)) AS signupCount
FROM tour_ledger t
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
WHERE t.delFlag = 0
AND t.matterId IS NOT NULL
AND t.matterId <> ''
GROUP BY t.matterId
) sc ON sc.matterId = m.id
LEFT JOIN (
SELECT t.matterId, SUM(1 + IFNULL(f.familyCount, 0)) AS currentUnionSignupCount
FROM tour_ledger t
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
WHERE t.delFlag = 0
AND t.matterId IS NOT NULL
AND t.matterId <> ''
AND t.unionId = @currentUnionId
GROUP BY t.matterId
) usc ON usc.matterId = m.id
$condition
ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC
""");
listSql.setCondition(cnd);
listSql.setParam("currentUnionId", currentUnionId);
listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName()));
listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
tourMatterService.dao().execute(listSql);
List<NutMap> list = listSql.getList(NutMap.class);
for (NutMap item : list) {
item.put("showCurrentUnionSignupCount", showCurrentUnionSignupCount);
item.put("canRelease", sysAdmin && "claimed".equals(item.getString("undertakeStatus", "")));
}
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@At
@SaCheckPermission("tour.undertake")
public Result unionOptions() {
Cnd cnd = Cnd.NEW();
cnd.asc("unionCode");
cnd.asc("name");
return Result.success(tourMatterService.dao().query(Sys_union.class, cnd));
}
@At
@SaCheckPermission("tour.undertake")
public Result currentUnion() {
NutMap unionInfo = currentUnionInfo();
if (unionInfo.isEmpty()) {
return Result.error("未获取到当前登录人工会");
}
return Result.success(unionInfo);
}
@At
@SaCheckPermission("tour.undertake")
public Result undertakeLine(String matterId) {
if (StrUtil.isBlank(matterId)) {
return Result.error("参数错误");
}
NutMap unionInfo = currentUnionInfo();
if (unionInfo.isEmpty()) {
return Result.error("未获取到当前登录人工会");
}
TourMatter matter = tourMatterService.fetch(matterId);
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag())) {
return Result.error("事项不存在");
}
if ("claimed".equals(matter.getUndertakeStatus())) {
return Result.error("当前线路已领办,不允许修改");
}
matter.setUnionId(unionInfo.getString("unionId"));
matter.setUndertakeStatus("claimed");
tourMatterService.update(matter);
return Result.success(unionInfo);
}
@At
@SaCheckPermission("tour.undertake")
public Result releaseUndertakeLine(String matterId) {
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
return Result.error("仅系统管理员可以解除领办");
}
if (StrUtil.isBlank(matterId)) {
return Result.error("参数错误");
}
TourMatter matter = tourMatterService.fetch(matterId);
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag())) {
return Result.error("事项不存在");
}
if (!"claimed".equals(matter.getUndertakeStatus())) {
return Result.error("当前线路未领办,无需解除");
}
matter.setUnionId("");
matter.setUndertakeStatus("unclaimed");
tourMatterService.update(matter);
return Result.success();
}
@At
@SaCheckPermission("tour.undertake")
public Result lineTypeOptions(Integer year) {
Cnd cnd = buildQueryCnd(year, null, null, null, null);
Sql sql = Sqls.create("""
SELECT DISTINCT l.lineType
FROM tour_matter m
INNER JOIN tour_line l ON l.id = m.lineId
LEFT JOIN sys_union u ON u.id = m.unionId
$condition
ORDER BY l.lineType ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourMatterService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
private NutMap currentUnionInfo() {
String unionId = SecurityUtil.getUnionId();
if (StrUtil.isBlank(unionId)) {
return NutMap.NEW();
}
Sys_union union = tourMatterService.dao().fetch(Sys_union.class, unionId);
if (union == null) {
return NutMap.NEW();
}
return NutMap.NEW()
.addv("unionId", union.getId())
.addv("unionName", union.getName());
}
@At
@SaCheckPermission("tour.undertake")
public Result signupPageData(PageForm pageForm, String matterId, String keyword, String unionId) {
if (StrUtil.isBlank(matterId)) {
return Result.error("参数错误");
}
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
pageForm.defaultSortDesc("signupTime");
}
Cnd cnd = buildSignupQueryCnd(matterId, keyword, unionId);
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_ledger t
INNER JOIN tour_matter m ON m.id = t.matterId
INNER JOIN tour_line l ON l.id = m.lineId
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT
t.id,
t.jobNo,
t.userName,
'' AS mobile,
t.idCard,
t.unionId,
t.unionName,
t.signupTime
FROM tour_ledger t
INNER JOIN tour_matter m ON m.id = t.matterId
INNER JOIN tour_line l ON l.id = m.lineId
$condition
ORDER BY $orderColumn $orderBy, t.signupTime DESC, t.createdAt DESC
""");
listSql.setCondition(cnd);
listSql.setVar("orderColumn", getSignupOrderColumn(pageForm.getPageOrderName()));
listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc");
listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(listSql);
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);
}
private Cnd buildSignupQueryCnd(String matterId, String keyword, String unionId) {
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.and("t.matterId", "=", matterId);
cnd.and("m.delFlag", "=", false);
cnd.and("m.enabled", "=", true);
cnd.and("m.lineId", "IS NOT", null);
cnd.and("m.lineId", "<>", "");
cnd.and("l.delFlag", "=", false);
cnd.and("l.enabled", "=", true);
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup keywordGroup = new SqlExpressionGroup();
keywordGroup.orLike("t.userName", keyword.trim());
keywordGroup.orLike("t.jobNo", keyword.trim());
cnd.and(keywordGroup);
}
cnd.andEX("t.unionId", "=", unionId);
applyMatterDataScope(cnd);
return cnd;
}
private void fillSignupMobile(List<NutMap> list) {
if (list == null || list.isEmpty()) {
return;
}
List<String> jobNos = new ArrayList<>();
for (NutMap item : list) {
String jobNo = item.getString("jobNo", "");
if (StrUtil.isNotBlank(jobNo) && !jobNos.contains(jobNo)) {
jobNos.add(jobNo);
}
}
if (jobNos.isEmpty()) {
return;
}
Sql sql = Sqls.create("SELECT loginname, mobile FROM vw_user WHERE loginname IN (@jobNos)");
sql.setParam("jobNos", jobNos.toArray(new String[0]));
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
Map<String, String> mobileMap = new HashMap<>();
for (NutMap user : sql.getList(NutMap.class)) {
mobileMap.put(user.getString("loginname", ""), user.getString("mobile", ""));
}
for (NutMap item : list) {
item.put("mobile", mobileMap.getOrDefault(item.getString("jobNo", ""), ""));
}
}
/**
* 查看成团报名人员时按台账ID带出家属子列表,避免前端逐行请求家属信息。
*/
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.contains(ledgerId)) {
ledgerIds.add(ledgerId);
}
}
if (ledgerIds.isEmpty()) {
return;
}
Sql sql = Sqls.create("""
SELECT
ledgerId,
staffJobNo AS jobNo,
familyName AS userName,
'' 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 Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId, String undertakeStatus) {
Cnd cnd = Cnd.NEW();
cnd.and("m.delFlag", "=", false);
cnd.and("m.enabled", "=", true);
cnd.and("m.lineId", "IS NOT", null);
cnd.and("m.lineId", "<>", "");
cnd.and("l.delFlag", "=", false);
cnd.and("l.enabled", "=", true);
cnd.andEX("m.`year`", "=", year == null ? LocalDate.now().getYear() : year);
cnd.and(Cnd.likeEX("l.lineName", lineName));
cnd.andEX("l.lineType", "=", lineType);
cnd.andEX("m.unionId", "=", unionId);
if ("claimed".equals(undertakeStatus)) {
cnd.and("m.undertakeStatus", "=", "claimed");
} else if ("unclaimed".equals(undertakeStatus)) {
SqlExpressionGroup statusGroup = new SqlExpressionGroup();
statusGroup.or("m.undertakeStatus", "=", "unclaimed");
statusGroup.or("m.undertakeStatus", "IS", null);
statusGroup.or("m.undertakeStatus", "=", "");
cnd.and(statusGroup);
}
applyMatterDataScope(cnd);
return cnd;
}
/**
* 线路成团按事项归属控制数据范围:校工会查看全部;分工会查看本工会事项;其它角色仅查看自己创建的事项。
*/
private void applyMatterDataScope(Cnd cnd) {
if (hasSchoolUnionScope() || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
return;
}
if (hasBranchUnionScope()) {
cnd.and("m.unionId", "=", SecurityUtil.getUnionId());
return;
}
SqlExpressionGroup creatorGroup = new SqlExpressionGroup();
creatorGroup.or("m.creatorUserId", "=", SecurityUtil.getUserId());
creatorGroup.or("m.createdBy", "=", SecurityUtil.getUserId());
cnd.and(creatorGroup);
}
private boolean hasSchoolUnionScope() {
return AuthUtil.hasRole(RoleConstant.SYSADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CHAIRMAN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_VICE_CHAIRMAN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_WELFARE_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_MEMBER_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_TC_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_WC_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_DC_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_PROPOSAL_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
}
private boolean hasBranchUnionScope() {
return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_VICE_CHAIRMAN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_OPERATOR.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_GROUP_LEADER.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ARTICLE_WRITER.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WY.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ZUZHI_WY.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_XUANCHUAN_WY.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_NVGONG_WY.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_QINGNIAN_WY.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_WY.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_SHENGGHUO_WY.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_TIAOJIE_WY.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name());
}
private String getOrderColumn(String orderName) {
if ("lineName".equals(orderName)) {
return "l.lineName";
}
if ("travelPeriod".equals(orderName)) {
return "m.travelStartTime";
}
if ("matterName".equals(orderName)) {
return "m.matterName";
}
if ("unionName".equals(orderName)) {
return "u.name";
}
if ("undertakeStatusName".equals(orderName)) {
return "undertakeStatus";
}
if ("lineType".equals(orderName)) {
return "l.lineType";
}
if ("signupCount".equals(orderName)) {
return "signupCount";
}
if ("minGroupPeople".equals(orderName)) {
return "m.minGroupPeople";
}
if ("maxGroupPeople".equals(orderName)) {
return "m.maxGroupPeople";
}
if ("groupStatusName".equals(orderName)) {
return "groupStatus";
}
return "l.lineName";
}
private String getSignupOrderColumn(String orderName) {
if ("jobNo".equals(orderName)) {
return "t.jobNo";
}
if ("userName".equals(orderName)) {
return "t.userName";
}
if ("idCard".equals(orderName)) {
return "t.idCard";
}
if ("unionName".equals(orderName)) {
return "t.unionName";
}
return "t.signupTime";
}
}
@@ -7,6 +7,7 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative;
@@ -20,6 +21,7 @@ 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.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
@@ -180,7 +182,7 @@ public class TourUnionApprovalController {
.and(TourLedgerDirectRelative::getDelFlag, "=", false));
NutMap signupConfig = getSignupConfig(ledger);
return Result.success(NutMap.NEW()
.addv("ledger", ledger)
.addv("ledger", buildLedgerView(ledger))
.addv("travelPeriod", getTravelPeriod(ledger))
.addv("families", tourLedgerFamilyService.query(familyCnd))
.addv("directRelative", directRelative)
@@ -189,6 +191,13 @@ public class TourUnionApprovalController {
.addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false)));
}
private NutMap buildLedgerView(TourLedger ledger) {
NutMap map = Lang.obj2nutmap(ledger);
View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where("loginname", "=", ledger.getJobNo()));
map.addv("mobile", StrUtil.blankToDefault(ledger.getMobile(), user == null ? "" : StrUtil.blankToDefault(user.getMobile(), "")));
return map;
}
@At
@SaCheckPermission(value = {"tour.unionApproval", "h5.tour.unionApproval"}, mode = SaMode.OR)
public Result unionOptions(Boolean audit, Integer startYear, Integer endYear, String keyword, String lineId, String travelPeriod, String lineType) {
@@ -51,6 +51,11 @@ public class TourLedger extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 30)
private String idCard;
@Column
@Comment("手机号")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String mobile;
@Column
@Comment("所在单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@@ -32,7 +32,7 @@ public class TourLine extends BaseModel implements Serializable {
private Integer year;
@Column
@Comment("线路编号")
@Comment("排序编号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String lineCode;
@@ -56,6 +56,12 @@ public class TourMatter extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("领办状态")
@ColDefine(type = ColType.VARCHAR, width = 20)
@Default("unclaimed")
private String undertakeStatus;
@Column
@Comment("组织形式")
@ColDefine(type = ColType.VARCHAR, width = 100)
@@ -63,6 +63,16 @@ public class TourSetting extends BaseModel implements Serializable {
@ColDefine(type = ColType.INT)
private Integer maxGroupPeople;
@Column
@Comment("报名开始时间")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String signupStartTime;
@Column
@Comment("报名结束时间")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String signupEndTime;
@Column
@Comment("省外几年去一次")
@ColDefine(type = ColType.INT)
@@ -32,7 +32,7 @@ public class TourTravelAgency extends BaseModel implements Serializable {
private Integer year;
@Column
@Comment("旅行社编号")
@Comment("排序编号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String agencyCode;
@@ -99,6 +99,12 @@ FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.group') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10013', p.id, CONCAT(p.path, '0012'), '领办线路', 'Tour Undertake', 'menu', '/platform/tour/undertake', 'data-pjax', '', 1, 0, 'tour.undertake', NULL, 12, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.undertake') t);
-- 给系统管理员默认授权,其他角色可在角色管理中按需分配。
INSERT INTO sys_role_menu (roleId, menuId)
SELECT r.id, m.id
@@ -111,6 +117,7 @@ JOIN sys_menu m ON m.permission IN (
'tour.matter',
'tour.signup',
'tour.group',
'tour.undertake',
'tour.mysignup',
'tour.unionledger',
'tour.unionApproval',
@@ -121,6 +128,17 @@ LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
WHERE r.code = 'SYSADMIN'
AND rm.roleId IS NULL;
INSERT INTO sys_role_menu (roleId, menuId)
SELECT r.id, m.id
FROM sys_role r
JOIN sys_menu m ON m.permission IN (
'tour',
'tour.undertake'
)
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
WHERE r.code = 'BRANCH_UNION_CHAIRMAN'
AND rm.roleId IS NULL;
-- 普惠疗休养移动端菜单。
-- 手机端接口使用 h5. 前缀权限,保留 PC 权限的同时支持单独分配 H5 角色。
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
@@ -0,0 +1,2 @@
ALTER TABLE `tour_ledger`
ADD COLUMN `mobile` varchar(30) DEFAULT NULL COMMENT '手机号' AFTER `idCard`;
@@ -0,0 +1,6 @@
ALTER TABLE `tour_matter`
ADD COLUMN `undertakeStatus` varchar(20) DEFAULT 'unclaimed' COMMENT '领办状态 claimed已领 unclaimed未领' AFTER `unionId`;
UPDATE `tour_matter`
SET `undertakeStatus` = 'unclaimed'
WHERE `undertakeStatus` IS NULL OR `undertakeStatus` = '';
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
`creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID',
`creatorName` varchar(100) DEFAULT NULL COMMENT '创建人',
`unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID',
`undertakeStatus` varchar(20) DEFAULT 'unclaimed' COMMENT '领办状态 claimed已领 unclaimed未领',
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
@@ -0,0 +1,4 @@
-- 疗休养配置增加全局报名时间字段。
ALTER TABLE `tour_setting`
ADD COLUMN `signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间' AFTER `maxGroupPeople`,
ADD COLUMN `signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间' AFTER `signupStartTime`;
@@ -9,6 +9,8 @@ CREATE TABLE IF NOT EXISTS `tour_setting` (
`sortNo` int DEFAULT NULL COMMENT '排序编号',
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
`signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间',
`signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间',
`outProvinceYears` int DEFAULT NULL COMMENT '省外几年去一次',
`outProvinceRatio` decimal(10,2) DEFAULT NULL COMMENT '省外人数占比',
`outProvinceRatioType` varchar(50) DEFAULT '当年报名人数' COMMENT '省外人数占比类型',
@@ -116,6 +118,7 @@ CREATE TABLE IF NOT EXISTS `tour_matter` (
`creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID',
`creatorName` varchar(100) DEFAULT NULL COMMENT '创建人',
`unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID',
`undertakeStatus` varchar(20) DEFAULT 'unclaimed' COMMENT '领办状态 claimed已领 unclaimed未领',
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
@@ -0,0 +1,16 @@
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10013', p.id, CONCAT(p.path, '0012'), '领办线路', 'Tour Undertake', 'menu', '/platform/tour/undertake', 'data-pjax', '', 1, 0, 'tour.undertake', NULL, 12, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.undertake') t);
INSERT INTO sys_role_menu (roleId, menuId)
SELECT r.id, m.id
FROM sys_role r
JOIN sys_menu m ON m.permission IN (
'tour',
'tour.undertake'
)
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
WHERE r.code IN ('SYSADMIN', 'BRANCH_UNION_CHAIRMAN')
AND rm.roleId IS NULL;
@@ -173,8 +173,8 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
@@ -58,13 +58,13 @@ layout("/layouts/platform.html"){
@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="year" width="110" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="事项名称" prop="matterName" min-width="200" 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="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="组织形式" prop="organizationTypeName" width="150" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="事项名称" prop="matterName" min-width="240" sortable="custom" 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="lineName" min-width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="组织形式" prop="organizationTypeName" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系人" prop="contactName" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系方式" prop="contactPhone" width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="事项状态" prop="enabled" width="160" align="center" header-align="center">
<el-table-column label="事项状态" prop="enabled" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
v-model="row.enabled"
@@ -74,7 +74,7 @@ layout("/layouts/platform.html"){
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="260" align="center" header-align="center">
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openBatchForm(row)">选择线路</el-button>
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
@@ -116,7 +116,7 @@ layout("/layouts/platform.html"){
</el-col>
</el-row>
<el-form-item label="事项名称" prop="matterName">
<el-input v-model="formData.matterName" maxlength="30" show-word-limit placeholder="请输入事项名称"></el-input>
<el-input v-model="formData.matterName" maxlength="30" show-word-limit placeholder="如恩施大峡谷五日第一批次"></el-input>
</el-form-item>
<el-form-item label="创建人">
<el-input v-model="formData.creatorName" readonly placeholder="当前登录人"></el-input>
@@ -504,12 +504,10 @@ layout("/layouts/platform.html"){
this.batchForm.travelStartTime = this.dateOnly(this.batchForm.travelStartTime)
this.batchForm.travelEndTime = this.dateOnly(this.batchForm.travelEndTime)
this.applySelectedLineDefaults(false)
if (!this.batchForm.minGroupPeople || !this.batchForm.maxGroupPeople) {
this.loadSettingPeople((data) => {
this.batchForm.minGroupPeople = data.minGroupPeople || null
this.batchForm.maxGroupPeople = data.maxGroupPeople || null
})
}
this.loadSettingDefaults((data) => {
this.applySettingPeople(data)
this.applySettingSignupTime(data)
})
this.batchFormVisible = true
this.$nextTick(() => {
this.applySelectedLineDefaults(true)
@@ -589,8 +587,39 @@ layout("/layouts/platform.html"){
if (!value) return ""
return String(value).substring(0, 10)
},
loadSettingPeople(callback) {
this.$axios.post(loc() + "/settingPeople", { matterId: this.currentMatter.id }).then((res) => {
applySettingSignupTime(data) {
if (!data) return
if (data.signupStartTime) {
this.$set(this.batchForm, "signupStartTime", data.signupStartTime)
}
if (data.signupEndTime) {
this.$set(this.batchForm, "signupEndTime", data.signupEndTime)
}
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["signupStartTime", "signupEndTime"])
}
})
},
applySettingPeople(data) {
if (!data) return
if (data.minGroupPeople !== null && data.minGroupPeople !== undefined && data.minGroupPeople !== "") {
this.$set(this.batchForm, "minGroupPeople", data.minGroupPeople)
}
if (data.maxGroupPeople !== null && data.maxGroupPeople !== undefined && data.maxGroupPeople !== "") {
this.$set(this.batchForm, "maxGroupPeople", data.maxGroupPeople)
}
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["minGroupPeople", "maxGroupPeople"])
}
})
},
loadSettingDefaults(callback) {
this.$axios.post(loc() + "/settingPeople", {
matterId: this.currentMatter.id,
year: this.currentMatter.year
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
callback && callback(data)
@@ -50,7 +50,8 @@ layout("/layouts/platform.html"){
<el-table-column v-if="false" 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="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>
<el-table-column label="所属工会" prop="matterUnionName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="false" label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="携带家属" prop="familyCount" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
@@ -79,7 +80,7 @@ layout("/layouts/platform.html"){
<el-button v-if="canModify(row) || isTravelEnded(row)" size="mini" type="primary" :disabled="isTravelEnded(row)" @click="openSignup(row)">修改</el-button>
<el-button v-if="canRevoke(row)" size="mini" type="danger" @click="revokeSignup(row)">撤回</el-button>
<el-button v-if="showExport(row)" size="mini" type="primary" :disabled="!canExport(row)" @click="exportSignup(row)">报销申请</el-button>
<el-button v-if="showDirectRelativeExport(row)" size="mini" type="primary" :disabled="!canDirectRelativeExport(row)" @click="exportDirectRelative(row)">亲属线路申请</el-button>
<el-button v-if="showDirectRelativeExport(row)" size="mini" type="primary" :disabled="!canDirectRelativeExport(row)" @click="exportDirectRelative(row)">下载亲属线路申请</el-button>
</template>
</el-table-column>
</el-table>
@@ -113,8 +114,8 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item v-if="detailFillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="detailFillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
@@ -249,12 +250,12 @@ layout("/layouts/platform.html"){
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-col v-if="false" :span="8">
<el-form-item label="报名酒店">
<el-input v-model="signupForm.hotelName" :disabled="isDirectFamilyLine(signupForm)" maxlength="100" placeholder="请输入报名酒店"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col v-if="false" :span="8">
<el-form-item label="报名旅行社">
<el-input v-model="signupForm.travelAgencyName" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
</el-form-item>
@@ -469,7 +470,7 @@ layout("/layouts/platform.html"){
<div class="tour-export-date">时间:<span class="tour-export-sign-date">{{ formatDate(exportForm.unionAuditDate) }}</span></div>
</div>
<div class="tour-export-sign school">
<div>学校工会:盖章</div>
<div>学校工会:盖章<span class="tour-export-sign-value">{{ exportForm.schoolUnionAuditorName }}</span></div>
<div class="tour-export-date">时间:<span class="tour-export-sign-date">{{ formatDate(exportForm.schoolUnionAuditDate) }}</span></div>
</div>
</div>
@@ -491,7 +492,7 @@ layout("/layouts/platform.html"){
<div class="tour-export-date">时间:<span class="tour-export-sign-date">{{ formatDate(directRelativeExportForm.unionAuditDate) }}</span></div>
</div>
<div class="tour-export-sign tour-export-direct-sign">
<div>学校工会:盖章</div>
<div>学校工会:盖章<span class="tour-export-sign-value">{{ directRelativeExportForm.schoolUnionAuditorName }}</span></div>
<div class="tour-export-date">时间:<span class="tour-export-sign-date">{{ formatDate(directRelativeExportForm.schoolUnionAuditDate) }}</span></div>
</div>
<div class="tour-export-sign tour-export-direct-sign">
@@ -772,6 +773,7 @@ layout("/layouts/platform.html"){
applyDate: "",
unionAuditorName: "",
unionAuditDate: "",
schoolUnionAuditorName: "",
schoolUnionAuditDate: ""
},
directRelativeExportForm: {
@@ -790,6 +792,7 @@ layout("/layouts/platform.html"){
applyDate: "",
unionAuditorName: "",
unionAuditDate: "",
schoolUnionAuditorName: "",
schoolUnionAuditDate: "",
relativeUnitAuditDate: ""
},
@@ -49,16 +49,16 @@ layout("/layouts/platform.html"){
:data="tableData"
:size="tableSize"
border
:default-sort="{prop: 'enabled', order: 'descending'}"
:default-sort="{prop: 'lineCode', order: 'ascending'}"
:row-class-name="tableRowClassName"
@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="year" width="120" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="线路编号" prop="lineCode" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路名称" prop="lineName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="时间标段" prop="lotName" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路类型" prop="lineType" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="创建人" prop="creatorName" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="排序编号" prop="lineCode" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="是否对外开放" prop="openFlag" width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
@@ -79,9 +79,10 @@ layout("/layouts/platform.html"){
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="260" align="center" header-align="center">
<el-table-column label="操作" width="340" align="center" header-align="center">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button size="mini" type="primary" @click="createMatter(row)">新建事项</el-button>
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
@@ -136,8 +137,8 @@ layout("/layouts/platform.html"){
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="线路编号" prop="lineCode">
<el-input v-model="formData.lineCode" maxlength="50" placeholder="建议年度加序号,如202601"></el-input>
<el-form-item label="排序编号" prop="lineCode">
<el-input-number v-model="formData.lineCode" :controls="false" :min="0" :precision="0" placeholder="请输入排序编号" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -206,6 +207,132 @@ layout("/layouts/platform.html"){
<el-button v-if="!viewMode" type="primary" :loading="submitLoading" @click="doSubmit">保存</el-button>
</span>
</el-dialog>
<el-dialog
title="新建事项"
:visible.sync="matterDialogVisible"
:close-on-click-modal="false"
width="64%">
<el-form :model="matterForm" :rules="matterFormRules" label-width="110px" ref="matterFormRef">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年度" prop="year">
<el-date-picker
v-model="matterForm.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%"
@change="matterYearChange">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="事项状态" prop="enabled">
<el-radio-group v-model="matterForm.enabled">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="事项名称" prop="matterName">
<el-input v-model="matterForm.matterName" maxlength="30" show-word-limit placeholder="请输入事项名称"></el-input>
<div class="tour-matter-name-tip">建议事项名称:{{ matterNameTip() }}</div>
</el-form-item>
<el-form-item label="创建人">
<el-input v-model="matterForm.creatorName" readonly placeholder="当前登录人"></el-input>
</el-form-item>
<el-form-item label="疗休养配置" prop="settingId">
<el-select v-model="matterForm.settingId" clearable filterable placeholder="请选择配置" style="width: 100%">
<el-option v-for="item in matterSettingOptions" :key="item.id" :label="item.configName" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="组织形式" prop="organizationType">
<el-select v-model="matterForm.organizationType" clearable filterable placeholder="请选择组织形式" style="width: 100%" @change="matterOrganizationTypeChange">
<el-option v-for="item in matterOrganizationTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
<el-form-item label="所属工会" prop="unionId">
<el-select v-model="matterForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%" :disabled="isMatterUnionDisabled()">
<el-option v-for="item in matterUnionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="matterDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="matterSubDis" @click="submitMatterStep">下一步</el-button>
</span>
</el-dialog>
<el-dialog
title="选择线路"
:visible.sync="lineConfigDialogVisible"
:close-on-click-modal="false"
width="74%">
<el-form :model="batchForm" :rules="batchRules" label-width="110px" ref="batchFormRef">
<el-form-item label="线路" prop="lineId">
<el-select v-model="batchForm.lineId" disabled filterable placeholder="请选择线路" style="width: 100%">
<el-option v-for="item in matterLineOptions" :key="item.id" :label="matterLineOptionLabel(item)" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="报名开始时间" prop="signupStartTime">
<el-date-picker v-model="batchForm.signupStartTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="选择时间" style="width: 100%"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="报名结束时间" prop="signupEndTime">
<el-date-picker v-model="batchForm.signupEndTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="选择时间" style="width: 100%"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出行开始时间" prop="travelStartTime">
<el-date-picker v-model="batchForm.travelStartTime" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width: 100%" @change="matterTravelStartChange"></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="出行结束时间" prop="travelEndTime">
<el-date-picker v-model="batchForm.travelEndTime" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width: 100%" @change="validateMatterTravelFields"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系人" prop="contactName">
<el-input v-model="batchForm.contactName" maxlength="10" placeholder="请输入联系人"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系方式" prop="contactPhone">
<el-input v-model="batchForm.contactPhone" maxlength="11" placeholder="请输入联系方式"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="预计费用" prop="estimatedCost">
<el-input v-model="batchForm.estimatedCost" maxlength="10" placeholder="请输入预计费用"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="最少成团人数" prop="minGroupPeople">
<el-input-number v-model="batchForm.minGroupPeople" :min="1" :precision="0" controls-position="right" style="width: 100%" @change="matterPeopleChange"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="最多成团人数" prop="maxGroupPeople">
<el-input-number v-model="batchForm.maxGroupPeople" :min="1" :precision="0" controls-position="right" style="width: 100%" @change="matterPeopleChange"></el-input-number>
</el-form-item>
</el-col>
</el-row>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="backToMatterStep">上一步</el-button>
<el-button type="primary" :loading="batchSubDis" @click="submitMatterLineConfig">保存</el-button>
</span>
</el-dialog>
</div>
<style>
@@ -213,6 +340,13 @@ layout("/layouts/platform.html"){
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
.tour-matter-name-tip {
margin-top: 6px;
color: #f56c6c;
font-size: 13px;
line-height: 20px;
user-select: text;
}
</style>
<script nonce="${cspNonce!}">
@@ -222,25 +356,93 @@ layout("/layouts/platform.html"){
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
const validateMatterUnion = (rule, value, callback) => {
if (this.matterForm.organizationType === "schoolUnion" || value) {
callback()
} else {
callback(new Error("必填"))
}
}
const validateMatterMobile = (rule, value, callback) => {
const mobileReg = /^1[3-9]\d{9}$/
if (!value) {
callback(new Error("必填"))
} else if (!mobileReg.test(value)) {
callback(new Error("手机号码格式不正确"))
} else {
callback()
}
}
const validateMatterMoney = (rule, value, callback) => {
const moneyReg = /^(0|[1-9]\d*)(\.\d{1,2})?$/
if (value === "" || value === null || value === undefined) {
callback(new Error("必填"))
} else if (!moneyReg.test(String(value))) {
callback(new Error("请输入非负金额,最多两位小数"))
} else {
callback()
}
}
const validateMatterPeople = (rule, value, callback) => {
if (!value || value <= 0) {
callback(new Error("人数必须大于0"))
} else if (this.batchForm.minGroupPeople && this.batchForm.maxGroupPeople
&& this.batchForm.minGroupPeople > this.batchForm.maxGroupPeople) {
callback(new Error("最少人数不能大于最多人数"))
} else {
callback()
}
}
const validateMatterTime = (rule, value, callback) => {
if (!value) {
callback(new Error("必填"))
return
}
const form = this.batchForm
if (form.signupStartTime && form.signupEndTime && form.signupStartTime >= form.signupEndTime) {
callback(new Error("报名开始时间必须小于报名结束时间"))
return
}
if (form.travelStartTime && form.travelEndTime && form.travelStartTime > form.travelEndTime) {
callback(new Error("出行开始时间不能晚于出行结束时间"))
return
}
if (form.signupEndTime && form.travelStartTime && form.signupEndTime >= form.travelStartTime) {
callback(new Error("报名结束时间必须小于出行开始时间"))
return
}
callback()
}
return {
title: "",
dialogVisible: false,
viewMode: false,
submitLoading: false,
matterDialogVisible: false,
matterSubDis: false,
lineConfigDialogVisible: false,
batchSubDis: false,
selectedMatterLine: {},
travelAgencyOptions: [],
lotOptions: [],
lineTypeOptions: [],
matterSettingOptions: [],
matterUnionOptions: [],
matterOrganizationTypeOptions: [],
matterLineOptions: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "enabled",
pageOrderBy: "descending",
pageOrderName: "lineCode",
pageOrderBy: "ascending",
year: currentYear,
lineName: "",
lotId: ""
},
formData: {},
matterForm: {},
batchForm: {},
formRules: {
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
travelAgencyId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
@@ -249,6 +451,25 @@ layout("/layouts/platform.html"){
lineType: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lotId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
mobileThumb: [{ required: true, message: "请上传移动端缩略图", trigger: ["blur", "change"] }]
},
matterFormRules: {
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
matterName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
settingId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
unionId: [{ validator: validateMatterUnion, trigger: ["blur", "change"] }],
organizationType: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
},
batchRules: {
lineId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
signupStartTime: [{ validator: validateMatterTime, trigger: ["blur", "change"] }],
signupEndTime: [{ validator: validateMatterTime, trigger: ["blur", "change"] }],
travelStartTime: [{ validator: validateMatterTime, trigger: ["blur", "change"] }],
travelEndTime: [{ validator: validateMatterTime, trigger: ["blur", "change"] }],
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactPhone: [{ validator: validateMatterMobile, trigger: ["blur", "change"] }],
minGroupPeople: [{ validator: validateMatterPeople, trigger: ["blur", "change"] }],
maxGroupPeople: [{ validator: validateMatterPeople, trigger: ["blur", "change"] }],
estimatedCost: [{ validator: validateMatterMoney, trigger: ["blur", "change"] }]
}
}
},
@@ -307,6 +528,291 @@ layout("/layouts/platform.html"){
this.viewMode = true
this.loadDetail(row.id)
},
createMatter(row) {
if (!row || !row.id) {
this.$message.warning("Line info is incomplete")
return
}
this.selectedMatterLine = row
this.matterForm = this.emptyMatterForm()
this.batchForm = {}
this.matterLineOptions = []
this.loadMatterUnions()
this.loadMatterOrganizationTypes()
this.$axios.post(this.matterBasePath() + "/lineInfo", { lineId: row.id }).then((res) => {
if (res.code === 0) {
this.selectedMatterLine = Object.assign({}, row, res.data || {})
if (this.selectedMatterLine.year) {
this.matterForm.year = String(this.selectedMatterLine.year)
}
this.loadMatterSettings(this.matterForm.year, true)
} else {
this.$message.warning(res.msg || "线路信息查询失败")
this.loadMatterSettings(this.matterForm.year, true)
}
this.matterDialogVisible = true
this.$nextTick(() => this.$refs.matterFormRef && this.$refs.matterFormRef.clearValidate())
})
},
matterBasePath() {
return "/platform/tour/matter"
},
emptyMatterForm() {
const user = (this.$store && this.$store.state && this.$store.state.user) || {}
return {
year: moment().format("YYYY"),
matterName: "",
settingId: "",
creatorUserId: user.id || "",
creatorName: user.username || "",
unionId: "",
organizationType: "",
enabled: true
}
},
matterNameTip() {
const lineName = this.selectedMatterLine && this.selectedMatterLine.lineName ? this.selectedMatterLine.lineName : "线路名称"
return lineName + "第一批次"
},
emptyMatterBatchForm(matterId) {
return {
id: matterId || "",
lineId: this.selectedMatterLine.id || "",
signupStartTime: "",
signupEndTime: "",
travelStartTime: "",
travelEndTime: "",
contactName: "",
contactPhone: "",
minGroupPeople: null,
maxGroupPeople: null,
estimatedCost: ""
}
},
matterYearChange(year) {
this.matterForm.settingId = ""
this.loadMatterSettings(year, true)
},
matterOrganizationTypeChange() {
this.applyMatterOrganizationTypeRule()
this.$nextTick(() => this.$refs.matterFormRef && this.$refs.matterFormRef.validateField("unionId"))
},
applyMatterOrganizationTypeRule() {
if (this.matterForm.organizationType === "schoolUnion") {
this.matterForm.unionId = ""
return
}
if (this.matterForm.organizationType === "branchUnion" || this.matterForm.organizationType === "personal") {
const union = this.currentUserUnion()
this.matterForm.unionId = union.id || ""
this.ensureMatterUnionOption(union)
}
},
currentUserUnion() {
const user = (this.$store && this.$store.state && this.$store.state.user) || {}
return user.union || {}
},
ensureMatterUnionOption(union) {
if (!union || !union.id) return
const exists = this.matterUnionOptions.some(item => item.id === union.id)
if (!exists) {
this.matterUnionOptions.push({ id: union.id, name: union.name || "" })
}
},
isMatterUnionDisabled() {
return ["schoolUnion", "branchUnion", "personal"].includes(this.matterForm.organizationType)
},
loadMatterSettings(year, autoSelectLatest) {
this.$axios.post(this.matterBasePath() + "/settingOptions", { year }).then((res) => {
if (res.code === 0) {
this.matterSettingOptions = res.data || []
if (autoSelectLatest && !this.matterForm.settingId && this.matterSettingOptions.length > 0) {
this.matterForm.settingId = this.matterSettingOptions[0].id
}
}
})
},
loadMatterUnions() {
this.$axios.post(this.matterBasePath() + "/unionOptions").then((res) => {
if (res.code === 0) {
this.matterUnionOptions = res.data || []
}
})
},
loadMatterOrganizationTypes() {
this.$axios.post(this.matterBasePath() + "/organizationTypeOptions").then((res) => {
if (res.code === 0) {
this.matterOrganizationTypeOptions = res.data || []
}
})
},
submitMatterStep() {
this.$refs.matterFormRef.validate((valid) => {
if (!valid) return
this.matterSubDis = true
this.$axios.post(this.matterBasePath() + "/doSubmit", this.matterForm).then((res) => {
this.matterSubDis = false
if (res.code === 0) {
const data = res.data || {}
const matterId = data.id || this.matterForm.id || ""
this.matterForm.id = matterId
this.matterDialogVisible = false
this.openMatterLineConfig(matterId)
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.matterSubDis = false
})
})
},
openMatterLineConfig(matterId) {
this.batchForm = this.emptyMatterBatchForm(matterId)
this.matterLineOptions = [this.selectedMatterLine]
this.$axios.post(this.matterBasePath() + "/lineConfig", { matterId }).then((res) => {
if (res.code === 0) {
this.batchForm = Object.assign(this.emptyMatterBatchForm(matterId), res.data || {})
this.batchForm.id = matterId
this.batchForm.lineId = this.selectedMatterLine.id || this.batchForm.lineId
this.batchForm.travelStartTime = this.dateOnly(this.batchForm.travelStartTime)
this.batchForm.travelEndTime = this.dateOnly(this.batchForm.travelEndTime)
this.applyMatterSelectedLineDefaults(false)
this.loadMatterSettingDefaults((data) => {
this.applyMatterSettingPeople(data)
this.applyMatterSettingSignupTime(data)
})
this.lineConfigDialogVisible = true
this.$nextTick(() => {
this.applyMatterSelectedLineDefaults(true)
this.$refs.batchFormRef && this.$refs.batchFormRef.clearValidate()
})
} else {
this.$message.warning(res.msg || "事项信息查询失败")
}
})
},
backToMatterStep() {
this.lineConfigDialogVisible = false
this.matterDialogVisible = true
this.$nextTick(() => this.$refs.matterFormRef && this.$refs.matterFormRef.clearValidate())
},
matterLineOptionLabel(item) {
if (!item) return ""
const lineName = this.matterLineField(item, "lineName") || ""
const lineType = this.matterLineField(item, "lineType") || ""
return lineType ? lineName + " (" + lineType + ")" : lineName
},
matterSelectedLine() {
return (this.matterLineOptions || []).find((item) => this.matterLineField(item, "id") === this.batchForm.lineId) || null
},
applyMatterSelectedLineDefaults(recalculateEnd) {
const line = this.matterSelectedLine()
if (!line) return
const activityCost = this.matterLineField(line, "activityCost")
if (activityCost !== null && activityCost !== undefined && activityCost !== "") {
this.batchForm.estimatedCost = activityCost
}
if (recalculateEnd) {
this.fillMatterTravelEndByLot()
}
},
matterTravelStartChange() {
this.fillMatterTravelEndByLot()
},
fillMatterTravelEndByLot() {
const line = this.matterSelectedLine()
const days = this.matterLineLotDays(line)
if (!this.batchForm.travelStartTime || !days) return
this.$set(this.batchForm, "travelEndTime", moment(this.batchForm.travelStartTime).add(days - 1, "days").format("YYYY-MM-DD"))
this.validateMatterTravelFields()
},
matterLineLotDays(line) {
if (!line) return null
const lotDays = this.matterLineField(line, "lotDays") || this.matterLineField(line, "lotValue")
if (!/^\d+$/.test(String(lotDays || ""))) return null
const days = parseInt(lotDays, 10)
return days > 0 ? days : null
},
matterLineField(line, field) {
if (!line) return null
if (line[field] !== undefined) return line[field]
const lowerField = field.toLowerCase()
const matchedKey = Object.keys(line).find((key) => key.toLowerCase() === lowerField)
return matchedKey ? line[matchedKey] : null
},
validateMatterTravelFields() {
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["travelStartTime", "travelEndTime"])
}
})
},
dateOnly(value) {
if (!value) return ""
return String(value).substring(0, 10)
},
applyMatterSettingSignupTime(data) {
if (!data) return
if (data.signupStartTime) {
this.$set(this.batchForm, "signupStartTime", data.signupStartTime)
}
if (data.signupEndTime) {
this.$set(this.batchForm, "signupEndTime", data.signupEndTime)
}
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["signupStartTime", "signupEndTime"])
}
})
},
applyMatterSettingPeople(data) {
if (!data) return
if (data.minGroupPeople !== null && data.minGroupPeople !== undefined && data.minGroupPeople !== "") {
this.$set(this.batchForm, "minGroupPeople", data.minGroupPeople)
}
if (data.maxGroupPeople !== null && data.maxGroupPeople !== undefined && data.maxGroupPeople !== "") {
this.$set(this.batchForm, "maxGroupPeople", data.maxGroupPeople)
}
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["minGroupPeople", "maxGroupPeople"])
}
})
},
loadMatterSettingDefaults(callback) {
this.$axios.post(this.matterBasePath() + "/settingPeople", {
matterId: this.matterForm.id,
year: this.matterForm.year
}).then((res) => {
if (res.code === 0) {
callback && callback(res.data || {})
}
})
},
matterPeopleChange() {
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["minGroupPeople", "maxGroupPeople"])
}
})
},
submitMatterLineConfig() {
this.$refs.batchFormRef.validate((valid) => {
if (!valid) return
this.batchSubDis = true
this.$axios.post(this.matterBasePath() + "/lineConfigDoSubmit", this.batchForm).then((res) => {
this.batchSubDis = false
if (res.code === 0) {
this.$message.success("保存成功")
this.lineConfigDialogVisible = false
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.batchSubDis = false
})
})
},
loadDetail(id) {
this.$axios.post(loc() + "/detail", { id }).then((res) => {
if (res.code === 0) {
@@ -190,8 +190,8 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时段">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">
{{ familyText() }}
</el-descriptions-item>
@@ -107,7 +107,7 @@ layout("/layouts/platform.html"){
<el-input v-model="formData.configName" maxlength="100" placeholder="请输入配置名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col v-if="false" :span="12">
<el-form-item label="疗休养类型" prop="tourType">
<el-select v-model="formData.tourType" clearable allow-create filterable placeholder="请选择或输入类型" style="width: 100%">
<el-option label="省内疗休养" value="省内疗休养"></el-option>
@@ -145,6 +145,28 @@ 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="signupStartTime">
<el-date-picker
v-model="formData.signupStartTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择开始报名时间"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束报名时间" prop="signupEndTime">
<el-date-picker
v-model="formData.signupEndTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择结束报名时间"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="tour-setting-basic-divider"></div>
</el-col>
@@ -155,7 +177,7 @@ layout("/layouts/platform.html"){
</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-input-number v-model="formData.outProvinceRatio" :controls="false" :min="0" :max="100" :precision="2" disabled placeholder="请输入省外人数占比" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -300,6 +322,18 @@ layout("/layouts/platform.html"){
callback()
}
}
const checkSignupTime = (rule, value, callback) => {
if (!value) {
callback(new Error("必填"))
return
}
if (this.formData.signupStartTime && this.formData.signupEndTime
&& this.formData.signupStartTime >= this.formData.signupEndTime) {
callback(new Error("开始报名时间必须小于结束报名时间"))
} else {
callback()
}
}
const checkCycleYear = (rule, value, callback) => {
const startYear = parseInt(this.formData.cycleStartYear, 10)
const endYear = parseInt(this.formData.cycleEndYear, 10)
@@ -337,6 +371,8 @@ layout("/layouts/platform.html"){
configName: [{required: true, message: "必填", trigger: ["blur", "change"]}],
minGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
maxGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
signupStartTime: [{validator: checkSignupTime, trigger: ["blur", "change"]}],
signupEndTime: [{validator: checkSignupTime, trigger: ["blur", "change"]}],
outProvinceRatioType: [{required: true, message: "必填", trigger: ["blur", "change"]}],
outProvinceFixedPeople: [{validator: checkOutProvinceFixedPeople, trigger: ["blur", "change"]}],
cycleStartYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}],
@@ -382,6 +418,8 @@ layout("/layouts/platform.html"){
sortNo: 0,
minGroupPeople: 0,
maxGroupPeople: 0,
signupStartTime: "",
signupEndTime: "",
outProvinceYears: 0,
outProvinceRatio: 0,
outProvinceRatioType: "当年报名人数",
@@ -466,6 +504,8 @@ layout("/layouts/platform.html"){
activityGroupId: "",
outProvinceRatioType: "当年报名人数",
outProvinceFixedPeople: 0,
signupStartTime: "",
signupEndTime: "",
cycleStartYear: "",
cycleEndYear: "",
cycleTotalCost: null,
@@ -54,11 +54,13 @@ layout("/layouts/platform.html"){
@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="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" width="330" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" width="240" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<!-- 临时屏蔽事项名称列,保留代码便于后续恢复
<el-table-column label="事项名称" prop="matterName" min-width="200" 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="lineType" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="最少成团人数" prop="minGroupPeople" width="140" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
@@ -75,10 +77,11 @@ layout("/layouts/platform.html"){
<el-tag size="mini" :type="Number(row.signupCount || 0) > 0 ? 'success' : 'info'">{{ row.signupCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="260" align="center" header-align="center">
<el-table-column label="操作" width="300" align="center" header-align="center">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openLineView(row)">查看线路</el-button>
<el-button size="mini" :type="signupActionType(row)" :disabled="signupActionDisabled(row)" @click="handleSignupAction(row)">{{ signupActionText(row) }}</el-button>
<el-button size="mini" type="primary" @click="openLineView(row)">查看线路</el-button>
<el-button size="mini" type="primary" @click="openPeopleView(row)">查看人员</el-button>
</template>
</el-table-column>
</el-table>
@@ -148,16 +151,22 @@ layout("/layouts/platform.html"){
</el-col>
</el-row>
<el-row :gutter="20">
<!-- 临时屏蔽报名酒店字段,保留代码便于后续恢复
<el-col :span="8">
<el-form-item label="报名酒店">
<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-input v-model="signupForm.travelAgencyName" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
</el-form-item>
</el-col>
-->
<el-col :span="8">
<el-form-item label="出行时间">
<el-input v-model="signupForm.travelPeriod" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
@@ -369,11 +378,72 @@ layout("/layouts/platform.html"){
<div class="tour-signup-section">
<div class="tour-signup-title">线路内容</div>
<div ref="lineContent" class="tour-line-content" v-html="lineContentHtml" @click="handleLineContentClick"></div>
<div ref="lineContent" class="tour-line-content" v-html="lineContentHtml"></div>
</div>
</div>
</el-drawer>
<el-drawer
:title="peopleViewTitle"
:visible.sync="peopleViewVisible"
direction="rtl"
size="72%"
custom-class="tour-group-view-drawer"
:close-on-click-modal="false">
<div class="tour-group-view">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="姓名/工号">
<el-input
v-model="peopleViewForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="peopleViewSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="peopleViewForm.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>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="peopleViewSearch">搜索</el-button>
<el-button size="medium" @click="peopleViewReset">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<div class="tour-group-view-title">报名人员列表</div>
<el-table
v-loading="peopleViewLoading"
:data="peopleViewTableData"
:size="tableSize"
border
@sort-change="peopleViewPageOrder">
<el-table-column label="序号" type="index" :index="peopleViewIndexMethod" 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="unionName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
<el-row class="el-pagination-container tour-group-view-pagination">
<el-pagination
background
:current-page="peopleViewForm.pageNumber"
:page-size="peopleViewForm.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="peopleViewForm.totalCount"
layout="total, sizes, prev, pager, next, jumper"
@size-change="peopleViewSizeChange"
@current-change="peopleViewCurrentChange">
</el-pagination>
</el-row>
</el-card>
</div>
</el-drawer>
</div>
<style>
@@ -416,21 +486,76 @@ layout("/layouts/platform.html"){
.tour-line-content .tour-file-preview {
border: 1px solid #dcdfe6;
display: block;
min-height: 520px;
height: 620px;
width: 100%;
}
.tour-line-content .tour-pdf-preview {
.tour-line-content .tour-file-preview-box {
margin-top: 10px;
}
.tour-line-content .tour-file-link {
color: #0079c2;
text-decoration: underline;
.tour-line-content .tour-pdf-lazy-preview {
border: 1px solid #dcdfe6;
background: #f5f7fa;
min-height: 180px;
margin-top: 10px;
overflow: hidden;
}
.tour-line-content .tour-file-loading {
.tour-line-content .tour-pdf-lazy-toolbar {
background: #fff;
border-bottom: 1px solid #ebeef5;
color: #606266;
font-size: 13px;
line-height: 36px;
padding: 0 12px;
}
.tour-line-content .tour-pdf-page-placeholder {
align-items: center;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
color: #909399;
display: flex;
justify-content: center;
min-height: 220px;
}
.tour-line-content .tour-pdf-page-placeholder canvas {
background: #fff;
display: block;
margin: 0 auto;
max-width: 100%;
}
.tour-line-content .tour-image-preview {
border: 1px solid #dcdfe6;
display: block;
margin-top: 10px;
max-height: 680px;
object-fit: contain;
width: 100%;
}
.tour-line-content .tour-file-unpreviewable {
align-items: center;
border: 1px solid #dcdfe6;
color: #909399;
display: flex;
justify-content: center;
min-height: 160px;
margin-top: 10px;
}
.tour-group-view-drawer .el-drawer__body {
background: #f5f7fa;
padding: 12px;
overflow: auto;
}
.tour-group-view-pagination {
margin-top: 12px;
text-align: right;
}
.tour-group-view-title {
border-left: 4px solid #0079c2;
color: #0079c2;
font-size: 14px;
font-weight: 600;
line-height: 16px;
margin-bottom: 12px;
padding-left: 10px;
}
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
@@ -461,6 +586,9 @@ layout("/layouts/platform.html"){
}
</style>
<script src="${base!}/assets/platform/plugins/pdfJs/h5/pdf.js" type="text/javascript" charset="utf-8"></script>
<script src="${base!}/assets/platform/plugins/pdfJs/h5/pdf.worker.js" type="text/javascript" charset="utf-8"></script>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
@@ -484,6 +612,14 @@ layout("/layouts/platform.html"){
lineVisible: false,
lineDetail: {},
lineContentHtml: "",
linePdfPreviews: [],
linePdfObserver: null,
peopleViewVisible: false,
peopleViewLoading: false,
peopleViewRequestSeq: 0,
peopleViewTitle: "",
peopleViewRow: {},
peopleViewTableData: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
@@ -494,6 +630,15 @@ layout("/layouts/platform.html"){
lineName: "",
lineType: "",
unionId: ""
},
peopleViewForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
keyword: "",
unionId: ""
}
}
},
@@ -535,6 +680,10 @@ layout("/layouts/platform.html"){
this.revokeSignup(row)
return
}
if (row && row.signed) {
this.loadSignupDetail(row)
return
}
this.openSignup(row)
},
isApprovalSignup(row) {
@@ -570,6 +719,79 @@ layout("/layouts/platform.html"){
|| row.directFamilyUnitLine === 1
|| row.directFamilyUnitLine === "1"
},
openPeopleView(row) {
this.peopleViewRow = row || {}
this.peopleViewTitle = (this.peopleViewRow.lineName || "报名人员") + " - 报名人员"
this.peopleViewVisible = true
this.peopleViewForm.pageNumber = 1
this.peopleViewForm.pageSize = 10
this.peopleViewForm.totalCount = 0
this.peopleViewForm.pageOrderName = "signupTime"
this.peopleViewForm.pageOrderBy = "descending"
this.peopleViewForm.keyword = ""
this.peopleViewForm.unionId = ""
this.peopleViewTableData = []
this.loadPeopleViewData()
},
peopleViewSearch() {
this.peopleViewForm.pageNumber = 1
this.loadPeopleViewData()
},
peopleViewReset() {
this.peopleViewForm.keyword = ""
this.peopleViewForm.unionId = ""
this.peopleViewForm.pageNumber = 1
this.loadPeopleViewData()
},
peopleViewIndexMethod(index) {
return (this.peopleViewForm.pageNumber - 1) * this.peopleViewForm.pageSize + index + 1
},
peopleViewSizeChange(size) {
this.peopleViewForm.pageSize = size
this.peopleViewForm.pageNumber = 1
this.loadPeopleViewData()
},
peopleViewCurrentChange(page) {
this.peopleViewForm.pageNumber = page
this.loadPeopleViewData()
},
peopleViewPageOrder({ prop, order }) {
this.peopleViewForm.pageOrderName = prop || "signupTime"
this.peopleViewForm.pageOrderBy = order || "descending"
this.peopleViewForm.pageNumber = 1
this.loadPeopleViewData()
},
loadPeopleViewData() {
if (!this.peopleViewRow || !this.peopleViewRow.matterId) {
this.peopleViewTableData = []
this.peopleViewForm.totalCount = 0
return
}
this.peopleViewLoading = true
const requestSeq = ++this.peopleViewRequestSeq
this.$axios.post(loc() + "/signupPageData", {
matterId: this.peopleViewRow.matterId,
keyword: this.peopleViewForm.keyword,
unionId: this.peopleViewForm.unionId,
pageNumber: this.peopleViewForm.pageNumber,
pageSize: this.peopleViewForm.pageSize,
pageOrderName: this.peopleViewForm.pageOrderName,
pageOrderBy: this.peopleViewForm.pageOrderBy
}).then((res) => {
if (requestSeq !== this.peopleViewRequestSeq) return
if (res.code === 0) {
const data = res.data || {}
this.peopleViewTableData = data.list || []
this.peopleViewForm.totalCount = data.totalCount || 0
} else {
this.$message.warning(res.msg || "查询失败")
}
}).finally(() => {
if (requestSeq === this.peopleViewRequestSeq) {
this.peopleViewLoading = false
}
})
},
canApplyOverReimbursement(row) {
if (!row) return false
return row.allowOverReimbursement === true
@@ -580,15 +802,51 @@ layout("/layouts/platform.html"){
this.$axios.post(loc() + "/lineDetail", { lineId: row.lineId }).then((res) => {
if (res.code === 0) {
this.lineDetail = res.data || {}
this.lineContentHtml = this.buildLineContentHtml(this.lineDetail.lineContent)
this.lineContentHtml = ""
this.lineVisible = true
this.$nextTick(() => this.loadPdfPreviews())
this.buildLineContentHtmlAsync(this.lineDetail.lineContent).then((html) => {
this.lineContentHtml = html
this.$nextTick(() => {
this.initLinePdfPreviews()
})
})
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
buildLineContentHtml(content) {
buildLineContentHtmlAsync(content) {
const ids = this.extractLineFileIds(content)
if (!ids.length) {
return Promise.resolve(this.buildLineContentHtml(content, {}))
}
return this.$axios.post("/platform/sys/file/previewFileData", {ids: JSON.stringify(ids)}).then((res) => {
const fileMap = {}
if (res.code === 0 && Array.isArray(res.data)) {
res.data.forEach((file) => {
if (file && file.id) {
file.suffix = (file.suffix || "").toLowerCase()
fileMap[file.id] = file
}
})
}
return this.buildLineContentHtml(content, fileMap)
}).catch(() => this.buildLineContentHtml(content, {}))
},
extractLineFileIds(content) {
if (!content) return []
const wrap = document.createElement("div")
wrap.innerHTML = content
const ids = []
Array.prototype.slice.call(wrap.querySelectorAll("a[href]")).forEach((anchor) => {
const id = this.getFileIdFromUrl(anchor.getAttribute("href") || "")
if (id && !ids.includes(id)) {
ids.push(id)
}
})
return ids
},
buildLineContentHtml(content, fileMap) {
if (!content) return "暂无线路内容"
const wrap = document.createElement("div")
wrap.innerHTML = content
@@ -596,63 +854,226 @@ layout("/layouts/platform.html"){
anchors.forEach((anchor) => {
const href = anchor.getAttribute("href") || ""
const text = anchor.textContent || href
const cleanHref = href.split("#")[0].split("?")[0]
const lowerHref = cleanHref.toLowerCase()
const lowerText = text.toLowerCase()
anchor.removeAttribute("download")
anchor.setAttribute("target", "_blank")
anchor.setAttribute("rel", "noopener noreferrer")
anchor.classList.add("tour-file-link")
if (/\.pdf$/.test(lowerHref) || /\.pdf$/.test(lowerText) || lowerText.indexOf(".pdf") > -1) {
const box = document.createElement("div")
box.className = "tour-pdf-preview"
box.setAttribute("data-pdf-url", encodeURIComponent(href))
box.innerHTML = '<div class="tour-file-preview tour-file-loading">PDF加载中...</div>'
anchor.parentNode.insertBefore(box, anchor.nextSibling)
} else if (/\.(jpg|jpeg|png|gif|bmp|webp)$/.test(lowerHref)) {
const img = document.createElement("img")
img.src = href
img.alt = text
img.style.display = "block"
img.style.marginTop = "10px"
anchor.parentNode.insertBefore(img, anchor.nextSibling)
const fileId = this.getFileIdFromUrl(href)
const file = fileId && fileMap ? fileMap[fileId] : null
const fileType = this.detectPreviewFileType(href, text, file)
if (!fileType) {
return
}
const preview = this.createLineFilePreview(href, fileType, file)
if (anchor.parentNode && preview) {
anchor.parentNode.replaceChild(preview, anchor)
}
})
return wrap.innerHTML
},
isPdfLink(anchor) {
if (!anchor) return false
const href = anchor.getAttribute("href") || ""
const text = anchor.textContent || href
const lowerHref = href.split("#")[0].split("?")[0].toLowerCase()
const lowerText = text.toLowerCase()
return /\.pdf$/.test(lowerHref) || /\.pdf$/.test(lowerText) || lowerText.indexOf(".pdf") > -1
detectPreviewFileType(href, text, file) {
const suffix = file && file.suffix ? file.suffix.toLowerCase() : ""
if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(suffix)) {
return "image"
}
if (suffix === "pdf") {
return "pdf"
}
if (["doc", "docx", "xls", "xlsx"].includes(suffix)) {
return "office"
}
const lowerHref = (href || "").split("#")[0].split("?")[0].toLowerCase()
const lowerText = (text || "").toLowerCase()
const value = lowerHref + " " + lowerText
if (/\.(jpg|jpeg|png|gif|bmp|webp)(\s|$)/.test(value)) {
return "image"
}
if (/\.pdf(\s|$)/.test(value)) {
return "pdf"
}
if (/\.(doc|docx|xls|xlsx)(\s|$)/.test(value)) {
return "office"
}
return ""
},
previewPdf(url, callback) {
fetch(url, { credentials: "include" }).then((res) => {
if (!res.ok) throw new Error("PDF加载失败")
return res.blob()
}).then((blob) => {
const previewUrl = URL.createObjectURL(new Blob([blob], { type: "application/pdf" }))
callback(previewUrl)
}).catch(() => {
callback(url)
})
createLineFilePreview(href, fileType, file) {
if (fileType === "image") {
const img = document.createElement("img")
img.className = "tour-image-preview"
img.src = file && file.downloadPath ? file.downloadPath : href
img.alt = ""
return img
}
const previewUrl = this.buildLinePreviewUrl(href, fileType, file)
if (!previewUrl) {
const tip = document.createElement("div")
tip.className = "tour-file-unpreviewable"
tip.textContent = "当前文件暂不支持预览"
return tip
}
const box = document.createElement("div")
box.className = "tour-file-preview-box"
const preview = document.createElement("div")
preview.className = "tour-pdf-lazy-preview"
preview.setAttribute("data-pdf-url", previewUrl)
preview.innerHTML = "<div class=\"tour-pdf-lazy-toolbar\">PDF预览加载中...</div>"
box.appendChild(preview)
return box
},
loadPdfPreviews() {
buildLinePreviewUrl(href, fileType, file) {
if (fileType === "office") {
const id = file && file.id ? file.id : this.getFileIdFromUrl(href)
return id ? "/platform/sys/file/convertPDF?id=" + id : ""
}
if (fileType === "pdf") {
return file && file.downloadPath ? file.downloadPath : href
}
return ""
},
getPdfLib() {
return window.pdfjsLib || window["pdfjs-dist/build/pdf"]
},
initLinePdfPreviews() {
this.cleanupLinePdfPreviews()
const root = this.$refs.lineContent
if (!root) return
const boxes = Array.prototype.slice.call(root.querySelectorAll(".tour-pdf-preview[data-pdf-url]"))
boxes.forEach((box) => {
const url = decodeURIComponent(box.getAttribute("data-pdf-url") || "")
if (!url) return
this.previewPdf(url, (previewUrl) => {
box.innerHTML = '<object class="tour-file-preview" data="' + previewUrl + '#toolbar=1&navpanes=0" type="application/pdf">'
+ '<embed class="tour-file-preview" src="' + previewUrl + '#toolbar=1&navpanes=0" type="application/pdf" />'
+ '</object>'
const previews = root ? Array.prototype.slice.call(root.querySelectorAll(".tour-pdf-lazy-preview")) : []
if (!previews.length) {
return
}
const pdfLib = this.getPdfLib()
if (!pdfLib || !pdfLib.getDocument) {
previews.forEach((el) => {
el.innerHTML = "<div class=\"tour-file-unpreviewable\">PDF预览组件加载失败</div>"
})
return
}
if (pdfLib.GlobalWorkerOptions) {
pdfLib.GlobalWorkerOptions.workerSrc = "/assets/platform/plugins/pdfJs/h5/pdf.worker.js"
}
if (window.IntersectionObserver) {
this.linePdfObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return
}
this.renderLazyPdfPage(entry.target)
this.linePdfObserver && this.linePdfObserver.unobserve(entry.target)
})
}, {
root: null,
rootMargin: "420px 0px",
threshold: 0.01
})
}
previews.forEach((el) => {
this.initLazyPdfPreview(el, pdfLib)
})
},
initLazyPdfPreview(el, pdfLib) {
const url = el.getAttribute("data-pdf-url")
if (!url) {
el.innerHTML = "<div class=\"tour-file-unpreviewable\">当前文件暂不支持预览</div>"
return
}
const task = pdfLib.getDocument({
url: url,
disableAutoFetch: true,
disableStream: true,
disableRange: false
})
const state = {
el: el,
doc: null,
task: task,
rendered: {},
rendering: {}
}
this.linePdfPreviews.push(state)
task.promise.then((pdf) => {
state.doc = pdf
const count = pdf.numPages || 0
const toolbar = el.querySelector(".tour-pdf-lazy-toolbar")
if (toolbar) {
toolbar.textContent = "PDF预览:共 " + count + " 页,已优先加载第 1 页"
}
for (let i = 1; i <= count; i++) {
const page = document.createElement("div")
page.className = "tour-pdf-page-placeholder"
page.setAttribute("data-page-number", i)
page.textContent = i === 1 ? "正在加载第 1 页..." : "第 " + i + " 页,滚动到此处加载"
el.appendChild(page)
}
const firstPage = el.querySelector("[data-page-number='1']")
if (firstPage) {
this.renderLazyPdfPage(firstPage)
}
Array.prototype.slice.call(el.querySelectorAll(".tour-pdf-page-placeholder")).forEach((page) => {
if (page.getAttribute("data-page-number") !== "1" && this.linePdfObserver) {
this.linePdfObserver.observe(page)
}
})
}).catch(() => {
el.innerHTML = "<div class=\"tour-file-unpreviewable\">PDF加载失败,请稍后重试</div>"
})
},
renderLazyPdfPage(pageEl) {
const previewEl = pageEl.closest(".tour-pdf-lazy-preview")
const state = this.linePdfPreviews.find((item) => item.el === previewEl)
if (!state || !state.doc) {
return
}
const pageNumber = Number(pageEl.getAttribute("data-page-number") || 0)
if (!pageNumber || state.rendered[pageNumber] || state.rendering[pageNumber]) {
return
}
state.rendering[pageNumber] = true
pageEl.textContent = "正在加载第 " + pageNumber + " 页..."
state.doc.getPage(pageNumber).then((page) => {
const containerWidth = Math.max(previewEl.clientWidth - 24, 420)
const baseViewport = page.getViewport({scale: 1})
const cssScale = containerWidth / baseViewport.width
const outputScale = Math.min(window.devicePixelRatio || 1, 2)
const viewport = page.getViewport({scale: cssScale})
const canvas = document.createElement("canvas")
const context = canvas.getContext("2d")
canvas.width = Math.floor(viewport.width * outputScale)
canvas.height = Math.floor(viewport.height * outputScale)
canvas.style.width = Math.floor(viewport.width) + "px"
canvas.style.height = Math.floor(viewport.height) + "px"
pageEl.innerHTML = ""
pageEl.appendChild(canvas)
return page.render({
canvasContext: context,
viewport: viewport,
transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null
}).promise.then(() => {
state.rendered[pageNumber] = true
state.rendering[pageNumber] = false
})
}).catch(() => {
state.rendering[pageNumber] = false
pageEl.textContent = "第 " + pageNumber + " 页加载失败"
})
},
cleanupLinePdfPreviews() {
if (this.linePdfObserver) {
this.linePdfObserver.disconnect()
this.linePdfObserver = null
}
this.linePdfPreviews.forEach((item) => {
if (item.task && item.task.destroy) {
try {
item.task.destroy()
} catch (e) {
}
}
})
this.linePdfPreviews = []
},
getFileIdFromUrl(href) {
if (!href) return ""
const idMatch = href.match(/[?&]id=([^&#]+)/)
if (idMatch) {
return decodeURIComponent(idMatch[1])
}
return ""
},
revokeSignup(row) {
if (!this.canRevokeSignup(row)) {
this.$message.warning("当前报名申请暂不支持撤回")
@@ -676,16 +1097,6 @@ layout("/layouts/platform.html"){
})
}).catch(() => {})
},
handleLineContentClick(event) {
const anchor = event.target.closest && event.target.closest("a[href]")
if (!anchor || !this.$refs.lineContent || !this.$refs.lineContent.contains(anchor)) return
if (!this.isPdfLink(anchor)) return
event.preventDefault()
const href = anchor.getAttribute("href")
this.previewPdf(href, (previewUrl) => {
window.open(previewUrl, "_blank", "noopener,noreferrer")
})
},
openSignup(row) {
this.$axios.post(loc() + "/signupEligibilityNotice", { matterId: row.matterId }).then((res) => {
if (res.code !== 0) {
@@ -769,10 +1180,7 @@ layout("/layouts/platform.html"){
hasFamily: ledger.hasFamily === true || ledger.hasFamily === 1 || ledger.hasFamily === "1"
})
this.directRelativeForm = Object.assign(this.emptyDirectRelative(), {
lineId: matter.lineId || "",
lineName: matter.lineName || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || ""
lineId: matter.lineId || ""
}, directRelative)
this.familyData = (data.families || []).map(item => Object.assign(this.emptyFamily(), item))
if (this.familyData.length > 0) {
@@ -815,9 +1223,9 @@ layout("/layouts/platform.html"){
relationshipCode: "",
relationshipName: "",
lineId: this.signupForm.lineId || "",
lineName: this.signupForm.lineName || "",
travelStartTime: this.signupForm.travelStartTime || "",
travelEndTime: this.signupForm.travelEndTime || ""
lineName: "",
travelStartTime: "",
travelEndTime: ""
}
},
directRelativeChange(code) {
@@ -949,6 +1357,7 @@ layout("/layouts/platform.html"){
this.cancelLoading = false
},
destroyLineView() {
this.cleanupLinePdfPreviews()
this.lineDetail = {}
this.lineContentHtml = ""
},
@@ -62,13 +62,14 @@ layout("/layouts/platform.html"){
:data="tableData"
:size="tableSize"
border
:default-sort="{prop: 'agencyCode', order: 'ascending'}"
@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="year" width="110" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="旅行社编号" prop="agencyCode" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="旅行社名称" prop="agencyName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系人" prop="contactName" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系电话" prop="contactPhone" width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="排序编号" prop="agencyCode" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="激活状态" prop="enabled" width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
@@ -117,11 +118,6 @@ layout("/layouts/platform.html"){
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="旅行社编号" prop="agencyCode">
<el-input v-model="formData.agencyCode" maxlength="50" placeholder="请输入旅行社编号"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="激活状态" prop="enabled">
<el-radio-group v-model="formData.enabled">
@@ -144,6 +140,11 @@ layout("/layouts/platform.html"){
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="排序编号" prop="agencyCode">
<el-input-number v-model="formData.agencyCode" :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="email">
<el-input v-model="formData.email" maxlength="100" placeholder="请输入邮箱"></el-input>
@@ -213,7 +214,9 @@ layout("/layouts/platform.html"){
year: currentYear,
agencyName: "",
contactName: "",
contactPhone: ""
contactPhone: "",
pageOrderName: "agencyCode",
pageOrderBy: "ascending"
},
formData: {},
formRules: {
@@ -0,0 +1,470 @@
<!--#
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%">
</el-date-picker>
</search-item>
<search-item label="线路名称">
<el-input
v-model="pageForm.lineName"
clearable
placeholder="请输入线路名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="线路类型">
<el-select v-model="pageForm.lineType" clearable filterable placeholder="请选择线路类型" style="width: 100%">
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></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>
<div class="search-query" style="margin-left: auto;">
<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-radio-group class="mr5" size="small" v-model="pageForm.undertakeStatus" @change="changeUndertakeStatus">
<el-radio-button label="claimed">已领</el-radio-button>
<el-radio-button label="unclaimed">未领</el-radio-button>
</el-radio-group>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
:default-sort="{prop: 'lineName', order: 'ascending'}"
:row-class-name="tableRowClassName"
@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="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="false" label="事项名称" prop="matterName" min-width="200" 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="undertakeStatusName" width="150" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-button size="mini" :type="isClaimed(row) ? 'info' : 'primary'" plain>{{ row.undertakeStatusName || '未领' }}</el-button>
</template>
</el-table-column>
<el-table-column label="线路类型" prop="lineType" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="最少成团人数" prop="minGroupPeople" width="140" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
{{ row.minGroupPeople || 0 }}人
</template>
</el-table-column>
<el-table-column label="最多成团人数" prop="maxGroupPeople" width="140" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
{{ row.maxGroupPeople || 0 }}人
</template>
</el-table-column>
<el-table-column label="报名人数" prop="signupCount" width="160" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="Number(row.signupCount || 0) > 0 ? 'success' : 'info'">
{{ row.signupCount || 0 }}人<span v-if="row.showCurrentUnionSignupCount">({{ row.currentUnionSignupCount || 0 }}人)</span>
</el-tag>
</template>
</el-table-column>
<el-table-column label="成团状态" prop="groupStatusName" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="groupStatusType(row.groupStatus)">{{ row.groupStatusName || '未成团' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button size="mini" type="primary" :disabled="isClaimed(row)" @click="undertakeLine(row)">领办</el-button>
<el-button v-if="row.canRelease" size="mini" type="danger" plain @click="releaseUndertakeLine(row)">解除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
<div class="tour-undertake-tip">温馨提醒:列表中报名人数字段括号内的数字是本工会所报其线路的人数。</div>
</el-card>
<el-drawer
:title="viewTitle"
:visible.sync="viewVisible"
direction="rtl"
size="72%"
custom-class="tour-group-view-drawer"
:close-on-click-modal="false">
<div class="tour-group-view">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="姓名/工号">
<el-input
v-model="viewForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="viewSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="viewForm.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>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="viewSearch">搜索</el-button>
<el-button size="medium" @click="viewReset">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<div class="tour-group-view-title">报名人员列表</div>
<el-table
v-loading="viewLoading"
:data="viewTableData"
:size="tableSize"
border
@sort-change="viewPageOrder">
<el-table-column type="expand" width="50">
<template slot-scope="{row}">
<div v-if="row.families && row.families.length > 0" class="tour-family-list">
<el-table :data="row.families" :size="tableSize" border>
<el-table-column label="工号" prop="jobNo" width="130" align="center" header-align="center" show-overflow-tooltip></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="relationship" width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="手机号" prop="mobile" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="身份证号" prop="idCard" min-width="190" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</div>
<div v-else class="tour-family-empty">暂无亲属信息</div>
</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>
<el-table-column label="手机号码" prop="mobile" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="身份证号" prop="idCard" min-width="190" 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>
<el-row class="el-pagination-container tour-group-view-pagination">
<el-pagination
background
:current-page="viewForm.pageNumber"
:page-size="viewForm.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="viewForm.totalCount"
layout="total, sizes, prev, pager, next, jumper"
@size-change="viewSizeChange"
@current-change="viewCurrentChange">
</el-pagination>
</el-row>
</el-card>
</div>
</el-drawer>
</guava>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
unionOptions: [],
lineTypeOptions: [],
viewVisible: false,
viewLoading: false,
viewRequestSeq: 0,
viewTitle: "",
viewRow: {},
viewTableData: [],
currentUnionInfo: null,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "lineName",
pageOrderBy: "ascending",
year: currentYear,
lineName: "",
lineType: "",
unionId: "",
undertakeStatus: "unclaimed"
},
viewForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
keyword: "",
unionId: ""
}
}
},
methods: {
resetSearch() {
this.pageForm.year = moment().format("YYYY")
this.pageForm.lineName = ""
this.pageForm.lineType = ""
this.pageForm.unionId = ""
this.pageForm.undertakeStatus = "unclaimed"
this.loadLineTypeOptions()
this.doSearch()
},
changeUndertakeStatus() {
this.pageForm.pageNumber = 1
this.pageData()
},
groupStatusType(status) {
if (status === "formed") return "success"
if (status === "over") return "danger"
return "info"
},
tableRowClassName({ row }) {
return this.isDirectFamilyLine(row) ? "direct-family-row" : ""
},
isDirectFamilyLine(row) {
if (!row) return false
return row.directFamilyUnitLine === true
|| row.directFamilyUnitLine === 1
|| row.directFamilyUnitLine === "1"
},
isClaimed(row) {
return row && row.undertakeStatus === "claimed"
},
openView(row) {
this.viewRow = row || {}
this.viewTitle = this.viewRow.lineName || "报名人员"
this.viewVisible = true
this.viewForm.pageNumber = 1
this.viewForm.pageSize = 10
this.viewForm.totalCount = 0
this.viewForm.pageOrderName = "signupTime"
this.viewForm.pageOrderBy = "descending"
this.viewForm.keyword = ""
this.viewForm.unionId = ""
this.viewTableData = []
this.loadViewData()
},
loadCurrentUnion() {
if (this.currentUnionInfo) {
return Promise.resolve(this.currentUnionInfo)
}
return this.$axios.post(loc() + "/currentUnion").then((res) => {
if (res.code === 0) {
this.currentUnionInfo = res.data || {}
return this.currentUnionInfo
}
this.$message.warning(res.msg || "未获取到当前登录人工会")
return Promise.reject(new Error(res.msg || "未获取到当前登录人工会"))
})
},
undertakeLine(row) {
if (!row || !row.matterId) {
this.$message.warning("线路信息不完整")
return
}
if (this.isClaimed(row)) {
this.$message.warning("当前线路已领办,不允许修改")
return
}
this.loadCurrentUnion().then((union) => {
const lineName = row.lineName || "当前线路"
const unionName = (union && union.unionName) || ""
return this.$confirm(lineName + "由" + unionName + "领办,领办后,系统不允许修改,请确认。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
return this.$axios.post(loc() + "/undertakeLine", {
matterId: row.matterId
})
})
}).then((res) => {
if (!res) return
if (res.code === 0) {
this.$message.success("领办成功")
this.pageData()
} else {
this.$message.warning(res.msg || "领办失败")
}
}).catch(() => {})
},
releaseUndertakeLine(row) {
if (!row || !row.matterId) {
this.$message.warning("线路信息不完整")
return
}
if (!this.isClaimed(row)) {
this.$message.warning("当前线路未领办,无需解除")
return
}
const lineName = row.lineName || "当前线路"
this.$confirm("确认解除【" + lineName + "】的领办状态吗?解除后该线路将变为未领。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
return this.$axios.post(loc() + "/releaseUndertakeLine", {
matterId: row.matterId
})
}).then((res) => {
if (!res) return
if (res.code === 0) {
this.$message.success("解除成功")
this.pageData()
} else {
this.$message.warning(res.msg || "解除失败")
}
}).catch(() => {})
},
viewSearch() {
this.viewForm.pageNumber = 1
this.loadViewData()
},
viewReset() {
this.viewForm.keyword = ""
this.viewForm.unionId = ""
this.viewForm.pageNumber = 1
this.loadViewData()
},
viewIndexMethod(index) {
return (this.viewForm.pageNumber - 1) * this.viewForm.pageSize + index + 1
},
viewSizeChange(size) {
this.viewForm.pageSize = size
this.viewForm.pageNumber = 1
this.loadViewData()
},
viewCurrentChange(page) {
this.viewForm.pageNumber = page
this.loadViewData()
},
viewPageOrder({ prop, order }) {
this.viewForm.pageOrderName = prop || "signupTime"
this.viewForm.pageOrderBy = order || "descending"
this.viewForm.pageNumber = 1
this.loadViewData()
},
loadViewData() {
if (!this.viewRow || !this.viewRow.matterId) {
this.viewTableData = []
this.viewForm.totalCount = 0
return
}
this.viewLoading = true
const requestSeq = ++this.viewRequestSeq
this.$axios.post(loc() + "/signupPageData", {
matterId: this.viewRow.matterId,
keyword: this.viewForm.keyword,
unionId: this.viewForm.unionId,
pageNumber: this.viewForm.pageNumber,
pageSize: this.viewForm.pageSize,
pageOrderName: this.viewForm.pageOrderName,
pageOrderBy: this.viewForm.pageOrderBy
}).then((res) => {
if (requestSeq !== this.viewRequestSeq) return
if (res.code === 0) {
const data = res.data || {}
this.viewTableData = data.list || []
this.viewForm.totalCount = data.totalCount || 0
} else {
this.$message.warning(res.msg || "查询失败")
}
}).finally(() => {
if (requestSeq === this.viewRequestSeq) {
this.viewLoading = false
}
})
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions").then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
}
})
},
loadLineTypeOptions() {
this.$axios.post(loc() + "/lineTypeOptions", {
year: this.pageForm.year
}).then((res) => {
if (res.code === 0) {
this.lineTypeOptions = (res.data || []).filter(item => item.lineType)
}
})
}
},
mounted() {
this.loadUnionOptions()
this.loadLineTypeOptions()
this.pageData()
},
watch: {
"pageForm.year"() {
this.pageForm.lineType = ""
this.loadLineTypeOptions()
}
}
})
</script>
<style>
.tour-group-view-drawer .el-drawer__body {
background: #f5f7fa;
padding: 12px;
overflow: auto;
}
.tour-group-view-pagination {
margin-top: 12px;
text-align: right;
}
.tour-group-view-title {
border-left: 4px solid #0079c2;
color: #0079c2;
font-size: 14px;
font-weight: 600;
line-height: 16px;
margin-bottom: 12px;
padding-left: 10px;
}
.tour-family-list {
padding: 8px 32px;
}
.tour-family-empty {
color: #909399;
padding: 8px 32px;
}
.tour-undertake-tip {
margin-top: 8px;
color: #f56c6c;
font-size: 13px;
line-height: 20px;
text-align: center;
}
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
</style>
<!--#
}
#-->
@@ -190,8 +190,8 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时段">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">
{{ familyText() }}
</el-descriptions-item>
@@ -163,8 +163,8 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item v-if="false" label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
@@ -380,7 +380,7 @@ layout("/layouts/platform_h5.html"){
<van-button v-if="canRevoke(row)" size="small" type="danger" plain @click="revokeSignup(row)">撤回</van-button>
<van-button v-if="showCancel(row)" size="small" type="danger" plain :disabled="isTravelEnded(row)" @click="cancelByRow(row)">取消报名</van-button>
<van-button v-if="showExport(row)" size="small" type="info" plain :disabled="!canExport(row)" @click="downloadExport(row, 'cost')">报销申请</van-button>
<van-button v-if="showDirectRelativeExport(row)" size="small" type="info" plain :disabled="!canDirectRelativeExport(row)" @click="downloadExport(row, 'direct')">亲属线路申请</van-button>
<van-button v-if="showDirectRelativeExport(row)" size="small" type="info" plain :disabled="!canDirectRelativeExport(row)" @click="downloadExport(row, 'direct')">下载亲属线路申请</van-button>
</div>
</div>
</van-list>
@@ -416,6 +416,7 @@ layout("/layouts/platform_h5.html"){
<van-cell title="姓名" :value="detail.userName || ''"></van-cell>
<van-cell title="性别" :value="detail.gender || ''"></van-cell>
<van-cell title="身份证号" :value="detail.idCard || ''"></van-cell>
<van-cell title="手机号" :value="detail.mobile || ''"></van-cell>
<van-cell title="所在单位" :value="detail.unitName || ''"></van-cell>
<van-cell title="所在工会" :value="detail.unionName || ''"></van-cell>
</div>
@@ -424,8 +425,8 @@ layout("/layouts/platform_h5.html"){
<van-cell title="报名线路" :value="detail.lineName || ''"></van-cell>
<van-cell title="线路类型" :value="detail.lineType || ''"></van-cell>
<van-cell title="出行时间" :value="detail.travelPeriod || ''"></van-cell>
<van-cell title="报名酒店" :value="detail.hotelName || ''"></van-cell>
<van-cell title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
<van-cell v-if="false" title="报名酒店" :value="detail.hotelName || ''"></van-cell>
<van-cell v-if="false" title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
<van-cell v-if="detailFillBedInfo" title="床型" :value="detail.bedType || ''"></van-cell>
<van-cell v-if="detailFillBedInfo" title="床位信息" :value="detail.bedInfo || ''"></van-cell>
<van-cell v-if="detailFillBedInfo" title="意向拼床人" :value="detail.intendedRoommate || ''"></van-cell>
@@ -469,7 +470,8 @@ layout("/layouts/platform_h5.html"){
<div class="tour-section-title">教职工信息</div>
<van-field label="姓名" readonly v-model="signupForm.userName"></van-field>
<van-field label="工号" readonly v-model="signupForm.jobNo"></van-field>
<van-field label="身份证号" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号"></van-field>
<van-field required label="身份证号" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号"></van-field>
<van-field required label="手机号" v-model="signupForm.mobile" type="tel" maxlength="30" placeholder="请填写手机号"></van-field>
<van-field label="所在工会" readonly v-model="signupForm.unionName"></van-field>
</div>
@@ -478,7 +480,7 @@ layout("/layouts/platform_h5.html"){
<van-field label="报名线路" readonly v-model="signupForm.lineName"></van-field>
<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 v-if="false" label="报名酒店" v-model="signupForm.hotelName" maxlength="100" placeholder="请输入报名酒店"></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>
@@ -783,6 +785,7 @@ layout("/layouts/platform_h5.html"){
userName: "",
gender: "",
idCard: "",
mobile: "",
unitId: "",
unitName: "",
unionId: "",
@@ -821,12 +824,9 @@ layout("/layouts/platform_h5.html"){
overCostReimbursed: ledger.overCostReimbursed === true || ledger.overCostReimbursed === 1 || ledger.overCostReimbursed === "1",
hasFamily: ledger.hasFamily === true || ledger.hasFamily === 1 || ledger.hasFamily === "1"
})
this.directRelativeForm = Object.assign(this.emptyDirectRelative(), directRelative, {
lineId: matter.lineId || "",
lineName: matter.lineName || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || ""
})
this.directRelativeForm = Object.assign(this.emptyDirectRelative(), {
lineId: matter.lineId || ""
}, directRelative)
this.familyData = (data.families || []).map(item => Object.assign(this.emptyFamily(), item))
if (this.familyData.length > 0) {
this.signupForm.hasFamily = true
@@ -861,9 +861,9 @@ layout("/layouts/platform_h5.html"){
relationshipCode: "",
relationshipName: "",
lineId: this.signupForm.lineId || "",
lineName: this.signupForm.lineName || "",
travelStartTime: this.signupForm.travelStartTime || "",
travelEndTime: this.signupForm.travelEndTime || ""
lineName: "",
travelStartTime: "",
travelEndTime: ""
}
},
addFamily() {
@@ -956,8 +956,19 @@ layout("/layouts/platform_h5.html"){
}
return true
},
validateSelfInfo() {
if (!String(this.signupForm.idCard || "").trim()) {
vant.Toast("请输入身份证号")
return false
}
if (!String(this.signupForm.mobile || "").trim()) {
vant.Toast("请输入手机号")
return false
}
return true
},
submitSignup() {
if (!this.validateFamilies() || !this.validateDirectRelative()) return
if (!this.validateSelfInfo() || !this.validateFamilies() || !this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
const form = Object.assign({}, this.signupForm, {
families: JSON.stringify(families),
@@ -205,6 +205,7 @@ layout("/layouts/platform_h5.html"){
<van-cell title="姓名" :value="detail.userName || ''"></van-cell>
<van-cell title="性别" :value="detail.gender || ''"></van-cell>
<van-cell title="身份证号" :value="detail.idCard || ''"></van-cell>
<van-cell title="手机号" :value="detail.mobile || ''"></van-cell>
<van-cell title="所在单位" :value="detail.unitName || ''"></van-cell>
<van-cell title="所在工会" :value="detail.unionName || ''"></van-cell>
</div>
@@ -215,8 +216,8 @@ layout("/layouts/platform_h5.html"){
<van-cell title="线路类型" :value="detail.lineType || ''"></van-cell>
<van-cell title="出行时段" :value="detail.travelPeriod || ''"></van-cell>
<van-cell title="报名时间" :value="detail.signupTime || ''"></van-cell>
<van-cell title="报名酒店" :value="detail.hotelName || ''"></van-cell>
<van-cell title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
<van-cell v-if="false" title="报名酒店" :value="detail.hotelName || ''"></van-cell>
<van-cell v-if="false" title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="床型" :value="detail.bedType || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="床位信息" :value="detail.bedInfo || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="意向拼床人" :value="detail.intendedRoommate || ''"></van-cell>
@@ -109,8 +109,8 @@ layout("/layouts/platform_h5.html"){
<div class="tour-apply-title">教职工信息</div>
<van-field label="姓名" readonly v-model="signupForm.userName"></van-field>
<van-field label="工号" readonly v-model="signupForm.jobNo"></van-field>
<van-field label="身份证号码" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号码"></van-field>
<van-field label="手机号" v-model="signupForm.mobile" type="tel" maxlength="30" placeholder="请填写手机号"></van-field>
<van-field required label="身份证号码" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号码"></van-field>
<van-field required label="手机号" v-model="signupForm.mobile" type="tel" maxlength="30" placeholder="请填写手机号"></van-field>
<van-field label="所属工会" readonly v-model="signupForm.unionName"></van-field>
</div>
@@ -119,7 +119,7 @@ layout("/layouts/platform_h5.html"){
<van-field label="报名线路" readonly v-model="signupForm.lineName"></van-field>
<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 v-if="false" label="报名酒店" v-model="signupForm.hotelName" maxlength="100" placeholder="请输入报名酒店"></van-field>
<van-field
v-if="fillBedInfo"
label="床型"
@@ -256,7 +256,8 @@ layout("/layouts/platform_h5.html"){
relationshipPickerVisible: false,
directRelativePickerVisible: false,
bedPickerTarget: "self",
matterId: GetQueryString("matterId") || ""
matterId: GetQueryString("matterId") || "",
noticeChecked: GetQueryString("noticeChecked") === "1"
}
},
computed: {
@@ -280,25 +281,42 @@ layout("/layouts/platform_h5.html"){
this.pageLoading = false
return
}
Promise.all([this.loadOptions(), this.loadSignupDetail()]).finally(() => {
this.loadOptions().catch(() => {})
this.loadSignupDetail().finally(() => {
this.pageLoading = false
})
},
loadOptions() {
return Promise.all([
this.$axios.post("/platform/tour/signup/bedTypeOptions").then(res => {
if (res.code === 0) this.bedTypeOptions = res.data || []
}),
this.$axios.post("/platform/tour/signup/familyRelationshipOptions").then(res => {
if (res.code === 0) this.familyRelationshipOptions = res.data || []
}),
this.$axios.post("/platform/tour/signup/directRelativeOptions").then(res => {
if (res.code === 0) this.directRelativeOptions = res.data || []
})
])
const cacheKey = "tourSignupOptions"
try {
const cached = JSON.parse(sessionStorage.getItem(cacheKey) || "null")
if (cached && cached.data) {
this.applyOptions(cached.data)
return Promise.resolve(cached.data)
}
} catch (e) {
}
return this.$axios.post("/platform/tour/signup/signupOptions").then(res => {
if (res.code === 0) {
const data = res.data || {}
this.applyOptions(data)
try {
sessionStorage.setItem(cacheKey, JSON.stringify({data: data}))
} catch (e) {
}
}
})
},
applyOptions(data) {
this.bedTypeOptions = data.bedTypeOptions || []
this.familyRelationshipOptions = data.familyRelationshipOptions || []
this.directRelativeOptions = data.directRelativeOptions || []
},
loadSignupDetail() {
return this.$axios.post("/platform/tour/signup/signupDetail", { matterId: this.matterId }).then((res) => {
return this.$axios.post("/platform/tour/signup/signupDetail", {
matterId: this.matterId,
noticeChecked: this.noticeChecked
}).then((res) => {
if (res.code !== 0) {
vant.Dialog.alert({
title: "提示",
@@ -361,10 +379,7 @@ layout("/layouts/platform_h5.html"){
hasFamily: ledger.hasFamily === true || ledger.hasFamily === 1 || ledger.hasFamily === "1"
})
this.directRelativeForm = Object.assign(this.emptyDirectRelative(), {
lineId: matter.lineId || "",
lineName: matter.lineName || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || ""
lineId: matter.lineId || ""
}, directRelative)
this.familyData = (data.families || []).map(item => Object.assign(this.emptyFamily(), item))
if (this.familyData.length > 0) {
@@ -398,9 +413,9 @@ layout("/layouts/platform_h5.html"){
relationshipCode: "",
relationshipName: "",
lineId: this.signupForm.lineId || "",
lineName: this.signupForm.lineName || "",
travelStartTime: this.signupForm.travelStartTime || "",
travelEndTime: this.signupForm.travelEndTime || ""
lineName: "",
travelStartTime: "",
travelEndTime: ""
}
},
isDirectFamilyLine(row) {
@@ -512,8 +527,20 @@ layout("/layouts/platform_h5.html"){
}
return true
},
validateSelfInfo() {
if (!String(this.signupForm.idCard || "").trim()) {
vant.Toast("请输入身份证号码")
return false
}
if (!String(this.signupForm.mobile || "").trim()) {
vant.Toast("请输入手机号")
return false
}
return true
},
submitSignup() {
if (this.pageLoading || this.submitLoading) return
if (!this.validateSelfInfo()) return
if (!this.validateFamilies()) return
if (!this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
@@ -153,7 +153,7 @@ layout("/layouts/platform_h5.html"){
</div>
<div class="tour-signup-footer">
<van-button block type="info" color="#0f74bc" round @click="confirmRead">我已阅读</van-button>
<van-button block type="info" color="#0f74bc" round :loading="readLoading" :disabled="readLoading" loading-text="进入中..." @click="confirmRead">我已阅读</van-button>
</div>
</div>
@@ -167,6 +167,7 @@ layout("/layouts/platform_h5.html"){
data() {
return {
loading: false,
readLoading: false,
currentYear: new Date().getFullYear(),
serviceNoticeKey: "",
bannerList: [
@@ -196,7 +197,15 @@ layout("/layouts/platform_h5.html"){
})
},
confirmRead() {
window.location.href = "/platform/tour/signup/h5/signup"
if (this.readLoading) {
return
}
this.readLoading = true
this.$nextTick(() => {
setTimeout(() => {
window.location.href = "/platform/tour/signup/h5/signup"
}, 80)
})
}
},
created() {
@@ -81,6 +81,14 @@ layout("/layouts/platform_h5.html"){
flex: 1;
}
.tour-signup-people-link {
margin-left: 4px;
color: #d40000;
font-size: 13px;
font-weight: 600;
line-height: inherit;
}
.tour-detail-section {
margin-top: 10px;
min-height: 0;
@@ -105,9 +113,16 @@ layout("/layouts/platform_h5.html"){
line-height: 1.2;
}
.tour-detail-content /deep/ img {
.tour-detail-content {
color: #334155;
font-size: 14px;
line-height: 1.75;
word-break: break-word;
}
.tour-detail-content /deep/ img,
.tour-detail-content /deep/ iframe {
max-width: 100%;
height: auto;
}
.tour-detail-section-scroll {
@@ -117,39 +132,73 @@ layout("/layouts/platform_h5.html"){
-webkit-overflow-scrolling: touch;
}
.tour-pdf-preview {
min-height: 220px;
.tour-detail-content /deep/ .tour-line-file-preview-box {
margin-top: 10px;
}
.tour-pdf-status {
padding: 16px 0;
color: #64748b;
font-size: 13px;
text-align: center;
}
.tour-pdf-toolbar {
margin-bottom: 8px;
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.tour-pdf-pages {
.tour-detail-content /deep/ .tour-pdf-lazy-preview {
margin-top: 10px;
min-height: 180px;
}
.tour-pdf-page {
margin-bottom: 10px;
border-radius: 6px;
border: 1px solid #dcdfe6;
overflow: hidden;
background: #f8fafc;
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
}
.tour-pdf-page canvas {
width: 100%;
.tour-detail-content /deep/ .tour-pdf-lazy-toolbar {
padding: 0 10px;
border-bottom: 1px solid #e5e7eb;
color: #64748b;
font-size: 12px;
line-height: 32px;
background: #fff;
}
.tour-detail-content /deep/ .tour-pdf-page-placeholder {
display: flex;
align-items: center;
justify-content: center;
min-height: 180px;
border-bottom: 1px solid #e5e7eb;
color: #94a3b8;
font-size: 13px;
background: #f8fafc;
}
.tour-detail-content /deep/ .tour-pdf-page-placeholder canvas {
display: block;
max-width: 100%;
margin: 0 auto;
background: #fff;
}
.tour-detail-content /deep/ .tour-line-file-preview {
display: block;
width: 100%;
height: 70vh;
min-height: 420px;
border: 1px solid #dcdfe6;
background: #fff;
}
.tour-detail-content /deep/ .tour-line-image-preview {
display: block;
width: 100%;
max-height: 70vh;
height: auto;
margin-top: 10px;
border: 1px solid #dcdfe6;
object-fit: contain;
background: #fff;
}
.tour-detail-content /deep/ .tour-line-file-unpreviewable {
margin-top: 10px;
padding: 12px;
border: 1px solid #e5e7eb;
color: #94a3b8;
font-size: 13px;
text-align: center;
background: #f8fafc;
}
.tour-detail-empty {
@@ -170,6 +219,99 @@ layout("/layouts/platform_h5.html"){
background: #ffffff;
box-sizing: border-box;
}
.tour-people-popup {
height: 72vh;
display: flex;
flex-direction: column;
background: #ffffff;
overflow: hidden;
padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
}
.tour-people-popup-title {
flex-shrink: 0;
padding: 14px 16px 8px;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 22px;
text-align: center;
}
.tour-people-search {
flex-shrink: 0;
padding: 0 10px 8px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid #edf0f4;
}
.tour-people-search /deep/ .van-search {
flex: 1;
padding: 0;
background: transparent;
}
.tour-people-search-btns {
flex-shrink: 0;
display: flex;
gap: 6px;
}
.tour-people-table {
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tour-people-table-head,
.tour-people-table-row {
display: grid;
grid-template-columns: 54px minmax(88px, 1fr) minmax(128px, 1.25fr);
align-items: center;
min-height: 42px;
border-bottom: 1px solid #edf0f4;
color: #0f172a;
font-size: 13px;
line-height: 18px;
}
.tour-people-table-head {
flex-shrink: 0;
background: #f8fafc;
color: #334155;
font-weight: 700;
}
.tour-people-table-cell {
min-width: 0;
padding: 9px 8px;
text-align: center;
word-break: break-word;
}
.tour-people-sort {
color: #0b75bd;
}
.tour-people-table-body {
min-height: 0;
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.tour-people-empty {
padding: 22px 0;
color: #94a3b8;
font-size: 13px;
text-align: center;
}
</style>
<div id="app">
@@ -192,7 +334,10 @@ layout("/layouts/platform_h5.html"){
</div>
<div class="tour-detail-meta-row">
<span class="tour-detail-meta-label"><span>已报人数</span><span>:</span></span>
<span class="tour-detail-meta-value">{{ signupDetail.signupCount || 0 }}(家属{{ signupDetail.familyCount || 0 }}人)</span>
<span class="tour-detail-meta-value">
{{ signupDetail.signupCount || 0 }}(家属{{ signupDetail.familyCount || 0 }}人)
<span class="tour-signup-people-link" @click.stop="openPeoplePopup">查看</span>
</span>
</div>
<div class="tour-detail-meta-row">
<span class="tour-detail-meta-label"><span>联系人</span><span>:</span></span>
@@ -207,20 +352,7 @@ layout("/layouts/platform_h5.html"){
<div class="tour-detail-section">
<div class="tour-detail-section-title">线路介绍</div>
<div class="tour-detail-section-scroll">
<div v-if="lineDetail.lineContent && lineContentHref" class="tour-pdf-preview">
<div v-if="pdfPageCount" class="tour-pdf-toolbar">
已加载 {{ pdfRenderedPages }} / {{ pdfPageCount }} 页
</div>
<div ref="pdfPreviewContainer" class="tour-pdf-pages"></div>
<div v-if="pdfLoading && pdfRenderedPages === 0" class="tour-pdf-status">
<van-loading size="22px" vertical>正在加载第一页...</van-loading>
</div>
<div v-else-if="pdfLoading" class="tour-pdf-status">
正在继续加载后续页面...
</div>
<div v-if="pdfError" class="tour-pdf-status">{{ pdfError }}</div>
</div>
<div v-else-if="lineDetail.lineContent" class="tour-detail-content" v-html="lineDetail.lineContent">
<div v-if="lineDetail.lineContent" class="tour-detail-content" v-html="lineContentHtml">
</div>
<div v-else class="tour-detail-empty">暂无线路介绍</div>
</div>
@@ -229,10 +361,51 @@ layout("/layouts/platform_h5.html"){
</div>
<div class="tour-detail-footer">
<van-button :type="detailActionType()" block round :disabled="detailActionDisabled()" @click="handleDetailAction">
<van-button :type="detailActionType()" block round :loading="applyLoading" :disabled="detailActionDisabled() || applyLoading" @click="handleDetailAction">
{{ detailActionText() }}
</van-button>
</div>
<van-popup v-model="peopleVisible" position="bottom" round>
<div class="tour-people-popup">
<div class="tour-people-popup-title">报名人员</div>
<div class="tour-people-search">
<van-search
v-model="peopleForm.keyword"
shape="round"
placeholder="请输入姓名或工号"
@search="peopleSearch">
</van-search>
<div class="tour-people-search-btns">
<van-button size="small" type="info" @click="peopleSearch">搜索</van-button>
<van-button size="small" plain @click="peopleReset">重置</van-button>
</div>
</div>
<div class="tour-people-table">
<div class="tour-people-table-head">
<div class="tour-people-table-cell">序号</div>
<div class="tour-people-table-cell">姓名</div>
<div class="tour-people-table-cell tour-people-sort" @click="togglePeopleUnionSort">
所属工会{{ peopleSortText() }}
</div>
</div>
<div class="tour-people-table-body">
<van-list
v-model="peopleLoading"
:finished="peopleFinished"
finished-text="没有更多了"
@load="loadPeople">
<div v-for="item in peopleList" :key="item.id" class="tour-people-table-row">
<div class="tour-people-table-cell">{{ item.displayIndex }}</div>
<div class="tour-people-table-cell">{{ item.userName || '' }}</div>
<div class="tour-people-table-cell">{{ item.unionName || '' }}</div>
</div>
</van-list>
<div v-if="!peopleLoading && peopleList.length === 0" class="tour-people-empty">暂无报名人员</div>
</div>
</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
@@ -242,16 +415,25 @@ layout("/layouts/platform_h5.html"){
data() {
return {
detailLoading: true,
applyLoading: false,
lineDetail: {},
signupDetail: {},
lineContentHref: "",
pdfDoc: null,
pdfLoadingTask: null,
pdfLoading: false,
pdfError: "",
pdfPageCount: 0,
pdfRenderedPages: 0,
pdfRenderToken: 0,
lineContentHtml: "",
linePdfPreviews: [],
linePdfObserver: null,
peopleVisible: false,
peopleLoading: false,
peopleFinished: true,
peopleList: [],
peopleForm: {
pageNumber: 1,
pageSize: 20,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
keyword: "",
unionId: ""
},
matterId: GetQueryString("matterId") || "",
lineId: GetQueryString("lineId") || ""
}
@@ -314,6 +496,94 @@ layout("/layouts/platform_h5.html"){
detailActionDisabled() {
return this.isSigned() && this.isApprovalSignup() && (this.isFinishedSignup() || this.isRejectedSignup() || (!this.canRevokeSignup() && !this.canModifySignup()))
},
peopleSortText() {
if (this.peopleForm.pageOrderName !== "unionName") {
return ""
}
return this.peopleForm.pageOrderBy === "ascending" ? "↑" : "↓"
},
resetPeopleData() {
this.peopleForm.pageNumber = 1
this.peopleForm.totalCount = 0
this.peopleList = []
this.peopleFinished = false
},
openPeoplePopup() {
if (!this.matterId) {
vant.Toast("报名事项信息不完整")
return
}
this.peopleVisible = true
this.peopleForm.keyword = ""
this.peopleForm.pageOrderName = "signupTime"
this.peopleForm.pageOrderBy = "descending"
this.resetPeopleData()
this.$nextTick(() => {
this.loadPeople()
})
},
peopleSearch() {
this.resetPeopleData()
this.loadPeople()
},
peopleReset() {
this.peopleForm.keyword = ""
this.peopleForm.pageOrderName = "signupTime"
this.peopleForm.pageOrderBy = "descending"
this.resetPeopleData()
this.loadPeople()
},
togglePeopleUnionSort() {
if (this.peopleForm.pageOrderName === "unionName") {
this.peopleForm.pageOrderBy = this.peopleForm.pageOrderBy === "ascending" ? "descending" : "ascending"
} else {
this.peopleForm.pageOrderName = "unionName"
this.peopleForm.pageOrderBy = "ascending"
}
this.resetPeopleData()
this.loadPeople()
},
loadPeople() {
if (!this.matterId || this.peopleLoading || this.peopleFinished) {
return
}
this.peopleLoading = true
this.$axios.post("/platform/tour/signup/h5/signupPageData", {
matterId: this.matterId,
keyword: this.peopleForm.keyword,
unionId: this.peopleForm.unionId,
pageNumber: this.peopleForm.pageNumber,
pageSize: this.peopleForm.pageSize,
pageOrderName: this.peopleForm.pageOrderName,
pageOrderBy: this.peopleForm.pageOrderBy
}).then((res) => {
if (res.code !== 0) {
vant.Toast(res.msg || "报名人员加载失败")
this.peopleFinished = true
return
}
const data = res.data || {}
const rows = data.list || []
const startIndex = this.peopleList.length
const normalizedRows = rows.map((item, index) => {
return Object.assign({}, item, {
displayIndex: startIndex + index + 1
})
})
this.peopleForm.totalCount = data.totalCount || 0
this.peopleList = this.peopleList.concat(normalizedRows)
if (this.peopleList.length >= this.peopleForm.totalCount || rows.length < this.peopleForm.pageSize) {
this.peopleFinished = true
} else {
this.peopleForm.pageNumber += 1
}
}).catch(() => {
vant.Toast("报名人员加载失败")
this.peopleFinished = true
}).finally(() => {
this.peopleLoading = false
})
},
handleDetailAction() {
if (this.detailActionDisabled()) {
return
@@ -322,134 +592,269 @@ layout("/layouts/platform_h5.html"){
this.revokeSignup()
return
}
if (this.isSigned()) {
this.onApply(true)
return
}
this.onApply()
},
parseLineContentHref(content) {
if (!content) {
return ""
buildLineContentHtmlAsync(content) {
const ids = this.extractLineFileIds(content)
if (!ids.length) {
return Promise.resolve(this.buildLineContentHtml(content, {}))
}
try {
const parser = new DOMParser()
const doc = parser.parseFromString(content, "text/html")
const link = doc.querySelector("a[href]")
return link ? link.getAttribute("href") : ""
} catch (e) {
return ""
return this.$axios.post("/platform/sys/file/previewFileData", {ids: JSON.stringify(ids)}).then((res) => {
const fileMap = {}
if (res.code === 0 && Array.isArray(res.data)) {
res.data.forEach((file) => {
if (file && file.id) {
file.suffix = (file.suffix || "").toLowerCase()
fileMap[file.id] = file
}
})
}
return this.buildLineContentHtml(content, fileMap)
}).catch(() => this.buildLineContentHtml(content, {}))
},
extractLineFileIds(content) {
if (!content) return []
const wrap = document.createElement("div")
wrap.innerHTML = content
const ids = []
Array.prototype.slice.call(wrap.querySelectorAll("a[href]")).forEach((anchor) => {
const id = this.getFileIdFromUrl(anchor.getAttribute("href") || "")
if (id && !ids.includes(id)) {
ids.push(id)
}
})
return ids
},
buildLineContentHtml(content, fileMap) {
if (!content) return ""
const wrap = document.createElement("div")
wrap.innerHTML = content
const anchors = Array.prototype.slice.call(wrap.querySelectorAll("a[href]"))
anchors.forEach((anchor) => {
const href = anchor.getAttribute("href") || ""
const text = anchor.textContent || href
const fileId = this.getFileIdFromUrl(href)
const file = fileId && fileMap ? fileMap[fileId] : null
const fileType = this.detectPreviewFileType(href, text, file)
if (!fileType) {
return
}
const preview = this.createLineFilePreview(href, fileType, file)
if (anchor.parentNode && preview) {
anchor.parentNode.replaceChild(preview, anchor)
}
})
return wrap.innerHTML
},
detectPreviewFileType(href, text, file) {
const suffix = file && file.suffix ? file.suffix.toLowerCase() : ""
if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(suffix)) {
return "image"
}
if (suffix === "pdf") {
return "pdf"
}
if (["doc", "docx", "xls", "xlsx"].includes(suffix)) {
return "office"
}
const lowerHref = (href || "").split("#")[0].split("?")[0].toLowerCase()
const lowerText = (text || "").toLowerCase()
const value = lowerHref + " " + lowerText
if (/\.(jpg|jpeg|png|gif|bmp|webp)(\s|$)/.test(value)) {
return "image"
}
if (/\.pdf(\s|$)/.test(value)) {
return "pdf"
}
if (/\.(doc|docx|xls|xlsx)(\s|$)/.test(value)) {
return "office"
}
return ""
},
createLineFilePreview(href, fileType, file) {
if (fileType === "image") {
const img = document.createElement("img")
img.className = "tour-line-image-preview"
img.src = file && file.downloadPath ? file.downloadPath : href
img.alt = ""
return img
}
const previewUrl = this.buildLinePreviewUrl(href, fileType, file)
if (!previewUrl) {
const tip = document.createElement("div")
tip.className = "tour-line-file-unpreviewable"
tip.textContent = "当前文件暂不支持预览"
return tip
}
const box = document.createElement("div")
box.className = "tour-line-file-preview-box"
const preview = document.createElement("div")
preview.className = "tour-pdf-lazy-preview"
preview.setAttribute("data-pdf-url", previewUrl)
preview.innerHTML = "<div class=\"tour-pdf-lazy-toolbar\">PDF预览加载中...</div>"
box.appendChild(preview)
return box
},
buildLinePreviewUrl(href, fileType, file) {
if (fileType === "office") {
const id = file && file.id ? file.id : this.getFileIdFromUrl(href)
return id ? "/platform/sys/file/convertPDF?id=" + id : ""
}
if (fileType === "pdf") {
return file && file.downloadPath ? file.downloadPath : href
}
return ""
},
getPdfLib() {
return window.pdfjsLib || window["pdfjs-dist/build/pdf"]
},
clearPdfPreview() {
this.pdfRenderToken += 1
this.pdfDoc = null
this.pdfLoading = false
this.pdfError = ""
this.pdfPageCount = 0
this.pdfRenderedPages = 0
if (this.pdfLoadingTask && this.pdfLoadingTask.destroy) {
try {
this.pdfLoadingTask.destroy()
} catch (e) {
}
}
this.pdfLoadingTask = null
if (this.$refs.pdfPreviewContainer) {
this.$refs.pdfPreviewContainer.innerHTML = ""
}
},
renderLinePdf() {
this.clearPdfPreview()
if (!this.lineContentHref) {
initLinePdfPreviews() {
this.cleanupLinePdfPreviews()
const pdfLib = this.getPdfLib()
const previews = Array.prototype.slice.call(document.querySelectorAll(".tour-pdf-lazy-preview"))
if (!previews.length) {
return
}
const pdfLib = this.getPdfLib()
if (!pdfLib || !pdfLib.getDocument) {
this.pdfError = "PDF预览组件加载失败"
previews.forEach((el) => {
el.innerHTML = "<div class=\"tour-line-file-unpreviewable\">PDF预览组件加载失败</div>"
})
return
}
if (pdfLib.GlobalWorkerOptions) {
pdfLib.GlobalWorkerOptions.workerSrc = "/assets/platform/plugins/pdfJs/h5/pdf.worker.js"
}
const token = this.pdfRenderToken
this.pdfLoading = true
this.pdfLoadingTask = pdfLib.getDocument({
url: this.lineContentHref,
disableAutoFetch: false,
disableStream: false,
disableRange: false
})
this.pdfLoadingTask.promise.then((pdf) => {
if (token !== this.pdfRenderToken) {
return
}
this.pdfDoc = pdf
this.pdfPageCount = pdf.numPages || 0
return this.renderPdfPage(1, token)
}).then(() => {
if (token !== this.pdfRenderToken) {
return
}
this.renderRestPdfPages(2, token)
}).catch(() => {
if (token !== this.pdfRenderToken) {
return
}
this.pdfLoading = false
this.pdfError = "PDF加载失败,请稍后重试"
if (window.IntersectionObserver) {
this.linePdfObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return
}
this.renderLazyPdfPage(entry.target)
this.linePdfObserver && this.linePdfObserver.unobserve(entry.target)
})
}, {
root: null,
rootMargin: "360px 0px",
threshold: 0.01
})
}
previews.forEach((el) => {
this.initLazyPdfPreview(el, pdfLib)
})
},
renderPdfPage(pageNumber, token) {
if (!this.pdfDoc || token !== this.pdfRenderToken || pageNumber > this.pdfPageCount) {
return Promise.resolve()
initLazyPdfPreview(el, pdfLib) {
const url = el.getAttribute("data-pdf-url")
if (!url) {
el.innerHTML = "<div class=\"tour-line-file-unpreviewable\">当前文件暂不支持预览</div>"
return
}
return this.pdfDoc.getPage(pageNumber).then((page) => {
if (token !== this.pdfRenderToken || !this.$refs.pdfPreviewContainer) {
return
const task = pdfLib.getDocument({
url: url,
disableAutoFetch: true,
disableStream: true,
disableRange: false
})
const state = {
el: el,
doc: null,
task: task,
rendered: {},
rendering: {}
}
this.linePdfPreviews.push(state)
task.promise.then((pdf) => {
state.doc = pdf
const count = pdf.numPages || 0
const toolbar = el.querySelector(".tour-pdf-lazy-toolbar")
if (toolbar) {
toolbar.textContent = "PDF预览:共 " + count + " 页,已优先加载第 1 页"
}
const container = this.$refs.pdfPreviewContainer
for (let i = 1; i <= count; i++) {
const page = document.createElement("div")
page.className = "tour-pdf-page-placeholder"
page.setAttribute("data-page-number", i)
page.textContent = i === 1 ? "正在加载第 1 页..." : "第 " + i + " 页,滚动到此处加载"
el.appendChild(page)
}
const firstPage = el.querySelector("[data-page-number='1']")
if (firstPage) {
this.renderLazyPdfPage(firstPage)
}
Array.prototype.slice.call(el.querySelectorAll(".tour-pdf-page-placeholder")).forEach((page) => {
if (page.getAttribute("data-page-number") !== "1" && this.linePdfObserver) {
this.linePdfObserver.observe(page)
}
})
}).catch(() => {
el.innerHTML = "<div class=\"tour-line-file-unpreviewable\">PDF加载失败,请稍后重试</div>"
})
},
renderLazyPdfPage(pageEl) {
const previewEl = pageEl.closest(".tour-pdf-lazy-preview")
const state = this.linePdfPreviews.find((item) => item.el === previewEl)
if (!state || !state.doc) {
return
}
const pageNumber = Number(pageEl.getAttribute("data-page-number") || 0)
if (!pageNumber || state.rendered[pageNumber] || state.rendering[pageNumber]) {
return
}
state.rendering[pageNumber] = true
pageEl.textContent = "正在加载第 " + pageNumber + " 页..."
state.doc.getPage(pageNumber).then((page) => {
const containerWidth = Math.max(previewEl.clientWidth - 16, 260)
const baseViewport = page.getViewport({scale: 1})
const containerWidth = Math.max(container.clientWidth || 0, window.innerWidth - 44)
const cssScale = containerWidth / baseViewport.width
const outputScale = Math.min(window.devicePixelRatio || 1, 2)
const viewport = page.getViewport({scale: cssScale})
const pageEl = document.createElement("div")
pageEl.className = "tour-pdf-page"
const canvas = document.createElement("canvas")
const context = canvas.getContext("2d")
canvas.width = Math.floor(viewport.width * outputScale)
canvas.height = Math.floor(viewport.height * outputScale)
canvas.style.width = Math.floor(viewport.width) + "px"
canvas.style.height = Math.floor(viewport.height) + "px"
pageEl.innerHTML = ""
pageEl.appendChild(canvas)
container.appendChild(pageEl)
const renderContext = {
return page.render({
canvasContext: context,
viewport: viewport,
transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null
}
return page.render(renderContext).promise.then(() => {
if (token === this.pdfRenderToken) {
this.pdfRenderedPages = Math.max(this.pdfRenderedPages, pageNumber)
}
}).promise.then(() => {
state.rendered[pageNumber] = true
state.rendering[pageNumber] = false
})
}).catch(() => {
state.rendering[pageNumber] = false
pageEl.textContent = "第 " + pageNumber + " 页加载失败"
})
},
renderRestPdfPages(pageNumber, token) {
if (token !== this.pdfRenderToken || !this.pdfDoc || pageNumber > this.pdfPageCount) {
this.pdfLoading = false
return
cleanupLinePdfPreviews() {
if (this.linePdfObserver) {
this.linePdfObserver.disconnect()
this.linePdfObserver = null
}
setTimeout(() => {
this.renderPdfPage(pageNumber, token).then(() => {
this.renderRestPdfPages(pageNumber + 1, token)
}).catch(() => {
if (token === this.pdfRenderToken) {
this.pdfLoading = false
this.pdfError = "部分页面加载失败"
this.linePdfPreviews.forEach((item) => {
if (item.task && item.task.destroy) {
try {
item.task.destroy()
} catch (e) {
}
})
}, 120)
}
})
this.linePdfPreviews = []
},
getFileIdFromUrl(href) {
if (!href) return ""
const idMatch = href.match(/[?&]id=([^&#]+)/)
if (idMatch) {
return decodeURIComponent(idMatch[1])
}
return ""
},
loadDetail() {
if (!this.matterId || !this.lineId) {
@@ -470,9 +875,12 @@ layout("/layouts/platform_h5.html"){
vant.Toast(lineRes.msg || "线路详情加载失败")
} else {
this.lineDetail = lineRes.data || {}
this.lineContentHref = this.parseLineContentHref(this.lineDetail.lineContent)
this.$nextTick(() => {
this.renderLinePdf()
this.lineContentHtml = ""
this.buildLineContentHtmlAsync(this.lineDetail.lineContent).then((html) => {
this.lineContentHtml = html
this.$nextTick(() => {
this.initLinePdfPreviews()
})
})
}
if (signupRes.code !== 0) {
@@ -489,20 +897,27 @@ layout("/layouts/platform_h5.html"){
vant.Toast("线路详情加载失败")
})
},
onApply() {
onApply(skipNotice) {
if (!this.matterId) {
vant.Toast("报名事项信息不完整")
return
}
const goApply = () => {
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(this.matterId)
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(this.matterId) + "&noticeChecked=1"
}
if (skipNotice) {
goApply()
return
}
this.applyLoading = true
this.$axios.post("/platform/tour/signup/signupEligibilityNotice", { matterId: this.matterId }).then((res) => {
if (res.code !== 0) {
vant.Dialog.alert({
title: "温馨提醒",
message: res.msg || "当前不能报名",
confirmButtonColor: "#1867b0"
}).then(() => {
this.applyLoading = false
})
return
}
@@ -518,14 +933,20 @@ layout("/layouts/platform_h5.html"){
confirmButtonText: "继续报名",
cancelButtonText: "稍后报名",
confirmButtonColor: "#1867b0"
}).then(goApply).catch(() => {})
}).then(goApply).catch(() => {
this.applyLoading = false
})
} else {
vant.Dialog.alert({
title: "温馨提醒",
message: data.message || "当前不能报名",
confirmButtonColor: "#1867b0"
}).then(() => {
this.applyLoading = false
})
}
}).catch(() => {
this.applyLoading = false
})
},
revokeSignup() {
@@ -549,7 +970,7 @@ layout("/layouts/platform_h5.html"){
this.loadDetail()
},
beforeDestroy() {
this.clearPdfPreview()
this.cleanupLinePdfPreviews()
}
})
</script>
@@ -476,9 +476,9 @@ layout("/layouts/platform_h5.html"){
'tour-line-action--danger': canRevokeSignup(row),
'tour-line-action--disabled': signupActionDisabled(row)
}"
:disabled="signupActionDisabled(row)"
:disabled="signupActionDisabled(row) || signupCheckingMatterId === row.matterId"
@click.stop="handleSignupAction(row)">
{{ signupActionText(row) }}
{{ signupCheckingMatterId === row.matterId ? '进入中...' : signupActionText(row) }}
</button>
</div>
</div>
@@ -520,17 +520,18 @@ layout("/layouts/platform_h5.html"){
loading: false,
finished: false,
lineChecking: false,
signupCheckingMatterId: "",
showUnionPopup: false,
unionList: [
{id: "", name: "全部分工会"}
],
pageForm: {
pageNumber: 1,
pageSize: 10,
pageSize: 5,
totalCount: 0,
year: new Date().getFullYear(),
lineName: "",
lineType: "",
lineType: "省内线路",
unionId: "",
directFamilyUnitLine: null,
pageOrderName: "lineName",
@@ -604,22 +605,34 @@ layout("/layouts/platform_h5.html"){
this.revokeSignup(row)
return
}
if (this.isSigned(row)) {
this.goApply(row, true)
return
}
this.goApply(row)
},
goApply(row) {
goApply(row, skipNotice) {
if (!row || !row.matterId) {
vant.Toast("报名事项信息不完整")
return
}
const go = () => {
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(row.matterId)
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(row.matterId) + "&noticeChecked=1"
}
if (skipNotice) {
this.signupCheckingMatterId = row.matterId
go()
return
}
this.signupCheckingMatterId = row.matterId
this.$axios.post("/platform/tour/signup/signupEligibilityNotice", { matterId: row.matterId }).then((res) => {
if (res.code !== 0) {
vant.Dialog.alert({
title: "温馨提醒",
message: res.msg || "当前不能报名",
confirmButtonColor: "#1867b0"
}).then(() => {
this.signupCheckingMatterId = ""
})
return
}
@@ -635,14 +648,20 @@ layout("/layouts/platform_h5.html"){
confirmButtonText: "继续报名",
cancelButtonText: "稍后报名",
confirmButtonColor: "#1867b0"
}).then(go).catch(() => {})
}).then(go).catch(() => {
this.signupCheckingMatterId = ""
})
} else {
vant.Dialog.alert({
title: "温馨提醒",
message: data.message || "当前不能报名",
confirmButtonColor: "#1867b0"
}).then(() => {
this.signupCheckingMatterId = ""
})
}
}).catch(() => {
this.signupCheckingMatterId = ""
})
},
revokeSignup(row) {
@@ -203,6 +203,7 @@ layout("/layouts/platform_h5.html"){
<van-cell title="姓名" :value="detail.userName || ''"></van-cell>
<van-cell title="性别" :value="detail.gender || ''"></van-cell>
<van-cell title="身份证号" :value="detail.idCard || ''"></van-cell>
<van-cell title="手机号" :value="detail.mobile || ''"></van-cell>
<van-cell title="所在单位" :value="detail.unitName || ''"></van-cell>
<van-cell title="所在工会" :value="detail.unionName || ''"></van-cell>
</div>
@@ -213,8 +214,8 @@ layout("/layouts/platform_h5.html"){
<van-cell title="线路类型" :value="detail.lineType || ''"></van-cell>
<van-cell title="出行时段" :value="detail.travelPeriod || ''"></van-cell>
<van-cell title="报名时间" :value="detail.signupTime || ''"></van-cell>
<van-cell title="报名酒店" :value="detail.hotelName || ''"></van-cell>
<van-cell title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
<van-cell v-if="false" title="报名酒店" :value="detail.hotelName || ''"></van-cell>
<van-cell v-if="false" title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="床型" :value="detail.bedType || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="床位信息" :value="detail.bedInfo || ''"></van-cell>
<van-cell v-if="fillBedInfo" title="意向拼床人" :value="detail.intendedRoommate || ''"></van-cell>