This commit is contained in:
zhouhefeng
2026-05-20 08:54:05 +08:00
parent b3a84786c3
commit 3f76d2c6cb
105 changed files with 28881 additions and 296 deletions
@@ -213,6 +213,7 @@ public class FlowTodoCenterController {
public Result statistics() {
// 查询我发起的流程
int startedCount = dao.count(ProcessInstance.class, Cnd.where(ProcessInstance::getOperator, "=", SecurityUtil.getUserId()));
int completedCount = dao.count(ProcessInstance.class, Cnd.where(ProcessInstance::getOperator, "=", SecurityUtil.getUserId()).and(ProcessInstance::getState, "=", 20));
// 查询我的待办任务
Sql todoSql = Sqls.create("""
SELECT
@@ -262,7 +263,7 @@ public class FlowTodoCenterController {
dao.execute(msgSql);
int msgCount = msgSql.getInt();
return Result.success(Map.of("startedCount", startedCount, "todoCount", todoCount, "doneCount", doneCount, "notifications", msgCount));
return Result.success(Map.of("startedCount", startedCount, "completedCount", completedCount, "todoCount", todoCount, "doneCount", doneCount, "notifications", msgCount));
}
}
@@ -0,0 +1,625 @@
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.base.utils.CommonDownloadUtil;
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.service.TourLedgerService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
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 javax.servlet.http.HttpServletResponse;
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/group")
public class TourGroupController {
@Inject
private TourMatterService tourMatterService;
@Inject
private TourLedgerService tourLedgerService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/group/index.html")
@SaCheckPermission("tour.group")
public void index() {
}
@At
@SaCheckPermission("tour.group")
public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) {
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
pageForm.defaultSortAsc("lineName");
}
Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId);
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,
COALESCE(u.name, '校工会') AS unionName,
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,
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
$condition
ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC
""");
listSql.setCondition(cnd);
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);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class));
return Result.success(pagination);
}
@At
@SaCheckPermission("tour.group")
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.group")
public Result lineTypeOptions(Integer year) {
Cnd cnd = buildQueryCnd(year, 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));
}
@At
@SaCheckPermission("tour.group")
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);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("tour.group")
public void exportParticipants(String matterId, HttpServletResponse response) {
if (StrUtil.isBlank(matterId)) {
return;
}
NutMap matter = fetchExportMatter(matterId);
if (matter == null || matter.isEmpty()) {
return;
}
List<NutMap> list = queryExportParticipants(matterId);
Workbook workbook = buildParticipantsWorkbook(matter, list);
String lineName = StrUtil.blankToDefault(matter.getString("lineName", ""), "线路");
CommonDownloadUtil.download(lineName + "参加人员名单.xls", workbook, response);
}
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 NutMap fetchExportMatter(String matterId) {
Cnd cnd = Cnd.NEW();
cnd.and("m.delFlag", "=", false);
cnd.and("m.enabled", "=", true);
cnd.and("m.id", "=", matterId);
cnd.and("m.lineId", "IS NOT", null);
cnd.and("m.lineId", "<>", "");
cnd.and("l.delFlag", "=", false);
cnd.and("l.enabled", "=", true);
applyMatterDataScope(cnd);
Sql sql = Sqls.create("""
SELECT
m.id AS matterId,
m.`year`,
m.unionId,
COALESCE(u.name, '校工会') AS unionName,
l.lineName,
l.lineType,
m.travelStartTime,
m.travelEndTime,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE ''
END AS travelPeriod
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
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.map());
tourMatterService.dao().execute(sql);
return sql.getObject(NutMap.class);
}
private List<NutMap> queryExportParticipants(String matterId) {
Cnd cnd = buildSignupQueryCnd(matterId, null, null);
Sql sql = Sqls.create("""
SELECT
t.id AS ledgerId,
t.jobNo,
t.userName,
t.idCard,
vu.mobile,
t.unionName,
l.lineName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE ''
END AS travelPeriod,
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 t.signupTime DESC, t.createdAt DESC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
List<NutMap> list = sql.getList(NutMap.class);
fillExportFamilies(list);
return list;
}
private void fillExportFamilies(List<NutMap> list) {
if (list == null || list.isEmpty()) {
return;
}
List<String> ledgerIds = new ArrayList<>();
for (NutMap item : list) {
String ledgerId = item.getString("ledgerId", "");
if (StrUtil.isNotBlank(ledgerId)) {
ledgerIds.add(ledgerId);
}
}
if (ledgerIds.isEmpty()) {
return;
}
Sql sql = Sqls.create("""
SELECT
ledgerId,
familyName,
relationship,
idCard
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("ledgerId", ""), List.of()));
}
}
private Workbook buildParticipantsWorkbook(NutMap matter, List<NutMap> list) {
Workbook workbook = new HSSFWorkbook();
Sheet sheet = workbook.createSheet("参加人员名单");
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 10));
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 10));
double[] widths = {6, 14, 12, 22, 16, 24, 34, 26, 12, 12, 16};
for (int i = 0; i < widths.length; i++) {
sheet.setColumnWidth(i, (int) (widths[i] * 256));
}
CellStyle titleStyle = createStyle(workbook, "黑体", (short) 16, true, HorizontalAlignment.CENTER, false, false);
CellStyle unionStyle = createStyle(workbook, "宋体", (short) 12, true, HorizontalAlignment.LEFT, false, false);
CellStyle headerStyle = createStyle(workbook, "宋体", (short) 11, true, HorizontalAlignment.CENTER, true, true);
CellStyle bodyStyle = createStyle(workbook, "宋体", (short) 11, false, HorizontalAlignment.CENTER, true, true);
Row titleRow = sheet.createRow(0);
titleRow.setHeightInPoints(30);
setCell(titleRow, 0, StrUtil.blankToDefault(matter.getString("lineName", ""), ""), titleStyle);
fillMergedCells(titleRow, 1, 10, titleStyle);
Row unionRow = sheet.createRow(1);
unionRow.setHeightInPoints(26);
setCell(unionRow, 0, "分工会:" + StrUtil.blankToDefault(matter.getString("unionName", ""), ""), unionStyle);
fillMergedCells(unionRow, 1, 10, unionStyle);
String[] headers = {"序号", "工号", "姓名", "身份证号码", "电话号码", "所属分工会", "所选线路名称", "疗休养时间", "亲属关系", "姓名", "备注"};
Row headerRow = sheet.createRow(2);
headerRow.setHeightInPoints(35);
for (int i = 0; i < headers.length; i++) {
setCell(headerRow, i, headers[i], headerStyle);
}
int rowIndex = 3;
int seq = 1;
for (NutMap item : list == null ? List.<NutMap>of() : list) {
Row staffRow = sheet.createRow(rowIndex++);
staffRow.setHeightInPoints(24);
setParticipantRow(staffRow, seq++, item.getString("jobNo", ""), item.getString("userName", ""),
item.getString("idCard", ""), item.getString("mobile", ""), item.getString("unionName", ""),
item.getString("lineName", matter.getString("lineName", "")), item.getString("travelPeriod", matter.getString("travelPeriod", "")),
"", item.getString("userName", ""), bodyStyle);
Object familiesObj = item.get("families");
if (familiesObj instanceof List<?> families) {
for (Object obj : families) {
if (!(obj instanceof NutMap family)) {
continue;
}
Row familyRow = sheet.createRow(rowIndex++);
familyRow.setHeightInPoints(24);
setParticipantRow(familyRow, seq++, "", family.getString("familyName", ""),
family.getString("idCard", ""), "", "",
item.getString("lineName", matter.getString("lineName", "")),
item.getString("travelPeriod", matter.getString("travelPeriod", "")),
family.getString("relationship", ""), item.getString("userName", ""), bodyStyle);
}
}
}
int minRows = Math.max(rowIndex, 23);
while (rowIndex < minRows) {
Row row = sheet.createRow(rowIndex++);
row.setHeightInPoints(24);
for (int i = 0; i < headers.length; i++) {
setCell(row, i, "", bodyStyle);
}
}
return workbook;
}
private void setParticipantRow(Row row, int seq, String jobNo, String userName, String idCard, String mobile,
String unionName, String lineName, String travelPeriod, String relationship,
String participantName, CellStyle style) {
setCell(row, 0, String.valueOf(seq), style);
setCell(row, 1, jobNo, style);
setCell(row, 2, userName, style);
setCell(row, 3, idCard, style);
setCell(row, 4, mobile, style);
setCell(row, 5, unionName, style);
setCell(row, 6, lineName, style);
setCell(row, 7, travelPeriod, style);
setCell(row, 8, relationship, style);
setCell(row, 9, participantName, style);
setCell(row, 10, "", style);
}
private CellStyle createStyle(Workbook workbook, String fontName, short fontSize, boolean bold, HorizontalAlignment alignment, boolean wrap, boolean border) {
Font font = workbook.createFont();
font.setFontName(fontName);
font.setFontHeightInPoints(fontSize);
font.setBold(bold);
CellStyle style = workbook.createCellStyle();
style.setFont(font);
style.setAlignment(alignment);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setWrapText(wrap);
if (border) {
style.setBorderLeft(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
}
return style;
}
private void setCell(Row row, int col, String value, CellStyle style) {
Cell cell = row.createCell(col);
cell.setCellValue(StrUtil.blankToDefault(value, ""));
cell.setCellStyle(style);
}
private void fillMergedCells(Row row, int startCol, int endCol, CellStyle style) {
for (int i = startCol; i <= endCol; i++) {
setCell(row, i, "", style);
}
}
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);
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);
applyMatterDataScope(cnd);
return cnd;
}
/**
* 线路成团按事项归属控制数据范围:校工会查看全部;分工会查看本工会事项;其它角色仅查看自己创建的事项。
*/
private void applyMatterDataScope(Cnd cnd) {
if (hasSchoolUnionScope()) {
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 ("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";
}
}
@@ -0,0 +1,959 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.model.ExcelImportRes;
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.base.utils.CommonDownloadUtil;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.dayofficework.tour.mode.TourLedgerImportExcelMode;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative;
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.TourTravelAgency;
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;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
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;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.AdaptBy;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import org.nutz.mvc.upload.TempFile;
import org.nutz.mvc.upload.UploadAdaptor;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@IocBean
@Ok("json:full")
@At("/platform/tour/ledger")
public class TourLedgerController {
@Inject
private TourLedgerService tourLedgerService;
@Inject
private TourLedgerFamilyService tourLedgerFamilyService;
@Inject
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/ledger/index.html")
@SaCheckPermission("tour.ledger")
public void index() {
}
@At
@SaCheckPermission("tour.ledger")
public Result pageData(PageForm pageForm, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly);
Sql countSql = Sqls.create("""
SELECT COUNT(DISTINCT t.id)
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT
t.*,
ins.id AS instanceId,
ins.state AS instanceState,
ins.processDefineId AS instanceProcessDefineId,
IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0) AS directFamilyUnitLine,
dr.id AS directRelativeId,
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName,
IFNULL(f.familyCount, 0) AS familyCount,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE tp.travelPeriod
END AS travelPeriod
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
GROUP BY t.id
ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC
""");
listSql.setCondition(cnd);
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);
var list = listSql.getList(NutMap.class);
list.forEach(item -> item.put("lineName", item.getString("currentLineName")));
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@At
@SaCheckPermission("tour.ledger")
public Result detail(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
TourLedger ledger = tourLedgerService.fetch(id);
if (ledger == null) {
return Result.error("台账记录不存在");
}
ledger.setLineName(getCurrentLineName(ledger));
Cnd familyCnd = Cnd.NEW();
familyCnd.and(TourLedgerFamily::getLedgerId, "=", id);
familyCnd.and(TourLedgerFamily::getDelFlag, "=", false);
familyCnd.asc(TourLedgerFamily::getCreatedAt);
TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch(
Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false)
.and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
return Result.success(NutMap.NEW()
.addv("ledger", ledger)
.addv("travelPeriod", getTravelPeriod(ledger))
.addv("families", tourLedgerFamilyService.query(familyCnd))
.addv("directRelative", directRelative)
.addv("fillBedInfo", isFillBedInfo(ledger))
.addv("directFamilyUnitLine", isDirectFamilyUnitLine(ledger)));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.ledger")
@SLog(type = "tour", tag = "疗休养台账", msg = "删除疗休养台账")
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
TourLedger ledger = tourLedgerService.fetch(id);
if (ledger == null) {
return Result.error("台账记录不存在");
}
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", id));
tourLedgerService.delete(id);
return Result.success();
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.ledger")
@SLog(type = "tour", tag = "疗休养台账", msg = "设置参加人员")
public Result setParticipants(@Param("ids") String ids) {
List<String> idList = StrUtil.isBlank(ids) ? Collections.emptyList() : Json.fromJsonAsList(String.class, ids);
if (idList.isEmpty()) {
return Result.error("请选择要设置的参加人员");
}
tourLedgerService.dao().update(TourLedger.class, Chain.make("joined", true), Cnd.where("id", "in", idList));
return Result.success();
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.ledger")
@SLog(type = "tour", tag = "疗休养台账", msg = "参加人员导入")
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
public Result importParticipants(@Param("file") TempFile file) {
if (file == null) {
return Result.error("请选择导入文件");
}
List<TourLedgerImportExcelMode> importRows = ExcelImportUtil.importExcel(file.getFile(), TourLedgerImportExcelMode.class, new ImportParams());
ExcelImportRes<TourLedgerImportExcelMode> excelImportRes = new ExcelImportRes<>();
excelImportRes.setTotalRecords(importRows.size());
for (int i = 0; i < importRows.size(); i++) {
TourLedgerImportExcelMode row = importRows.get(i);
if (!validateImportRow(row, i)) {
continue;
}
Boolean joined = parseJoined(row.getJoined());
if (joined == null) {
row.setErrInfo("是否参加只能填写1、0、是、否、true、false、已参加、未参加", i);
continue;
}
TourLine line = fetchImportLine(row);
if (line == null) {
row.setErrInfo("未找到唯一匹配的线路", i);
continue;
}
List<TourLedger> ledgers = queryImportLedgerCandidates(row);
if (ledgers.isEmpty()) {
Result createResult = insertImportLedger(row, line, joined);
if (createResult.isSuccess()) {
continue;
}
row.setErrInfo(createResult.getMsg(), i);
continue;
}
TourLedger ledger = resolveImportLedger(row, ledgers, line);
if (ledger == null) {
row.setErrInfo("匹配到多条台账记录,报名时间或线路未能唯一匹配,请核对年度、工号、报名时间、线路名称", i);
continue;
}
Chain chain = Chain.make("joined", joined)
.add("lineId", line.getId())
.add("lineName", line.getLineName())
.add("lineType", line.getLineType());
String signupTime = normalizeSignupTime(row.getSignupTime());
if (StrUtil.isNotBlank(signupTime)) {
chain.add("signupTime", signupTime);
}
tourLedgerService.dao().update(TourLedger.class, chain, Cnd.where("id", "=", ledger.getId()));
}
excelImportRes.setErrorDetails(importRows.stream().filter(s -> StrUtil.isNotBlank(s.getErrMsg())).collect(Collectors.toList()));
excelImportRes.setFailedCount(excelImportRes.getErrorDetails().size());
excelImportRes.setSuccessCount(Math.max(importRows.size() - excelImportRes.getFailedCount(), 0));
return Result.success(excelImportRes);
}
@At
@SaCheckPermission("tour.ledger")
@Ok("void")
public void downloadTemplate(HttpServletResponse response) {
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("年度", "year", 12));
entities.add(new ExcelExportEntity("工号", "jobNo", 18));
entities.add(new ExcelExportEntity("姓名", "userName", 18));
entities.add(new ExcelExportEntity("报名时间", "signupTime", 24));
entities.add(new ExcelExportEntity("线路名称", "lineName", 30));
entities.add(new ExcelExportEntity("线路类型", "lineType", 18));
entities.add(new ExcelExportEntity("是否参加", "joined", 14));
ExportParams exportParams = new ExportParams();
exportParams.setSheetName("tour_ledger");
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, Collections.emptyList());
CommonDownloadUtil.download("参加人员导入模板.xlsx", workbook, response);
}
@At
@SaCheckPermission("tour.ledger")
@Ok("void")
public void exportUnionSignupZip(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly,
HttpServletResponse response) throws Exception {
List<NutMap> rows = queryUnionSignupRows(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType,
directFamilyOnly, overCostOnly);
Map<String, List<NutMap>> unionRows = groupRowsByUnion(rows);
String zipName = URLEncoder.encode("疗休养分工会报名汇总表.zip", StandardCharsets.UTF_8.toString());
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment;filename=" + zipName + ";filename*=UTF-8''" + zipName);
try (ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()))) {
if (unionRows.isEmpty()) {
zipOutputStream.putNextEntry(new ZipEntry("无数据报名汇总表.xls"));
try (Workbook workbook = buildUnionSignupWorkbook("无数据", Collections.emptyList())) {
workbook.write(zipOutputStream);
}
zipOutputStream.closeEntry();
} else {
for (Map.Entry<String, List<NutMap>> entry : unionRows.entrySet()) {
String unionName = entry.getValue().isEmpty() ? entry.getKey() : entry.getValue().get(0).getString("unionName", entry.getKey());
String fileName = safeFileName(unionName + "报名汇总表.xls");
zipOutputStream.putNextEntry(new ZipEntry(fileName));
try (Workbook workbook = buildUnionSignupWorkbook(unionName, entry.getValue())) {
workbook.write(zipOutputStream);
}
zipOutputStream.closeEntry();
}
}
zipOutputStream.flush();
}
}
private List<NutMap> queryUnionSignupRows(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly);
Sql sql = Sqls.create("""
SELECT
t.id,
t.jobNo,
t.userName,
t.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(l.lineName, ''), t.lineName, '') AS lineName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE tp.travelPeriod
END AS travelPeriod
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
LEFT JOIN sys_union su ON su.id = t.unionId
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
GROUP BY t.id
ORDER BY unionCode ASC, unionName ASC, t.signupTime DESC, t.createdAt DESC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return sql.getList(NutMap.class);
}
private Map<String, List<NutMap>> groupRowsByUnion(List<NutMap> rows) {
Map<String, List<NutMap>> unionRows = new LinkedHashMap<>();
for (NutMap row : rows) {
String unionName = StrUtil.blankToDefault(row.getString("unionName"), "未分工会");
String unionCode = StrUtil.blankToDefault(row.getString("unionCode"), "");
String key = StrUtil.isBlank(unionCode) ? unionName : unionCode + "_" + unionName;
unionRows.computeIfAbsent(key, item -> new ArrayList<>()).add(row);
}
return unionRows;
}
private Workbook buildUnionSignupWorkbook(String unionName, List<NutMap> rows) {
HSSFWorkbook workbook = new HSSFWorkbook();
Sheet sheet = workbook.createSheet("报名汇总表");
sheet.setColumnWidth(0, 8 * 256);
sheet.setColumnWidth(1, 14 * 256);
sheet.setColumnWidth(2, 14 * 256);
sheet.setColumnWidth(3, 24 * 256);
sheet.setColumnWidth(4, 16 * 256);
sheet.setColumnWidth(5, 24 * 256);
sheet.setColumnWidth(6, 36 * 256);
sheet.setColumnWidth(7, 26 * 256);
sheet.setColumnWidth(8, 18 * 256);
CellStyle titleStyle = createUnionSignupTitleStyle(workbook);
CellStyle headerStyle = createUnionSignupHeaderStyle(workbook);
CellStyle bodyStyle = createUnionSignupBodyStyle(workbook);
Row titleRow = sheet.createRow(0);
titleRow.setHeightInPoints(28);
Cell titleCell = titleRow.createCell(0);
titleCell.setCellValue(StrUtil.blankToDefault(unionName, "未分工会") + "报名汇总表");
titleCell.setCellStyle(titleStyle);
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 8));
for (int i = 1; i <= 8; i++) {
titleRow.createCell(i).setCellStyle(titleStyle);
}
Row headerRow = sheet.createRow(1);
headerRow.setHeightInPoints(24);
String[] headers = {"序号", "工号", "姓名", "身份证号码", "电话号码", "所属分工会", "所选线路名称", "疗休养时间", "备注"};
for (int i = 0; i < headers.length; i++) {
setCell(headerRow, i, headers[i], headerStyle);
}
int rowIndex = 2;
int seq = 1;
for (NutMap row : rows) {
Row dataRow = sheet.createRow(rowIndex++);
dataRow.setHeightInPoints(22);
setCell(dataRow, 0, String.valueOf(seq++), bodyStyle);
setCell(dataRow, 1, row.getString("jobNo", ""), bodyStyle);
setCell(dataRow, 2, row.getString("userName", ""), bodyStyle);
setCell(dataRow, 3, row.getString("idCard", ""), bodyStyle);
setCell(dataRow, 4, row.getString("mobile", ""), bodyStyle);
setCell(dataRow, 5, row.getString("unionName", ""), bodyStyle);
setCell(dataRow, 6, row.getString("lineName", ""), bodyStyle);
setCell(dataRow, 7, row.getString("travelPeriod", ""), bodyStyle);
setCell(dataRow, 8, "", bodyStyle);
}
return workbook;
}
private CellStyle createUnionSignupTitleStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderTop(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
Font font = workbook.createFont();
font.setBold(true);
font.setFontHeightInPoints((short) 16);
style.setFont(font);
return style;
}
private CellStyle createUnionSignupHeaderStyle(Workbook workbook) {
CellStyle style = createUnionSignupBodyStyle(workbook);
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}
private CellStyle createUnionSignupBodyStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setWrapText(true);
style.setBorderTop(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
return style;
}
private void setCell(Row row, int index, String value, CellStyle style) {
Cell cell = row.createCell(index);
cell.setCellValue(StrUtil.blankToDefault(value, ""));
cell.setCellStyle(style);
}
private String safeFileName(String fileName) {
String safeName = StrUtil.blankToDefault(fileName, "报名汇总表.xls")
.replaceAll("[\\\\/:*?\"<>|]", "");
return safeName.length() > 120 ? safeName.substring(0, 120) : safeName;
}
private boolean validateImportRow(TourLedgerImportExcelMode row, int index) {
if (row.getYear() == null) {
row.setErrInfo("年度为空", index);
return false;
}
if (StrUtil.isBlank(row.getJobNo())) {
row.setErrInfo("工号为空", index);
return false;
}
if (StrUtil.isBlank(row.getLineName())) {
row.setErrInfo("线路名称为空", index);
return false;
}
if (StrUtil.isBlank(row.getLineType())) {
row.setErrInfo("线路类型为空", index);
return false;
}
if (StrUtil.isBlank(row.getJoined())) {
row.setErrInfo("是否参加为空", index);
return false;
}
return true;
}
private TourLine fetchImportLine(TourLedgerImportExcelMode row) {
Cnd cnd = Cnd.where(TourLine::getDelFlag, "=", false)
.and(TourLine::getYear, "=", row.getYear())
.and(TourLine::getLineName, "=", row.getLineName().trim())
.and(TourLine::getLineType, "=", row.getLineType().trim());
List<TourLine> lines = tourLedgerService.dao().query(TourLine.class, cnd);
return lines.size() == 1 ? lines.get(0) : null;
}
private List<TourLedger> queryImportLedgerCandidates(TourLedgerImportExcelMode row) {
Cnd cnd = Cnd.where(TourLedger::getDelFlag, "=", false)
.and(TourLedger::getYear, "=", row.getYear())
.and(TourLedger::getJobNo, "=", row.getJobNo().trim());
return tourLedgerService.query(cnd);
}
private TourLedger resolveImportLedger(TourLedgerImportExcelMode row, List<TourLedger> ledgers, TourLine line) {
if (ledgers.size() == 1) {
return ledgers.get(0);
}
List<TourLedger> matched = ledgers;
if (row.getSignupTime() != null) {
String importSignupTime = normalizeSignupTime(row.getSignupTime());
matched = matched.stream()
.filter(item -> signupTimeEquals(importSignupTime, item.getSignupTime()))
.collect(Collectors.toList());
if (matched.size() == 1) {
return matched.get(0);
}
}
matched = ledgers.stream()
.filter(item -> ledgerLineEquals(item, line))
.collect(Collectors.toList());
return matched.size() == 1 ? matched.get(0) : null;
}
private boolean ledgerLineEquals(TourLedger ledger, TourLine line) {
if (ledger == null || line == null) {
return false;
}
if (StrUtil.isNotBlank(ledger.getLineId()) && ledger.getLineId().equals(line.getId())) {
return true;
}
return StrUtil.equals(ledger.getLineName(), line.getLineName())
&& StrUtil.equals(ledger.getLineType(), line.getLineType());
}
private Result insertImportLedger(TourLedgerImportExcelMode row, TourLine line, Boolean joined) {
View_user user = fetchImportUser(row.getJobNo());
if (user == null) {
return Result.error("系统用户中未找到该工号");
}
TourLedger ledger = new TourLedger();
ledger.setYear(row.getYear());
ledger.setJobNo(StrUtil.blankToDefault(user.getLoginname(), row.getJobNo().trim()));
ledger.setUserName(StrUtil.blankToDefault(user.getUsername(), row.getUserName()));
ledger.setGender(user.getSex());
ledger.setIdCard(user.getIdCard());
ledger.setUnitId(user.getUnitId());
ledger.setUnitName(user.getUnitName());
ledger.setUnionId(user.getUnionId());
ledger.setUnionName(user.getUnionName());
ledger.setSignupTime(normalizeSignupTime(row.getSignupTime()));
ledger.setLineId(line.getId());
ledger.setLineName(line.getLineName());
ledger.setLineType(line.getLineType());
ledger.setTravelAgencyId(line.getTravelAgencyId());
ledger.setTravelAgencyName(getTravelAgencyName(line.getTravelAgencyId()));
ledger.setHasFamily(false);
ledger.setJoined(joined);
ledger.setReimbursed(false);
ledger.setOverCostReimbursed(false);
tourLedgerService.insert(ledger);
return Result.success();
}
private View_user fetchImportUser(String jobNo) {
if (StrUtil.isBlank(jobNo)) {
return null;
}
return tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", jobNo.trim()));
}
private String getTravelAgencyName(String travelAgencyId) {
if (StrUtil.isBlank(travelAgencyId)) {
return "";
}
TourTravelAgency agency = tourLedgerService.dao().fetch(TourTravelAgency.class, travelAgencyId);
return agency == null ? "" : StrUtil.blankToDefault(agency.getAgencyName(), "");
}
private boolean signupTimeEquals(String importSignupTime, String ledgerSignupTime) {
if (StrUtil.isBlank(importSignupTime) || StrUtil.isBlank(ledgerSignupTime)) {
return false;
}
String ledgerTime = normalizeSignupTime(ledgerSignupTime);
if (importSignupTime.equals(ledgerTime)) {
return true;
}
return importSignupTime.length() == 16 && ledgerTime.startsWith(importSignupTime)
|| ledgerTime.length() == 16 && importSignupTime.startsWith(ledgerTime);
}
private String normalizeSignupTime(Object value) {
if (value instanceof Date date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
}
String text = StrUtil.trimToEmpty(value == null ? "" : String.valueOf(value)).replace('T', ' ');
if (StrUtil.isBlank(text)) {
return "";
}
text = text.replaceAll("\\.0$", "");
String[] dateTimePatterns = {
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd HH:mm",
"yyyy/M/d H:mm:ss",
"yyyy/M/d H:mm",
"M/d/yy H:mm:ss",
"M/d/yy H:mm",
"M/d/yyyy H:mm:ss",
"M/d/yyyy H:mm"
};
for (String pattern : dateTimePatterns) {
try {
LocalDateTime dateTime = LocalDateTime.parse(text, DateTimeFormatter.ofPattern(pattern));
return dateTime.format(DateTimeFormatter.ofPattern(pattern.contains(":ss") ? "yyyy-MM-dd HH:mm:ss" : "yyyy-MM-dd HH:mm"));
} catch (DateTimeParseException ignored) {
}
}
String[] datePatterns = {"yyyy-MM-dd", "yyyy/M/d", "M/d/yy", "M/d/yyyy"};
for (String pattern : datePatterns) {
try {
LocalDate date = LocalDate.parse(text, DateTimeFormatter.ofPattern(pattern));
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} catch (DateTimeParseException ignored) {
}
}
return text;
}
private Boolean parseJoined(String value) {
String joined = StrUtil.trimToEmpty(value).toLowerCase();
if ("1".equals(joined) || "1.0".equals(joined) || "".equals(joined) || "true".equals(joined) || "已参加".equals(joined)) {
return true;
}
if ("0".equals(joined) || "0.0".equals(joined) || "".equals(joined) || "false".equals(joined) || "未参加".equals(joined)) {
return false;
}
return null;
}
@At
@SaCheckPermission("tour.ledger")
public Result unionOptions() {
Cnd cnd = Cnd.NEW();
cnd.asc("unionCode");
cnd.asc("name");
return Result.success(tourLedgerService.dao().query(Sys_union.class, cnd));
}
@At
@SaCheckPermission("tour.ledger")
public Result lineOptions(Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword, Boolean directFamilyOnly, Boolean overCostOnly) {
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.and("t.lineName", "IS NOT", null);
cnd.and("t.lineName", "<>", "");
cnd.andEX("t.unionId", "=", unionId);
cnd.andEX("t.lineType", "=", lineType);
appendTravelPeriodFilter(cnd, travelPeriod);
appendKeywordFilter(cnd, keyword);
appendScopeFilter(cnd, directFamilyOnly, overCostOnly);
appendApprovedWorkflowFilter(cnd);
Sql sql = Sqls.create("""
SELECT DISTINCT
CASE
WHEN IFNULL(t.lineId, '') <> '' THEN t.lineId
ELSE CONCAT('legacy:', t.lineName)
END AS lineId,
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS lineName
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
$condition
ORDER BY lineName ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.ledger")
public Result travelPeriodOptions(Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword, Boolean directFamilyOnly, Boolean overCostOnly) {
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.and("m.travelStartTime", "IS NOT", null);
cnd.and("m.travelStartTime", "<>", "");
cnd.and("m.travelEndTime", "IS NOT", null);
cnd.and("m.travelEndTime", "<>", "");
cnd.andEX("t.unionId", "=", unionId);
cnd.andEX("t.lineType", "=", lineType);
appendLineFilter(cnd, lineId);
appendKeywordFilter(cnd, keyword);
appendScopeFilter(cnd, directFamilyOnly, overCostOnly);
appendApprovedWorkflowFilter(cnd);
Sql sql = Sqls.create("""
SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
$condition
ORDER BY travelPeriod ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
private Cnd buildQueryCnd(Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.andEX("t.unionId", "=", unionId);
cnd.andEX("t.lineType", "=", lineType);
appendLineFilter(cnd, lineId);
appendTravelPeriodFilter(cnd, travelPeriod);
appendKeywordFilter(cnd, keyword);
appendScopeFilter(cnd, directFamilyOnly, overCostOnly);
appendApprovedWorkflowFilter(cnd);
return cnd;
}
private void appendApprovedWorkflowFilter(Cnd cnd) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.or("ins.id", "IS", null);
group.or("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.and(group);
}
private void appendScopeFilter(Cnd cnd, Boolean directFamilyOnly, Boolean overCostOnly) {
if (Boolean.TRUE.equals(directFamilyOnly)) {
cnd.and("IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0)", "=", 1);
}
if (Boolean.TRUE.equals(overCostOnly)) {
cnd.and("t.overCostReimbursed", "=", true);
}
}
private void appendKeywordFilter(Cnd cnd, String keyword) {
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.orLike("t.userName", keyword.trim());
group.orLike("t.jobNo", keyword.trim());
cnd.and(group);
}
}
private String getOrderColumn(String orderName) {
if ("year".equals(orderName)) {
return "t.`year`";
}
if ("jobNo".equals(orderName)) {
return "t.jobNo";
}
if ("userName".equals(orderName)) {
return "t.userName";
}
if ("lineName".equals(orderName)) {
return "COALESCE(NULLIF(l.lineName, ''), t.lineName)";
}
if ("travelPeriod".equals(orderName)) {
return "COALESCE(m.travelStartTime, tp.travelPeriod)";
}
if ("lineType".equals(orderName)) {
return "t.lineType";
}
if ("signupTime".equals(orderName)) {
return "t.signupTime";
}
if ("familyCount".equals(orderName)) {
return "familyCount";
}
if ("joined".equals(orderName)) {
return "t.joined";
}
return "t.`year`";
}
private String getTravelPeriod(TourLedger ledger) {
if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) {
return "";
}
if (StrUtil.isNotBlank(ledger.getMatterId())) {
Sql exactSql = Sqls.create("""
SELECT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE ''
END AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND id = @matterId
""");
exactSql.setParam("matterId", ledger.getMatterId());
exactSql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(exactSql);
return exactSql.getString();
}
Sql sql = Sqls.create("""
SELECT GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND `year` = @year
AND lineId = @lineId
""");
sql.setParam("year", ledger.getYear());
sql.setParam("lineId", ledger.getLineId());
sql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(sql);
return sql.getString();
}
private boolean isDirectFamilyUnitLine(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getLineId())) {
return false;
}
Sql sql = Sqls.create("""
SELECT IFNULL(directFamilyUnitLine, 0)
FROM tour_line
WHERE delFlag = 0
AND id = @lineId
""");
sql.setParam("lineId", ledger.getLineId());
sql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(sql);
return sql.getInt() > 0;
}
private boolean isFillBedInfo(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) {
return true;
}
Sql sql = Sqls.create("""
SELECT IFNULL(MAX(s.fillBedInfo), 1)
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0
WHERE t.id = @ledgerId
""");
sql.setParam("ledgerId", ledger.getId());
sql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(sql);
return sql.getInt() > 0;
}
private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) {
if (StrUtil.isBlank(travelPeriod)) {
return;
}
cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod);
}
private void appendLineFilter(Cnd cnd, String lineId) {
if (StrUtil.isBlank(lineId)) {
return;
}
if (lineId.startsWith("legacy:")) {
SqlExpressionGroup legacyGroup = new SqlExpressionGroup();
legacyGroup.or("t.lineId", "IS", null);
legacyGroup.or("t.lineId", "=", "");
cnd.and(legacyGroup);
cnd.and("t.lineName", "=", lineId.substring("legacy:".length()));
return;
}
cnd.and("t.lineId", "=", lineId);
}
private String getCurrentLineName(TourLedger ledger) {
if (ledger == null) {
return "";
}
if (StrUtil.isBlank(ledger.getLineId())) {
return ledger.getLineName();
}
Sql sql = Sqls.create("""
SELECT lineName
FROM tour_line
WHERE delFlag = 0
AND id = @lineId
""");
sql.setParam("lineId", ledger.getLineId());
sql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(sql);
return StrUtil.blankToDefault(sql.getString(), ledger.getLineName());
}
}
@@ -0,0 +1,297 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.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_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.views.View_user;
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.TourLine;
import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLineService;
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;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@IocBean
@Ok("json:full")
@At("/platform/tour/route")
public class TourLineController {
@Inject
private TourLineService tourLineService;
@Inject
private SysDictService sysDictService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/route/index.html")
@SaCheckPermission("tour.route")
public void index() {
}
@At
@SaCheckPermission("tour.route")
public Result pageData(PageForm pageForm, Integer year, String lineName, String lotId) {
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
pageForm.defaultSortDesc("enabled");
}
Cnd cnd = Cnd.NEW();
cnd.and("l.delFlag", "=", false);
cnd.andEX("l.`year`", "=", year);
cnd.and(Cnd.likeEX("l.lineName", lineName));
cnd.andEX("l.lotId", "=", lotId);
boolean sysAdmin = AuthUtil.hasRole(RoleConstant.SYSADMIN.name());
boolean branchUnionScope = !sysAdmin && AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name());
applyDataScope(cnd, sysAdmin, branchUnionScope);
String creatorJoin = branchUnionScope ? """
LEFT JOIN sys_unit lineUnit ON lineUnit.id = l.unitId
LEFT JOIN sys_user creatorUser ON creatorUser.id = COALESCE(NULLIF(l.creatorUserId, ''), l.createdBy)
LEFT JOIN sys_unit creatorUnit ON creatorUnit.id = creatorUser.unitId
""" : "";
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_line l
$creatorJoin
$condition
""");
countSql.setVar("creatorJoin", creatorJoin);
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourLineService.dao().execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT
l.*,
a.agencyName AS travelAgencyName,
lot.lotName AS lotName
FROM tour_line l
LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId
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
""");
listSql.setVar("creatorJoin", creatorJoin);
listSql.setCondition(cnd);
listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName()));
listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
listSql.setPager(tourLineService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
tourLineService.dao().execute(listSql);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class));
return Result.success(pagination);
}
@At
@SaCheckPermission("tour.route")
public Result detail(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
TourLine line = tourLineService.fetch(id);
return line == null ? Result.error("线路不存在") : Result.success(line);
}
@At
@SaCheckPermission("tour.route")
public Result travelAgencyOptions(Integer year) {
Cnd cnd = Cnd.where(TourTravelAgency::getEnabled, "=", true);
cnd.andEX(TourTravelAgency::getYear, "=", year);
cnd.asc(TourTravelAgency::getAgencyCode).asc(TourTravelAgency::getAgencyName);
return Result.success(tourLineService.dao().query(TourTravelAgency.class, cnd));
}
@At
@SaCheckPermission("tour.route")
public Result lotOptions(Integer year) {
Cnd cnd = Cnd.NEW();
cnd.andEX("s.`year`", "=", year);
Sql sql = Sqls.create("""
SELECT lot.id, lot.lotName, lot.lotValue, lot.activityCost
FROM tour_setting_lot lot
INNER JOIN tour_setting s ON s.id = lot.settingId
$condition
ORDER BY lot.lotValue DESC, lot.lotName ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLineService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.route")
public Result lineTypeOptions() {
List<Sys_dict> list = sysDictService.getSubListByCode("lineType");
if (list == null) {
return Result.success(Collections.emptyList());
}
return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList()));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.route")
@SLog(type = "tour", tag = "线路管理", msg = "保存线路信息")
public Result doSubmit(TourLine line) {
Result checkResult = check(line);
if (checkResult != null) {
return checkResult;
}
Cnd sameCodeCnd = Cnd.where(TourLine::getYear, "=", line.getYear())
.and(TourLine::getLineCode, "=", line.getLineCode());
if (StrUtil.isNotBlank(line.getId())) {
sameCodeCnd.and(TourLine::getId, "<>", line.getId());
}
if (tourLineService.count(sameCodeCnd) > 0) {
return Result.error("同年度下线路编号已存在");
}
if (line.getEnabled() == null) {
line.setEnabled(true);
}
if (line.getOpenFlag() == null) {
line.setOpenFlag(true);
}
if (line.getDirectFamilyUnitLine() == null) {
line.setDirectFamilyUnitLine(false);
}
if (StrUtil.isBlank(line.getId())) {
fillCreatorInfo(line, null);
tourLineService.insert(line);
} else {
fillCreatorInfo(line, tourLineService.fetch(line.getId()));
tourLineService.updateIgnoreNull(line);
}
return Result.success();
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.route")
@SLog(type = "tour", tag = "线路管理", msg = "删除线路信息")
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
tourLineService.delete(id);
return Result.success();
}
private Result check(TourLine line) {
if (line == null) {
return Result.error("参数错误");
}
if (line.getYear() == null) {
return Result.error("创建年度不能为空");
}
if (StrUtil.isBlank(line.getTravelAgencyId())) {
return Result.error("旅行社名称不能为空");
}
if (StrUtil.isBlank(line.getLineName())) {
return Result.error("线路名称不能为空");
}
if (StrUtil.isBlank(line.getLineCode())) {
return Result.error("线路编号不能为空");
}
if (StrUtil.isBlank(line.getLineType())) {
return Result.error("线路类型不能为空");
}
if (StrUtil.isBlank(line.getLotId())) {
return Result.error("时间标段不能为空");
}
if (StrUtil.isBlank(line.getMobileThumb())) {
return Result.error("移动端缩略图不能为空");
}
return null;
}
/**
* 创建人和所在单位由当前登录人生成,编辑时保留原创建信息。
*/
private void fillCreatorInfo(TourLine line, TourLine oldLine) {
if (oldLine != null) {
line.setCreatorUserId(defaultIfBlank(oldLine.getCreatorUserId(), SecurityUtil.getUserId()));
line.setCreatorName(defaultIfBlank(oldLine.getCreatorName(), SecurityUtil.getUserUsername()));
line.setUnitId(defaultIfBlank(oldLine.getUnitId(), SecurityUtil.getUnitId()));
line.setUnitName(defaultIfBlank(oldLine.getUnitName(), getCurrentUnitName()));
return;
}
line.setCreatorUserId(SecurityUtil.getUserId());
line.setCreatorName(SecurityUtil.getUserUsername());
line.setUnitId(SecurityUtil.getUnitId());
line.setUnitName(getCurrentUnitName());
}
private String getCurrentUnitName() {
View_user user = tourLineService.dao().fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
return user == null ? "" : defaultIfBlank(user.getUnitName(), "");
}
private String defaultIfBlank(String value, String defaultValue) {
return StrUtil.isBlank(value) ? defaultValue : value;
}
/**
* 线路数据范围:
* SYSADMIN 可查看全部;分工会主席查看本工会创建的线路;其它角色保持原逻辑,仅查看自己创建的线路。
*/
private void applyDataScope(Cnd cnd, boolean sysAdmin, boolean branchUnionScope) {
if (sysAdmin) {
return;
}
if (branchUnionScope) {
SqlExpressionGroup unionGroup = new SqlExpressionGroup();
unionGroup.or("lineUnit.unionId", "=", SecurityUtil.getUnionId());
unionGroup.or("creatorUnit.unionId", "=", SecurityUtil.getUnionId());
cnd.and(unionGroup);
return;
}
SqlExpressionGroup creatorGroup = new SqlExpressionGroup();
creatorGroup.or("l.creatorUserId", "=", SecurityUtil.getUserId());
creatorGroup.or("l.createdBy", "=", SecurityUtil.getUserId());
cnd.and(creatorGroup);
}
private String getOrderColumn(String orderName) {
if ("year".equals(orderName)) {
return "l.`year`";
}
if ("lineCode".equals(orderName)) {
return "l.lineCode";
}
if ("lotName".equals(orderName)) {
return "lot.lotName";
}
if ("lineType".equals(orderName)) {
return "l.lineType";
}
if ("enabled".equals(orderName)) {
return "l.enabled";
}
return "l.`year`";
}
}
@@ -0,0 +1,441 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.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_dict;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.sys.services.SysDictService;
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.TourLedger;
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.service.TourMatterService;
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;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@IocBean
@Ok("json:full")
@At("/platform/tour/matter")
public class TourMatterController {
private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$";
@Inject
private TourMatterService tourMatterService;
@Inject
private SysDictService sysDictService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/matter/index.html")
@SaCheckPermission("tour.matter")
public void index() {
}
@At
@SaCheckPermission("tour.matter")
public Result pageData(PageForm pageForm, Integer year, String matterName, String unionId, String organizationType) {
Cnd cnd = Cnd.NEW();
cnd.and("m.delFlag", "=", false);
cnd.andEX("m.`year`", "=", year);
cnd.and(Cnd.likeEX("m.matterName", matterName));
cnd.andEX("m.unionId", "=", unionId);
cnd.andEX("m.organizationType", "=", organizationType);
applyMatterDataScope(cnd);
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_matter m
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourMatterService.dao().execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT
m.*,
s.configName AS settingName,
u.name AS unionName,
d.name AS organizationTypeName,
l.lineName AS lineName,
l.lineType AS lineType,
l.directFamilyUnitLine AS directFamilyUnitLine
FROM tour_matter m
LEFT JOIN tour_setting s ON s.id = m.settingId
LEFT JOIN sys_union u ON u.id = m.unionId
LEFT JOIN tour_line l ON l.id = m.lineId
LEFT JOIN sys_dict d ON d.`code` = m.organizationType
AND d.parentId = (SELECT id FROM sys_dict WHERE `code` = 'organizationType' LIMIT 1)
$condition
ORDER BY $orderColumn $orderBy, m.`year` DESC, m.createdAt DESC
""");
listSql.setCondition(cnd);
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);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class));
return Result.success(pagination);
}
@At
@SaCheckPermission("tour.matter")
public Result detail(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
TourMatter matter = tourMatterService.fetch(id);
return matter == null ? Result.error("事项不存在") : Result.success(matter);
}
@At
@SaCheckPermission("tour.matter")
public Result settingOptions(Integer year) {
Cnd cnd = Cnd.where(TourSetting::getEnabled, "=", true);
cnd.andEX(TourSetting::getYear, "=", year);
cnd.desc(TourSetting::getCreatedAt);
cnd.desc(TourSetting::getUpdatedAt);
cnd.asc(TourSetting::getConfigName);
return Result.success(tourMatterService.dao().query(TourSetting.class, cnd));
}
@At
@SaCheckPermission("tour.matter")
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.matter")
public Result lineOptions(Integer year) {
Cnd cnd = Cnd.NEW();
cnd.and("l.delFlag", "=", false);
cnd.and("l.enabled", "=", true);
cnd.and("l.openFlag", "=", true);
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
ORDER BY l.lineCode ASC, l.lineName ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourMatterService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.matter")
public Result organizationTypeOptions() {
List<Sys_dict> list = sysDictService.getSubListByCode("organizationType");
if (list == null) {
return Result.success(Collections.emptyList());
}
return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList()));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.matter")
@SLog(type = "tour", tag = "疗休养事项", msg = "保存疗休养事项")
public Result doSubmit(TourMatter matter) {
Result checkResult = check(matter);
if (checkResult != null) {
return checkResult;
}
Cnd sameNameCnd = Cnd.where(TourMatter::getYear, "=", matter.getYear())
.and(TourMatter::getMatterName, "=", matter.getMatterName());
if (StrUtil.isNotBlank(matter.getId())) {
sameNameCnd.and(TourMatter::getId, "<>", matter.getId());
}
if (tourMatterService.count(sameNameCnd) > 0) {
return Result.error("同年度下事项名称已存在");
}
if (matter.getEnabled() == null) {
matter.setEnabled(true);
}
fillCreatorAndUnion(matter);
if (StrUtil.isBlank(matter.getId())) {
tourMatterService.insert(matter);
} else {
tourMatterService.updateIgnoreNull(matter);
}
return Result.success();
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.matter")
@SLog(type = "tour", tag = "疗休养事项", msg = "删除疗休养事项")
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
TourMatter matter = tourMatterService.fetch(id);
if (matter == null) {
return Result.error("事项不存在");
}
long signupCount = countSignup(id);
if (signupCount > 0) {
return Result.error("当前事项对应线路已有人员报名,不能删除");
}
tourMatterService.delete(id);
return Result.success();
}
@At
@SaCheckPermission("tour.matter")
public Result deleteInfo(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
TourMatter matter = tourMatterService.fetch(id);
if (matter == null) {
return Result.error("事项不存在");
}
long signupCount = countSignup(id);
return Result.success(NutMap.NEW()
.addv("signupCount", signupCount)
.addv("canDelete", signupCount <= 0)
.addv("message", signupCount > 0 ? "当前事项已有人员报名,不能删除" : "当前事项暂无人员报名,可删除"));
}
@At
@SaCheckPermission("tour.matter")
public Result lineConfig(String matterId) {
if (StrUtil.isBlank(matterId)) {
return Result.error("参数错误");
}
TourMatter matter = tourMatterService.fetch(matterId);
return matter == null ? Result.error("事项不存在") : Result.success(matter);
}
@At
@SaCheckPermission("tour.matter")
public Result settingPeople(String matterId) {
if (StrUtil.isBlank(matterId)) {
return Result.error("参数错误");
}
TourMatter matter = tourMatterService.fetch(matterId);
if (matter == null || StrUtil.isBlank(matter.getSettingId())) {
return Result.success(NutMap.NEW());
}
TourSetting setting = tourMatterService.dao().fetch(TourSetting.class, matter.getSettingId());
if (setting == null) {
return Result.success(NutMap.NEW());
}
return Result.success(NutMap.NEW()
.addv("minGroupPeople", setting.getMinGroupPeople())
.addv("maxGroupPeople", setting.getMaxGroupPeople()));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.matter")
@SLog(type = "tour", tag = "疗休养事项", msg = "保存事项线路配置")
public Result lineConfigDoSubmit(TourMatter matter) {
Result checkResult = checkLineConfig(matter);
if (checkResult != null) {
return checkResult;
}
TourMatter oldMatter = tourMatterService.fetch(matter.getId());
if (oldMatter == null) {
return Result.error("事项不存在");
}
oldMatter.setLineId(matter.getLineId());
oldMatter.setSignupStartTime(matter.getSignupStartTime());
oldMatter.setSignupEndTime(matter.getSignupEndTime());
oldMatter.setTravelStartTime(matter.getTravelStartTime());
oldMatter.setTravelEndTime(matter.getTravelEndTime());
oldMatter.setContactName(matter.getContactName());
oldMatter.setContactPhone(matter.getContactPhone());
oldMatter.setMinGroupPeople(matter.getMinGroupPeople());
oldMatter.setMaxGroupPeople(matter.getMaxGroupPeople());
oldMatter.setEstimatedCost(matter.getEstimatedCost());
tourMatterService.update(oldMatter);
return Result.success();
}
private Result check(TourMatter matter) {
if (matter == null) {
return Result.error("参数错误");
}
if (matter.getYear() == null) {
return Result.error("年度不能为空");
}
if (StrUtil.isBlank(matter.getMatterName())) {
return Result.error("事项名称不能为空");
}
if (StrUtil.isBlank(matter.getSettingId())) {
return Result.error("疗休养配置不能为空");
}
if (StrUtil.isBlank(matter.getOrganizationType())) {
return Result.error("组织形式不能为空");
}
if (!"schoolUnion".equals(matter.getOrganizationType()) && StrUtil.isBlank(matter.getUnionId())) {
return Result.error("所属工会不能为空");
}
return null;
}
private Result checkLineConfig(TourMatter matter) {
if (matter == null) {
return Result.error("参数错误");
}
if (StrUtil.isBlank(matter.getId())) {
return Result.error("事项不能为空");
}
if (StrUtil.isBlank(matter.getLineId())) {
return Result.error("线路不能为空");
}
if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())
|| StrUtil.isBlank(matter.getTravelStartTime()) || StrUtil.isBlank(matter.getTravelEndTime())) {
return Result.error("时间不能为空");
}
if (matter.getSignupStartTime().compareTo(matter.getSignupEndTime()) >= 0) {
return Result.error("报名开始时间必须小于报名结束时间");
}
if (matter.getTravelStartTime().compareTo(matter.getTravelEndTime()) > 0) {
return Result.error("出行开始时间不能晚于出行结束时间");
}
if (matter.getSignupEndTime().compareTo(matter.getTravelStartTime()) >= 0) {
return Result.error("报名结束时间必须小于出行开始时间");
}
if (StrUtil.isBlank(matter.getContactName())) {
return Result.error("联系人不能为空");
}
if (StrUtil.isBlank(matter.getContactPhone()) || !matter.getContactPhone().matches(MOBILE_PATTERN)) {
return Result.error("联系方式格式不正确");
}
if (matter.getMinGroupPeople() == null || matter.getMinGroupPeople() <= 0) {
return Result.error("最少成团人数必须大于0");
}
if (matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) {
return Result.error("最多成团人数必须大于0");
}
if (matter.getMinGroupPeople() > matter.getMaxGroupPeople()) {
return Result.error("最少成团人数不能大于最多成团人数");
}
if (matter.getEstimatedCost() == null || matter.getEstimatedCost().signum() < 0) {
return Result.error("预计费用不能小于0");
}
return null;
}
private void fillCreatorAndUnion(TourMatter matter) {
if (StrUtil.isBlank(matter.getId())) {
matter.setCreatorUserId(SecurityUtil.getUserId());
matter.setCreatorName(SecurityUtil.getUserUsername());
} else {
TourMatter oldMatter = tourMatterService.fetch(matter.getId());
if (oldMatter != null) {
matter.setCreatorUserId(oldMatter.getCreatorUserId());
matter.setCreatorName(oldMatter.getCreatorName());
}
}
if ("schoolUnion".equals(matter.getOrganizationType())) {
matter.setUnionId("");
}
}
private long countSignup(String matterId) {
return tourMatterService.dao().count(TourLedger.class, Cnd.where(TourLedger::getDelFlag, "=", false)
.and(TourLedger::getMatterId, "=", matterId));
}
/**
* 事项数据范围:SYSADMIN 查看全部;分工会主席查看本工会事项;其它角色仅查看自己创建的事项。
*/
private void applyMatterDataScope(Cnd cnd) {
if (AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
return;
}
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
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 void applyLineDataScope(Cnd cnd, boolean sysAdmin, boolean branchUnionScope) {
if (sysAdmin) {
return;
}
if (branchUnionScope) {
SqlExpressionGroup unionGroup = new SqlExpressionGroup();
unionGroup.or("lineUnit.unionId", "=", SecurityUtil.getUnionId());
unionGroup.or("creatorUnit.unionId", "=", SecurityUtil.getUnionId());
cnd.and(unionGroup);
return;
}
SqlExpressionGroup creatorGroup = new SqlExpressionGroup();
creatorGroup.or("l.creatorUserId", "=", SecurityUtil.getUserId());
creatorGroup.or("l.createdBy", "=", SecurityUtil.getUserId());
cnd.and(creatorGroup);
}
private String getOrderColumn(String orderName) {
if ("year".equals(orderName)) {
return "m.`year`";
}
if ("matterName".equals(orderName)) {
return "m.matterName";
}
if ("unionName".equals(orderName)) {
return "u.name";
}
if ("lineName".equals(orderName)) {
return "l.lineName";
}
if ("settingName".equals(orderName)) {
return "s.configName";
}
if ("organizationTypeName".equals(orderName)) {
return "d.location";
}
if ("enabled".equals(orderName)) {
return "m.enabled";
}
return "m.`year`";
}
}
@@ -0,0 +1,13 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
/**
* 分阶段建设的菜单占位入口。
*/
@IocBean
@At("/platform/tour")
public class TourPlaceholderController {
}
@@ -0,0 +1,580 @@
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.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.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily;
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;
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.List;
@IocBean
@Ok("json:full")
@At("/platform/tour/schoolUnionApproval")
public class TourSchoolUnionApprovalController {
private static final String WORKFLOW_KEY = "LXYBZXQSXL";
private static final String TASK_DISPLAY_NAME = "校工会审核";
@Inject
private TourLedgerService tourLedgerService;
@Inject
private TourLedgerFamilyService tourLedgerFamilyService;
@Inject
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/schoolUnionApproval/index.html")
@SaCheckPermission("tour.schoolUnionApproval")
public void index() {
}
@At("/h5")
@Ok("beetl:/platform/zhghh5/dayofficework/tour/schoolUnionApproval/index.html")
@SaCheckPermission("tour.schoolUnionApproval")
public void h5() {
}
@At
@SaCheckPermission("tour.schoolUnionApproval")
public Result pageData(PageForm pageForm, Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType);
Sql countSql = Sqls.create("""
SELECT COUNT(DISTINCT task.id)
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT
t.*,
ins.id AS instanceId,
ins.businessNo,
ins.state AS instanceState,
ins.variable AS instanceVariable,
ins.processDefineId AS instanceProcessDefineId,
task.id AS taskId,
task.taskName AS taskKey,
task.displayName AS taskName,
task.taskType,
task.performType AS taskPerformType,
task.taskState,
task.finishTime,
task.taskParentId,
task.variable AS taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName,
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS currentLineName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE tp.travelPeriod
END AS travelPeriod,
IFNULL(f.familyCount, 0) AS familyCount
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
GROUP BY task.id
ORDER BY $orderColumn $orderBy, task.createdAt DESC, t.signupTime DESC, t.createdAt DESC
""");
listSql.setCondition(cnd);
listSql.setVar("orderColumn", getOrderColumn(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);
list.forEach(item -> item.put("lineName", item.getString("currentLineName")));
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@At
@SaCheckPermission("tour.schoolUnionApproval")
public Result detail(String id) {
TourLedger ledger = fetchAuditLedger(id);
if (ledger == null) {
return Result.error("报名记录不存在或无权查看");
}
ledger.setLineName(getCurrentLineName(ledger));
Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())
.and(TourLedgerFamily::getDelFlag, "=", false);
familyCnd.asc(TourLedgerFamily::getCreatedAt);
TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch(
Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())
.and(TourLedgerDirectRelative::getDelFlag, "=", false));
NutMap signupConfig = getSignupConfig(ledger);
return Result.success(NutMap.NEW()
.addv("ledger", ledger)
.addv("travelPeriod", getTravelPeriod(ledger))
.addv("families", tourLedgerFamilyService.query(familyCnd))
.addv("directRelative", directRelative)
.addv("allowFamily", signupConfig.getBoolean("allowFamily", false))
.addv("fillBedInfo", signupConfig.getBoolean("fillBedInfo", true))
.addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false)));
}
@At
@SaCheckPermission("tour.schoolUnionApproval")
public Result unionOptions(Boolean audit, Integer startYear, Integer endYear, String keyword, String lineId, String travelPeriod, String lineType) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, null, lineId, travelPeriod, lineType);
cnd.and("t.unionId", "IS NOT", null);
cnd.and("t.unionId", "<>", "");
Sql sql = Sqls.create("""
SELECT DISTINCT
t.unionId AS id,
COALESCE(NULLIF(u.name, ''), t.unionName) AS name,
u.unionCode
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union u ON u.id = t.unionId
$condition
ORDER BY u.unionCode ASC, name ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.schoolUnionApproval")
public Result lineOptions(Boolean audit, Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, null, travelPeriod, lineType);
cnd.and("t.lineName", "IS NOT", null);
cnd.and("t.lineName", "<>", "");
Sql sql = Sqls.create("""
SELECT DISTINCT
CASE
WHEN IFNULL(t.lineId, '') <> '' THEN CONCAT(t.lineId, '|', IFNULL(m.unionId, ''))
ELSE CONCAT('legacy:', t.lineName, '|', IFNULL(m.unionId, ''))
END AS lineId,
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS lineName
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
$condition
ORDER BY lineName ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.schoolUnionApproval")
public Result travelPeriodOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, null, lineType);
cnd.and("m.travelStartTime", "IS NOT", null);
cnd.and("m.travelStartTime", "<>", "");
cnd.and("m.travelEndTime", "IS NOT", null);
cnd.and("m.travelEndTime", "<>", "");
Sql sql = Sqls.create("""
SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
$condition
ORDER BY travelPeriod ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.schoolUnionApproval")
public Result lineTypeOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String travelPeriod, String unionId, String keyword) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, null);
cnd.and("t.lineType", "IS NOT", null);
cnd.and("t.lineType", "<>", "");
Sql sql = Sqls.create("""
SELECT DISTINCT t.lineType
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
$condition
ORDER BY t.lineType ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
private Cnd buildAuditCnd(Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) {
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.and("def.name", "=", WORKFLOW_KEY);
cnd.and("task.displayName", "=", TASK_DISPLAY_NAME);
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
appendAuditStateFilter(cnd, audit);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.andEX("t.unionId", "=", unionId);
cnd.andEX("t.lineType", "=", lineType);
appendLineFilter(cnd, lineId);
appendTravelPeriodFilter(cnd, travelPeriod);
appendKeywordFilter(cnd, keyword);
return cnd;
}
private void appendAuditStateFilter(Cnd cnd, Boolean audit) {
if (Boolean.TRUE.equals(audit)) {
cnd.and("task.taskState", "in", List.of(
ProcessTaskStateEnum.FINISHED.getCode(),
ProcessTaskStateEnum.WITHDRAW.getCode(),
ProcessTaskStateEnum.INTERRUPT.getCode()));
return;
}
cnd.and("task.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
private void appendKeywordFilter(Cnd cnd, String keyword) {
if (StrUtil.isBlank(keyword)) {
return;
}
SqlExpressionGroup group = new SqlExpressionGroup();
group.orLike("t.userName", keyword.trim());
group.orLike("t.jobNo", keyword.trim());
group.orLike("t.lineName", keyword.trim());
group.orLike("l.lineName", keyword.trim());
cnd.and(group);
}
private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) {
if (StrUtil.isBlank(travelPeriod)) {
return;
}
cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod);
}
private void appendLineFilter(Cnd cnd, String lineId) {
if (StrUtil.isBlank(lineId)) {
return;
}
LineFilter lineFilter = parseLineFilter(lineId);
if (lineFilter.getLineId().startsWith("legacy:")) {
SqlExpressionGroup legacyGroup = new SqlExpressionGroup();
legacyGroup.or("t.lineId", "IS", null);
legacyGroup.or("t.lineId", "=", "");
cnd.and(legacyGroup);
cnd.and("t.lineName", "=", lineFilter.getLineId().substring("legacy:".length()));
} else {
cnd.and("t.lineId", "=", lineFilter.getLineId());
}
appendMatterUnionFilter(cnd, lineFilter.getUnionId());
}
private LineFilter parseLineFilter(String value) {
int delimiterIndex = value.indexOf("|");
if (delimiterIndex < 0) {
return new LineFilter(value, null);
}
return new LineFilter(value.substring(0, delimiterIndex), value.substring(delimiterIndex + 1));
}
private void appendMatterUnionFilter(Cnd cnd, String unionId) {
if (unionId == null) {
return;
}
if (StrUtil.isBlank(unionId)) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.or("m.unionId", "IS", null);
group.or("m.unionId", "=", "");
cnd.and(group);
return;
}
cnd.and("m.unionId", "=", unionId);
}
private String getOrderColumn(String orderName) {
if ("jobNo".equals(orderName)) {
return "t.jobNo";
}
if ("userName".equals(orderName)) {
return "t.userName";
}
if ("lineName".equals(orderName)) {
return """
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END
""";
}
if ("travelPeriod".equals(orderName)) {
return "COALESCE(m.travelStartTime, tp.travelPeriod)";
}
if ("lineType".equals(orderName)) {
return "t.lineType";
}
if ("signupTime".equals(orderName)) {
return "t.signupTime";
}
if ("familyCount".equals(orderName)) {
return "familyCount";
}
if ("instanceState".equals(orderName)) {
return "ins.state";
}
return "task.createdAt";
}
private TourLedger fetchAuditLedger(String id) {
if (StrUtil.isBlank(id)) {
return null;
}
Sql sql = Sqls.create("""
SELECT COUNT(1)
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
WHERE t.delFlag = 0
AND t.id = @id
AND def.name = @workflowKey
AND task.displayName = @taskDisplayName
AND ta.actorId = @actorId
""");
sql.setParam("id", id);
sql.setParam("workflowKey", WORKFLOW_KEY);
sql.setParam("taskDisplayName", TASK_DISPLAY_NAME);
sql.setParam("actorId", SecurityUtil.getUserId());
sql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(sql);
return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null;
}
private String getCurrentLineName(TourLedger ledger) {
if (ledger == null) {
return "";
}
Sql sql = Sqls.create("""
SELECT CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS lineName
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
WHERE t.id = @ledgerId
""");
sql.setParam("ledgerId", ledger.getId());
sql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(sql);
return StrUtil.blankToDefault(sql.getString(), ledger.getLineName());
}
private String getTravelPeriod(TourLedger ledger) {
if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) {
return "";
}
if (StrUtil.isNotBlank(ledger.getMatterId())) {
Sql exactSql = Sqls.create("""
SELECT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE ''
END AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND id = @matterId
""");
exactSql.setParam("matterId", ledger.getMatterId());
exactSql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(exactSql);
return exactSql.getString();
}
Sql sql = Sqls.create("""
SELECT GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND `year` = @year
AND lineId = @lineId
""");
sql.setParam("year", ledger.getYear());
sql.setParam("lineId", ledger.getLineId());
sql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(sql);
return sql.getString();
}
private NutMap getSignupConfig(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getLineId())) {
return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false);
}
Sql sql = Sqls.create("""
SELECT
IFNULL(s.allowFamily, 0) AS allowFamily,
IFNULL(s.fillBedInfo, 1) AS fillBedInfo,
IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
WHERE t.id = @ledgerId
""");
sql.setParam("ledgerId", ledger.getId());
sql.setCallback(Sqls.callback.map());
tourLedgerService.dao().execute(sql);
NutMap map = sql.getObject(NutMap.class);
if (map == null || map.isEmpty()) {
return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false);
}
return map;
}
private static class LineFilter {
private final String lineId;
private final String unionId;
private LineFilter(String lineId, String unionId) {
this.lineId = lineId;
this.unionId = unionId;
}
private String getLineId() {
return lineId;
}
private String getUnionId() {
return unionId;
}
}
}
@@ -0,0 +1,246 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingLot;
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@IocBean
@Ok("json:full")
@At("/platform/tour/setting")
public class TourSettingController {
@Inject
private TourSettingService tourSettingService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/setting/index.html")
@SaCheckPermission("tour.setting")
public void index() {
}
@At
@SaCheckPermission("tour.setting")
public Result pageData(PageForm pageForm, Integer year, String configName) {
Cnd cnd = Cnd.NEW();
cnd.andEX(TourSetting::getYear, "=", year);
cnd.and(Cnd.likeEX(TourSetting::getConfigName, configName));
cnd.desc(TourSetting::getYear).asc(TourSetting::getSortNo).desc(TourSetting::getCreatedAt);
Pagination<TourSetting> pagination = tourSettingService.listPage(
pageForm.getPageNumber(),
pageForm.getPageSize(),
TourSetting.class,
cnd
);
return Result.success(pagination);
}
@At
@SaCheckPermission("tour.setting")
public Result detail(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
TourSetting tourSetting = tourSettingService.fetch(id);
if (tourSetting == null) {
return Result.error("配置不存在");
}
// 编辑页面需要一起带出标段,按标段值倒序保持与老疗休养配置一致。
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
return Result.success(tourSetting);
}
@At
@SaCheckPermission("tour.setting")
public Result previousYearInfo(Integer year) {
if (year == null) {
return Result.error("请先选择年度");
}
List<TourSetting> settings = tourSettingService.query(Cnd.where(TourSetting::getYear, "=", year - 1)
.and(TourSetting::getDelFlag, "=", false)
.desc(TourSetting::getUpdatedAt)
.desc(TourSetting::getCreatedAt));
if (Lang.isEmpty(settings)) {
return Result.error("未找到上一年度配置");
}
TourSetting tourSetting = settings.get(0);
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
return Result.success(tourSetting);
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.setting")
@SLog(type = "tour", tag = "疗休养设置", msg = "保存疗休养配置")
public Result doSubmit(TourSetting tourSetting,
@Param(value = "lots") String lots,
@Param(value = "lotDeleteList") String[] lotDeleteList) {
Result checkResult = check(tourSetting);
if (checkResult != null) {
return checkResult;
}
Cnd sameNameCnd = Cnd.where(TourSetting::getYear, "=", tourSetting.getYear())
.and(TourSetting::getConfigName, "=", tourSetting.getConfigName());
if (StrUtil.isNotBlank(tourSetting.getId())) {
sameNameCnd.and(TourSetting::getId, "<>", tourSetting.getId());
}
if (tourSettingService.count(sameNameCnd) > 0) {
return Result.error("同年度下配置名称已存在");
}
// 布尔值给默认值,避免前端未传时出现空状态。
if (tourSetting.getEnabled() == null) {
tourSetting.setEnabled(true);
}
if (tourSetting.getAllowFamily() == null) {
tourSetting.setAllowFamily(false);
}
if (tourSetting.getFillBedInfo() == null) {
tourSetting.setFillBedInfo(true);
}
if (StrUtil.isBlank(tourSetting.getOutProvinceRatioType())) {
tourSetting.setOutProvinceRatioType("当年报名人数");
}
if (!"固定人数".equals(tourSetting.getOutProvinceRatioType())) {
tourSetting.setOutProvinceFixedPeople(0);
}
// 前端按项目既有约定把子表数组序列化提交,这里显式解析,避免自动绑定漏掉标段。
if (StrUtil.isNotBlank(lots)) {
try {
tourSetting.setLots(Json.fromJsonAsList(TourSettingLot.class, lots));
} catch (Exception e) {
return Result.error("标段值和标段费用必须为整数");
}
}
tourSetting.setLots(normalizeLots(tourSetting));
Result lotCheckResult = checkLots(tourSetting.getLots());
if (lotCheckResult != null) {
return lotCheckResult;
}
if (StrUtil.isBlank(tourSetting.getId())) {
if (Lang.isEmpty(tourSetting.getLots())) {
tourSettingService.insert(tourSetting);
} else {
tourSettingService.insertWith(tourSetting, "lots");
}
} else {
// 编辑时先处理页面删除的标段,再保存配置和当前标段行。
if (Lang.isNotEmpty(lotDeleteList)) {
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getId, "in", lotDeleteList));
}
tourSettingService.updateIgnoreNull(tourSetting);
saveLots(tourSetting);
}
return Result.success();
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.setting")
@SLog(type = "tour", tag = "疗休养设置", msg = "删除疗休养配置")
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id));
tourSettingService.delete(id);
return Result.success();
}
private List<TourSettingLot> normalizeLots(TourSetting tourSetting) {
if (tourSetting == null || Lang.isEmpty(tourSetting.getLots())) {
return Collections.emptyList();
}
// 过滤前端空行,避免误写入只有ID或空字段的标段记录。
return tourSetting.getLots().stream()
.filter(item -> item != null
&& (StrUtil.isNotBlank(item.getLotName())
|| StrUtil.isNotBlank(item.getLotValue())
|| item.getActivityCost() != null))
.peek(item -> item.setSettingId(tourSetting.getId()))
.collect(Collectors.toList());
}
private void saveLots(TourSetting tourSetting) {
List<TourSettingLot> lots = tourSetting.getLots();
if (Lang.isEmpty(lots)) {
return;
}
lots.forEach(item -> item.setSettingId(tourSetting.getId()));
tourSettingService.dao().insertOrUpdate(lots);
}
private Result check(TourSetting tourSetting) {
if (tourSetting == null) {
return Result.error("参数错误");
}
if (tourSetting.getYear() == null) {
return Result.error("年度不能为空");
}
if (StrUtil.isBlank(tourSetting.getConfigName())) {
return Result.error("配置名称不能为空");
}
if (tourSetting.getMinGroupPeople() != null && tourSetting.getMaxGroupPeople() != null
&& tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) {
return Result.error("最少成团人数不能大于最多成团人数");
}
if (tourSetting.getCycleStartYear() != null && tourSetting.getCycleEndYear() != null
&& tourSetting.getCycleStartYear() > tourSetting.getCycleEndYear()) {
return Result.error("周期开始年度不能大于周期结束年度");
}
if (StrUtil.isNotBlank(tourSetting.getOutProvinceRatioType())
&& !"当年参加人数".equals(tourSetting.getOutProvinceRatioType())
&& !"当年报名人数".equals(tourSetting.getOutProvinceRatioType())
&& !"可参加教职工人数".equals(tourSetting.getOutProvinceRatioType())
&& !"固定人数".equals(tourSetting.getOutProvinceRatioType())) {
return Result.error("省外人数占比类型不正确");
}
if ("固定人数".equals(tourSetting.getOutProvinceRatioType())
&& (tourSetting.getOutProvinceFixedPeople() == null || tourSetting.getOutProvinceFixedPeople() < 0)) {
return Result.error("固定人数必须大于等于0");
}
return null;
}
private Result checkLots(List<TourSettingLot> lots) {
if (Lang.isEmpty(lots)) {
return null;
}
for (int i = 0; i < lots.size(); i++) {
TourSettingLot lot = lots.get(i);
String rowNo = "" + (i + 1) + "";
if (StrUtil.isBlank(lot.getLotValue()) || !lot.getLotValue().matches("^\\d+$")) {
return Result.error(rowNo + "标段值必须为整数");
}
if (lot.getActivityCost() == null || lot.getActivityCost() < 0) {
return Result.error(rowNo + "标段费用必须为整数");
}
}
return null;
}
}
@@ -0,0 +1,160 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency;
import com.budwk.app.zhgh.dayofficework.tour.service.TourTravelAgencyService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
@IocBean
@Ok("json:full")
@At("/platform/tour/travelAgency")
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.-]+$";
@Inject
private TourTravelAgencyService travelAgencyService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/travelAgency/index.html")
@SaCheckPermission("tour.travelAgency")
public void index() {
}
@At
@SaCheckPermission("tour.travelAgency")
public Result pageData(PageForm pageForm, Integer year, String agencyName, String contactName, String contactPhone) {
Cnd cnd = Cnd.NEW();
cnd.andEX(TourTravelAgency::getYear, "=", year);
cnd.and(Cnd.likeEX(TourTravelAgency::getAgencyName, agencyName));
cnd.and(Cnd.likeEX(TourTravelAgency::getContactName, contactName));
cnd.and(Cnd.likeEX(TourTravelAgency::getContactPhone, contactPhone));
applyOrder(cnd, pageForm);
Pagination<TourTravelAgency> pagination = travelAgencyService.listPage(
pageForm.getPageNumber(),
pageForm.getPageSize(),
TourTravelAgency.class,
cnd
);
return Result.success(pagination);
}
@At
@SaCheckPermission("tour.travelAgency")
public Result detail(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
TourTravelAgency agency = travelAgencyService.fetch(id);
return agency == null ? Result.error("旅行社不存在") : Result.success(agency);
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.travelAgency")
@SLog(type = "tour", tag = "旅行社管理", msg = "保存旅行社信息")
public Result doSubmit(TourTravelAgency agency) {
Result checkResult = check(agency);
if (checkResult != null) {
return checkResult;
}
Cnd sameCodeCnd = Cnd.where(TourTravelAgency::getYear, "=", agency.getYear())
.and(TourTravelAgency::getAgencyCode, "=", agency.getAgencyCode());
if (StrUtil.isNotBlank(agency.getId())) {
sameCodeCnd.and(TourTravelAgency::getId, "<>", agency.getId());
}
if (travelAgencyService.count(sameCodeCnd) > 0) {
return Result.error("同年度下旅行社编号已存在");
}
if (agency.getEnabled() == null) {
agency.setEnabled(true);
}
if (StrUtil.isBlank(agency.getId())) {
travelAgencyService.insert(agency);
} else {
travelAgencyService.updateIgnoreNull(agency);
}
return Result.success();
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.travelAgency")
@SLog(type = "tour", tag = "旅行社管理", msg = "删除旅行社信息")
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
travelAgencyService.delete(id);
return Result.success();
}
private Result check(TourTravelAgency agency) {
if (agency == null) {
return Result.error("参数错误");
}
if (agency.getYear() == null) {
return Result.error("年度不能为空");
}
if (StrUtil.isBlank(agency.getAgencyName())) {
return Result.error("旅行社名称不能为空");
}
if (StrUtil.isBlank(agency.getAgencyCode())) {
return Result.error("旅行社编号不能为空");
}
if (StrUtil.isBlank(agency.getContactName())) {
return Result.error("联系人不能为空");
}
if (StrUtil.isBlank(agency.getContactPhone())) {
return Result.error("联系人手机不能为空");
}
if (!agency.getContactPhone().matches(MOBILE_PATTERN)) {
return Result.error("联系人手机格式不正确");
}
if (StrUtil.isBlank(agency.getEmail())) {
return Result.error("邮箱不能为空");
}
if (!agency.getEmail().matches(EMAIL_PATTERN)) {
return Result.error("邮箱格式不正确");
}
return null;
}
private void applyOrder(Cnd cnd, PageForm pageForm) {
String orderName = pageForm.getPageOrderName();
String orderBy = pageForm.getPageOrderBy();
if (StrUtil.isBlank(orderName)) {
cnd.asc(TourTravelAgency::getYear).asc(TourTravelAgency::getAgencyCode).desc(TourTravelAgency::getCreatedAt);
return;
}
boolean descending = "descending".equals(orderBy);
if ("year".equals(orderName)) {
if (descending) {
cnd.desc(TourTravelAgency::getYear);
} else {
cnd.asc(TourTravelAgency::getYear);
}
} else if ("agencyCode".equals(orderName)) {
if (descending) {
cnd.desc(TourTravelAgency::getAgencyCode);
} else {
cnd.asc(TourTravelAgency::getAgencyCode);
}
} else {
cnd.asc(TourTravelAgency::getYear).asc(TourTravelAgency::getAgencyCode).desc(TourTravelAgency::getCreatedAt);
}
}
}
@@ -0,0 +1,580 @@
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.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.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily;
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;
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.List;
@IocBean
@Ok("json:full")
@At("/platform/tour/unionApproval")
public class TourUnionApprovalController {
private static final String WORKFLOW_KEY = "LXYBZXQSXL";
private static final String TASK_DISPLAY_NAME = "分工会审核";
@Inject
private TourLedgerService tourLedgerService;
@Inject
private TourLedgerFamilyService tourLedgerFamilyService;
@Inject
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/unionApproval/index.html")
@SaCheckPermission("tour.unionApproval")
public void index() {
}
@At("/h5")
@Ok("beetl:/platform/zhghh5/dayofficework/tour/unionApproval/index.html")
@SaCheckPermission("tour.unionApproval")
public void h5() {
}
@At
@SaCheckPermission("tour.unionApproval")
public Result pageData(PageForm pageForm, Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType);
Sql countSql = Sqls.create("""
SELECT COUNT(DISTINCT task.id)
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT
t.*,
ins.id AS instanceId,
ins.businessNo,
ins.state AS instanceState,
ins.variable AS instanceVariable,
ins.processDefineId AS instanceProcessDefineId,
task.id AS taskId,
task.taskName AS taskKey,
task.displayName AS taskName,
task.taskType,
task.performType AS taskPerformType,
task.taskState,
task.finishTime,
task.taskParentId,
task.variable AS taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName,
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS currentLineName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE tp.travelPeriod
END AS travelPeriod,
IFNULL(f.familyCount, 0) AS familyCount
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
GROUP BY task.id
ORDER BY $orderColumn $orderBy, task.createdAt DESC, t.signupTime DESC, t.createdAt DESC
""");
listSql.setCondition(cnd);
listSql.setVar("orderColumn", getOrderColumn(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);
list.forEach(item -> item.put("lineName", item.getString("currentLineName")));
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@At
@SaCheckPermission("tour.unionApproval")
public Result detail(String id) {
TourLedger ledger = fetchAuditLedger(id);
if (ledger == null) {
return Result.error("报名记录不存在或无权查看");
}
ledger.setLineName(getCurrentLineName(ledger));
Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())
.and(TourLedgerFamily::getDelFlag, "=", false);
familyCnd.asc(TourLedgerFamily::getCreatedAt);
TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch(
Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())
.and(TourLedgerDirectRelative::getDelFlag, "=", false));
NutMap signupConfig = getSignupConfig(ledger);
return Result.success(NutMap.NEW()
.addv("ledger", ledger)
.addv("travelPeriod", getTravelPeriod(ledger))
.addv("families", tourLedgerFamilyService.query(familyCnd))
.addv("directRelative", directRelative)
.addv("allowFamily", signupConfig.getBoolean("allowFamily", false))
.addv("fillBedInfo", signupConfig.getBoolean("fillBedInfo", true))
.addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false)));
}
@At
@SaCheckPermission("tour.unionApproval")
public Result unionOptions(Boolean audit, Integer startYear, Integer endYear, String keyword, String lineId, String travelPeriod, String lineType) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, null, lineId, travelPeriod, lineType);
cnd.and("t.unionId", "IS NOT", null);
cnd.and("t.unionId", "<>", "");
Sql sql = Sqls.create("""
SELECT DISTINCT
t.unionId AS id,
COALESCE(NULLIF(u.name, ''), t.unionName) AS name,
u.unionCode
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union u ON u.id = t.unionId
$condition
ORDER BY u.unionCode ASC, name ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.unionApproval")
public Result lineOptions(Boolean audit, Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, null, travelPeriod, lineType);
cnd.and("t.lineName", "IS NOT", null);
cnd.and("t.lineName", "<>", "");
Sql sql = Sqls.create("""
SELECT DISTINCT
CASE
WHEN IFNULL(t.lineId, '') <> '' THEN CONCAT(t.lineId, '|', IFNULL(m.unionId, ''))
ELSE CONCAT('legacy:', t.lineName, '|', IFNULL(m.unionId, ''))
END AS lineId,
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS lineName
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
$condition
ORDER BY lineName ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.unionApproval")
public Result travelPeriodOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, null, lineType);
cnd.and("m.travelStartTime", "IS NOT", null);
cnd.and("m.travelStartTime", "<>", "");
cnd.and("m.travelEndTime", "IS NOT", null);
cnd.and("m.travelEndTime", "<>", "");
Sql sql = Sqls.create("""
SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
$condition
ORDER BY travelPeriod ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.unionApproval")
public Result lineTypeOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String travelPeriod, String unionId, String keyword) {
Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, null);
cnd.and("t.lineType", "IS NOT", null);
cnd.and("t.lineType", "<>", "");
Sql sql = Sqls.create("""
SELECT DISTINCT t.lineType
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
$condition
ORDER BY t.lineType ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
private Cnd buildAuditCnd(Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) {
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.and("def.name", "=", WORKFLOW_KEY);
cnd.and("task.displayName", "=", TASK_DISPLAY_NAME);
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
appendAuditStateFilter(cnd, audit);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.andEX("t.unionId", "=", unionId);
cnd.andEX("t.lineType", "=", lineType);
appendLineFilter(cnd, lineId);
appendTravelPeriodFilter(cnd, travelPeriod);
appendKeywordFilter(cnd, keyword);
return cnd;
}
private void appendAuditStateFilter(Cnd cnd, Boolean audit) {
if (Boolean.TRUE.equals(audit)) {
cnd.and("task.taskState", "in", List.of(
ProcessTaskStateEnum.FINISHED.getCode(),
ProcessTaskStateEnum.WITHDRAW.getCode(),
ProcessTaskStateEnum.INTERRUPT.getCode()));
return;
}
cnd.and("task.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
private void appendKeywordFilter(Cnd cnd, String keyword) {
if (StrUtil.isBlank(keyword)) {
return;
}
SqlExpressionGroup group = new SqlExpressionGroup();
group.orLike("t.userName", keyword.trim());
group.orLike("t.jobNo", keyword.trim());
group.orLike("t.lineName", keyword.trim());
group.orLike("l.lineName", keyword.trim());
cnd.and(group);
}
private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) {
if (StrUtil.isBlank(travelPeriod)) {
return;
}
cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod);
}
private void appendLineFilter(Cnd cnd, String lineId) {
if (StrUtil.isBlank(lineId)) {
return;
}
LineFilter lineFilter = parseLineFilter(lineId);
if (lineFilter.getLineId().startsWith("legacy:")) {
SqlExpressionGroup legacyGroup = new SqlExpressionGroup();
legacyGroup.or("t.lineId", "IS", null);
legacyGroup.or("t.lineId", "=", "");
cnd.and(legacyGroup);
cnd.and("t.lineName", "=", lineFilter.getLineId().substring("legacy:".length()));
} else {
cnd.and("t.lineId", "=", lineFilter.getLineId());
}
appendMatterUnionFilter(cnd, lineFilter.getUnionId());
}
private LineFilter parseLineFilter(String value) {
int delimiterIndex = value.indexOf("|");
if (delimiterIndex < 0) {
return new LineFilter(value, null);
}
return new LineFilter(value.substring(0, delimiterIndex), value.substring(delimiterIndex + 1));
}
private void appendMatterUnionFilter(Cnd cnd, String unionId) {
if (unionId == null) {
return;
}
if (StrUtil.isBlank(unionId)) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.or("m.unionId", "IS", null);
group.or("m.unionId", "=", "");
cnd.and(group);
return;
}
cnd.and("m.unionId", "=", unionId);
}
private String getOrderColumn(String orderName) {
if ("jobNo".equals(orderName)) {
return "t.jobNo";
}
if ("userName".equals(orderName)) {
return "t.userName";
}
if ("lineName".equals(orderName)) {
return """
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END
""";
}
if ("travelPeriod".equals(orderName)) {
return "COALESCE(m.travelStartTime, tp.travelPeriod)";
}
if ("lineType".equals(orderName)) {
return "t.lineType";
}
if ("signupTime".equals(orderName)) {
return "t.signupTime";
}
if ("familyCount".equals(orderName)) {
return "familyCount";
}
if ("instanceState".equals(orderName)) {
return "ins.state";
}
return "task.createdAt";
}
private TourLedger fetchAuditLedger(String id) {
if (StrUtil.isBlank(id)) {
return null;
}
Sql sql = Sqls.create("""
SELECT COUNT(1)
FROM wf_process_task task
LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId
LEFT JOIN wf_process_define def ON def.id = ins.processDefineId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id
INNER JOIN tour_ledger t ON t.id = ins.businessNo
WHERE t.delFlag = 0
AND t.id = @id
AND def.name = @workflowKey
AND task.displayName = @taskDisplayName
AND ta.actorId = @actorId
""");
sql.setParam("id", id);
sql.setParam("workflowKey", WORKFLOW_KEY);
sql.setParam("taskDisplayName", TASK_DISPLAY_NAME);
sql.setParam("actorId", SecurityUtil.getUserId());
sql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(sql);
return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null;
}
private String getCurrentLineName(TourLedger ledger) {
if (ledger == null) {
return "";
}
Sql sql = Sqls.create("""
SELECT CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS lineName
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
WHERE t.id = @ledgerId
""");
sql.setParam("ledgerId", ledger.getId());
sql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(sql);
return StrUtil.blankToDefault(sql.getString(), ledger.getLineName());
}
private String getTravelPeriod(TourLedger ledger) {
if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) {
return "";
}
if (StrUtil.isNotBlank(ledger.getMatterId())) {
Sql exactSql = Sqls.create("""
SELECT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE ''
END AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND id = @matterId
""");
exactSql.setParam("matterId", ledger.getMatterId());
exactSql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(exactSql);
return exactSql.getString();
}
Sql sql = Sqls.create("""
SELECT GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND `year` = @year
AND lineId = @lineId
""");
sql.setParam("year", ledger.getYear());
sql.setParam("lineId", ledger.getLineId());
sql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(sql);
return sql.getString();
}
private NutMap getSignupConfig(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getLineId())) {
return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false);
}
Sql sql = Sqls.create("""
SELECT
IFNULL(s.allowFamily, 0) AS allowFamily,
IFNULL(s.fillBedInfo, 1) AS fillBedInfo,
IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
WHERE t.id = @ledgerId
""");
sql.setParam("ledgerId", ledger.getId());
sql.setCallback(Sqls.callback.map());
tourLedgerService.dao().execute(sql);
NutMap map = sql.getObject(NutMap.class);
if (map == null || map.isEmpty()) {
return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false);
}
return map;
}
private static class LineFilter {
private final String lineId;
private final String unionId;
private LineFilter(String lineId, String unionId) {
this.lineId = lineId;
this.unionId = unionId;
}
private String getLineId() {
return lineId;
}
private String getUnionId() {
return unionId;
}
}
}
@@ -0,0 +1,905 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_union;
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;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily;
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;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
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;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
@IocBean
@Ok("json:full")
@At("/platform/tour/unionledger")
public class TourUnionLedgerController {
@Inject
private TourLedgerService tourLedgerService;
@Inject
private TourLedgerFamilyService tourLedgerFamilyService;
@Inject
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/unionledger/index.html")
@SaCheckPermission("tour.unionledger")
public void index() {
}
@At
@SaCheckPermission("tour.unionledger")
public Result pageData(PageForm pageForm, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, String scopeType, Boolean overCostOnly) {
String currentUnionId = currentUnionId();
if (StrUtil.isBlank(currentUnionId)) {
return Result.error("当前登录人所属工会不能为空");
}
Cnd cnd = buildQueryCnd(currentUnionId, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, scopeType, overCostOnly);
Sql countSql = Sqls.create("""
SELECT COUNT(DISTINCT t.id)
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT
t.*,
ins.id AS instanceId,
ins.state AS instanceState,
ins.processDefineId AS instanceProcessDefineId,
IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine,
IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName,
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS currentLineName,
IFNULL(f.familyCount, 0) AS familyCount,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE tp.travelPeriod
END AS travelPeriod
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
GROUP BY t.id
ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC
""");
listSql.setCondition(cnd);
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);
var list = listSql.getList(NutMap.class);
list.forEach(item -> item.put("lineName", item.getString("currentLineName")));
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("tour.unionledger")
public void exportOverCostSummary(PageForm pageForm, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, String scopeType, HttpServletResponse response) {
String currentUnionId = currentUnionId();
if (StrUtil.isBlank(currentUnionId)) {
return;
}
Cnd cnd = buildQueryCnd(currentUnionId, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, scopeType, true);
List<NutMap> list = queryOverCostSummaryList(cnd, pageForm);
Workbook workbook = buildOverCostSummaryWorkbook(currentUnionName(), list);
CommonDownloadUtil.download("5天外超出部分疗休养费用由单位承担申请人员汇总表.xls", workbook, response);
}
@At
@SaCheckPermission("tour.unionledger")
public Result detail(String id) {
TourLedger ledger = fetchScopedLedger(id);
if (ledger == null) {
return Result.error("台账记录不存在或无权查看");
}
ledger.setLineName(getCurrentLineName(ledger));
Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", id)
.and(TourLedgerFamily::getDelFlag, "=", false);
familyCnd.asc(TourLedgerFamily::getCreatedAt);
TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch(
Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false)
.and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
return Result.success(NutMap.NEW()
.addv("ledger", ledger)
.addv("travelPeriod", getTravelPeriod(ledger))
.addv("families", tourLedgerFamilyService.query(familyCnd))
.addv("directRelative", directRelative)
.addv("fillBedInfo", isFillBedInfo(ledger))
.addv("directFamilyUnitLine", isDirectFamilyUnitLine(ledger)));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.unionledger")
@SLog(type = "tour", tag = "分工会疗休养台账", msg = "删除分工会疗休养台账")
public Result doDelete(String id) {
TourLedger ledger = fetchScopedLedger(id);
if (ledger == null) {
return Result.error("台账记录不存在或无权删除");
}
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
return Result.success();
}
@At
@SaCheckPermission("tour.unionledger")
public Result unionOptions(Integer startYear, Integer endYear) {
String currentUnionId = currentUnionId();
if (StrUtil.isBlank(currentUnionId)) {
return Result.error("当前登录人所属工会不能为空");
}
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.and("t.unionId", "IS NOT", null);
cnd.and("t.unionId", "<>", "");
applyUnionLedgerScope(cnd, currentUnionId);
Sql sql = Sqls.create("""
SELECT DISTINCT
t.unionId AS id,
COALESCE(NULLIF(u.name, ''), t.unionName) AS name,
u.unionCode
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN sys_union u ON u.id = t.unionId
$condition
ORDER BY
CASE WHEN t.unionId = @currentUnionId THEN 0 ELSE 1 END,
u.unionCode ASC,
name ASC
""");
sql.setCondition(cnd);
sql.setParam("currentUnionId", currentUnionId);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.unionledger")
public Result lineOptions(Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword, String scopeType) {
String currentUnionId = currentUnionId();
if (StrUtil.isBlank(currentUnionId)) {
return Result.error("当前登录人所属工会不能为空");
}
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.and("t.lineName", "IS NOT", null);
cnd.and("t.lineName", "<>", "");
cnd.andEX("t.unionId", "=", unionId);
cnd.andEX("t.lineType", "=", lineType);
appendTravelPeriodFilter(cnd, travelPeriod);
appendScopeFilter(cnd, currentUnionId, scopeType);
appendKeywordFilter(cnd, keyword);
Sql sql = Sqls.create("""
SELECT DISTINCT
CASE
WHEN IFNULL(t.lineId, '') <> '' THEN CONCAT(t.lineId, '|', IFNULL(m.unionId, ''))
ELSE CONCAT('legacy:', t.lineName, '|', IFNULL(m.unionId, ''))
END AS lineId,
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS lineName
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
$condition
ORDER BY lineName ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.unionledger")
public Result travelPeriodOptions(Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword, String scopeType) {
String currentUnionId = currentUnionId();
if (StrUtil.isBlank(currentUnionId)) {
return Result.error("当前登录人所属工会不能为空");
}
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.and("m.travelStartTime", "IS NOT", null);
cnd.and("m.travelStartTime", "<>", "");
cnd.and("m.travelEndTime", "IS NOT", null);
cnd.and("m.travelEndTime", "<>", "");
cnd.andEX("t.unionId", "=", unionId);
cnd.andEX("t.lineType", "=", lineType);
appendLineFilter(cnd, lineId);
appendScopeFilter(cnd, currentUnionId, scopeType);
appendKeywordFilter(cnd, keyword);
Sql sql = Sqls.create("""
SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
$condition
ORDER BY travelPeriod ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.unionledger")
public Result lineTypeOptions(Integer startYear, Integer endYear, String lineId, String travelPeriod, String unionId, String keyword, String scopeType) {
String currentUnionId = currentUnionId();
if (StrUtil.isBlank(currentUnionId)) {
return Result.error("当前登录人所属工会不能为空");
}
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.and("t.lineType", "IS NOT", null);
cnd.and("t.lineType", "<>", "");
cnd.andEX("t.unionId", "=", unionId);
appendLineFilter(cnd, lineId);
appendTravelPeriodFilter(cnd, travelPeriod);
appendScopeFilter(cnd, currentUnionId, scopeType);
appendKeywordFilter(cnd, keyword);
Sql sql = Sqls.create("""
SELECT DISTINCT t.lineType
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
$condition
ORDER BY t.lineType ASC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission("tour.unionledger")
public Result summaryStats(Integer startYear, Integer endYear, String unionId, String lineId, String travelPeriod, String lineType) {
String currentUnionId = currentUnionId();
if (StrUtil.isBlank(currentUnionId)) {
return Result.error("当前登录人所属工会不能为空");
}
Cnd ownCnd = buildSummaryCnd(currentUnionId, startYear, endYear, unionId, lineId, travelPeriod, lineType, "ownUnionJoined");
Cnd organizedCnd = buildSummaryCnd(currentUnionId, startYear, endYear, unionId, lineId, travelPeriod, lineType, "organizedLineJoined");
Sql ownSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
$condition
""");
ownSql.setCondition(ownCnd);
ownSql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(ownSql);
Sql organizedSql = Sqls.create("""
SELECT CAST(IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS SIGNED)
FROM tour_ledger t
INNER JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
$condition
""");
organizedSql.setCondition(organizedCnd);
organizedSql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(organizedSql);
return Result.success(NutMap.NEW()
.addv("ownUnionJoinedStaffCount", ownSql.getInt())
.addv("organizedLineJoinedTotalCount", organizedSql.getInt()));
}
private List<NutMap> queryOverCostSummaryList(Cnd cnd, PageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
t.id,
t.userName,
t.idCard,
t.unionName,
vu.mobile,
m.travelStartTime,
m.travelEndTime,
m.estimatedCost,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE tp.travelPeriod
END AS travelPeriod,
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS currentLineName
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC
""");
sql.setCondition(cnd);
sql.setVar("orderColumn", getOrderColumn(pageForm == null ? null : pageForm.getPageOrderName()));
sql.setVar("orderBy", pageForm != null && "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc");
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
List<NutMap> list = sql.getList(NutMap.class);
for (NutMap item : list) {
item.put("lineName", item.getString("currentLineName"));
item.put("totalDays", calcDays(item.getString("travelStartTime", ""), item.getString("travelEndTime", "")));
item.put("estimatedCostText", formatAmount(item.get("estimatedCost")));
}
return list;
}
private Workbook buildOverCostSummaryWorkbook(String unionName, List<NutMap> list) {
Workbook workbook = new HSSFWorkbook();
Sheet sheet = workbook.createSheet("汇总表");
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 9));
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 9));
sheet.addMergedRegion(new CellRangeAddress(2, 2, 0, 9));
double[] widths = {5.14, 14.14, 21.29, 15.71, 27, 40, 50.43, 13.71, 22.57, 15.43};
for (int i = 0; i < widths.length; i++) {
sheet.setColumnWidth(i, (int) (widths[i] * 256));
}
CellStyle attachStyle = createStyle(workbook, "宋体", (short) 12, false, HorizontalAlignment.LEFT, false, false);
CellStyle titleStyle = createStyle(workbook, "黑体", (short) 18, true, HorizontalAlignment.CENTER, false, false);
CellStyle unionStyle = createStyle(workbook, "仿宋_GB2312", (short) 14, true, HorizontalAlignment.LEFT, false, false);
unionStyle.setBorderBottom(BorderStyle.THIN);
CellStyle headerStyle = createStyle(workbook, "宋体", (short) 12, true, HorizontalAlignment.CENTER, true, true);
CellStyle bodyStyle = createStyle(workbook, "宋体", (short) 12, false, HorizontalAlignment.CENTER, false, true);
Row row1 = sheet.createRow(0);
row1.setHeightInPoints(32);
setCell(row1, 0, "附件12", attachStyle);
fillMergedCells(row1, 1, 9, attachStyle);
Row row2 = sheet.createRow(1);
row2.setHeightInPoints(32);
setCell(row2, 0, "5天外超出部分疗休养费用由单位承担申请人员汇总表", titleStyle);
fillMergedCells(row2, 1, 9, titleStyle);
Row row3 = sheet.createRow(2);
row3.setHeightInPoints(32);
setCell(row3, 0, "分工会:" + StrUtil.blankToDefault(unionName, ""), unionStyle);
fillMergedCells(row3, 1, 9, unionStyle);
String[] headers = {"序号", "姓名", "身份证号码", "电话号码", "所属分工会", "所选线路名称", "疗休养时间", "疗休养时长", "疗休养费用总额(元)", "备注"};
Row header = sheet.createRow(3);
header.setHeightInPoints(43);
for (int i = 0; i < headers.length; i++) {
setCell(header, i, headers[i], headerStyle);
}
int rowCount = Math.max(list == null ? 0 : list.size(), 20);
for (int i = 0; i < rowCount; i++) {
Row row = sheet.createRow(i + 4);
row.setHeightInPoints(25);
NutMap item = list != null && i < list.size() ? list.get(i) : null;
setCell(row, 0, String.valueOf(i + 1), bodyStyle);
setCell(row, 1, item == null ? "" : item.getString("userName", ""), bodyStyle);
setCell(row, 2, item == null ? "" : item.getString("idCard", ""), bodyStyle);
setCell(row, 3, item == null ? "" : item.getString("mobile", ""), bodyStyle);
setCell(row, 4, item == null ? "" : item.getString("unionName", ""), bodyStyle);
setCell(row, 5, item == null ? "" : item.getString("lineName", ""), bodyStyle);
setCell(row, 6, item == null ? "" : item.getString("travelPeriod", ""), bodyStyle);
setCell(row, 7, item == null ? "" : item.getString("totalDays", ""), bodyStyle);
setCell(row, 8, item == null ? "" : item.getString("estimatedCostText", ""), bodyStyle);
setCell(row, 9, "", bodyStyle);
}
return workbook;
}
private CellStyle createStyle(Workbook workbook, String fontName, short fontSize, boolean bold, HorizontalAlignment alignment, boolean wrap, boolean border) {
Font font = workbook.createFont();
font.setFontName(fontName);
font.setFontHeightInPoints(fontSize);
font.setBold(bold);
CellStyle style = workbook.createCellStyle();
style.setFont(font);
style.setAlignment(alignment);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setWrapText(wrap);
if (border) {
style.setBorderLeft(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
}
return style;
}
private void setCell(Row row, int col, String value, CellStyle style) {
Cell cell = row.createCell(col);
cell.setCellValue(StrUtil.blankToDefault(value, ""));
cell.setCellStyle(style);
}
private void fillMergedCells(Row row, int startCol, int endCol, CellStyle style) {
for (int i = startCol; i <= endCol; i++) {
setCell(row, i, "", style);
}
}
private Integer calcDays(String start, String end) {
if (StrUtil.isBlank(start) || StrUtil.isBlank(end)) {
return null;
}
try {
LocalDate startDate = LocalDate.parse(start.substring(0, 10));
LocalDate endDate = LocalDate.parse(end.substring(0, 10));
return Math.toIntExact(ChronoUnit.DAYS.between(startDate, endDate) + 1);
} catch (Exception e) {
return null;
}
}
private String formatAmount(Object value) {
if (value == null) {
return "";
}
try {
BigDecimal amount = new BigDecimal(String.valueOf(value));
return amount.stripTrailingZeros().toPlainString();
} catch (Exception e) {
return String.valueOf(value);
}
}
private Cnd buildQueryCnd(String currentUnionId, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, String scopeType, Boolean overCostOnly) {
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.andEX("t.unionId", "=", unionId);
cnd.andEX("t.lineType", "=", lineType);
if (Boolean.TRUE.equals(overCostOnly)) {
cnd.and("t.overCostReimbursed", "=", true);
}
appendLineFilter(cnd, lineId);
appendTravelPeriodFilter(cnd, travelPeriod);
appendScopeFilter(cnd, currentUnionId, scopeType);
appendKeywordFilter(cnd, keyword);
return cnd;
}
private Cnd buildSummaryCnd(String currentUnionId, Integer startYear, Integer endYear, String unionId, String lineId, String travelPeriod, String lineType, String scopeType) {
Cnd cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.andEX("t.`year`", ">=", startYear);
cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear);
cnd.andEX("t.unionId", "=", unionId);
cnd.andEX("t.lineType", "=", lineType);
appendLineFilter(cnd, lineId);
appendTravelPeriodFilter(cnd, travelPeriod);
appendScopeFilter(cnd, currentUnionId, scopeType);
return cnd;
}
private void appendKeywordFilter(Cnd cnd, String keyword) {
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.orLike("t.userName", keyword.trim());
group.orLike("t.jobNo", keyword.trim());
cnd.and(group);
}
}
private void applyUnionLedgerScope(Cnd cnd, String currentUnionId) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.or("t.unionId", "=", currentUnionId);
group.or("m.unionId", "=", currentUnionId);
cnd.and(group);
}
private void appendScopeFilter(Cnd cnd, String currentUnionId, String scopeType) {
if ("ownUnionJoined".equals(scopeType)) {
cnd.and("t.unionId", "=", currentUnionId);
return;
}
if ("organizedLineJoined".equals(scopeType)) {
cnd.and("m.unionId", "=", currentUnionId);
return;
}
applyUnionLedgerScope(cnd, currentUnionId);
}
private String buildYearCondition(Integer startYear, Integer endYear) {
StringBuilder builder = new StringBuilder();
if (startYear != null) {
builder.append(" AND t.`year` >= ").append(startYear);
}
if (endYear != null) {
builder.append(" AND t.`year` <= ").append(endYear);
}
return builder.toString();
}
private TourLedger fetchScopedLedger(String id) {
if (StrUtil.isBlank(id) || StrUtil.isBlank(currentUnionId())) {
return null;
}
Sql sql = Sqls.create("""
SELECT COUNT(1)
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
WHERE t.delFlag = 0
AND t.id = @id
AND (t.unionId = @unionId OR m.unionId = @unionId)
""");
sql.setParam("id", id);
sql.setParam("unionId", currentUnionId());
sql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(sql);
return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null;
}
private String getTravelPeriod(TourLedger ledger) {
if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) {
return "";
}
if (StrUtil.isNotBlank(ledger.getMatterId())) {
Sql exactSql = Sqls.create("""
SELECT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE ''
END AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND id = @matterId
""");
exactSql.setParam("matterId", ledger.getMatterId());
exactSql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(exactSql);
return exactSql.getString();
}
Sql sql = Sqls.create("""
SELECT GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND `year` = @year
AND lineId = @lineId
""");
sql.setParam("year", ledger.getYear());
sql.setParam("lineId", ledger.getLineId());
sql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(sql);
return sql.getString();
}
private boolean isDirectFamilyUnitLine(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getLineId())) {
return false;
}
Sql sql = Sqls.create("""
SELECT IFNULL(directFamilyUnitLine, 0)
FROM tour_line
WHERE delFlag = 0
AND id = @lineId
""");
sql.setParam("lineId", ledger.getLineId());
sql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(sql);
return sql.getInt() > 0;
}
private boolean isFillBedInfo(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) {
return true;
}
Sql sql = Sqls.create("""
SELECT IFNULL(MAX(s.fillBedInfo), 1)
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0
WHERE t.id = @ledgerId
""");
sql.setParam("ledgerId", ledger.getId());
sql.setCallback(Sqls.callback.integer());
tourLedgerService.dao().execute(sql);
return sql.getInt() > 0;
}
private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) {
if (StrUtil.isBlank(travelPeriod)) {
return;
}
cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod);
}
private void appendLineFilter(Cnd cnd, String lineId) {
if (StrUtil.isBlank(lineId)) {
return;
}
LineFilter lineFilter = parseLineFilter(lineId);
if (lineFilter.getLineId().startsWith("legacy:")) {
SqlExpressionGroup legacyGroup = new SqlExpressionGroup();
legacyGroup.or("t.lineId", "IS", null);
legacyGroup.or("t.lineId", "=", "");
cnd.and(legacyGroup);
cnd.and("t.lineName", "=", lineFilter.getLineId().substring("legacy:".length()));
} else {
cnd.and("t.lineId", "=", lineFilter.getLineId());
}
appendMatterUnionFilter(cnd, lineFilter.getUnionId());
}
private LineFilter parseLineFilter(String value) {
int delimiterIndex = value.indexOf("|");
if (delimiterIndex < 0) {
return new LineFilter(value, null);
}
return new LineFilter(value.substring(0, delimiterIndex), value.substring(delimiterIndex + 1));
}
private void appendMatterUnionFilter(Cnd cnd, String unionId) {
if (unionId == null) {
return;
}
if (StrUtil.isBlank(unionId)) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.or("m.unionId", "IS", null);
group.or("m.unionId", "=", "");
cnd.and(group);
return;
}
cnd.and("m.unionId", "=", unionId);
}
private String getCurrentLineName(TourLedger ledger) {
if (ledger == null) {
return "";
}
Sql sql = Sqls.create("""
SELECT CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END AS lineName
FROM tour_ledger t
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN sys_union mu ON mu.id = m.unionId
WHERE t.id = @ledgerId
""");
sql.setParam("ledgerId", ledger.getId());
sql.setCallback(Sqls.callback.str());
tourLedgerService.dao().execute(sql);
return StrUtil.blankToDefault(sql.getString(), ledger.getLineName());
}
private static class LineFilter {
private final String lineId;
private final String unionId;
private LineFilter(String lineId, String unionId) {
this.lineId = lineId;
this.unionId = unionId;
}
private String getLineId() {
return lineId;
}
private String getUnionId() {
return unionId;
}
}
private String currentUnionId() {
return SecurityUtil.getUnionId();
}
private String currentUnionName() {
String unionId = currentUnionId();
if (StrUtil.isBlank(unionId)) {
return "";
}
Sys_union union = tourLedgerService.dao().fetch(Sys_union.class, unionId);
return union == null ? "" : StrUtil.blankToDefault(union.getName(), "");
}
private String getOrderColumn(String orderName) {
if ("year".equals(orderName)) {
return "t.`year`";
}
if ("jobNo".equals(orderName)) {
return "t.jobNo";
}
if ("userName".equals(orderName)) {
return "t.userName";
}
if ("lineName".equals(orderName)) {
return """
CASE
WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName)
ELSE CONCAT(
COALESCE(NULLIF(l.lineName, ''), t.lineName),
'',
CASE
WHEN IFNULL(m.unionId, '') = '' THEN '校工会'
ELSE IFNULL(mu.name, '')
END,
''
)
END
""";
}
if ("travelPeriod".equals(orderName)) {
return "COALESCE(m.travelStartTime, tp.travelPeriod)";
}
if ("lineType".equals(orderName)) {
return "t.lineType";
}
if ("signupTime".equals(orderName)) {
return "t.signupTime";
}
if ("familyCount".equals(orderName)) {
return "familyCount";
}
if ("joined".equals(orderName)) {
return "t.joined";
}
return "t.`year`";
}
}
@@ -0,0 +1,34 @@
package com.budwk.app.zhgh.dayofficework.tour.mode;
import cn.afterturn.easypoi.excel.annotation.Excel;
import com.budwk.app.base.model.ExcelImportError;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Data
public class TourLedgerImportExcelMode extends ExcelImportError {
@Excel(name = "年度")
private Integer year;
@Excel(name = "工号")
private String jobNo;
@Excel(name = "姓名")
private String userName;
@Excel(name = "报名时间", format = "yyyy-MM-dd HH:mm:ss")
private Date signupTime;
@Excel(name = "线路名称")
private String lineName;
@Excel(name = "线路类型")
private String lineType;
@Excel(name = "是否参加")
private String joined;
}
@@ -0,0 +1,152 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养教职工报名台账。
* 后续报名模块完成后,将已报名或已参加的教职工写入本表,台账页负责跨年度查询和详情查看。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_ledger")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养台账")
public class TourLedger extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("年度")
@ColDefine(type = ColType.INT, width = 4)
private Integer year;
@Column
@Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String jobNo;
@Column
@Comment("姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String userName;
@Column
@Comment("性别")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String gender;
@Column
@Comment("身份证号")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String idCard;
@Column
@Comment("所在单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unitId;
@Column
@Comment("所在单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unitName;
@Column
@Comment("所在工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("所在工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("报名时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String signupTime;
@Column
@Comment("报名事项ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String matterId;
@Column
@Comment("报名线路ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lineId;
@Column
@Comment("报名线路")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String lineName;
@Column
@Comment("线路类型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String lineType;
@Column
@Comment("报名酒店")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String hotelName;
@Column
@Comment("旅行社ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String travelAgencyId;
@Column
@Comment("旅行社")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String travelAgencyName;
@Column
@Comment("是否携带家属")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean hasFamily;
@Column
@Comment("意向拼床人")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String intendedRoommate;
@Column
@Comment("床型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String bedType;
@Column
@Comment("床位信息")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bedInfo;
@Column
@Comment("是否参加")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean joined;
@Column
@Comment("是否报销")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean reimbursed;
@Column
@Comment("报销超出费用")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean overCostReimbursed;
}
@@ -0,0 +1,73 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 直系亲属线路报名信息。
* 与普通携带亲属信息分表存放,用于直系亲属线路的专属申请信息。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_ledger_direct_relative")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养直系亲属线路报名信息")
public class TourLedgerDirectRelative extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("台账ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String ledgerId;
@Column
@Comment("亲属姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String relativeName;
@Column
@Comment("所在单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unitName;
@Column
@Comment("亲属关系编码")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String relationshipCode;
@Column
@Comment("亲属关系")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String relationshipName;
@Column
@Comment("线路ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lineId;
@Column
@Comment("线路名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String lineName;
@Column
@Comment("出行开始日期")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String travelStartTime;
@Column
@Comment("出行结束日期")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String travelEndTime;
}
@@ -0,0 +1,83 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养台账家属信息。
* 与教职工台账通过 ledgerId 关联,用于查看教职工携带家属的历史记录。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_ledger_family")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养台账家属信息")
public class TourLedgerFamily extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("台账ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String ledgerId;
@Column
@Comment("教职工工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String staffJobNo;
@Column
@Comment("教职工姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String staffName;
@Column
@Comment("家属姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String familyName;
@Column
@Comment("性别")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String gender;
@Column
@Comment("身份证号码")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String idCard;
@Column
@Comment("年龄")
@ColDefine(type = ColType.INT, width = 3)
private Integer age;
@Column
@Comment("床型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String bedType;
@Column
@Comment("床位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bedInfo;
@Column
@Comment("意向拼床人")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String intendedRoommate;
@Column
@Comment("关系")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String relationship;
}
@@ -0,0 +1,106 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养线路管理。
* 当前阶段先维护线路基础信息,后续报名、台账等模块可通过线路ID继续关联。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_line")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养线路")
public class TourLine extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("创建年度")
@ColDefine(type = ColType.INT, width = 4)
private Integer year;
@Column
@Comment("线路编号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String lineCode;
@Column
@Comment("线路名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String lineName;
@Column
@Comment("旅行社ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String travelAgencyId;
@Column
@Comment("创建人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String creatorUserId;
@Column
@Comment("创建人")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String creatorName;
@Column
@Comment("所在单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unitId;
@Column
@Comment("所在单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unitName;
@Column
@Comment("线路类型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String lineType;
@Column
@Comment("时间标段ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lotId;
@Column
@Comment("线路内容")
@ColDefine(type = ColType.TEXT)
private String lineContent;
@Column
@Comment("移动端缩略图")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String mobileThumb;
@Column
@Comment("是否对外开放")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean openFlag;
@Column
@Comment("是否直系亲属线路")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean directFamilyUnitLine;
@Column
@Comment("激活状态")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enabled;
}
@@ -0,0 +1,124 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 疗休养事项。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_matter")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养事项")
public class TourMatter extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("年度")
@ColDefine(type = ColType.INT, width = 4)
private Integer year;
@Column
@Comment("事项名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String matterName;
@Column
@Comment("疗休养配置ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String settingId;
@Column
@Comment("创建人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String creatorUserId;
@Column
@Comment("创建人")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String creatorName;
@Column
@Comment("所属工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("组织形式")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String organizationType;
@Column
@Comment("移动端缩略图")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String mobileThumb;
@Column
@Comment("线路ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lineId;
@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.VARCHAR, width = 20)
private String travelStartTime;
@Column
@Comment("出行结束时间")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String travelEndTime;
@Column
@Comment("联系人")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String contactName;
@Column
@Comment("联系方式")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String contactPhone;
@Column
@Comment("最少成团人数")
@ColDefine(type = ColType.INT)
private Integer minGroupPeople;
@Column
@Comment("最多成团人数")
@ColDefine(type = ColType.INT)
private Integer maxGroupPeople;
@Column
@Comment("预计费用")
@ColDefine(customType = "decimal(10,2)")
private BigDecimal estimatedCost;
@Column
@Comment("事项状态")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enabled;
}
@@ -0,0 +1,134 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 疗休养基础配置。
* 这里先沉淀创建事项会复用的基础字段,后续线路、报名等阶段可以继续通过配置ID关联扩展。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_setting")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养配置")
public class TourSetting extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("年度")
@ColDefine(type = ColType.INT, width = 4)
private Integer year;
@Column
@Comment("疗休养配置名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String configName;
@Column
@Comment("疗休养类型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String tourType;
@Column
@Comment("可参加人员范围ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String activityGroupId;
@Column
@Comment("排序编号")
@ColDefine(type = ColType.INT)
private Integer sortNo;
@Column
@Comment("最少成团人数")
@ColDefine(type = ColType.INT)
private Integer minGroupPeople;
@Column
@Comment("最多成团人数")
@ColDefine(type = ColType.INT)
private Integer maxGroupPeople;
@Column
@Comment("省外几年去一次")
@ColDefine(type = ColType.INT)
private Integer outProvinceYears;
@Column
@Comment("省外人数占比")
@ColDefine(customType = "decimal(10,2)")
private BigDecimal outProvinceRatio;
@Column
@Comment("省外人数占比类型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String outProvinceRatioType;
@Column
@Comment("省外固定人数")
@ColDefine(type = ColType.INT)
private Integer outProvinceFixedPeople;
@Column
@Comment("周期开始年度")
@ColDefine(type = ColType.INT, width = 4)
private Integer cycleStartYear;
@Column
@Comment("周期结束年度")
@ColDefine(type = ColType.INT, width = 4)
private Integer cycleEndYear;
@Column
@Comment("周期内总费用")
@ColDefine(type = ColType.INT)
private Integer cycleTotalCost;
@Column
@Comment("周期允许次数")
@ColDefine(type = ColType.INT)
private Integer cycleAllowedTimes;
@Column
@Comment("是否允许携带家属")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean allowFamily;
@Column
@Comment("是否填报床位信息")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean fillBedInfo;
@Column
@Comment("是否启用")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enabled;
/**
* 标段管理沿用老疗休养配置的子表设计,后续线路、旅行社等模块可通过标段ID继续关联。
*/
@Many(field = "settingId")
private List<TourSettingLot> lots;
@Column
@Comment("服务须知")
@ColDefine(type = ColType.TEXT)
private String serviceNotice;
}
@@ -0,0 +1,53 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养配置标段。
* 标段从基础配置中拆成子表,便于后续线路、目的地、报名等模块复用同一标段口径。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_setting_lot")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养配置标段")
public class TourSettingLot extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("所属配置ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String settingId;
@Column
@Comment("标段名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String lotName;
@Column
@Comment("标段值")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String lotValue;
@Column
@Comment("标段费用")
@ColDefine(type = ColType.INT)
private Integer activityCost;
@Column
@Comment("允许超出报销")
@ColDefine(type = ColType.BOOLEAN)
private Boolean allowOverReimbursement;
}
@@ -0,0 +1,74 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 旅行社管理。
* 先维护疗休养线路创建会复用的旅行社基础信息,后续线路模块可通过旅行社ID关联。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_travel_agency")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养旅行社")
public class TourTravelAgency extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("年度")
@ColDefine(type = ColType.INT, width = 4)
private Integer year;
@Column
@Comment("旅行社编号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String agencyCode;
@Column
@Comment("旅行社名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String agencyName;
@Column
@Comment("联系人")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String contactName;
@Column
@Comment("联系人手机")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String contactPhone;
@Column
@Comment("邮箱")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String email;
@Column
@Comment("备注")
@ColDefine(type = ColType.TEXT)
private String remark;
@Column
@Comment("移动端缩略图")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String mobileThumb;
@Column
@Comment("激活状态")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enabled;
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative;
public interface TourLedgerDirectRelativeService extends BaseService<TourLedgerDirectRelative> {
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily;
public interface TourLedgerFamilyService extends BaseService<TourLedgerFamily> {
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
public interface TourLedgerService extends BaseService<TourLedger> {
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLine;
public interface TourLineService extends BaseService<TourLine> {
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
public interface TourMatterService extends BaseService<TourMatter> {
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
public interface TourSettingService extends BaseService<TourSetting> {
}
@@ -0,0 +1,7 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency;
public interface TourTravelAgencyService extends BaseService<TourTravelAgency> {
}
@@ -0,0 +1,15 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class TourLedgerDirectRelativeServiceImpl extends BaseServiceImpl<TourLedgerDirectRelative> implements TourLedgerDirectRelativeService {
public TourLedgerDirectRelativeServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,15 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class TourLedgerFamilyServiceImpl extends BaseServiceImpl<TourLedgerFamily> implements TourLedgerFamilyService {
public TourLedgerFamilyServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,15 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class TourLedgerServiceImpl extends BaseServiceImpl<TourLedger> implements TourLedgerService {
public TourLedgerServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,15 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLine;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLineService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class TourLineServiceImpl extends BaseServiceImpl<TourLine> implements TourLineService {
public TourLineServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,15 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class TourMatterServiceImpl extends BaseServiceImpl<TourMatter> implements TourMatterService {
public TourMatterServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,15 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class TourSettingServiceImpl extends BaseServiceImpl<TourSetting> implements TourSettingService {
public TourSettingServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,15 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency;
import com.budwk.app.zhgh.dayofficework.tour.service.TourTravelAgencyService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class TourTravelAgencyServiceImpl extends BaseServiceImpl<TourTravelAgency> implements TourTravelAgencyService {
public TourTravelAgencyServiceImpl(Dao dao) {
super(dao);
}
}
@@ -25,6 +25,7 @@ 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;
import org.nutz.lang.Strings;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
@@ -68,11 +69,15 @@ public class TeacherCongressInstitutionController {
Teacher_congress_institution rootInstitution = new Teacher_congress_institution();
rootInstitution.setId("0");
rootInstitution.setParentId("");
rootInstitution.setName("代会机构");
rootInstitution.setName("代会组织机构");
rootInstitution.setLocation(0);
institutionList.add(rootInstitution);
List<TreeNode<String>> treeNodes = institutionList.stream().map(institution -> new TreeNode<>(institution.getId(), institution.getParentId(), institution.getName(), institution.getLocation())).toList();
List<TreeNode<String>> treeNodes = institutionList.stream().map(institution -> {
TreeNode<String> treeNode = new TreeNode<>(institution.getId(), institution.getParentId(), institution.getName(), institution.getLocation());
treeNode.setExtra(Map.of("code", Strings.sNull(institution.getCode())));
return treeNode;
}).toList();
List<Tree<String>> treeList = TreeUtil.build(treeNodes, "");
return Result.success(treeList);
}
@@ -97,6 +102,16 @@ public class TeacherCongressInstitutionController {
return Result.success(Map.of("treeFlat", children, "treeList", treeList));
}
@At
@SaCheckPermission("tc.institution")
public Result specialCommitteeRoleOptions() {
Sys_dict parent = sysDictService.fetch(Cnd.where(Sys_dict::getCode, "=", "SPECIAL_COMMITTEE_ROLES"));
if (ObjectUtil.isEmpty(parent)) {
return Result.success(List.of());
}
List<Sys_dict> children = sysDictService.query(Cnd.where(Sys_dict::getPath, "like", parent.getPath() + "%").and(Sys_dict::getId, "!=", parent.getId()).asc(Sys_dict::getLocation));
return Result.success(children);
}
/**
* 机构分页查询
@@ -113,6 +128,7 @@ public class TeacherCongressInstitutionController {
cnd.and("parentId", "=", parentId);
cnd.and("sessionId", "=", sessionId);
cnd.asc("location");
cnd.asc("code");
Pagination pagination = baseService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), "teacher_congress_institution", cnd);
return Result.success(pagination);
}
@@ -129,7 +145,7 @@ public class TeacherCongressInstitutionController {
if (dao.count(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getCode, "=", institution.getCode()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId())) > 0) {
return Result.error("机构已存在");
}
if (StrUtil.isNotBlank(institution.getParentId()) && !institution.getParentId().equals("0")) {
if (StrUtil.isNotBlank(institution.getParentId()) && !institution.getParentId().equals("0") && dao.count(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getId, "=", institution.getParentId()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId())) == 0) {
Sys_dict sysDict = sysDictService.fetch(Cnd.where(Sys_dict::getId, "=", institution.getParentId()));
Teacher_congress_institution parentInstitution = dao.fetch(Teacher_congress_institution.class,
Cnd.where(Teacher_congress_institution::getCode, "=", sysDict.getCode())
@@ -151,10 +167,34 @@ public class TeacherCongressInstitutionController {
* @param id
* @return
*/
@At
@SaCheckPermission("tc.institution")
public Result update(Teacher_congress_institution institution) {
Teacher_congress_institution dbInstitution = dao.fetch(Teacher_congress_institution.class, institution.getId());
if (ObjectUtil.isEmpty(dbInstitution)) {
return Result.error("机构不存在");
}
dbInstitution.setName(institution.getName());
dbInstitution.setIntroduce(institution.getIntroduce());
dbInstitution.setLocation(institution.getLocation());
dao.update(dbInstitution);
return Result.success();
}
@At
@SaCheckPermission("tc.institution")
@Aop(TransAop.READ_COMMITTED)
public Result delete(@Valid String id) {
Teacher_congress_institution institution = dao.fetch(Teacher_congress_institution.class, id);
if (ObjectUtil.isEmpty(institution)) {
return Result.error("机构不存在");
}
List<Teacher_congress_institution> siblingList = dao.query(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getParentId, "=", institution.getParentId()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId()).asc(Teacher_congress_institution::getLocation).asc(Teacher_congress_institution::getCode));
for (int i = 0; i < siblingList.size() && i < 6; i++) {
if (id.equals(siblingList.get(i).getId())) {
return Result.error("你选择的组织机构是两代会基本机构,不允许删除。");
}
}
dao.delete(Teacher_congress_institution.class, id);
dao.clear(Teacher_congress_institution_user.class, Cnd.where("institutionId", "=", id));
return Result.success();
@@ -214,8 +254,8 @@ public class TeacherCongressInstitutionController {
@SaCheckPermission("tc.institution")
@SLog(tag = "教代会-机构设置", msg = "添加人员")
@Aop(TransAop.READ_COMMITTED)
public Result userInsert(@Valid String userId, @Valid String institutionId, @Valid String sessionId, @Valid String identity) {
teacherCongressInstitutionUserService.userInsert(userId, institutionId, sessionId, identity);
public Result userInsert(@Valid String userId, @Valid String institutionId, @Valid String sessionId, @Valid String identity, String roleCode) {
teacherCongressInstitutionUserService.userInsert(userId, institutionId, sessionId, identity, roleCode);
return Result.success();
}
@@ -14,7 +14,7 @@ public interface TeacherCongressInstitutionUserService extends BaseService<Teach
* @param sessionId 届次id
* @param identity 身份 主任|副主任|成员
*/
void userInsert(String userId, String institutionId, String sessionId, String identity);
void userInsert(String userId, String institutionId, String sessionId, String identity, String roleCode);
/**
* 删除机构成员
@@ -1,5 +1,6 @@
package com.budwk.app.zhgh.democratic.teachercongress.institution.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
@@ -30,7 +31,7 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
@Override
@Aop(TransAop.READ_COMMITTED)
public void userInsert(String userId, String institutionId, String sessionId, String identity) {
public void userInsert(String userId, String institutionId, String sessionId, String identity, String roleCode) {
int count = dao().count(Teacher_congress_institution_user.class,
Cnd.where(Teacher_congress_institution_user::getInstitutionId, "=", institutionId)
.and(Teacher_congress_institution_user::getUserId, "=", userId)
@@ -49,6 +50,28 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
dao().insert(institutionUser);
Teacher_congress_institution institution = dao().fetch(Teacher_congress_institution.class, institutionId);
if ("ZWH001".equals(institution.getCode()) || "ZXWYH".equals(institution.getCode()) || "DBZGSCXZ".equals(institution.getCode())) {
if (StrUtil.isBlank(roleCode)) {
throw new BaseException("请选择角色");
}
Sys_role sysRole = sysRoleService.fetch(Cnd.where(Sys_role::getCode, "=", roleCode));
if (sysRole == null) {
throw new BaseException("无法找到" + roleCode + "对应编码的角色");
}
int existsRole = dao().count(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", userId)
.and(Sys_user_role::getTcSessionId, "=", sessionId)
.and(Sys_user_role::getRoleId, "=", sysRole.getId())
);
if (existsRole == 0) {
Sys_user_role insertSysUserRole = new Sys_user_role();
insertSysUserRole.setRoleId(sysRole.getId());
insertSysUserRole.setUserId(userId);
insertSysUserRole.setTcSessionId(sessionId);
dao().insert(insertSysUserRole);
sysRoleService.clearCache();
sysUserService.clearCache();
}
}
if (institution.getCode().contains("TEACHER_CONGRESS_INSTITUTION_PROPOSAL_COMMITTEE") && identity.equals("主任")) {
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_COMMITTEE_DIRECTOR);
int existsRole = dao().count(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", userId)
@@ -0,0 +1,383 @@
package com.budwk.app.zhgh.learning.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.learning.models.LearningCourse;
import com.budwk.app.zhgh.learning.models.LearningCourseOutline;
import com.budwk.app.zhgh.learning.models.LearningCourseType;
import com.budwk.app.zhgh.learning.models.LearningOutlineResource;
import com.budwk.app.zhgh.learning.models.LearningStudyRule;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.pager.Pager;
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.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@IocBean
@Ok("json:full")
@Api("学习教育平台-章节内容管理")
@At("/platform/learning/chapter/content")
public class LearningChapterContentController {
@Inject
private Dao dao;
@At("")
@Ok("beetl:/platform/zhgh/Learning/chapterContent/index.html")
@SaCheckPermission("learning.chapter.content")
public void index() {
}
@At
@ApiOperation("课程列表")
@SaCheckPermission("learning.chapter.content")
public Result courses(String courseId, String courseName, String courseTypeId, String lecturerName, String status) {
Cnd cnd = Cnd.NEW();
cnd.andEX("id", "=", courseId);
cnd.and(Cnd.likeEX("courseName", courseName));
cnd.andEX("courseTypeId", "=", courseTypeId);
cnd.and(Cnd.likeEX("lecturerName", lecturerName));
cnd.andEX("status", "=", status);
cnd.desc("createdAt");
return Result.success(dao.query(LearningCourse.class, cnd, dao.createPager(1, 50)));
}
@At
@ApiOperation("课程类型")
@SaCheckPermission("learning.chapter.content")
public Result courseTypes() {
return Result.success(dao.query(LearningCourseType.class, Cnd.where("enabled", "=", true).asc("sortNum").asc("createdAt")));
}
@At
@ApiOperation("课程大纲树")
@SaCheckPermission("learning.chapter.content")
public Result tree(String courseId) {
if (StrUtil.isBlank(courseId)) {
return Result.success(List.of());
}
List<LearningCourseOutline> outlines = dao.query(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).asc("sortOrder").asc("createdAt"));
Map<String, List<NutMap>> childrenMap = new HashMap<>();
List<NutMap> roots = new ArrayList<>();
for (LearningCourseOutline outline : outlines) {
NutMap map = NutMap.NEW()
.addv("id", outline.getId())
.addv("courseId", outline.getCourseId())
.addv("courseName", outline.getCourseName())
.addv("parentId", outline.getParentId())
.addv("nodeType", outline.getNodeType())
.addv("title", outline.getTitle())
.addv("subtitle", outline.getSubtitle())
.addv("description", outline.getDescription())
.addv("sortOrder", outline.getSortOrder())
.addv("required", outline.getRequired())
.addv("status", outline.getStatus())
.addv("children", new ArrayList<>());
String parentId = StrUtil.blankToDefault(outline.getParentId(), "");
childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(map);
}
roots.addAll(childrenMap.getOrDefault("", new ArrayList<>()));
appendChildren(roots, childrenMap);
return Result.success(roots);
}
private void appendChildren(List<NutMap> nodes, Map<String, List<NutMap>> childrenMap) {
for (NutMap node : nodes) {
List<NutMap> children = childrenMap.getOrDefault(node.getString("id"), new ArrayList<>());
node.put("children", children);
appendChildren(children, childrenMap);
}
}
@At
@ApiOperation("新增/编辑大纲节点")
@SaCheckPermission("learning.chapter.content")
@SLog(tag = "保存大纲节点", msg = "节点标题:${args[0].title}")
public Result saveNode(LearningCourseOutline outline) {
Result checkResult = checkNode(outline);
if (checkResult != null) {
return checkResult;
}
LearningCourse course = dao.fetch(LearningCourse.class, outline.getCourseId());
outline.setCourseName(course == null ? outline.getCourseName() : course.getCourseName());
if ("chapter".equals(outline.getNodeType())) {
outline.setParentId("");
}
if (outline.getRequired() == null) {
outline.setRequired(false);
}
if (StrUtil.isBlank(outline.getStatus())) {
outline.setStatus("enabled");
}
if (outline.getSortOrder() == null) {
outline.setSortOrder(nextNodeSort(outline.getCourseId(), outline.getParentId()));
}
if (StrUtil.isBlank(outline.getId())) {
dao.insert(outline);
} else {
dao.updateIgnoreNull(outline);
}
ensureRule(outline.getCourseId(), "outline", outline.getId(), outline.getRequired(), "enabled");
return Result.success();
}
@At
@ApiOperation("删除大纲节点")
@SaCheckPermission("learning.chapter.content")
@SLog(tag = "删除大纲节点", msg = "节点ID:${args[0]}")
public Result deleteNode(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("请选择要删除的数据");
}
int childCount = dao.count(LearningCourseOutline.class, Cnd.where("parentId", "=", id));
int resourceCount = dao.count(LearningOutlineResource.class, Cnd.where("outlineId", "=", id));
if (childCount > 0 || resourceCount > 0) {
return Result.error("该节点下存在子节点或学习资料,请先删除后再操作");
}
dao.clear(LearningStudyRule.class, Cnd.where("targetId", "=", id).and("targetType", "=", "outline"));
dao.clear(LearningCourseOutline.class, Cnd.where("id", "=", id));
return Result.success();
}
@At
@ApiOperation("启用/禁用大纲节点")
@SaCheckPermission("learning.chapter.content")
public Result toggleNode(String id, String status) {
if (StrUtil.isBlank(id)) {
return Result.error("请选择要操作的数据");
}
dao.update(LearningCourseOutline.class, Chain.make("status", status), Cnd.where("id", "=", id));
dao.update(LearningStudyRule.class, Chain.make("status", status), Cnd.where("targetId", "=", id).and("targetType", "=", "outline"));
return Result.success();
}
@At
@ApiOperation("移动大纲节点")
@SaCheckPermission("learning.chapter.content")
public Result moveNode(String id, String direction) {
LearningCourseOutline current = dao.fetch(LearningCourseOutline.class, id);
if (current == null) {
return Result.error("节点不存在");
}
Cnd cnd = Cnd.where("courseId", "=", current.getCourseId()).and("parentId", "=", StrUtil.blankToDefault(current.getParentId(), ""));
if ("up".equals(direction)) {
cnd.and("sortOrder", "<", current.getSortOrder()).desc("sortOrder");
} else {
cnd.and("sortOrder", ">", current.getSortOrder()).asc("sortOrder");
}
LearningCourseOutline target = dao.fetch(LearningCourseOutline.class, cnd);
if (target == null) {
return Result.success();
}
Integer currentSort = current.getSortOrder();
dao.update(LearningCourseOutline.class, Chain.make("sortOrder", target.getSortOrder()), Cnd.where("id", "=", current.getId()));
dao.update(LearningCourseOutline.class, Chain.make("sortOrder", currentSort), Cnd.where("id", "=", target.getId()));
return Result.success();
}
@At
@ApiOperation("学习资料分页")
@SaCheckPermission("learning.chapter.content")
public Result resourcePage(PageForm pageForm, String outlineId) {
if (StrUtil.isBlank(outlineId)) {
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, List.of()));
}
Cnd cnd = Cnd.where("outlineId", "=", outlineId);
String orderColumn = getResourceOrderColumn(pageForm.getPageOrderName());
if (StrUtil.isBlank(orderColumn)) {
cnd.asc("sortOrder").asc("createdAt");
} else if ("descending".equals(pageForm.getPageOrderBy())) {
cnd.desc(orderColumn);
} else {
cnd.asc(orderColumn);
}
int count = dao.count(LearningOutlineResource.class, Cnd.where("outlineId", "=", outlineId));
Pager pager = dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize());
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, dao.query(LearningOutlineResource.class, cnd, pager)));
}
@At
@ApiOperation("新增/编辑学习资料")
@SaCheckPermission("learning.chapter.content")
@SLog(tag = "保存学习资料", msg = "资料标题:${args[0].resourceTitle}")
public Result saveResource(LearningOutlineResource resource) {
Result checkResult = checkResource(resource);
if (checkResult != null) {
return checkResult;
}
if (resource.getRequired() == null) {
resource.setRequired(false);
}
if (resource.getAllowPreview() == null) {
resource.setAllowPreview(true);
}
if (resource.getAllowDownload() == null) {
resource.setAllowDownload(false);
}
if (StrUtil.isBlank(resource.getStatus())) {
resource.setStatus("enabled");
}
if (resource.getSortOrder() == null) {
resource.setSortOrder(nextResourceSort(resource.getOutlineId()));
}
if (StrUtil.isBlank(resource.getId())) {
dao.insert(resource);
} else {
dao.updateIgnoreNull(resource);
}
ensureRule(resource.getCourseId(), "resource", resource.getId(), resource.getRequired(), resource.getStatus());
return Result.success();
}
@At
@ApiOperation("删除学习资料")
@SaCheckPermission("learning.chapter.content")
@SLog(tag = "删除学习资料", msg = "资料ID:${args[0]}")
public Result deleteResource(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("请选择要删除的数据");
}
dao.clear(LearningStudyRule.class, Cnd.where("targetId", "=", id).and("targetType", "=", "resource"));
dao.clear(LearningOutlineResource.class, Cnd.where("id", "=", id));
return Result.success();
}
@At
@ApiOperation("查询学习规则")
@SaCheckPermission("learning.chapter.content")
public Result getRule(String courseId, String targetType, String targetId) {
LearningStudyRule rule = dao.fetch(LearningStudyRule.class, Cnd.where("courseId", "=", courseId).and("targetType", "=", targetType).and("targetId", "=", targetId));
if (rule == null) {
rule = defaultRule(courseId, targetType, targetId, false, "enabled");
}
return Result.success(rule);
}
@At
@ApiOperation("保存学习规则")
@SaCheckPermission("learning.chapter.content")
public Result saveRule(LearningStudyRule rule) {
if (StrUtil.isBlank(rule.getCourseId()) || StrUtil.isBlank(rule.getTargetType()) || StrUtil.isBlank(rule.getTargetId())) {
return Result.error("规则对象不能为空");
}
if (StrUtil.isBlank(rule.getStatus())) {
rule.setStatus("enabled");
}
LearningStudyRule old = dao.fetch(LearningStudyRule.class, Cnd.where("courseId", "=", rule.getCourseId()).and("targetType", "=", rule.getTargetType()).and("targetId", "=", rule.getTargetId()));
if (old == null) {
dao.insert(rule);
} else {
rule.setId(old.getId());
dao.updateIgnoreNull(rule);
}
if ("outline".equals(rule.getTargetType())) {
dao.update(LearningCourseOutline.class, Chain.make("required", rule.getRequired()).add("status", rule.getStatus()), Cnd.where("id", "=", rule.getTargetId()));
}
if ("resource".equals(rule.getTargetType())) {
dao.update(LearningOutlineResource.class, Chain.make("required", rule.getRequired()).add("status", rule.getStatus()), Cnd.where("id", "=", rule.getTargetId()));
}
return Result.success();
}
private Result checkNode(LearningCourseOutline outline) {
if (outline == null || StrUtil.isBlank(outline.getCourseId())) {
return Result.error("请选择课程");
}
if (StrUtil.isBlank(outline.getNodeType())) {
return Result.error("请选择节点类型");
}
if (StrUtil.isBlank(outline.getTitle())) {
return Result.error("标题不能为空");
}
if ("section".equals(outline.getNodeType()) && StrUtil.isBlank(outline.getParentId())) {
return Result.error("新增节需要选择所属章");
}
return null;
}
private Result checkResource(LearningOutlineResource resource) {
if (resource == null || StrUtil.isBlank(resource.getCourseId()) || StrUtil.isBlank(resource.getOutlineId())) {
return Result.error("请选择章/节节点");
}
if (StrUtil.isBlank(resource.getResourceTitle())) {
return Result.error("资料标题不能为空");
}
if (StrUtil.isBlank(resource.getResourceType())) {
return Result.error("请选择资料类型");
}
if (StrUtil.isBlank(resource.getFileData())) {
return Result.error("请上传附件");
}
return null;
}
private Integer nextNodeSort(String courseId, String parentId) {
LearningCourseOutline outline = dao.fetch(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).and("parentId", "=", StrUtil.blankToDefault(parentId, "")).desc("sortOrder"));
return outline == null || outline.getSortOrder() == null ? 1 : outline.getSortOrder() + 1;
}
private Integer nextResourceSort(String outlineId) {
LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, Cnd.where("outlineId", "=", outlineId).desc("sortOrder"));
return resource == null || resource.getSortOrder() == null ? 1 : resource.getSortOrder() + 1;
}
private void ensureRule(String courseId, String targetType, String targetId, Boolean required, String status) {
LearningStudyRule rule = dao.fetch(LearningStudyRule.class, Cnd.where("courseId", "=", courseId).and("targetType", "=", targetType).and("targetId", "=", targetId));
if (rule == null) {
dao.insert(defaultRule(courseId, targetType, targetId, required, status));
} else {
dao.update(LearningStudyRule.class,
Chain.make("required", required != null && required).add("status", StrUtil.blankToDefault(status, "enabled")),
Cnd.where("id", "=", rule.getId()));
}
}
private LearningStudyRule defaultRule(String courseId, String targetType, String targetId, Boolean required, String status) {
LearningStudyRule rule = new LearningStudyRule();
rule.setCourseId(courseId);
rule.setTargetType(targetType);
rule.setTargetId(targetId);
rule.setRequired(required != null && required);
rule.setStudyMode("mixed");
rule.setCompletionRule("all_required_resource");
rule.setCompletePercent(90);
rule.setMinStudySeconds(0);
rule.setUnlockRule("free");
rule.setAllowSkip(false);
rule.setAllowDrag(true);
rule.setPauseCountTime(false);
rule.setHiddenCountTime(false);
rule.setInactiveCountTime(false);
rule.setStatus(StrUtil.blankToDefault(status, "enabled"));
return rule;
}
private String getResourceOrderColumn(String prop) {
Map<String, String> columns = new HashMap<>();
columns.put("resourceTitle", "resourceTitle");
columns.put("resourceType", "resourceType");
columns.put("fileExt", "fileExt");
columns.put("durationSeconds", "durationSeconds");
columns.put("sortOrder", "sortOrder");
columns.put("required", "required");
columns.put("allowPreview", "allowPreview");
columns.put("allowDownload", "allowDownload");
columns.put("status", "status");
return columns.get(prop);
}
}
@@ -0,0 +1,297 @@
package com.budwk.app.zhgh.learning.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
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.base.utils.OfficePlusUtil;
import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.learning.models.LearningCourseOutline;
import com.budwk.app.zhgh.learning.models.LearningCourseType;
import com.budwk.app.zhgh.learning.models.LearningOutlineResource;
import com.budwk.app.zhgh.learning.models.LearningStudyRecord;
import com.google.common.net.HttpHeaders;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@IocBean
@Ok("json:full")
@Api("学习教育平台-课程展示")
@At("/platform/learning/course/display")
public class LearningCourseDisplayController {
@Inject
private Dao dao;
@Inject
private SysDictService sysDictService;
@At("")
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/index.html")
@SaCheckPermission("learning.course.display")
public void index() {
}
@At("/detail")
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/detail.html")
@SaCheckPermission("learning.course.display")
public void detail() {
}
@At("/study")
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/study.html")
@SaCheckPermission("learning.course.display")
public void study() {
}
@At
@ApiOperation("课程展示列表")
@SaCheckPermission("learning.course.display")
public Result pageData(PageForm pageForm, String keyword, String courseTypeId, String recommendFlag) {
Cnd cnd = Cnd.where("c.status", "=", "published");
if (StrUtil.isNotBlank(keyword)) {
cnd.and(Cnd.exps("c.courseName", "like", "%" + keyword + "%")
.or("c.courseIntro", "like", "%" + keyword + "%"));
}
cnd.andEX("c.courseTypeId", "=", courseTypeId);
if (StrUtil.isNotBlank(recommendFlag)) {
cnd.and("c.recommendFlags", "like", "%" + recommendFlag + "%");
}
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM learning_course c
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
dao.execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT c.id, c.courseName, c.courseIntro, c.startTime, c.endTime, c.openType, c.cover,
c.recommendFlags, c.courseTypeId, c.lecturerName, c.sortNum, t.typeName AS courseTypeName
FROM learning_course c
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
$condition
ORDER BY c.sortNum ASC, c.createdAt DESC
""");
listSql.setCondition(cnd);
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
dao.execute(listSql);
List<NutMap> rows = listSql.getList(NutMap.class);
rows.forEach(row -> {
Object startTime = row.get("startTime");
Object endTime = row.get("endTime");
row.put("startTimeText", startTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) startTime)));
row.put("endTimeText", endTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) endTime)));
});
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, rows));
}
@At
@ApiOperation("课程学习基础信息")
@SaCheckPermission("learning.course.display")
public Result courseInfo(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("请选择课程");
}
Sql sql = Sqls.create("""
SELECT c.id, c.courseName, c.courseIntro, c.startTime, c.endTime, c.openType, c.cover,
c.recommendFlags, c.lecturerName, t.typeName AS courseTypeName
FROM learning_course c
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
WHERE c.id = @id
""");
sql.params().set("id", id);
sql.setCallback(Sqls.callback.map());
dao.execute(sql);
NutMap course = sql.getObject(NutMap.class);
if (course == null) {
return Result.error("课程不存在");
}
Object startTime = course.get("startTime");
Object endTime = course.get("endTime");
course.put("startTimeText", startTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) startTime)));
course.put("endTimeText", endTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) endTime)));
return Result.success(course);
}
@At
@ApiOperation("课程学习安排")
@SaCheckPermission("learning.course.display")
public Result studyTree(String courseId) {
if (StrUtil.isBlank(courseId)) {
return Result.success(List.of());
}
List<LearningCourseOutline> outlines = dao.query(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).and("status", "=", "enabled").asc("sortOrder").asc("createdAt"));
List<LearningOutlineResource> resources = dao.query(LearningOutlineResource.class, Cnd.where("courseId", "=", courseId).and("status", "=", "enabled").asc("sortOrder").asc("createdAt"));
List<LearningStudyRecord> records = dao.query(LearningStudyRecord.class, Cnd.where("courseId", "=", courseId).and("userId", "=", SecurityUtil.getUserId()));
Map<String, List<NutMap>> childrenMap = new HashMap<>();
List<NutMap> roots = new ArrayList<>();
Map<String, LearningStudyRecord> recordMap = new HashMap<>();
for (LearningStudyRecord record : records) {
recordMap.put(record.getOutlineId(), record);
}
Map<String, NutMap> outlineNodeMap = new HashMap<>();
for (LearningCourseOutline outline : outlines) {
List<NutMap> children = new ArrayList<>();
NutMap node = NutMap.NEW()
.addv("id", outline.getId())
.addv("type", "outline")
.addv("nodeType", outline.getNodeType())
.addv("title", outline.getTitle())
.addv("required", outline.getRequired())
.addv("progressPercent", recordMap.containsKey(outline.getId()) ? recordMap.get(outline.getId()).getProgressPercent() : 0)
.addv("studySeconds", recordMap.containsKey(outline.getId()) ? recordMap.get(outline.getId()).getStudySeconds() : 0)
.addv("lastPositionSeconds", recordMap.containsKey(outline.getId()) ? value(recordMap.get(outline.getId()).getLastPositionSeconds()) : 0)
.addv("completeStatus", recordMap.containsKey(outline.getId()) ? recordMap.get(outline.getId()).getCompleteStatus() : "not_started")
.addv("children", children);
outlineNodeMap.put(outline.getId(), node);
childrenMap.put(outline.getId(), children);
}
for (LearningCourseOutline outline : outlines) {
NutMap node = outlineNodeMap.get(outline.getId());
if (StrUtil.isBlank(outline.getParentId())) {
roots.add(node);
} else {
childrenMap.computeIfAbsent(outline.getParentId(), key -> new ArrayList<>()).add(node);
}
}
for (LearningOutlineResource resource : resources) {
NutMap node = NutMap.NEW()
.addv("id", resource.getId())
.addv("type", "resource")
.addv("outlineId", resource.getOutlineId())
.addv("title", resource.getResourceTitle())
.addv("resourceType", resource.getResourceType())
.addv("fileExt", resource.getFileExt())
.addv("fileData", resource.getFileData())
.addv("durationSeconds", resource.getDurationSeconds())
.addv("required", resource.getRequired())
.addv("progressPercent", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getProgressPercent() : 0)
.addv("studySeconds", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getStudySeconds() : 0)
.addv("lastPositionSeconds", recordMap.containsKey(resource.getOutlineId()) ? value(recordMap.get(resource.getOutlineId()).getLastPositionSeconds()) : 0)
.addv("completeStatus", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getCompleteStatus() : "not_started");
childrenMap.computeIfAbsent(resource.getOutlineId(), key -> new ArrayList<>()).add(node);
}
return Result.success(roots);
}
@At
@Ok("void")
@ApiOperation("PDF内联预览")
@SaCheckPermission("learning.course.display")
public void pdfPreview(String id, HttpServletResponse response) throws IOException {
if (StrUtil.isBlank(id)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "文件ID不能为空");
return;
}
Sys_file file = dao.fetch(Sys_file.class, id);
if (file == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
return;
}
byte[] bytes;
if (SysFileEngineTypeEnum.MINIO.getValue().equals(file.getEngine())) {
bytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
} else {
File localFile = FileUtil.file(file.getStoragePath());
if (!FileUtil.exist(localFile)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
return;
}
bytes = IoUtil.readBytes(FileUtil.getInputStream(localFile));
}
if (!"pdf".equalsIgnoreCase(file.getSuffix())) {
File sourceFile = File.createTempFile("learning_preview_origin", "." + file.getSuffix());
File pdfFile = File.createTempFile("learning_preview", ".pdf");
try {
Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE);
OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath());
if (!FileUtil.exist(pdfFile) || pdfFile.length() == 0) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "文件转换PDF失败");
return;
}
bytes = IoUtil.readBytes(FileUtil.getInputStream(pdfFile));
} finally {
FileUtil.del(sourceFile);
FileUtil.del(pdfFile);
}
}
String fileName = StrUtil.blankToDefault(FileUtil.mainName(file.getName()), "preview") + ".pdf";
String encodedName = URLEncoder.encode(fileName, CharsetUtil.UTF_8).replace("+", "%20");
response.setContentType("application/pdf");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"preview.pdf\"; filename*=UTF-8''" + encodedName);
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(bytes.length));
try (OutputStream out = response.getOutputStream()) {
out.write(bytes);
}
}
@At
@ApiOperation("启用课程类型")
@SaCheckPermission("learning.course.display")
public Result courseTypes() {
return Result.success(dao.query(LearningCourseType.class, Cnd.where("enabled", "=", true).asc("sortNum").asc("createdAt")));
}
@At
@ApiOperation("推荐标识")
@SaCheckPermission("learning.course.display")
public Result recommendOptions() {
Sys_dict root = sysDictService.fetch(Cnd.where("code", "=", "学习教育").or("name", "=", "学习教育"));
if (root == null) {
return Result.success(List.of());
}
List<Sys_dict> firstLevel = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(root.getId())).and("disabled", "=", false).asc("location"));
Sys_dict group = firstLevel.stream()
.filter(dict -> "推荐标识".equals(dict.getName()) || "推荐标识".equals(dict.getCode()))
.findFirst()
.orElse(null);
if (group == null) {
return Result.success(firstLevel);
}
List<Sys_dict> children = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(group.getId())).and("disabled", "=", false).asc("location"));
return Result.success(children.isEmpty() ? firstLevel : children);
}
private int value(Integer value) {
return value == null ? 0 : value;
}
}
@@ -0,0 +1,232 @@
package com.budwk.app.zhgh.learning.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
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_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.zhgh.learning.models.LearningCourse;
import com.budwk.app.zhgh.learning.models.LearningCourseOutline;
import com.budwk.app.zhgh.learning.models.LearningCourseType;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@IocBean
@Ok("json:full")
@Api("学习教育平台-课程管理")
@At("/platform/learning/course/manage")
public class LearningCourseManageController {
@Inject
private Dao dao;
@Inject
private SysDictService sysDictService;
@At("")
@Ok("beetl:/platform/zhgh/Learning/courseManage/index.html")
@SaCheckPermission("learning.course.manage")
public void index() {
}
@At
@ApiOperation("课程列表")
@SaCheckPermission("learning.course.manage")
public Result pageData(PageForm pageForm,
String courseName,
String courseTypeId,
String startTime,
String endTime,
String lecturerName,
String status,
String recommendFlag) {
Cnd cnd = Cnd.NEW();
cnd.and(Cnd.likeEX("c.courseName", courseName));
cnd.andEX("c.courseTypeId", "=", courseTypeId);
cnd.and(Cnd.likeEX("c.lecturerName", lecturerName));
cnd.andEX("c.status", "=", status);
if (StrUtil.isNotBlank(recommendFlag)) {
cnd.and("c.recommendFlags", "like", "%" + recommendFlag + "%");
}
if (StrUtil.isNotBlank(startTime)) {
cnd.and("c.startTime", ">=", DateUtil.parse(startTime));
}
if (StrUtil.isNotBlank(endTime)) {
cnd.and("c.endTime", "<=", DateUtil.parse(endTime));
}
String orderColumn = getOrderColumn(pageForm.getPageOrderName());
String orderBy = "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc";
if (StrUtil.isBlank(orderColumn)) {
orderColumn = "c.sortNum";
orderBy = "asc";
}
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM learning_course c
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
dao.execute(countSql);
int count = countSql.getInt();
Sql listSql = Sqls.create("""
SELECT c.*, t.typeName AS courseTypeName
FROM learning_course c
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
$condition
ORDER BY $orderColumn $orderBy
""");
listSql.setCondition(cnd);
listSql.setVar("orderColumn", orderColumn);
listSql.setVar("orderBy", orderBy);
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
dao.execute(listSql);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class));
return Result.success(pagination);
}
@At
@ApiOperation("新增课程")
@SaCheckPermission("learning.course.manage")
@SLog(tag = "新增课程", msg = "课程名称:${args[0].courseName}")
public Result doAdd(LearningCourse course) {
Result checkResult = checkCourse(course, null);
if (checkResult != null) {
return checkResult;
}
if (StrUtil.isBlank(course.getStatus())) {
course.setStatus("draft");
}
dao.insert(course);
return Result.success();
}
@At
@ApiOperation("编辑课程")
@SaCheckPermission("learning.course.manage")
@SLog(tag = "编辑课程", msg = "课程ID:${args[0].id}")
public Result doEdit(LearningCourse course) {
if (StrUtil.isBlank(course.getId())) {
return Result.error("请选择要编辑的数据");
}
Result checkResult = checkCourse(course, course.getId());
if (checkResult != null) {
return checkResult;
}
dao.updateIgnoreNull(course);
return Result.success();
}
@At
@ApiOperation("删除课程")
@SaCheckPermission("learning.course.manage")
@SLog(tag = "删除课程", msg = "课程ID:${args[0]}")
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("请选择要删除的数据");
}
int outlineCount = dao.count(LearningCourseOutline.class, Cnd.where("courseId", "=", id));
if (outlineCount > 0) {
return Result.error("该课程存在章节内容,请先删除章节内容后再删除课程");
}
dao.clear(LearningCourse.class, Cnd.where("id", "=", id));
return Result.success();
}
@At
@ApiOperation("启用课程类型")
@SaCheckPermission("learning.course.manage")
public Result courseTypes() {
return Result.success(dao.query(LearningCourseType.class, Cnd.where("enabled", "=", true).asc("sortNum").asc("createdAt")));
}
@At
@ApiOperation("学习教育数据字典")
@SaCheckPermission("learning.course.manage")
public Result learningDictOptions(String name) {
Sys_dict root = sysDictService.fetch(Cnd.where("code", "=", "学习教育").or("name", "=", "学习教育"));
if (root == null) {
return Result.success(List.of());
}
List<Sys_dict> firstLevel = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(root.getId())).and("disabled", "=", false).asc("location"));
if (StrUtil.isBlank(name)) {
return Result.success(firstLevel);
}
Sys_dict group = firstLevel.stream()
.filter(dict -> name.equals(dict.getName()) || name.equals(dict.getCode()))
.findFirst()
.orElse(null);
if (group == null) {
return Result.success(firstLevel);
}
List<Sys_dict> children = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(group.getId())).and("disabled", "=", false).asc("location"));
return Result.success(children.isEmpty() ? firstLevel : children);
}
private Result checkCourse(LearningCourse course, String excludeId) {
if (course == null || StrUtil.isBlank(course.getCourseName())) {
return Result.error("课程名称不能为空");
}
if (StrUtil.isBlank(course.getCourseTypeId())) {
return Result.error("请选择课程类型");
}
if (StrUtil.isBlank(course.getLecturerName())) {
return Result.error("授课讲师不能为空");
}
if (course.getSortNum() == null) {
return Result.error("排序编码不能为空");
}
if (!"long_term".equals(course.getOpenType()) && (course.getStartTime() == null || course.getEndTime() == null)) {
return Result.error("请选择开课时间");
}
Cnd cnd = Cnd.where("courseName", "=", course.getCourseName().trim());
if (StrUtil.isNotBlank(excludeId)) {
cnd.and("id", "!=", excludeId);
}
if (dao.count(LearningCourse.class, cnd) > 0) {
return Result.error("课程名称已存在");
}
course.setCourseName(course.getCourseName().trim());
if ("long_term".equals(course.getOpenType())) {
course.setStartTime(null);
course.setEndTime(null);
}
return null;
}
private String getOrderColumn(String prop) {
Map<String, String> columns = new HashMap<>();
columns.put("courseName", "c.courseName");
columns.put("courseTypeName", "t.typeName");
columns.put("lecturerName", "c.lecturerName");
columns.put("startTime", "c.startTime");
columns.put("status", "c.status");
columns.put("recommendFlags", "c.recommendFlags");
columns.put("sortNum", "c.sortNum");
return columns.get(prop);
}
}
@@ -0,0 +1,110 @@
package com.budwk.app.zhgh.learning.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.learning.models.LearningCourseType;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.pager.Pager;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
@IocBean
@Ok("json:full")
@Api("学习教育平台-课程类型设置")
@At("/platform/learning/course/type")
public class LearningCourseTypeController {
@Inject
private Dao dao;
@At("")
@Ok("beetl:/platform/zhgh/Learning/courseType/index.html")
@SaCheckPermission("learning.course.type")
public void index() {
}
@At
@ApiOperation("课程类型列表")
@SaCheckPermission("learning.course.type")
public Result pageData(PageForm pageForm, @Param("typeName") String typeName) {
Cnd cnd = Cnd.NEW();
cnd.and(Cnd.likeEX("typeName", typeName));
cnd.asc("sortNum").asc("createdAt");
int count = dao.count(LearningCourseType.class, cnd);
Pager pager = dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize());
Pagination<LearningCourseType> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, dao.query(LearningCourseType.class, cnd, pager));
return Result.success(pagination);
}
@At
@ApiOperation("新增课程类型")
@SaCheckPermission("learning.course.type")
@SLog(tag = "新增课程类型", msg = "课程类型名称:${args[0].typeName}")
public Result doAdd(LearningCourseType courseType) {
Result checkResult = checkCourseType(courseType, null);
if (checkResult != null) {
return checkResult;
}
if (courseType.getEnabled() == null) {
courseType.setEnabled(true);
}
dao.insert(courseType);
return Result.success();
}
@At
@ApiOperation("编辑课程类型")
@SaCheckPermission("learning.course.type")
@SLog(tag = "编辑课程类型", msg = "课程类型ID:${args[0].id}")
public Result doEdit(LearningCourseType courseType) {
if (StrUtil.isBlank(courseType.getId())) {
return Result.error("请选择要编辑的数据");
}
Result checkResult = checkCourseType(courseType, courseType.getId());
if (checkResult != null) {
return checkResult;
}
dao.updateIgnoreNull(courseType);
return Result.success();
}
@At
@ApiOperation("删除课程类型")
@SaCheckPermission("learning.course.type")
@SLog(tag = "删除课程类型", msg = "课程类型ID:${args[0]}")
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("请选择要删除的数据");
}
dao.clear(LearningCourseType.class, Cnd.where("id", "=", id));
return Result.success();
}
private Result checkCourseType(LearningCourseType courseType, String excludeId) {
if (courseType == null || StrUtil.isBlank(courseType.getTypeName())) {
return Result.error("课程类型名称不能为空");
}
if (courseType.getSortNum() == null) {
return Result.error("排序编号不能为空");
}
Cnd cnd = Cnd.where("typeName", "=", courseType.getTypeName().trim());
if (StrUtil.isNotBlank(excludeId)) {
cnd.and("id", "!=", excludeId);
}
if (dao.count(LearningCourseType.class, cnd) > 0) {
return Result.error("课程类型名称已存在");
}
courseType.setTypeName(courseType.getTypeName().trim());
return null;
}
}
@@ -0,0 +1,47 @@
package com.budwk.app.zhgh.learning.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
@IocBean
@At("/platform/learning")
public class LearningPlatformController {
@At("/activity/manage")
@Ok("beetl:/platform/zhgh/Learning/activityManage/index.html")
@SaCheckPermission("learning.activity.manage")
public void activityManage() {
}
@At("/my/record")
@Ok("beetl:/platform/zhgh/Learning/myRecord/index.html")
@SaCheckPermission("learning.my.record")
public void myRecord() {
}
@At("/my/record/h5")
@Ok("beetl:/platform/zhghh5/learning/myRecord/index.html")
@SaCheckPermission("learning.my.record")
public void myRecordH5() {
}
@At("/statistics")
@Ok("beetl:/platform/zhgh/Learning/statistics/index.html")
@SaCheckPermission("learning.statistics")
public void statistics() {
}
@At("/course/h5")
@Ok("beetl:/platform/zhghh5/learning/course/index.html")
@SaCheckPermission("learning.course.display")
public void courseH5() {
}
@At("/course/h5/study")
@Ok("beetl:/platform/zhghh5/learning/course/study.html")
@SaCheckPermission("learning.course.display")
public void courseH5Study() {
}
}
@@ -0,0 +1,330 @@
package com.budwk.app.zhgh.learning.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.result.Result;
import com.budwk.app.base.utils.PageUtil;
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.learning.models.LearningCourse;
import com.budwk.app.zhgh.learning.param.LearningStatisticsPageForm;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Static;
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.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@IocBean
@Ok("json:full")
@Api("学习教育平台-学习统计")
@At("/platform/learning/statistics")
public class LearningStatisticsController {
@Inject
private Dao dao;
@At("")
@Ok("beetl:/platform/zhgh/Learning/statistics/index.html")
@SaCheckPermission("learning.statistics")
public void index() {
}
@At
@ApiOperation("课程统计")
@SaCheckPermission("learning.statistics")
public Result coursePageData(LearningStatisticsPageForm pageForm) {
Cnd cnd = courseCondition(pageForm);
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM learning_course c
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
dao.execute(countSql);
Sql listSql = Sqls.create("""
SELECT
c.id AS courseId,
c.courseName,
IFNULL(COUNT(uc.userId), 0) AS learnerCount,
IFNULL(SUM(CASE WHEN target.targetCount > 0 AND uc.completedOutlineCount >= target.targetCount THEN 1 ELSE 0 END), 0) AS completedCount,
IFNULL(ROUND(AVG(uc.studySeconds)), 0) AS avgStudySeconds
FROM learning_course c
LEFT JOIN (
SELECT
courseId,
CASE
WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0
THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END)
ELSE COUNT(1)
END AS targetCount,
SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount
FROM learning_course_outline
WHERE status = 'enabled'
GROUP BY courseId
) target ON target.courseId = c.id
LEFT JOIN (
SELECT
r.courseId,
r.userId,
SUM(IFNULL(r.studySeconds, 0)) AS studySeconds,
COUNT(DISTINCT CASE
WHEN r.completeStatus = 'completed'
AND ((target.requiredCount > 0 AND o.required = 1) OR target.requiredCount = 0)
THEN r.outlineId
ELSE NULL
END) AS completedOutlineCount
FROM learning_study_record r
JOIN learning_course_outline o ON o.id = r.outlineId AND o.courseId = r.courseId AND o.status = 'enabled'
JOIN (
SELECT
courseId,
CASE
WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0
THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END)
ELSE COUNT(1)
END AS targetCount,
SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount
FROM learning_course_outline
WHERE status = 'enabled'
GROUP BY courseId
) target ON target.courseId = r.courseId
$recordCondition
GROUP BY r.courseId, r.userId
) uc ON uc.courseId = c.id
$condition
GROUP BY c.id, c.courseName
ORDER BY $orderColumn $orderBy
""");
listSql.setCondition(cnd);
listSql.setVar("recordCondition", new Static(recordWhere(pageForm)));
setOrder(listSql, pageForm, "course");
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
dao.execute(listSql);
List<NutMap> rows = listSql.getList(NutMap.class);
rows.forEach(row -> row.put("avgStudyTimeText", formatStudySeconds(row.getInt("avgStudySeconds", 0))));
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), countSql.getInt(), rows));
}
@At
@ApiOperation("分工会统计")
@SaCheckPermission("learning.statistics")
public Result unionPageData(LearningStatisticsPageForm pageForm) {
Cnd cnd = unionCondition(pageForm);
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM learning_course c
JOIN (
SELECT r.courseId, r.unionId
FROM learning_study_record r
$recordCondition
GROUP BY r.courseId, r.unionId
) uc ON uc.courseId = c.id
LEFT JOIN sys_union un ON un.id = uc.unionId
$condition
""");
countSql.setCondition(cnd);
countSql.setVar("recordCondition", new Static(recordWhere(pageForm)));
countSql.setCallback(Sqls.callback.integer());
dao.execute(countSql);
Sql listSql = Sqls.create("""
SELECT
c.id AS courseId,
c.courseName,
un.id AS unionId,
IFNULL(un.name, uc.unionName) AS unionName,
COUNT(uc.userId) AS learnerCount,
IFNULL(SUM(uc.studySeconds), 0) AS studySeconds,
IFNULL(SUM(CASE WHEN target.targetCount > 0 AND uc.completedOutlineCount >= target.targetCount THEN 1 ELSE 0 END), 0) AS completedCount
FROM learning_course c
JOIN (
SELECT
r.courseId,
r.userId,
r.unionId,
MAX(r.unionName) AS unionName,
SUM(IFNULL(r.studySeconds, 0)) AS studySeconds,
COUNT(DISTINCT CASE
WHEN r.completeStatus = 'completed'
AND ((target.requiredCount > 0 AND o.required = 1) OR target.requiredCount = 0)
THEN r.outlineId
ELSE NULL
END) AS completedOutlineCount
FROM learning_study_record r
JOIN learning_course_outline o ON o.id = r.outlineId AND o.courseId = r.courseId AND o.status = 'enabled'
JOIN (
SELECT
courseId,
CASE
WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0
THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END)
ELSE COUNT(1)
END AS targetCount,
SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount
FROM learning_course_outline
WHERE status = 'enabled'
GROUP BY courseId
) target ON target.courseId = r.courseId
$recordCondition
GROUP BY r.courseId, r.userId, r.unionId
) uc ON uc.courseId = c.id
LEFT JOIN sys_union un ON un.id = uc.unionId
LEFT JOIN (
SELECT
courseId,
CASE
WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0
THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END)
ELSE COUNT(1)
END AS targetCount,
SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount
FROM learning_course_outline
WHERE status = 'enabled'
GROUP BY courseId
) target ON target.courseId = c.id
$condition
GROUP BY c.id, c.courseName, uc.unionId, un.id, un.name
ORDER BY $orderColumn $orderBy
""");
listSql.setCondition(cnd);
listSql.setVar("recordCondition", new Static(recordWhere(pageForm)));
setOrder(listSql, pageForm, "union");
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
dao.execute(listSql);
List<NutMap> rows = listSql.getList(NutMap.class);
rows.forEach(row -> {
int learnerCount = row.getInt("learnerCount", 0);
int completedCount = row.getInt("completedCount", 0);
row.put("studyTimeText", formatStudySeconds(row.getInt("studySeconds", 0)));
row.put("completeRate", learnerCount == 0 ? "0%" : BigDecimal.valueOf(completedCount * 100.0 / learnerCount).setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() + "%");
});
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), countSql.getInt(), rows));
}
@At
@ApiOperation("可选课程")
@SaCheckPermission("learning.statistics")
public Result courseOptions(String courseName) {
Cnd cnd = Cnd.NEW();
cnd.and(Cnd.likeEX("courseName", courseName));
cnd.desc("createdAt").asc("sortNum");
return Result.success(dao.query(LearningCourse.class, cnd, dao.createPager(1, 50)));
}
@At
@ApiOperation("可选分工会")
@SaCheckPermission("learning.statistics")
public Result unionOptions() {
if (hasSchoolScope()) {
return Result.success(dao.query(Sys_union.class, Cnd.NEW().asc("unionCode").asc("name")));
}
if (hasBranchScope()) {
return Result.success(dao.query(Sys_union.class, Cnd.where("id", "=", SecurityUtil.getUnionId())));
}
return Result.success(List.of());
}
private Cnd courseCondition(LearningStatisticsPageForm pageForm) {
Cnd cnd = Cnd.NEW();
cnd.andEX("c.id", "=", pageForm.getCourseId());
cnd.and(Cnd.likeEX("c.courseName", pageForm.getCourseName()));
return cnd;
}
private Cnd unionCondition(LearningStatisticsPageForm pageForm) {
Cnd cnd = courseCondition(pageForm);
cnd.andEX("uc.unionId", "=", pageForm.getUnionId());
return cnd;
}
private String recordWhere(LearningStatisticsPageForm pageForm) {
StringBuilder where = new StringBuilder("WHERE 1 = 1");
if (hasBranchScope()) {
where.append(" AND r.unionId = '").append(escapeSql(SecurityUtil.getUnionId())).append("'");
} else if (!hasSchoolScope()) {
where.append(" AND r.userId = '").append(escapeSql(SecurityUtil.getUserId())).append("'");
}
if (StrUtil.isNotBlank(pageForm.getUnionId())) {
where.append(" AND r.unionId = '").append(escapeSql(pageForm.getUnionId())).append("'");
}
return where.toString();
}
private void setOrder(Sql sql, LearningStatisticsPageForm pageForm, String type) {
String orderColumn = "union".equals(type) ? getUnionOrderColumn(pageForm.getPageOrderName()) : getCourseOrderColumn(pageForm.getPageOrderName());
String orderBy = PageUtil.getOrder(pageForm.getPageOrderBy());
if (StrUtil.isBlank(orderColumn)) {
orderColumn = "union".equals(type) ? "c.courseName ASC, un.unionCode" : "c.sortNum ASC, c.createdAt";
orderBy = "DESC";
} else if (StrUtil.isBlank(orderBy)) {
orderBy = "ASC";
}
sql.setVar("orderColumn", new Static(orderColumn));
sql.setVar("orderBy", new Static(orderBy));
}
private String getCourseOrderColumn(String prop) {
Map<String, String> columns = new HashMap<>();
columns.put("courseName", "c.courseName");
columns.put("learnerCount", "learnerCount");
columns.put("completedCount", "completedCount");
columns.put("avgStudySeconds", "avgStudySeconds");
return columns.get(prop);
}
private String getUnionOrderColumn(String prop) {
Map<String, String> columns = new HashMap<>();
columns.put("courseName", "c.courseName");
columns.put("unionName", "unionName");
columns.put("learnerCount", "learnerCount");
columns.put("studySeconds", "studySeconds");
columns.put("completeRate", "completedCount");
return columns.get(prop);
}
private boolean hasSchoolScope() {
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name());
}
private boolean hasBranchScope() {
return AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name());
}
private String escapeSql(String value) {
return value == null ? "" : value.replace("'", "''");
}
private String formatStudySeconds(Integer seconds) {
int value = seconds == null ? 0 : seconds;
int hour = value / 3600;
int minute = value % 3600 / 60;
int second = value % 60;
if (hour > 0) {
return hour + "小时" + minute + "" + second + "";
}
if (minute > 0) {
return minute + "" + second + "";
}
return second + "";
}
}
@@ -0,0 +1,493 @@
package com.budwk.app.zhgh.learning.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.base.utils.PageUtil;
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.learning.models.LearningCourse;
import com.budwk.app.zhgh.learning.models.LearningCourseOutline;
import com.budwk.app.zhgh.learning.models.LearningOutlineResource;
import com.budwk.app.zhgh.learning.models.LearningStudyRecord;
import com.budwk.app.zhgh.learning.models.LearningStudyRule;
import com.budwk.app.zhgh.learning.models.LearningStudySegment;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@IocBean
@Ok("json:full")
@Api("学习教育平台-学习记录")
@At("/platform/learning/study/record")
public class LearningStudyRecordController {
private static final int MAX_HEARTBEAT_SECONDS = 30;
@Inject
private Dao dao;
@At
@ApiOperation("学习记录分页")
@SaCheckPermission("learning.my.record")
public Result pageData(PageForm pageForm,
@Param("keyword") String keyword,
@Param("unionId") String unionId,
@Param("courseId") String courseId,
@Param("completeStatus") String completeStatus) {
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("r.loginName", "like", "%" + keyword + "%");
seg.or("r.userName", "like", "%" + keyword + "%");
cnd.and(seg);
}
cnd.andEX("r.unionId", "=", unionId);
cnd.andEX("r.courseId", "=", courseId);
cnd.andEX("r.completeStatus", "=", completeStatus);
appendScope(cnd);
String orderColumn = getOrderColumn(pageForm.getPageOrderName());
if (StrUtil.isNotBlank(orderColumn) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) {
cnd.orderBy(orderColumn, PageUtil.getOrder(pageForm.getPageOrderBy()));
} else {
cnd.desc("r.latestStudyTime");
}
Sql countSql = Sqls.create("SELECT COUNT(1) FROM learning_study_record r $condition");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
dao.execute(countSql);
Sql listSql = Sqls.create("""
SELECT r.*
FROM learning_study_record r
$condition
""");
listSql.setCondition(cnd);
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
dao.execute(listSql);
List<NutMap> rows = listSql.getList(NutMap.class);
rows.forEach(row -> row.put("studyTimeText", formatStudySeconds(row.getInt("studySeconds", 0))));
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), countSql.getInt(), rows));
}
@At
@ApiOperation("移动端我的学习汇总")
@SaCheckPermission("learning.my.record")
public Result h5Summary() {
String userId = SecurityUtil.getUserId();
Sql sql = Sqls.create("""
SELECT IFNULL(SUM(IFNULL(studySeconds, 0)), 0) AS studySeconds,
COUNT(DISTINCT courseId) AS courseCount,
COUNT(DISTINCT CASE WHEN completeStatus = 'completed' THEN courseId END) AS completedCourseCount,
COUNT(DISTINCT outlineId) AS outlineCount,
COUNT(DISTINCT CASE WHEN completeStatus = 'completed' THEN outlineId END) AS completedOutlineCount
FROM learning_study_record
WHERE userId = @userId
""");
sql.params().set("userId", userId);
sql.setCallback(Sqls.callback.map());
dao.execute(sql);
NutMap data = sql.getObject(NutMap.class);
if (data == null) {
data = NutMap.NEW();
}
int studySeconds = data.getInt("studySeconds", 0);
data.put("studyHour", studySeconds / 3600);
data.put("studyMinute", studySeconds % 3600 / 60);
return Result.success(data);
}
@At
@ApiOperation("移动端我的学习课程")
@SaCheckPermission("learning.my.record")
public Result h5Courses() {
String userId = SecurityUtil.getUserId();
Sql sql = Sqls.create("""
SELECT r.courseId,
MAX(r.courseName) AS courseName,
MAX(c.cover) AS cover,
COUNT(1) AS outlineCount,
SUM(CASE WHEN r.completeStatus = 'completed' THEN 1 ELSE 0 END) AS completedOutlineCount,
IFNULL(SUM(IFNULL(r.studySeconds, 0)), 0) AS studySeconds,
IFNULL(ROUND(AVG(IFNULL(r.progressPercent, 0))), 0) AS progressPercent,
MAX(r.latestStudyTime) AS latestStudyTime
FROM learning_study_record r
LEFT JOIN learning_course c ON c.id = r.courseId
WHERE r.userId = @userId
GROUP BY r.courseId
ORDER BY latestStudyTime DESC
""");
sql.params().set("userId", userId);
sql.setCallback(Sqls.callback.maps());
dao.execute(sql);
List<NutMap> rows = sql.getList(NutMap.class);
rows.forEach(row -> row.put("studyTimeText", formatStudySeconds(row.getInt("studySeconds", 0))));
return Result.success(rows);
}
@At
@ApiOperation("开始学习")
@SaCheckPermission("learning.course.display")
public Result start(@Param("courseId") String courseId,
@Param("resourceId") String resourceId,
@Param("positionSeconds") Integer positionSeconds) {
if (StrUtil.hasBlank(courseId, resourceId)) {
return Result.error("请选择课程资料后开始学习");
}
LearningCourse course = dao.fetch(LearningCourse.class, courseId);
LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, resourceId);
if (course == null || resource == null || !courseId.equals(resource.getCourseId())) {
return Result.error("课程资料不存在");
}
LearningCourseOutline outline = dao.fetch(LearningCourseOutline.class, resource.getOutlineId());
if (outline == null) {
return Result.error("章节不存在");
}
String userId = SecurityUtil.getUserId();
Date now = new Date();
closeUnfinishedSegments(userId, "interrupted");
LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, Cnd.where("userId", "=", userId)
.and("courseId", "=", courseId)
.and("outlineId", "=", outline.getId()));
if (record == null) {
record = buildRecord(userId, course, outline, now);
record.setLastPositionSeconds(sanitizePosition(positionSeconds, 0));
dao.insert(record);
} else {
int requiredSeconds = requiredSeconds(outline.getId());
dao.update(LearningStudyRecord.class, Chain.make("latestStudyTime", now)
.add("courseName", course.getCourseName())
.add("outlineName", outline.getTitle())
.add("lastPositionSeconds", sanitizePosition(positionSeconds, record.getLastPositionSeconds()))
.add("requiredSeconds", requiredSeconds)
.add("progressPercent", progress(record.getStudySeconds(), requiredSeconds))
.add("completeStatus", "completed".equals(record.getCompleteStatus()) ? "completed" : "studying"),
Cnd.where("id", "=", record.getId()));
record = dao.fetch(LearningStudyRecord.class, record.getId());
}
LearningStudySegment segment = new LearningStudySegment();
segment.setRecordId(record.getId());
segment.setUserId(userId);
segment.setCourseId(courseId);
segment.setOutlineId(outline.getId());
segment.setResourceId(resourceId);
segment.setResourceName(resource.getResourceTitle());
segment.setStartTime(now);
segment.setLastHeartbeatTime(now);
segment.setActiveSeconds(0);
segment.setState("studying");
dao.insert(segment);
return Result.success(NutMap.NEW()
.addv("recordId", record.getId())
.addv("segmentId", segment.getId())
.addv("studySeconds", record.getStudySeconds())
.addv("lastPositionSeconds", record.getLastPositionSeconds())
.addv("requiredSeconds", record.getRequiredSeconds())
.addv("progressPercent", record.getProgressPercent())
.addv("completeStatus", record.getCompleteStatus()));
}
@At
@ApiOperation("学习心跳")
@SaCheckPermission("learning.course.display")
public Result heartbeat(@Param("segmentId") String segmentId,
@Param("activeSeconds") Integer activeSeconds,
@Param("positionSeconds") Integer positionSeconds) {
LearningStudySegment segment = fetchOwnStudyingSegment(segmentId);
if (segment == null) {
return Result.error("学习时段已结束,请重新开始学习");
}
int seconds = Math.max(0, Math.min(activeSeconds == null ? 0 : activeSeconds, MAX_HEARTBEAT_SECONDS));
return Result.success(addActiveSeconds(segment, seconds, false, positionSeconds));
}
@At
@ApiOperation("结束学习")
@SaCheckPermission("learning.course.display")
public Result finish(@Param("segmentId") String segmentId,
@Param("activeSeconds") Integer activeSeconds,
@Param("positionSeconds") Integer positionSeconds) {
LearningStudySegment segment = fetchOwnStudyingSegment(segmentId);
if (segment == null) {
return Result.success();
}
int seconds = Math.max(0, Math.min(activeSeconds == null ? 0 : activeSeconds, MAX_HEARTBEAT_SECONDS));
NutMap result = addActiveSeconds(segment, seconds, true, positionSeconds);
dao.update(LearningStudySegment.class, Chain.make("endTime", new Date()).add("state", "finished"), Cnd.where("id", "=", segmentId));
return Result.success(result);
}
@At
@ApiOperation("保存播放位置")
@SaCheckPermission("learning.course.display")
public Result position(@Param("courseId") String courseId,
@Param("resourceId") String resourceId,
@Param("positionSeconds") Integer positionSeconds) {
if (StrUtil.hasBlank(courseId, resourceId) || positionSeconds == null || positionSeconds < 0) {
return Result.success();
}
LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, resourceId);
if (resource == null || !courseId.equals(resource.getCourseId())) {
return Result.success();
}
dao.update(LearningStudyRecord.class,
Chain.make("lastPositionSeconds", positionSeconds).add("latestStudyTime", new Date()),
Cnd.where("userId", "=", SecurityUtil.getUserId())
.and("courseId", "=", courseId)
.and("outlineId", "=", resource.getOutlineId()));
return Result.success();
}
@At
@ApiOperation("可选课程")
@SaCheckPermission("learning.my.record")
public Result courseOptions() {
return Result.success(dao.query(LearningCourse.class, Cnd.NEW().asc("sortNum").desc("createdAt")));
}
@At
@ApiOperation("可选分工会")
@SaCheckPermission("learning.my.record")
public Result unionOptions() {
if (hasSchoolScope()) {
return Result.success(dao.query(Sys_union.class, Cnd.NEW().asc("unionCode").asc("name")));
}
if (hasBranchScope()) {
return Result.success(dao.query(Sys_union.class, Cnd.where("id", "=", SecurityUtil.getUnionId())));
}
return Result.success(List.of());
}
@At
@ApiOperation("删除学习记录")
@SaCheckPermission("learning.my.record")
public Result delete(@Param("id") String id) {
if (StrUtil.isBlank(id)) {
return Result.error("请选择要删除的学习记录");
}
LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, id);
if (record == null) {
return Result.error("学习记录不存在");
}
if (!canOperate(record)) {
return Result.error("无权删除该学习记录");
}
dao.clear(LearningStudySegment.class, Cnd.where("recordId", "=", id));
dao.clear(LearningStudyRecord.class, Cnd.where("id", "=", id));
return Result.success();
}
private LearningStudyRecord buildRecord(String userId, LearningCourse course, LearningCourseOutline outline, Date now) {
NutMap user = currentUserInfo(userId);
int requiredSeconds = requiredSeconds(outline.getId());
LearningStudyRecord record = new LearningStudyRecord();
record.setUserId(userId);
record.setLoginName(user.getString("loginname", SecurityUtil.getUserLoginname()));
record.setUserName(user.getString("username", SecurityUtil.getUserUsername()));
record.setUnionId(user.getString("unionId", SecurityUtil.getUnionId()));
record.setUnionName(user.getString("unionName", ""));
record.setCourseId(course.getId());
record.setCourseName(course.getCourseName());
record.setOutlineId(outline.getId());
record.setOutlineName(outline.getTitle());
record.setFirstStudyTime(now);
record.setLatestStudyTime(now);
record.setStudySeconds(0);
record.setLastPositionSeconds(0);
record.setRequiredSeconds(requiredSeconds);
record.setProgressPercent(0);
record.setCompleteStatus("studying");
return record;
}
private NutMap addActiveSeconds(LearningStudySegment segment, int activeSeconds, boolean finish, Integer positionSeconds) {
Date now = new Date();
int segmentSeconds = value(segment.getActiveSeconds()) + activeSeconds;
dao.update(LearningStudySegment.class, Chain.make("activeSeconds", segmentSeconds).add("lastHeartbeatTime", now), Cnd.where("id", "=", segment.getId()));
LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, segment.getRecordId());
int studySeconds = value(record.getStudySeconds()) + activeSeconds;
int requiredSeconds = requiredSeconds(record.getOutlineId());
int progressPercent = progress(studySeconds, requiredSeconds);
String completeStatus = completeStatus(studySeconds, requiredSeconds, finish);
if ("completed".equals(completeStatus)) {
progressPercent = 100;
}
Chain chain = Chain.make("latestStudyTime", now)
.add("studySeconds", studySeconds)
.add("requiredSeconds", requiredSeconds)
.add("progressPercent", progressPercent)
.add("completeStatus", completeStatus);
int lastPositionSeconds = value(record.getLastPositionSeconds());
if (positionSeconds != null && positionSeconds >= 0) {
lastPositionSeconds = positionSeconds;
chain.add("lastPositionSeconds", positionSeconds);
}
if ("completed".equals(completeStatus) && record.getCompletedAt() == null) {
chain.add("completedAt", now);
}
dao.update(LearningStudyRecord.class, chain, Cnd.where("id", "=", record.getId()));
return NutMap.NEW()
.addv("studySeconds", studySeconds)
.addv("studyTimeText", formatStudySeconds(studySeconds))
.addv("lastPositionSeconds", lastPositionSeconds)
.addv("requiredSeconds", requiredSeconds)
.addv("progressPercent", progressPercent)
.addv("completeStatus", completeStatus);
}
private LearningStudySegment fetchOwnStudyingSegment(String segmentId) {
if (StrUtil.isBlank(segmentId)) {
return null;
}
return dao.fetch(LearningStudySegment.class, Cnd.where("id", "=", segmentId)
.and("userId", "=", SecurityUtil.getUserId())
.and("state", "=", "studying"));
}
private void closeUnfinishedSegments(String userId, String state) {
dao.update(LearningStudySegment.class,
Chain.make("endTime", new Date()).add("state", state),
Cnd.where("userId", "=", userId).and("state", "=", "studying"));
}
private int requiredSeconds(String outlineId) {
LearningStudyRule rule = dao.fetch(LearningStudyRule.class, Cnd.where("targetType", "=", "outline")
.and("targetId", "=", outlineId)
.and("status", "=", "enabled"));
if (rule != null && value(rule.getMinStudySeconds()) > 0) {
return rule.getMinStudySeconds();
}
Sql sql = Sqls.create("""
SELECT IFNULL(SUM(IFNULL(durationSeconds, 0)), 0)
FROM learning_outline_resource
WHERE outlineId = @outlineId AND status = 'enabled'
""");
sql.params().set("outlineId", outlineId);
sql.setCallback(Sqls.callback.integer());
dao.execute(sql);
return Math.max(0, sql.getInt());
}
private int progress(Integer studySeconds, int requiredSeconds) {
if (requiredSeconds <= 0) {
return 0;
}
return Math.min(100, (int) Math.floor(value(studySeconds) * 100.0 / requiredSeconds));
}
private String completeStatus(Integer studySeconds, int requiredSeconds, boolean finish) {
if (requiredSeconds <= 0) {
return finish ? "completed" : "studying";
}
if (value(studySeconds) >= requiredSeconds) {
return "completed";
}
return value(studySeconds) > 0 || finish ? "studying" : "not_started";
}
private NutMap currentUserInfo(String userId) {
Sql sql = Sqls.create("SELECT id, loginname, username, unionId AS unionId, unionName AS unionName FROM vw_user WHERE id = @id");
sql.params().set("id", userId);
sql.setCallback(Sqls.callback.map());
dao.execute(sql);
NutMap user = sql.getObject(NutMap.class);
return user == null ? NutMap.NEW() : user;
}
private void appendScope(Cnd cnd) {
if (hasSchoolScope()) {
return;
}
if (hasBranchScope()) {
cnd.and("r.unionId", "=", SecurityUtil.getUnionId());
return;
}
cnd.and("r.userId", "=", SecurityUtil.getUserId());
}
private boolean hasSchoolScope() {
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name());
}
private boolean hasBranchScope() {
return AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name());
}
private boolean canOperate(LearningStudyRecord record) {
if (hasSchoolScope()) {
return true;
}
if (hasBranchScope()) {
return StrUtil.equals(record.getUnionId(), SecurityUtil.getUnionId());
}
return StrUtil.equals(record.getUserId(), SecurityUtil.getUserId());
}
private String getOrderColumn(String prop) {
Map<String, String> columns = new HashMap<>();
columns.put("loginName", "r.loginName");
columns.put("userName", "r.userName");
columns.put("unionName", "r.unionName");
columns.put("courseName", "r.courseName");
columns.put("outlineName", "r.outlineName");
columns.put("firstStudyTime", "r.firstStudyTime");
columns.put("latestStudyTime", "r.latestStudyTime");
columns.put("studySeconds", "r.studySeconds");
columns.put("progressPercent", "r.progressPercent");
columns.put("completeStatus", "r.completeStatus");
return columns.get(prop);
}
private String formatStudySeconds(Integer seconds) {
int value = value(seconds);
int hour = value / 3600;
int minute = value % 3600 / 60;
int second = value % 60;
if (hour > 0) {
return hour + "小时" + minute + "" + second + "";
}
if (minute > 0) {
return minute + "" + second + "";
}
return second + "";
}
private int value(Integer value) {
return value == null ? 0 : value;
}
private int sanitizePosition(Integer positionSeconds, Integer defaultValue) {
if (positionSeconds == null || positionSeconds < 0) {
return value(defaultValue);
}
return positionSeconds;
}
}
@@ -0,0 +1,117 @@
package com.budwk.app.zhgh.learning.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
@Data
@Table
@EqualsAndHashCode(callSuper = true)
public class LearningCourse extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("课程名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String courseName;
@Column
@Comment("课程类型")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String courseTypeId;
@Column
@Comment("授课讲师")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String lecturerName;
@Column
@Comment("讲师基本信息")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String lecturerInfo;
@Column
@Comment("课程简介")
@ColDefine(customType = "longtext")
private String courseIntro;
@Column
@Comment("适合人群")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String suitablePeople;
@Column
@Comment("学习目标")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String learningGoal;
@Column
@Comment("课程周期")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String coursePeriod;
@Column
@Comment("开课类型")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String openType;
@Column
@Comment("开始时间")
@ColDefine(type = ColType.DATETIME)
private Date startTime;
@Column
@Comment("结束时间")
@ColDefine(type = ColType.DATETIME)
private Date endTime;
@Column
@Comment("课程状态")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String status;
@Column
@Comment("推荐标识")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String recommendFlags;
@Column
@Comment("课程标签")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String courseTags;
@Column
@Comment("学习对象")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String targetType;
@Column
@Comment("指定组织")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String targetOrgText;
@Column
@Comment("排序编码")
@ColDefine(type = ColType.INT)
private Integer sortNum;
@Column
@Comment("课程封面")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String cover;
}
@@ -0,0 +1,75 @@
package com.budwk.app.zhgh.learning.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.interceptor.annotation.PrevInsert;
@Data
@Table
@EqualsAndHashCode(callSuper = true)
public class LearningCourseOutline extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("所属课程ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String courseId;
@Column
@Comment("所属课程名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String courseName;
@Column
@Comment("父节点ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String parentId;
@Column
@Comment("节点类型")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String nodeType;
@Column
@Comment("章/节标题")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String title;
@Column
@Comment("副标题")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String subtitle;
@Column
@Comment("简介")
@ColDefine(customType = "longtext")
private String description;
@Column
@Comment("排序值")
@ColDefine(type = ColType.INT)
private Integer sortOrder;
@Column
@Comment("是否必学")
@ColDefine(type = ColType.BOOLEAN)
private Boolean required;
@Column
@Comment("状态")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String status;
}
@@ -0,0 +1,45 @@
package com.budwk.app.zhgh.learning.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.interceptor.annotation.PrevInsert;
@Data
@Table
@EqualsAndHashCode(callSuper = true)
public class LearningCourseType extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("课程类型名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String typeName;
@Column
@Comment("排序编号")
@ColDefine(type = ColType.INT)
private Integer sortNum;
@Column
@Comment("是否启用")
@ColDefine(type = ColType.BOOLEAN)
private Boolean enabled;
@Column
@Comment("备注")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String remark;
}
@@ -0,0 +1,85 @@
package com.budwk.app.zhgh.learning.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.interceptor.annotation.PrevInsert;
@Data
@Table
@EqualsAndHashCode(callSuper = true)
public class LearningOutlineResource extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("所属课程ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String courseId;
@Column
@Comment("所属章/节ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String outlineId;
@Column
@Comment("资料标题")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String resourceTitle;
@Column
@Comment("资料类型")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String resourceType;
@Column
@Comment("文件扩展名")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String fileExt;
@Column
@Comment("附件")
@ColDefine(customType = "longtext")
private String fileData;
@Column
@Comment("视频/音频时长")
@ColDefine(type = ColType.INT)
private Integer durationSeconds;
@Column
@Comment("排序值")
@ColDefine(type = ColType.INT)
private Integer sortOrder;
@Column
@Comment("是否必学")
@ColDefine(type = ColType.BOOLEAN)
private Boolean required;
@Column
@Comment("是否允许预览")
@ColDefine(type = ColType.BOOLEAN)
private Boolean allowPreview;
@Column
@Comment("是否允许下载")
@ColDefine(type = ColType.BOOLEAN)
private Boolean allowDownload;
@Column
@Comment("状态")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String status;
}
@@ -0,0 +1,119 @@
package com.budwk.app.zhgh.learning.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Index;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableIndexes;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
@Data
@Table("learning_study_record")
@TableIndexes({
@Index(name = "idx_learning_record_user_course_outline", fields = {"userId", "courseId", "outlineId"}, unique = true),
@Index(name = "idx_learning_record_union", fields = {"unionId"}, unique = false),
@Index(name = "idx_learning_record_course", fields = {"courseId"}, unique = false)
})
@EqualsAndHashCode(callSuper = true)
public class LearningStudyRecord extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("学习人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String loginName;
@Column
@Comment("姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String userName;
@Column
@Comment("所属分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("所属分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("课程ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String courseId;
@Column
@Comment("课程名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String courseName;
@Column
@Comment("章节ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String outlineId;
@Column
@Comment("章节名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String outlineName;
@Column
@Comment("第一次进入时间")
@ColDefine(type = ColType.DATETIME)
private Date firstStudyTime;
@Column
@Comment("最近学习时间")
@ColDefine(type = ColType.DATETIME)
private Date latestStudyTime;
@Column
@Comment("累计有效学习时长(秒)")
@ColDefine(type = ColType.INT)
private Integer studySeconds;
@Column
@Comment("最近播放位置(秒)")
@ColDefine(type = ColType.INT)
private Integer lastPositionSeconds;
@Column
@Comment("要求学习时长(秒)")
@ColDefine(type = ColType.INT)
private Integer requiredSeconds;
@Column
@Comment("学习进度")
@ColDefine(type = ColType.INT)
private Integer progressPercent;
@Column
@Comment("完成状态")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String completeStatus;
@Column
@Comment("完成时间")
@ColDefine(type = ColType.DATETIME)
private Date completedAt;
}
@@ -0,0 +1,100 @@
package com.budwk.app.zhgh.learning.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.interceptor.annotation.PrevInsert;
@Data
@Table
@EqualsAndHashCode(callSuper = true)
public class LearningStudyRule extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("所属课程ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String courseId;
@Column
@Comment("规则对象类型")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String targetType;
@Column
@Comment("规则对象ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String targetId;
@Column
@Comment("是否必学")
@ColDefine(type = ColType.BOOLEAN)
private Boolean required;
@Column
@Comment("学习方式")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String studyMode;
@Column
@Comment("完成规则")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String completionRule;
@Column
@Comment("完成比例")
@ColDefine(type = ColType.INT)
private Integer completePercent;
@Column
@Comment("最少学习时长")
@ColDefine(type = ColType.INT)
private Integer minStudySeconds;
@Column
@Comment("解锁规则")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String unlockRule;
@Column
@Comment("是否允许跳过")
@ColDefine(type = ColType.BOOLEAN)
private Boolean allowSkip;
@Column
@Comment("是否允许拖动")
@ColDefine(type = ColType.BOOLEAN)
private Boolean allowDrag;
@Column
@Comment("暂停是否计时")
@ColDefine(type = ColType.BOOLEAN)
private Boolean pauseCountTime;
@Column
@Comment("页面隐藏是否计时")
@ColDefine(type = ColType.BOOLEAN)
private Boolean hiddenCountTime;
@Column
@Comment("长时间无操作是否计时")
@ColDefine(type = ColType.BOOLEAN)
private Boolean inactiveCountTime;
@Column
@Comment("状态")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String status;
}
@@ -0,0 +1,88 @@
package com.budwk.app.zhgh.learning.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Index;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableIndexes;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
@Data
@Table("learning_study_segment")
@TableIndexes({
@Index(name = "idx_learning_segment_record", fields = {"recordId"}, unique = false),
@Index(name = "idx_learning_segment_user_state", fields = {"userId", "state"}, unique = false)
})
@EqualsAndHashCode(callSuper = true)
public class LearningStudySegment extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("学习记录ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String recordId;
@Column
@Comment("学习人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("课程ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String courseId;
@Column
@Comment("章节ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String outlineId;
@Column
@Comment("资源ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String resourceId;
@Column
@Comment("资源名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String resourceName;
@Column
@Comment("开始时间")
@ColDefine(type = ColType.DATETIME)
private Date startTime;
@Column
@Comment("结束时间")
@ColDefine(type = ColType.DATETIME)
private Date endTime;
@Column
@Comment("最近心跳时间")
@ColDefine(type = ColType.DATETIME)
private Date lastHeartbeatTime;
@Column
@Comment("本时段有效学习时长(秒)")
@ColDefine(type = ColType.INT)
private Integer activeSeconds;
@Column
@Comment("状态")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String state;
}
@@ -0,0 +1,16 @@
package com.budwk.app.zhgh.learning.param;
import com.budwk.app.base.param.PageForm;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class LearningStatisticsPageForm extends PageForm {
private String courseId;
private String courseName;
private String unionId;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -0,0 +1,439 @@
;(function (window, $) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
const commandWords = [
"打开",
"进入",
"跳转",
"跳到",
"去",
"访问",
"帮我",
"请",
"页面",
"菜单",
"一下"
]
const state = {
recognition: null,
listening: false,
menus: null,
parentMap: {},
lastText: "",
pendingMatches: [],
awaitingChoice: false,
afterRecognitionEnd: null
}
function normalizePlain(text) {
return String(text || "")
.toLowerCase()
.replace(/[,。!?、,.!?;:\s]/g, "")
}
function normalizeCommand(text) {
return normalizePlain(text)
.replace(new RegExp(commandWords.join("|"), "g"), "")
}
function notify(message, type) {
if (window.ELEMENT && ELEMENT.Message) {
ELEMENT.Message({message, type: type || "info"})
return
}
window.alert(message)
}
function speak(message, onEnd) {
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) {
if (typeof onEnd === "function") {
onEnd()
}
return
}
window.speechSynthesis.cancel()
const utterance = new SpeechSynthesisUtterance(message)
let ended = false
function finish() {
if (ended) {
return
}
ended = true
if (typeof onEnd === "function") {
onEnd()
}
}
utterance.lang = "zh-CN"
utterance.rate = 1
utterance.volume = 1
utterance.onend = function () {
finish()
}
utterance.onerror = function () {
finish()
}
window.speechSynthesis.speak(utterance)
setTimeout(finish, Math.max(2500, message.length * 220))
}
function runAfterRecognitionEnd(callback) {
state.afterRecognitionEnd = callback
if (!state.listening && typeof state.afterRecognitionEnd === "function") {
const next = state.afterRecognitionEnd
state.afterRecognitionEnd = null
setTimeout(next, 150)
}
}
function escapeHtml(text) {
return String(text || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
}
function flattenMenus(menus, parentId, result) {
result = result || []
;(menus || []).forEach(function (menu) {
const id = menu.id
const realParentId = menu.parentId || parentId || ""
if (id) {
state.parentMap[id] = realParentId
}
result.push(menu)
if (menu.children && menu.children.length) {
flattenMenus(menu.children, id, result)
}
})
return result
}
function getStoreMenus() {
try {
return window.store && window.store.state && window.store.state.user && window.store.state.user.menus
} catch (e) {
return null
}
}
function getSessionMenus() {
try {
return JSON.parse(window.sessionStorage.getItem("zhgh_sub_app_menus") || "[]")
} catch (e) {
return []
}
}
function getMenus() {
const cached = state.menus
if (cached && cached.length) {
return $.Deferred().resolve(cached).promise()
}
const storeMenus = getStoreMenus()
if (storeMenus && storeMenus.length) {
state.menus = flattenMenus(storeMenus, "", []).filter(function (menu) {
return menu.href
})
return $.Deferred().resolve(state.menus).promise()
}
return $.get("/platform/sys/user/getLogonUser").then(function (res) {
if (res && res.code === 0 && res.data && res.data.menus) {
state.parentMap = {}
state.menus = flattenMenus(res.data.menus, "", []).filter(function (menu) {
return menu.href
})
return state.menus
}
const sessionMenus = getSessionMenus()
if (sessionMenus && sessionMenus.length) {
state.menus = flattenMenus(sessionMenus, "", []).filter(function (menu) {
return menu.href
})
return state.menus
}
return []
}, function () {
const sessionMenus = getSessionMenus()
if (sessionMenus && sessionMenus.length) {
state.menus = flattenMenus(sessionMenus, "", []).filter(function (menu) {
return menu.href
})
return state.menus
}
return []
})
}
function scoreMenu(menu, rawText) {
const query = normalizeCommand(rawText)
const rawQuery = normalizePlain(rawText)
const name = normalizePlain(menu.name)
const aliasName = normalizePlain(menu.aliasName)
const href = normalizePlain(menu.href)
const permission = normalizePlain(menu.permission)
if (!query || !name) {
return 0
}
if (query === name || rawQuery === name || query === aliasName || rawQuery === aliasName) {
return 100
}
if (name.indexOf(query) > -1 || name.indexOf(rawQuery) > -1 || aliasName.indexOf(query) > -1 || aliasName.indexOf(rawQuery) > -1) {
return 80
}
if (query.indexOf(name) > -1 || rawQuery.indexOf(name) > -1 || (aliasName && (query.indexOf(aliasName) > -1 || rawQuery.indexOf(aliasName) > -1))) {
return 70
}
if (href.indexOf(query) > -1 || permission.indexOf(query) > -1) {
return 45
}
return 0
}
function matchMenus(text, menus) {
return (menus || [])
.map(function (menu) {
return {
menu,
score: scoreMenu(menu, text)
}
})
.filter(function (item) {
return item.score > 0
})
.sort(function (a, b) {
if (b.score !== a.score) {
return b.score - a.score
}
return String(a.menu.name || "").length - String(b.menu.name || "").length
})
.slice(0, 5)
}
function getRootMenuId(menu) {
let id = menu.id
let parentId = state.parentMap[id] || menu.parentId
while (parentId) {
id = parentId
parentId = state.parentMap[id]
}
return id
}
function getCurrentSubAppId() {
try {
const app = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app") || "{}")
return app.id
} catch (e) {
return null
}
}
function openMenu(menu) {
state.pendingMatches = []
state.awaitingChoice = false
if (!menu || !menu.href) {
notify("该菜单没有配置可打开的地址", "warning")
return
}
const targetRootId = getRootMenuId(menu)
const currentSubAppId = getCurrentSubAppId()
const hasSubAppContainer = $("#sub-app-container-main-content-body").length > 0
if (hasSubAppContainer && currentSubAppId && currentSubAppId === targetRootId && typeof commonUtil !== "undefined" && commonUtil.pjaxPush) {
commonUtil.pjaxPush(menu.href)
return
}
window.location.href = menu.href
}
function showCandidateMessage(matches) {
const items = matches
.map(function (item, index) {
const href = item.menu.href ? " <span style=\"color:#909399\">" + escapeHtml(item.menu.href) + "</span>" : ""
return "<p style=\"margin:6px 0\">" + (index + 1) + ". " + escapeHtml(item.menu.name) + href + "</p>"
})
.join("")
if (window.ELEMENT && ELEMENT.MessageBox) {
ELEMENT.MessageBox.alert(items, "找到多个菜单,请说“打开第几个”", {
dangerouslyUseHTMLString: true,
confirmButtonText: "知道了",
type: "info",
callback: function () {}
})
}
}
function getChoiceIndex(text) {
const normalized = normalizePlain(text)
const numberMap = {
"1": 0,
"一": 0,
"壹": 0,
"幺": 0,
"2": 1,
"二": 1,
"两": 1,
"贰": 1,
"3": 2,
"三": 2,
"叁": 2,
"4": 3,
"四": 3,
"肆": 3,
"5": 4,
"五": 4,
"伍": 4
}
const digitMatch = normalized.match(/第?([1-5])个?/)
if (digitMatch) {
return numberMap[digitMatch[1]]
}
const chineseMatch = normalized.match(/第?([一二两三四五壹贰叁肆伍幺])个?/)
if (chineseMatch) {
return numberMap[chineseMatch[1]]
}
return -1
}
function handleChoice(text) {
const index = getChoiceIndex(text)
const matches = state.pendingMatches || []
if (index >= 0 && index < matches.length) {
if (window.ELEMENT && ELEMENT.MessageBox && typeof ELEMENT.MessageBox.close === "function") {
ELEMENT.MessageBox.close()
}
openMenu(matches[index].menu)
return
}
notify("没有识别到有效序号,请说打开第几个", "warning")
speak("没有识别到有效序号,请说打开第几个", function () {
listenForChoice()
})
}
function listenForChoice() {
state.awaitingChoice = true
setTimeout(function () {
start(true)
}, 300)
}
function chooseMenu(matches) {
if (!matches.length) {
notify("未找到可访问的菜单,请换个名称再试", "warning")
return
}
if (matches.length === 1 || matches[0].score > matches[1].score) {
openMenu(matches[0].menu)
return
}
state.pendingMatches = matches
state.awaitingChoice = true
showCandidateMessage(matches)
const prompt = matches
.map(function (item, index) {
return "第" + (index + 1) + "个," + item.menu.name
})
.join("。")
speak("找到多个菜单,您需要打开第几个。" + prompt, function () {
listenForChoice()
})
}
function updateButton(listening) {
const $button = $("#voice-menu-btn")
$button.toggleClass("is-listening", listening)
$button.attr("title", listening ? "正在听,请说出菜单名称" : "语音打开菜单")
$button.find(".voice-menu-text").text(listening ? "聆听中" : "语音")
}
function start(choiceMode) {
choiceMode = choiceMode || state.awaitingChoice
if (!SpeechRecognition) {
notify("当前浏览器不支持语音识别,请使用 Chrome 或 Edge", "warning")
return
}
if (state.listening) {
state.recognition.stop()
return
}
const recognition = new SpeechRecognition()
recognition.lang = "zh-CN"
recognition.interimResults = false
recognition.continuous = false
recognition.maxAlternatives = 1
recognition.onstart = function () {
state.listening = true
updateButton(true)
notify(choiceMode ? "请说打开第几个" : "请说出要打开的菜单名称", "info")
}
recognition.onend = function () {
state.listening = false
updateButton(false)
if (typeof state.afterRecognitionEnd === "function") {
const next = state.afterRecognitionEnd
state.afterRecognitionEnd = null
setTimeout(next, 150)
}
}
recognition.onerror = function (event) {
const message = event.error === "not-allowed" ? "麦克风授权失败,请确认 HTTPS 或 localhost 环境并允许浏览器使用麦克风" : "语音识别失败,请再试一次"
notify(message, "warning")
}
recognition.onresult = function (event) {
const text = event.results && event.results[0] && event.results[0][0] && event.results[0][0].transcript
state.lastText = text || ""
if (!state.lastText) {
notify("没有识别到语音内容", "warning")
return
}
if (choiceMode || state.awaitingChoice) {
runAfterRecognitionEnd(function () {
handleChoice(state.lastText)
})
return
}
getMenus().then(function (menus) {
const matches = matchMenus(state.lastText, menus)
runAfterRecognitionEnd(function () {
chooseMenu(matches)
})
})
}
state.recognition = recognition
recognition.start()
}
function init() {
$(document).on("click", "#voice-menu-btn", function () {
start()
})
}
window.voiceMenuNavigator = {
start,
refreshMenus: function () {
state.menus = null
state.parentMap = {}
}
}
$(init)
})(window, jQuery)
@@ -85,6 +85,7 @@
<script src="${base!}/components/plugins/sysDict/DictData.js"></script>
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
<script src="${base!}/assets/platform/js/util/coordinateUtil.js"></script>
<script src="${base!}/assets/platform/js/util/voiceMenuNavigator.js"></script>
<script src="${base!}/assets/platform/js/main.js"></script>
<script src="${base!}/assets/platform/js/mixin/initTableMixins.js"></script>
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
@@ -541,6 +542,33 @@
background-color: rgba(255, 255, 255, 0.15);
}
.v4-voice-menu {
color: #ffffff;
cursor: pointer;
padding: 0 14px;
height: 100%;
border: 0;
background: transparent;
border-radius: 0;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
font: inherit;
outline: none;
}
.v4-voice-menu:hover,
.v4-voice-menu.is-listening {
background-color: rgba(255, 255, 255, 0.15);
color: #ffffff;
}
.v4-voice-menu.is-listening i {
color: #ffdf6b;
}
.v4-avatar {
width: 36px;
height: 36px;
@@ -606,6 +634,10 @@
.v4-nav {
display: none;
}
.voice-menu-text {
display: none;
}
}
/* 页脚样式 */
@@ -677,6 +709,10 @@
</div>
<div class="v4-user-section">
<button type="button" class="v4-voice-menu" id="voice-menu-btn" title="语音打开菜单">
<i class="fa fa-microphone"></i>
<span class="voice-menu-text">语音</span>
</button>
<div class="v4-user-info">
<span class="v4-user-name">${@auth.getPrincipalProperty('username')} (${@auth.getPrincipalProperty('loginname')})</span>
<!-- <i class="fa fa-angle-down"></i> -->
@@ -1,31 +1,31 @@
const entry = {
template: /*language=HTML*/ `
<div class="entry-wrapper">
<!-- 标签页导航 -->
<div class="tabs">
<div class="tab-item" :class="{ active: activeCategory === 'serv' }"
@click="setActiveCategory('serv')">
<i class="fa fa-star"></i> 推荐服务
<div class="entry-section" v-for="section in entrySections" :key="section.key">
<div class="entry-section-header">
<div class="entry-section-title">
<i :class="section.icon"></i>
<span>{{ section.title }}</span>
<em>/Applications</em>
</div>
</div>
<div class="tab-item" :class="{ active: activeCategory === 'app' }"
@click="setActiveCategory('app')">
<i class="fa fa-fire"></i> 推荐应用
</div>
<div class="tab-item" :class="{ active: activeCategory === 'fav' }"
@click="setActiveCategory('fav')">
<i class="fa fa-heart"></i> 我的收藏
</div>
</div>
<!-- 应用块区域 -->
<div class="apps-container">
<div class="app-grid">
<div class="app-item" v-for="(item, index) in currentServices" :key="index"
@click="openService(item)">
<div class="app-icon">
<img :src="item.picIcon" alt="" />
<div class="apps-container">
<div class="app-grid">
<div class="app-item"
v-for="(item, index) in section.list"
:key="item.id || index"
@click="openService(item, section.key)">
<div class="app-icon">
<img :src="item.picIcon" alt="" />
</div>
<div class="app-name">{{ item.name }}</div>
</div>
<div class="app-name">{{ item.name }}</div>
<el-empty class="app-empty"
v-if="!section.list || section.list.length === 0"
description="暂无数据"
:image-size="72">
</el-empty>
</div>
</div>
</div>
@@ -33,7 +33,6 @@ const entry = {
`,
data() {
return {
activeCategory: 'serv',
hasMore: true,
entries: {
serv: [],
@@ -43,8 +42,12 @@ const entry = {
}
},
computed: {
currentServices() {
return this.entries[this.activeCategory] || [];
entrySections() {
return [
{ key: 'serv', title: '推荐服务', icon: 'fa fa-star', list: this.entries.serv },
{ key: 'app', title: '推荐应用', icon: 'fa fa-fire', list: this.entries.app },
{ key: 'fav', title: '我的收藏', icon: 'fa fa-heart', list: this.entries.fav }
];
}
},
created() {
@@ -53,10 +56,6 @@ const entry = {
this.getFavorite()
},
methods: {
setActiveCategory(category) {
this.activeCategory = category;
},
// 查询推荐服务
async getRecommendService() {
this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => {
@@ -84,35 +83,37 @@ const entry = {
})
},
// 点击服务
openService(service) {
if(this.activeCategory === 'serv'){
if(!service.href) this.$message.error('无效的链接地址')
// 点击服务或应用
openService(service, category) {
if (category === 'serv') {
if (!service.href) {
this.$message.error('无效的链接地址')
return
}
window.open(service.href)
return
}
if(this.activeCategory === 'app'){
// 储存到缓存
if (category === 'app') {
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(service))
// 新标签页打开
window.open("/platform/v4/subApp?appId=" + service.id, "_blank")
return
}
if(this.activeCategory === 'fav'){
if(!service.href) this.$message.error('无效的链接地址')
window.open(service.href)
return
if(!service.parentId){
// 储存到缓存
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(app))
// 新标签页打开
window.open("/platform/v4/subApp?appId=" + app.id, "_blank")
if (category === 'fav') {
if (service.href) {
window.open(service.href)
return
}
}
if (!service.parentId) {
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(service))
window.open("/platform/v4/subApp?appId=" + service.id, "_blank")
return
}
this.$message.error('无效的链接地址')
}
}
},
style: /*language=CSS*/ `
@@ -121,37 +122,36 @@ const entry = {
box-sizing: border-box;
}
.tabs {
margin-top: 5px;
display: flex;
gap: 20px;
border-bottom: 2px solid #e8e8e8;
padding-bottom: 10px;
.entry-section + .entry-section {
margin-top: 22px;
}
.tab-item {
.entry-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.entry-section-title {
display: inline-flex;
align-items: baseline;
gap: 6px;
color: #19324d;
font-size: 18px;
font-weight: 700;
}
.entry-section-title i {
color: #409eff;
font-size: 16px;
}
.entry-section-title em {
color: #b5c0d6;
font-size: 14px;
color: #666;
cursor: pointer;
padding-bottom: 8px;
transition: color 0.2s ease;
}
.tab-item:hover {
color: #1890ff;
}
.tab-item.active {
color: #1890ff;
font-weight: bold;
border-bottom: 2px solid #1890ff;
}
.tab-item i {
font-size: 14px;
font-style: normal;
font-weight: 500;
}
.apps-container {
@@ -162,19 +162,17 @@ const entry = {
.app-grid {
display: grid;
/* 改为自适应列数:每格最小 80px,自动换行 */
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
justify-items: center; /* 子项内容居中(图标和文字) */
justify-content: start; /* 整体网格靠左对齐,避免居中 */
gap: 4px;
padding: 12px 4px;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
justify-items: center;
justify-content: start;
gap: 12px 18px;
min-height: 116px;
padding: 18px 20px;
background: #fff;
border: 1px solid #edf1f7;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
overflow-x: hidden;
max-height: calc(350px - 74px);
overflow-y: auto;
}
.app-item {
@@ -187,6 +185,7 @@ const entry = {
width: 100%;
max-width: 100%;
padding: 6px;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
@@ -208,6 +207,7 @@ const entry = {
}
.app-name {
min-height: 34px;
font-size: 12px;
color: #333;
line-height: 1.4;
@@ -215,31 +215,26 @@ const entry = {
text-align: center;
}
/* 响应式设计 */
.app-empty {
grid-column: 1 / -1;
width: 100%;
}
@media (max-width: 768px) {
.tabs {
flex-wrap: wrap;
gap: 10px;
}
.tab-item {
padding: 8px 0;
font-size: 12px;
}
.app-grid {
grid-template-columns: repeat(2, 1fr);
gap: 15px;
grid-template-columns: repeat(3, minmax(72px, 1fr));
gap: 12px;
padding: 14px 10px;
}
.app-item {
padding: 12px;
padding: 8px 4px;
}
}
@media (max-width: 480px) {
.app-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, minmax(72px, 1fr));
gap: 12px;
}
}
@@ -11,7 +11,7 @@ layout("/layouts/v4/baseLayout.html"){
.section-banner .banner-img {
width: 100%;
height: 100%;
height: auto;
}
.section-banner .banner-img img {
@@ -21,6 +21,11 @@ layout("/layouts/v4/baseLayout.html"){
object-fit: cover;
}
/* 临时屏蔽首页背景图,保留原图片节点便于恢复 */
.section-banner .banner-img > img {
display: none;
}
.section-wrapper {
width: 100%;
}
@@ -29,7 +34,7 @@ layout("/layouts/v4/baseLayout.html"){
width: 80%;
margin: 20px auto;
display: grid;
grid-template-columns: 2.2fr 0.8fr;
grid-template-columns: 1fr;
gap: 24px;
align-items: stretch;
row-gap: 24px;
@@ -39,16 +44,16 @@ layout("/layouts/v4/baseLayout.html"){
display: flex;
/*align-items: center;*/
min-width: 320px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
padding: 16px;
background: transparent;
border-radius: 0;
box-shadow: none;
padding: 0;
transition: transform 0.2s, box-shadow 0.2s;
}
.entry-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
transform: none;
box-shadow: none;
}
.act-card {
@@ -77,10 +82,6 @@ layout("/layouts/v4/baseLayout.html"){
<user></user>
<div class="home-grid">
<div class="act-card">
<act></act>
</div>
<div class="entry-card">
<entry></entry>
</div>
@@ -12,7 +12,7 @@ const stats = {
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<div class="stat-icon">
<div class="stat-icon stat-icon-hidden">
<i class="fa fa-clock-o"></i>
</div>
<div class="stat-content">
@@ -28,7 +28,7 @@ const stats = {
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<div class="stat-icon">
<div class="stat-icon stat-icon-hidden">
<i class="fa fa-check-circle"></i>
</div>
<div class="stat-content">
@@ -44,7 +44,7 @@ const stats = {
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<div class="stat-icon">
<div class="stat-icon stat-icon-hidden">
<i class="fa fa-bell"></i>
</div>
<div class="stat-content">
@@ -60,7 +60,7 @@ const stats = {
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<div class="stat-icon">
<div class="stat-icon stat-icon-hidden">
<i class="fa fa-file-text"></i>
</div>
<div class="stat-content">
@@ -122,7 +122,7 @@ const stats = {
display: flex;
justify-content: space-between;
align-items: center;
margin: -130px auto 0;
margin: 20px auto 0;
/*border: 1px solid #e8e8e8;*/
}
@@ -131,7 +131,8 @@ const stats = {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
width: 32%;
margin-left: 20px;
min-width: 360px;
margin-left: auto;
}
.stat-item {
@@ -162,6 +163,11 @@ const stats = {
justify-content: center;
}
/* 临时屏蔽待办、已办、消息、发起四个统计图标 */
.stat-icon-hidden {
display: none !important;
}
.stat-item.pending .stat-icon,
.stat-item.completed .stat-icon,
.stat-item.messages .stat-icon,
@@ -185,7 +191,7 @@ const stats = {
.stat-content {
display: flex;
flex-direction: column;
margin-left: 10px;
margin-left: 0;
}
.stat-label {
@@ -209,5 +215,10 @@ const stats = {
height: 190px !important;
border-radius: 30px;
}
/* 临时屏蔽“欢迎来到职工之家”图片,保留原节点便于恢复 */
.stats-family {
display: none;
}
`
};
@@ -68,10 +68,10 @@ const user = {
<div class="header">
<h3>待办中心</h3>
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
<el-tab-pane label="待办事宜" name="todo"></el-tab-pane>
<el-tab-pane label="已办事宜" name="done"></el-tab-pane>
<el-tab-pane label="我的发起" name="started"></el-tab-pane>
<el-tab-pane label="办结事务" name="completed"></el-tab-pane>
<el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane>
<el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane>
<el-tab-pane :label="tabLabel('我的发起', taskStats.startedCount)" name="started"></el-tab-pane>
<el-tab-pane :label="tabLabel('办结事务', taskStats.completedCount)" name="completed"></el-tab-pane>
</el-tabs>
</div>
@@ -124,6 +124,12 @@ const user = {
pageSize: 5,
totalCount: 0
},
taskStats: {
todoCount: 0,
doneCount: 0,
startedCount: 0,
completedCount: 0
},
// 🔆 天气相关数据
weatherData: null,
weatherError: false,
@@ -228,6 +234,19 @@ const user = {
}
},
// 获取任务列表
tabLabel(title, count) {
return title + "" + (count || 0) + ""
},
async getStatistics() {
try {
const res = await $.post("/flow/todoCenter/statistics")
if (res.code === 0) {
this.taskStats = Object.assign({}, this.taskStats, res.data)
}
} catch (error) {
console.error("获取待办统计失败:", error)
}
},
async getTasks() {
this.loading = true
try {
@@ -245,6 +264,7 @@ const user = {
},
pageData() {
this.getTasks()
this.getStatistics()
},
handleTabClick(tab) {
this.pageData()
@@ -0,0 +1,9 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">学习活动管理模块待完善</el-card>
</div>
<!--#
}
#-->
@@ -0,0 +1,883 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.learning-outline-layout {
height: calc(100vh - 235px);
}
.learning-outline-tree {
height: 100%;
overflow: auto;
}
.outline-context-menu {
position: fixed;
z-index: 3000;
padding: 5px 0;
margin: 0;
list-style: none;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
}
.outline-context-menu li {
min-width: 120px;
padding: 7px 18px;
cursor: pointer;
}
.outline-context-menu li:hover {
background: #f2f6fc;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="课程名称">
<el-select
v-model="currentCourseId"
clearable
filterable
remote
:remote-method="courseNameRemoteSearch"
placeholder="请选择或搜索课程名称"
style="width: 100%"
@change="courseChange"
@clear="courseClear">
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="课程类型">
<el-select v-model="courseQuery.courseTypeId" clearable filterable placeholder="请选择课程类型" style="width: 100%">
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="授课讲师">
<el-input v-model="courseQuery.lecturerName" clearable placeholder="请输入授课讲师" style="width: 100%" @keyup.enter.native="loadCourses"></el-input>
</search-item>
<search-item label="课程状态">
<el-select v-model="courseQuery.status" clearable placeholder="请选择课程状态" style="width: 100%">
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="loadCourses">搜索</el-button>
<el-button size="medium" @click="resetCourseQuery">重置</el-button>
</div>
</search>
</el-card>
<el-row :gutter="10" class="mt10 learning-outline-layout">
<el-col :span="6" style="height: 100%">
<el-card shadow="never" style="height: 100%">
<table-tool label="课程大纲">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openNodeForm('chapter')">新增章</el-button>
</table-tool>
<div class="learning-outline-tree">
<el-tree
ref="outlineTree"
:data="outlineTreeData"
node-key="id"
default-expand-all
highlight-current
:expand-on-click-node="false"
:props="{children:'children', label:'title'}"
@node-click="nodeClick"
@node-contextmenu="openContextMenu">
<span slot-scope="{ node, data }">
<i :class="data.nodeType === 'course' ? 'el-icon-collection' : (data.nodeType === 'chapter' ? 'el-icon-folder' : 'el-icon-document')"></i>
<span>{{data.title}}</span>
<el-tag v-if="data.status === 'disabled'" size="mini" type="info" style="margin-left: 6px">禁用</el-tag>
<el-tag v-if="data.required" size="mini" type="warning" style="margin-left: 6px">必学</el-tag>
</span>
</el-tree>
<el-empty v-if="treeData.length === 0" description="暂无课程大纲"></el-empty>
</div>
</el-card>
</el-col>
<el-col :span="18" style="height: 100%">
<el-card shadow="never" style="height: 100%; overflow: auto">
<el-empty v-if="!selectedNode.id" description="请选择左侧章/节节点"></el-empty>
<el-tabs v-else v-model="activeTab">
<el-tab-pane label="基础信息" name="basic">
<el-form :model="nodeForm" ref="nodeForm" label-width="110px" :rules="nodeRules">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="节点类型">
<el-tag :type="nodeForm.nodeType === 'chapter' ? 'primary' : 'success'">{{nodeForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序值" prop="sortOrder">
<el-input-number v-model="nodeForm.sortOrder" :controls="false" :min="1" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="标题" prop="title">
<el-input v-model="nodeForm.title" maxlength="100" placeholder="请输入章/节标题"></el-input>
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
<el-input v-model="nodeForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="nodeForm.description" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入章/节简介"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveNodeBasic">保存基础信息</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="学习资料" name="resource">
<table-tool label="学习资料">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openResourceForm()">新增资料</el-button>
</table-tool>
<el-table :data="resourceData" border @sort-change="resourcePageOrder">
<el-table-column label="序号" type="index" width="70" :index="resourceIndex"></el-table-column>
<el-table-column label="资料标题" prop="resourceTitle" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column label="资料类型" prop="resourceType" sortable="custom" width="110">
<template slot-scope="{row}">{{getResourceTypeName(row.resourceType)}}</template>
</el-table-column>
<el-table-column label="文件格式" prop="fileExt" sortable="custom" width="110"></el-table-column>
<el-table-column label="学习时长(分钟)" prop="durationSeconds" sortable="custom" width="140">
<template slot-scope="{row}">{{formatDurationMinutes(row.durationSeconds)}}</template>
</el-table-column>
<el-table-column label="排序" prop="sortOrder" sortable="custom" width="90"></el-table-column>
<el-table-column label="必学" prop="required" sortable="custom" width="90">
<template slot-scope="{row}">
<el-tag :type="row.required ? 'warning' : 'info'" size="mini">{{row.required ? '是' : '否'}}</el-tag>
</template>
</el-table-column>
<el-table-column label="预览" prop="allowPreview" sortable="custom" width="90">
<template slot-scope="{row}">{{row.allowPreview ? '允许' : '不允许'}}</template>
</el-table-column>
<el-table-column label="下载" prop="allowDownload" sortable="custom" width="90">
<template slot-scope="{row}">{{row.allowDownload ? '允许' : '不允许'}}</template>
</el-table-column>
<el-table-column label="状态" prop="status" sortable="custom" width="90">
<template slot-scope="{row}">
<el-tag :type="row.status === 'enabled' ? 'success' : 'info'" size="mini">{{row.status === 'enabled' ? '启用' : '禁用'}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openResourceForm(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="deleteResource(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="mt20"
@size-change="resourceSizeChange"
@current-change="resourceNumberChange"
:current-page="resourcePageForm.pageNumber"
:page-sizes="[5,10,20,30,50]"
:page-size="resourcePageForm.pageSize"
layout="total, sizes, prev, pager, next"
:total="resourcePageForm.totalCount">
</el-pagination>
</el-tab-pane>
<el-tab-pane label="完成规则" name="rule">
<el-form :model="ruleForm" label-width="150px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否必学">
<el-switch v-model="ruleForm.required"></el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学习方式">
<el-select v-model="ruleForm.studyMode" style="width: 100%">
<el-option v-for="item in studyModeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="完成规则">
<el-select v-model="ruleForm.completionRule" style="width: 100%">
<el-option v-for="item in completionRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="完成比例">
<el-input-number v-model="ruleForm.completePercent" :min="0" :max="100" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最少学习时长(分钟)">
<el-input-number v-model="ruleForm.minStudyMinutes" :min="0" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="解锁规则">
<el-select v-model="ruleForm.unlockRule" style="width: 100%">
<el-option v-for="item in unlockRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="允许跳过"><el-switch v-model="ruleForm.allowSkip"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="允许拖动"><el-switch v-model="ruleForm.allowDrag"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="暂停计时"><el-switch v-model="ruleForm.pauseCountTime"></el-switch></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="隐藏计时"><el-switch v-model="ruleForm.hiddenCountTime"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="无操作计时"><el-switch v-model="ruleForm.inactiveCountTime"></el-switch></el-form-item></el-col>
</el-row>
<el-form-item>
<el-button type="primary" @click="saveRule">保存完成规则</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="状态设置" name="status">
<el-form :model="nodeForm" label-width="110px">
<el-form-item label="节点状态">
<el-radio-group v-model="nodeForm.status">
<el-radio label="enabled">启用</el-radio>
<el-radio label="disabled">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否必学">
<el-switch v-model="nodeForm.required"></el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveNodeBasic">保存状态</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
<ul v-show="contextVisible" class="outline-context-menu" :style="{left: contextLeft + 'px', top: contextTop + 'px'}">
<li @click="openNodeForm('chapter')">新增章</li>
<li v-if="contextNode.nodeType === 'chapter'" @click="openNodeForm('section', contextNode)">新增节</li>
<li v-if="contextNode.nodeType !== 'course'" @click="openNodeForm(contextNode.nodeType, contextNode)">编辑</li>
<li v-if="contextNode.nodeType !== 'course'" @click="deleteNode(contextNode)">删除</li>
<li v-if="contextNode.nodeType !== 'course'" @click="toggleNode(contextNode)">{{contextNode.status === 'enabled' ? '禁用' : '启用'}}</li>
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'up')">上移</li>
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'down')">下移</li>
<li v-if="contextNode.nodeType !== 'course'" @click="openResourceForm(null, contextNode)">上传资料</li>
<li v-if="contextNode.nodeType !== 'course'" @click="setRequired(contextNode)">设置必学</li>
</ul>
<el-dialog :title="nodeDialogTitle" :visible.sync="nodeDialogVisible" :close-on-click-modal="false" width="45%">
<el-form :model="nodeDialogForm" ref="nodeDialogForm" label-width="100px" :rules="nodeRules">
<el-form-item label="节点类型">
<el-tag>{{nodeDialogForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
</el-form-item>
<el-form-item label="上级节点">
<el-input v-model="nodeDialogForm.parentName" disabled placeholder="上级节点"></el-input>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="nodeDialogForm.title" maxlength="100" placeholder="请输入标题"></el-input>
</el-form-item>
<el-form-item label="副标题">
<el-input v-model="nodeDialogForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
</el-form-item>
<el-form-item label="简介">
<el-input v-model="nodeDialogForm.description" type="textarea" :rows="4" maxlength="2000" show-word-limit></el-input>
</el-form-item>
<el-form-item label="排序值">
<el-input-number v-model="nodeDialogForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="是否必学">
<el-switch v-model="nodeDialogForm.required"></el-switch>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="nodeDialogForm.status">
<el-radio label="enabled">启用</el-radio>
<el-radio label="disabled">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="nodeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitNodeDialog">确定</el-button>
</span>
</el-dialog>
<el-dialog :title="resourceForm.id ? '编辑学习资料' : '新增学习资料'" :visible.sync="resourceDialogVisible" :close-on-click-modal="false" width="55%">
<el-form :model="resourceForm" ref="resourceForm" label-width="120px" :rules="resourceRules">
<el-form-item label="所属章节">
<el-input v-model="resourceForm.outlineName" disabled placeholder="所属章节"></el-input>
</el-form-item>
<el-form-item label="资料标题" prop="resourceTitle">
<el-input v-model="resourceForm.resourceTitle" maxlength="100" placeholder="请输入资料标题"></el-input>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="资料类型" prop="resourceType">
<el-select v-model="resourceForm.resourceType" placeholder="请选择资料类型" style="width: 100%" @change="resourceTypeChange">
<el-option v-for="item in resourceTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序值" prop="sortOrder">
<el-input-number v-model="resourceForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学习时长(分钟)">
<el-input-number v-model="resourceForm.durationMinutes" :min="0" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="resourceForm.status">
<el-radio label="enabled">启用</el-radio>
<el-radio label="disabled">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="是否必学"><el-switch v-model="resourceForm.required"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="允许预览"><el-switch v-model="resourceForm.allowPreview"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="允许下载"><el-switch v-model="resourceForm.allowDownload"></el-switch></el-form-item></el-col>
</el-row>
<el-form-item label="附件" prop="fileData">
<file-upload
:value.sync="resourceForm.fileData"
:upload_number="1"
upload_mode="drag"
upload_result_category="array"
complete_result
:accept="fileAccept">
</file-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="resourceDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitResource">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
courseQuery: {},
courseOptions: [],
courseTypeOptions: [],
currentCourseId: "",
treeData: [],
selectedNode: {},
activeTab: "basic",
nodeForm: {},
nodeDialogVisible: false,
nodeDialogTitle: "",
nodeDialogForm: {},
nodeRules: {
title: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
},
contextVisible: false,
contextLeft: 0,
contextTop: 0,
contextNode: {},
resourceData: [],
resourcePageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
resourceDialogVisible: false,
resourceForm: {},
resourceRules: {
resourceTitle: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
resourceType: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
fileData: [{ required: true, message: "请上传附件", trigger: ["blur", "change"] }]
},
fileAccept: "",
ruleForm: {},
statusOptions: [
{ name: "草稿", code: "draft" },
{ name: "待发布", code: "pending" },
{ name: "已发布", code: "published" },
{ name: "已下架", code: "offline" }
],
resourceTypeOptions: [
{ name: "视频", code: "video", accept: ".mp4,.mov" },
{ name: "音频", code: "audio", accept: ".mp3,.wav,.aac" },
{ name: "PDF", code: "pdf", accept: ".pdf" },
{ name: "PPT课件", code: "ppt", accept: ".ppt,.pptx" },
{ name: "Word文档", code: "word", accept: ".doc,.docx" },
{ name: "图片", code: "image", accept: ".jpg,.jpeg,.png" },
{ name: "其他", code: "other", accept: "" }
],
studyModeOptions: [
{ name: "视频", code: "video" },
{ name: "音频", code: "audio" },
{ name: "文档", code: "document" },
{ name: "图片", code: "image" },
{ name: "混合资料", code: "mixed" }
],
completionRuleOptions: [
{ name: "完成所有必学资料", code: "all_required_resource" },
{ name: "按资料学习进度", code: "resource_progress" },
{ name: "达到最少学习时长", code: "min_study_time" },
{ name: "管理员手动确认", code: "manual" }
],
unlockRuleOptions: [
{ name: "自由学习", code: "free" },
{ name: "顺序解锁", code: "sequential" }
]
}
},
computed: {
currentCourse() {
return this.courseOptions.find(v => v.id === this.currentCourseId)
},
outlineTreeData() {
if (!this.currentCourseId || !this.currentCourse) {
return []
}
return [{
id: "course-" + this.currentCourseId,
courseId: this.currentCourseId,
title: this.currentCourse.courseName,
nodeType: "course",
children: this.treeData
}]
}
},
methods: {
resetCourseQuery() {
this.courseQuery = {}
this.currentCourseId = ""
this.loadCourses()
},
getQueryParam(name) {
return new URLSearchParams(window.location.search).get(name) || ""
},
async loadCourseTypes() {
const resp = await this.$axios.post(loc() + "/courseTypes")
if (resp.code === 0) {
this.courseTypeOptions = resp.data
}
},
async loadCourses(keepCurrent = false) {
const resp = await this.$axios.post(loc() + "/courses", this.courseQuery)
if (resp.code === 0) {
this.courseOptions = resp.data
if (!keepCurrent && (!this.currentCourseId || !this.courseOptions.some(v => v.id === this.currentCourseId))) {
this.currentCourseId = this.courseOptions.length ? this.courseOptions[0].id : ""
}
await this.loadTree()
}
},
courseNameRemoteSearch(query) {
this.courseQuery.courseName = query
this.loadCourses(true)
},
courseChange() {
this.selectedNode = {}
this.loadTree()
},
courseClear() {
this.currentCourseId = ""
this.selectedNode = {}
this.treeData = []
},
async loadTree() {
if (!this.currentCourseId) {
this.treeData = []
this.selectedNode = {}
return
}
const resp = await this.$axios.post(loc() + "/tree", { courseId: this.currentCourseId })
if (resp.code === 0) {
this.treeData = resp.data
let nodeToSelect = null
if (this.selectedNode.id) {
nodeToSelect = this.findNode(this.treeData, this.selectedNode.id)
}
if (!nodeToSelect) {
nodeToSelect = this.findFirstOutlineNode(this.treeData)
}
if (nodeToSelect) {
this.nodeClick(nodeToSelect)
this.$nextTick(() => {
if (this.$refs.outlineTree) {
this.$refs.outlineTree.setCurrentKey(nodeToSelect.id)
}
})
} else {
this.selectedNode = {}
this.nodeForm = {}
this.resourceData = []
}
}
},
findFirstOutlineNode(nodes) {
if (!nodes || !nodes.length) return null
for (const node of nodes) {
if (node.nodeType !== "course") return node
const child = this.findFirstOutlineNode(node.children || [])
if (child) return child
}
return null
},
findNode(nodes, id) {
for (const node of nodes) {
if (node.id === id) return node
const child = this.findNode(node.children || [], id)
if (child) return child
}
return null
},
getParentName(type, node, course, isEdit) {
if (type === "chapter") {
return course ? course.courseName : ""
}
if (!isEdit) {
return node ? node.title : ""
}
const parent = node && node.parentId ? this.findNode(this.treeData, node.parentId) : null
return parent ? parent.title : (course ? course.courseName : "")
},
nodeClick(data) {
if (data.nodeType === "course") {
this.selectedNode = {}
this.nodeForm = {}
this.resourceData = []
return
}
this.selectedNode = data
this.nodeForm = Object.assign({}, data)
this.activeTab = "basic"
this.loadResources()
this.loadRule()
},
openContextMenu(event, data) {
event.preventDefault()
this.contextNode = data
this.contextVisible = true
this.contextLeft = event.clientX
this.contextTop = event.clientY
},
openNodeForm(type, node = null) {
if (!this.currentCourseId) {
this.$message.warning("请先选择课程")
return
}
const course = this.courseOptions.find(v => v.id === this.currentCourseId)
if (node && node.id && node.nodeType === type) {
this.nodeDialogTitle = "编辑" + (type === "chapter" ? "章" : "节")
this.nodeDialogForm = Object.assign({}, node, {
parentName: this.getParentName(type, node, course, true)
})
} else {
this.nodeDialogTitle = "新增" + (type === "chapter" ? "章" : "节")
this.nodeDialogForm = {
courseId: this.currentCourseId,
courseName: course ? course.courseName : "",
parentId: type === "section" && node ? node.id : "",
parentName: this.getParentName(type, node, course, false),
nodeType: type,
title: "",
subtitle: "",
description: "",
sortOrder: 1,
required: false,
status: "enabled"
}
}
this.nodeDialogVisible = true
this.$nextTick(() => this.$refs.nodeDialogForm && this.$refs.nodeDialogForm.clearValidate())
},
submitNodeDialog() {
this.$refs.nodeDialogForm.validate(async valid => {
if (!valid) return
const resp = await this.$axios.post(loc() + "/saveNode", this.nodeDialogForm)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.nodeDialogVisible = false
await this.loadTree()
} else {
this.$message.warning(resp.msg)
}
})
},
saveNodeBasic() {
this.$refs.nodeForm.validate(async valid => {
if (!valid) return
const resp = await this.$axios.post(loc() + "/saveNode", this.nodeForm)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.selectedNode = Object.assign({}, this.nodeForm)
await this.loadTree()
await this.loadRule()
} else {
this.$message.warning(resp.msg)
}
})
},
deleteNode(node) {
this.$confirm("确定要删除该节点吗?若下级有内容将不允许删除。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post(loc() + "/deleteNode", { id: node.id })
if (resp.code === 0) {
this.$message.success(resp.msg)
this.selectedNode = {}
await this.loadTree()
} else {
this.$message.warning(resp.msg)
}
})
},
async toggleNode(node) {
const status = node.status === "enabled" ? "disabled" : "enabled"
const resp = await this.$axios.post(loc() + "/toggleNode", { id: node.id, status })
if (resp.code === 0) {
this.$message.success(resp.msg)
await this.loadTree()
}
},
async moveNode(node, direction) {
const resp = await this.$axios.post(loc() + "/moveNode", { id: node.id, direction })
if (resp.code === 0) {
await this.loadTree()
}
},
async setRequired(node) {
const next = !node.required
const data = Object.assign({}, node, { required: next })
const resp = await this.$axios.post(loc() + "/saveNode", data)
if (resp.code === 0) {
this.$message.success(resp.msg)
await this.loadTree()
if (this.selectedNode.id === node.id) {
this.nodeClick(Object.assign({}, node, { required: next }))
}
}
},
async loadResources() {
if (!this.selectedNode.id) return
const resp = await this.$axios.post(loc() + "/resourcePage", Object.assign({}, this.resourcePageForm, { outlineId: this.selectedNode.id }))
if (resp.code === 0) {
this.resourceData = resp.data.list
this.resourcePageForm.totalCount = resp.data.totalCount
}
},
resourcePageOrder(column) {
this.resourcePageForm.pageOrderName = column.prop
this.resourcePageForm.pageOrderBy = column.order
this.loadResources()
},
resourceSizeChange(val) {
this.resourcePageForm.pageSize = val
this.loadResources()
},
resourceNumberChange(val) {
this.resourcePageForm.pageNumber = val
this.loadResources()
},
resourceIndex(index) {
return index + (this.resourcePageForm.pageNumber - 1) * this.resourcePageForm.pageSize + 1
},
openResourceForm(row = null, node = null) {
const target = node || this.selectedNode
if (!target.id) {
this.$message.warning("请先选择章/节节点")
return
}
if (row) {
this.resourceForm = Object.assign({}, row, {
outlineName: target.title,
fileData: this.parseFiles(row.fileData),
durationMinutes: this.formatDurationMinutes(row.durationSeconds)
})
} else {
this.resourceForm = {
courseId: target.courseId,
outlineId: target.id,
outlineName: target.title,
resourceTitle: "",
resourceType: "video",
fileExt: "",
fileData: [],
durationSeconds: 0,
durationMinutes: 0,
sortOrder: 1,
required: false,
allowPreview: true,
allowDownload: false,
status: "enabled"
}
}
this.resourceTypeChange(this.resourceForm.resourceType)
this.resourceDialogVisible = true
this.$nextTick(() => this.$refs.resourceForm && this.$refs.resourceForm.clearValidate())
},
resourceTypeChange(val) {
const type = this.resourceTypeOptions.find(v => v.code === val)
this.fileAccept = type ? type.accept : ""
this.autoFillMediaDuration()
},
submitResource() {
this.$refs.resourceForm.validate(async valid => {
if (!valid) return
const data = Object.assign({}, this.resourceForm)
data.durationSeconds = Math.round((data.durationMinutes || 0) * 60)
data.fileExt = this.getFileExt(data.fileData)
if (!this.validResourceExt(data.resourceType, data.fileExt)) {
this.$message.warning("当前资料类型不支持上传 ." + data.fileExt + " 格式文件")
return
}
delete data.outlineName
delete data.durationMinutes
data.fileData = JSON.stringify(data.fileData || [])
const resp = await this.$axios.post(loc() + "/saveResource", data)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.resourceDialogVisible = false
this.loadResources()
} else {
this.$message.warning(resp.msg)
}
})
},
deleteResource(row) {
this.$confirm("确定要删除该学习资料吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post(loc() + "/deleteResource", { id: row.id })
if (resp.code === 0) {
this.$message.success(resp.msg)
this.loadResources()
}
})
},
async loadRule() {
if (!this.selectedNode.id) return
const resp = await this.$axios.post(loc() + "/getRule", {
courseId: this.selectedNode.courseId,
targetType: "outline",
targetId: this.selectedNode.id
})
if (resp.code === 0) {
this.ruleForm = Object.assign({}, resp.data, {
minStudyMinutes: this.formatDurationMinutes(resp.data.minStudySeconds)
})
}
},
async saveRule() {
const data = Object.assign({}, this.ruleForm, {
minStudySeconds: Math.round((this.ruleForm.minStudyMinutes || 0) * 60)
})
delete data.minStudyMinutes
const resp = await this.$axios.post(loc() + "/saveRule", data)
if (resp.code === 0) {
this.$message.success(resp.msg)
await this.loadTree()
await this.loadRule()
} else {
this.$message.warning(resp.msg)
}
},
parseFiles(value) {
if (!value) return []
if (Array.isArray(value)) return value
try {
return JSON.parse(value)
} catch (e) {
return []
}
},
getFileUrl(files) {
const file = Array.isArray(files) && files.length ? files[0] : null
if (!file) return ""
return file.url || file.response?.data || file.data || ""
},
getFileExt(files) {
const file = Array.isArray(files) && files.length ? files[0] : null
if (!file) return ""
const name = file.name || file.url || file.response?.data || ""
const index = name.lastIndexOf(".")
return index > -1 ? name.substring(index + 1).toLowerCase() : ""
},
formatDurationMinutes(durationSeconds) {
const seconds = Number(durationSeconds || 0)
return seconds > 0 ? Math.ceil(seconds / 60) : 0
},
autoFillMediaDuration() {
if (!["video", "audio"].includes(this.resourceForm.resourceType)) return
const url = this.getFileUrl(this.resourceForm.fileData)
if (!url) return
const media = document.createElement(this.resourceForm.resourceType === "video" ? "video" : "audio")
media.preload = "metadata"
media.onloadedmetadata = () => {
window.URL.revokeObjectURL(media.src)
if (isFinite(media.duration) && media.duration > 0) {
this.$set(this.resourceForm, "durationMinutes", Math.ceil(media.duration / 60))
}
}
media.onerror = () => {
window.URL.revokeObjectURL(media.src)
}
media.src = url
},
validResourceExt(resourceType, fileExt) {
const item = this.resourceTypeOptions.find(v => v.code === resourceType)
if (!item || !item.accept) return true
return item.accept.split(",").map(v => v.replace(".", "").toLowerCase()).includes(fileExt)
},
getResourceTypeName(code) {
const item = this.resourceTypeOptions.find(v => v.code === code)
return item ? item.name : code
}
},
watch: {
"resourceForm.fileData": {
handler() {
this.autoFillMediaDuration()
},
deep: true
}
},
mounted() {
document.addEventListener("click", () => this.contextVisible = false)
},
async created() {
await this.loadCourseTypes()
const courseId = this.getQueryParam("courseId")
if (courseId) {
this.currentCourseId = courseId
this.courseQuery.courseId = courseId
await this.loadCourses(true)
this.$delete(this.courseQuery, "courseId")
} else {
await this.loadCourses()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,12 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<div slot="header">课程介绍</div>
<el-empty description="课程介绍页面后续设计"></el-empty>
</el-card>
</div>
<!--#
}
#-->
@@ -0,0 +1,350 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.course-display {
padding: 8px 10px 22px;
background: #f4f5f7;
min-height: calc(100vh - 88px);
}
.course-filter {
background: #fff;
border: 1px solid #e5e8ef;
margin-bottom: 28px;
}
.filter-row {
display: flex;
min-height: 70px;
border-bottom: 1px solid #e8ebf0;
}
.filter-row:last-child {
border-bottom: 0;
}
.filter-label {
width: 132px;
padding: 20px 16px;
background: #eef5ff;
color: #1f2d3d;
font-weight: 600;
text-align: center;
}
.filter-options {
flex: 1;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 10px 18px;
padding: 16px 18px;
}
.filter-chip {
min-width: 76px;
height: 32px;
padding: 0 18px;
border: 0;
border-radius: 9px;
background: transparent;
color: #526070;
cursor: pointer;
line-height: 32px;
text-align: center;
}
.filter-chip.active {
background: #1e63e9;
color: #fff;
font-weight: 600;
}
.course-result-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.course-count {
color: #1f2d3d;
}
.course-count span {
color: #2468f2;
padding: 0 4px;
}
.course-search {
width: 310px;
}
.course-list {
background: #fff;
border-top: 1px solid #dfe4ec;
}
.course-item {
display: flex;
gap: 20px;
padding: 18px 0;
border-bottom: 1px solid #dfe4ec;
}
.course-cover {
position: relative;
width: 270px;
height: 150px;
flex: 0 0 270px;
background: #e9eef5;
border-radius: 4px;
overflow: hidden;
}
.course-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.course-cover-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #8a97a8;
background: linear-gradient(135deg, #eaf0f8, #d8e3f2);
font-weight: 600;
}
.course-cover-tag {
position: absolute;
right: 0;
bottom: 12px;
min-width: 70px;
height: 24px;
padding: 0 10px;
background: #f5a400;
color: #fff;
font-size: 12px;
line-height: 24px;
text-align: center;
border-radius: 12px 0 0 12px;
}
.course-info {
flex: 1;
min-width: 0;
padding-right: 12px;
}
.course-title {
display: inline-block;
margin: 4px 0 8px;
color: #111827;
font-size: 18px;
font-weight: 700;
cursor: pointer;
}
.course-title:hover {
color: #006fc9;
}
.course-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
color: #8b96a6;
font-size: 13px;
margin-bottom: 12px;
}
.course-intro {
color: #4f5f73;
line-height: 1.8;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.course-time {
color: #8b96a6;
font-size: 13px;
}
.recommend-tags {
display: inline-flex;
gap: 6px;
margin-left: 8px;
vertical-align: 2px;
}
.course-empty {
background: #fff;
padding: 80px 0;
text-align: center;
color: #8b96a6;
border-top: 1px solid #dfe4ec;
}
</style>
<div id="app" class="course-display" v-cloak>
<div class="course-filter">
<div class="filter-row">
<div class="filter-label">推荐标识</div>
<div class="filter-options">
<button class="filter-chip" :class="{active: !query.recommendFlag}" @click="selectRecommend('')">全部</button>
<button
v-for="item in recommendOptions"
:key="item.code"
class="filter-chip"
:class="{active: query.recommendFlag === item.code}"
@click="selectRecommend(item.code)">
{{item.name}}
</button>
</div>
</div>
<div class="filter-row">
<div class="filter-label">课程类型</div>
<div class="filter-options">
<button class="filter-chip" :class="{active: !query.courseTypeId}" @click="selectCourseType('')">全部</button>
<button
v-for="item in courseTypeOptions"
:key="item.id"
class="filter-chip"
:class="{active: query.courseTypeId === item.id}"
@click="selectCourseType(item.id)">
{{item.typeName}}
</button>
</div>
</div>
</div>
<div class="course-result-bar">
<div class="course-count">为您找到相关课程<span>{{pageForm.totalCount || 0}}</span></div>
<el-input
class="course-search"
v-model="query.keyword"
clearable
placeholder="搜索关键字"
prefix-icon="el-icon-search"
@keyup.enter.native="doSearch"
@clear="doSearch">
<el-button slot="append" icon="el-icon-search" @click="doSearch"></el-button>
</el-input>
</div>
<div v-if="pageForm.list && pageForm.list.length" class="course-list">
<div v-for="course in pageForm.list" :key="course.id" class="course-item">
<div class="course-cover">
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
<div v-else class="course-cover-empty">{{course.courseTypeName || '课程'}}</div>
<div v-if="firstRecommendName(course.recommendFlags)" class="course-cover-tag">{{firstRecommendName(course.recommendFlags)}}</div>
</div>
<div class="course-info">
<div>
<span class="course-title" @click="openDetail(course)">{{course.courseName}}</span>
<span class="recommend-tags">
<el-tag v-for="code in splitFlags(course.recommendFlags)" :key="code" size="mini" type="warning">{{getRecommendName(code)}}</el-tag>
</span>
</div>
<div class="course-meta">
<span><i class="el-icon-office-building"></i> {{course.courseTypeName || '未设置课程类型'}}</span>
<span><i class="el-icon-user"></i> {{course.lecturerName || '未设置讲师'}}</span>
</div>
<div class="course-intro">{{course.courseIntro || '暂无课程介绍'}}</div>
<div class="course-time">
<i class="el-icon-time"></i>
<span v-if="course.openType === 'long_term'">长期开放</span>
<span v-else>{{course.startTimeText || '-'}} 至 {{course.endTimeText || '-'}}</span>
</div>
</div>
</div>
</div>
<div v-else class="course-empty">暂无相关课程</div>
<el-pagination
class="mt20"
background
layout="total, sizes, prev, pager, next"
:current-page.sync="pageForm.pageNumber"
:page-size.sync="pageForm.pageSize"
:total="pageForm.totalCount"
@size-change="pageData"
@current-change="pageData">
</el-pagination>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
query: {
keyword: "",
courseTypeId: "",
recommendFlag: ""
},
courseTypeOptions: [],
recommendOptions: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
list: []
}
}
},
methods: {
async pageData() {
const resp = await this.$axios.post(loc() + "/pageData", Object.assign({}, this.query, {
pageNumber: this.pageForm.pageNumber,
pageSize: this.pageForm.pageSize
}))
if (resp.code === 0) {
this.pageForm = resp.data
}
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
selectRecommend(code) {
this.query.recommendFlag = code
this.doSearch()
},
selectCourseType(id) {
this.query.courseTypeId = id
this.doSearch()
},
async loadOptions() {
const typeResp = await this.$axios.post(loc() + "/courseTypes")
if (typeResp.code === 0) {
this.courseTypeOptions = typeResp.data
}
const recommendResp = await this.$axios.post(loc() + "/recommendOptions")
if (recommendResp.code === 0) {
this.recommendOptions = recommendResp.data
}
},
getCoverUrl(cover) {
if (!cover) return ""
if (Array.isArray(cover)) {
return cover.length ? (cover[0].url || cover[0].response?.data || cover[0].data || "") : ""
}
if (typeof cover === "string" && cover.trim().startsWith("[")) {
try {
const files = JSON.parse(cover)
return files.length ? (files[0].url || files[0].response?.data || files[0].data || "") : ""
} catch (e) {
return ""
}
}
return cover
},
splitFlags(flags) {
return flags ? flags.split(",").filter(Boolean) : []
},
getRecommendName(code) {
const item = this.recommendOptions.find(v => v.code === code)
return item ? item.name : code
},
firstRecommendName(flags) {
const codes = this.splitFlags(flags)
return codes.length ? this.getRecommendName(codes[0]) : ""
},
openDetail(course) {
window.location.href = loc() + "/study?id=" + course.id
}
},
async created() {
await this.loadOptions()
await this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,755 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
#sidebar-menu,
#menu-toggle-btn,
#menu-overlay {
display: none !important;
}
#sub-app-container-main-content {
padding: 0 !important;
}
#sub-app-container-main-content-body {
height: 100%;
}
.study-page {
min-height: calc(100vh - 50px);
background: #eef3f8;
}
.study-header {
height: 150px;
margin: 12px 14px 0;
padding: 20px 24px;
color: #1f2937;
background: linear-gradient(120deg, #ffffff 0%, #f4f8ff 58%, #eaf2ff 100%);
border: 1px solid #dbe7f5;
border-radius: 10px;
box-shadow: 0 6px 18px rgba(31, 64, 116, .08);
display: flex;
align-items: center;
gap: 22px;
box-sizing: border-box;
}
.study-back {
align-self: flex-start;
margin-left: auto;
order: 5;
color: #1e63c8;
border-color: #b8d0f2;
background: #fff;
}
.study-cover {
width: 190px;
height: 102px;
object-fit: cover;
background: #eef4fb;
border: 1px solid #d7e3f1;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(31, 64, 116, .12);
}
.study-cover-empty {
width: 190px;
height: 102px;
display: flex;
align-items: center;
justify-content: center;
background: #eef4fb;
color: #6b7d90;
border: 1px solid #d7e3f1;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(31, 64, 116, .12);
font-weight: 600;
}
.study-course-main {
flex: 1;
min-width: 0;
}
.study-title {
font-size: 28px;
line-height: 1.2;
font-weight: 700;
margin-bottom: 14px;
}
.study-title .el-tag {
margin-left: 8px;
vertical-align: 5px;
}
.study-meta {
display: flex;
flex-wrap: wrap;
gap: 10px 26px;
color: #526376;
font-size: 14px;
}
.study-body {
display: flex;
gap: 14px;
height: calc(100vh - 212px);
padding: 14px;
box-sizing: border-box;
}
.study-tree-panel {
width: 340px;
background: #fff;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.study-tree-title {
height: 54px;
padding: 0 18px;
display: flex;
align-items: center;
justify-content: space-between;
color: #1768e5;
font-size: 18px;
font-weight: 700;
border-bottom: 1px solid #edf1f7;
}
.study-tree {
flex: 1;
overflow: auto;
padding: 10px;
}
.study-node {
display: inline-flex;
align-items: center;
max-width: 100%;
gap: 6px;
}
.study-node-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.study-content {
flex: 1;
min-width: 0;
background: #fff;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.resource-head {
min-height: 54px;
padding: 0 18px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #edf1f7;
}
.resource-title {
color: #1f2937;
font-size: 16px;
font-weight: 600;
}
.resource-actions {
display: flex;
align-items: center;
gap: 10px;
}
.study-progress {
width: 180px;
}
.resource-view {
flex: 1;
overflow: auto;
padding: 20px;
background: #f8fafc;
}
.media-player {
width: 100%;
max-height: calc(100vh - 300px);
background: #000;
}
.audio-wrap {
display: flex;
align-items: center;
justify-content: center;
height: 320px;
background: linear-gradient(135deg, #132544, #0a1224);
border-radius: 6px;
}
.audio-wrap audio {
width: 80%;
}
.image-preview {
max-width: 100%;
display: block;
margin: 0 auto;
background: #fff;
}
.doc-frame {
width: 100%;
height: calc(100vh - 275px);
border: 0;
background: #fff;
}
.file-fallback {
padding: 60px 20px;
text-align: center;
color: #667085;
background: #fff;
border-radius: 6px;
}
</style>
<div id="app" class="study-page" v-cloak>
<div class="study-header">
<el-button class="study-back" icon="el-icon-back" size="small" @click="goBack">返回</el-button>
<img v-if="coverUrl" class="study-cover" :src="coverUrl" :alt="course.courseName">
<div v-else class="study-cover-empty">课程图片</div>
<div class="study-course-main">
<div class="study-title">
{{course.courseName || '课程学习'}}
<el-tag v-for="code in splitFlags(course.recommendFlags)" :key="code" size="small" type="warning">{{getRecommendName(code)}}</el-tag>
</div>
<div class="study-meta">
<span><i class="el-icon-user"></i> 授课讲师:{{course.lecturerName || '未设置'}}</span>
<span><i class="el-icon-date"></i> {{course.openType === 'long_term' ? '长期开放' : ((course.startTimeText || '-') + ' 至 ' + (course.endTimeText || '-'))}}</span>
<span><i class="el-icon-collection-tag"></i> 课程类型:{{course.courseTypeName || '未设置'}}</span>
</div>
</div>
</div>
<div class="study-body">
<div class="study-tree-panel">
<div class="study-tree-title">
<span>课程安排</span>
<el-button type="text" size="mini" @click="collapseAll">收起</el-button>
</div>
<div class="study-tree">
<el-tree
ref="studyTree"
:data="treeData"
node-key="id"
default-expand-all
highlight-current
:expand-on-click-node="false"
:props="{children:'children', label:'title'}"
@node-click="nodeClick">
<span slot-scope="{ data }" class="study-node">
<i :class="getNodeIcon(data)"></i>
<span class="study-node-title">{{data.title}}</span>
<el-tag v-if="data.required" size="mini" type="warning">必学</el-tag>
</span>
</el-tree>
</div>
</div>
<div class="study-content">
<div class="resource-head">
<div class="resource-title">{{selectedResource.title || '请选择课程资料'}}</div>
<div class="resource-actions">
<el-progress
v-if="selectedResource.id"
class="study-progress"
:percentage="recordProgress"
:stroke-width="8">
</el-progress>
<el-tag v-if="studying" size="mini" type="success">学习中 {{recordStudyTime}}</el-tag>
<el-button v-if="selectedResource.id && !studying" size="mini" type="primary" icon="el-icon-video-play" @click="startStudy">开始学习</el-button>
<el-button v-if="studying" size="mini" type="danger" icon="el-icon-video-pause" @click="finishStudy(false)">结束学习</el-button>
<el-button v-if="fileUrl" size="mini" type="text" icon="el-icon-download" @click="openFile">打开原文件</el-button>
</div>
</div>
<div class="resource-view">
<template v-if="selectedResource.id">
<video
v-if="selectedResource.resourceType === 'video'"
ref="mediaPlayer"
class="media-player"
:src="fileUrl"
controls
controlslist="nodownload"
@play="mediaPlay"
@ended="mediaEnded"
@timeupdate="saveProgress"
@loadedmetadata="mediaReady">
</video>
<div v-else-if="selectedResource.resourceType === 'audio'" class="audio-wrap">
<audio
ref="mediaPlayer"
:src="fileUrl"
controls
controlslist="nodownload"
@play="mediaPlay"
@ended="mediaEnded"
@timeupdate="saveProgress"
@loadedmetadata="mediaReady">
</audio>
</div>
<img v-else-if="selectedResource.resourceType === 'image'" class="image-preview" :src="fileUrl" :alt="selectedResource.title">
<iframe v-else-if="canInlinePreview(selectedResource)" class="doc-frame" :src="inlinePreviewUrl"></iframe>
<div v-else class="file-fallback">
<file-preview :files="selectedResource.fileData" complete_result></file-preview>
<el-button class="mt20" type="primary" icon="el-icon-view" @click="openFile">打开资料</el-button>
</div>
</template>
<el-empty v-else description="请选择左侧课程资料"></el-empty>
</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
courseId: "",
apiBase: "/platform/learning/course/display",
recordApi: "/platform/learning/study/record",
course: {},
treeData: [],
selectedResource: {},
recommendOptions: [],
studying: false,
currentSegmentId: "",
recordProgress: 0,
recordStudyTime: "0秒",
pendingSeconds: 0,
heartbeatTimer: null,
startingStudy: false,
autoResumeKey: "",
lastPositionSyncAt: 0,
lastActiveAt: Date.now(),
inactiveLimit: 60000
}
},
computed: {
coverUrl() {
return this.getFileUrl(this.course.cover)
},
fileUrl() {
return this.getFileUrl(this.selectedResource.fileData)
},
inlinePreviewUrl() {
const id = this.getFileId(this.selectedResource.fileData)
return id ? this.apiBase + "/pdfPreview?id=" + encodeURIComponent(id) : this.fileUrl
}
},
methods: {
getQuery(name) {
return new URLSearchParams(window.location.search).get(name) || ""
},
async loadCourse() {
const resp = await this.$axios.post(this.apiBase + "/courseInfo", { id: this.courseId })
if (resp.code === 0) {
this.course = resp.data
} else {
this.$message.warning(resp.msg)
}
},
async loadTree() {
const resp = await this.$axios.post(this.apiBase + "/studyTree", { courseId: this.courseId })
if (resp.code === 0) {
this.treeData = resp.data || []
const first = this.findFirstResource(this.treeData)
if (first) {
this.nodeClick(first)
this.$nextTick(() => this.$refs.studyTree && this.$refs.studyTree.setCurrentKey(first.id))
}
}
},
async loadRecommendOptions() {
const resp = await this.$axios.post(this.apiBase + "/recommendOptions")
if (resp.code === 0) {
this.recommendOptions = resp.data
}
},
findFirstResource(nodes) {
for (const node of nodes || []) {
if (node.type === "resource") return node
const child = this.findFirstResource(node.children || [])
if (child) return child
}
return null
},
nodeClick(data) {
if (data.type !== "resource") return
if (this.studying && this.selectedResource.id !== data.id) {
this.$message.warning("请先结束当前章节资料的学习")
return
}
this.selectedResource = data
this.recordProgress = data.progressPercent || 0
this.recordStudyTime = this.formatSeconds(Number(data.studySeconds || 0))
},
getNodeIcon(data) {
if (data.type === "resource") {
const map = {
video: "el-icon-video-camera",
audio: "el-icon-headset",
image: "el-icon-picture-outline",
pdf: "el-icon-document",
word: "el-icon-document",
ppt: "el-icon-document"
}
return map[data.resourceType] || "el-icon-paperclip"
}
return data.nodeType === "chapter" ? "el-icon-folder" : "el-icon-notebook-2"
},
getFileUrl(value) {
if (!value) return ""
if (Array.isArray(value)) {
return value.length ? (value[0].url || value[0].response?.data || value[0].data || "") : ""
}
if (typeof value === "string" && value.trim().startsWith("[")) {
try {
const files = JSON.parse(value)
return files.length ? (files[0].url || files[0].response?.data || files[0].data || "") : ""
} catch (e) {
return ""
}
}
return value
},
getFileId(value) {
const url = this.getFileUrl(value)
if (!url) return ""
const matched = url.match(/[?&]id=([^&]+)/)
if (matched) return decodeURIComponent(matched[1])
if (!url.includes("/") && !url.includes(".")) return url
return ""
},
canInlinePreview(resource) {
const type = resource.resourceType
const ext = (resource.fileExt || "").toLowerCase()
return ["pdf", "ppt", "word"].includes(type) || ["pdf", "ppt", "pptx", "doc", "docx"].includes(ext)
},
progressKey() {
return "learning-progress-" + this.courseId + "-" + this.selectedResource.id
},
mediaReady() {
this.askResume()
},
askResume() {
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
if (this.studying || this.startingStudy) return
const currentKey = this.progressKey()
if (this.autoResumeKey === currentKey) return
this.autoResumeKey = currentKey
const player = this.$refs.mediaPlayer
const localSaved = Number(localStorage.getItem(this.progressKey()) || 0)
const serverSaved = Number(this.selectedResource.lastPositionSeconds || 0)
const saved = Math.max(localSaved, serverSaved)
if (!player || !saved || saved < 5 || saved >= player.duration - 5) return
this.$confirm("检测到上次学习到 " + this.formatSeconds(saved) + ",请选择继续学习或从头开始。", "继续学习", {
confirmButtonText: "继续学习",
cancelButtonText: "从头开始",
type: "info"
}).then(() => {
this.startStudy({ autoPlay: true, seekTo: saved })
}).catch(() => {
localStorage.removeItem(this.progressKey())
this.saveServerPosition(0)
this.startStudy({ autoPlay: true, seekTo: 0 })
})
},
async mediaPlay() {
if (this.studying || this.startingStudy) return
await this.startStudy({ fromMedia: true })
},
mediaEnded() {
this.finishStudy(false, { silent: true })
localStorage.removeItem(this.progressKey())
},
async playSelectedMedia(seekTo) {
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
await this.$nextTick()
const player = this.$refs.mediaPlayer
if (!player) return
try {
if (typeof seekTo === "number" && !Number.isNaN(seekTo)) {
await this.seekBeforePlay(player, seekTo)
}
const playPromise = player.play && player.play()
if (playPromise && playPromise.catch) {
await playPromise.catch(() => {
this.$message.warning("浏览器阻止了自动播放,请点击播放器播放按钮继续")
})
}
} catch (e) {
this.$message.warning("播放器定位失败,请手动点击播放后继续")
}
},
async seekBeforePlay(player, seekTo) {
await this.waitForMediaReady(player)
const target = this.normalizeSeekTarget(player, seekTo)
if (target === null) return
if (Math.abs((player.currentTime || 0) - target) <= 1) return
player.pause()
player.currentTime = target
await this.waitForSeek(player, target, 1800)
if (Math.abs((player.currentTime || 0) - target) > 1) {
player.currentTime = target
await this.waitForSeek(player, target, 1200)
}
},
waitForMediaReady(player) {
if (player.readyState >= 1 && !Number.isNaN(player.duration)) {
return Promise.resolve()
}
return new Promise(resolve => {
const done = () => {
player.removeEventListener("loadedmetadata", done)
player.removeEventListener("durationchange", done)
resolve()
}
player.addEventListener("loadedmetadata", done, { once: true })
player.addEventListener("durationchange", done, { once: true })
setTimeout(done, 2000)
})
},
waitForSeek(player, target, timeout) {
return new Promise(resolve => {
const done = () => {
player.removeEventListener("seeked", done)
player.removeEventListener("timeupdate", done)
resolve()
}
player.addEventListener("seeked", done, { once: true })
player.addEventListener("timeupdate", done, { once: true })
setTimeout(done, timeout)
})
},
normalizeSeekTarget(player, seekTo) {
const raw = Math.max(0, Number(seekTo || 0))
if (Number.isNaN(raw)) return null
if (!Number.isNaN(player.duration) && player.duration > 0) {
return Math.min(raw, Math.max(0, player.duration - 1))
}
return raw
},
saveProgress() {
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
const player = this.$refs.mediaPlayer
if (player && player.currentTime > 0) {
const position = Math.floor(player.currentTime)
localStorage.setItem(this.progressKey(), String(position))
this.$set(this.selectedResource, "lastPositionSeconds", position)
if (Date.now() - this.lastPositionSyncAt > 5000) {
this.saveServerPosition(position)
}
}
},
saveServerPosition(position, useBeacon) {
if (!this.selectedResource.id || !["video", "audio"].includes(this.selectedResource.resourceType)) return
const seconds = Math.max(0, Math.floor(Number(position || 0)))
this.lastPositionSyncAt = Date.now()
if (useBeacon && navigator.sendBeacon) {
const body = new URLSearchParams()
body.append("courseId", this.courseId)
body.append("resourceId", this.selectedResource.id)
body.append("positionSeconds", String(seconds))
const blob = new Blob([body.toString()], { type: "application/x-www-form-urlencoded;charset=UTF-8" })
navigator.sendBeacon(this.recordApi + "/position", blob)
return
}
this.$axios.post(this.recordApi + "/position", {
courseId: this.courseId,
resourceId: this.selectedResource.id,
positionSeconds: seconds
})
},
async startStudy(options) {
const config = options && !options.target ? options : {}
if (!this.selectedResource.id) {
this.$message.warning("请选择左侧课程资料")
return
}
if (this.studying) {
if (config.autoPlay) {
this.playSelectedMedia(config.seekTo)
}
return
}
this.startingStudy = true
let resp
try {
resp = await this.$axios.post(this.recordApi + "/start", {
courseId: this.courseId,
resourceId: this.selectedResource.id,
positionSeconds: typeof config.seekTo === "number" ? Math.floor(config.seekTo) : this.getMediaPosition()
})
} finally {
this.startingStudy = false
}
if (resp.code !== 0) {
this.$message.warning(resp.msg)
return
}
this.currentSegmentId = resp.data.segmentId
this.studying = true
this.pendingSeconds = 0
this.lastActiveAt = Date.now()
this.applyRecordState(resp.data)
this.startHeartbeatTimer()
if (config.autoPlay !== false && !config.fromMedia) {
this.playSelectedMedia(config.seekTo)
}
this.$message.success("已开始学习")
},
async heartbeat() {
if (!this.studying || !this.currentSegmentId || this.pendingSeconds <= 0) return
const seconds = this.pendingSeconds
this.pendingSeconds = 0
const resp = await this.$axios.post(this.recordApi + "/heartbeat", {
segmentId: this.currentSegmentId,
activeSeconds: seconds,
positionSeconds: this.getMediaPosition()
})
if (resp.code === 0) {
this.applyRecordState(resp.data)
} else {
this.stopHeartbeatTimer()
this.studying = false
this.currentSegmentId = ""
this.$message.warning(resp.msg)
}
},
async finishStudy(force, options) {
const config = options || {}
if (!this.currentSegmentId) return
const segmentId = this.currentSegmentId
const seconds = this.pendingSeconds
this.pendingSeconds = 0
this.stopHeartbeatTimer()
this.studying = false
this.currentSegmentId = ""
if (!force) {
this.pauseSelectedMedia()
}
if (force && navigator.sendBeacon) {
this.saveServerPosition(this.getMediaPosition(), true)
const formData = new FormData()
formData.append("segmentId", segmentId)
formData.append("activeSeconds", String(seconds))
formData.append("positionSeconds", String(this.getMediaPosition()))
navigator.sendBeacon(this.recordApi + "/finish", formData)
return
}
const resp = await this.$axios.post(this.recordApi + "/finish", {
segmentId: segmentId,
activeSeconds: seconds,
positionSeconds: this.getMediaPosition()
})
if (resp.code === 0 && resp.data) {
this.applyRecordState(resp.data)
if (!config.silent) {
this.$message.success("学习已结束")
}
}
},
pauseSelectedMedia() {
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
const player = this.$refs.mediaPlayer
if (player && !player.paused) {
player.pause()
}
},
startHeartbeatTimer() {
this.stopHeartbeatTimer()
this.heartbeatTimer = setInterval(() => {
if (!this.studying) return
if (this.isEffectiveLearning()) {
this.pendingSeconds += 1
}
if (this.pendingSeconds >= 15) {
this.heartbeat()
}
}, 1000)
},
stopHeartbeatTimer() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
},
isEffectiveLearning() {
if (document.hidden) return false
if (Date.now() - this.lastActiveAt > this.inactiveLimit) return false
if (["video", "audio"].includes(this.selectedResource.resourceType)) {
const player = this.$refs.mediaPlayer
return !!player && !player.paused && !player.ended
}
return true
},
markActive() {
this.lastActiveAt = Date.now()
},
applyRecordState(data) {
this.recordProgress = Number(data.progressPercent || 0)
this.recordStudyTime = data.studyTimeText || this.formatSeconds(Number(data.studySeconds || 0))
if (this.selectedResource.id && data.lastPositionSeconds !== undefined) {
this.$set(this.selectedResource, "lastPositionSeconds", Number(data.lastPositionSeconds || 0))
}
},
getMediaPosition() {
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return 0
const player = this.$refs.mediaPlayer
return player ? Math.floor(player.currentTime || 0) : Number(this.selectedResource.lastPositionSeconds || 0)
},
formatSeconds(seconds) {
const hour = Math.floor(seconds / 3600)
const minute = Math.floor(seconds % 3600 / 60)
const second = Math.floor(seconds % 60)
if (hour > 0) return hour + "小时" + minute + "分" + second + "秒"
if (minute > 0) return minute + "分" + second + "秒"
return second + "秒"
},
splitFlags(flags) {
return flags ? flags.split(",").filter(Boolean) : []
},
getRecommendName(code) {
const item = this.recommendOptions.find(v => v.code === code)
return item ? item.name : code
},
openFile() {
if (this.fileUrl) window.open(this.fileUrl)
},
collapseAll() {
const nodesMap = this.$refs.studyTree && this.$refs.studyTree.store.nodesMap
Object.keys(nodesMap || {}).forEach(key => {
nodesMap[key].expanded = false
})
},
async goBack() {
this.saveServerPosition(this.getMediaPosition())
await this.finishStudy(false, { silent: true })
window.location.href = this.apiBase
}
},
async created() {
this.courseId = this.getQuery("id")
window.addEventListener("mousemove", this.markActive)
window.addEventListener("keydown", this.markActive)
window.addEventListener("click", this.markActive)
window.addEventListener("scroll", this.markActive, true)
window.addEventListener("beforeunload", () => this.finishStudy(true))
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.saveServerPosition(this.getMediaPosition(), true)
}
})
await this.loadRecommendOptions()
await this.loadCourse()
await this.loadTree()
},
beforeDestroy() {
this.finishStudy(true)
this.stopHeartbeatTimer()
window.removeEventListener("mousemove", this.markActive)
window.removeEventListener("keydown", this.markActive)
window.removeEventListener("click", this.markActive)
window.removeEventListener("scroll", this.markActive, true)
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,396 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="课程名称">
<el-input v-model="pageForm.courseName" clearable placeholder="请输入课程名称" style="width: 100%" @keyup.enter.native="doSearch"></el-input>
</search-item>
<search-item label="课程类型">
<el-select v-model="pageForm.courseTypeId" clearable filterable placeholder="请选择课程类型" style="width: 100%">
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="开课时间">
<el-date-picker
v-model="pageForm.courseTime"
type="daterange"
start-placeholder="开始时间"
end-placeholder="结束时间"
range-separator="至"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="授课讲师">
<el-input v-model="pageForm.lecturerName" clearable placeholder="请输入授课讲师" style="width: 100%" @keyup.enter.native="doSearch"></el-input>
</search-item>
<search-item label="课程状态">
<el-select v-model="pageForm.status" clearable placeholder="请选择课程状态" style="width: 100%">
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</search-item>
<search-item label="推荐标识">
<el-select v-model="pageForm.recommendFlag" clearable placeholder="请选择推荐标识" style="width: 100%">
<el-option v-for="item in recommendOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="课程列表">
<el-button @click="openAdd" size="medium" type="primary">
<i class="el-icon-plus"></i>
新增课程
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="80"></el-table-column>
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="180"></el-table-column>
<el-table-column align="center" header-align="center" label="课程类型" prop="courseTypeName" sortable="custom" show-overflow-tooltip width="140"></el-table-column>
<el-table-column align="center" header-align="center" label="授课讲师" prop="lecturerName" sortable="custom" show-overflow-tooltip width="140"></el-table-column>
<el-table-column align="center" header-align="center" label="开课时间" prop="startTime" sortable="custom" width="230">
<template slot-scope="{row}">
<span v-if="row.openType === 'long_term'">长期开放</span>
<span v-else>{{formatTime(row.startTime)}} 至 {{formatTime(row.endTime)}}</span>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="课程状态" prop="status" sortable="custom" width="120">
<template slot-scope="{row}">
<el-tag :type="getStatusType(row.status)" size="mini">{{getStatusName(row.status)}}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="推荐标识" prop="recommendFlags" sortable="custom" show-overflow-tooltip min-width="140">
<template slot-scope="{row}">
<el-tag
v-for="code in splitValue(row.recommendFlags)"
:key="code"
:type="getRecommendType(code)"
size="mini"
style="margin: 2px 3px">
{{getDictName(code, recommendOptions)}}
</el-tag>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="排序编码" prop="sortNum" sortable="custom" width="120"></el-table-column>
<el-table-column align="center" header-align="center" label="操作" width="260">
<template slot-scope="{row}">
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="goChapterContent(row)" size="mini" type="success">章节内容</el-button>
<el-button @click="doDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" width="72%">
<el-form :model="formData" ref="courseForm" :rules="formRules" label-width="110px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="课程名称" prop="courseName">
<el-input v-model="formData.courseName" maxlength="100" placeholder="请输入课程名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课程类型" prop="courseTypeId">
<el-select v-model="formData.courseTypeId" filterable placeholder="请选择课程类型" style="width: 100%">
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="授课讲师" prop="lecturerName">
<el-input v-model="formData.lecturerName" maxlength="100" placeholder="请输入教师姓名"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="课程状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择课程状态" style="width: 100%">
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开课方式" prop="openType">
<el-radio-group v-model="formData.openType">
<el-radio label="fixed">固定开课时间</el-radio>
<el-radio label="long_term">长期开放</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开课时间" prop="openTime" v-if="formData.openType !== 'long_term'">
<el-date-picker
v-model="formData.openTime"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结束时间"
range-separator="至"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="推荐标识" prop="recommendFlagList">
<el-select v-model="formData.recommendFlagList" multiple clearable placeholder="请选择推荐标识" style="width: 100%">
<el-option v-for="item in recommendOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序编码" prop="sortNum">
<el-input-number v-model="formData.sortNum" :controls="false" :min="0" :precision="0" style="width: 100%" placeholder="请输入排序编码"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="课程介绍" prop="courseIntro">
<el-input v-model="formData.courseIntro" type="textarea" maxlength="2000" :rows="4" show-word-limit placeholder="请输入课程简介"></el-input>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="适合人群" prop="suitablePeople">
<el-input v-model="formData.suitablePeople" type="textarea" maxlength="500" :rows="3" show-word-limit placeholder="请输入适合人群"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学习目标" prop="learningGoal">
<el-input v-model="formData.learningGoal" type="textarea" maxlength="500" :rows="3" show-word-limit placeholder="请输入学习目标"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="课程封面" prop="cover">
<file-upload
:upload_number="1"
:upload_size="5242880"
:value.sync="formData.cover"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
<div class="el-upload__tip">只能上传1个文件;只能上传.jpg,.jpeg,.png文件;单个文件大小不能超过5M。</div>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :disabled="subDis" @click="operation">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
title: "",
dialogVisible: false,
subDis: false,
courseTypeOptions: [],
recommendOptions: [],
statusOptions: [
{ name: "草稿", code: "draft" },
{ name: "待发布", code: "pending" },
{ name: "已发布", code: "published" },
{ name: "已下架", code: "offline" }
],
formData: {},
formRules: {
courseName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
courseTypeId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lecturerName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
status: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
sortNum: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
cover: [{ required: true, message: "请上传课程封面", trigger: ["blur", "change"] }]
}
}
},
methods: {
buildPageParams() {
const params = Object.assign({}, this.pageForm)
params.startTime = params.courseTime && params.courseTime.length ? params.courseTime[0] : ""
params.endTime = params.courseTime && params.courseTime.length ? params.courseTime[1] : ""
return params
},
pageData(data = null) {
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
this.tableLoading = true
this.$axios.post(address, data ? data : this.buildPageParams()).then((res) => {
this.tableLoading = false
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
resetSearch() {
this.pageForm.courseName = ""
this.pageForm.courseTypeId = ""
this.pageForm.courseTime = []
this.pageForm.lecturerName = ""
this.pageForm.status = ""
this.pageForm.recommendFlag = ""
this.doSearch()
},
openAdd() {
this.title = "新增课程"
this.formData = {
courseName: "",
courseTypeId: "",
lecturerName: "",
courseIntro: "",
suitablePeople: "",
learningGoal: "",
openType: "fixed",
openTime: [],
status: "draft",
recommendFlagList: [],
sortNum: 0,
cover: ""
}
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.courseForm && this.$refs.courseForm.clearValidate()
})
},
openEdit(row) {
this.title = "编辑课程"
this.formData = Object.assign({}, row, {
openTime: row.startTime && row.endTime ? [this.formatTime(row.startTime), this.formatTime(row.endTime)] : [],
recommendFlagList: this.splitValue(row.recommendFlags)
})
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.courseForm && this.$refs.courseForm.clearValidate()
})
},
goChapterContent(row) {
const url = "/platform/learning/chapter/content?courseId=" + encodeURIComponent(row.id || "")
if (window.commonUtil && commonUtil.pjaxPush) {
commonUtil.pjaxPush(url)
} else {
window.location.href = url
}
},
operation() {
this.$refs.courseForm.validate(async (valid) => {
if (!valid) return
if (this.formData.openType !== "long_term" && (!this.formData.openTime || this.formData.openTime.length !== 2)) {
this.$message.warning("请选择开课时间")
return
}
const data = Object.assign({}, this.formData)
data.startTime = data.openType === "long_term" ? "" : data.openTime[0]
data.endTime = data.openType === "long_term" ? "" : data.openTime[1]
data.recommendFlags = (data.recommendFlagList || []).join(",")
delete data.openTime
delete data.recommendFlagList
const method = data.id ? "/doEdit" : "/doAdd"
this.subDis = true
const resp = await this.$axios.post(loc() + method, data)
this.subDis = false
if (resp.code === 0) {
this.dialogVisible = false
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.warning(resp.msg)
}
})
},
doDelete(row) {
this.$confirm("确定要删除该课程吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post(loc() + "/doDelete", { id: row.id })
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.error(resp.msg)
}
})
},
async getCourseTypes() {
const resp = await this.$axios.post(loc() + "/courseTypes")
if (resp.code === 0) {
this.courseTypeOptions = resp.data
}
},
async getLearningDict(name) {
const resp = await this.$axios.post(loc() + "/learningDictOptions", { name })
return resp.code === 0 ? resp.data : []
},
splitValue(value) {
return value ? value.split(",").filter(Boolean) : []
},
getStatusName(code) {
const item = this.statusOptions.find(v => v.code === code)
return item ? item.name : ""
},
getStatusType(code) {
const map = {
draft: "info",
pending: "warning",
published: "success",
offline: "danger"
}
return map[code] || ""
},
getDictName(code, options) {
const item = options.find(v => v.code === code)
return item ? item.name : code
},
getDictNames(value, options) {
const values = this.splitValue(value)
return values.map(code => {
const item = options.find(v => v.code === code)
return item ? item.name : code
}).join("、")
},
getRecommendType(code) {
const name = this.getDictName(code, this.recommendOptions)
if (name.includes("热门") || name.includes("推荐")) return "warning"
if (name.includes("新")) return "success"
return ""
},
formatTime(time) {
return time ? this.$moment(time).format("YYYY-MM-DD HH:mm:ss") : ""
}
},
async created() {
this.pageForm.courseTime = []
await this.getCourseTypes()
this.recommendOptions = await this.getLearningDict("推荐标识")
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,189 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="课程类型名称">
<el-input
v-model="pageForm.typeName"
clearable
placeholder="请输入课程类型名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="课程类型列表">
<el-button @click="openAdd" size="medium" type="primary">
<i class="el-icon-plus"></i>
新建
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border>
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="80"></el-table-column>
<el-table-column align="center" header-align="center" label="课程类型名称" prop="typeName" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" label="备注" prop="remark" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" label="排序编号" prop="sortNum" width="140"></el-table-column>
<el-table-column align="center" header-align="center" label="是否启用" prop="enabled" width="140">
<template slot-scope="{row}">
<el-switch
v-model="row.enabled"
active-color="#13ce66"
inactive-color="#ff4949"
@change="switchChange(row)">
</el-switch>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="操作" width="180">
<template slot-scope="{row}">
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="doDelete(row)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" width="40%">
<el-form :model="formData" ref="courseTypeForm" :rules="formRules" label-width="120px">
<el-form-item label="课程类型名称" prop="typeName">
<el-input v-model="formData.typeName" maxlength="100" placeholder="请输入课程类型名称"></el-input>
</el-form-item>
<el-form-item label="排序编号" prop="sortNum">
<el-input-number
v-model="formData.sortNum"
:controls="false"
:min="0"
:precision="0"
placeholder="请输入排序编号"
style="width: 100%">
</el-input-number>
</el-form-item>
<el-form-item label="是否启用" prop="enabled">
<el-switch v-model="formData.enabled" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
maxlength="500"
show-word-limit
:rows="4"
placeholder="请输入备注">
</el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :disabled="subDis" @click="operation">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
title: "",
dialogVisible: false,
subDis: false,
formData: {},
formRules: {
typeName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
sortNum: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
}
}
},
methods: {
resetSearch() {
this.pageForm.typeName = ""
this.doSearch()
},
openAdd() {
this.title = "新建课程类型"
this.formData = {
typeName: "",
sortNum: 0,
enabled: true,
remark: ""
}
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.courseTypeForm && this.$refs.courseTypeForm.clearValidate()
})
},
openEdit(row) {
this.title = "编辑课程类型"
this.formData = Object.assign({}, row)
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.courseTypeForm && this.$refs.courseTypeForm.clearValidate()
})
},
operation() {
const method = this.formData.id ? "/doEdit" : "/doAdd"
this.$refs.courseTypeForm.validate(async (valid) => {
if (!valid) return
this.subDis = true
const resp = await this.$axios.post(loc() + method, this.formData)
this.subDis = false
if (resp.code === 0) {
this.dialogVisible = false
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.warning(resp.msg)
}
})
},
async switchChange(row) {
const resp = await this.$axios.post(loc() + "/doEdit", row)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.error(resp.msg)
row.enabled = !row.enabled
}
},
doDelete(row) {
this.$confirm("确定要删除该课程类型吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post(loc() + "/doDelete", { id: row.id })
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.error(resp.msg)
}
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,180 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="工号/姓名">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入工号或姓名"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属分工会" v-if="unionOptions.length">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属分工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="课程名称">
<el-select v-model="pageForm.courseId" clearable filterable placeholder="请选择课程" style="width: 100%">
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="是否完成">
<el-select v-model="pageForm.completeStatus" clearable placeholder="请选择完成状态" style="width: 100%">
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="学习列表"></table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="70"></el-table-column>
<el-table-column align="center" header-align="center" label="工号" prop="loginName" sortable="custom" show-overflow-tooltip width="120"></el-table-column>
<el-table-column align="center" header-align="center" label="姓名" prop="userName" sortable="custom" show-overflow-tooltip width="110"></el-table-column>
<el-table-column align="center" header-align="center" label="所属分工会" prop="unionName" sortable="custom" show-overflow-tooltip min-width="150"></el-table-column>
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="180"></el-table-column>
<el-table-column align="center" header-align="center" label="章节名称" prop="outlineName" sortable="custom" show-overflow-tooltip min-width="160"></el-table-column>
<el-table-column align="center" header-align="center" label="学习开始时间" prop="firstStudyTime" sortable="custom" width="170">
<template slot-scope="{row}">{{formatTime(row.firstStudyTime)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="最近学习时间" prop="latestStudyTime" sortable="custom" width="170">
<template slot-scope="{row}">{{formatTime(row.latestStudyTime)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="学习时长" prop="studySeconds" sortable="custom" width="130">
<template slot-scope="{row}">{{row.studyTimeText}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="学习进度" prop="progressPercent" sortable="custom" width="180">
<template slot-scope="{row}">
<el-progress :percentage="Number(row.progressPercent || 0)" :stroke-width="8"></el-progress>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="是否完成" prop="completeStatus" sortable="custom" width="110">
<template slot-scope="{row}">
<el-tag :type="statusType(row.completeStatus)" size="mini">{{statusName(row.completeStatus)}}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="操作" width="100">
<template slot-scope="{row}">
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteRecord(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
apiBase: "/platform/learning/study/record",
courseOptions: [],
unionOptions: [],
statusOptions: [
{ name: "未开始", code: "not_started", type: "info" },
{ name: "学习中", code: "studying", type: "warning" },
{ name: "已完成", code: "completed", type: "success" }
]
}
},
methods: {
pageData() {
this.tableLoading = true
this.$axios.post(this.apiBase + "/pageData", this.pageForm).then(resp => {
this.tableLoading = false
if (resp.code === 0) {
this.tableData = resp.data.list || []
this.pageForm.totalCount = resp.data.totalCount
} else {
this.$message.warning(resp.msg)
}
}).catch(() => {
this.tableLoading = false
})
},
pageOrder(column) {
this.pageForm.pageOrderName = column.prop
this.pageForm.pageOrderBy = column.order
this.pageData()
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
resetSearch() {
this.pageForm.keyword = ""
this.pageForm.unionId = ""
this.pageForm.courseId = ""
this.pageForm.completeStatus = ""
this.pageForm.pageNumber = 1
this.pageData()
},
async loadOptions() {
const courseResp = await this.$axios.post(this.apiBase + "/courseOptions")
if (courseResp.code === 0) {
this.courseOptions = courseResp.data || []
}
const unionResp = await this.$axios.post(this.apiBase + "/unionOptions")
if (unionResp.code === 0) {
this.unionOptions = unionResp.data || []
}
},
statusName(code) {
const item = this.statusOptions.find(v => v.code === code)
return item ? item.name : "未开始"
},
statusType(code) {
const item = this.statusOptions.find(v => v.code === code)
return item ? item.type : "info"
},
formatTime(value) {
return value ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : ""
},
deleteRecord(row) {
this.$confirm("确定要删除该学习记录吗?删除后该章节的学习时段也会一并清理。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post(this.apiBase + "/delete", { id: row.id })
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.warning(resp.msg)
}
})
}
},
async created() {
this.$set(this.pageForm, "keyword", "")
this.$set(this.pageForm, "unionId", "")
this.$set(this.pageForm, "courseId", "")
this.$set(this.pageForm, "completeStatus", "")
await this.loadOptions()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,167 @@
<!--#
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-select
v-model="pageForm.courseId"
clearable
filterable
remote
:remote-method="queryCourseOptions"
placeholder="请选择课程"
style="width: 100%">
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="所属分工会" v-if="unionOptions.length">
<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">
<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">
<el-tabs v-model="activeTab" @tab-click="tabChange">
<el-tab-pane label="课程统计" name="course"></el-tab-pane>
<el-tab-pane label="分工会统计" name="union"></el-tab-pane>
</el-tabs>
<table-tool :app="this" :label="activeTab === 'course' ? '课程统计' : '分工会统计'"></table-tool>
<el-table
v-if="activeTab === 'course'"
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="70"></el-table-column>
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="220" width="360"></el-table-column>
<el-table-column align="center" header-align="center" label="学习人数" prop="learnerCount" sortable="custom" width="130"></el-table-column>
<el-table-column align="center" header-align="center" label="完成人数" prop="completedCount" sortable="custom" width="130"></el-table-column>
<el-table-column align="center" header-align="center" label="平均学习时长" prop="avgStudySeconds" sortable="custom" width="170">
<template slot-scope="{row}">{{row.avgStudyTimeText}}</template>
</el-table-column>
</el-table>
<el-table
v-else
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="70"></el-table-column>
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="220"></el-table-column>
<el-table-column align="center" header-align="center" label="分工会名称" prop="unionName" sortable="custom" show-overflow-tooltip min-width="170"></el-table-column>
<el-table-column align="center" header-align="center" label="学习人数" prop="learnerCount" sortable="custom" width="130"></el-table-column>
<el-table-column align="center" header-align="center" label="学习时长" prop="studySeconds" sortable="custom" width="160">
<template slot-scope="{row}">{{row.studyTimeText}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="完成率" prop="completeRate" sortable="custom" width="130"></el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
apiBase: "/platform/learning/statistics",
activeTab: "course",
courseOptions: [],
unionOptions: []
}
},
methods: {
pageData() {
this.tableLoading = true
const url = this.activeTab === "course" ? "/coursePageData" : "/unionPageData"
this.$axios.post(this.apiBase + url, this.pageForm).then(resp => {
this.tableLoading = false
if (resp.code === 0) {
this.tableData = resp.data.list || []
this.pageForm.totalCount = resp.data.totalCount
} else {
this.$message.warning(resp.msg)
}
}).catch(() => {
this.tableLoading = false
})
},
pageOrder(column) {
this.pageForm.pageOrderName = column.prop
this.pageForm.pageOrderBy = column.order
this.pageData()
},
async doSearch() {
this.pageForm.pageNumber = 1
await this.ensureUnionDefaultCourse()
this.pageData()
},
async resetSearch() {
this.pageForm.courseId = ""
this.pageForm.unionId = ""
this.pageForm.pageOrderName = ""
this.pageForm.pageOrderBy = ""
this.pageForm.pageNumber = 1
await this.queryCourseOptions("")
await this.ensureUnionDefaultCourse()
this.pageData()
},
async tabChange() {
this.tableData = []
this.pageForm.pageNumber = 1
this.pageForm.pageOrderName = ""
this.pageForm.pageOrderBy = ""
await this.ensureUnionDefaultCourse()
this.pageData()
},
async queryCourseOptions(query) {
const resp = await this.$axios.post(this.apiBase + "/courseOptions", { courseName: query || "" })
if (resp.code === 0) {
this.courseOptions = resp.data || []
}
},
async ensureUnionDefaultCourse() {
if (this.activeTab !== "union" || this.pageForm.courseId) {
return
}
if (!this.courseOptions.length) {
await this.queryCourseOptions("")
}
if (this.courseOptions.length) {
this.pageForm.courseId = this.courseOptions[0].id
}
},
async loadUnionOptions() {
const resp = await this.$axios.post(this.apiBase + "/unionOptions")
if (resp.code === 0) {
this.unionOptions = resp.data || []
}
}
},
async created() {
this.$set(this.pageForm, "courseId", "")
this.$set(this.pageForm, "unionId", "")
await this.queryCourseOptions("")
await this.loadUnionOptions()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,353 @@
<!--#
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="线路列表"></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 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}">
{{ 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="120" 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 }}人</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="160" 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" @click="exportLine(row)">导出</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</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 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: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "lineName",
pageOrderBy: "ascending",
year: currentYear,
lineName: "",
lineType: "",
unionId: ""
},
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.loadLineTypeOptions()
this.doSearch()
},
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"
},
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()
},
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
}
})
},
exportLine(row) {
if (!row || !row.matterId) {
this.$message.warning("线路信息不完整")
return
}
window.location.href = loc() + "/exportParticipants?matterId=" + encodeURIComponent(row.matterId)
},
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;
}
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,653 @@
<!--#
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.startYear"
type="year"
value-format="yyyy"
placeholder="请选择开始年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="结束年度">
<el-date-picker
v-model="pageForm.endYear"
type="year"
value-format="yyyy"
placeholder="请选择结束年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="线路">
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
</el-select>
</search-item>
<search-item label="出行时段">
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
</el-select>
</search-item>
<search-item label="线路类型">
<dict-select
v-model="pageForm.lineType"
code="lineType"
option_value="name"
placeholder="请选择线路类型">
</dict-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">
<div class="tour-ledger-toolbar">
<div class="tour-ledger-toolbar-title">
<table-tool :app="this" label="台账列表"></table-tool>
</div>
<div class="tour-ledger-actions">
<el-button size="medium" type="primary" icon="el-icon-upload2" @click="showImportDialog = true">参加人员导入</el-button>
<el-button size="medium" type="primary" icon="el-icon-check" @click="setParticipants">设置参加人员</el-button>
<el-button size="medium" type="primary" icon="el-icon-download" @click="exportUnionSignupZip">导出分工会报名压缩包</el-button>
</div>
<div class="tour-ledger-scope">
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.directFamilyOnly}"
@click="setDirectFamilyOnly">
参加直系亲属单位线路人员
</el-button>
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.overCostOnly}"
@click="setOverCostOnly">
申请超出费用由单位承担人员
</el-button>
</div>
</div>
<el-table
ref="tableRef"
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
row-key="id"
:row-class-name="tableRowClassName"
@selection-change="handleSelectionChange"
@sort-change="pageOrder">
<el-table-column type="selection" width="55" align="center" header-align="center"></el-table-column>
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="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="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}">
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="是否参加" prop="joined" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.joined ? 'success' : 'info'">{{ row.joined ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" 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="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
<div class="tour-ledger-tip">温馨提醒:走审核流程的报名,审核通过后才在台账中显示</div>
</el-card>
</guava>
<excel-import
ref="excelImportRef"
url="/platform/tour/ledger/importParticipants"
template_url="/platform/tour/ledger/downloadTemplate"
:visible.sync="showImportDialog"
title="参加人员导入"
width="700px"
@import-success="afterImport"
:extra_params="{}">
</excel-import>
<el-dialog
title="台账详情"
:visible.sync="detailVisible"
:close-on-click-modal="false"
width="72%">
<div class="tour-ledger-section">
<div class="tour-ledger-title">教职工信息</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="tour-ledger-section mt10">
<div class="tour-ledger-title">
报名信息
<el-link v-if="detailHasWorkflow()" type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="3" border size="medium">
<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 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>
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">{{ familyText() }}</el-descriptions-item>
<el-descriptions-item label="是否参加">{{ detail.joined ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="是否报销">{{ detail.reimbursed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="isDirectFamilyLine()" class="tour-ledger-section mt10">
<div class="tour-ledger-title">直系亲属线路</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="showFamilySection()" class="tour-ledger-section mt10">
<div class="tour-ledger-title">家属信息</div>
<el-table :data="familyData" border :size="tableSize" empty-text="暂无家属信息">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</div>
<template v-if="detailHasWorkflow()" v-for="task in doneTasks">
<div class="tour-ledger-section mt10" :key="task.id">
<div class="tour-ledger-title">{{ task.displayName }}</div>
<el-descriptions border :column="3" v-if="task.ext && task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border :column="3" v-else>
<el-descriptions-item label="办理用户">
{{ (task.taskFormData && task.taskFormData.userName) || '' }}({{ (task.taskFormData && task.taskFormData.loginName) || '' }})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
{{ (task.taskFormData && (task.taskFormData.opinion || task.taskFormData.tf_opinion)) || '' }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<snaker-chart ref="snakerChartRef"></snaker-chart>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="detailVisible = false">确定</el-button>
</span>
</el-dialog>
</div>
<style>
.tour-ledger-section {
padding: 0 2px;
}
.tour-ledger-title {
border-left: 4px solid #0079c2;
color: #0079c2;
font-size: 14px;
font-weight: 600;
line-height: 16px;
margin-bottom: 14px;
padding-left: 10px;
}
.tour-ledger-toolbar {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 24px;
margin-bottom: 12px;
white-space: nowrap;
}
.tour-ledger-toolbar-title {
flex: 0 0 auto;
}
.tour-ledger-actions {
display: flex;
align-items: center;
flex: 0 0 auto;
gap: 10px;
white-space: nowrap;
}
.tour-ledger-actions .el-button {
margin: 0;
}
.tour-ledger-scope {
display: flex;
align-items: center;
flex: 0 0 auto;
flex-wrap: nowrap;
gap: 12px;
margin: 0;
white-space: nowrap;
}
.tour-ledger-scope .el-button {
margin: 0;
}
.tour-ledger-scope .el-button + .el-button {
margin-left: 0;
}
.tour-ledger-scope .tour-scope-btn {
background: #ecf5ff;
border-color: #b3d8ff;
border-radius: 4px;
color: #0079c2;
font-weight: 600;
height: 34px;
line-height: 1;
padding: 8px 18px;
}
.tour-ledger-scope .tour-scope-btn:hover,
.tour-ledger-scope .tour-scope-btn:focus {
background: #d9ecff;
border-color: #66b1ff;
color: #006bb0;
}
.tour-ledger-scope .tour-scope-btn.is-active {
background: #0079c2;
border-color: #0079c2;
box-shadow: 0 2px 6px rgba(0, 121, 194, 0.24);
color: #fff;
}
.tour-ledger-scope .tour-scope-btn.is-active:hover,
.tour-ledger-scope .tour-scope-btn.is-active:focus {
background: #006bb0;
border-color: #006bb0;
color: #fff;
}
.tour-ledger-tip {
color: #f56c6c;
font-family: SimSun, "宋体", serif;
font-size: 9pt;
line-height: 1.6;
margin-top: -4px;
text-align: center;
}
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
const currentYear = moment().format("YYYY")
return {
detailVisible: false,
unionOptions: [],
lineOptions: [],
travelPeriodOptions: [],
detail: {},
familyData: [],
directRelative: {},
directFamilyUnitLine: false,
fillBedInfo: true,
detailRow: {},
doneTasks: [],
showImportDialog: false,
multipleSelection: [],
filterOptionsTimer: null,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
startYear: currentYear,
endYear: currentYear,
keyword: "",
unionId: "",
lineId: "",
travelPeriod: "",
lineType: "",
directFamilyOnly: false,
overCostOnly: false
}
}
},
methods: {
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
tableRowClassName({ row }) {
return this.isDirectFamilyRow(row) ? "direct-family-row" : ""
},
isDirectFamilyRow(row) {
return this.toBoolean(row && row.directFamilyUnitLine) || !!(row && row.directRelativeId)
},
handleSelectionChange(val) {
this.multipleSelection = val || []
},
clearTableSelection() {
this.multipleSelection = []
if (this.$refs.tableRef) {
this.$refs.tableRef.clearSelection()
}
},
setParticipants() {
if (!this.multipleSelection.length) {
this.$message.warning("请选择要设置的参加人员")
return
}
this.$confirm("确定将选中的" + this.multipleSelection.length + "条台账设置为已参加吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/setParticipants", {
ids: JSON.stringify(this.multipleSelection.map(item => item.id))
}).then((res) => {
if (res.code === 0) {
this.$message.success("设置成功")
this.clearTableSelection()
this.pageData()
} else {
this.$message.warning(res.msg || "设置失败")
}
})
})
},
exportUnionSignupZip() {
const params = new URLSearchParams()
;["startYear", "endYear", "keyword", "unionId", "lineId", "travelPeriod", "lineType"].forEach(key => {
const value = this.pageForm[key]
if (value !== undefined && value !== null && value !== "") {
params.append(key, value)
}
})
if (this.pageForm.directFamilyOnly) {
params.append("directFamilyOnly", "true")
}
if (this.pageForm.overCostOnly) {
params.append("overCostOnly", "true")
}
window.location.href = loc() + "/exportUnionSignupZip?" + params.toString()
},
afterImport() {
this.showImportDialog = false
this.clearTableSelection()
this.pageData()
this.loadFilterOptions()
},
isDirectFamilyLine() {
return this.directFamilyUnitLine
|| this.toBoolean(this.detail && this.detail.directFamilyUnitLine)
|| !!(this.directRelative && this.directRelative.id)
},
detailHasWorkflow() {
return !!(this.detailRow && this.detailRow.instanceId)
},
hasFamily() {
return this.toBoolean(this.detail && this.detail.hasFamily) || this.familyData.length > 0
},
familyText() {
if (this.isDirectFamilyLine()) {
return "否"
}
if (!this.hasFamily()) {
return "否"
}
const count = Number(this.familyData.length || 0)
return count > 0 ? count + "人" : "是"
},
showFamilySection() {
return this.hasFamily() && !this.isDirectFamilyLine()
},
loadDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.detailRow.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
openChart() {
if (!this.detailRow.instanceProcessDefineId || !this.detailRow.instanceId) {
this.$message.warning("暂无流程图信息")
return
}
this.$refs.snakerChartRef.onOpenFull(this.detailRow.instanceProcessDefineId, this.detailRow.instanceId)
},
resetSearch() {
this.pageForm.startYear = moment().format("YYYY")
this.pageForm.endYear = moment().format("YYYY")
this.pageForm.keyword = ""
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.pageForm.directFamilyOnly = false
this.pageForm.overCostOnly = false
this.doSearch()
},
openView(row) {
this.detailRow = row || {}
this.detail = {}
this.familyData = []
this.directRelative = {}
this.directFamilyUnitLine = false
this.fillBedInfo = true
this.doneTasks = []
this.$axios.post(loc() + "/detail", { id: row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.detailVisible = true
if (this.detailHasWorkflow()) {
this.loadDoneTasks()
}
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.userName + "】的报名台账吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
this.loadFilterOptions()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions").then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
}
})
},
loadFilterOptions() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
},
scheduleFilterOptions(delay) {
if (this.filterOptionsTimer) {
clearTimeout(this.filterOptionsTimer)
}
this.filterOptionsTimer = setTimeout(() => {
this.loadFilterOptions()
this.filterOptionsTimer = null
}, delay === undefined ? 80 : delay)
},
loadLineOptions() {
this.$axios.post(loc() + "/lineOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
directFamilyOnly: this.pageForm.directFamilyOnly,
overCostOnly: this.pageForm.overCostOnly
}).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
this.pageForm.lineId = ""
}
}
})
},
loadTravelPeriodOptions() {
this.$axios.post(loc() + "/travelPeriodOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
lineId: this.pageForm.lineId,
lineType: this.pageForm.lineType,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
directFamilyOnly: this.pageForm.directFamilyOnly,
overCostOnly: this.pageForm.overCostOnly
}).then((res) => {
if (res.code === 0) {
this.travelPeriodOptions = res.data || []
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
this.pageForm.travelPeriod = ""
}
}
})
},
doSearch() {
this.tableKey = new Date().getTime()
this.pageForm.pageNumber = 1
this.clearTableSelection()
this.pageData()
this.scheduleFilterOptions(0)
},
setDirectFamilyOnly() {
this.pageForm.directFamilyOnly = !this.pageForm.directFamilyOnly
this.pageForm.pageNumber = 1
this.clearTableSelection()
this.pageData()
this.scheduleFilterOptions(0)
},
setOverCostOnly() {
this.pageForm.overCostOnly = !this.pageForm.overCostOnly
this.pageForm.pageNumber = 1
this.clearTableSelection()
this.pageData()
this.scheduleFilterOptions(0)
}
},
mounted() {
this.loadUnionOptions()
this.pageData()
this.scheduleFilterOptions(150)
},
watch: {
"pageForm.startYear"() {
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.scheduleFilterOptions()
},
"pageForm.endYear"() {
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.scheduleFilterOptions()
},
"pageForm.lineId"() {
this.scheduleFilterOptions()
},
"pageForm.travelPeriod"() {
this.scheduleFilterOptions()
},
"pageForm.lineType"() {
this.scheduleFilterOptions()
},
"pageForm.unionId"() {
this.scheduleFilterOptions()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,712 @@
<!--#
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-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="事项名称">
<el-input
v-model="pageForm.matterName"
clearable
placeholder="请输入事项名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="组织形式">
<el-select v-model="pageForm.organizationType" clearable filterable placeholder="请选择组织形式" style="width: 100%">
<el-option v-for="item in organizationTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="事项列表">
<el-button @click="openAdd" size="medium" type="primary">
<i class="el-icon-plus"></i>
新增
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
: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="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="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">
<template slot-scope="{row}">
<el-switch
v-model="row.enabled"
active-text="启用"
inactive-text="禁用"
@change="toggleEnabled(row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="260" 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>
<el-button size="mini" type="danger" :loading="row.deleteLoading" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="54%"
@closed="destroyForm">
<el-form :model="formData" :rules="formRules" label-width="110px" ref="form" :disabled="viewMode">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年度" prop="year">
<el-date-picker
v-model="formData.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%"
@change="yearChange">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="事项状态" prop="enabled">
<el-radio-group v-model="formData.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="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>
</el-form-item>
<el-form-item label="疗休养配置" prop="settingId">
<el-select v-model="formData.settingId" clearable filterable placeholder="请选择配置" style="width: 100%">
<el-option v-for="item in settingOptions" :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="formData.organizationType" clearable filterable placeholder="请选择组织形式" style="width: 100%" @change="organizationTypeChange">
<el-option v-for="item in organizationTypeOptions" :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="formData.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%" :disabled="isUnionDisabled()">
<el-option v-for="item in unionOptions" :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="dialogVisible = false">取消</el-button>
<el-button v-if="!viewMode" type="primary" :loading="subDis" @click="doSubmit">保存</el-button>
</span>
</el-dialog>
<el-dialog
:title="batchTitle"
:visible.sync="batchFormVisible"
: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" clearable filterable placeholder="请选择线路" style="width: 100%" @change="lineChange">
<el-option v-for="item in lineOptions" :key="item.id" :label="lineOptionLabel(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="travelStartChange"></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="travelEndChange"></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="peopleChange"></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="peopleChange"></el-input-number>
</el-form-item>
</el-col>
</el-row>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="batchFormVisible = false">取消</el-button>
<el-button type="primary" :loading="batchSubDis" @click="submitBatch">确定</el-button>
</span>
</el-dialog>
</div>
<style>
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
const validateUnion = (rule, value, callback) => {
if (this.formData.organizationType === "schoolUnion" || value) {
callback()
} else {
callback(new Error("必填"))
}
}
const validateMobile = (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 validateMoney = (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 validatePeople = (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 validateBatchTime = (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,
subDis: false,
batchFormVisible: false,
batchSubDis: false,
batchTitle: "",
currentMatter: {},
batchForm: {},
lineOptions: [],
settingOptions: [],
unionOptions: [],
organizationTypeOptions: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: currentYear,
matterName: "",
unionId: "",
organizationType: ""
},
formData: {},
formRules: {
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
matterName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
settingId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
unionId: [{ validator: validateUnion, trigger: ["blur", "change"] }],
organizationType: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
},
batchRules: {
lineId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
signupStartTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
signupEndTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
travelStartTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
travelEndTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactPhone: [{ validator: validateMobile, trigger: ["blur", "change"] }],
minGroupPeople: [{ validator: validatePeople, trigger: ["blur", "change"] }],
maxGroupPeople: [{ validator: validatePeople, trigger: ["blur", "change"] }],
estimatedCost: [{ validator: validateMoney, trigger: ["blur", "change"] }]
}
}
},
watch: {
"pageForm.year"(year) {
this.loadSettings(year)
},
"batchForm.lineId"() {
if (this.batchFormVisible) {
this.applySelectedLineDefaults(true)
}
},
"batchForm.travelStartTime"() {
if (this.batchFormVisible) {
this.fillTravelEndByLot()
}
},
"batchForm.travelEndTime"() {
if (this.batchFormVisible) {
this.validateTravelFields()
}
}
},
methods: {
resetSearch() {
this.pageForm.year = moment().format("YYYY")
this.pageForm.matterName = ""
this.pageForm.unionId = ""
this.pageForm.organizationType = ""
this.loadSettings(this.pageForm.year)
this.doSearch()
},
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"
},
emptyForm() {
const user = this.currentUser()
return {
year: moment().format("YYYY"),
matterName: "",
settingId: "",
creatorUserId: user.id || "",
creatorName: user.username || "",
unionId: "",
organizationType: "",
enabled: true
}
},
openAdd() {
this.title = "新增事项信息"
this.viewMode = false
this.formData = this.emptyForm()
this.loadSettings(this.formData.year, true)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
openEdit(row) {
this.title = "编辑事项信息"
this.viewMode = false
this.loadDetail(row.id)
},
openView(row) {
this.title = "查看事项信息"
this.viewMode = true
this.loadDetail(row.id)
},
loadDetail(id) {
this.$axios.post(loc() + "/detail", { id }).then((res) => {
if (res.code === 0) {
this.formData = Object.assign(this.emptyForm(), res.data || {})
this.formData.year = this.formData.year ? String(this.formData.year) : ""
this.ensureSelectedUnionOption()
this.loadSettings(this.formData.year)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
currentUser() {
return (this.$store && this.$store.state && this.$store.state.user) || {}
},
currentUnion() {
return this.currentUser().union || {}
},
organizationTypeChange() {
this.applyOrganizationTypeRule()
this.$nextTick(() => this.$refs.form && this.$refs.form.validateField("unionId"))
},
applyOrganizationTypeRule() {
if (this.formData.organizationType === "schoolUnion") {
this.formData.unionId = ""
return
}
if (this.formData.organizationType === "branchUnion" || this.formData.organizationType === "personal") {
const union = this.currentUnion()
this.formData.unionId = union.id || ""
this.ensureCurrentUnionOption(union)
}
},
ensureCurrentUnionOption(union) {
if (!union || !union.id) return
const exists = this.unionOptions.some(item => item.id === union.id)
if (!exists) {
this.unionOptions.push({ id: union.id, name: union.name || "" })
}
},
ensureSelectedUnionOption() {
if (!this.formData.unionId) return
const exists = this.unionOptions.some(item => item.id === this.formData.unionId)
if (!exists) {
this.unionOptions.push({ id: this.formData.unionId, name: this.formData.unionName || "" })
}
},
isUnionDisabled() {
return ["schoolUnion", "branchUnion", "personal"].includes(this.formData.organizationType)
},
yearChange(year) {
this.formData.settingId = ""
this.loadSettings(year, true)
},
loadSettings(year, autoSelectLatest) {
this.$axios.post(loc() + "/settingOptions", { year }).then((res) => {
if (res.code === 0) {
this.settingOptions = res.data || []
if (autoSelectLatest && !this.formData.settingId && this.settingOptions.length > 0) {
this.formData.settingId = this.settingOptions[0].id
}
}
})
},
loadUnions() {
this.$axios.post(loc() + "/unionOptions").then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
}
})
},
loadOrganizationTypes() {
this.$axios.post(loc() + "/organizationTypeOptions").then((res) => {
if (res.code === 0) {
this.organizationTypeOptions = res.data || []
}
})
},
emptyBatchForm() {
return {
id: this.currentMatter.id || "",
lineId: "",
signupStartTime: "",
signupEndTime: "",
travelStartTime: "",
travelEndTime: "",
contactName: "",
contactPhone: "",
minGroupPeople: null,
maxGroupPeople: null,
estimatedCost: ""
}
},
openBatchForm(row) {
this.currentMatter = row
this.loadLines(row.year)
this.batchTitle = "选择线路"
this.batchForm = this.emptyBatchForm()
this.$axios.post(loc() + "/lineConfig", { matterId: row.id }).then((res) => {
if (res.code === 0) {
this.batchForm = Object.assign(this.emptyBatchForm(), res.data || {})
this.batchForm.travelStartTime = this.dateOnly(this.batchForm.travelStartTime)
this.batchForm.travelEndTime = this.dateOnly(this.batchForm.travelEndTime)
this.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.batchFormVisible = true
this.$nextTick(() => {
this.applySelectedLineDefaults(true)
this.$refs.batchFormRef && this.$refs.batchFormRef.clearValidate()
})
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
loadLines(year) {
this.$axios.post(loc() + "/lineOptions", { year }).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
this.applySelectedLineDefaults(true)
}
})
},
lineOptionLabel(item) {
if (!item) return ""
const lineName = this.lineField(item, "lineName") || ""
const lineType = this.lineField(item, "lineType") || ""
return lineType ? lineName + "" + lineType + "" : lineName
},
lineChange() {
this.applySelectedLineDefaults(true)
},
travelStartChange() {
this.fillTravelEndByLot()
},
travelEndChange() {
this.validateTravelFields()
},
selectedLine() {
return (this.lineOptions || []).find((item) => this.lineField(item, "id") === this.batchForm.lineId) || null
},
applySelectedLineDefaults(recalculateEnd) {
const line = this.selectedLine()
if (!line) return
const activityCost = this.lineField(line, "activityCost")
if (activityCost !== null && activityCost !== undefined && activityCost !== "") {
this.batchForm.estimatedCost = activityCost
}
if (recalculateEnd) {
this.fillTravelEndByLot()
}
},
fillTravelEndByLot() {
const line = this.selectedLine()
const days = this.lineLotDays(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.validateTravelFields()
},
lineLotDays(line) {
if (!line) return null
const lotDays = this.lineField(line, "lotDays") || this.lineField(line, "lotValue")
if (!/^\d+$/.test(String(lotDays || ""))) return null
const days = parseInt(lotDays, 10)
return days > 0 ? days : null
},
lineField(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
},
validateTravelFields() {
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["travelStartTime", "travelEndTime"])
}
})
},
dateOnly(value) {
if (!value) return ""
return String(value).substring(0, 10)
},
loadSettingPeople(callback) {
this.$axios.post(loc() + "/settingPeople", { matterId: this.currentMatter.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
callback && callback(data)
}
})
},
peopleChange() {
this.$nextTick(() => {
if (this.$refs.batchFormRef) {
this.$refs.batchFormRef.validateField(["minGroupPeople", "maxGroupPeople"])
}
})
},
submitBatch() {
this.$refs.batchFormRef.validate((valid) => {
if (!valid) return
this.batchSubDis = true
this.$axios.post(loc() + "/lineConfigDoSubmit", this.batchForm).then((res) => {
this.batchSubDis = false
if (res.code === 0) {
this.$message.success("保存成功")
this.batchFormVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.batchSubDis = false
})
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return
this.subDis = true
this.$axios.post(loc() + "/doSubmit", this.formData).then((res) => {
this.subDis = false
if (res.code === 0) {
this.$message.success("保存成功")
this.dialogVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.subDis = false
})
})
},
toggleEnabled(row) {
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
if (res.code === 0) {
this.$message.success("状态已更新")
} else {
row.enabled = !row.enabled
this.$message.warning(res.msg || "状态更新失败")
}
}).catch(() => {
row.enabled = !row.enabled
})
},
doDelete(row) {
this.$set(row, "deleteLoading", true)
this.$axios.post(loc() + "/deleteInfo", { id: row.id }).then((res) => {
this.$set(row, "deleteLoading", false)
if (res.code !== 0) {
this.$message.warning(res.msg || "删除检查失败")
return
}
const data = res.data || {}
const signupCount = Number(data.signupCount || 0)
const matterName = row.matterName || ""
if (!data.canDelete) {
this.$alert("事项【" + matterName + "】已有 " + signupCount + " 人报名,不能删除。", "删除提醒", {
confirmButtonText: "知道了",
type: "warning"
})
return
}
this.$confirm("事项【" + matterName + "】暂无人员报名,确认删除吗?", "删除提醒", {
confirmButtonText: "确认删除",
cancelButtonText: "取消",
type: "warning",
confirmButtonClass: "el-button--danger"
}).then(() => {
this.$set(row, "deleteLoading", true)
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((deleteRes) => {
this.$set(row, "deleteLoading", false)
if (deleteRes.code === 0) {
this.$message.success("删除成功")
this.pageData()
} else {
this.$message.warning(deleteRes.msg || "删除失败")
}
}).catch(() => {
this.$set(row, "deleteLoading", false)
})
}).catch(() => {})
}).catch(() => {
this.$set(row, "deleteLoading", false)
})
},
destroyForm() {
this.formData = {}
this.viewMode = false
}
},
mounted() {
this.loadSettings(this.pageForm.year)
this.loadUnions()
this.loadOrganizationTypes()
this.pageData()
}
})
</script>
<!--#
}
#-->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<el-empty description="${title!'功能建设中'}">
<template slot="description">
<span>${title!'功能建设中'}正在分阶段建设中</span>
</template>
</el-empty>
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app"
})
</script>
<!--#
}
#-->
@@ -0,0 +1,432 @@
<!--#
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.lotId" clearable filterable placeholder="请选择时间标段" style="width: 100%">
<el-option v-for="item in lotOptions" :key="item.id" :label="item.lotName" :value="item.id"></el-option>
</el-select>
</search-item>
<div class="search-query" style="margin-left: auto;">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="线路列表">
<el-button @click="openAdd" size="medium" type="primary">
<i class="el-icon-plus"></i>
新增
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
:default-sort="{prop: 'enabled', order: 'descending'}"
: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="openFlag" width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
v-model="row.openFlag"
active-text="是"
inactive-text="否"
@change="toggleOpenFlag(row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="激活状态" prop="enabled" width="160" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
v-model="row.enabled"
active-text="启用"
inactive-text="禁用"
@change="toggleEnabled(row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="260" 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="openEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="72%"
@closed="destroyEditor">
<el-form :model="formData" :rules="formRules" label-width="110px" ref="form" :disabled="viewMode">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="创建年度" prop="year">
<el-date-picker
v-model="formData.year"
type="year"
value-format="yyyy"
placeholder="请选择创建年度"
style="width: 100%"
@change="yearChange">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="旅行社名称" prop="travelAgencyId">
<el-select v-model="formData.travelAgencyId" clearable filterable placeholder="请选择旅行社" style="width: 100%">
<el-option v-for="item in travelAgencyOptions" :key="item.id" :label="item.agencyName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="线路名称" prop="lineName">
<el-input v-model="formData.lineName" maxlength="30" show-word-limit placeholder="请输入线路名称"></el-input>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="创建人">
<el-input v-model="formData.creatorName" readonly placeholder="当前登录人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所在单位">
<el-input v-model="formData.unitName" readonly placeholder="当前登录人所在单位"></el-input>
</el-form-item>
</el-col>
</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>
</el-col>
<el-col :span="12">
<el-form-item label="激活状态" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否对外开放" prop="openFlag">
<el-radio-group v-model="formData.openFlag">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="直系亲属线路" prop="directFamilyUnitLine">
<el-radio-group v-model="formData.directFamilyUnitLine">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="线路类型" prop="lineType">
<el-select v-model="formData.lineType" clearable placeholder="请选择线路类型" style="width: 100%">
<el-option v-for="item in lineTypeOptions" :key="item.code" :label="item.name" :value="item.name"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="时间标段" prop="lotId">
<el-select v-model="formData.lotId" clearable filterable placeholder="请选择时间标段" style="width: 100%">
<el-option v-for="item in lotOptions" :key="item.id" :label="item.lotName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="线路内容" prop="lineContent">
<text-editor v-model="formData.lineContent"></text-editor>
</el-form-item>
<el-form-item label="移动端缩略图" prop="mobileThumb">
<!-- 与旅行社管理保持一致:移动端缩略图使用单图上传并保存URL。 -->
<file-upload
style="--upload-width: 200px;--upload-height:108px"
:upload_number="1"
:upload_size="20971520"
:value.sync="formData.mobileThumb"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
<div class="el-upload__tip">支持jpg、jpeg、png格式,大小不超过20MB。</div>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button v-if="!viewMode" type="primary" :loading="submitLoading" @click="doSubmit">保存</el-button>
</span>
</el-dialog>
</div>
<style>
.el-table .direct-family-row > td,
.el-table .direct-family-row > td .cell {
color: #f56c6c;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
title: "",
dialogVisible: false,
viewMode: false,
submitLoading: false,
travelAgencyOptions: [],
lotOptions: [],
lineTypeOptions: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "enabled",
pageOrderBy: "descending",
year: currentYear,
lineName: "",
lotId: ""
},
formData: {},
formRules: {
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
travelAgencyId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lineName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lineCode: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lineType: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
lotId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
mobileThumb: [{ required: true, message: "请上传移动端缩略图", trigger: ["blur", "change"] }]
}
}
},
methods: {
resetSearch() {
this.pageForm.year = moment().format("YYYY")
this.pageForm.lineName = ""
this.pageForm.lotId = ""
this.loadOptions(this.pageForm.year)
this.doSearch()
},
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"
},
emptyForm() {
const user = (this.$store && this.$store.state && this.$store.state.user) || {}
return {
year: moment().format("YYYY"),
travelAgencyId: "",
creatorUserId: user.id || "",
creatorName: user.username || "",
unitId: user.unit?.id || "",
unitName: user.unit?.name || "",
lineName: "",
lineCode: "",
enabled: true,
openFlag: true,
directFamilyUnitLine: false,
lineType: "",
lotId: "",
lineContent: "",
mobileThumb: ""
}
},
openAdd() {
this.title = "新增线路信息"
this.viewMode = false
this.formData = this.emptyForm()
this.loadOptions(this.formData.year)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
openEdit(row) {
this.title = "编辑线路信息"
this.viewMode = false
this.loadDetail(row.id)
},
openView(row) {
this.title = "查看线路信息"
this.viewMode = true
this.loadDetail(row.id)
},
loadDetail(id) {
this.$axios.post(loc() + "/detail", { id }).then((res) => {
if (res.code === 0) {
this.formData = Object.assign(this.emptyForm(), res.data || {})
this.formData.year = this.formData.year ? String(this.formData.year) : ""
this.loadOptions(this.formData.year)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
yearChange(year) {
this.formData.travelAgencyId = ""
this.formData.lotId = ""
this.loadOptions(year)
},
loadOptions(year) {
this.loadTravelAgencies(year)
this.loadLots(year)
this.loadLineTypes()
},
loadTravelAgencies(year) {
this.$axios.post(loc() + "/travelAgencyOptions", { year }).then((res) => {
if (res.code === 0) {
this.travelAgencyOptions = res.data || []
}
})
},
loadLots(year) {
this.$axios.post(loc() + "/lotOptions", { year }).then((res) => {
if (res.code === 0) {
this.lotOptions = res.data || []
}
})
},
loadLineTypes() {
this.$axios.post(loc() + "/lineTypeOptions").then((res) => {
if (res.code === 0) {
this.lineTypeOptions = res.data || []
}
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return
this.submitLoading = true
this.$axios.post(loc() + "/doSubmit", this.formData).then((res) => {
this.submitLoading = false
if (res.code === 0) {
this.$message.success("保存成功")
this.dialogVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.submitLoading = false
})
})
},
toggleEnabled(row) {
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
if (res.code === 0) {
this.$message.success("状态已更新")
} else {
row.enabled = !row.enabled
this.$message.warning(res.msg || "状态更新失败")
}
}).catch(() => {
row.enabled = !row.enabled
})
},
toggleOpenFlag(row) {
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
if (res.code === 0) {
this.$message.success("对外开放状态已更新")
} else {
row.openFlag = !row.openFlag
this.$message.warning(res.msg || "对外开放状态更新失败")
}
}).catch(() => {
row.openFlag = !row.openFlag
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.lineName + "】吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
destroyEditor() {
this.formData = {}
this.viewMode = false
}
},
mounted() {
this.loadOptions(this.pageForm.year)
this.pageData()
},
watch: {
"pageForm.year"(year) {
this.pageForm.lotId = ""
this.loadOptions(year)
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,547 @@
<!--#
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.startYear"
type="year"
value-format="yyyy"
placeholder="请选择开始年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="结束年度">
<el-date-picker
v-model="pageForm.endYear"
type="year"
value-format="yyyy"
placeholder="请选择结束年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="线路">
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
</el-select>
</search-item>
<search-item label="出行时段">
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
</el-select>
</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>
<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.audit" @change="changeAudit">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="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="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="220" 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="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}">
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="当前节点" prop="curTaskName" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="流程状态" prop="instanceState" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button v-if="row.taskState === 10" size="mini" type="primary" @click="openApproval(row)">审核</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<tour-approval-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{ formData.taskName }}
</div>
<el-form
:model="formData"
ref="formRef"
label-width="0"
label-suffix=""
class="flow-task-form">
<el-form-item
label="审批意见"
prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到发起人</el-button>
<el-button @click="handleTaskAction(2)" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意申请</el-button>
</el-row>
</div>
</tour-approval-info>
</template>
</guava>
</div>
<style>
#app .search .search-item {
width: calc((100% - 150px) / 4);
}
#app .search .search-query {
margin-left: auto;
}
@media screen and (max-width: 1350px) {
#app .search .search-item {
width: calc((100% - 100px) / 3);
}
}
@media screen and (max-width: 1200px) {
#app .search .search-item {
width: calc((100% - 50px) / 2);
}
}
@media screen and (max-width: 992px) {
#app .search .search-item {
width: 100%;
}
}
.tour-approval-section {
margin-bottom: 14px;
}
.tour-approval-table {
width: 100%;
}
.tour-approval-empty {
color: #909399;
}
</style>
<script nonce="${cspNonce!}">
const TOUR_APPROVAL_INFO = {
template: /*language=HTML*/ `
<div>
<div class="process-title">
报名信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="3" border class="tour-approval-section">
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
<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 label="是否携带家属">
{{ familyText() }}
</el-descriptions-item>
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</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>
</el-descriptions>
<div v-if="isDirectFamilyLine()" class="tour-approval-section">
<div class="process-title">直系亲属线路</div>
<el-descriptions :column="3" border>
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="showFamilySection()" class="tour-approval-section">
<div class="process-title">亲属信息</div>
<el-table :data="familyData" border size="mini" empty-text="暂无亲属信息" class="tour-approval-table">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</div>
<template v-for="task in doneTasks">
<div class="mt10">
<div class="process-title">{{ task.displayName }}</div>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<el-descriptions-item label="办理用户">
{{ task.taskFormData.userName }}({{ task.taskFormData.loginName }})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<slot></slot>
<snaker-chart ref="snakerChartRef"></snaker-chart>
</div>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
row: {},
detail: {},
familyData: [],
directRelative: {},
allowFamily: false,
fillBedInfo: true,
directFamilyUnitLine: false,
doneTasks: []
}
},
methods: {
onOpen(row) {
this.row = row || {}
this.detail = {}
this.familyData = []
this.directRelative = {}
this.allowFamily = false
this.fillBedInfo = true
this.directFamilyUnitLine = false
this.doneTasks = []
this.info()
this.getDoneTasks()
},
info() {
this.$axios.post(loc() + "/detail", { id: this.row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.allowFamily = this.toBoolean(data.allowFamily)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
toBoolean(value) {
return value === true || value === 1 || value === "1"
},
isDirectFamilyLine() {
return this.directFamilyUnitLine
|| this.toBoolean(this.detail.directFamilyUnitLine)
|| (this.directRelative && this.directRelative.id)
},
hasFamily() {
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
},
showFamilySection() {
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
},
familyText() {
if (this.isDirectFamilyLine()) {
return "否"
}
if (!this.allowFamily) {
return "否"
}
return this.hasFamily() ? "是" : "否"
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
openChart() {
if (!this.row.instanceProcessDefineId || !this.row.instanceId) {
this.$message.warning("暂无流程图信息")
return
}
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
}
}
}
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
unionOptions: [],
lineOptions: [],
travelPeriodOptions: [],
lineTypeOptions: [],
showApprovalForm: false,
formData: {
tf_opinion: ""
},
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
audit: false,
startYear: currentYear,
endYear: currentYear,
keyword: "",
unionId: "",
lineId: "",
travelPeriod: "",
lineType: ""
}
}
},
methods: {
resetSearch() {
this.pageForm.startYear = moment().format("YYYY")
this.pageForm.endYear = moment().format("YYYY")
this.pageForm.keyword = ""
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.loadOptions()
this.doSearch()
},
changeAudit() {
this.pageForm.pageNumber = 1
this.clearCascadeFilters()
this.loadOptions()
this.pageData()
},
clearCascadeFilters() {
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
},
openView(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = false
this.formData = {
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
})
},
openApproval(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName || row.taskName,
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
})
},
handleTaskAction(val) {
this.$refs.formRef.validate((valid) => {
if (!valid) return
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const loading = createLoading()
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg || "操作成功")
this.doSearch()
} else {
this.$message.warning(res.msg || "操作失败")
}
}).finally(() => {
loading.close()
})
}).catch(() => {})
})
},
loadOptions() {
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
},
baseOptionParams() {
return {
audit: this.pageForm.audit,
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
keyword: this.pageForm.keyword,
unionId: this.pageForm.unionId,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType
}
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
if (this.pageForm.unionId && !this.unionOptions.some(item => item.id === this.pageForm.unionId)) {
this.pageForm.unionId = ""
}
}
})
},
loadLineOptions() {
this.$axios.post(loc() + "/lineOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
this.pageForm.lineId = ""
}
}
})
},
loadTravelPeriodOptions() {
this.$axios.post(loc() + "/travelPeriodOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.travelPeriodOptions = res.data || []
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
this.pageForm.travelPeriod = ""
}
}
})
},
loadLineTypeOptions() {
this.$axios.post(loc() + "/lineTypeOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.lineTypeOptions = res.data || []
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.lineType === this.pageForm.lineType)) {
this.pageForm.lineType = ""
}
}
})
}
},
mounted() {
this.loadOptions()
this.pageData()
},
components: {
"tour-approval-info": TOUR_APPROVAL_INFO
},
watch: {
"pageForm.startYear"() {
this.clearCascadeFilters()
this.loadOptions()
},
"pageForm.endYear"() {
this.clearCascadeFilters()
this.loadOptions()
},
"pageForm.lineId"() {
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
},
"pageForm.travelPeriod"() {
this.loadLineOptions()
this.loadLineTypeOptions()
},
"pageForm.lineType"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
},
"pageForm.unionId"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,634 @@
<!--#
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.configName"
clearable
placeholder="请输入疗休养配置名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="疗休养配置">
<el-button type="primary" size="medium" @click="openAdd">
<i class="el-icon-plus"></i>
新建
</el-button>
</table-tool>
<el-table
:data="tableData"
:size="tableSize"
border
class="vi-table"
ref="table"
row-key="id"
v-loading="tableLoading"
@sort-change="pageOrder"
>
<el-table-column label="序号" type="index" :index="indexMethod" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="年度" prop="year" width="100" align="center" header-align="center"></el-table-column>
<el-table-column label="疗休养配置名称" prop="configName" min-width="260" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="周期开始年度" prop="cycleStartYear" width="130" align="center" header-align="center"></el-table-column>
<el-table-column label="最少成团人数" prop="minGroupPeople" width="120" align="center" header-align="center"></el-table-column>
<el-table-column label="最多成团人数" prop="maxGroupPeople" width="120" align="center" header-align="center"></el-table-column>
<el-table-column label="是否携带家属" prop="allowFamily" width="120" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" type="success" v-if="row.allowFamily">允许</el-tag>
<el-tag size="mini" type="info" v-else>不允许</el-tag>
</template>
</el-table-column>
<el-table-column label="是否启用" prop="enabled" width="100" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" type="success" v-if="row.enabled">启用</el-tag>
<el-tag size="mini" type="info" v-else>停用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" header-align="center">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="64.8%"
@closed="destroyEditor">
<el-form :model="formData" :rules="formRules" label-width="120px" ref="form">
<div v-if="title === '新建疗休养配置'" class="tour-setting-inherit">
<el-button type="primary" plain size="medium" icon="el-icon-copy-document" :loading="inheritLoading" @click="inheritPreviousYearInfo">延用上一年信息</el-button>
</div>
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="基本信息" name="basic">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年度" prop="year">
<el-date-picker
v-model="formData.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="配置名称" prop="configName">
<el-input v-model="formData.configName" maxlength="100" placeholder="请输入配置名称"></el-input>
</el-form-item>
</el-col>
<el-col :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>
<el-option label="省外疗休养" value="省外疗休养"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="可参加人员" prop="activityGroupId">
<div class="tour-user-scope">
<el-select
v-model="formData.activityGroupId"
placeholder="请选择可参加人员"
filterable
clearable
style="width: 100%">
<el-option
v-for="item in activityGroupList"
:key="item.groupId"
:label="item.groupName"
:value="item.groupId">
</el-option>
</el-select>
<el-button type="primary" @click="openUserScope">设置</el-button>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最少成团人数" prop="minGroupPeople">
<el-input-number v-model="formData.minGroupPeople" :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="maxGroupPeople">
<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="24">
<div class="tour-setting-basic-divider"></div>
</el-col>
<el-col :span="12">
<el-form-item label="省外几年去一次" prop="outProvinceYears">
<el-input-number v-model="formData.outProvinceYears" :controls="false" :min="0" :precision="0" placeholder="请输入省外间隔年限" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="省外人数占比" prop="outProvinceRatio">
<el-input-number v-model="formData.outProvinceRatio" :controls="false" :min="0" :max="100" :precision="2" placeholder="请输入省外人数占比" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="省外占比类型" prop="outProvinceRatioType">
<el-select v-model="formData.outProvinceRatioType" placeholder="请选择省外占比类型" style="width: 100%" @change="outProvinceRatioTypeChange">
<el-option label="当年报名人数" value="当年报名人数"></el-option>
<el-option label="可参加教职工人数" value="可参加教职工人数"></el-option>
<el-option label="固定人数" value="固定人数"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col v-if="formData.outProvinceRatioType === '固定人数'" :span="12">
<el-form-item label="固定人数" prop="outProvinceFixedPeople">
<el-input-number v-model="formData.outProvinceFixedPeople" :controls="false" :min="0" :precision="0" placeholder="请输入固定人数" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="tour-setting-basic-divider"></div>
</el-col>
<el-col :span="12">
<el-form-item label="周期开始年度" prop="cycleStartYear">
<el-date-picker
v-model="formData.cycleStartYear"
type="year"
value-format="yyyy"
clearable
placeholder="请选择周期开始年度"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="周期结束年度" prop="cycleEndYear">
<el-date-picker
v-model="formData.cycleEndYear"
type="year"
value-format="yyyy"
clearable
placeholder="请选择周期结束年度"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="周期内总费用" prop="cycleTotalCost">
<el-input-number v-model="formData.cycleTotalCost" :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="cycleAllowedTimes">
<el-input-number v-model="formData.cycleAllowedTimes" :controls="false" :min="0" :precision="0" placeholder="请输入周期允许次数" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="tour-setting-basic-divider"></div>
</el-col>
<el-col :span="12">
<el-form-item label="允许携带家属" prop="allowFamily">
<el-switch v-model="formData.allowFamily" active-text="允许" inactive-text="不允许"></el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="填报床位信息" prop="fillBedInfo">
<el-switch v-model="formData.fillBedInfo" active-text="是" inactive-text="否"></el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否启用" prop="enabled">
<el-switch v-model="formData.enabled" active-text="启用" inactive-text="停用"></el-switch>
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="标段管理" name="lots">
<div style="text-align: right; margin-bottom: 10px;">
<el-button type="primary" size="medium" icon="el-icon-plus" @click="addLot">增加一条</el-button>
</div>
<el-table
:data="formData.lots"
border
class="vi-table"
empty-text="暂无标段"
style="width: 100%">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="标段名称" min-width="180" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.lotName" maxlength="50" placeholder="请输入标段名称"></el-input>
</template>
</el-table-column>
<el-table-column label="标段值" min-width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.lotValue" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'lotValue', $event)"></el-input>
</template>
</el-table-column>
<el-table-column label="标段费用" min-width="150" align="center" header-align="center">
<template slot-scope="{row}">
<el-input v-model="row.activityCost" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'activityCost', $event)"></el-input>
</template>
</el-table-column>
<el-table-column label="允许超出报销" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-checkbox v-model="row.allowOverReimbursement"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" header-align="center">
<template slot-scope="scope">
<el-button type="danger" size="mini" icon="el-icon-delete" @click="deleteLot(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="服务须知" name="notice">
<el-form-item prop="serviceNotice" label-width="0">
<text-editor v-model="formData.serviceNotice"></text-editor>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="doSubmit">确认</el-button>
</span>
</el-dialog>
<drawer-user-scope
ref="drawerUserScope"
:group_id.sync="formData.activityGroupId"
@group_change="handleActivityGroupChange">
</drawer-user-scope>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
// 前端先做成团人数区间校验,后端也会再兜底校验一次。
const checkGroupPeople = (rule, value, callback) => {
if (this.formData.minGroupPeople !== null && this.formData.maxGroupPeople !== null
&& this.formData.minGroupPeople > this.formData.maxGroupPeople) {
callback(new Error("最少成团人数不能大于最多成团人数"))
} else {
callback()
}
}
const checkCycleYear = (rule, value, callback) => {
const startYear = parseInt(this.formData.cycleStartYear, 10)
const endYear = parseInt(this.formData.cycleEndYear, 10)
if (!isNaN(startYear) && !isNaN(endYear) && startYear > endYear) {
callback(new Error("周期开始年度不能大于周期结束年度"))
} else {
callback()
}
}
const checkOutProvinceFixedPeople = (rule, value, callback) => {
if (this.formData.outProvinceRatioType === "固定人数"
&& (value === null || value === undefined || value === "" || value < 0)) {
callback(new Error("固定人数必须大于等于0"))
} else {
callback()
}
}
return {
title: "",
dialogVisible: false,
activeTab: "basic",
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: "",
configName: ""
},
activityGroupList: [],
formData: {},
lotDeleteList: [],
inheritLoading: false,
formRules: {
year: [{required: true, message: "必填", trigger: ["blur", "change"]}],
configName: [{required: true, message: "必填", trigger: ["blur", "change"]}],
minGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
maxGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
outProvinceRatioType: [{required: true, message: "必填", trigger: ["blur", "change"]}],
outProvinceFixedPeople: [{validator: checkOutProvinceFixedPeople, trigger: ["blur", "change"]}],
cycleStartYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}],
cycleEndYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}]
}
}
},
components: {
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue")
},
methods: {
async getActivityGroup() {
const {data} = await this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup")
this.activityGroupList = (data || []).map((item) => {
return Object.assign({}, item, {
groupId: item.groupId === null || item.groupId === undefined ? "" : String(item.groupId)
})
})
},
openUserScope() {
if (this.$refs.drawerUserScope) {
this.$refs.drawerUserScope.groupId = this.formData.activityGroupId || ""
this.$refs.drawerUserScope.userScopeDialog = true
}
},
async handleActivityGroupChange() {
if (this.formData.activityGroupId !== null && this.formData.activityGroupId !== undefined) {
this.formData.activityGroupId = String(this.formData.activityGroupId)
}
await this.getActivityGroup()
},
resetSearch() {
this.pageForm.year = ""
this.pageForm.configName = ""
this.doSearch()
},
defaultFormData() {
return {
year: moment().format("YYYY"),
configName: "",
tourType: "",
activityGroupId: "",
sortNo: 0,
minGroupPeople: 0,
maxGroupPeople: 0,
outProvinceYears: 0,
outProvinceRatio: 0,
outProvinceRatioType: "当年报名人数",
outProvinceFixedPeople: 0,
cycleStartYear: "",
cycleEndYear: "",
cycleTotalCost: null,
cycleAllowedTimes: null,
allowFamily: false,
fillBedInfo: true,
enabled: true,
lots: [],
serviceNotice: ""
}
},
openAdd() {
this.title = "新建疗休养配置"
this.lotDeleteList = []
this.activeTab = "basic"
// 新建时给出默认年度和开关值,减少校工会管理员录入成本。
this.formData = this.defaultFormData()
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
inheritPreviousYearInfo() {
const currentYear = parseInt(this.formData.year, 10)
if (isNaN(currentYear)) {
this.$message.warning("请先选择年度")
return
}
this.inheritLoading = true
this.$axios.post(loc() + "/previousYearInfo", {year: currentYear}).then((res) => {
this.inheritLoading = false
if (res.code !== 0) {
this.$message.warning(res.msg || "未找到上一年度配置")
return
}
const previous = res.data || {}
const inheritedLots = (previous.lots || []).map(item => {
return {
lotName: item.lotName || "",
lotValue: item.lotValue || "",
activityCost: item.activityCost,
allowOverReimbursement: !!item.allowOverReimbursement
}
})
this.formData = Object.assign(this.defaultFormData(), previous, {
year: String(currentYear),
cycleStartYear: previous.cycleStartYear ? String(previous.cycleStartYear) : "",
cycleEndYear: previous.cycleEndYear ? String(previous.cycleEndYear) : "",
lots: inheritedLots
})
delete this.formData.id
delete this.formData.createdAt
delete this.formData.createdBy
delete this.formData.updatedAt
delete this.formData.updatedBy
delete this.formData.delFlag
if (!this.formData.outProvinceRatioType) {
this.$set(this.formData, "outProvinceRatioType", "当年报名人数")
}
if (this.formData.outProvinceFixedPeople === null || this.formData.outProvinceFixedPeople === undefined) {
this.$set(this.formData, "outProvinceFixedPeople", 0)
}
if (this.formData.fillBedInfo === null || this.formData.fillBedInfo === undefined) {
this.$set(this.formData, "fillBedInfo", true)
}
this.lotDeleteList = []
this.$message.success("已延用上一年配置信息")
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
}).catch(() => {
this.inheritLoading = false
})
},
openEdit(row) {
this.title = "编辑疗休养配置"
this.lotDeleteList = []
this.activeTab = "basic"
this.$axios.post(loc() + "/detail", {id: row.id}).then((res) => {
if (res.code === 0) {
this.formData = Object.assign({
activityGroupId: "",
outProvinceRatioType: "当年报名人数",
outProvinceFixedPeople: 0,
cycleStartYear: "",
cycleEndYear: "",
cycleTotalCost: null,
cycleAllowedTimes: null,
allowFamily: false,
fillBedInfo: true,
enabled: true,
lots: [],
serviceNotice: ""
}, res.data || {})
this.formData.year = this.formData.year ? String(this.formData.year) : ""
if (!this.formData.outProvinceRatioType) {
this.$set(this.formData, "outProvinceRatioType", "当年报名人数")
}
if (this.formData.outProvinceFixedPeople === null || this.formData.outProvinceFixedPeople === undefined) {
this.$set(this.formData, "outProvinceFixedPeople", 0)
}
if (this.formData.fillBedInfo === null || this.formData.fillBedInfo === undefined) {
this.$set(this.formData, "fillBedInfo", true)
}
this.formData.cycleStartYear = this.formData.cycleStartYear ? String(this.formData.cycleStartYear) : ""
this.formData.cycleEndYear = this.formData.cycleEndYear ? String(this.formData.cycleEndYear) : ""
this.formData.lots = (this.formData.lots || []).map(item => Object.assign({}, item, {
allowOverReimbursement: !!item.allowOverReimbursement
}))
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return
if (!this.validateLots()) return
this.submitLoading = true
// Nutz 对子表集合按字符串化 JSON 绑定更稳定,和体检项目维护的提交方式保持一致。
const submitData = JSON.parse(JSON.stringify(this.formData))
if (submitData.outProvinceRatioType !== "固定人数") {
submitData.outProvinceFixedPeople = 0
}
submitData.lots = JSON.stringify(this.formData.lots || [])
submitData.lotDeleteList = JSON.stringify(this.lotDeleteList)
this.$axios.post(loc() + "/doSubmit", submitData).then((res) => {
this.submitLoading = false
if (res.code === 0) {
this.$message.success("保存成功")
this.dialogVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.submitLoading = false
})
})
},
addLot() {
if (!this.formData.lots) {
this.$set(this.formData, "lots", [])
}
this.formData.lots.push({
lotName: "",
lotValue: "",
activityCost: null,
allowOverReimbursement: false
})
},
integerInput(row, field, value) {
const nextValue = String(value || "").replace(/[^\d]/g, "")
this.$set(row, field, nextValue)
},
outProvinceRatioTypeChange(value) {
if (value !== "固定人数") {
this.$set(this.formData, "outProvinceFixedPeople", 0)
}
this.$nextTick(() => this.$refs.form && this.$refs.form.validateField("outProvinceFixedPeople"))
},
validateLots() {
const lots = this.formData.lots || []
for (let i = 0; i < lots.length; i++) {
const row = lots[i]
const hasContent = row && (row.lotName || row.lotValue || row.activityCost !== null && row.activityCost !== undefined && row.activityCost !== "")
if (!hasContent) continue
if (!/^\d+$/.test(String(row.lotValue || ""))) {
this.$message.warning("第" + (i + 1) + "行标段值必须为整数")
return false
}
if (!/^\d+$/.test(String(row.activityCost === null || row.activityCost === undefined ? "" : row.activityCost))) {
this.$message.warning("第" + (i + 1) + "行标段费用必须为整数")
return false
}
}
return true
},
deleteLot(index, row) {
this.$confirm("确定删除该标段吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
if (row && row.id) {
this.lotDeleteList.push(row.id)
}
this.formData.lots.splice(index, 1)
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.configName + "】吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", {id: row.id}).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
destroyEditor() {
this.formData = {}
this.lotDeleteList = []
this.inheritLoading = false
this.activeTab = "basic"
}
},
mounted() {
this.getActivityGroup()
this.pageData()
}
})
</script>
<style>
.tour-user-scope {
display: flex;
align-items: center;
gap: 10px;
}
.tour-user-scope .el-select {
flex: 1;
}
.tour-setting-inherit {
margin-bottom: -40px;
padding-right: 96px;
position: relative;
text-align: right;
z-index: 1;
}
.tour-setting-basic-divider {
border-top: 1px dashed #dcdfe6;
margin: 2px 0 18px;
}
</style>
<!--#
}
#-->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,338 @@
<!--#
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.agencyName"
clearable
placeholder="请输入旅行社名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="联系人">
<el-input
v-model="pageForm.contactName"
clearable
placeholder="请输入联系人"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="联系电话">
<el-input
v-model="pageForm.contactPhone"
clearable
placeholder="请输入联系电话"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</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-button @click="openAdd" size="medium" type="primary">
<i class="el-icon-plus"></i>
新增
</el-button>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="年度" prop="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="enabled" width="160" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch
v-model="row.enabled"
active-text="启用"
inactive-text="禁用"
@change="toggleEnabled(row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="260" 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="openEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="72%"
@closed="destroyForm">
<el-form :model="formData" :rules="formRules" label-width="110px" ref="form" :disabled="viewMode">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年度" prop="year">
<el-date-picker
v-model="formData.year"
type="year"
value-format="yyyy"
placeholder="请选择年度"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="旅行社名称" prop="agencyName">
<el-input v-model="formData.agencyName" maxlength="30" show-word-limit placeholder="请输入旅行社名称"></el-input>
</el-form-item>
</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">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系人" prop="contactName">
<el-input v-model="formData.contactName" maxlength="10" show-word-limit placeholder="请输入联系人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人手机" prop="contactPhone">
<el-input v-model="formData.contactPhone" maxlength="30" placeholder="请输入联系人手机"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" maxlength="100" placeholder="请输入邮箱"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" maxlength="100" :rows="3" show-word-limit placeholder="请输入备注"></el-input>
</el-form-item>
<el-form-item label="移动端缩略图" prop="mobileThumb">
<!-- 复用课程管理的封面上传逻辑:单图上传,保存URL,供移动端列表展示使用。 -->
<file-upload
style="--upload-width: 200px;--upload-height:108px"
:upload_number="1"
:upload_size="20971520"
:value.sync="formData.mobileThumb"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
<div class="el-upload__tip">支持jpg、jpeg、png格式,大小不超过20MB。</div>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button v-if="!viewMode" type="primary" :loading="subDis" @click="doSubmit">保存</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
const validateMobile = (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 validateEmail = (rule, value, callback) => {
const emailReg = /^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$/
if (!value) {
callback(new Error("必填"))
} else if (!emailReg.test(value)) {
callback(new Error("邮箱格式不正确"))
} else {
callback()
}
}
return {
title: "",
dialogVisible: false,
viewMode: false,
subDis: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: currentYear,
agencyName: "",
contactName: "",
contactPhone: ""
},
formData: {},
formRules: {
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
agencyName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
agencyCode: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
contactPhone: [{ validator: validateMobile, trigger: ["blur", "change"] }],
email: [{ validator: validateEmail, trigger: ["blur", "change"] }]
}
}
},
methods: {
resetSearch() {
this.pageForm.year = moment().format("YYYY")
this.pageForm.agencyName = ""
this.pageForm.contactName = ""
this.pageForm.contactPhone = ""
this.doSearch()
},
emptyForm() {
return {
year: moment().format("YYYY"),
agencyName: "",
agencyCode: "",
contactName: "",
contactPhone: "",
email: "",
remark: "",
mobileThumb: "",
enabled: true
}
},
openAdd() {
this.title = "新增旅行社信息"
this.viewMode = false
this.formData = this.emptyForm()
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
openEdit(row) {
this.title = "编辑旅行社信息"
this.viewMode = false
this.loadDetail(row.id)
},
openView(row) {
this.title = "查看旅行社信息"
this.viewMode = true
this.loadDetail(row.id)
},
loadDetail(id) {
this.$axios.post(loc() + "/detail", { id }).then((res) => {
if (res.code === 0) {
this.formData = Object.assign(this.emptyForm(), res.data || {})
this.formData.year = this.formData.year ? String(this.formData.year) : ""
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return
this.subDis = true
this.$axios.post(loc() + "/doSubmit", this.formData).then((res) => {
this.subDis = false
if (res.code === 0) {
this.$message.success("保存成功")
this.dialogVisible = false
this.pageData()
} else {
this.$message.warning(res.msg || "保存失败")
}
}).catch(() => {
this.subDis = false
})
})
},
toggleEnabled(row) {
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
if (res.code === 0) {
this.$message.success("状态已更新")
} else {
row.enabled = !row.enabled
this.$message.warning(res.msg || "状态更新失败")
}
}).catch(() => {
row.enabled = !row.enabled
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.agencyName + "】吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
destroyForm() {
this.formData = {}
this.viewMode = false
}
},
mounted() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,547 @@
<!--#
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.startYear"
type="year"
value-format="yyyy"
placeholder="请选择开始年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="结束年度">
<el-date-picker
v-model="pageForm.endYear"
type="year"
value-format="yyyy"
placeholder="请选择结束年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="线路">
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
</el-select>
</search-item>
<search-item label="出行时段">
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
</el-select>
</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>
<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.audit" @change="changeAudit">
<el-radio-button :label="true">已审核</el-radio-button>
<el-radio-button :label="false">未审核</el-radio-button>
</el-radio-group>
</table-tool>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="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="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="220" 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="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}">
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="当前节点" prop="curTaskName" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="流程状态" prop="instanceState" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
<el-button v-if="row.taskState === 10" size="mini" type="primary" @click="openApproval(row)">审核</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<tour-approval-info ref="infoRef">
<div v-if="showApprovalForm">
<div class="process-title">
{{ formData.taskName }}
</div>
<el-form
:model="formData"
ref="formRef"
label-width="0"
label-suffix=""
class="flow-task-form">
<el-form-item
label="审批意见"
prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到发起人</el-button>
<el-button @click="handleTaskAction(2)" size="small" type="danger">拒绝申请</el-button>
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意申请</el-button>
</el-row>
</div>
</tour-approval-info>
</template>
</guava>
</div>
<style>
#app .search .search-item {
width: calc((100% - 150px) / 4);
}
#app .search .search-query {
margin-left: auto;
}
@media screen and (max-width: 1350px) {
#app .search .search-item {
width: calc((100% - 100px) / 3);
}
}
@media screen and (max-width: 1200px) {
#app .search .search-item {
width: calc((100% - 50px) / 2);
}
}
@media screen and (max-width: 992px) {
#app .search .search-item {
width: 100%;
}
}
.tour-approval-section {
margin-bottom: 14px;
}
.tour-approval-table {
width: 100%;
}
.tour-approval-empty {
color: #909399;
}
</style>
<script nonce="${cspNonce!}">
const TOUR_APPROVAL_INFO = {
template: /*language=HTML*/ `
<div>
<div class="process-title">
报名信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="3" border class="tour-approval-section">
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
<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 label="是否携带家属">
{{ familyText() }}
</el-descriptions-item>
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</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>
</el-descriptions>
<div v-if="isDirectFamilyLine()" class="tour-approval-section">
<div class="process-title">直系亲属线路</div>
<el-descriptions :column="3" border>
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="showFamilySection()" class="tour-approval-section">
<div class="process-title">亲属信息</div>
<el-table :data="familyData" border size="mini" empty-text="暂无亲属信息" class="tour-approval-table">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</div>
<template v-for="task in doneTasks">
<div class="mt10">
<div class="process-title">{{ task.displayName }}</div>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
<el-descriptions-item label="办理用户">
{{ task.taskFormData.userName }}({{ task.taskFormData.loginName }})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<slot></slot>
<snaker-chart ref="snakerChartRef"></snaker-chart>
</div>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
row: {},
detail: {},
familyData: [],
directRelative: {},
allowFamily: false,
fillBedInfo: true,
directFamilyUnitLine: false,
doneTasks: []
}
},
methods: {
onOpen(row) {
this.row = row || {}
this.detail = {}
this.familyData = []
this.directRelative = {}
this.allowFamily = false
this.fillBedInfo = true
this.directFamilyUnitLine = false
this.doneTasks = []
this.info()
this.getDoneTasks()
},
info() {
this.$axios.post(loc() + "/detail", { id: this.row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.allowFamily = this.toBoolean(data.allowFamily)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
toBoolean(value) {
return value === true || value === 1 || value === "1"
},
isDirectFamilyLine() {
return this.directFamilyUnitLine
|| this.toBoolean(this.detail.directFamilyUnitLine)
|| (this.directRelative && this.directRelative.id)
},
hasFamily() {
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
},
showFamilySection() {
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
},
familyText() {
if (this.isDirectFamilyLine()) {
return "否"
}
if (!this.allowFamily) {
return "否"
}
return this.hasFamily() ? "是" : "否"
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
openChart() {
if (!this.row.instanceProcessDefineId || !this.row.instanceId) {
this.$message.warning("暂无流程图信息")
return
}
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
}
}
}
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const currentYear = moment().format("YYYY")
return {
unionOptions: [],
lineOptions: [],
travelPeriodOptions: [],
lineTypeOptions: [],
showApprovalForm: false,
formData: {
tf_opinion: ""
},
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
audit: false,
startYear: currentYear,
endYear: currentYear,
keyword: "",
unionId: "",
lineId: "",
travelPeriod: "",
lineType: ""
}
}
},
methods: {
resetSearch() {
this.pageForm.startYear = moment().format("YYYY")
this.pageForm.endYear = moment().format("YYYY")
this.pageForm.keyword = ""
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.loadOptions()
this.doSearch()
},
changeAudit() {
this.pageForm.pageNumber = 1
this.clearCascadeFilters()
this.loadOptions()
this.pageData()
},
clearCascadeFilters() {
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
},
openView(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = false
this.formData = {
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
})
},
openApproval(row) {
this.$refs.guava.edit(() => {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName || row.taskName,
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
})
},
handleTaskAction(val) {
this.$refs.formRef.validate((valid) => {
if (!valid) return
this.$confirm("您确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const loading = createLoading()
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.guava.index()
this.$message.success(res.msg || "操作成功")
this.doSearch()
} else {
this.$message.warning(res.msg || "操作失败")
}
}).finally(() => {
loading.close()
})
}).catch(() => {})
})
},
loadOptions() {
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
},
baseOptionParams() {
return {
audit: this.pageForm.audit,
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
keyword: this.pageForm.keyword,
unionId: this.pageForm.unionId,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType
}
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
if (this.pageForm.unionId && !this.unionOptions.some(item => item.id === this.pageForm.unionId)) {
this.pageForm.unionId = ""
}
}
})
},
loadLineOptions() {
this.$axios.post(loc() + "/lineOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
this.pageForm.lineId = ""
}
}
})
},
loadTravelPeriodOptions() {
this.$axios.post(loc() + "/travelPeriodOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.travelPeriodOptions = res.data || []
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
this.pageForm.travelPeriod = ""
}
}
})
},
loadLineTypeOptions() {
this.$axios.post(loc() + "/lineTypeOptions", this.baseOptionParams()).then((res) => {
if (res.code === 0) {
this.lineTypeOptions = res.data || []
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.lineType === this.pageForm.lineType)) {
this.pageForm.lineType = ""
}
}
})
}
},
mounted() {
this.loadOptions()
this.pageData()
},
components: {
"tour-approval-info": TOUR_APPROVAL_INFO
},
watch: {
"pageForm.startYear"() {
this.clearCascadeFilters()
this.loadOptions()
},
"pageForm.endYear"() {
this.clearCascadeFilters()
this.loadOptions()
},
"pageForm.lineId"() {
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
},
"pageForm.travelPeriod"() {
this.loadLineOptions()
this.loadLineTypeOptions()
},
"pageForm.lineType"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
},
"pageForm.unionId"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,609 @@
<!--#
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.startYear"
type="year"
value-format="yyyy"
placeholder="请选择开始年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="结束年度">
<el-date-picker
v-model="pageForm.endYear"
type="year"
value-format="yyyy"
placeholder="请选择结束年度"
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="线路">
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
</el-select>
</search-item>
<search-item label="出行时段">
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
</el-select>
</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>
<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">
<div class="tour-ledger-toolbar">
<div class="tour-ledger-toolbar-title">
<table-tool :app="this" label="分工会台账"></table-tool>
</div>
<div class="tour-ledger-scope">
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.scopeType === 'ownUnionJoined'}"
@click="setScopeType('ownUnionJoined')">
本工会参加疗休养教职工({{ summaryStats.ownUnionJoinedStaffCount || 0 }}
</el-button>
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.scopeType === 'organizedLineJoined'}"
@click="setScopeType('organizedLineJoined')">
参加本工会组织线路人员(含其他工会及家属)({{ summaryStats.organizedLineJoinedTotalCount || 0 }}
</el-button>
<el-button
class="tour-scope-btn"
size="medium"
:class="{'is-active': pageForm.overCostOnly}"
@click="setOverCostOnly">
超出费用由单位承担
</el-button>
<el-button
v-if="pageForm.overCostOnly"
type="primary"
size="medium"
icon="el-icon-download"
@click="downloadOverCostSummary">
导出
</el-button>
</div>
</div>
<el-table
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="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="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="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}">
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
</template>
</el-table-column>
<el-table-column label="是否参加" prop="joined" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.joined ? 'success' : 'info'">{{ row.joined ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" 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="danger" @click="doDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
title="台账详情"
:visible.sync="detailVisible"
:close-on-click-modal="false"
width="72%">
<div class="tour-ledger-section">
<div class="tour-ledger-title">教职工信息</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="tour-ledger-section mt10">
<div class="tour-ledger-title">
报名信息
<el-link v-if="detailHasWorkflow()" type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions :column="3" border size="medium">
<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 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>
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">{{ familyText() }}</el-descriptions-item>
<el-descriptions-item label="是否参加">{{ detail.joined ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="是否报销">{{ detail.reimbursed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="isDirectFamilyLine()" class="tour-ledger-section mt10">
<div class="tour-ledger-title">直系亲属线路</div>
<el-descriptions :column="3" border size="medium">
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="showFamilySection()" class="tour-ledger-section mt10">
<div class="tour-ledger-title">家属信息</div>
<el-table :data="familyData" border :size="tableSize" empty-text="暂无家属信息">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
</div>
<template v-if="detailHasWorkflow()" v-for="task in doneTasks">
<div class="tour-ledger-section mt10" :key="task.id">
<div class="tour-ledger-title">{{ task.displayName }}</div>
<el-descriptions border :column="3" v-if="task.ext && task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<el-descriptions border :column="3" v-else>
<el-descriptions-item label="办理用户">
{{ (task.taskFormData && task.taskFormData.userName) || '' }}({{ (task.taskFormData && task.taskFormData.loginName) || '' }})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
{{ (task.taskFormData && (task.taskFormData.opinion || task.taskFormData.tf_opinion)) || '' }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<snaker-chart ref="snakerChartRef"></snaker-chart>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="detailVisible = false">确定</el-button>
</span>
</el-dialog>
</div>
<style>
.tour-ledger-section {
padding: 0 2px;
}
.tour-ledger-title {
border-left: 4px solid #0079c2;
color: #0079c2;
font-size: 14px;
font-weight: 600;
line-height: 16px;
margin-bottom: 14px;
padding-left: 10px;
}
.tour-ledger-toolbar {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 24px;
margin-bottom: 12px;
white-space: nowrap;
}
.tour-ledger-toolbar-title {
flex: 0 0 auto;
}
.tour-ledger-scope {
display: flex;
align-items: center;
flex: 0 0 auto;
flex-wrap: nowrap;
gap: 12px;
margin: 0;
white-space: nowrap;
}
.tour-ledger-scope .el-button {
margin: 0;
}
.tour-ledger-scope .el-button + .el-button {
margin-left: 0;
}
.tour-ledger-scope .tour-scope-btn {
background: #ecf5ff;
border-color: #b3d8ff;
border-radius: 4px;
color: #0079c2;
font-weight: 600;
height: 34px;
line-height: 1;
padding: 8px 18px;
}
.tour-ledger-scope .tour-scope-btn:hover,
.tour-ledger-scope .tour-scope-btn:focus {
background: #d9ecff;
border-color: #66b1ff;
color: #006bb0;
}
.tour-ledger-scope .tour-scope-btn.is-active {
background: #0079c2;
border-color: #0079c2;
box-shadow: 0 2px 6px rgba(0, 121, 194, 0.24);
color: #fff;
}
.tour-ledger-scope .tour-scope-btn.is-active:hover,
.tour-ledger-scope .tour-scope-btn.is-active:focus {
background: #006bb0;
border-color: #006bb0;
color: #fff;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
const currentYear = moment().format("YYYY")
return {
detailVisible: false,
unionOptions: [],
lineOptions: [],
travelPeriodOptions: [],
lineTypeOptions: [],
summaryStats: {},
detail: {},
familyData: [],
directRelative: {},
directFamilyUnitLine: false,
fillBedInfo: true,
detailRow: {},
doneTasks: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
startYear: currentYear,
endYear: currentYear,
keyword: "",
unionId: "",
lineId: "",
travelPeriod: "",
lineType: "",
scopeType: "",
overCostOnly: false
}
}
},
methods: {
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true"
},
isDirectFamilyLine() {
return this.directFamilyUnitLine
|| this.toBoolean(this.detail && this.detail.directFamilyUnitLine)
|| !!(this.directRelative && this.directRelative.id)
},
detailHasWorkflow() {
return !!(this.detailRow && this.detailRow.instanceId)
},
hasFamily() {
return this.toBoolean(this.detail && this.detail.hasFamily) || this.familyData.length > 0
},
familyText() {
if (this.isDirectFamilyLine()) {
return "否"
}
if (!this.hasFamily()) {
return "否"
}
const count = Number(this.familyData.length || 0)
return count > 0 ? count + "人" : "是"
},
showFamilySection() {
return this.hasFamily() && !this.isDirectFamilyLine()
},
loadDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.detailRow.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
openChart() {
if (!this.detailRow.instanceProcessDefineId || !this.detailRow.instanceId) {
this.$message.warning("暂无流程图信息")
return
}
this.$refs.snakerChartRef.onOpenFull(this.detailRow.instanceProcessDefineId, this.detailRow.instanceId)
},
resetSearch() {
this.pageForm.startYear = moment().format("YYYY")
this.pageForm.endYear = moment().format("YYYY")
this.pageForm.keyword = ""
this.pageForm.unionId = ""
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.pageForm.scopeType = ""
this.pageForm.overCostOnly = false
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
this.doSearch()
},
openView(row) {
this.detailRow = row || {}
this.detail = {}
this.familyData = []
this.directRelative = {}
this.directFamilyUnitLine = false
this.fillBedInfo = true
this.doneTasks = []
this.$axios.post(loc() + "/detail", { id: row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.detailVisible = true
if (this.detailHasWorkflow()) {
this.loadDoneTasks()
}
} else {
this.$message.warning(res.msg || "查询失败")
}
})
},
doDelete(row) {
this.$confirm("确定删除【" + row.userName + "】的报名台账吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
this.loadSummaryStats()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
} else {
this.$message.warning(res.msg || "删除失败")
}
})
})
},
loadUnionOptions() {
this.$axios.post(loc() + "/unionOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
}).then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
}
})
},
loadLineOptions() {
this.$axios.post(loc() + "/lineOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
scopeType: this.pageForm.scopeType
}).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
this.pageForm.lineId = ""
}
}
})
},
loadTravelPeriodOptions() {
this.$axios.post(loc() + "/travelPeriodOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
lineId: this.pageForm.lineId,
lineType: this.pageForm.lineType,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
scopeType: this.pageForm.scopeType
}).then((res) => {
if (res.code === 0) {
this.travelPeriodOptions = res.data || []
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
this.pageForm.travelPeriod = ""
}
}
})
},
loadLineTypeOptions() {
this.$axios.post(loc() + "/lineTypeOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
unionId: this.pageForm.unionId,
keyword: this.pageForm.keyword,
scopeType: this.pageForm.scopeType
}).then((res) => {
if (res.code === 0) {
this.lineTypeOptions = res.data || []
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.lineType === this.pageForm.lineType)) {
this.pageForm.lineType = ""
}
}
})
},
loadSummaryStats() {
this.$axios.post(loc() + "/summaryStats", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
unionId: this.pageForm.unionId,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType
}).then((res) => {
if (res.code === 0) {
this.summaryStats = res.data || {}
}
})
},
setScopeType(scopeType) {
this.pageForm.scopeType = this.pageForm.scopeType === scopeType ? "" : scopeType
this.pageForm.pageNumber = 1
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.pageData()
},
setOverCostOnly() {
this.pageForm.overCostOnly = !this.pageForm.overCostOnly
this.pageForm.pageNumber = 1
this.pageData()
},
downloadOverCostSummary() {
this.$downLoad(loc() + "/exportOverCostSummary", this.pageForm)
}
},
mounted() {
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
this.pageData()
},
watch: {
"pageForm.startYear"() {
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.pageForm.unionId = ""
this.pageForm.scopeType = ""
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
},
"pageForm.endYear"() {
this.pageForm.lineId = ""
this.pageForm.travelPeriod = ""
this.pageForm.lineType = ""
this.pageForm.unionId = ""
this.pageForm.scopeType = ""
this.loadUnionOptions()
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
},
"pageForm.lineId"() {
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
},
"pageForm.travelPeriod"() {
this.loadLineOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
},
"pageForm.lineType"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadSummaryStats()
},
"pageForm.unionId"() {
this.loadLineOptions()
this.loadTravelPeriodOptions()
this.loadLineTypeOptions()
this.loadSummaryStats()
}
}
})
</script>
<!--#
}
#-->
@@ -50,7 +50,7 @@ const basicForm = {
onSave() {
this.$refs.formRef.validate(valid => {
if (valid) {
this.$axios.post('/platform/edu/courses/' + (this.formData.id ? 'update' : 'insert'), this.formData).then(res => {
this.$axios.post('/' + (this.formData.id ? 'update' : 'insert'), this.formData).then(res => {
if (res.code === 0) {
this.visible = false
this.$message.success(res.msg)
@@ -2,47 +2,57 @@ const BASIC_TABLE_COMPONENT = {
template: `
<div>
<el-row type="flex">
<el-col></el-col>
</el-row>
<el-col></el-col>
</el-row>
<table-tool>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">新建机构</el-button>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">新建机构</el-button>
</table-tool>
<el-table key="1" :data="tableData" ref="tableRef">
<el-table-column label="序号" width="100px" type="index" :index="indexMethod"></el-table-column>
<el-table-column prop="name" label="机构名称"></el-table-column>
<el-table-column prop="introduce" label="描述"></el-table-column>
<el-table-column label="操作" width="100px">
<el-table-column prop="code" label="机构代码"></el-table-column>
<el-table-column prop="introduce" label="描述" min-width="220px">
<template slot-scope="{row}">
<el-link size="mini" type="danger" @click="del(row.id)">删除</el-link>
<div class="institution-description">{{row.introduce}}</div>
</template>
</el-table-column>
<el-table-column label="操作" width="140px">
<template slot-scope="scope">
<template>
<el-link size="mini" type="primary" @click="openEdit(scope.row)">修改</el-link>
</template>
<el-divider direction="vertical"></el-divider>
<el-tooltip v-if="isBasicInstitution(scope.$index)"
content="你选择的组织机构是两代会基本机构,不允许删除。"
placement="top">
<span class="disabled-delete-link">删除</span>
</el-tooltip>
<el-link v-else size="mini" type="danger" @click="del(scope.row.id)">删除</el-link>
</template>
</el-table-column>
</el-table>
<el-dialog :visible.sync="dialogFormVisible" :title="formData.id?'编辑':'新增'" width="40%">
<el-form :model="formData" ref="formRef" :rules="rules" label-width="100px">
<el-form-item label="机构名称" prop="ids">
<el-cascader
v-model="ids"
:options="treeList"
:props="props"
@change="parentChange"
placeholder="请选择机构"
style="width: 100%"
></el-cascader>
</el-form-item>
<el-form-item label="机构代码" prop="code">
<el-input placeholder="请输入机构代码" v-model="formData.code" disabled></el-input>
</el-form-item>
<el-form-item label="描述" prop="introduce">
<el-input v-model="formData.introduce" placeholder="请输入描述"></el-input>
</el-form-item>
<el-form-item label="备注" prop="notes">
<el-input v-model="formData.notes" placeholder="请输入备注"></el-input>
</el-form-item>
</el-form>
<el-dialog :visible.sync="dialogFormVisible" :title="isEdit ? '编辑' : '新增'" width="40%">
<el-form :model="formData" ref="formRef" :rules="rules" label-width="100px">
<el-form-item label="上级机构名称">
<el-input :value="parentName || '-'" disabled></el-input>
</el-form-item>
<el-form-item label="机构名称" prop="name">
<el-input placeholder="请输入机构名称" v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="机构代码" prop="code">
<el-input placeholder="请输入机构代码" v-model="formData.code" :disabled="isEdit"></el-input>
</el-form-item>
<el-form-item label="排序编码" prop="location">
<el-input-number v-model="formData.location" :min="0" :step="1" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="描述" prop="introduce">
<el-input v-model="formData.introduce" placeholder="请输入描述"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false"> </el-button>
<el-button type="primary" @click="doSubmit"> </el-button>
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="doSubmit">确定</el-button>
</div>
</el-dialog>
</div>
@@ -56,6 +66,10 @@ const BASIC_TABLE_COMPONENT = {
parentId: {
type: String,
required: true
},
parentName: {
type: String,
default: ""
}
},
data() {
@@ -63,20 +77,13 @@ const BASIC_TABLE_COMPONENT = {
rules: {
name: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
code: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
parentId: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
parentId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
location: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
},
parentData: {},
parentIds: [],
ids: [],
treeList: [],
treeFlat: [],
props: {
checkStrictly: true,
multiple: false,
label: "name",
value: "id"
},
dialogFormVisible: false
dialogFormVisible: false,
isEdit: false
}
},
methods: {
@@ -95,38 +102,44 @@ const BASIC_TABLE_COMPONENT = {
})
},
openAdd() {
this.isEdit = false
this.dialogFormVisible = true
this.formData = {}
this.ids = []
$.post("/platform/teacherCongress/institution/formTree", { sessionId: this.sessionId }).then((res) => {
if (res.code === 0) {
this.treeList = res.data.treeList
this.treeFlat = res.data.treeFlat
}
})
this.formData = {
parentId: this.parentId,
location: 0
}
},
// 上级机构选择变化
parentChange(val) {
const id = val[val.length - 1]
const tree = this.treeFlat.find((tree) => tree.id === id)
this.$set(this.formData, "code", tree?.code)
this.$set(this.formData, "name", tree?.name)
openEdit(row) {
this.isEdit = true
this.dialogFormVisible = true
this.formData = {
...row,
location: row.location || 0
}
},
isBasicInstitution(index) {
const pageNumber = this.pageForm.pageNumber || 1
const pageSize = this.pageForm.pageSize || 10
return (pageNumber - 1) * pageSize + index < 6
},
doSubmit() {
this.$refs.formRef.validate((valid) => {
this.formData.id = this.ids[this.ids.length - 1]
this.formData.parentId = this.ids[this.ids.length - 2] || this.parentId
this.formData.sessionId = this.sessionId
if (valid) {
this.$axios.post(loc() + "/insert", this.formData).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.doSearch()
this.$emit("refresh", null)
}
})
if (!valid) {
return
}
this.formData.sessionId = this.sessionId
if (!this.isEdit) {
this.formData.parentId = this.parentId
}
const url = this.isEdit ? loc() + "/update" : loc() + "/insert"
this.$axios.post(url, this.formData).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.doSearch()
this.$emit("refresh", null)
}
})
})
},
@@ -145,5 +158,18 @@ const BASIC_TABLE_COMPONENT = {
})
})
}
}
},
style: /*language=CSS*/ `
.institution-description {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
.disabled-delete-link {
color: #c0c4cc;
cursor: not-allowed;
font-size: 12px;
}
`
}
@@ -9,21 +9,27 @@ layout("/layouts/platform.html"){
</el-col>
<el-col :span="19">
<el-card shadow="never" style="height: 100%">
<template>
<basic-table
:session-id="sessionId"
:parent-id="currentTreeData.id"
v-if="showInstitutionTable || (currentTreeData && currentTreeData.id==='0')"
ref="basicTableRef"
@refresh="$refs.treeRef.listTree()"
></basic-table>
</template>
<user-table
:session-id="sessionId"
:institution-id="currentTreeData.id"
ref="userTableRef"
v-if="!showInstitutionTable && sessionId"
></user-table>
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane v-if="hasChildInstitution" label="机构列表" name="institution">
<basic-table
:session-id="sessionId"
:parent-id="currentTreeData && currentTreeData.id"
:parent-name="currentTreeData && currentTreeData.name"
v-if="activeTab === 'institution' && sessionId && currentTreeData"
ref="basicTableRef"
@refresh="refreshTree"
></basic-table>
</el-tab-pane>
<el-tab-pane label="人员列表" name="user">
<user-table
:session-id="sessionId"
:institution-id="currentTreeData && currentTreeData.id"
:institution-code="currentTreeData && currentTreeData.code"
ref="userTableRef"
v-if="activeTab === 'user' && sessionId && currentTreeData"
></user-table>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
@@ -43,16 +49,16 @@ layout("/layouts/platform.html"){
"basic-table": BASIC_TABLE_COMPONENT
},
computed: {
showInstitutionTable() {
const hasChildren = this.currentTreeData?.children?.length > 0
return hasChildren
hasChildInstitution() {
return this.currentTreeData && this.currentTreeData.children && this.currentTreeData.children.length > 0
}
},
data() {
return {
currentTreeNode: null,
currentTreeData: null,
sessionId: null
sessionId: null,
activeTab: "institution"
}
},
methods: {
@@ -60,14 +66,28 @@ layout("/layouts/platform.html"){
this.currentTreeData = data
this.currentTreeNode = node
this.sessionId = sessionId
if (this.hasChildInstitution) {
this.activeTab = "institution"
} else if (this.activeTab === "institution") {
this.activeTab = "user"
}
this.refreshActiveTab()
},
handleTabClick() {
this.refreshActiveTab()
},
refreshActiveTab() {
this.$nextTick(() => {
if (this.showInstitutionTable) {
if (this.activeTab === "institution") {
this.$refs.basicTableRef && this.$refs.basicTableRef.doSearch()
} else {
this.$refs.userTableRef && this.$refs.userTableRef.doSearch()
}
})
},
refreshTree() {
this.$refs.treeRef.listTree(this.currentTreeData && this.currentTreeData.id)
},
openEdit(row) {}
},
created() {}
@@ -46,11 +46,30 @@ const TREE_COMPONENT = {
this.$emit("node-click", data, node, this.sessionId)
},
filterNode() {},
listTree() {
findTreeNode(list, id) {
if (!id || !list) {
return null
}
for (const item of list) {
if (item.id === id) {
return item
}
const child = this.findTreeNode(item.children, id)
if (child) {
return child
}
}
return null
},
listTree(selectedId) {
this.$axios.post("/platform/teacherCongress/institution/leftTree", { sessionId: this.sessionId }).then((res) => {
if (res.code === 0) {
this.treeData = res.data
this.$emit("node-click", this.treeData[0], null, this.sessionId)
if (this.treeData && this.treeData.length > 0) {
this.treeData[0].name = "两代会组织机构"
}
const selectedNode = this.findTreeNode(this.treeData, selectedId) || this.treeData[0]
this.$emit("node-click", selectedNode, null, this.sessionId)
}
})
},
@@ -3,58 +3,73 @@ const USER_TABLE_COMPONENT = {
<div>
<el-card shadow="never" style="height: 100%">
<el-row type="flex" :gutter="20">
<el-col :span="4">
<el-input v-model="pageForm.searchKeyword" clearable placeholder="请输入姓名或者工号">" @keyup.enter.native="doSearch">
</el-input>
</el-col>
<el-col :span="4">
<div class="search-query">
<el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
</div>
</el-col>
<el-col :span="4">
<el-input v-model="pageForm.searchKeyword"
clearable
placeholder="请输入姓名或者工号"
@keyup.enter.native="doSearch">
</el-input>
</el-col>
<el-col :span="4">
<div class="search-query">
<el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
</div>
</el-col>
</el-row>
<el-divider class="mb10 mt10"></el-divider>
<table-tool>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">设置人员</el-button>
</table-tool>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">设置人员</el-button>
</table-tool>
<el-table key="1" :data="tableData" ref="tableRef">
<el-table-column label="序号" width="100px" type="index" :index="indexMethod"></el-table-column>
<el-table-column prop="username" label="姓名"></el-table-column>
<el-table-column prop="loginname" label="工号"></el-table-column>
<el-table-column prop="sex" label="性别"></el-table-column>
<el-table-column prop="mobile" label="手机号码"></el-table-column>
<el-table-column prop="unitName" label="单位" show-overflow-tooltip></el-table-column>
<el-table-column prop="identity" label="身份" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="100px">
<template slot-scope="{row}">
<el-link size="mini" type="danger" @click="del(row.id)">删除</el-link>
</template>
<el-table-column label="序号" width="100px" type="index" :index="indexMethod"></el-table-column>
<el-table-column prop="username" label="姓名"></el-table-column>
<el-table-column prop="loginname" label="工号"></el-table-column>
<el-table-column prop="sex" label="性别"></el-table-column>
<el-table-column prop="mobile" label="手机号码"></el-table-column>
<el-table-column prop="unitName" label="单位" show-overflow-tooltip></el-table-column>
<el-table-column prop="identity" label="身份" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="100px">
<template slot-scope="{row}">
<el-link size="mini" type="danger" @click="del(row.id)">删除</el-link>
</template>
</el-table-column>
</el-table>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="设置人员" width="50%" :visible.sync="dialogVisible">
<el-form :model="formData" ref="formRef" label-width="120px" :rules="formRules">
<el-form :model="formData" ref="formRef" label-width="120px" :rules="formRules">
<el-form-item label="届次" prop="sessionId">
<el-select v-model="formData.sessionId" disabled>
<el-option v-for="i in sessionOptions" :label="i.fullName" :value="i.id" :key="i.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="工号或姓名" prop="userId">
<el-form-item v-if="needRole" label="角色" prop="roleCode">
<el-select v-model="formData.roleCode" placeholder="请选择角色" style="width: 100%">
<el-option v-for="item in roleOptions"
:key="item.code"
:label="item.name"
:value="item.code">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="工号或者姓名" prop="userId">
<user-select
v-model="formData.userId"
style="width: 100%"
api_input_key_name="query"
:option_label_func="(item)=>{return item.username + item.loginname}"
v-model="formData.userId"
style="width: 100%"
api_input_key_name="query"
:option_label_func="(item)=>{return item.username + item.loginname}"
></user-select>
</el-form-item>
<el-form-item label="身份" prop="identity">
<dict-select v-model="formData.identity" code="TEACHER_CONGRESS_INSTITUTION_USER_ROLE"></dict-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="doSubmit"> </el-button>
</el-form>
<div style="color: #e6a23c; line-height: 22px; margin: 0 0 12px 120px;">
注意如果需要跟角色绑定请在数据字典双代会组织机构中添加对应的角色标识
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="doSubmit">确定</el-button>
</div>
</el-dialog>
</div>
@@ -68,6 +83,10 @@ const USER_TABLE_COMPONENT = {
institutionId: {
required: true,
type: String
},
institutionCode: {
type: String,
default: ""
}
},
data() {
@@ -75,10 +94,17 @@ const USER_TABLE_COMPONENT = {
formRules: {
sessionId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
userId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
identity: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
identity: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
roleCode: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
},
dialogVisible: false,
sessionOptions: []
sessionOptions: [],
roleOptions: []
}
},
computed: {
needRole() {
return ["ZWH001", "ZXWYH", "DBZGSCXZ"].includes(this.institutionCode)
}
},
methods: {
@@ -136,6 +162,18 @@ const USER_TABLE_COMPONENT = {
})
},
listRoleOptions() {
if (!this.needRole) {
this.roleOptions = []
return
}
this.$axios.post("/platform/teacherCongress/institution/specialCommitteeRoleOptions").then((res) => {
if (res.code === 0) {
this.roleOptions = res.data || []
}
})
},
openAdd() {
this.dialogVisible = true
this.formData = {
@@ -143,6 +181,7 @@ const USER_TABLE_COMPONENT = {
institutionId: this.institutionId
}
this.listSession()
this.listRoleOptions()
}
}
}
@@ -3,6 +3,55 @@ layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.site-cug-audit-page {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f4f6f8;
box-sizing: border-box;
}
.site-cug-audit-sticky {
flex-shrink: 0;
background: #f4f6f8;
}
.site-cug-audit-list-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
padding-bottom: 18px;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
.site-cug-audit-search /deep/ .van-search__content,
.site-cug-audit-search .van-search__content {
border-radius: 8px;
background: #ffffff;
}
.site-cug-card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.site-cug-tag {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 4px;
background: #eef6ff;
color: #0b75bd;
font-size: 12px;
}
.site-cug-tag.gray {
background: #f1f5f9;
color: #64748b;
}
.site-cug-tag.warn {
background: #fff3e5;
color: #d97706;
}
.action-btn.disabled {
opacity: 0.45;
pointer-events: none;
@@ -15,18 +64,23 @@ layout("/layouts/platform_h5.html"){
}
</style>
<div id="app" v-cloak>
<div id="app" class="site-cug-audit-page" v-cloak>
<van-nav-bar title="校工会审核" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px">
<van-sticky offset-top="46px" class="site-cug-audit-sticky">
<van-search
class="site-cug-audit-search"
v-model="pageForm.searchKeyword"
:show-action="false"
:reverse-color="false"
show-action
clearable
input-align="left"
placeholder="请输入姓名/工号/场地搜索"
@search="doSearch"
></van-search>
@clear="doSearch">
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.siteType" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.siteId" :options="siteOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
@@ -37,32 +91,38 @@ layout("/layouts/platform_h5.html"){
</van-tabs>
</van-sticky>
<table-list api="/platform/siteCug/schoolUnionAudit/pageData" :page_form.sync="pageForm" ref="tableListRef" title="applyUserName" @ready="onReady">
<template v-slot="{index,row}">
<table-column label="场地名称">{{row.siteName}}</table-column>
<table-column label="预约类型">{{row.reserveType === 'club' ? '协会预约' : '分工会预约'}}</table-column>
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '-'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="canRevoke(row)" :class="{ loading: revokeLoading }" @click="onRevoke(row)">
<van-loading v-if="revokeLoading" size="14px" color="#fff"></van-loading>
<i v-else class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
<div class="site-cug-audit-list-scroll">
<table-list api="/platform/siteCug/schoolUnionAudit/pageData" :page_form.sync="pageForm" ref="tableListRef" title="applyUserName" @ready="onReady">
<template v-slot="{index,row}">
<div class="site-cug-card-tags">
<span v-if="row.applyLoginName" class="site-cug-tag">{{ row.applyLoginName }}</span>
<span class="site-cug-tag gray">{{ reserveTypeText(row) }}</span>
<span v-if="row.yearlyBatch" class="site-cug-tag warn">预约本年</span>
</div>
<table-column label="场地名称">{{row.siteName}}</table-column>
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '-'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="canRevoke(row)" :class="{ loading: revokeLoading }" @click="onRevoke(row)">
<van-loading v-if="revokeLoading" size="14px" color="#fff"></van-loading>
<i v-else class="fa fa-reply"></i>
<span>撤回</span>
</div>
</template>
</table-list>
</div>
<info ref="infoRef">
<div v-if="showApprovalForm">
@@ -140,6 +200,9 @@ layout("/layouts/platform_h5.html"){
canRevoke(row) {
return Number(row.instanceState) === 20
},
reserveTypeText(row) {
return row && row.reserveType === 'club' ? '协会预约' : '分工会预约'
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,478 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.tour-approval-page {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f4f6f8;
box-sizing: border-box;
}
.tour-approval-sticky {
flex-shrink: 0;
background: #f4f6f8;
}
.tour-approval-list-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
padding-bottom: 18px;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
.tour-approval-search /deep/ .van-search__content,
.tour-approval-search .van-search__content {
border-radius: 8px;
background: #ffffff;
}
.tour-card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.tour-tag {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 4px;
background: #eef6ff;
color: #0b75bd;
font-size: 12px;
}
.tour-tag.gray {
background: #f1f5f9;
color: #64748b;
}
.tour-tag.warn {
background: #fff3e5;
color: #d97706;
}
.tour-approval-sheet {
max-height: 86vh;
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
background: #f4f6f8;
}
.tour-section {
margin: 10px 12px;
overflow: hidden;
border-radius: 8px;
background: #ffffff;
}
.tour-section-title {
position: relative;
padding: 14px 16px;
border-bottom: 1px solid #edf0f4;
color: #0b75bd;
font-size: 16px;
font-weight: 700;
line-height: 1.2;
}
.tour-section-title::before {
content: "";
position: absolute;
left: 16px;
top: 0;
width: 30px;
height: 4px;
border-radius: 0 0 3px 3px;
background: #0b75bd;
}
.tour-approval-actions {
display: flex;
gap: 8px;
padding: 10px 12px 14px;
background: #ffffff;
}
.tour-process-opinion {
white-space: pre-wrap;
word-break: break-word;
}
</style>
<div id="app" class="tour-approval-page" v-cloak>
<van-nav-bar title="校工会审核" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<van-sticky offset-top="46px" class="tour-approval-sticky">
<van-search
class="tour-approval-search"
v-model="pageForm.keyword"
placeholder="请输入工号/姓名/线路搜索"
show-action
clearable
input-align="left"
@search="doSearch"
@clear="doSearch">
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item :options="auditOptions" @change="doSearch" v-model="pageForm.audit"></van-dropdown-item>
<van-dropdown-item :options="unionOptions" @change="doSearch" v-model="pageForm.unionId"></van-dropdown-item>
<van-dropdown-item :options="lineTypeOptions" @change="doSearch" v-model="pageForm.lineType"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<div class="tour-approval-list-scroll">
<table-list api="/platform/tour/schoolUnionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" @ready="onReady">
<template v-slot="{index,row}">
<div class="tour-card-tags">
<span v-if="row.jobNo" class="tour-tag">{{ row.jobNo }}</span>
<span v-if="row.lineType" class="tour-tag gray">{{ row.lineType }}</span>
<span v-if="row.overCostReimbursed" class="tour-tag warn">报销超出费用</span>
</div>
<table-column label="线路名称">{{ row.lineName || '' }}</table-column>
<table-column label="出行时段">{{ row.travelPeriod || '' }}</table-column>
<table-column label="报销超出费用">{{ row.overCostReimbursed ? '是' : '否' }}</table-column>
<table-column label="当前节点">{{ row.curTaskName || '' }}</table-column>
<table-column label="流程状态">
<enum-tag
v-if="row.instanceState !== null && row.instanceState !== undefined && row.instanceState !== ''"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small">
</enum-tag>
</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-check"></i>
<span>审核</span>
</div>
</template>
</table-list>
</div>
<tour-info ref="infoRef">
<div v-if="showApprovalForm" class="tour-section">
<div class="tour-section-title">{{ formData.taskName || '审核' }}</div>
<van-form ref="formRef">
<van-field
v-model="formData.tf_opinion"
name="tf_opinion"
label="审批意见"
placeholder="请输入审批意见"
:rules="[{ required: true, message: '请填写审批意见' }]"
required
type="textarea"
rows="3"
maxlength="200"
show-word-limit>
</van-field>
</van-form>
<div class="tour-approval-actions">
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
<van-button type="danger" block @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
</div>
</div>
</tour-info>
</div>
<script nonce="${cspNonce!}">
const TOUR_INFO = {
template: /*language=HTML*/ `
<van-action-sheet v-model="visible" title="报名详情">
<div class="tour-approval-sheet">
<div class="tour-section">
<div class="tour-section-title">教职工信息</div>
<van-cell title="工号" :value="detail.jobNo || ''"></van-cell>
<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.unitName || ''"></van-cell>
<van-cell title="所在工会" :value="detail.unionName || ''"></van-cell>
</div>
<div class="tour-section">
<div class="tour-section-title">报名信息</div>
<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.signupTime || ''"></van-cell>
<van-cell title="报名酒店" :value="detail.hotelName || ''"></van-cell>
<van-cell 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>
<van-cell title="携带家属" :value="familyText()"></van-cell>
<van-cell title="报销超出费用" :value="detail.overCostReimbursed ? '是' : '否'"></van-cell>
</div>
<div v-if="isDirectFamilyLine()" class="tour-section">
<div class="tour-section-title">直系亲属线路</div>
<van-cell title="亲属姓名" :value="directRelative.relativeName || ''"></van-cell>
<van-cell title="所在单位" :value="directRelative.unitName || ''"></van-cell>
<van-cell title="亲属关系" :value="directRelative.relationshipName || ''"></van-cell>
<van-cell title="线路名称" :value="directRelative.lineName || ''"></van-cell>
<van-cell title="出行开始" :value="directRelative.travelStartTime || ''"></van-cell>
<van-cell title="出行结束" :value="directRelative.travelEndTime || ''"></van-cell>
</div>
<div v-if="showFamilySection()" class="tour-section">
<div class="tour-section-title">家属信息</div>
<template v-for="(item,index) in familyData">
<van-cell :key="'title_' + index" :title="'家属' + (index + 1)"></van-cell>
<van-cell :key="'name_' + index" title="姓名" :value="item.familyName || ''"></van-cell>
<van-cell :key="'gender_' + index" title="性别" :value="item.gender || ''"></van-cell>
<van-cell :key="'id_' + index" title="身份证号" :value="item.idCard || ''"></van-cell>
<van-cell :key="'rel_' + index" title="关系" :value="item.relationship || ''"></van-cell>
<van-cell v-if="fillBedInfo" :key="'bed_' + index" title="床型" :value="item.bedType || ''"></van-cell>
</template>
</div>
<div v-if="doneTasks.length > 0" class="tour-section">
<div class="tour-section-title">流程记录</div>
<template v-for="task in doneTasks">
<van-cell-group :key="task.id" :title="task.displayName || task.taskName">
<van-cell title="办理时间" :value="task.finishTime || ''"></van-cell>
<van-cell title="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见" v-if="task.taskFormData" class="direction-column-cell">
<div class="tour-process-opinion">{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}</div>
</van-cell>
</van-cell-group>
</template>
</div>
<slot></slot>
</div>
</van-action-sheet>
`,
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
visible: false,
row: {},
detail: {},
familyData: [],
directRelative: {},
allowFamily: false,
fillBedInfo: true,
directFamilyUnitLine: false,
doneTasks: []
}
},
methods: {
onOpen(row) {
this.row = row || {}
this.visible = true
this.detail = {}
this.familyData = []
this.directRelative = {}
this.allowFamily = false
this.fillBedInfo = true
this.directFamilyUnitLine = false
this.doneTasks = []
this.info()
this.getDoneTasks()
},
onClose() {
this.visible = false
},
info() {
this.$axios.post("/platform/tour/schoolUnionApproval/detail", { id: this.row.id }).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.detail = data.ledger || {}
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
this.familyData = data.families || []
this.directRelative = data.directRelative || {}
this.allowFamily = this.toBoolean(data.allowFamily)
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
} else {
vant.Toast(res.msg || "查询失败")
}
})
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data || []
}
})
},
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
isDirectFamilyLine() {
return this.directFamilyUnitLine || this.toBoolean(this.detail.directFamilyUnitLine) || !!(this.directRelative && this.directRelative.id)
},
hasFamily() {
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
},
showFamilySection() {
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
},
familyText() {
if (this.isDirectFamilyLine() || !this.allowFamily) {
return "否"
}
return this.hasFamily() ? "是" : "否"
}
}
}
new Vue({
el: "#app",
store,
components: {
"tour-info": TOUR_INFO
},
data() {
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "signupTime",
pageOrderBy: "descending",
audit: false,
keyword: "",
unionId: "",
lineType: ""
},
auditOptions: [
{ text: "未审核", value: false },
{ text: "已审核", value: true }
],
unionOptions: [
{ text: "全部工会", value: "" }
],
lineTypeOptions: [
{ text: "全部线路类型", value: "" }
],
formData: {},
showApprovalForm: false
}
},
methods: {
historyBack,
onReady() {
this.doSearch()
},
doSearch() {
this.$nextTick(async () => {
await Promise.all([this.loadUnionOptions(), this.loadLineTypeOptions()])
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
})
},
loadUnionOptions() {
return this.$axios.post("/platform/tour/schoolUnionApproval/unionOptions", {
audit: this.pageForm.audit,
keyword: this.pageForm.keyword,
lineType: this.pageForm.lineType
}).then((res) => {
if (res.code === 0) {
const options = (res.data || []).map(item => ({
text: item.name,
value: item.id
})).filter(item => item.value)
this.unionOptions = [{ text: "全部工会", value: "" }].concat(options)
if (this.pageForm.unionId && !this.unionOptions.some(item => item.value === this.pageForm.unionId)) {
this.pageForm.unionId = ""
}
}
})
},
loadLineTypeOptions() {
return this.$axios.post("/platform/tour/schoolUnionApproval/lineTypeOptions", {
audit: this.pageForm.audit,
keyword: this.pageForm.keyword,
unionId: this.pageForm.unionId
}).then((res) => {
if (res.code === 0) {
const options = (res.data || []).map(item => ({
text: item.lineType,
value: item.lineType
})).filter(item => item.value)
this.lineTypeOptions = [{ text: "全部线路类型", value: "" }].concat(options)
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.value === this.pageForm.lineType)) {
this.pageForm.lineType = ""
}
}
})
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
},
onApproval(row) {
this.showApprovalForm = true
this.formData = {
processTaskId: row.taskId,
taskName: row.curTaskName || row.taskName,
tf_opinion: ""
}
this.$refs.infoRef.onOpen(row)
},
async handleTaskAction(val) {
try {
await this.$refs.formRef.validate()
} catch (e) {
return
}
this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$refs.infoRef.onClose()
this.$toast.success(res.msg || "操作成功")
this.doSearch()
} else {
this.$toast(res.msg || "操作失败")
}
}).finally(() => {
loading.close()
})
}).catch(() => {})
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,556 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.tour-apply-page {
min-height: 100vh;
padding: 10px 14px 78px;
background: #f4f6f8;
box-sizing: border-box;
}
.tour-apply-card {
margin-bottom: 14px;
overflow: hidden;
border-radius: 6px;
background: #ffffff;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
}
.tour-apply-title {
position: relative;
padding: 14px 16px;
border-bottom: 1px solid #edf0f4;
color: #0b75bd;
font-size: 16px;
font-weight: 700;
line-height: 1.2;
}
.tour-apply-title::before {
content: "";
position: absolute;
left: 16px;
top: 0;
width: 30px;
height: 4px;
border-radius: 0 0 3px 3px;
background: #0b75bd;
}
.tour-card-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.tour-card-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.tour-family-tab {
margin: 10px 12px 0;
height: 30px;
border-radius: 5px;
background: #1684d2;
color: #ffffff;
font-size: 14px;
font-weight: 700;
line-height: 30px;
text-align: center;
}
.tour-field-tip {
padding: 0 16px 12px;
color: #ee2f2f;
font-size: 12px;
line-height: 1.5;
}
.tour-over-cost /deep/ .van-field__label,
.tour-over-cost /deep/ .van-field__label span,
.tour-over-cost /deep/ .van-radio__label {
color: #ee2f2f;
font-weight: 700;
}
.tour-submit-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 20;
padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf0f4;
background: #ffffff;
box-sizing: border-box;
}
.tour-apply-empty {
padding: 24px 0;
color: #94a3b8;
font-size: 13px;
text-align: center;
}
</style>
<div id="app">
<van-nav-bar title="疗休养报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="tour-apply-page">
<van-loading v-if="pageLoading" size="24px" vertical>加载中...</van-loading>
<template v-else>
<div class="tour-apply-card">
<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 label="所属工会" readonly v-model="signupForm.unionName"></van-field>
</div>
<div class="tour-apply-card">
<div class="tour-apply-title">报名信息</div>
<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="fillBedInfo"
label="床型"
readonly
clickable
is-link
placeholder="请选择房型"
v-model="signupForm.bedType"
@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>
<van-field v-if="canApplyOverReimbursement(signupForm)" class="tour-over-cost" label="报销超出费用">
<template #input>
<van-radio-group v-model="signupForm.overCostReimbursed" direction="horizontal">
<van-radio :name="true"></van-radio>
<van-radio :name="false"></van-radio>
</van-radio-group>
</template>
</van-field>
<div v-if="canApplyOverReimbursement(signupForm) && signupForm.overCostReimbursed" class="tour-field-tip">
提醒:您选择了报销超出费用,会影响到下一年度的疗休养,请认真思考。
</div>
</div>
<div v-if="isDirectFamilyLine(signupForm)" class="tour-apply-card">
<div class="tour-apply-title">直系亲属线路</div>
<van-field label="亲属姓名" v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></van-field>
<van-field label="所在单位" v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></van-field>
<van-field
label="亲属关系"
readonly
clickable
is-link
placeholder="请选择亲属关系"
v-model="directRelativeForm.relationshipName"
@click="openDirectRelativePicker">
</van-field>
<van-field label="线路名称" v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></van-field>
<van-field label="出行开始" v-model="directRelativeForm.travelStartTime" type="date" placeholder="请选择出行开始日期"></van-field>
<van-field label="出行结束" v-model="directRelativeForm.travelEndTime" type="date" placeholder="请选择出行结束日期"></van-field>
</div>
<div class="tour-apply-card">
<div class="tour-apply-title">
<div class="tour-card-title-row">
<span>亲属信息</span>
<div class="tour-card-actions">
<van-button size="small" type="default" :disabled="familyData.length === 0" @click="removeFamily">删除亲属</van-button>
<van-button size="small" type="info" :disabled="!allowFamily || isDirectFamilyLine(signupForm)" @click="addFamily">添加亲属</van-button>
</div>
</div>
</div>
<div v-if="!allowFamily || isDirectFamilyLine(signupForm)" class="tour-apply-empty">
当前线路不支持填写亲属
</div>
<template v-else-if="familyData.length > 0">
<div class="tour-family-tab">亲属{{ familyActive + 1 }}</div>
<van-field label="姓名" v-model="currentFamily.familyName" maxlength="100" placeholder="请填写姓名"></van-field>
<van-field label="年龄" v-model.number="currentFamily.age" type="digit" placeholder="请填写年龄"></van-field>
<van-field label="性别">
<template #input>
<van-radio-group v-model="currentFamily.gender" direction="horizontal">
<van-radio name="男"></van-radio>
<van-radio name="女"></van-radio>
</van-radio-group>
</template>
</van-field>
<van-field label="身份证号码" v-model="currentFamily.idCard" maxlength="18" placeholder="请填写身份证号码" @blur="normalizeFamilyIdCard(currentFamily)"></van-field>
<van-field label="手机号" v-model="currentFamily.mobile" maxlength="30" placeholder="请填写手机号"></van-field>
<van-field
label="与本人关系"
readonly
clickable
is-link
placeholder="请选择关系"
v-model="currentFamily.relationship"
@click="openRelationshipPicker">
</van-field>
<van-field
v-if="fillBedInfo"
label="床型"
readonly
clickable
is-link
placeholder="请选择房型"
v-model="currentFamily.bedType"
@click="openBedPicker('family')">
</van-field>
<van-field v-if="fillBedInfo" label="床位" v-model="currentFamily.bedInfo" maxlength="100" placeholder="请输入床位数量"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="currentFamily.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
<div v-if="fillBedInfo" class="tour-field-tip">提醒:如果跟报名人员同一房间,无须选择!</div>
</template>
<div v-else class="tour-apply-empty">暂无亲属,请按需添加</div>
</div>
</template>
</div>
<div class="tour-submit-bar">
<van-button type="info" block round :loading="submitLoading" @click="submitSignup">提交报名</van-button>
</div>
<van-popup v-model="bedPickerVisible" position="bottom">
<van-picker show-toolbar :columns="bedTypeColumns" @confirm="confirmBedType" @cancel="bedPickerVisible=false"></van-picker>
</van-popup>
<van-popup v-model="relationshipPickerVisible" position="bottom">
<van-picker show-toolbar :columns="relationshipColumns" @confirm="confirmRelationship" @cancel="relationshipPickerVisible=false"></van-picker>
</van-popup>
<van-popup v-model="directRelativePickerVisible" position="bottom">
<van-picker show-toolbar :columns="directRelativeColumns" @confirm="confirmDirectRelative" @cancel="directRelativePickerVisible=false"></van-picker>
</van-popup>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
pageLoading: true,
submitLoading: false,
allowFamily: false,
fillBedInfo: true,
signupForm: {},
directRelativeForm: {},
familyData: [],
familyActive: 0,
bedTypeOptions: [],
familyRelationshipOptions: [],
directRelativeOptions: [],
bedPickerVisible: false,
relationshipPickerVisible: false,
directRelativePickerVisible: false,
bedPickerTarget: "self",
matterId: GetQueryString("matterId") || ""
}
},
computed: {
currentFamily() {
return this.familyData[this.familyActive] || {}
},
bedTypeColumns() {
return this.bedTypeOptions.map(item => item.name || item.code).filter(Boolean)
},
relationshipColumns() {
return this.familyRelationshipOptions.map(item => item.name || item.code).filter(Boolean)
},
directRelativeColumns() {
return this.directRelativeOptions.map(item => ({ text: item.name || item.code, item }))
}
},
methods: {
init() {
if (!this.matterId) {
vant.Dialog.alert({ title: "提示", message: "报名事项参数缺失", confirmButtonColor: "#1867b0" })
this.pageLoading = false
return
}
Promise.all([this.loadOptions(), 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 || []
})
])
},
loadSignupDetail() {
return this.$axios.post("/platform/tour/signup/signupDetail", { matterId: this.matterId }).then((res) => {
if (res.code !== 0) {
vant.Dialog.alert({
title: "提示",
message: res.msg || "报名信息加载失败",
confirmButtonColor: "#1867b0"
})
return
}
const data = res.data || {}
const matter = data.matter || {}
const staff = data.staff || {}
const ledger = data.ledger || {}
const directRelative = data.directRelative || {}
this.allowFamily = matter.allowFamily === true || matter.allowFamily === 1 || matter.allowFamily === "1"
this.fillBedInfo = matter.fillBedInfo === undefined || matter.fillBedInfo === null || matter.fillBedInfo === true || matter.fillBedInfo === 1 || matter.fillBedInfo === "1"
this.signupForm = Object.assign({
id: "",
year: matter.year,
jobNo: "",
userName: "",
gender: "",
idCard: "",
mobile: "",
unitId: "",
unitName: "",
unionId: "",
unionName: "",
signupTime: this.$moment ? this.$moment().format("YYYY-MM-DD HH:mm:ss") : "",
matterId: matter.matterId || "",
lineId: matter.lineId || "",
lineName: matter.lineName || "",
lineType: matter.lineType || "",
directFamilyUnitLine: matter.directFamilyUnitLine,
hotelName: "",
travelAgencyId: matter.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
allowOverReimbursement: matter.allowOverReimbursement,
overCostReimbursed: false,
hasFamily: false,
intendedRoommate: "",
bedType: "",
bedInfo: ""
}, staff, ledger, {
year: matter.year,
matterId: matter.matterId || ledger.matterId || "",
lineId: matter.lineId || ledger.lineId || "",
lineName: matter.lineName || ledger.lineName || "",
lineType: matter.lineType || ledger.lineType || "",
directFamilyUnitLine: matter.directFamilyUnitLine,
travelAgencyId: matter.travelAgencyId || ledger.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || ledger.travelAgencyName || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
allowOverReimbursement: matter.allowOverReimbursement,
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(), {
lineId: matter.lineId || "",
lineName: matter.lineName || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || ""
}, directRelative)
this.familyData = (data.families || []).map(item => Object.assign(this.emptyFamily(), item))
if (this.familyData.length > 0) {
this.signupForm.hasFamily = true
}
if (!this.allowFamily || this.isDirectFamilyLine(this.signupForm)) {
this.familyData = []
this.signupForm.hasFamily = false
}
})
},
emptyFamily() {
return {
familyName: "",
gender: "",
idCard: "",
mobile: "",
age: null,
bedType: "",
bedInfo: "",
intendedRoommate: "",
staffJobNo: this.signupForm.jobNo || "",
staffName: this.signupForm.userName || "",
relationship: ""
}
},
emptyDirectRelative() {
return {
relativeName: "",
unitName: "",
relationshipCode: "",
relationshipName: "",
lineId: this.signupForm.lineId || "",
lineName: this.signupForm.lineName || "",
travelStartTime: this.signupForm.travelStartTime || "",
travelEndTime: this.signupForm.travelEndTime || ""
}
},
isDirectFamilyLine(row) {
return row && (row.directFamilyUnitLine === true || row.directFamilyUnitLine === 1 || row.directFamilyUnitLine === "1")
},
canApplyOverReimbursement(row) {
return row && (row.allowOverReimbursement === true || row.allowOverReimbursement === 1 || row.allowOverReimbursement === "1")
},
addFamily() {
if (!this.allowFamily || this.isDirectFamilyLine(this.signupForm)) return
this.familyData.push(this.emptyFamily())
this.familyActive = this.familyData.length - 1
this.signupForm.hasFamily = true
},
removeFamily() {
if (this.familyData.length === 0) return
this.familyData.splice(this.familyActive, 1)
this.familyActive = Math.max(0, this.familyActive - 1)
this.signupForm.hasFamily = this.familyData.length > 0
},
openBedPicker(target) {
if (this.bedTypeColumns.length === 0) {
vant.Toast("暂无床型选项")
return
}
this.bedPickerTarget = target
this.bedPickerVisible = true
},
confirmBedType(value) {
if (this.bedPickerTarget === "family") {
this.currentFamily.bedType = value
} else {
this.signupForm.bedType = value
}
this.bedPickerVisible = false
},
openRelationshipPicker() {
if (this.relationshipColumns.length === 0) {
vant.Toast("暂无关系选项")
return
}
this.relationshipPickerVisible = true
},
confirmRelationship(value) {
this.currentFamily.relationship = value
this.relationshipPickerVisible = false
},
openDirectRelativePicker() {
if (this.directRelativeColumns.length === 0) {
vant.Toast("暂无亲属关系选项")
return
}
this.directRelativePickerVisible = true
},
confirmDirectRelative(value) {
const item = value && value.item ? value.item : {}
this.directRelativeForm.relationshipCode = item.code || ""
this.directRelativeForm.relationshipName = item.name || value.text || ""
this.directRelativePickerVisible = false
},
normalizeFamilyIdCard(row) {
if (!row || !row.idCard) return
row.idCard = String(row.idCard).trim().toUpperCase()
},
isValidIdCard(value) {
if (!value) return false
const idCard = String(value).trim().toUpperCase()
return /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/.test(idCard)
},
validateFamilies() {
if (!this.allowFamily || !this.signupForm.hasFamily) return true
if (this.familyData.length === 0) {
vant.Toast("请添加亲属信息")
return false
}
const invalid = this.familyData.some(item => !item.familyName || !item.gender || !item.idCard || !item.relationship)
if (invalid) {
vant.Toast("请完善亲属姓名、性别、身份证号码和关系")
return false
}
const invalidIdCard = this.familyData.some(item => !this.isValidIdCard(item.idCard))
if (invalidIdCard) {
vant.Toast("请输入正确的亲属身份证号码")
return false
}
return true
},
validateDirectRelative() {
if (!this.isDirectFamilyLine(this.signupForm)) return true
if (!this.directRelativeForm.relativeName) {
vant.Toast("请输入亲属姓名")
return false
}
if (!this.directRelativeForm.unitName) {
vant.Toast("请输入亲属所在单位")
return false
}
if (!this.directRelativeForm.relationshipCode) {
vant.Toast("请选择亲属关系")
return false
}
if (!this.directRelativeForm.lineName) {
vant.Toast("请输入直系亲属线路名称")
return false
}
if (!this.directRelativeForm.travelStartTime || !this.directRelativeForm.travelEndTime) {
vant.Toast("请选择直系亲属出行时间")
return false
}
return true
},
submitSignup() {
if (this.pageLoading || this.submitLoading) return
if (!this.validateFamilies()) return
if (!this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
const form = Object.assign({}, this.signupForm, {
families: JSON.stringify(families),
directRelative: this.isDirectFamilyLine(this.signupForm) ? JSON.stringify(this.directRelativeForm) : ""
})
this.submitLoading = true
this.$axios.post("/platform/tour/signup/doSignup", form).then((res) => {
this.submitLoading = false
if (res.code === 0) {
vant.Dialog.alert({
title: "提示",
message: res.msg || "报名成功",
confirmButtonColor: "#1867b0"
}).then(() => {
window.location.href = "/platform/tour/signup/h5/signup"
})
} else {
vant.Dialog.alert({
title: "提示",
message: res.msg || "报名失败",
confirmButtonColor: "#1867b0"
})
}
}).catch(() => {
this.submitLoading = false
vant.Toast("报名提交失败")
})
}
},
created() {
this.init()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,210 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.tour-signup-h5 {
min-height: 100vh;
background: #f5f7fb;
padding-bottom: 80px;
box-sizing: border-box;
}
.tour-signup-banner {
position: relative;
height: 193px;
overflow: hidden;
background: #0f74bc;
color: #ffffff;
}
.tour-signup-swipe {
width: 100%;
height: 100%;
}
.tour-signup-swipe img {
display: block;
width: 100%;
height: 193px;
object-fit: cover;
}
.tour-signup-banner::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgba(5, 83, 143, 0.72) 0%, rgba(5, 83, 143, 0.32) 48%, rgba(5, 83, 143, 0.08) 100%);
pointer-events: none;
}
.tour-signup-banner__text {
position: absolute;
left: 16px;
right: 16px;
bottom: 20px;
z-index: 1;
}
.tour-signup-title {
font-size: 22px;
font-weight: 700;
line-height: 1.35;
}
.tour-signup-subtitle {
margin-top: 8px;
color: rgba(255, 255, 255, 0.86);
font-size: 13px;
line-height: 1.5;
}
.tour-signup-card {
margin: 10px 12px 0;
padding: 14px 14px 16px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
}
.tour-signup-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding-bottom: 12px;
border-bottom: 1px solid #eef2f7;
}
.tour-signup-card__title {
color: #111827;
font-size: 17px;
font-weight: 700;
line-height: 1.4;
}
.tour-signup-card__year {
flex-shrink: 0;
color: #0f74bc;
font-size: 13px;
font-weight: 600;
}
.tour-signup-notice {
margin-top: 14px;
color: #334155;
font-size: 15px;
line-height: 1.8;
word-break: break-word;
}
.tour-signup-notice /deep/ img,
.tour-signup-notice /deep/ video {
max-width: 100%;
height: auto;
}
.tour-signup-notice /deep/ #pdf-container {
min-height: 560px;
}
.tour-signup-empty {
padding: 34px 0 26px;
}
.tour-signup-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 -6px 18px rgba(15, 23, 42, 0.08);
box-sizing: border-box;
}
</style>
<div id="app" class="tour-signup-h5">
<van-nav-bar title="疗休养报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="tour-signup-banner">
<van-swipe class="tour-signup-swipe" :autoplay="3500" indicator-color="white">
<van-swipe-item v-for="item in bannerList" :key="item">
<img :src="item" alt="疗休养报名">
</van-swipe-item>
</van-swipe>
<div class="tour-signup-banner__text">
<div class="tour-signup-title">疗休养报名</div>
</div>
</div>
<div class="tour-signup-card">
<div class="tour-signup-card__header">
<div class="tour-signup-card__title">服务须知</div>
<div class="tour-signup-card__year">{{ setting.year || currentYear }}年度</div>
</div>
<van-skeleton title :row="8" :loading="loading">
<div v-if="setting.serviceNotice" class="tour-signup-notice">
<pdf-preview :key="serviceNoticeKey" :content="setting.serviceNotice"></pdf-preview>
</div>
<van-empty v-else class="tour-signup-empty" description="暂无服务须知"></van-empty>
</van-skeleton>
</div>
<div class="tour-signup-footer">
<van-button block type="info" color="#0f74bc" round @click="confirmRead">我已阅读</van-button>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
components: {
"pdf-preview": httpVueLoader("/components/plugins/sysFilePreview/PdfIndex.vue?v=" + new Date().getTime())
},
data() {
return {
loading: false,
currentYear: new Date().getFullYear(),
serviceNoticeKey: "",
bannerList: [
"/assets/platform/images/tour/tour-h5-banner-1.jpg",
"/assets/platform/images/tour/tour-h5-banner-2.jpg"
],
setting: {
year: "",
configName: "",
serviceNotice: ""
}
}
},
methods: {
loadServiceNotice() {
this.loading = true
this.$axios.post("/platform/tour/signup/h5/serviceNotice").then((res) => {
this.loading = false
if (res.code === 0) {
this.setting = Object.assign({}, this.setting, res.data || {})
this.serviceNoticeKey = (this.setting.year || this.currentYear) + "_" + new Date().getTime()
} else {
vant.Toast(res.msg || "服务须知加载失败")
}
}).catch(() => {
this.loading = false
})
},
confirmRead() {
window.location.href = "/platform/tour/signup/h5/signup"
}
},
created() {
this.loadServiceNotice()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,559 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
#app {
height: 100vh;
overflow: hidden;
}
.tour-detail-page {
height: calc(100vh - 46px);
overflow: hidden;
display: flex;
flex-direction: column;
padding: 10px 10px 70px;
background: #f4f6f8;
box-sizing: border-box;
}
.tour-detail-header {
padding: 14px 12px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
}
.tour-detail-title {
color: #0f172a;
font-size: 18px;
font-weight: 700;
line-height: 1.35;
word-break: break-word;
}
.tour-detail-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.tour-line-tag {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 7px;
border-radius: 4px;
background: #0b75bd;
color: #ffffff;
font-size: 12px;
font-weight: 600;
line-height: 20px;
}
.tour-detail-meta {
margin-top: 10px;
color: #334155;
font-size: 13px;
line-height: 1.7;
word-break: break-word;
}
.tour-detail-meta-row {
display: flex;
align-items: flex-start;
min-width: 0;
}
.tour-detail-meta-label {
width: 72px;
flex-shrink: 0;
display: flex;
justify-content: space-between;
color: #64748b;
white-space: nowrap;
}
.tour-detail-meta-value {
min-width: 0;
flex: 1;
}
.tour-detail-section {
margin-top: 10px;
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
padding: 12px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
box-sizing: border-box;
}
.tour-detail-section-title {
flex-shrink: 0;
margin-bottom: 8px;
padding-left: 8px;
border-left: 3px solid #0b75bd;
color: #0f172a;
font-size: 15px;
font-weight: 700;
line-height: 1.2;
}
.tour-detail-content /deep/ img {
max-width: 100%;
height: auto;
}
.tour-detail-section-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.tour-pdf-preview {
min-height: 220px;
}
.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 {
min-height: 180px;
}
.tour-pdf-page {
margin-bottom: 10px;
border-radius: 6px;
overflow: hidden;
background: #f8fafc;
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
}
.tour-pdf-page canvas {
width: 100%;
display: block;
}
.tour-detail-empty {
padding: 18px 0;
color: #94a3b8;
font-size: 13px;
text-align: center;
}
.tour-detail-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
padding: 9px 12px calc(9px + env(safe-area-inset-bottom));
border-top: 1px solid #edf0f4;
background: #ffffff;
box-sizing: border-box;
}
</style>
<div id="app">
<van-nav-bar title="线路详情" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="tour-detail-page">
<van-loading v-if="detailLoading" size="24px" vertical>加载中...</van-loading>
<template v-else>
<div class="tour-detail-header">
<div class="tour-detail-title">{{ lineDetail.lineName || signupDetail.lineName || '线路详情' }}</div>
<div class="tour-detail-tags">
<span v-if="lineDetail.lotName || signupDetail.lotName" class="tour-line-tag">{{ lineDetail.lotName || signupDetail.lotName }}</span>
<span v-if="lineDetail.lineType || signupDetail.lineType" class="tour-line-tag">{{ lineDetail.lineType || signupDetail.lineType }}</span>
<span v-if="signupDetail.unionName" class="tour-line-tag">{{ signupDetail.unionName }}</span>
</div>
<div class="tour-detail-meta">
<div v-if="travelPeriod(signupDetail)" class="tour-detail-meta-row">
<span class="tour-detail-meta-label"><span>出行时间</span><span></span></span>
<span class="tour-detail-meta-value">{{ travelPeriod(signupDetail) }}</span>
</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>
</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.contactName || '暂无' }}</span>
</div>
<div class="tour-detail-meta-row">
<span class="tour-detail-meta-label"><span>联系方式</span><span></span></span>
<span class="tour-detail-meta-value">{{ lineDetail.contactPhone || signupDetail.contactPhone || '暂无' }}</span>
</div>
</div>
</div>
<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>
<div v-else class="tour-detail-empty">暂无线路介绍</div>
</div>
</div>
</template>
</div>
<div class="tour-detail-footer">
<van-button :type="detailActionType()" block round :disabled="detailActionDisabled()" @click="handleDetailAction">
{{ detailActionText() }}
</van-button>
</div>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
detailLoading: true,
lineDetail: {},
signupDetail: {},
lineContentHref: "",
pdfDoc: null,
pdfLoadingTask: null,
pdfLoading: false,
pdfError: "",
pdfPageCount: 0,
pdfRenderedPages: 0,
pdfRenderToken: 0,
matterId: GetQueryString("matterId") || "",
lineId: GetQueryString("lineId") || ""
}
},
methods: {
travelPeriod(row) {
if (!row) {
return ""
}
if (row.travelStartTime && row.travelEndTime) {
return row.travelStartTime + " 至 " + row.travelEndTime
}
return row.travelPeriod || row.travelStartTime || row.travelEndTime || ""
},
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
isSigned() {
return !!(this.signupDetail && this.signupDetail.signed)
},
isApprovalSignup() {
return this.isSigned() && !!this.signupDetail.instanceId
},
canRevokeSignup() {
return this.isApprovalSignup() && this.toBoolean(this.signupDetail.canRevoke) && !!this.signupDetail.startTaskId
},
isFinishedSignup() {
return this.isApprovalSignup() && Number(this.signupDetail.instanceState) === 20
},
isRejectedSignup() {
return this.isApprovalSignup() && Number(this.signupDetail.instanceState) === 45
},
canModifySignup() {
if (!this.isSigned()) {
return true
}
if (!this.isApprovalSignup()) {
return true
}
return !this.isFinishedSignup() && !this.isRejectedSignup() && this.signupDetail.taskKey === "startTask"
},
detailActionText() {
if (this.isSigned()) {
if (this.canRevokeSignup()) {
return "撤回"
}
if (this.isFinishedSignup()) {
return "已完成"
}
if (this.isRejectedSignup()) {
return "已拒绝"
}
return this.canModifySignup() ? "修改报名" : "审核中"
}
return "我要报名"
},
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
}
this.onApply()
},
parseLineContentHref(content) {
if (!content) {
return ""
}
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 ""
}
},
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) {
return
}
const pdfLib = this.getPdfLib()
if (!pdfLib || !pdfLib.getDocument) {
this.pdfError = "PDF预览组件加载失败"
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加载失败,请稍后重试"
})
},
renderPdfPage(pageNumber, token) {
if (!this.pdfDoc || token !== this.pdfRenderToken || pageNumber > this.pdfPageCount) {
return Promise.resolve()
}
return this.pdfDoc.getPage(pageNumber).then((page) => {
if (token !== this.pdfRenderToken || !this.$refs.pdfPreviewContainer) {
return
}
const container = this.$refs.pdfPreviewContainer
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.appendChild(canvas)
container.appendChild(pageEl)
const renderContext = {
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)
}
})
})
},
renderRestPdfPages(pageNumber, token) {
if (token !== this.pdfRenderToken || !this.pdfDoc || pageNumber > this.pdfPageCount) {
this.pdfLoading = false
return
}
setTimeout(() => {
this.renderPdfPage(pageNumber, token).then(() => {
this.renderRestPdfPages(pageNumber + 1, token)
}).catch(() => {
if (token === this.pdfRenderToken) {
this.pdfLoading = false
this.pdfError = "部分页面加载失败"
}
})
}, 120)
},
loadDetail() {
if (!this.matterId || !this.lineId) {
this.detailLoading = false
vant.Dialog.alert({
title: "提示",
message: "线路参数不完整",
confirmButtonColor: "#1867b0"
})
return
}
Promise.all([
this.$axios.post("/platform/tour/signup/lineDetail", { lineId: this.lineId }),
this.$axios.post("/platform/tour/signup/signupDetail", { matterId: this.matterId })
]).then(([lineRes, signupRes]) => {
this.detailLoading = false
if (lineRes.code !== 0) {
vant.Toast(lineRes.msg || "线路详情加载失败")
} else {
this.lineDetail = lineRes.data || {}
this.lineContentHref = this.parseLineContentHref(this.lineDetail.lineContent)
this.$nextTick(() => {
this.renderLinePdf()
})
}
if (signupRes.code !== 0) {
vant.Toast(signupRes.msg || "报名信息加载失败")
} else {
const data = signupRes.data || {}
const process = data.process || {}
this.signupDetail = Object.assign({}, data.matter || {}, {
signed: !!(data.ledger && data.ledger.id)
}, process)
}
}).catch(() => {
this.detailLoading = false
vant.Toast("线路详情加载失败")
})
},
onApply() {
if (!this.matterId) {
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"
})
}
})
},
revokeSignup() {
vant.Dialog.confirm({
title: "提示",
message: "您确定要撤回吗?",
confirmButtonColor: "#ee2f2f"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", { taskId: this.signupDetail.startTaskId }).then((resp) => {
if (resp.code === 0) {
vant.Toast.success(resp.msg || "撤回成功")
this.loadDetail()
} else {
vant.Dialog.alert({ title: "提示", message: resp.msg || "撤回失败", confirmButtonColor: "#1867b0" })
}
})
}).catch(() => {})
}
},
created() {
this.loadDetail()
},
beforeDestroy() {
this.clearPdfPreview()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,787 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.tour-line-page {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background: #f4f6f8;
box-sizing: border-box;
}
.tour-line-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
padding-bottom: 18px;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
.tour-line-toolbar {
padding: 10px 0 8px 16px;
background: #f4f6f8;
}
.tour-line-toolbar /deep/ .van-search {
flex: 1;
min-width: 0;
padding: 0;
background: transparent;
}
.tour-line-toolbar .van-search {
flex: 1;
min-width: 0;
padding: 0;
background: transparent;
}
.tour-line-toolbar /deep/ .van-search__content {
height: 38px;
border-radius: 8px;
background: #ffffff;
}
.tour-line-toolbar .van-search__content {
height: 38px;
border-radius: 8px;
background: #ffffff;
}
.tour-line-toolbar /deep/ .van-search__action {
padding: 0 0 0 6px;
}
.tour-line-toolbar .van-search__action {
padding: 0 0 0 6px;
}
.tour-line-search-button {
width: 42px;
height: 38px;
border: 0;
border-radius: 8px;
background: #1e88e5;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
line-height: 1;
}
.tour-line-search-row {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
box-sizing: border-box;
}
.tour-line-union-entry {
width: 48px;
min-height: 42px;
flex-shrink: 0;
border: 0;
padding: 0;
background: transparent;
color: #64748b;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
font-size: 11px;
line-height: 1.2;
}
.tour-line-union-entry .van-icon {
color: #64748b;
font-size: 24px;
line-height: 1;
}
.tour-line-union-entry--active,
.tour-line-union-entry--active .van-icon {
color: #0b75bd;
}
.tour-line-union-popup {
max-height: 68vh;
display: flex;
flex-direction: column;
}
.tour-line-union-popup__title {
position: sticky;
top: 0;
z-index: 1;
padding: 14px 16px 10px;
background: #ffffff;
color: #0f172a;
font-size: 16px;
font-weight: 700;
text-align: center;
box-sizing: border-box;
}
.tour-line-union-popup__list {
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
}
.tour-line-union-popup__item {
min-height: 48px;
transition: background-color 0.18s ease, color 0.18s ease;
}
.tour-line-union-popup__item:hover,
.tour-line-union-popup__item:active {
background: rgba(30, 136, 229, 0.1);
}
.tour-line-union-popup__item:hover /deep/ .van-cell__title,
.tour-line-union-popup__item:active /deep/ .van-cell__title {
color: #0b75bd;
}
.tour-line-union-popup__item--active /deep/ .van-cell__title {
color: #0b75bd;
font-weight: 700;
}
.tour-line-shortcuts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-top: 10px;
}
.tour-line-shortcut {
min-height: 76px;
padding: 12px 8px;
border: 1px solid transparent;
border-radius: 8px;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
box-sizing: border-box;
}
.tour-line-shortcut--active {
border-color: #0b75bd;
background: rgba(11, 117, 189, 0.08);
}
.tour-line-shortcut__icon {
width: 42px;
height: 42px;
flex-shrink: 0;
display: block;
border-radius: 50%;
object-fit: cover;
}
.tour-line-shortcut__text {
min-width: 0;
color: #0f172a;
font-size: 13px;
font-weight: 700;
line-height: 1.3;
text-align: center;
}
.tour-line-shortcut__hint {
margin-top: 2px;
color: #64748b;
font-size: 11px;
font-weight: 400;
}
.tour-line-list {
padding: 0 8px;
}
.tour-line-card {
position: relative;
display: flex;
gap: 12px;
min-height: 112px;
margin: 8px 0;
padding: 10px;
border-radius: 6px;
background: #ffffff;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
box-sizing: border-box;
}
.tour-line-thumb {
width: 145px;
height: 98px;
flex-shrink: 0;
overflow: hidden;
border-radius: 6px;
background: #e5edf6;
}
.tour-line-thumb img,
.tour-line-thumb .van-image {
width: 100%;
height: 100%;
display: block;
}
.tour-line-thumb-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #8a98a8;
font-size: 12px;
}
.tour-line-main {
min-width: 0;
flex: 1;
}
.tour-line-signed-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
border-radius: 50%;
background: #a78bfa;
color: #ffffff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 13px;
line-height: 1;
box-shadow: 0 2px 5px rgba(167, 139, 250, 0.32);
}
.tour-line-title-row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.tour-line-title {
min-width: 0;
color: #0f172a;
font-size: 16px;
font-weight: 700;
line-height: 1.35;
word-break: break-word;
}
.tour-line-type-badge {
height: 18px;
flex-shrink: 0;
padding: 0 5px;
border: 1px solid #ffd6a8;
border-radius: 4px;
background: #fff3e5;
color: #d97706;
font-size: 11px;
font-weight: 700;
line-height: 17px;
box-sizing: border-box;
}
.tour-line-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 5px;
}
.tour-line-tag {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 7px;
border-radius: 4px;
background: #39a9ed;
color: #ffffff;
font-size: 12px;
font-weight: 600;
line-height: 20px;
}
.tour-line-info {
margin-top: 5px;
color: #111827;
font-size: 13px;
line-height: 1.55;
word-break: break-word;
}
.tour-line-info span {
color: #64748b;
}
.tour-line-actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.tour-line-action {
min-width: 70px;
height: 28px;
padding: 0 10px;
border: 1px solid #1e88e5;
border-radius: 14px;
background: #1e88e5;
color: #ffffff;
font-size: 12px;
line-height: 26px;
}
.tour-line-action--danger {
border-color: #ee2f2f;
background: #ee2f2f;
}
.tour-line-action--disabled {
border-color: #d1d5db;
background: #e5e7eb;
color: #94a3b8;
}
@media (max-width: 360px) {
.tour-line-card {
gap: 9px;
padding: 8px;
}
.tour-line-thumb {
width: 126px;
height: 92px;
}
.tour-line-title {
font-size: 15px;
}
}
</style>
<div id="app" class="tour-line-page">
<van-nav-bar title="疗休养线路" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
<div class="tour-line-toolbar">
<div class="tour-line-search-row">
<van-search
v-model="pageForm.lineName"
show-action
clearable
placeholder="请输入线路名称搜索"
@search="doSearch"
@clear="doSearch">
<template #action>
<button type="button" class="tour-line-search-button" @click="doSearch">
<van-icon name="search"></van-icon>
</button>
</template>
</van-search>
<button
type="button"
class="tour-line-union-entry"
:class="{'tour-line-union-entry--active': !!pageForm.unionId}"
@click="openUnionPopup">
<van-icon name="friends-o"></van-icon>
<span>分工会</span>
</button>
</div>
<div class="tour-line-shortcuts">
<div
class="tour-line-shortcut"
:class="{'tour-line-shortcut--active': pageForm.lineType === '省内线路' && pageForm.directFamilyUnitLine === null}"
@click="toggleLineType('省内线路')">
<img class="tour-line-shortcut__icon" src="/assets/platform/images/tour/tour-in-province.png" alt="省内线路">
<div class="tour-line-shortcut__text">
<div>省内线路</div>
</div>
</div>
<div
class="tour-line-shortcut"
:class="{'tour-line-shortcut--active': pageForm.lineType === '省外线路' && pageForm.directFamilyUnitLine === null}"
@click="toggleLineType('省外线路')">
<img class="tour-line-shortcut__icon" src="/assets/platform/images/tour/tour-out-province.png" alt="省外线路">
<div class="tour-line-shortcut__text">
<div>省外线路</div>
</div>
</div>
<div
class="tour-line-shortcut"
:class="{'tour-line-shortcut--active': pageForm.directFamilyUnitLine === true}"
@click="toggleDirectFamilyLine">
<img class="tour-line-shortcut__icon" src="/assets/platform/images/tour/tour-direct-family.jpg" alt="直系亲属">
<div class="tour-line-shortcut__text">
<div>直系亲属</div>
</div>
</div>
</div>
</div>
<div class="tour-line-scroll">
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="loadData">
<div class="tour-line-list">
<div class="tour-line-card" v-for="row in list" :key="row.matterId" @click="onLine(row)">
<div class="tour-line-thumb">
<van-image v-if="thumbUrl(row)" :src="thumbUrl(row)" fit="cover"></van-image>
<div v-else class="tour-line-thumb-empty">暂无图片</div>
</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>
<div class="tour-line-tags">
<span v-if="row.lotName" class="tour-line-tag">{{ row.lotName }}</span>
<span v-if="row.unionName" class="tour-line-tag">{{ row.unionName }}</span>
</div>
<div class="tour-line-info">
<div><span>出行开始时间:</span>{{ row.travelStartTime || '暂无' }}</div>
<div><span>出行结束时间:</span>{{ row.travelEndTime || '暂无' }}</div>
<!-- 临时屏蔽旅行社信息,保留字段和接口便于后续恢复。
<div><span>旅行社:</span>{{ row.travelAgencyName || '暂无' }}</div>
-->
</div>
<div class="tour-line-actions">
<button
v-if="showSignupAction(row)"
type="button"
class="tour-line-action"
:class="{
'tour-line-action--danger': canRevokeSignup(row),
'tour-line-action--disabled': signupActionDisabled(row)
}"
:disabled="signupActionDisabled(row)"
@click.stop="handleSignupAction(row)">
{{ signupActionText(row) }}
</button>
</div>
</div>
</div>
</div>
</van-list>
<van-empty v-if="!loading && list.length === 0" description="暂无线路"></van-empty>
</div>
<van-popup v-model="showUnionPopup" position="bottom" round>
<div class="tour-line-union-popup">
<div class="tour-line-union-popup__title">选择分工会</div>
<div class="tour-line-union-popup__list">
<van-cell
v-for="item in unionList"
:key="item.id || 'all'"
clickable
class="tour-line-union-popup__item"
:class="{'tour-line-union-popup__item--active': pageForm.unionId === item.id}"
:title="item.name"
@click="selectUnion(item)">
<template #right-icon>
<van-icon v-if="pageForm.unionId === item.id" name="success" color="#0b75bd"></van-icon>
</template>
</van-cell>
</div>
</div>
</van-popup>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
store,
data() {
return {
list: [],
loading: false,
finished: false,
lineChecking: false,
showUnionPopup: false,
unionList: [
{id: "", name: "全部分工会"}
],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
year: new Date().getFullYear(),
lineName: "",
lineType: "",
unionId: "",
directFamilyUnitLine: null,
pageOrderName: "lineName",
pageOrderBy: "ascending"
}
}
},
methods: {
thumbUrl(row) {
return row.lineMobileThumb || row.agencyMobileThumb || ""
},
lineTypeBadge(lineType) {
if (lineType === "省内线路" || lineType === "省内") {
return "省内"
}
if (lineType === "省外线路" || lineType === "省外") {
return "省外"
}
return ""
},
isSigned(row) {
return !!(row && (row.signed === true || row.signed === 1 || row.signed === "1" || row.ledgerId))
},
isApprovalSignup(row) {
return !!(row && this.isSigned(row) && row.instanceId)
},
canRevokeSignup(row) {
return this.isApprovalSignup(row) && this.toBoolean(row.canRevoke) && !!row.startTaskId
},
isFinishedSignup(row) {
return this.isApprovalSignup(row) && Number(row.instanceState) === 20
},
isRejectedSignup(row) {
return this.isApprovalSignup(row) && Number(row.instanceState) === 45
},
canModifySignup(row) {
if (!row || !this.isSigned(row)) {
return true
}
if (!this.isApprovalSignup(row)) {
return true
}
return !this.isFinishedSignup(row) && !this.isRejectedSignup(row) && row.taskKey === "startTask"
},
signupActionText(row) {
if (this.isSigned(row)) {
if (this.canRevokeSignup(row)) {
return "撤回"
}
if (this.isRejectedSignup(row)) {
return "已拒绝"
}
return this.canModifySignup(row) ? "修改" : "审核中"
}
return "报名"
},
showSignupAction(row) {
return this.isSigned(row) && this.canModifySignup(row)
},
signupActionDisabled(row) {
return !!(this.isSigned(row) && this.isApprovalSignup(row) && (this.isRejectedSignup(row) || (!this.canRevokeSignup(row) && !this.canModifySignup(row))))
},
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
handleSignupAction(row) {
if (this.signupActionDisabled(row)) {
return
}
if (this.canRevokeSignup(row)) {
this.revokeSignup(row)
return
}
this.goApply(row)
},
goApply(row) {
if (!row || !row.matterId) {
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"
})
}
})
},
revokeSignup(row) {
vant.Dialog.confirm({
title: "提示",
message: "您确定要撤回吗?",
confirmButtonColor: "#ee2f2f"
}).then(() => {
this.$axios.post("/flow/common/revokeTask", { taskId: row.startTaskId }).then((resp) => {
if (resp.code === 0) {
vant.Toast.success(resp.msg || "撤回成功")
this.doSearch()
} else {
vant.Dialog.alert({ title: "提示", message: resp.msg || "撤回失败", confirmButtonColor: "#1867b0" })
}
})
}).catch(() => {})
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.list = []
this.finished = false
this.loadData()
},
toggleLineType(lineType) {
const active = this.pageForm.lineType === lineType && this.pageForm.directFamilyUnitLine === null
this.pageForm.lineType = active ? "" : lineType
this.pageForm.directFamilyUnitLine = null
this.doSearch()
},
toggleDirectFamilyLine() {
const active = this.pageForm.directFamilyUnitLine === true
this.pageForm.directFamilyUnitLine = active ? null : true
this.pageForm.lineType = ""
this.doSearch()
},
openUnionPopup() {
this.showUnionPopup = true
if (this.unionList.length <= 1) {
this.loadUnionOptions()
}
},
selectUnion(item) {
const unionId = item && item.id ? item.id : ""
this.showUnionPopup = false
if (this.pageForm.unionId === unionId) {
return
}
this.pageForm.unionId = unionId
this.doSearch()
},
loadUnionOptions() {
this.$axios.post("/platform/tour/signup/unionOptions").then((res) => {
if (res.code !== 0) {
return
}
const rows = res.data || []
this.unionList = [{id: "", name: "全部分工会"}].concat(rows.map((item) => {
return {
id: item.id || "",
name: item.name || "未命名分工会"
}
}))
})
},
loadData() {
if (this.finished) {
this.loading = false
return
}
this.loading = true
this.$axios.post("/platform/tour/signup/h5/pageData", this.pageForm).then((res) => {
this.loading = false
if (res.code !== 0) {
vant.Toast(res.msg || "线路加载失败")
this.finished = true
return
}
const data = res.data || {}
const rows = data.list || []
this.pageForm.totalCount = data.totalCount || 0
this.list = this.pageForm.pageNumber === 1 ? rows : this.list.concat(rows)
if (this.list.length >= this.pageForm.totalCount || rows.length === 0) {
this.finished = true
} else {
this.pageForm.pageNumber += 1
}
}).catch(() => {
this.loading = false
this.finished = true
})
},
onLine(row) {
if (!row || !row.lineId || !row.matterId) {
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"
})
})
}
},
created() {
this.loadUnionOptions()
this.loadData()
}
})
</script>
<!--#
}
#-->

Some files were not shown because too many files have changed in this diff Show More