疗休养优化

This commit is contained in:
2026-06-14 17:21:05 +08:00
parent f94ca77ee3
commit f4f8387c1e
20 changed files with 733 additions and 281 deletions
@@ -5,7 +5,6 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.result.Result;
import com.budwk.app.sys.models.Sys_home_activity;
@@ -287,76 +286,67 @@ public class SysHomeController {
// @CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "web_news", isHash = true)
// @CacheResult(cacheKey = "news", ignoreNull = true, cacheLiveTime = 60 * 60 * 24)
public Result getNews() {
// 定义要爬取的URL
String domain = "https://cdgh.ncu.edu.cn/";
try {
// 使用Jsoup连接到URL并获取页面内容
Document doc = Jsoup.connect(domain).get();
JSONObject root = new JSONObject();
// 1. 新闻快讯(来自 .list1
List<JSONObject> newsArray = new ArrayList<>();
Elements newsItems = doc.select(".group1 .list1 li");
for (Element item : newsItems) {
String date = item.select(".date").text().trim();
Element link = item.select("a.text").first();
String title = link.select(".title").text().trim();
String url = link.attr("href").trim();
JSONObject node = new JSONObject();
node.set("title", title);
node.set("date", date);
node.set("url", domain + url);
newsArray.add(node);
}
root.set("news", newsArray);
// 2. 通知公告(来自 .list2
List<JSONObject> noticesArray = new ArrayList<>();
Elements noticeItems = doc.select(".group2 .list2 li");
for (Element item : noticeItems) {
String date = item.select(".date").text().trim();
Element link = item.select("a.title").first();
String title = link.text().trim();
String url = link.attr("href").trim();
JSONObject node = new JSONObject();
node.set("title", title);
node.set("date", date);
node.set("url", domain + url);
noticesArray.add(node);
}
root.set("notices", noticesArray);
// 3. 基层风采(来自 .pic-list1
List<JSONObject> grassrootsArray = new ArrayList<>();
Elements grassrootsItems = doc.select(".group3 .pic-list1 li");
for (Element item : grassrootsItems) {
Element link = item.select("a.img-scale").first();
String date = link.select(".date").text().trim();
String title = link.select(".title").text().trim();
String url = link.attr("href").trim();
String summary = link.select(".info").text().trim();
String imageUrl = "";
Element img = link.select("img").first();
if (img != null) {
imageUrl = img.attr("src").trim();
}
JSONObject node = new JSONObject();
node.set("title", title);
node.set("date", date);
node.set("url", domain + url);
node.set("summary", summary);
node.set("image", domain + imageUrl);
grassrootsArray.add(node);
}
root.set("grassroots", grassrootsArray);
return Result.success(root);
List<NutMap> news = new ArrayList<>();
news.add(NutMap.NEW().setv("label", "新闻快讯")
.setv("value", parseHomeNewsItems(domain, doc.select(".group1 .list1 li"), "a.text", ".title")));
news.add(NutMap.NEW().setv("label", "通知公告")
.setv("value", parseHomeNewsItems(domain, doc.select(".group2 .list2 li"), "a.title", "")));
news.add(NutMap.NEW().setv("label", "基层风采")
.setv("value", parseHomeNewsItems(domain, doc.select(".group3 .pic-list1 li"), "a.img-scale", ".title")));
return Result.success(news);
} catch (Exception e) {
log.error(e);
return Result.success();
return Result.success(Collections.emptyList());
}
}
/**
* Converts the NCU union website list nodes to the PC v4 home carousel structure.
* The migrated jcdt component renders label/value and item href/time/text fields.
*
* @param domain base website domain used to complete relative links
* @param items news list nodes parsed from the source page
* @param linkSelector selector for the clickable element inside each list node
* @param titleSelector selector for the title inside the clickable element, blank means using link text
* @return normalized news items for the PC v4 home page
*/
private List<NutMap> parseHomeNewsItems(String domain, Elements items, String linkSelector, String titleSelector) {
List<NutMap> rows = new ArrayList<>();
for (Element item : items) {
Element link = item.select(linkSelector).first();
if (link == null) {
continue;
}
String title = StrUtil.isBlank(titleSelector) ? link.text().trim() : link.select(titleSelector).text().trim();
String href = link.attr("href").trim();
String time = item.select(".date").text().trim();
rows.add(NutMap.NEW()
.setv("href", completeNewsUrl(domain, href))
.setv("time", time)
.setv("text", title));
}
return rows;
}
/**
* Completes relative news links while preserving absolute website links.
*
* @param domain base website domain
* @param href original href from the source page
* @return absolute link for opening the news detail page
*/
private String completeNewsUrl(String domain, String href) {
if (StrUtil.isBlank(href) || StrUtil.startWithIgnoreCase(href, "http://")
|| StrUtil.startWithIgnoreCase(href, "https://")) {
return href;
}
if (href.startsWith("/")) {
return domain + href.substring(1);
}
return domain + href;
}
}
@@ -193,14 +193,15 @@ public class TourGroupController {
t.id,
t.jobNo,
t.userName,
'' AS mobile,
t.idCard,
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile,
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS 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
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
$condition
ORDER BY $orderColumn $orderBy, t.signupTime DESC, t.createdAt DESC
""");
@@ -212,7 +213,6 @@ public class TourGroupController {
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);
@@ -300,8 +300,8 @@ public class TourGroupController {
t.id AS ledgerId,
t.jobNo,
t.userName,
t.idCard,
vu.mobile,
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard,
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile,
t.unionName,
l.lineName,
CASE
@@ -526,35 +526,6 @@ public class TourGroupController {
}
}
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", ""), ""));
}
}
private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId) {
Cnd cnd = Cnd.NEW();
cnd.and("m.delFlag", "=", false);
@@ -133,8 +133,8 @@ public class TourLedgerController {
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName,
COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS currentGender,
vu.age AS age,
COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS currentIdCard,
COALESCE(NULLIF(vu.mobile, ''), '') AS mobile,
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS currentIdCard,
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS currentMobile,
IFNULL(f.familyCount, 0) AS familyCount,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
@@ -188,6 +188,7 @@ public class TourLedgerController {
item.put("lineName", item.getString("currentLineName"));
item.put("gender", item.getString("currentGender"));
item.put("idCard", item.getString("currentIdCard"));
item.put("mobile", item.getString("currentMobile"));
});
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
@@ -206,6 +207,7 @@ public class TourLedgerController {
if (!canAccessLedgerUnion(ledger.getUnionId())) {
return Result.error("无权查看该台账记录");
}
fillLedgerContactFallback(ledger);
ledger.setLineName(getCurrentLineName(ledger));
Cnd familyCnd = Cnd.NEW();
familyCnd.and(TourLedgerFamily::getLedgerId, "=", id);
@@ -392,8 +394,8 @@ public class TourLedgerController {
t.userName,
COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS gender,
vu.age AS age,
COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS idCard,
COALESCE(NULLIF(vu.mobile, ''), '') AS mobile,
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard,
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile,
t.unitName,
t.boardingPlace,
COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName,
@@ -491,11 +493,11 @@ public class TourLedgerController {
t.id,
t.jobNo,
t.userName,
t.idCard,
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard,
t.unionId,
COALESCE(NULLIF(t.unionName, ''), su.name, vu.unionName, '') AS unionName,
COALESCE(NULLIF(su.unionCode, ''), vu.unionCode, '') AS unionCode,
COALESCE(NULLIF(vu.mobile, ''), '') AS mobile,
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile,
COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
@@ -948,6 +950,23 @@ public class TourLedgerController {
return StrUtil.isNotBlank(currentUnionId) && StrUtil.equals(currentUnionId, unionId);
}
private void fillLedgerContactFallback(TourLedger ledger) {
if (ledger == null || (StrUtil.isNotBlank(ledger.getIdCard()) && StrUtil.isNotBlank(ledger.getMobile()))) {
return;
}
// Detail views prefer ledger contact data; blank ledger fields fall back to the user profile snapshot for display only.
View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", ledger.getJobNo()));
if (user == null) {
return;
}
if (StrUtil.isBlank(ledger.getIdCard())) {
ledger.setIdCard(user.getIdCard());
}
if (StrUtil.isBlank(ledger.getMobile())) {
ledger.setMobile(user.getMobile());
}
}
private void appendApprovedWorkflowFilter(Cnd cnd) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.or("ins.id", "IS", null);
@@ -27,6 +27,7 @@ import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLine;
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
@@ -147,8 +148,13 @@ public class TourMySignupController {
(SELECT MAX(id) FROM wf_process_task WHERE processInstanceId = ins.id AND taskName = 'startTask' AND taskState IN (10, 20)) AS startTaskId,
IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0) AS directFamilyUnitLine,
dr.id AS directRelativeId,
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS currentIdCard,
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS currentMobile,
IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName,
IFNULL(f.familyCount, 0) AS familyCount,
a.id AS assignmentId,
IFNULL(a.cancelled, 0) AS assignmentCancelled,
m.travelStartTime AS actionTravelStartTime,
COALESCE(m.travelEndTime, tp.travelEndTime) AS travelEndTime,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
@@ -160,6 +166,7 @@ public class TourMySignupController {
LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
@@ -167,6 +174,10 @@ 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 tour_user_assignment a ON a.matterId = t.matterId
AND a.userId = @currentUserId
AND a.loginName = t.jobNo
AND a.delFlag = 0
LEFT JOIN (
SELECT
`year`,
@@ -192,13 +203,20 @@ public class TourMySignupController {
ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC
""");
listSql.setCondition(cnd);
listSql.setParam("currentUserId", SecurityUtil.getUserId());
listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName()));
listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
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));
List<NutMap> list = listSql.getList(NutMap.class);
list.forEach(item -> {
item.put("idCard", item.getString("currentIdCard"));
item.put("mobile", item.getString("currentMobile"));
});
appendMySignupActionFlags(list);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@@ -209,6 +227,7 @@ public class TourMySignupController {
if (ledger == null) {
return Result.error("报名记录不存在");
}
fillLedgerContactFallback(ledger);
Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false)
.and(TourLedgerFamily::getLedgerId, "=", ledger.getId());
familyCnd.asc(TourLedgerFamily::getCreatedAt);
@@ -279,6 +298,7 @@ public class TourMySignupController {
if (ledger == null) {
return Result.error("报名记录不存在");
}
fillLedgerContactFallback(ledger);
if (StrUtil.isBlank(ledger.getMatterId())) {
return Result.error("历史报名记录缺少事项信息,不能修改");
}
@@ -536,6 +556,29 @@ public class TourMySignupController {
return Result.success().addMsg("取消报名成功");
}
/**
* 移动端我的疗休养退出报名入口。
* 仅允许当前登录人操作自己的人员分配记录,具体退出截止天数和台账清理逻辑复用 service 层。
*
* @param assignmentId 人员分配记录ID
* @return 退出结果
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR)
public Result doLeaveTour(String assignmentId) {
TourUserAssignment assignment = fetchOwnAssignment(assignmentId);
if (assignment == null) {
return Result.error("人员分配记录不存在或无权操作");
}
try {
tourUserAssignmentService.cancelAssignment(assignment.getId());
return Result.success().addMsg("退出疗休养报名成功");
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR)
@@ -605,6 +648,55 @@ public class TourMySignupController {
.and(TourLedger::getJobNo, "=", currentJobNo()));
}
/**
* 查询当前登录人的人员分配记录,避免移动端传入他人分配ID进行退出。
*/
private TourUserAssignment fetchOwnAssignment(String assignmentId) {
if (StrUtil.isBlank(assignmentId)) {
return null;
}
return tourUserAssignmentService.fetch(Cnd.where(TourUserAssignment::getId, "=", assignmentId)
.and(TourUserAssignment::getUserId, "=", SecurityUtil.getUserId())
.and(TourUserAssignment::getLoginName, "=", currentJobNo())
.and(TourUserAssignment::getDelFlag, "=", false));
}
/**
* 给移动端我的疗休养列表追加操作按钮显示标识,退出截止时间与 service 层最终校验保持一致。
*/
private void appendMySignupActionFlags(List<NutMap> list) {
if (Lang.isEmpty(list)) {
return;
}
int cancelDeadlineDays = tourUserAssignmentService.getCancelDeadlineDays();
LocalDateTime now = LocalDateTime.now();
for (NutMap item : list) {
boolean assignmentCancelled = item.getBoolean("assignmentCancelled", false);
String assignmentId = item.getString("assignmentId", "");
item.put("cancelDeadlineDays", cancelDeadlineDays);
item.put("canLeaveTour", StrUtil.isNotBlank(assignmentId)
&& !assignmentCancelled
&& isWithinLeaveTimeRange(item.getString("actionTravelStartTime", ""), cancelDeadlineDays, now));
}
}
/**
* 判断当前时间是否处于“出行开始前N天”到“出行开始前”的可退出范围内。
*/
private boolean isWithinLeaveTimeRange(String travelStartTime, int cancelDeadlineDays, LocalDateTime now) {
try {
String normalized = normalizeDateTime(travelStartTime, false);
if (StrUtil.isBlank(normalized)) {
return false;
}
LocalDateTime startTime = LocalDateTime.parse(normalized, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
LocalDateTime deadline = startTime.minusDays(cancelDeadlineDays);
return !now.isBefore(deadline) && now.isBefore(startTime);
} catch (Exception e) {
return false;
}
}
private Result checkSignup(TourLedger ledger) {
if (ledger == null) {
return Result.error("参数错误");
@@ -1037,13 +1129,30 @@ public class TourMySignupController {
ledger.setJobNo(user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname()));
ledger.setUserName(user == null ? SecurityUtil.getUserUsername() : defaultIfBlank(user.getUsername(), SecurityUtil.getUserUsername()));
ledger.setGender(user == null ? ledger.getGender() : defaultIfBlank(user.getSex(), ledger.getGender()));
ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(user.getIdCard(), ledger.getIdCard()));
ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(ledger.getIdCard(), user.getIdCard()));
ledger.setUnitId(user == null ? SecurityUtil.getUnitId() : defaultIfBlank(user.getUnitId(), SecurityUtil.getUnitId()));
ledger.setUnitName(user == null ? ledger.getUnitName() : defaultIfBlank(user.getUnitName(), ledger.getUnitName()));
ledger.setUnionId(user == null ? SecurityUtil.getUnionId() : defaultIfBlank(user.getUnionId(), ledger.getUnionId()));
ledger.setUnionName(user == null ? ledger.getUnionName() : defaultIfBlank(user.getUnionName(), ledger.getUnionName()));
}
private void fillLedgerContactFallback(TourLedger ledger) {
if (ledger == null || (StrUtil.isNotBlank(ledger.getIdCard()) && StrUtil.isNotBlank(ledger.getMobile()))) {
return;
}
// My signup detail keeps ledger contact data first; blank contact fields use the user profile snapshot for display only.
View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", ledger.getJobNo()));
if (user == null) {
return;
}
if (StrUtil.isBlank(ledger.getIdCard())) {
ledger.setIdCard(user.getIdCard());
}
if (StrUtil.isBlank(ledger.getMobile())) {
ledger.setMobile(user.getMobile());
}
}
private void clearBedInfo(TourLedger ledger, List<TourLedgerFamily> familyList) {
if (ledger != null) {
ledger.setBedType("");
@@ -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;
@@ -171,6 +172,7 @@ public class TourSchoolUnionApprovalController {
if (ledger == null) {
return Result.error("报名记录不存在或无权查看");
}
fillLedgerContactFallback(ledger);
ledger.setLineName(getCurrentLineName(ledger));
Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())
.and(TourLedgerFamily::getDelFlag, "=", false);
@@ -465,6 +467,23 @@ public class TourSchoolUnionApprovalController {
return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null;
}
private void fillLedgerContactFallback(TourLedger ledger) {
if (ledger == null || (StrUtil.isNotBlank(ledger.getIdCard()) && StrUtil.isNotBlank(ledger.getMobile()))) {
return;
}
// Approval detail keeps ledger contact data first; blank contact fields use the user profile snapshot for display only.
View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", ledger.getJobNo()));
if (user == null) {
return;
}
if (StrUtil.isBlank(ledger.getIdCard())) {
ledger.setIdCard(user.getIdCard());
}
if (StrUtil.isBlank(ledger.getMobile())) {
ledger.setMobile(user.getMobile());
}
}
private String getCurrentLineName(TourLedger ledger) {
if (ledger == null) {
return "";
@@ -26,6 +26,7 @@ import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLine;
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
@@ -167,6 +168,8 @@ public class TourSignupController {
m.`year`,
m.unionId,
COALESCE(u.name, '校工会') AS unionName,
m.signupStartTime,
m.signupEndTime,
m.lineId,
l.lineName,
l.lineType,
@@ -185,6 +188,8 @@ public class TourSignupController {
my.instanceState,
my.taskKey,
my.startTaskId,
my.assignmentId,
IFNULL(my.assignmentCancelled, 0) AS assignmentCancelled,
IFNULL(my.canRevoke, 0) AS canRevoke,
CASE WHEN my.ledgerId IS NULL THEN 0 ELSE 1 END AS signed
FROM tour_matter m
@@ -217,9 +222,12 @@ public class TourSignupController {
MAX(ins.id) AS instanceId,
MAX(ins.state) AS instanceState,
MAX(task.taskName) AS taskKey,
MAX(ua.id) AS assignmentId,
MAX(IFNULL(ua.cancelled, 0)) AS assignmentCancelled,
MAX((SELECT MAX(st.id) FROM wf_process_task st WHERE st.processInstanceId = ins.id AND st.taskName = 'startTask' AND st.taskState IN (10, 20))) AS startTaskId,
MAX(IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = task.taskParentId) = 'startTask', 1, 0)) AS canRevoke
FROM tour_ledger t
LEFT JOIN tour_user_assignment ua ON ua.matterId = t.matterId AND ua.loginName = t.jobNo AND ua.delFlag = 0
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id AND ins.state <> @abandonState
LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10
WHERE t.delFlag = 0
@@ -240,17 +248,73 @@ public class TourSignupController {
listSql.setCallback(Sqls.callback.maps());
tourMatterService.dao().execute(listSql);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class));
List<NutMap> list = listSql.getList(NutMap.class);
appendH5LineActionFlags(list);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
/**
* 移动端退出疗休养入口,复用人员分配退出逻辑并限定只能操作当前登录人的分配记录。
*
* @param assignmentId 人员分配记录ID
* @return 退出结果
*/
@At("/h5/doLeaveTour")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("h5.tour.signup")
public Result h5DoLeaveTour(String assignmentId) {
TourUserAssignment assignment = fetchCurrentUserAssignment(assignmentId);
if (assignment == null) {
return Result.error("人员分配记录不存在或无权操作");
}
try {
tourUserAssignmentService.cancelAssignment(assignment.getId());
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 移动端取消当前线路,删除本人的报名台账并清空人员分配中的线路快照。
*
* @param ledgerId 报名台账ID
* @return 取消结果
*/
@At("/h5/doCancelLine")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("h5.tour.signup")
public Result h5DoCancelLine(String ledgerId) {
if (StrUtil.isBlank(ledgerId)) {
return Result.error("报名台账参数错误");
}
TourLedger ledger = tourLedgerService.fetch(Cnd.where(TourLedger::getId, "=", ledgerId)
.and(TourLedger::getJobNo, "=", currentJobNo())
.and(TourLedger::getDelFlag, "=", false));
if (ledger == null) {
return Result.error("报名台账不存在或无权操作");
}
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
Result signupTimeResult = checkSignupTime(matter);
if (signupTimeResult != null) {
return signupTimeResult;
}
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.delete(ledger.getId());
tourUserAssignmentService.clearExistingAssignmentMatterAfterLedgerDelete(ledger.getMatterId(), ledger.getJobNo());
return Result.success();
}
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) {
public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId, String travelPeriod) {
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
pageForm.defaultSortAsc("lineName");
}
Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId);
appendTravelPeriodCnd(cnd, travelPeriod);
String currentJobNo = currentJobNo();
Sql countSql = Sqls.create("""
@@ -375,9 +439,10 @@ public class TourSignupController {
}
/**
* 查询移动端选择路线页的出行时间筛选项,按 yyyyMMdd-yyyyMMdd 格式拼接事项出行起止时间并去重。
* Queries distinct travel periods for PC signup and H5 line selection.
* The value uses yyyyMMdd-yyyyMMdd so the frontend can pass it back as a stable filter key.
*/
@At("/h5/travelPeriodOptions")
@At({"/h5/travelPeriodOptions", "/travelPeriodOptions"})
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result h5TravelPeriodOptions(Integer year, String lineType, Boolean directFamilyUnitLine) {
Cnd cnd = buildQueryCnd(year, null, null, null);
@@ -490,20 +555,11 @@ public class TourSignupController {
.and(TourLedger::getJobNo, "=", staff.getString("jobNo"));
ledgerCnd.desc(TourLedger::getCreatedAt);
TourLedger ledger = tourLedgerService.fetch(ledgerCnd);
TourMatter matterEntity = tourMatterService.fetch(matter.getString("matterId"));
TourLedger ruleLedger = new TourLedger();
ruleLedger.setId(ledger == null ? null : ledger.getId());
ruleLedger.setYear(matterEntity == null ? matter.getInt("year") : matterEntity.getYear());
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;
}
// Viewing a line should only load detail data; signup rules are enforced when the user starts or submits signup.
List<TourLedgerFamily> families = Collections.emptyList();
TourLedgerDirectRelative directRelative = null;
if (ledger != null) {
fillLedgerContactFallback(ledger, staff);
Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false)
.and(TourLedgerFamily::getLedgerId, "=", ledger.getId());
familyCnd.asc(TourLedgerFamily::getCreatedAt);
@@ -793,7 +849,7 @@ public class TourSignupController {
return workflowResult;
}
}
return Result.success().addMsg(approvalRequired ? (update ? "修改已提交,等待审核" : "申请已提交,等待审核") : (update ? "修改成功" : "报名成功"));
return Result.success().addMsg(approvalRequired ? (update ? "修改已提交,等待审核" : "申请已提交,等待审核") : (update ? "修改成功" : "恭喜您已报名成功"));
}
@At
@@ -1199,6 +1255,65 @@ public class TourSignupController {
return setting != null && "ASSIGNED_USER".equals(setting.getSignupEligibilityMode());
}
private void appendH5LineActionFlags(List<NutMap> list) {
if (Lang.isEmpty(list)) {
return;
}
int cancelDeadlineDays = tourUserAssignmentService.getCancelDeadlineDays();
LocalDateTime now = LocalDateTime.now();
for (NutMap item : list) {
boolean signed = item.getBoolean("signed", false);
boolean assignmentCancelled = item.getBoolean("assignmentCancelled", false);
String assignmentId = item.getString("assignmentId", "");
String ledgerId = item.getString("ledgerId", "");
// Button visibility is calculated on the server so H5 follows the same time rules as backend submit actions.
item.put("cancelDeadlineDays", cancelDeadlineDays);
item.put("canLeaveTour", signed && StrUtil.isNotBlank(assignmentId) && !assignmentCancelled
&& isWithinLeaveTimeRange(item.getString("travelStartTime", ""), cancelDeadlineDays, now));
item.put("canCancelLine", signed && StrUtil.isNotBlank(ledgerId)
&& isWithinSignupRange(item.getString("signupStartTime", ""), item.getString("signupEndTime", ""), now));
}
}
private boolean isWithinLeaveTimeRange(String travelStartTime, int cancelDeadlineDays, LocalDateTime now) {
LocalDateTime startTime = parseDateTimeValue(travelStartTime, false);
if (startTime == null) {
return false;
}
LocalDateTime deadline = startTime.minusDays(cancelDeadlineDays);
return !now.isBefore(deadline) && now.isBefore(startTime);
}
private boolean isWithinSignupRange(String signupStartTime, String signupEndTime, LocalDateTime now) {
LocalDateTime startTime = parseDateTimeValue(signupStartTime, false);
LocalDateTime endTime = parseDateTimeValue(signupEndTime, true);
if (startTime == null || endTime == null) {
return false;
}
return !now.isBefore(startTime) && !now.isAfter(endTime);
}
private LocalDateTime parseDateTimeValue(String value, boolean endOfDay) {
String normalized = normalizeDateTime(value, endOfDay);
if (StrUtil.isBlank(normalized)) {
return null;
}
try {
return LocalDateTime.parse(normalized.substring(0, 19), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} catch (Exception e) {
return null;
}
}
private TourUserAssignment fetchCurrentUserAssignment(String assignmentId) {
if (StrUtil.isBlank(assignmentId)) {
return null;
}
return tourLedgerService.dao().fetch(TourUserAssignment.class, Cnd.where(TourUserAssignment::getId, "=", assignmentId)
.and(TourUserAssignment::getUserId, "=", SecurityUtil.getUserId())
.and(TourUserAssignment::getDelFlag, "=", false));
}
/**
* 统一校验疗休养事项报名时间,确保资格名单校验在时间校验通过后再执行。
*/
@@ -1497,13 +1612,26 @@ public class TourSignupController {
ledger.setJobNo(user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname()));
ledger.setUserName(user == null ? SecurityUtil.getUserUsername() : defaultIfBlank(user.getUsername(), SecurityUtil.getUserUsername()));
ledger.setGender(user == null ? ledger.getGender() : defaultIfBlank(user.getSex(), ledger.getGender()));
ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(user.getIdCard(), ledger.getIdCard()));
ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(ledger.getIdCard(), user.getIdCard()));
ledger.setUnitId(user == null ? SecurityUtil.getUnitId() : defaultIfBlank(user.getUnitId(), SecurityUtil.getUnitId()));
ledger.setUnitName(user == null ? ledger.getUnitName() : defaultIfBlank(user.getUnitName(), ledger.getUnitName()));
ledger.setUnionId(user == null ? SecurityUtil.getUnionId() : defaultIfBlank(user.getUnionId(), SecurityUtil.getUnionId()));
ledger.setUnionName(user == null ? ledger.getUnionName() : defaultIfBlank(user.getUnionName(), ledger.getUnionName()));
}
private void fillLedgerContactFallback(TourLedger ledger, NutMap staff) {
if (ledger == null || staff == null) {
return;
}
// Signup detail keeps saved ledger contact data first; blank contact fields use the user profile snapshot for display only.
if (StrUtil.isBlank(ledger.getIdCard())) {
ledger.setIdCard(staff.getString("idCard", ""));
}
if (StrUtil.isBlank(ledger.getMobile())) {
ledger.setMobile(staff.getString("mobile", ""));
}
}
private List<TourLedgerFamily> parseFamilies(String families) {
if (StrUtil.isBlank(families)) {
return Collections.emptyList();
@@ -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;
@@ -171,6 +172,7 @@ public class TourUnionApprovalController {
if (ledger == null) {
return Result.error("报名记录不存在或无权查看");
}
fillLedgerContactFallback(ledger);
ledger.setLineName(getCurrentLineName(ledger));
Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())
.and(TourLedgerFamily::getDelFlag, "=", false);
@@ -465,6 +467,23 @@ public class TourUnionApprovalController {
return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null;
}
private void fillLedgerContactFallback(TourLedger ledger) {
if (ledger == null || (StrUtil.isNotBlank(ledger.getIdCard()) && StrUtil.isNotBlank(ledger.getMobile()))) {
return;
}
// Approval detail keeps ledger contact data first; blank contact fields use the user profile snapshot for display only.
View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", ledger.getJobNo()));
if (user == null) {
return;
}
if (StrUtil.isBlank(ledger.getIdCard())) {
ledger.setIdCard(user.getIdCard());
}
if (StrUtil.isBlank(ledger.getMobile())) {
ledger.setMobile(user.getMobile());
}
}
private String getCurrentLineName(TourLedger ledger) {
if (ledger == null) {
return "";
@@ -416,9 +416,9 @@ public class TourUnionLedgerController {
SELECT
t.id,
t.userName,
t.idCard,
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard,
t.unionName,
vu.mobile,
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile,
m.travelStartTime,
m.travelEndTime,
m.estimatedCost,
@@ -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)
@@ -272,6 +272,13 @@ public interface TourUserAssignmentService extends BaseService<TourUserAssignmen
*/
void cancelAssignment(String id);
/**
* 查询疗休养退出取消的出行开始前限制天数供移动端按钮展示和实际退出校验保持一致
*
* @return 出行开始前允许退出的天数
*/
int getCancelDeadlineDays();
/**
* 将人员分配记录从已退出恢复为未退出
* 只恢复人员分配表状态不自动恢复已删除的报名台账
@@ -886,12 +886,15 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl<TourUserAssig
LocalDateTime travelStartTime = parseTravelStartTime(matter.getTravelStartTime());
int days = getCancelDeadlineDays();
LocalDateTime deadline = travelStartTime.minusDays(days);
if (LocalDateTime.now().isAfter(deadline)) {
throw new IllegalArgumentException("当前路线已超过取消时间,需在出行开始前" + days + "天取消");
LocalDateTime now = LocalDateTime.now();
// 退出报名仅允许在出行开始前N天出行开始前之间办理出行开始时刻不能再退出
if (now.isBefore(deadline) || !now.isBefore(travelStartTime)) {
throw new IllegalArgumentException("当前不在退出时间范围内,需在出行开始前" + days + "天至出行开始前办理");
}
}
private int getCancelDeadlineDays() {
@Override
public int getCancelDeadlineDays() {
try {
Sys_config config = sysConfigService == null ? null : sysConfigService.getValueByKey(TOUR_CANCEL_DEADLINE_DAYS_KEY);
String value = config == null ? "" : config.getConfigValue();
@@ -0,0 +1,3 @@
-- Add signup ledger mobile field; signup mobile is stored with the ledger and does not update user profile data.
ALTER TABLE `tour_ledger`
ADD COLUMN `mobile` varchar(30) DEFAULT NULL COMMENT '手机号' AFTER `idCard`;
@@ -128,7 +128,7 @@ layout("/layouts/v4/baseLayout.html"){
</div>
</div>
<work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
<!-- <work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>-->
<jcdt :list="websiteNews"></jcdt>
</div>
@@ -80,7 +80,7 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
<el-table-column label="分配时间" prop="assignedAt" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="320" align="center" header-align="center" fixed="right">
<el-table-column label="操作" width="380" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button v-if="!row.matterId && row.personType !== 'BACKUP'" size="mini" type="primary" @click="openSelectMatter(row)">选择路线</el-button>
<el-button size="mini" type="primary" :loading="personTypeSwitching === row.id" @click="switchPersonType(row, row.personType === 'BACKUP' ? 'FORMAL' : 'BACKUP')">{{ row.personType === 'BACKUP' ? '转为正式' : '转为替补' }}</el-button>
@@ -42,7 +42,23 @@ layout("/layouts/platform.html"){
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="线路列表"></table-tool>
<div class="tour-list-header">
<table-tool :app="this" label="线路列表"></table-tool>
<div class="tour-period-area" v-if="travelPeriodOptions.length > 0">
<div class="tour-period-filter">
<button
v-for="item in travelPeriodOptions"
:key="item.value"
type="button"
class="tour-period-chip"
:class="{'tour-period-chip--active': pageForm.travelPeriod === item.value}"
@click="selectTravelPeriod(item)">
{{ item.value }}
</button>
</div>
<div class="tour-period-tip">可选择指定出行时间,快速查看对应路线</div>
</div>
</div>
<el-table
v-loading="tableLoading"
@@ -470,6 +486,62 @@ layout("/layouts/platform.html"){
.over-cost-form-item .el-radio__input.is-checked + .el-radio__label {
color: #f56c6c;
}
.tour-list-header {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
}
.tour-list-header /deep/ .wk-table-tool,
.tour-list-header .wk-table-tool {
flex: 0 0 auto;
}
.tour-period-area {
flex: 1 1 0;
min-width: 240px;
}
.tour-period-filter {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 0;
padding: 0;
}
.tour-period-tip {
margin-top: 6px;
color: #f56c6c;
font-size: 12px;
line-height: 1.4;
}
.tour-period-chip {
min-width: 118px;
height: 34px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 14px;
background: #ffffff;
color: #606266;
font-size: 14px;
line-height: 32px;
text-align: center;
cursor: pointer;
box-sizing: border-box;
transition: all 0.18s ease;
}
.tour-period-chip:hover,
.tour-period-chip--active {
border-color: #0076c8;
background: #0076c8;
color: #ffffff;
}
</style>
<script nonce="${cspNonce!}">
@@ -481,6 +553,7 @@ layout("/layouts/platform.html"){
return {
unionOptions: [],
lineTypeOptions: [],
travelPeriodOptions: [],
signupVisible: false,
signupLoading: false,
cancelLoading: false,
@@ -505,17 +578,36 @@ layout("/layouts/platform.html"){
year: currentYear,
lineName: "",
lineType: "",
unionId: ""
unionId: "",
travelPeriod: ""
}
}
},
methods: {
resetSearch() {
this.pageForm.year = moment().format("YYYY")
const currentYear = moment().format("YYYY")
const yearChanged = this.pageForm.year !== currentYear
this.pageForm.year = currentYear
this.pageForm.lineName = ""
this.pageForm.lineType = ""
this.pageForm.unionId = ""
this.pageForm.travelPeriod = ""
if (yearChanged) {
return
}
this.loadLineTypeOptions()
this.loadTravelPeriodOptions(true).then(() => {
this.doSearch()
})
},
selectTravelPeriod(item) {
const travelPeriod = item && item.value ? item.value : ""
if (this.pageForm.travelPeriod === travelPeriod) {
this.pageForm.travelPeriod = ""
this.doSearch()
return
}
this.pageForm.travelPeriod = travelPeriod
this.doSearch()
},
signupActionText(row) {
@@ -1026,6 +1118,31 @@ layout("/layouts/platform.html"){
}
})
},
loadTravelPeriodOptions(defaultFirst) {
return this.$axios.post(loc() + "/travelPeriodOptions", {
year: this.pageForm.year
}).then((res) => {
if (res.code !== 0) {
return
}
this.travelPeriodOptions = (res.data || []).map((item) => {
const value = item.travelPeriod || ""
return {
value: value,
name: value || "未设置出行时间"
}
}).filter((item) => item.value).sort((a, b) => {
return String(a.value).localeCompare(String(b.value))
})
if (defaultFirst) {
this.pageForm.travelPeriod = this.travelPeriodOptions.length > 0 ? this.travelPeriodOptions[0].value : ""
}
}).catch(() => {
if (defaultFirst) {
this.pageForm.travelPeriod = ""
}
})
},
loadDirectRelativeOptions() {
this.$axios.post(loc() + "/directRelativeOptions").then((res) => {
if (res.code === 0) {
@@ -1054,12 +1171,18 @@ layout("/layouts/platform.html"){
this.loadDirectRelativeOptions()
this.loadBedTypeOptions()
this.loadFamilyRelationshipOptions()
this.pageData()
this.loadTravelPeriodOptions(true).then(() => {
this.pageData()
})
},
watch: {
"pageForm.year"() {
this.pageForm.lineType = ""
this.pageForm.travelPeriod = ""
this.loadLineTypeOptions()
this.loadTravelPeriodOptions(true).then(() => {
this.doSearch()
})
}
}
})
@@ -378,6 +378,7 @@ layout("/layouts/platform_h5.html"){
<van-button size="small" type="info" plain @click="openView(row)">查看</van-button>
<van-button v-if="canModify(row) || isTravelEnded(row)" size="small" type="info" :disabled="isTravelEnded(row)" @click="openSignup(row)">修改</van-button>
<van-button v-if="canRevoke(row)" size="small" type="danger" plain @click="revokeSignup(row)">撤回</van-button>
<van-button v-if="showLeaveTour(row)" size="small" type="danger" plain :loading="leaveLoading" @click="leaveTour(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>
@@ -584,6 +585,7 @@ layout("/layouts/platform_h5.html"){
signupVisible: false,
signupLoading: false,
cancelLoading: false,
leaveLoading: false,
allowFamily: false,
fillBedInfo: true,
signupForm: {},
@@ -733,6 +735,10 @@ layout("/layouts/platform_h5.html"){
showCancel(row) {
return !this.isApprovalRow(row) || this.canModify(row)
},
// 退出疗休养报名依赖人员分配记录,已退出的数据不再展示入口。
showLeaveTour(row) {
return this.toBoolean(row && row.canLeaveTour)
},
showExport(row) {
return this.toBoolean(row && row.overCostReimbursed)
},
@@ -1075,6 +1081,32 @@ layout("/layouts/platform_h5.html"){
})
}).catch(() => {})
},
leaveTour(row) {
if (!row || !row.assignmentId) {
vant.Toast("暂无可退出的疗休养报名")
return
}
vant.Dialog.confirm({
title: "温馨提醒",
message: "是否确认退出本次疗休养报名,退出后将取消报名资格!",
confirmButtonColor: "#ee2f2f"
}).then(() => {
this.leaveLoading = true
this.$axios.post("/platform/tour/mysignup/doLeaveTour", {
assignmentId: row.assignmentId
}).then((res) => {
this.leaveLoading = false
if (res.code === 0) {
vant.Toast.success(res.msg || "退出疗休养报名成功")
this.doSearch()
} else {
vant.Dialog.alert({ title: "提示", message: res.msg || "退出失败", confirmButtonColor: "#1867b0" })
}
}).catch(() => {
this.leaveLoading = false
})
}).catch(() => {})
},
revokeSignup(row) {
vant.Dialog.confirm({
title: "温馨提醒",
@@ -1106,6 +1138,7 @@ layout("/layouts/platform_h5.html"){
this.fillBedInfo = true
this.signupLoading = false
this.cancelLoading = false
this.leaveLoading = false
}
},
created() {
@@ -128,10 +128,10 @@ 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="身份证号码" required v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号码"></van-field>
<van-field label="性别" readonly v-model="signupForm.gender"></van-field>
<van-field label="年龄" readonly :value="staffAge(signupForm)"></van-field>
<van-field label="手机号" v-model="signupForm.mobile" type="tel" maxlength="30" placeholder="请填写手机号"></van-field>
<van-field label="手机号" required v-model="signupForm.mobile" type="tel" maxlength="30" placeholder="请填写手机号"></van-field>
<van-field label="所属工会" readonly v-model="signupForm.unionName"></van-field>
</div>
@@ -142,6 +142,7 @@ layout("/layouts/platform_h5.html"){
<van-field v-if="signupForm.travelPeriod" label="出行时间" readonly v-model="signupForm.travelPeriod"></van-field>
<van-field
label="乘车地点"
required
readonly
clickable
is-link
@@ -620,6 +621,27 @@ layout("/layouts/platform_h5.html"){
}
return true
},
showSubmitError(error) {
// Keep signup submit errors local so the global axios interceptor does not show a duplicate toast.
vant.Toast.clear()
const responseData = error && error.response && error.response.data ? error.response.data : null
const message = responseData && (responseData.msg || responseData.message)
? (responseData.msg || responseData.message)
: (error && (error.msg || error.message) ? (error.msg || error.message) : "报名提交失败")
vant.Toast({
message: message,
duration: 3000
})
},
submitSignupRequest(form) {
// Use raw axios for this submit only; the page controls backend error text and toast duration locally.
return axios.post("/platform/tour/signup/doSignup", $.param(form), {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"x-requested-with": "XMLHttpRequest"
}
})
},
submitSignup() {
if (this.pageLoading || this.submitLoading) return
if (this.boardingPlaceOptions.length > 0 && !this.signupForm.boardingPlace) {
@@ -634,26 +656,23 @@ layout("/layouts/platform_h5.html"){
directRelative: this.isDirectFamilyLine(this.signupForm) ? JSON.stringify(this.directRelativeForm) : ""
})
this.submitLoading = true
this.$axios.post("/platform/tour/signup/doSignup", form).then((res) => {
this.submitSignupRequest(form).then((resp) => {
this.submitLoading = false
const res = resp.data || {}
if (res.code === 0) {
vant.Dialog.alert({
title: "提示",
message: res.msg || "报名成功",
message: res.msg || "恭喜您已报名成功",
confirmButtonColor: "#1867b0"
}).then(() => {
window.location.replace("/platform/tour/signup/h5/signup")
})
} else {
vant.Dialog.alert({
title: "提示",
message: res.msg || "报名失败",
confirmButtonColor: "#1867b0"
})
this.showSubmitError(res)
}
}).catch(() => {
}).catch((error) => {
this.submitLoading = false
vant.Toast("报名提交失败")
this.showSubmitError(error)
})
}
},
@@ -229,7 +229,7 @@ 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 @click="handleDetailAction">
{{ detailActionText() }}
</van-button>
</div>
@@ -311,13 +311,7 @@ layout("/layouts/platform_h5.html"){
detailActionType() {
return this.canRevokeSignup() ? "danger" : "info"
},
detailActionDisabled() {
return this.isSigned() && this.isApprovalSignup() && (this.isFinishedSignup() || this.isRejectedSignup() || (!this.canRevokeSignup() && !this.canModifySignup()))
},
handleDetailAction() {
if (this.detailActionDisabled()) {
return
}
if (this.canRevokeSignup()) {
this.revokeSignup()
return
@@ -522,39 +516,8 @@ layout("/layouts/platform_h5.html"){
vant.Toast("报名事项信息不完整")
return
}
const goApply = () => {
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(this.matterId)
}
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"
})
return
}
const data = res.data || {}
if (!data.noticeRequired) {
goApply()
return
}
if (data.canApply) {
vant.Dialog.confirm({
title: "温馨提醒",
message: data.message || "当前可报名,是否继续报名?",
confirmButtonText: "继续报名",
cancelButtonText: "稍后报名",
confirmButtonColor: "#1867b0"
}).then(goApply).catch(() => {})
} else {
vant.Dialog.alert({
title: "温馨提醒",
message: data.message || "当前不能报名",
confirmButtonColor: "#1867b0"
})
}
})
// 线路详情底部按钮不做报名资格前置校验,所有业务规则统一放到最终提交报名时处理。
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(this.matterId)
},
revokeSignup() {
vant.Dialog.confirm({
@@ -326,13 +326,12 @@ layout("/layouts/platform_h5.html"){
}
.tour-line-title-row {
display: flex;
align-items: center;
gap: 6px;
position: relative;
padding-right: 76px;
min-width: 0;
}
.tour-line-title {
.tour-line-title-left {
min-width: 0;
color: #0f172a;
font-size: 16px;
@@ -341,9 +340,15 @@ layout("/layouts/platform_h5.html"){
word-break: break-word;
}
.tour-line-title {
display: inline;
}
.tour-line-type-badge {
height: 18px;
flex-shrink: 0;
display: inline-block;
margin-left: 5px;
vertical-align: 2px;
padding: 0 5px;
border: 1px solid #ffd6a8;
border-radius: 4px;
@@ -355,6 +360,21 @@ layout("/layouts/platform_h5.html"){
box-sizing: border-box;
}
.tour-line-signup-count {
position: absolute;
top: 0;
right: 0;
height: 22px;
padding: 0 7px;
border-radius: 11px;
background: #d9fde8;
color: #2fcf6b;
font-size: 11px;
font-weight: 700;
line-height: 22px;
white-space: nowrap;
}
.tour-line-tags {
display: flex;
flex-wrap: wrap;
@@ -389,7 +409,9 @@ layout("/layouts/platform_h5.html"){
.tour-line-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
margin-top: 8px;
}
@@ -410,6 +432,12 @@ layout("/layouts/platform_h5.html"){
background: #ee2f2f;
}
.tour-line-action--plain {
border-color: #1e88e5;
background: #ffffff;
color: #1e88e5;
}
.tour-line-action--disabled {
border-color: #d1d5db;
background: #e5e7eb;
@@ -509,6 +537,7 @@ layout("/layouts/platform_h5.html"){
<van-list
v-model="loading"
:finished="finished"
:immediate-check="false"
finished-text="没有更多了"
@load="loadData">
<div class="tour-line-list">
@@ -526,11 +555,14 @@ layout("/layouts/platform_h5.html"){
</div>
<div class="tour-line-main">
<div class="tour-line-title-row">
<div class="tour-line-title">{{ row.lineName || '未命名线路' }}</div>
<span v-if="lineTypeBadge(row.lineType)" class="tour-line-type-badge">{{ lineTypeBadge(row.lineType) }}</span>
<span v-if="isSigned(row)" class="tour-line-signed-icon">
<van-icon name="good-job"></van-icon>
</span>
<div class="tour-line-title-left">
<div class="tour-line-title">{{ row.lineName || '未命名线路' }}</div>
<span v-if="lineTypeBadge(row.lineType)" class="tour-line-type-badge">{{ lineTypeBadge(row.lineType) }}</span>
<span v-if="isSigned(row)" class="tour-line-signed-icon">
<van-icon name="good-job"></van-icon>
</span>
</div>
<span v-if="hasSignupCount(row)" class="tour-line-signup-count">已报名 {{ row.signupCount }}人</span>
</div>
<div class="tour-line-tags">
<span v-if="row.lotName" class="tour-line-tag">{{ row.lotName }}</span>
@@ -556,6 +588,20 @@ layout("/layouts/platform_h5.html"){
@click.stop="handleSignupAction(row)">
{{ signupActionText(row) }}
</button>
<button
v-if="showLeaveTourAction(row)"
type="button"
class="tour-line-action tour-line-action--danger"
@click.stop="leaveTour(row)">
退出疗休养
</button>
<button
v-if="showCancelLineAction(row)"
type="button"
class="tour-line-action tour-line-action--plain"
@click.stop="cancelLine(row)">
取消本线路
</button>
</div>
</div>
</div>
@@ -596,7 +642,6 @@ layout("/layouts/platform_h5.html"){
list: [],
loading: false,
finished: false,
lineChecking: false,
showUnionPopup: false,
unionList: [
{id: "", name: "全部分工会"}
@@ -680,6 +725,9 @@ layout("/layouts/platform_h5.html"){
isSigned(row) {
return !!(row && (row.signed === true || row.signed === 1 || row.signed === "1" || row.ledgerId))
},
hasSignupCount(row) {
return row && row.signupCount !== undefined && row.signupCount !== null && row.signupCount !== ""
},
isApprovalSignup(row) {
return !!(row && this.isSigned(row) && row.instanceId)
},
@@ -714,7 +762,13 @@ layout("/layouts/platform_h5.html"){
return "报名"
},
showSignupAction(row) {
return this.isSigned(row) && this.canModifySignup(row)
return false
},
showLeaveTourAction(row) {
return this.isSigned(row) && this.toBoolean(row && row.canLeaveTour) && !!(row && row.assignmentId)
},
showCancelLineAction(row) {
return this.isSigned(row) && this.toBoolean(row && row.canCancelLine) && !!(row && row.ledgerId)
},
signupActionDisabled(row) {
return !!(this.isSigned(row) && this.isApprovalSignup(row) && (this.isRejectedSignup(row) || (!this.canRevokeSignup(row) && !this.canModifySignup(row))))
@@ -737,39 +791,8 @@ layout("/layouts/platform_h5.html"){
vant.Toast("报名事项信息不完整")
return
}
const go = () => {
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(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"
})
return
}
const data = res.data || {}
if (!data.noticeRequired) {
go()
return
}
if (data.canApply) {
vant.Dialog.confirm({
title: "温馨提醒",
message: data.message || "当前可报名,是否继续报名?",
confirmButtonText: "继续报名",
cancelButtonText: "稍后报名",
confirmButtonColor: "#1867b0"
}).then(go).catch(() => {})
} else {
vant.Dialog.alert({
title: "温馨提醒",
message: data.message || "当前不能报名",
confirmButtonColor: "#1867b0"
})
}
})
// 进入报名页不再提前做资格校验,所有业务校验统一放到最终提交报名时处理。
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(row.matterId)
},
revokeSignup(row) {
vant.Dialog.confirm({
@@ -787,6 +810,46 @@ layout("/layouts/platform_h5.html"){
})
}).catch(() => {})
},
leaveTour(row) {
if (!row || !row.assignmentId) {
vant.Toast("人员分配信息不完整")
return
}
vant.Dialog.confirm({
title: "提示",
message: "是否确认退出本次疗休养,退出后将取消报名资格!",
confirmButtonColor: "#ee2f2f"
}).then(() => {
this.$axios.post("/platform/tour/signup/h5/doLeaveTour", { assignmentId: row.assignmentId }).then((res) => {
if (res.code === 0) {
vant.Toast.success(res.msg || "退出成功")
this.doSearch()
} else {
vant.Dialog.alert({ title: "提示", message: res.msg || "退出失败", confirmButtonColor: "#1867b0" })
}
})
}).catch(() => {})
},
cancelLine(row) {
if (!row || !row.ledgerId) {
vant.Toast("报名台账信息不完整")
return
}
vant.Dialog.confirm({
title: "提示",
message: "是否确认取消本线路?",
confirmButtonColor: "#ee2f2f"
}).then(() => {
this.$axios.post("/platform/tour/signup/h5/doCancelLine", { ledgerId: row.ledgerId }).then((res) => {
if (res.code === 0) {
vant.Toast.success(res.msg || "取消成功")
this.doSearch()
} else {
vant.Dialog.alert({ title: "提示", message: res.msg || "取消失败", confirmButtonColor: "#1867b0" })
}
})
}).catch(() => {})
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
@@ -799,16 +862,18 @@ layout("/layouts/platform_h5.html"){
this.pageForm.lineType = active ? "" : lineType
this.pageForm.directFamilyUnitLine = null
this.pageForm.travelPeriod = ""
this.loadTravelPeriodOptions()
this.doSearch()
this.loadTravelPeriodOptions(true).then(() => {
this.doSearch()
})
},
toggleDirectFamilyLine() {
const active = this.pageForm.directFamilyUnitLine === true
this.pageForm.directFamilyUnitLine = active ? null : true
this.pageForm.lineType = ""
this.pageForm.travelPeriod = ""
this.loadTravelPeriodOptions()
this.doSearch()
this.loadTravelPeriodOptions(true).then(() => {
this.doSearch()
})
},
openUnionPopup() {
this.showUnionPopup = true
@@ -849,8 +914,8 @@ layout("/layouts/platform_h5.html"){
}))
})
},
loadTravelPeriodOptions() {
this.$axios.post("/platform/tour/signup/h5/travelPeriodOptions", {
loadTravelPeriodOptions(defaultFirst) {
return this.$axios.post("/platform/tour/signup/h5/travelPeriodOptions", {
year: this.pageForm.year,
lineType: this.pageForm.lineType,
directFamilyUnitLine: this.pageForm.directFamilyUnitLine
@@ -865,8 +930,15 @@ layout("/layouts/platform_h5.html"){
name: item.travelPeriod || "未设置出行时间"
}
}).filter((item) => item.value).sort((a, b) => {
return String(b.value).localeCompare(String(a.value))
return String(a.value).localeCompare(String(b.value))
})
if (defaultFirst) {
this.pageForm.travelPeriod = this.travelPeriodList.length > 0 ? this.travelPeriodList[0].value : ""
}
}).catch(() => {
if (defaultFirst) {
this.pageForm.travelPeriod = ""
}
})
},
loadData() {
@@ -901,44 +973,18 @@ layout("/layouts/platform_h5.html"){
vant.Toast("线路信息不完整")
return
}
if (this.lineChecking) {
return
}
this.lineChecking = true
axios.post("/platform/tour/signup/signupDetail", $.param({ matterId: row.matterId }), {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"x-requested-with": "XMLHttpRequest"
}
}).then((resp) => {
this.lineChecking = false
const res = resp.data || {}
if (res.code !== 0) {
vant.Dialog.alert({
title: "温馨提醒",
message: res.msg || "当前线路暂不能报名",
confirmButtonColor: "#1867b0"
})
return
}
window.location.href = "/platform/tour/signup/h5/lineInfo?matterId="
+ encodeURIComponent(row.matterId)
+ "&lineId="
+ encodeURIComponent(row.lineId)
}).catch((err) => {
this.lineChecking = false
vant.Dialog.alert({
title: "温馨提醒",
message: err && err.msg ? err.msg : "当前线路暂不能报名",
confirmButtonColor: "#1867b0"
})
})
// 点击线路只进入详情页,报名资格和业务规则统一在提交报名按钮处校验。
window.location.href = "/platform/tour/signup/h5/lineInfo?matterId="
+ encodeURIComponent(row.matterId)
+ "&lineId="
+ encodeURIComponent(row.lineId)
}
},
created() {
this.loadUnionOptions()
this.loadTravelPeriodOptions()
this.loadData()
this.loadTravelPeriodOptions(true).then(() => {
this.loadData()
})
}
})
</script>
@@ -575,11 +575,7 @@ const home = {
.quick-scroll {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
-ms-overflow-style: none;
overflow: visible;
}
.quick-scroll::-webkit-scrollbar {
@@ -588,10 +584,9 @@ const home = {
.quick-scroll-grid {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(2, 82px);
grid-auto-columns: 25%;
min-width: 100%;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-auto-rows: 82px;
width: 100%;
}
.quick-entry-item {