移植v3疗休养到这个版本上

This commit is contained in:
2026-09-03 10:30:00 +08:00
parent ffe16d42df
commit 248b661320
11 changed files with 1806 additions and 94 deletions
@@ -157,12 +157,14 @@ public class RecuperationEvaluateStatisticsController {
List<ExcelExportEntity> columns = new ArrayList<>();
columns.add(new ExcelExportEntity("序号", "no", 10));
columns.add(new ExcelExportEntity("报名方式", "typeName", 12));
columns.add(new ExcelExportEntity("方案名称", "schemeName", 24));
columns.add(new ExcelExportEntity("方案名称", "projectName", 24));
columns.add(new ExcelExportEntity("旅行社", "travelAgencyName", 22));
columns.add(new ExcelExportEntity("评价人", "userName", 14));
columns.add(new ExcelExportEntity("所属单位", "unitName", 24));
columns.add(new ExcelExportEntity("旅行社评分", "evaluationForTravelAgency", 14));
columns.add(new ExcelExportEntity("住宿/酒店评分", "evaluationForAccommodation", 16));
String accommodationLabel = "线路".equals(typeName) ? "住宿评分"
: ("灵活组团".equals(typeName) || "定点".equals(typeName) ? "酒店评分" : "住宿/酒店评分");
columns.add(new ExcelExportEntity(accommodationLabel, "evaluationForAccommodation", 16));
columns.add(new ExcelExportEntity("行程评分", "evaluationForJourney", 14));
columns.add(new ExcelExportEntity("餐饮评分", "evaluationForDining", 14));
columns.add(new ExcelExportEntity("交通评分", "evaluationForTransportation", 14));
@@ -62,10 +62,11 @@ public class RecuperationFlexibleGroupQueryController {
return Result.success(flexibleGroupService.querySignUsers(pageForm, groupId, groupLeaderUserId, groupLeaderLoginName, noGroupLeader));
}
/** pageForm 传分页信息,groupId 传灵活组团 ID;data 返回全部报名人员的分页结构。 */
@At
@SaCheckLogin
public Result getAllSignUser(PageForm pageForm, String groupId) {
return Result.success(flexibleGroupService.querySignUsers(pageForm, groupId, null, null, false));
return Result.success(flexibleGroupService.queryAllSignUsers(pageForm, groupId));
}
@At
@@ -98,16 +99,16 @@ public class RecuperationFlexibleGroupQueryController {
String searchKeyword, HttpServletResponse response) {
List<ExcelExportEntity> columns = new ArrayList<>();
columns.add(new ExcelExportEntity("序号", "no", 10));
columns.add(new ExcelExportEntity("工号", "loginName", 18));
columns.add(new ExcelExportEntity("姓名", "userName", 14));
columns.add(new ExcelExportEntity("联系电话", "mobile", 18));
columns.add(new ExcelExportEntity("身份证号", "idCard", 24));
columns.add(new ExcelExportEntity("单位", "unitName", 24));
columns.add(new ExcelExportEntity("工会", "unionName", 20));
columns.add(new ExcelExportEntity("旅行社", "travelAgencyName", 22));
columns.add(new ExcelExportEntity("工号", "loginName", 20));
columns.add(new ExcelExportEntity("姓名", "userName", 20));
columns.add(new ExcelExportEntity("联系电话", "mobile", 20));
columns.add(new ExcelExportEntity("身份证号", "idCard", 30));
columns.add(new ExcelExportEntity("单位", "unitName", 30));
columns.add(new ExcelExportEntity("工会", "unionName", 30));
columns.add(new ExcelExportEntity("旅行社", "travelAgencyName", 30));
columns.add(new ExcelExportEntity("报名时间", "signingUptime", 20));
columns.add(new ExcelExportEntity("团长", "leaderName", 20));
columns.add(new ExcelExportEntity("是否成团", "formedTeamState", 14));
columns.add(new ExcelExportEntity("团长", "leaderName", 25));
columns.add(new ExcelExportEntity("是否成团", "formedTeamState", 15));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, columns,
@@ -13,6 +13,9 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/** PC 端线路报名情况及成团通知。 */
@IocBean
@At("/platform/recuperation/lineStatistics")
@@ -48,6 +51,24 @@ public class RecuperationLineStatisticsController {
return Result.success(statisticsService.lineOptions(startYear, endYear, signUpMode, regionalNature));
}
/** lineId 为线路 IDdata 中 selectId 表示具体出行批次,times 为时间及报名人数说明。 */
@At
@SaCheckPermission("recuperation.lineStatistics")
public Result getLinePlayTimes(String lineId, Integer startYear, Integer endYear) {
return Result.success(statisticsService.linePlayTimeOptions(lineId, startYear, endYear));
}
/** 按当前筛选条件导出达到最低成团人数的线路人员,并按线路生成 Excel 压缩包。 */
@At
@Ok("void")
@SaCheckPermission("recuperation.lineStatistics")
public void doExport(Integer startYear, Integer endYear, String unionId, String takePartInLineId,
String lotId, String selectId, String regionalNature,
HttpServletResponse response) throws IOException {
statisticsService.exportFormedLineUsers(startYear, endYear, unionId, takePartInLineId,
lotId, selectId, regionalNature, response);
}
@At
@SaCheckPermission("recuperation.lineStatistics")
public Result countSuccessNotice(String id) { return Result.success(enrollService.countLineGroupNoticeUsers(id, true)); }
@@ -4,6 +4,8 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/** PC 端线路成团统计查询。 */
@@ -14,4 +16,30 @@ public interface RecuperationLineStatisticsService {
Pagination userPage(PageForm pageForm, String lineSelectId, String searchKeyword, String unionId, String unitId);
List<NutMap> lineOptions(Integer startYear, Integer endYear, String signUpMode, String regionalNature);
/**
* 查询指定线路在年度范围内的出行批次选项。
*
* @param lineId 线路 ID
* @param startYear 开始年度
* @param endYear 结束年度
* @return selectId 为工会选线记录 ID,times 为出行时间及报名人数说明
*/
List<NutMap> linePlayTimeOptions(String lineId, Integer startYear, Integer endYear);
/**
* 导出达到最低成团人数的线路报名人员。
*
* @param startYear 开始年度
* @param endYear 结束年度
* @param unionId 报名人员所属工会 ID
* @param lineId 线路 ID
* @param lotId 标段 ID
* @param selectId 工会选线记录 ID,用于限定具体出行批次
* @param regionalNature 线路类型,可传省内、省外或空值
* @param response 返回 ZIP 文件的 HTTP 响应
*/
void exportFormedLineUsers(Integer startYear, Integer endYear, String unionId, String lineId,
String lotId, String selectId, String regionalNature,
HttpServletResponse response) throws IOException;
}
@@ -105,6 +105,15 @@ public interface RecuperationProvinceFlexibleGroupService extends BaseService<Re
Pagination querySignUsers(PageForm pageForm, String groupId, String groupLeaderUserId,
String groupLeaderLoginName, Boolean noGroupLeader);
/**
* 查询一个灵活组团的全部报名人员,并按团长、团内身份和报名时间排序。
*
* @param pageForm 分页信息
* @param groupId 灵活组团 ID
* @return 报名人员分页数据
*/
Pagination queryAllSignUsers(PageForm pageForm, String groupId);
/** 查询年度区间内可用旅行社。 */
List<NutMap> queryTravelAgencyOptions(Integer startYear, Integer endYear);
@@ -1594,7 +1594,7 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
AND rating.evaluationForAccommodation IS NOT NULL AND rating.evaluationForDining IS NOT NULL
AND rating.evaluationForTransportation IS NOT NULL THEN rating.loginName END) AS ratingCount
FROM the_rapy_recuperation_travel_agency agency
LEFT JOIN ($ratingSql) rating ON rating.travelAgencyId=agency.id
LEFT JOIN ($ratingSql) rating ON rating.travelAgencyId=agency.id AND rating.statisticYear=@year
WHERE agency.year=@year
GROUP BY agency.id, agency.travelAgencyName
ORDER BY CASE WHEN avg IS NULL THEN 1 ELSE 0 END, avg DESC, ratingCount DESC, agency.travelAgencyName
@@ -1623,7 +1623,7 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
AND rating.evaluationForAccommodation IS NOT NULL AND rating.evaluationForDining IS NOT NULL AND rating.evaluationForTransportation IS NOT NULL THEN rating.loginName
WHEN rating.typeName IN ('灵活组团','定点') AND rating.evaluationForTravelAgency IS NOT NULL AND rating.evaluationForAccommodation IS NOT NULL THEN rating.loginName END) AS ratingCount
FROM ($ratingSql) rating
WHERE IFNULL(rating.schemeId,'')<>''
WHERE rating.statisticYear=@year AND IFNULL(rating.schemeId,'')<>''
GROUP BY rating.typeName, rating.schemeId, rating.schemeName, rating.travelAgencyName
ORDER BY FIELD(rating.typeName,'线路','灵活组团','定点'), rating.schemeName
""");
@@ -1635,12 +1635,20 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
@Override
public List<NutMap> satisfactionFeedbackDetails(Integer year, String typeName) {
Sql sql = Sqls.create("""
SELECT rating.*,
SELECT rating.*, rating.schemeName AS projectName,
ROUND(CASE WHEN rating.typeName='线路' THEN
(rating.evaluationForTravelAgency+rating.evaluationForJourney+rating.evaluationForAccommodation+rating.evaluationForDining+rating.evaluationForTransportation)/5
ELSE (rating.evaluationForTravelAgency+rating.evaluationForAccommodation)/2 END,1) AS compositeScore
FROM ($ratingSql) rating
WHERE rating.feedbackContent IS NOT NULL AND rating.feedbackContent<>''
WHERE rating.statisticYear=@year AND (
rating.evaluationForTravelAgency IS NOT NULL
OR rating.evaluationForJourney IS NOT NULL
OR rating.evaluationForAccommodation IS NOT NULL
OR rating.evaluationForDining IS NOT NULL
OR rating.evaluationForTransportation IS NOT NULL
OR rating.evaluationForLine IS NOT NULL
OR IFNULL(rating.feedbackContent,'')<>''
)
ORDER BY rating.signingUptime DESC
""");
sql.setVar("ratingSql", satisfactionBaseSql()).setParam("year", year == null ? DateUtil.thisYear() : year);
@@ -1652,9 +1660,10 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
/** 满意度统计与明细共用同一报名方式、方案和旅行社映射。 */
private String satisfactionBaseSql() {
return """
SELECT enroll.id, enroll.loginName, enroll.userName, enroll.unitName, enroll.signingUptime, enroll.feedbackContent,
SELECT enroll.id, enroll.loginName, enroll.userName, enroll.unitName, enroll.signingUptime,
YEAR(enroll.signingUptime) AS statisticYear, enroll.feedbackContent,
enroll.evaluationForTravelAgency, enroll.evaluationForJourney, enroll.evaluationForAccommodation,
enroll.evaluationForDining, enroll.evaluationForTransportation,
enroll.evaluationForDining, enroll.evaluationForTransportation, enroll.evaluationForLine,
CASE WHEN enroll.takePartInLineId IS NOT NULL THEN '线路' WHEN enroll.takePartInBaseManagementId IS NOT NULL THEN '定点' ELSE '灵活组团' END AS typeName,
CASE WHEN enroll.takePartInLineId IS NOT NULL THEN COALESCE(line.id,directLine.id) WHEN enroll.takePartInBaseManagementId IS NOT NULL THEN base.id ELSE COALESCE(flexible.id,enroll.takePartInTravelAgencyId) END AS schemeId,
CASE WHEN enroll.takePartInLineId IS NOT NULL THEN COALESCE(line.lineName,directLine.lineName) WHEN enroll.takePartInBaseManagementId IS NOT NULL THEN base.baseName ELSE COALESCE(flexible.groupName, directAgency.travelAgencyName) END AS schemeName,
@@ -1676,7 +1685,7 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
AND firstFlexible.year=YEAR(enroll.signingUptime) AND firstFlexible.isDisabled=false
ORDER BY firstFlexible.sortNumber, firstFlexible.id LIMIT 1
)
WHERE enroll.isNormal=true AND enroll.isTakePartIn=true AND YEAR(enroll.signingUptime)=@year
WHERE enroll.isNormal=true AND enroll.isTakePartIn=true
""";
}
}
@@ -1,13 +1,20 @@
package com.budwk.app.zhgh.staffbenefit.recuperation.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.recuperation.common.RecuperationCommon;
import com.budwk.app.zhgh.staffbenefit.recuperation.model.RecuperationConfig;
import com.budwk.app.zhgh.staffbenefit.recuperation.model.RecuperationEnroll;
import com.budwk.app.zhgh.staffbenefit.recuperation.service.RecuperationLineStatisticsService;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
@@ -15,7 +22,21 @@ import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/** 线路统计服务,统一限定审核通过且未调出的报名记录。 */
@IocBean(args = {"refer:dao"})
@@ -26,11 +47,12 @@ public class RecuperationLineStatisticsServiceImpl extends BaseServiceImpl<Recup
public Pagination pageData(PageForm pageForm, Integer startYear, Integer endYear, String unionId,
String lineId, String lotId, String signUpMode, String selectId, String regionalNature) {
Sql sql = Sqls.create("""
SELECT line.id, line.lineName, line.regionalNature, lineu.id AS lineUId, lineu.signUpMode,
SELECT line.id, line.id AS lineId, line.lineName, line.regionalNature, lineu.id AS lineUId, lineu.signUpMode,
YEAR(lineu.selectTime) AS year,
CONCAT(DATE_FORMAT(lineu.playStartTime,'%m月%d日'),'-',DATE_FORMAT(lineu.playEndTime,'%m月%d日')) AS linePlayTime,
agency.travelAgencyName, agency.contact, agency.contactMobileNumber,
IF(lineu.signUpMode=2,'校工会',un.name) AS unionName, lot.lotName,
MAX(enroll.takePartInUnionId) AS usUnionId,
COALESCE((SELECT outsideQuota FROM the_rapy_recuperation_config LIMIT 1),0) AS minimumGroupSize,
COUNT(DISTINCT enroll.loginName) AS lineNum,
IF(COALESCE((SELECT familyInfo FROM the_rapy_recuperation_config LIMIT 1),1)=2,
@@ -94,4 +116,193 @@ public class RecuperationLineStatisticsServiceImpl extends BaseServiceImpl<Recup
sql.setCondition(cnd);
return listMap(sql);
}
@Override
public List<NutMap> linePlayTimeOptions(String lineId, Integer startYear, Integer endYear) {
Sql sql = Sqls.create("""
SELECT lineu.id AS selectId,
CONCAT(
DATE_FORMAT(lineu.playStartTime,'%m月%d日'),'-',DATE_FORMAT(lineu.playEndTime,'%m月%d日'),
'(报名人数:',
COUNT(DISTINCT enroll.loginName) +
IF(COALESCE((SELECT familyInfo FROM the_rapy_recuperation_config LIMIT 1),1)=2,
(SELECT COUNT(1)
FROM the_rapy_recuperation_enroll_companion companion
WHERE companion.trreId IN (
SELECT signed.id
FROM the_rapy_recuperation_enroll signed
WHERE signed.takePartInLineId=lineu.id
AND signed.isNormal=true
AND signed.stateId NOT IN (2715,2725,2735)
)),
COALESCE(SUM(enroll.familyNumber),0)),
',其中家属:',
IF(COALESCE((SELECT familyInfo FROM the_rapy_recuperation_config LIMIT 1),1)=2,
(SELECT COUNT(1)
FROM the_rapy_recuperation_enroll_companion companion
WHERE companion.trreId IN (
SELECT signed.id
FROM the_rapy_recuperation_enroll signed
WHERE signed.takePartInLineId=lineu.id
AND signed.isNormal=true
AND signed.stateId NOT IN (2715,2725,2735)
)),
COALESCE(SUM(enroll.familyNumber),0)),
'人)'
) AS times
FROM the_rapy_recuperation_line_union_select lineu
LEFT JOIN the_rapy_recuperation_enroll enroll
ON enroll.takePartInLineId=lineu.id
AND enroll.isNormal=true
AND enroll.stateId NOT IN (2715,2725,2735)
$condition
""");
Cnd cnd = Cnd.where("lineu.lineId", "=", lineId)
.andEX("YEAR(lineu.selectTime)", ">=", startYear)
.andEX("YEAR(lineu.selectTime)", "<=", endYear);
if (!AuthUtil.hasRoleOr("sysadmin", "H06", "A06")) {
cnd.and("lineu.unionId", "=", SecurityUtil.getUnionId());
}
cnd.groupBy("lineu.id", "lineu.playStartTime", "lineu.playEndTime");
cnd.asc("lineu.playStartTime");
sql.setCondition(cnd);
return listMap(sql);
}
@Override
public void exportFormedLineUsers(Integer startYear, Integer endYear, String unionId, String lineId,
String lotId, String selectId, String regionalNature,
HttpServletResponse response) throws IOException {
List<NutMap> exportRows = queryLineExportRows(startYear, endYear, unionId, lineId, lotId, selectId, regionalNature);
RecuperationConfig config = dao().fetch(RecuperationConfig.class, Cnd.NEW());
Integer minimumGroupSize = config == null ? null : config.getOutsideQuota();
Map<String, List<NutMap>> batchGroups = exportRows.stream().collect(Collectors.groupingBy(
row -> StrUtil.blankToDefault(row.getString("takePartInLineId"), ""),
LinkedHashMap::new,
Collectors.toList()));
List<NutMap> formedRows = batchGroups.values().stream()
.filter(rows -> isFormedGroup(rows, minimumGroupSize))
.flatMap(List::stream)
.collect(Collectors.toList());
Map<String, List<NutMap>> lineGroups = formedRows.stream().collect(Collectors.groupingBy(
row -> StrUtil.blankToDefault(row.getString("lineId"), ""),
LinkedHashMap::new,
Collectors.toList()));
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment;filename="
+ URLEncoder.encode("疗休养集体线路报名人员名单.zip", StandardCharsets.UTF_8));
Set<String> zipEntryNames = new HashSet<>();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()))) {
for (List<NutMap> rows : lineGroups.values()) {
String lineName = rows.isEmpty() ? "未命名" : StrUtil.blankToDefault(rows.get(0).getString("lineName"), "未命名");
writeExcelToZip(zipOutputStream, zipEntryNames, lineName, rows);
}
zipOutputStream.flush();
}
response.flushBuffer();
}
/** 查询导出所需的审核通过线路报名人员,并按当前登录角色限制可见工会。 */
private List<NutMap> queryLineExportRows(Integer startYear, Integer endYear, String unionId, String lineId,
String lotId, String selectId, String regionalNature) {
Sql sql = Sqls.create("""
SELECT line.id AS lineId, line.lineName, line.regionalNature, agency.travelAgencyName,
enroll.*, DATE_FORMAT(lineu.playStartTime,'%Y-%m-%d') AS playStartTime,
IF(COALESCE((SELECT familyInfo FROM the_rapy_recuperation_config LIMIT 1),1)=2,
(SELECT COUNT(1) FROM the_rapy_recuperation_enroll_companion companion WHERE companion.trreId=enroll.id),
COALESCE(enroll.familyNumber,0)) AS familyCount,
(SELECT GROUP_CONCAT(
companion.userName,'-',companion.sex,'',companion.idcard,'、',companion.mobile,'-',
IFNULL(bed.bedType,''),',家属备注:',IFNULL(companion.remark,'')
SEPARATOR '')
FROM the_rapy_recuperation_enroll_companion companion
LEFT JOIN the_rapy_recuperation_enroll_bed bed ON bed.id=companion.bedInfoId
WHERE companion.trreId=enroll.id) AS companionInfo,
user_info.arrivalAtSchoolDate AS schoolTime
FROM the_rapy_recuperation_enroll enroll
LEFT JOIN sys_user user_info ON user_info.loginname=enroll.loginName
LEFT JOIN the_rapy_recuperation_line_union_select lineu ON lineu.id=enroll.takePartInLineId
LEFT JOIN the_rapy_recuperation_line line ON line.id=lineu.lineId
LEFT JOIN the_rapy_recuperation_travel_agency agency ON agency.id=lineu.travelAgencyId
$condition
""");
Cnd cnd = Cnd.where("enroll.stateId", "=", 2750)
.and("enroll.isNormal", "=", true)
.and("enroll.takePartInLineId", "is not", null)
.and("enroll.takePartInLineId", "!=", "")
.andEX("YEAR(enroll.signingUptime)", ">=", startYear)
.andEX("YEAR(enroll.signingUptime)", "<=", endYear)
.andEX("line.id", "=", lineId)
.andEX("line.lotId", "=", lotId)
.andEX("lineu.id", "=", selectId)
.andEX("line.regionalNature", "=", regionalNature);
if (AuthUtil.hasRoleOr("sysadmin", "H06", "A06")) {
cnd.andEX("enroll.selfUnionId", "=", unionId);
} else {
cnd.and(Cnd.exps("enroll.takePartInUnionId", "=", SecurityUtil.getUnionId())
.or("enroll.selfUnionId", "=", SecurityUtil.getUnionId()));
}
cnd.asc("line.lineName").asc("lineu.playStartTime").asc("enroll.unionName");
sql.setCondition(cnd);
List<NutMap> rows = listMap(sql);
rows.forEach(row -> row.setv("remark", RecuperationCommon.getRemarkBySchoolTime(row.getString("schoolTime"))));
return rows;
}
/** 最低成团人数同时计算去重后的教职工人数和家属人数。 */
private boolean isFormedGroup(List<NutMap> rows, Integer minimumGroupSize) {
if (minimumGroupSize == null || minimumGroupSize <= 0) {
return false;
}
long teacherCount = rows.stream().map(row -> row.getString("loginName"))
.filter(StrUtil::isNotBlank).distinct().count();
int familyCount = rows.stream().mapToInt(row -> row.getInt("familyCount", 0)).sum();
return teacherCount + familyCount >= minimumGroupSize;
}
/** 每条线路生成一个 XSSF 工作簿后写入 ZIP,避免工作簿内部流与外层 ZIP 冲突。 */
private void writeExcelToZip(ZipOutputStream zipOutputStream, Set<String> zipEntryNames,
String rawLineName, List<NutMap> rows) throws IOException {
String safeLineName = rawLineName.replaceAll("[\\\\/:*?\"<>|]", "_").replaceAll("[\\r\\n]", "").trim();
String entryName = "集体线路报名人员/" + StrUtil.blankToDefault(safeLineName, "未命名") + ".xlsx";
int index = 1;
while (!zipEntryNames.add(entryName)) {
entryName = "集体线路报名人员/" + StrUtil.blankToDefault(safeLineName, "未命名") + "-" + index++ + ".xlsx";
}
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
try (Workbook workbook = ExcelExportUtil.exportExcel(exportParams, lineExportColumns(), rows);
ByteArrayOutputStream excelOutputStream = new ByteArrayOutputStream()) {
workbook.write(excelOutputStream);
zipOutputStream.putNextEntry(new ZipEntry(entryName));
zipOutputStream.write(excelOutputStream.toByteArray());
zipOutputStream.closeEntry();
}
}
/** 旧版成团线路人员导出的列结构。 */
private List<ExcelExportEntity> lineExportColumns() {
List<ExcelExportEntity> columns = new ArrayList<>();
columns.add(new ExcelExportEntity("姓名", "userName", 20));
columns.add(new ExcelExportEntity("工号", "loginName", 20));
columns.add(new ExcelExportEntity("性别", "sex", 10));
columns.add(new ExcelExportEntity("所属单位", "unitName", 20));
columns.add(new ExcelExportEntity("所属工会", "unionName", 20));
columns.add(new ExcelExportEntity("身份证号", "idCard", 30));
columns.add(new ExcelExportEntity("手机号", "mobile", 15));
columns.add(new ExcelExportEntity("备注", "remark", 30));
ExcelExportEntity signingTime = new ExcelExportEntity("报名时间", "signingUptime", 20);
signingTime.setFormat("yyyy-MM-dd HH:mm:ss");
columns.add(signingTime);
columns.add(new ExcelExportEntity("出行时间", "playStartTime", 20));
columns.add(new ExcelExportEntity("线路名称", "lineName", 20));
columns.add(new ExcelExportEntity("旅行社", "travelAgencyName", 20));
columns.add(new ExcelExportEntity("家属人数", "familyCount", 12));
ExcelExportEntity companionInfo = new ExcelExportEntity("家属信息", "companionInfo", 50);
companionInfo.setWrap(true);
columns.add(companionInfo);
return columns;
}
}
@@ -380,6 +380,9 @@ public class RecuperationProvinceFlexibleGroupServiceImpl extends BaseServiceImp
@Override
public Pagination queryPage(PageForm pageForm, Integer startYear, Integer endYear, String groupName,
String travelAgencyId, String searchKeyword) {
if (StrUtil.isNotBlank(searchKeyword)) {
return queryPersonMatchedGroups(pageForm, startYear, endYear, groupName, travelAgencyId, searchKeyword);
}
Sql sql = Sqls.create("""
SELECT fg.id, fg.groupName, fg.year, ta.travelAgencyName,
COUNT(DISTINCT CASE WHEN enroll.groupLeaderUserId IS NOT NULL AND enroll.groupLeaderUserId <> ''
@@ -412,6 +415,53 @@ public class RecuperationProvinceFlexibleGroupServiceImpl extends BaseServiceImp
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
/** 姓名或工号查询时按匹配人员展开,并保留组团总团队数和总报名人数。 */
private Pagination queryPersonMatchedGroups(PageForm pageForm, Integer startYear, Integer endYear,
String groupName, String travelAgencyId, String searchKeyword) {
Sql sql = Sqls.create("""
SELECT fg.id, CONCAT(fg.id,'_',enroll.id) AS rowKey, fg.groupName, fg.year,
ta.travelAgencyName, enroll.userName AS signUserName, enroll.loginName AS signLoginName,
(SELECT COUNT(DISTINCT CASE
WHEN grouped.groupLeaderUserId IS NOT NULL AND grouped.groupLeaderUserId<>'' THEN grouped.groupLeaderUserId
WHEN grouped.groupLeaderLoginName IS NOT NULL AND grouped.groupLeaderLoginName<>'' THEN grouped.groupLeaderLoginName
END)
FROM the_rapy_recuperation_enroll grouped
WHERE grouped.takePartInTravelAgencyId=fg.travelAgencyId
AND YEAR(grouped.signingUptime)=fg.year
AND grouped.isNormal=true
AND grouped.stateId NOT IN ($auditFailStates)) AS groupCount,
(SELECT COUNT(DISTINCT grouped.loginName)
FROM the_rapy_recuperation_enroll grouped
WHERE grouped.takePartInTravelAgencyId=fg.travelAgencyId
AND YEAR(grouped.signingUptime)=fg.year
AND grouped.isNormal=true
AND grouped.stateId NOT IN ($auditFailStates)) AS signCount
FROM the_rapy_recuperation_province_flexible_group fg
LEFT JOIN the_rapy_recuperation_travel_agency ta ON ta.id=fg.travelAgencyId
LEFT JOIN the_rapy_recuperation_enroll enroll
ON enroll.takePartInTravelAgencyId=fg.travelAgencyId
AND YEAR(enroll.signingUptime)=fg.year
AND enroll.isNormal=true
AND enroll.stateId NOT IN ($auditFailStates)
$condition
""");
Cnd cnd = Cnd.where("ta.signUpTravelAgency", "=", true)
.andEX("fg.year", ">=", startYear)
.andEX("fg.year", "<=", endYear)
.andEX("fg.travelAgencyId", "=", travelAgencyId)
.and(Cnd.likeEX("fg.groupName", groupName))
.and(Cnd.exps("enroll.userName", "like", "%" + searchKeyword + "%")
.or("enroll.loginName", "like", "%" + searchKeyword + "%"));
if (!AuthUtil.hasRoleOr("sysadmin", "A06") && AuthUtil.hasRoleOr("H04")) {
cnd.and("fg.createUnionId", "=", SecurityUtil.getUnionId());
}
cnd.groupBy("fg.id", "enroll.id");
cnd.desc("fg.year").asc("fg.groupName").asc("enroll.userName");
sql.setVar("auditFailStates", auditFailStates());
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public List<NutMap> queryGroupInfo(String groupId) {
Sql sql = Sqls.create("""
@@ -462,11 +512,44 @@ public class RecuperationProvinceFlexibleGroupServiceImpl extends BaseServiceImp
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public Pagination queryAllSignUsers(PageForm pageForm, String groupId) {
Sql sql = Sqls.create("""
SELECT enroll.id, enroll.loginName, enroll.userName,
CASE WHEN enroll.groupLeaderLoginName IS NOT NULL
AND enroll.groupLeaderLoginName<>''
AND enroll.loginName=enroll.groupLeaderLoginName
THEN '团长' ELSE '队员' END AS identity,
enroll.unionName, enroll.unitName, enroll.sex, enroll.mobile,
(SELECT COUNT(1) FROM the_rapy_recuperation_enroll_companion WHERE trreId=enroll.id) AS isFamily
FROM the_rapy_recuperation_enroll enroll
WHERE enroll.takePartInTravelAgencyId=(
SELECT travelAgencyId FROM the_rapy_recuperation_province_flexible_group WHERE id=@groupId)
AND YEAR(enroll.signingUptime)=(
SELECT year FROM the_rapy_recuperation_province_flexible_group WHERE id=@groupId)
AND enroll.isNormal=true
AND enroll.stateId NOT IN ($auditFailStates)
ORDER BY enroll.groupLeaderUserName,
CASE WHEN enroll.groupLeaderLoginName IS NOT NULL
AND enroll.groupLeaderLoginName<>''
AND enroll.loginName=enroll.groupLeaderLoginName
THEN 0 ELSE 1 END,
enroll.signingUptime
""");
sql.setParam("groupId", groupId).setVar("auditFailStates", auditFailStates());
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
/** 过滤浏览器可能传入的 undefined/null 文本,避免错误命中团长条件。 */
private boolean isValidGroupLeaderParam(String value) {
return StrUtil.isNotBlank(value) && !"undefined".equalsIgnoreCase(value) && !"null".equalsIgnoreCase(value);
}
/** 灵活组团查询排除分工会、线路工会和校工会审核失败状态。 */
private String auditFailStates() {
return RecuperationState.UNITFAIL + "," + RecuperationState.LINEUNITFAIL + "," + RecuperationState.SCHOOLFAIL;
}
@Override
public List<NutMap> queryTravelAgencyOptions(Integer startYear, Integer endYear) {
Sql sql = Sqls.create("""
@@ -498,24 +581,52 @@ public class RecuperationProvinceFlexibleGroupServiceImpl extends BaseServiceImp
Sql sql = Sqls.create("""
SELECT enroll.loginName, enroll.userName, enroll.mobile, enroll.idCard, enroll.unitName, enroll.unionName,
ta.travelAgencyName, DATE_FORMAT(enroll.signingUptime,'%Y-%m-%d %H:%i:%s') AS signingUptime,
CONCAT(IFNULL(enroll.groupLeaderUserName,''), IF(enroll.groupLeaderLoginName IS NULL OR enroll.groupLeaderLoginName='', '', CONCAT('',enroll.groupLeaderLoginName,''))) AS leaderName,
CASE WHEN (SELECT COUNT(DISTINCT sameLeader.loginName) FROM the_rapy_recuperation_enroll sameLeader
WHERE sameLeader.takePartInTravelAgencyId=enroll.takePartInTravelAgencyId
AND sameLeader.groupLeaderLoginName=enroll.groupLeaderLoginName AND sameLeader.isNormal=true
AND YEAR(sameLeader.signingUptime)=YEAR(enroll.signingUptime))>=3 THEN '已成团' ELSE '未成团' END AS formedTeamState
CASE
WHEN (enroll.groupLeaderUserName IS NULL OR enroll.groupLeaderUserName='')
AND (enroll.groupLeaderLoginName IS NULL OR enroll.groupLeaderLoginName='') THEN '未选择'
WHEN enroll.groupLeaderLoginName IS NULL OR enroll.groupLeaderLoginName='' THEN enroll.groupLeaderUserName
WHEN enroll.groupLeaderUserName IS NULL OR enroll.groupLeaderUserName='' THEN enroll.groupLeaderLoginName
ELSE CONCAT(enroll.groupLeaderUserName,'',enroll.groupLeaderLoginName,'')
END AS leaderName,
CASE WHEN (
SELECT COUNT(DISTINCT sameLeader.loginName)
FROM the_rapy_recuperation_enroll sameLeader
WHERE sameLeader.takePartInTravelAgencyId=enroll.takePartInTravelAgencyId
AND ((enroll.groupLeaderUserId IS NOT NULL AND enroll.groupLeaderUserId<>''
AND sameLeader.groupLeaderUserId=enroll.groupLeaderUserId)
OR (enroll.groupLeaderLoginName IS NOT NULL AND enroll.groupLeaderLoginName<>''
AND sameLeader.groupLeaderLoginName=enroll.groupLeaderLoginName))
AND YEAR(sameLeader.signingUptime)=YEAR(enroll.signingUptime)
AND sameLeader.isNormal=true
AND sameLeader.stateId NOT IN ($auditFailStates)
)>=3 THEN '已成团' ELSE '未成团' END AS formedTeamState
FROM the_rapy_recuperation_enroll enroll
LEFT JOIN the_rapy_recuperation_travel_agency ta ON ta.id=enroll.takePartInTravelAgencyId
LEFT JOIN the_rapy_recuperation_province_flexible_group fg ON fg.travelAgencyId=enroll.takePartInTravelAgencyId AND fg.year=YEAR(enroll.signingUptime)
LEFT JOIN the_rapy_recuperation_province_flexible_group fg ON fg.id=(
SELECT configured.id
FROM the_rapy_recuperation_province_flexible_group configured
WHERE configured.travelAgencyId=enroll.takePartInTravelAgencyId
AND configured.year=YEAR(enroll.signingUptime)
AND configured.isDisabled=false
ORDER BY configured.id
LIMIT 1)
$condition
""");
Cnd cnd = Cnd.where("fg.year", ">=", startYear).and("fg.year", "<=", endYear)
.and("enroll.isNormal", "=", true);
Cnd cnd = Cnd.where("ta.signUpTravelAgency", "=", true)
.and("fg.id", "is not", null)
.andEX("fg.year", ">=", startYear)
.andEX("fg.year", "<=", endYear)
.and("enroll.isNormal", "=", true)
.and("enroll.stateId", "not in", Lang.array(RecuperationState.UNITFAIL,
RecuperationState.LINEUNITFAIL, RecuperationState.SCHOOLFAIL));
cnd.andEX("fg.travelAgencyId", "=", travelAgencyId).and(Cnd.likeEX("fg.groupName", groupName));
if (StrUtil.isNotBlank(searchKeyword)) {
cnd.and(Cnd.exps("enroll.userName", "like", "%" + searchKeyword + "%").or("enroll.loginName", "like", "%" + searchKeyword + "%"));
}
if (!AuthUtil.hasRoleOr("sysadmin", "A06") && AuthUtil.hasRoleOr("H04")) cnd.and("fg.createUnionId", "=", SecurityUtil.getUnionId());
cnd.asc("fg.year").asc("ta.serialNumber * 1").asc("enroll.groupLeaderUserName").asc("enroll.signingUptime");
cnd.asc("ta.serialNumber * 1").asc("ta.travelAgencyName").asc("enroll.groupLeaderUserName")
.asc("enroll.groupLeaderLoginName").asc("fg.year").asc("enroll.signingUptime");
sql.setVar("auditFailStates", auditFailStates());
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
for (int i = 0; i < list.size(); i++) list.get(i).setv("no", i + 1);
@@ -1,43 +1,289 @@
<!--# layout("/layouts/platform.html"){ #-->
<style>
.query-row {
height: 70px;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.query-row .el-col { overflow: hidden; }
.query-row:not(:last-child) { border-bottom: 1px dashed rgb(230, 230, 230); }
.query-row-title { width: 120px; }
.el-table-container { padding-top: 0; }
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度范围"><el-date-picker v-model="yearRange" type="yearrange" value-format="yyyy" range-separator="至" start-placeholder="开始年度" end-placeholder="结束年度" @change="yearChange"></el-date-picker></search-item>
<search-item label="灵活组团"><el-select v-model="pageForm.groupName" clearable filterable @change="doSearch"><el-option v-for="item in groupOptions" :key="item.id" :label="item.groupName" :value="item.groupName"></el-option></el-select></search-item>
<search-item label="旅行社"><el-select v-model="pageForm.travelAgencyId" clearable filterable @change="doSearch"><el-option v-for="item in agencyOptions" :key="item.id" :label="item.travelAgencyName" :value="item.id"></el-option></el-select></search-item>
<search-item label="姓名/工号"><el-input v-model="pageForm.searchKeyword" clearable @keyup.enter.native="doSearch"></el-input></search-item>
</search>
<el-row align="middle" class="query-row" type="flex">
<el-col class="query-row-title"></el-col>
<el-col class="query-row-content">
<el-row><el-col :span="12">
<span>&emsp;&emsp;度:</span>
<el-date-picker :clearable="false" v-model="pageForm.startYear" type="year" value-format="yyyy" placeholder="选择年" style="width: 38%" @change="yearChange"></el-date-picker>
<span></span>
<el-date-picker :clearable="false" v-model="pageForm.endYear" type="year" value-format="yyyy" placeholder="请选择年" style="width: 38%" @change="yearChange"></el-date-picker>
</el-col></el-row>
</el-col>
</el-row>
<el-row align="middle" class="query-row" type="flex">
<el-col class="query-row-title"></el-col>
<el-col class="query-row-content">
<el-row>
<el-col :span="12">
<span>组团名称:</span>
<el-select v-model="pageForm.groupName" placeholder="请选择组团名称" filterable clearable style="width: 80%" @change="doSearch">
<el-option v-for="item in flexibleGroupList" :key="item.id" :label="item.groupName" :value="item.groupName"></el-option>
</el-select>
</el-col>
<el-col :span="12">
<span>&ensp;&ensp;社:</span>
<el-select v-model="pageForm.travelAgencyId" placeholder="请选择旅行社" filterable clearable style="width: 80%" @change="doSearch">
<el-option v-for="item in travelAgencyList" :key="item.id" :label="item.travelAgencyName + (item.serialNumber ? '(' + item.serialNumber + ')' : '')" :value="item.id"></el-option>
</el-select>
</el-col>
</el-row>
</el-col>
</el-row>
<el-row align="middle" class="query-row" type="flex">
<el-col class="query-row-title"></el-col>
<el-col class="query-row-content">
<el-row>
<el-col :span="12">
<span>姓名/工号:</span>
<el-input :value="pageForm.searchKeyword" clearable placeholder="请输入姓名或工号" style="width: 80%" @input="personKeywordChange" @clear="clearPersonKeyword" @keyup.enter.native="doSearch"></el-input>
</el-col>
<el-col :span="12">
<el-button icon="el-icon-search" size="small" type="primary" @click="doSearch">查询</el-button>
<el-button icon="el-icon-refresh-left" size="small" @click="clearPersonKeyword">重置</el-button>
</el-col>
</el-row>
</el-col>
</el-row>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="省内灵活组团查询"><template slot="right"><el-button type="success" size="small" @click="doExport">导出Excel</el-button></template></table-tool>
<el-table v-loading="tableLoading" :data="tableData" :size="tableSize">
<el-table-column type="index" :index="indexMethod" label="序号" width="60"></el-table-column><el-table-column prop="year" label="年度" width="90"></el-table-column><el-table-column prop="groupName" label="组团名称" min-width="180"></el-table-column><el-table-column prop="travelAgencyName" label="旅行社" min-width="180"></el-table-column><el-table-column prop="groupCount" label="团队数" width="90"></el-table-column><el-table-column prop="signCount" label="报名人数" width="100"></el-table-column><el-table-column label="操作" width="180"><template v-slot="{row}"><el-button size="mini" @click="openGroups(row)">查看团队</el-button><el-button type="primary" size="mini" @click="openAllUsers(row)">全部人员</el-button></template></el-table-column>
<el-card shadow="never" class="mt10">
<table-tool label="灵活组团查询" :app="this" ref="table_tool">
<el-button icon="el-icon-s-promotion" size="small" type="primary" @click="doExport">导出Excel</el-button>
</table-tool>
<el-table :data="tableData" :row-key="tableRowKey" style="width: 100%" ref="table" v-loading="tableLoading" @sort-change="pageOrder">
<el-table-column align="center" header-align="center" type="index" label="序号" width="80" key="#index">
<template v-slot="scope"><span>{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</span></template>
</el-table-column>
<el-table-column v-for="column in tableColumns" v-if="!column.personOnly || pageForm.searchKeyword" :key="column.prop" :label="column.label" :prop="column.prop" :width="column.width" :sortable="column.sortable" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="200">
<template v-slot="{row}"><el-button @click="openView(row)" size="mini" type="primary">查看团</el-button><el-button @click="openViewUser(row)" size="mini" type="primary">查看人员</el-button></template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="团队列表" :visible.sync="groupVisible" width="900px" append-to-body>
<el-table v-loading="dialogLoading" :data="groups"><el-table-column prop="groupLeaderUserName" label="团长姓名"></el-table-column><el-table-column prop="groupLeaderLoginName" label="团长工号"></el-table-column><el-table-column prop="groupLeaderPassword" label="团口令"></el-table-column><el-table-column prop="userCount" label="人数" width="90"></el-table-column><el-table-column label="操作" width="100"><template v-slot="{row}"><el-button type="primary" size="mini" @click="openGroupUsers(row)">人员</el-button></template></el-table-column></el-table>
</template>
</guava>
<el-dialog :visible.sync="userVisible" :close-on-click-modal="false" top="2%" title="报名人员" width="65%">
<el-table :data="signUserData" style="width: 100%" ref="userTable" v-loading="userLoading">
<el-table-column align="center" header-align="center" type="index" label="序号" width="80"></el-table-column>
<el-table-column prop="userName" label="姓名" header-align="center" align="center"></el-table-column>
<el-table-column prop="loginName" label="工号" header-align="center" align="center"></el-table-column>
<el-table-column prop="unionName" label="所属工会" header-align="center" align="center"></el-table-column>
<el-table-column prop="unitName" label="单位" header-align="center" align="center"></el-table-column>
<el-table-column prop="sex" label="性别" header-align="center" align="center"></el-table-column>
<el-table-column prop="mobile" label="手机号" header-align="center" align="center" width="120"></el-table-column>
<el-table-column prop="identity" label="身份" header-align="center" align="center"></el-table-column>
<el-table-column prop="isFamily" label="是否有家属" header-align="center" align="center"><template v-slot="{row}"><span v-if="row.isFamily > 0"></span><span v-else></span></template></el-table-column>
<el-table-column label="操作" width="100"><template v-slot="{row}"><el-button @click="deleteUser(row)" size="mini" type="danger">删除</el-button></template></el-table-column>
</el-table>
<span slot="footer" class="dialog-footer"><el-button @click="closeUserDialog" type="primary">取 消</el-button></span>
</el-dialog>
<el-dialog title="报名人员" :visible.sync="userVisible" width="1050px" append-to-body>
<el-table v-loading="userLoading" :data="users"><el-table-column type="index" label="序号" width="60"></el-table-column><el-table-column prop="loginName" label="工号" width="110"></el-table-column><el-table-column prop="userName" label="姓名" width="100"></el-table-column><el-table-column prop="identity" label="身份" width="80"></el-table-column><el-table-column prop="sex" label="性别" width="60"></el-table-column><el-table-column prop="unitName" label="单位" show-overflow-tooltip></el-table-column><el-table-column prop="unionName" label="工会" show-overflow-tooltip></el-table-column><el-table-column prop="mobile" label="联系电话" width="130"></el-table-column><el-table-column prop="isFamily" label="家属人数" width="90"></el-table-column><el-table-column label="操作" width="80"><template v-slot="{row}"><el-button type="danger" size="mini" @click="deleteUser(row)">删除</el-button></template></el-table-column></el-table>
<el-pagination class="mt20" background layout="total, sizes, prev, pager, next" :current-page="userPage.pageNumber" :page-size="userPage.pageSize" :total="userPage.totalCount" @current-change="userPageChange" @size-change="userSizeChange"></el-pagination>
<el-dialog :visible.sync="groupVisible" :close-on-click-modal="false" top="2%" title="团信息">
<el-table :data="groupTableData" max-height="600" v-loading="dialogLoading">
<el-table-column label="序号" type="index" header-align="center" align="center" width="50"></el-table-column>
<el-table-column prop="groupLeaderUserName" label="团长" header-align="center" align="center" sortable><template v-slot="{row}"><span v-if="row.groupLeaderUserName">{{row.groupLeaderUserName}}{{row.groupLeaderLoginName}}</span><span v-else>未选择</span></template></el-table-column>
<el-table-column prop="groupLeaderPassword" label="口令" header-align="center" align="center"><template v-slot="{row}"><span>{{row.groupLeaderPassword || '-'}}</span></template></el-table-column>
<el-table-column prop="userCount" label="报名人数" header-align="center" align="center" sortable></el-table-column>
<el-table-column label="操作" width="150"><template v-slot="{row}"><el-button @click="openViewUserByTime(row)" size="mini" type="primary">查看人员</el-button></template></el-table-column>
</el-table>
<span slot="footer" class="dialog-footer"><el-button @click="closeGroupDialog" type="primary">取 消</el-button></span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el:'#app',mixins:[initTableMixins],data(){const year=new Date().getFullYear().toString();return{yearRange:[year,year],pageForm:{pageNumber:1,pageSize:10,totalCount:0,startYear:year,endYear:year,groupName:'',travelAgencyId:'',searchKeyword:''},agencyOptions:[],groupOptions:[],currentGroup:{},groups:[],users:[],groupVisible:false,userVisible:false,dialogLoading:false,userLoading:false,userPage:{pageNumber:1,pageSize:10,totalCount:0},userQuery:{}}},
methods:{
pageData(){this.$set(this,'tableLoading',true);this.$axios.post(loc()+'/pageData',this.pageForm).then((res)=>{if(res.code===0){this.$set(this,'tableData',res.data.list||[]);this.$set(this.pageForm,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'tableLoading',false)})},
yearChange(value){this.$set(this.pageForm,'startYear',value&&value.length?value[0]:'');this.$set(this.pageForm,'endYear',value&&value.length?value[1]:'');this.loadOptions();this.doSearch()},
loadOptions(){const params={startYear:this.pageForm.startYear,endYear:this.pageForm.endYear};this.$axios.post(loc()+'/selectTravelAgencyList',params).then((res)=>{if(res.code===0)this.$set(this,'agencyOptions',res.data||[])});this.$axios.post(loc()+'/selectFlexibleGroupList',params).then((res)=>{if(res.code===0)this.$set(this,'groupOptions',res.data||[])})},
doExport(){window.open(loc()+'/doExport?'+$.param(this.pageForm))},
openGroups(row){this.$set(this,'currentGroup',row);this.$set(this,'groupVisible',true);this.$set(this,'dialogLoading',true);this.$axios.post(loc()+'/getGroupInfo',{groupId:row.id}).then((res)=>{if(res.code===0)this.$set(this,'groups',res.data||[])}).finally(()=>{this.$set(this,'dialogLoading',false)})},
openAllUsers(row){this.$set(this,'currentGroup',row);this.$set(this,'userQuery',{});this.$set(this.userPage,'pageNumber',1);this.$set(this,'userVisible',true);this.loadUsers()},
openGroupUsers(row){this.$set(this,'userQuery',{groupLeaderUserId:row.groupLeaderUserId,groupLeaderLoginName:row.groupLeaderLoginName,noGroupLeader:!row.groupLeaderUserId&&!row.groupLeaderLoginName});this.$set(this.userPage,'pageNumber',1);this.$set(this,'userVisible',true);this.loadUsers()},
loadUsers(){this.$set(this,'userLoading',true);const params=Object.assign({groupId:this.currentGroup.id,pageNumber:this.userPage.pageNumber,pageSize:this.userPage.pageSize},this.userQuery);this.$axios.post(loc()+'/getSignUser',params).then((res)=>{if(res.code===0){this.$set(this,'users',res.data.list||[]);this.$set(this.userPage,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'userLoading',false)})},
userPageChange(value){this.$set(this.userPage,'pageNumber',value);this.loadUsers()},userSizeChange(value){this.$set(this.userPage,'pageSize',value);this.$set(this.userPage,'pageNumber',1);this.loadUsers()},
deleteUser(row){this.$confirm('确定删除该报名人员吗?','提示',{type:'warning'}).then(()=>{this.$axios.post(loc()+'/deleteJoinUser',{id:row.id}).then((res)=>{if(res.code===0){this.$message.success(res.msg);this.loadUsers();this.pageData()}else this.$message.warning(res.msg)})}).catch(()=>{})}
},created(){this.loadOptions();this.pageData()}
})
const vue = new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
const currentYear = moment().format('YYYY')
return {
groupVisible: false,
userVisible: false,
userLoading: false,
dialogLoading: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
startYear: currentYear,
endYear: currentYear,
groupName: '',
travelAgencyId: '',
searchKeyword: ''
},
tableColumns: [
{prop: 'year', label: '年度', width: 60},
{prop: 'groupName', label: '组团名称', sortable: true},
{prop: 'travelAgencyName', label: '旅行社', sortable: true},
{prop: 'groupCount', label: '团数量'},
{prop: 'signCount', label: '报名人数'},
{prop: 'signUserName', label: '报名人员', personOnly: true},
{prop: 'signLoginName', label: '工号', personOnly: true}
],
travelAgencyList: [],
flexibleGroupList: [],
groupTableData: [],
signUserData: [],
currentGroupId: ''
}
},
methods: {
pageData() {
this.$set(this, 'tableLoading', true)
this.$axios.post(loc() + '/pageData', this.pageForm)
.then((res) => {
if (res.code === 0) {
this.$set(this, 'tableData', res.data.list || [])
this.$set(this.pageForm, 'totalCount', res.data.totalCount || 0)
}
})
.finally(() => {
this.$set(this, 'tableLoading', false)
})
},
tableRowKey(row) {
return row.rowKey || row.id
},
closeUserDialog() {
this.$set(this, 'userVisible', false)
},
closeGroupDialog() {
this.$set(this, 'groupVisible', false)
},
personKeywordChange(value) {
this.$set(this.pageForm, 'searchKeyword', value)
},
clearPersonKeyword() {
this.$set(this.pageForm, 'searchKeyword', '')
this.doSearch()
},
yearChange() {
this.getTravelAgency()
this.getFlexibleGroupList()
this.doSearch()
},
getTravelAgency() {
const params = {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
}
this.$axios.post(loc() + '/selectTravelAgencyList', params)
.then((res) => {
if (res.code === 0) {
this.$set(this, 'travelAgencyList', res.data || [])
}
})
},
getFlexibleGroupList() {
const params = {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
}
this.$axios.post(loc() + '/selectFlexibleGroupList', params)
.then((res) => {
if (res.code === 0) {
this.$set(this, 'flexibleGroupList', res.data || [])
}
})
},
doExport() {
window.open(loc() + '/doExport?' + $.param(this.pageForm))
},
openView(row) {
this.$set(this, 'currentGroupId', row.id)
this.$set(this, 'dialogLoading', true)
this.$axios.post(loc() + '/getGroupInfo', {groupId: row.id})
.then((res) => {
if (res.code === 0) {
this.$set(this, 'groupTableData', res.data || [])
this.$set(this, 'groupVisible', true)
}
})
.finally(() => {
this.$set(this, 'dialogLoading', false)
})
},
openViewUser(row) {
this.$set(this, 'currentGroupId', row.id)
this.loadAllSignUser()
},
openViewUserByTime(row) {
this.loadSignUserByTime(row.groupLeaderUserId, row.groupLeaderLoginName, !row.groupLeaderUserId && !row.groupLeaderLoginName)
},
loadAllSignUser() {
// groupId 传灵活组团配置主键,接口返回该组团下全部报名人员的分页结构。
this.$set(this, 'userLoading', true)
this.$axios.post(loc() + '/getAllSignUser', {
groupId: this.currentGroupId,
pageNumber: 1,
pageSize: 1000
}).then((res) => {
if (res.code === 0) {
this.$set(this, 'signUserData', res.data.list || [])
this.$set(this, 'userVisible', true)
}
}).finally(() => {
this.$set(this, 'userLoading', false)
})
},
loadSignUserByTime(groupLeaderUserId, groupLeaderLoginName, noGroupLeader) {
// 参数依次标识团长用户、团长工号和未选团长分组,接口返回对应团内人员分页结构。
this.$set(this, 'userLoading', true)
this.$axios.post(loc() + '/getSignUser', {
groupId: this.currentGroupId,
groupLeaderUserId: groupLeaderUserId,
groupLeaderLoginName: groupLeaderLoginName,
noGroupLeader: noGroupLeader,
pageNumber: 1,
pageSize: 1000
}).then((res) => {
if (res.code === 0) {
this.$set(this, 'signUserData', res.data.list || [])
this.$set(this, 'userVisible', true)
}
}).finally(() => {
this.$set(this, 'userLoading', false)
})
},
deleteUser(row) {
this.$confirm('您确定要删除该人员吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post(loc() + '/deleteJoinUser', {id: row.id})
.then((res) => {
if (res.code === 0) {
this.$message.success('删除成功')
this.$set(this, 'signUserData', this.signUserData.filter((item) => item.id !== row.id))
this.pageData()
} else {
this.$message.error(res.msg)
}
})
}).catch(() => {})
}
},
created() {
this.getTravelAgency()
this.getFlexibleGroupList()
this.pageData()
}
})
</script>
<!--# } #-->
@@ -1,26 +1,431 @@
<!--# layout("/layouts/platform.html"){ #-->
<style>
.line-statistics-query-row {
min-height: 70px;
display: flex;
align-items: center;
box-sizing: border-box;
}
.line-statistics-query-row:not(:last-child) {
border-bottom: 1px dashed #e6e6e6;
}
.line-statistics-query-content {
width: 100%;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never"><search @search="doSearch">
<search-item label="年度范围"><el-date-picker v-model="yearRange" type="yearrange" value-format="yyyy" range-separator="至" @change="yearChange"></el-date-picker></search-item>
<search-item label="所属工会"><el-select v-model="pageForm.unionId" filterable clearable @change="doSearch"><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.regionalNature" clearable @change="filterChange"><el-option label="省内" value="省内"></el-option><el-option label="省外" value="省外"></el-option></el-select></search-item>
<search-item label="组织方式"><el-select v-model="pageForm.signUpMode" clearable @change="filterChange"><el-option label="分工会组织" value="1"></el-option><el-option label="校工会组织" value="2"></el-option></el-select></search-item>
<search-item label="线路"><el-select v-model="pageForm.takePartInLineId" filterable clearable @change="doSearch"><el-option v-for="item in lineOptions" :key="item.id" :label="item.lineName+'-'+item.regionalNature+'【'+(item.lotName||'')+'】('+item.signUpMode+''" :value="item.id"></el-option></el-select></search-item>
<search-item label="标段"><el-select v-model="pageForm.lotId" filterable clearable @change="doSearch"><el-option v-for="item in lots" :key="item.id" :label="item.lotName" :value="item.id"></el-option></el-select></search-item>
</search></el-card>
<el-card shadow="never" class="mt20"><table-tool label="线路报名统计"></table-tool>
<el-table v-loading="tableLoading" :data="tableData" :size="tableSize"><el-table-column type="index" :index="indexMethod" label="序号" width="60"></el-table-column><el-table-column prop="year" label="年度" width="70"></el-table-column><el-table-column prop="lineName" label="线路名称" min-width="180"></el-table-column><el-table-column prop="linePlayTime" label="出行时间" width="150"></el-table-column><el-table-column prop="travelAgencyName" label="承担旅行社" min-width="150"></el-table-column><el-table-column prop="unionName" label="选择线路工会"></el-table-column><el-table-column prop="regionalNature" label="线路类型" width="90"></el-table-column><el-table-column prop="minimumGroupSize" label="最少成团人数" width="110"></el-table-column><el-table-column label="报名人数(家属)" width="130"><template v-slot="{row}"><el-link type="primary" @click="openUsers(row)">{{Number(row.lineNum||0)+Number(row.signUpUserFamilyNum||0)}}{{row.signUpUserFamilyNum||0}}</el-link></template></el-table-column><el-table-column label="操作" width="300"><template v-slot="{row}"><el-button size="mini" @click="openUsers(row)">查看人员</el-button><el-button type="success" size="mini" @click="notice(row,true,true)">成团通知</el-button><el-dropdown class="ml10"><el-button type="warning" size="mini">未成团通知<i class="el-icon-arrow-down el-icon--right"></i></el-button><el-dropdown-menu slot="dropdown"><el-dropdown-item @click.native="notice(row,false,true)">保留报名记录</el-dropdown-item><el-dropdown-item @click.native="notice(row,false,false)">删除报名记录</el-dropdown-item></el-dropdown-menu></el-dropdown></template></el-table-column></el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="线路报名人员" :visible.sync="userVisible" width="1050px" append-to-body><div class="mb10"><el-input v-model="userPage.searchKeyword" placeholder="姓名或工号" clearable style="width:220px" @keyup.enter.native="loadUsers"></el-input><el-button type="primary" class="ml10" @click="loadUsers">查询</el-button></div><el-table v-loading="userLoading" :data="users"><el-table-column type="index" label="序号" width="60"></el-table-column><el-table-column prop="loginName" label="工号"></el-table-column><el-table-column prop="userName" label="姓名"></el-table-column><el-table-column prop="unionName" label="所属工会"></el-table-column><el-table-column prop="unitName" label="所属单位"></el-table-column><el-table-column prop="familyCount" label="家属人数" width="90"></el-table-column><el-table-column prop="linePlayTime" label="出行时间"></el-table-column></el-table><el-pagination class="mt20" background layout="total, sizes, prev, pager, next" :current-page="userPage.pageNumber" :page-size="userPage.pageSize" :total="userPage.totalCount" @current-change="userPageChange" @size-change="userSizeChange"></el-pagination></el-dialog>
<guava ref="guava">
<template>
<el-card shadow="never">
<el-row class="line-statistics-query-row" type="flex" align="middle">
<el-col class="line-statistics-query-content">
<el-row :gutter="30">
<el-col :span="12">
<span>&emsp;&emsp;度:</span>
<el-date-picker v-model="pageForm.startYear" type="year" value-format="yyyy"
:clearable="false" placeholder="选择年" style="width: 38%"
@change="yearChange"></el-date-picker>
<span></span>
<el-date-picker v-model="pageForm.endYear" type="year" value-format="yyyy"
:clearable="false" placeholder="选择年" style="width: 38%"
@change="yearChange"></el-date-picker>
</el-col>
<el-col :span="12">
<span>所属工会:</span>
<el-select v-model="pageForm.unionId" filterable clearable placeholder="请选择所属工会"
style="width: 80%" @change="doSearch">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name"
:value="item.id"></el-option>
</el-select>
</el-col>
</el-row>
</el-col>
</el-row>
<el-row class="line-statistics-query-row" type="flex" align="middle">
<el-col class="line-statistics-query-content">
<el-row :gutter="30">
<el-col :span="12">
<span>线路类型:</span>
<el-select v-model="pageForm.regionalNature" filterable placeholder="请选择线路类型"
style="width: 80%" @change="lineTypeChange">
<el-option label="全部" value=""></el-option>
<el-option label="省内" value="省内"></el-option>
<el-option label="省外" value="省外"></el-option>
</el-select>
</el-col>
<el-col :span="12">
<span>线&emsp;&emsp;路:</span>
<el-select v-model="pageForm.takePartInLineId" filterable clearable
placeholder="请选择线路" style="width: 80%" @change="lineChange">
<el-option v-for="item in lineOptions" :key="item.id"
:label="item.lineName+'-'+item.regionalNature+'【'+(item.lotName||'')+'】('+item.signUpMode+''"
:value="item.id"></el-option>
</el-select>
</el-col>
</el-row>
</el-col>
</el-row>
<el-row class="line-statistics-query-row" type="flex" align="middle">
<el-col class="line-statistics-query-content">
<el-row :gutter="30">
<el-col :span="12">
<span>&emsp;&emsp;段:</span>
<el-select v-model="pageForm.lotId" filterable clearable placeholder="请选择标段"
style="width: 80%" @change="doSearch">
<el-option v-for="item in lots" :key="item.id" :label="item.lotName"
:value="item.id"></el-option>
</el-select>
</el-col>
<el-col :span="12">
<span>出行时间:</span>
<el-select v-model="pageForm.selectId" filterable clearable placeholder="请选择出行时间"
style="width: 80%" @change="doSearch">
<el-option v-for="item in linePlayTimes" :key="item.selectId" :label="item.times"
:value="item.selectId"></el-option>
</el-select>
</el-col>
</el-row>
</el-col>
</el-row>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="数据列表">
<el-button icon="el-icon-s-promotion" size="small" type="primary" class="mr10" @click="doExport">
导出成团线路人员
</el-button>
</table-tool>
<el-table ref="table" row-key="lineUId" :data="tableData" v-loading="tableLoading" style="width: 100%"
@sort-change="pageOrder">
<el-table-column align="center" header-align="center" type="index" label="序号" :index="indexMethod"
width="80"></el-table-column>
<el-table-column v-for="column in tableColumns" :key="column.prop" align="center"
header-align="center" show-overflow-tooltip :label="column.label"
:prop="column.prop" :width="column.width" :sortable="column.sortable">
<template v-slot="{row}">
<el-link v-if="column.prop==='lineName'" type="primary" @click="openLine(row)">
{{row.lineName}}
</el-link>
<el-link v-else-if="column.prop==='lineNum'" type="primary" @click="openUserData(row)">
{{Number(row.lineNum||0)+Number(row.signUpUserFamilyNum||0)}}{{row.signUpUserFamilyNum||0}}
</el-link>
<span v-else>{{row[column.prop]}}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="380">
<template v-slot="{row}">
<el-button size="mini" type="primary" @click="openUserData(row)">查看人员</el-button>
<el-button size="mini" type="primary" @click="sendNotice(row,true,true)">发送成团通知
</el-button>
<el-dropdown class="ml10 mr10" trigger="click">
<el-button size="mini" type="primary">发送未成团通知<i
class="el-icon-arrow-down el-icon--right"></i></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="sendNotice(row,false,true)">保留报名记录
</el-dropdown-item>
<el-dropdown-item @click.native="sendNotice(row,false,false)">删除报名记录
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #public>
<line-info ref="viewLineInfo"></line-info>
</template>
<template #edit>
<el-card shadow="never">
<div class="search">
<div class="search-item">
<div class="search-item-label">工号姓名</div>
<div class="search-item-option">
<el-input v-model="userPage.searchKeyword" maxlength="30" clearable></el-input>
</div>
</div>
<div class="search-item" v-if="$auth.hasRole('SYSADMIN') || $auth.hasRole('SCHOOL_UNION_ADMIN')">
<div class="search-item-label">所属工会</div>
<div class="search-item-option">
<el-select v-model="userPage.unionId" filterable clearable style="width: 100%"
@change="flushUnits">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name"
:value="item.id"></el-option>
</el-select>
</div>
</div>
<div class="search-item">
<div class="search-item-label">所属单位</div>
<div class="search-item-option">
<el-select v-model="userPage.unitId" filterable clearable style="width: 100%">
<el-option v-for="item in units" :key="item.id" :label="item.name"
:value="item.id"></el-option>
</el-select>
</div>
</div>
<div class="search-query">
<el-button type="primary" icon="el-icon-search" @click="loadUsers">搜索</el-button>
</div>
</div>
</el-card>
<el-card shadow="never" class="mt10">
<el-table :data="users" v-loading="userLoading">
<el-table-column v-for="column in userColumns" :key="column.prop" align="center"
header-align="center" show-overflow-tooltip :label="column.label"
:prop="column.prop" :sortable="column.sortable">
<template v-slot="{row}">
<el-link v-if="column.prop==='familyCount'" type="primary">
{{Number(row.familyCount||0)>0?'携带':'未携带'}}{{row.familyCount||0}}
</el-link>
<span v-else>{{row[column.prop]}}</span></template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container" style="margin-bottom: 0">
<el-pagination :current-page="userPage.pageNumber" :page-size="userPage.pageSize"
:page-sizes="[5,10,20,30,50]" :total="userPage.totalCount"
layout="total, sizes, prev, pager, next" @size-change="userSizeChange"
@current-change="userPageChange"></el-pagination>
</el-row>
</el-card>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
new Vue({el:'#app',mixins:[initTableMixins],data(){const year=new Date().getFullYear().toString();return{yearRange:[year,year],pageForm:{pageNumber:1,pageSize:10,totalCount:0,startYear:year,endYear:year,unionId:'',regionalNature:'',signUpMode:'',takePartInLineId:'',lotId:'',selectId:''},unionOptions:[],lineOptions:[],lots:[],userVisible:false,userLoading:false,currentRow:{},users:[],userPage:{pageNumber:1,pageSize:10,totalCount:0,searchKeyword:'',unionId:'',unitId:''}}},methods:{
pageData(){this.$set(this,'tableLoading',true);this.$axios.post(loc()+'/pageData',this.pageForm).then((res)=>{if(res.code===0){this.$set(this,'tableData',res.data.list||[]);this.$set(this.pageForm,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'tableLoading',false)})},
yearChange(value){this.$set(this.pageForm,'startYear',value[0]);this.$set(this.pageForm,'endYear',value[1]);this.filterChange()},filterChange(){this.$set(this.pageForm,'takePartInLineId','');this.loadLines();this.doSearch()},
loadLines(){this.$axios.post(loc()+'/getLineOptions',{startYear:this.pageForm.startYear,endYear:this.pageForm.endYear,signUpMode:this.pageForm.signUpMode,regionalNature:this.pageForm.regionalNature}).then((res)=>{if(res.code===0)this.$set(this,'lineOptions',res.data||[])})},
openUsers(row){this.$set(this,'currentRow',row);this.$set(this.userPage,'pageNumber',1);this.$set(this,'userVisible',true);this.loadUsers()},loadUsers(){this.$set(this,'userLoading',true);const params=Object.assign({},this.userPage,{takePartLineId:this.currentRow.lineUId});this.$axios.post(loc()+'/getUserDateByLine',params).then((res)=>{if(res.code===0){this.$set(this,'users',res.data.list||[]);this.$set(this.userPage,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'userLoading',false)})},userPageChange(value){this.$set(this.userPage,'pageNumber',value);this.loadUsers()},userSizeChange(value){this.$set(this.userPage,'pageSize',value);this.$set(this.userPage,'pageNumber',1);this.loadUsers()},
notice(row,success,keep){const countUrl=success?'/countSuccessNotice':'/countFailNotice';this.$axios.post(loc()+countUrl,{id:row.lineUId}).then((res)=>{if(res.code!==0){this.$message.warning(res.msg);return}const data=res.data||{};if(!data.sendCount){this.$message.warning(data.msg);return}this.$prompt('当前将通过'+data.sendTypeName+'发送'+data.sendCount+'人,请确认消息内容。','提示',{inputType:'textarea',inputValue:data.content||'',inputValidator:(value)=>!!(value&&value.trim()),inputErrorMessage:'消息内容不能为空'}).then((prompt)=>{const url=success?'/sendSuccess':'/sendFail';this.$axios.post(loc()+url,{id:row.lineUId,type:keep,content:prompt.value}).then((sendRes)=>{if(sendRes.code===0){const result=sendRes.data||{};this.$message.success(result.msg||'通知发送完成');this.pageData()}else this.$message.warning(sendRes.msg)})}).catch(()=>{})})}
},created(){this.$businessTool.listUnion().then((res)=>{this.$set(this,'unionOptions',res||[])});this.$axios.post('/platform/recuperation/config/fetchOne').then((res)=>{if(res.code===0)this.$set(this,'lots',(res.data&&res.data.lots)||[])});this.loadLines();this.pageData()}})
<!--#include('../line/info.js'){}#-->
new Vue({
el: '#app',
mixins: [initTableMixins],
components: {'line-info': info},
data() {
const year = new Date().getFullYear().toString()
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
startYear: year,
endYear: year,
unionId: '',
regionalNature: '',
takePartInLineId: '',
lotId: '',
selectId: '',
signUpMode: ''
},
tableColumns: [
{prop: 'year', label: '年度', width: 60}, {
prop: 'lineName',
label: '线路名称',
sortable: true,
width: 200
},
{prop: 'linePlayTime', label: '出行时间', width: 180}, {
prop: 'travelAgencyName',
label: '承担旅行社',
sortable: true,
width: 180
},
{prop: 'unionName', label: '选择线路工会', sortable: true}, {
prop: 'regionalNature',
label: '线路类型',
sortable: true
},
{prop: 'contact', label: '联系人', sortable: true, checked: 0}, {
prop: 'contactMobileNumber',
label: '联系方式',
checked: 0
},
{prop: 'minimumGroupSize', label: '最少成团人数(含家属)', width: 140}, {
prop: 'lineNum',
label: '报名人数(含家属)',
width: 140
}
],
unionOptions: [],
lineOptions: [],
linePlayTimes: [],
lots: [],
units: [],
lineUId: '',
users: [],
userLoading: false,
userPage: {pageNumber: 1, pageSize: 10, totalCount: 0, searchKeyword: '', unionId: '', unitId: ''},
userColumns: [
{prop: 'loginName', label: '工号'}, {prop: 'userName', label: '姓名'}, {
prop: 'unionName',
label: '所属工会'
},
{prop: 'unitName', label: '所属单位', sortable: true}, {prop: 'familyCount', label: '是否携带家属'},
{prop: 'linePlayTime', label: '出行时间', sortable: true}
]
}
},
methods: {
pageData() {
this.$set(this, 'tableLoading', true)
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.$set(this, 'tableData', res.data.list || []);
this.$set(this.pageForm, 'totalCount', res.data.totalCount || 0)
}
}).finally(() => {
this.$set(this, 'tableLoading', false)
})
},
yearChange() {
this.$set(this.pageForm, 'takePartInLineId', '');
this.$set(this.pageForm, 'selectId', '');
this.$set(this, 'linePlayTimes', [])
this.loadLines();
this.doSearch()
},
lineTypeChange() {
this.$set(this.pageForm, 'takePartInLineId', '');
this.$set(this.pageForm, 'selectId', '');
this.$set(this, 'linePlayTimes', [])
this.loadLines();
this.doSearch()
},
lineChange() {
this.$set(this.pageForm, 'selectId', '');
this.loadLinePlayTimes();
this.doSearch()
},
loadLines() {
this.$axios.post(loc() + '/getLineOptions', {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
signUpMode: '',
regionalNature: this.pageForm.regionalNature
})
.then((res) => {
if (res.code === 0) this.$set(this, 'lineOptions', res.data || [])
})
},
loadLinePlayTimes() {
if (!this.pageForm.takePartInLineId) {
this.$set(this, 'linePlayTimes', []);
return
}
this.$axios.post(loc() + '/getLinePlayTimes', {
lineId: this.pageForm.takePartInLineId,
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
})
.then((res) => {
if (res.code === 0) this.$set(this, 'linePlayTimes', res.data || [])
})
},
doExport() {
this.$downLoad(loc() + '/doExport', this.pageForm)
},
openLine(row) {
this.$refs.guava.public()
this.$nextTick(() => {
this.$refs.viewLineInfo.findOne(row.lineId, row.usUnionId)
this.$refs.viewLineInfo.findUnionSelectLineData(row.lineId, row.usUnionId)
this.$set(this.$refs.viewLineInfo, 'showTravelTimePeriod', true)
})
},
openUserData(row) {
this.$set(this, 'lineUId', row.lineUId)
this.$set(this, 'userPage', {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
unionId: '',
unitId: ''
})
this.$set(this, 'units', []);
this.$refs.guava.edit();
this.loadUsers()
},
flushUnits() {
this.$set(this.userPage, 'unitId', '')
if (!this.userPage.unionId) {
this.$set(this, 'units', []);
return
}
getUnits(this.userPage.unionId).then((res) => {
this.$set(this, 'units', res || [])
})
},
loadUsers() {
this.$set(this, 'userLoading', true)
const params = Object.assign({}, this.userPage, {takePartLineId: this.lineUId})
this.$axios.post(loc() + '/getUserDateByLine', params).then((res) => {
if (res.code === 0) {
this.$set(this, 'users', res.data.list || []);
this.$set(this.userPage, 'totalCount', res.data.totalCount || 0)
}
}).finally(() => {
this.$set(this, 'userLoading', false)
})
},
userPageChange(value) {
this.$set(this.userPage, 'pageNumber', value);
this.loadUsers()
},
userSizeChange(value) {
this.$set(this.userPage, 'pageSize', value);
this.$set(this.userPage, 'pageNumber', 1);
this.loadUsers()
},
sendNotice(row, success, keep) {
const countUrl = success ? '/countSuccessNotice' : '/countFailNotice'
this.$axios.post(loc() + countUrl, {id: row.lineUId}).then((res) => {
if (res.code !== 0) {
this.$message.warning(res.msg);
return
}
const data = res.data || {}
if (!data.sendCount) {
this.$message.warning(data.msg || '当前暂无需要发送通知的报名人员');
return
}
const noticeName = success ? '成团通知' : '未成团通知'
const recordAction = success ? '' : ',并' + (keep ? '保留' : '删除') + '报名记录'
const message = '当前将通过' + (data.sendTypeName || '微信、钉钉') + '发送' + noticeName + data.sendCount + '人' + recordAction + ',请确认或修改消息内容。'
this.$prompt(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'textarea',
inputValue: data.content || '',
inputValidator: (value) => !!(value && value.trim()),
inputErrorMessage: '消息内容不能为空'
}).then((prompt) => {
const url = success ? '/sendSuccess' : '/sendFail'
const params = {id: row.lineUId, content: prompt.value.trim()}
if (!success) this.$set(params, 'type', keep)
this.$axios.post(loc() + url, params).then((sendRes) => {
if (sendRes.code === 0) {
const result = sendRes.data || {};
this.$message.success(result.msg || '通知发送完成');
this.pageData()
} else this.$message.warning(sendRes.msg)
})
}).catch(() => {
})
})
}
},
created() {
this.$businessTool.listUnion().then((res) => {
this.$set(this, 'unionOptions', res || [])
})
this.$axios.post('/platform/recuperation/config/fetchOne').then((res) => {
if (res.code === 0) {
const source = (res.data && res.data.lots) || []
const lots = source.filter((item, index, list) => item && item.id && list.findIndex((lot) => lot && lot.id === item.id) === index)
this.$set(this, 'lots', lots)
}
})
this.loadLines();
this.pageData()
}
})
</script>
<!--# } #-->
@@ -1,19 +1,688 @@
<!--# layout("/layouts/platform.html"){ #-->
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.center-col {
background-color: white;
position: relative;
box-sizing: border-box;
padding: 20px;
width: calc((100% - (20px * 2)) / 2);
}
.col-title {
color: rgba(0, 0, 0, .45);
font-size: 14px;
margin-bottom: 10px;
}
.col-button {
margin-bottom: 30px;
}
.rating-summary {
float: right;
color: #ff9900;
font-size: 14px;
}
.travel-agency-summary {
display: flex;
padding: 22px 0;
}
.travel-agency-summary-item {
display: flex;
flex: 1;
align-items: center;
min-width: 0;
padding: 0 24px;
border-right: 1px solid #f0f0f0;
}
.travel-agency-summary-item:last-child {
border-right: none;
}
.travel-agency-summary-icon {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 48px;
height: 48px;
margin-right: 14px;
border-radius: 50%;
color: #1867b0;
background-color: #eef6ff;
font-size: 24px;
}
.travel-agency-summary-label {
color: rgba(0, 0, 0, .65);
font-size: 14px;
}
.travel-agency-summary-score {
margin-top: 2px;
color: #1867b0;
font-size: 26px;
font-weight: 600;
line-height: 30px;
}
.travel-agency-summary-score span {
margin-left: 2px;
font-size: 14px;
font-weight: normal;
}
.travel-agency-summary-count {
margin-top: 3px;
color: rgba(0, 0, 0, .45);
font-size: 13px;
}
.scheme-service-card {
min-height: 360px;
}
.scheme-service-title {
margin-bottom: 14px;
color: rgba(0, 0, 0, .85);
font-size: 16px;
font-weight: 500;
}
.scheme-type-switch {
margin-bottom: 18px;
}
.scheme-selector {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 18px;
}
.scheme-selector-item {
display: flex;
align-items: center;
justify-content: space-between;
width: calc((100% - 36px) / 4);
min-width: 200px;
height: 56px;
margin: 0 !important;
padding: 0 16px;
text-align: left;
}
.scheme-selector-name {
overflow: hidden;
margin-right: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.scheme-selector-score {
flex: 0 0 auto;
font-size: 15px;
}
.scheme-detail {
padding: 22px 24px;
border: 1px solid #e6eef8;
border-radius: 4px;
background-color: #fcfdff;
}
.scheme-detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 22px;
}
.scheme-detail-name {
color: rgba(0, 0, 0, .85);
font-size: 20px;
font-weight: 500;
}
.scheme-detail-agency {
margin-top: 8px;
color: rgba(0, 0, 0, .45);
font-size: 14px;
}
.scheme-detail-composite {
min-width: 130px;
text-align: right;
}
.scheme-detail-composite-label {
color: rgba(0, 0, 0, .45);
font-size: 13px;
}
.scheme-detail-composite-score {
margin-top: 3px;
color: #1867b0;
font-size: 30px;
font-weight: 600;
}
.scheme-detail-composite-score span {
margin-left: 2px;
font-size: 14px;
font-weight: normal;
}
.scheme-detail-composite-count {
margin-top: 3px;
color: rgba(0, 0, 0, .45);
font-size: 13px;
}
.scheme-metric-list {
display: flex;
border-top: 1px solid #edf1f5;
}
.scheme-metric-item {
flex: 1;
min-width: 0;
padding: 18px 14px 8px;
text-align: center;
border-right: 1px solid #edf1f5;
}
.scheme-metric-item:last-child {
border-right: none;
}
.scheme-metric-icon {
color: #1867b0;
font-size: 22px;
}
.scheme-metric-label {
margin-top: 8px;
color: rgba(0, 0, 0, .65);
font-size: 14px;
}
.scheme-metric-score {
margin-top: 5px;
color: #1867b0;
font-size: 23px;
font-weight: 600;
}
.scheme-metric-score span {
margin-left: 2px;
font-size: 13px;
font-weight: normal;
}
.scheme-metric-count {
margin-top: 3px;
color: rgba(0, 0, 0, .45);
font-size: 12px;
}
.scheme-feedback-action {
padding-top: 18px;
text-align: center;
border-top: 1px solid #edf1f5;
}
.scheme-feedback-list {
max-height: 500px;
overflow-y: auto;
}
.scheme-feedback-row {
padding: 16px 4px;
border-bottom: 1px solid #edf1f5;
}
.scheme-feedback-row:first-child {
padding-top: 0;
}
.scheme-feedback-row:last-child {
border-bottom: none;
}
.scheme-feedback-user {
color: rgba(0, 0, 0, .85);
font-size: 15px;
font-weight: 500;
}
.scheme-feedback-union {
margin-left: 10px;
color: rgba(0, 0, 0, .45);
font-size: 13px;
font-weight: normal;
}
.scheme-feedback-content {
margin-top: 9px;
color: rgba(0, 0, 0, .65);
line-height: 22px;
white-space: pre-wrap;
}
.scheme-empty {
padding: 64px 0;
color: rgba(0, 0, 0, .45);
text-align: center;
}
.feedback-content {
line-height: 22px;
white-space: pre-wrap;
}
.feedback-summary {
display: inline-block;
max-width: 100%;
overflow: hidden;
vertical-align: middle;
text-overflow: ellipsis;
white-space: nowrap;
}
.feedback-score-grid {
display: flex;
flex-wrap: wrap;
margin: 0 -6px 14px;
}
.feedback-score-item {
box-sizing: border-box;
width: 33.33%;
padding: 6px;
}
.feedback-score-item-inner {
padding: 10px 12px;
border-radius: 4px;
background-color: #f7faff;
}
.feedback-score-label {
color: rgba(0, 0, 0, .45);
font-size: 13px;
}
.feedback-score-value {
margin-top: 3px;
color: #1867b0;
font-size: 18px;
font-weight: 500;
}
.feedback-pagination {
margin-top: 20px;
text-align: right;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never"><el-row type="flex" align="middle"><el-col :span="8"><span>统计年度:</span><el-date-picker v-model="pageForm.year" type="year" value-format="yyyy" :clearable="false" @change="yearChange"></el-date-picker></el-col><el-col :span="16"><el-tabs v-model="activeTab" @tab-click="tabChange"><el-tab-pane label="满意度统计" name="summary"></el-tab-pane><el-tab-pane label="评价建议明细" name="feedback"></el-tab-pane><el-tab-pane label="评价人员名单" name="users"></el-tab-pane></el-tabs></el-col></el-row></el-card>
<template v-if="activeTab==='summary'">
<el-row :gutter="20" class="mt20"><el-col :span="8"><el-card shadow="never"><div slot="header">旅行社综合满意度</div><el-table v-loading="summaryLoading" :data="agencyRatings" height="530"><el-table-column type="index" label="排名" width="65"></el-table-column><el-table-column prop="travelAgencyName" label="旅行社" show-overflow-tooltip></el-table-column><el-table-column prop="avg" label="综合评分" width="90"><template v-slot="{row}">{{score(row.avg)}}</template></el-table-column><el-table-column prop="ratingCount" label="评价人数" width="85"></el-table-column></el-table></el-card></el-col><el-col :span="16"><el-card shadow="never"><div slot="header">方案服务评分</div><el-table v-loading="summaryLoading" :data="schemeRatings" height="530"><el-table-column prop="typeName" label="类型" width="85"></el-table-column><el-table-column prop="schemeName" label="方案名称" min-width="150" show-overflow-tooltip></el-table-column><el-table-column prop="travelAgencyName" label="旅行社" min-width="130" show-overflow-tooltip></el-table-column><el-table-column label="旅行社" width="75"><template v-slot="{row}">{{score(row.travelAgencyAvg)}}</template></el-table-column><el-table-column label="行程" width="65"><template v-slot="{row}">{{score(row.journeyAvg)}}</template></el-table-column><el-table-column label="住宿/酒店" width="90"><template v-slot="{row}">{{score(row.accommodationAvg)}}</template></el-table-column><el-table-column label="餐饮" width="65"><template v-slot="{row}">{{score(row.diningAvg)}}</template></el-table-column><el-table-column label="交通" width="65"><template v-slot="{row}">{{score(row.transportationAvg)}}</template></el-table-column><el-table-column label="综合" width="65"><template v-slot="{row}"><strong>{{score(row.compositeAvg)}}</strong></template></el-table-column><el-table-column prop="ratingCount" label="人数" width="65"></el-table-column></el-table></el-card></el-col></el-row>
</template>
<template v-if="activeTab==='feedback'"><el-card shadow="never" class="mt20"><table-tool label="评价建议"><el-select v-model="feedbackType" size="small" style="width:140px" @change="loadFeedback"><el-option v-for="item in ['全部','线路','灵活组团','定点']" :key="item" :label="item" :value="item"></el-option></el-select><el-button type="primary" size="small" class="ml10" @click="exportFeedback">导出评价明细</el-button></table-tool><el-table v-loading="feedbackLoading" :data="feedbackDetails"><el-table-column type="index" label="序号" width="60"></el-table-column><el-table-column prop="typeName" label="报名方式" width="100"></el-table-column><el-table-column prop="schemeName" label="方案名称" min-width="160"></el-table-column><el-table-column prop="travelAgencyName" label="旅行社" min-width="140"></el-table-column><el-table-column prop="userName" label="评价人" width="100"></el-table-column><el-table-column prop="unitName" label="所属单位" min-width="140"></el-table-column><el-table-column prop="compositeScore" label="综合评分" width="100"></el-table-column><el-table-column prop="feedbackContent" label="评价建议" min-width="240" show-overflow-tooltip></el-table-column><el-table-column label="操作" width="80"><template v-slot="{row}"><el-button type="primary" size="mini" @click="viewFeedback(row)">查看</el-button></template></el-table-column></el-table></el-card></template>
<template v-if="activeTab==='users'"><el-card shadow="never" class="mt20"><search @search="doSearch"><search-item label="线路"><el-select v-model="pageForm.lineId" filterable clearable @change="doSearch"><el-option v-for="item in lineList" :key="item.id" :label="item.lineName+''+item.unionName+''" :value="item.id"></el-option></el-select></search-item><search-item label="姓名/工号"><el-input v-model="pageForm.searchKeyword" clearable @keyup.enter.native="doSearch"></el-input></search-item><search-item label="所属工会"><el-select v-model="pageForm.unionId" filterable clearable @change="doSearch"><el-option v-for="item in unionList" :key="item.id" :label="item.name" :value="item.id"></el-option></el-select></search-item><search-item label="所属单位"><el-select v-model="pageForm.unitId" filterable clearable @change="doSearch"><el-option v-for="item in unitList" :key="item.id" :label="item.name" :value="item.id"></el-option></el-select></search-item><search-item label="评分"><el-select v-model="pageForm.evaluateScore" clearable @change="doSearch"><el-option v-for="item in ['满意','一般','不满意']" :key="item" :label="item" :value="item"></el-option></el-select></search-item></search><table-tool label="评价人员名单"><el-button type="primary" size="small" @click="exportEvaluate">导出名单</el-button></table-tool><el-table v-loading="tableLoading" :data="tableData"><el-table-column type="index" :index="indexMethod" label="序号" width="60"></el-table-column><el-table-column prop="lineName" label="线路"></el-table-column><el-table-column prop="playStartTime" label="出行时间"></el-table-column><el-table-column prop="loginName" label="工号"></el-table-column><el-table-column prop="userName" label="姓名"></el-table-column><el-table-column prop="unitName" label="所属单位"></el-table-column><el-table-column prop="unionName" label="所属工会"></el-table-column><el-table-column prop="evaluateScore" label="评分"></el-table-column><el-table-column prop="evaluateText" label="评价" show-overflow-tooltip></el-table-column></el-table><!--#include("/layouts/pagination.html"){}#--></el-card></template>
<el-dialog title="评价详情" :visible.sync="feedbackVisible" width="650px" append-to-body><el-descriptions :column="2" border><el-descriptions-item label="报名方式">{{feedbackDetail.typeName}}</el-descriptions-item><el-descriptions-item label="综合评分">{{feedbackDetail.compositeScore}}</el-descriptions-item><el-descriptions-item label="旅行社评分">{{feedbackDetail.evaluationForTravelAgency}}</el-descriptions-item><el-descriptions-item label="住宿/酒店评分">{{feedbackDetail.evaluationForAccommodation}}</el-descriptions-item><el-descriptions-item label="行程评分">{{feedbackDetail.evaluationForJourney}}</el-descriptions-item><el-descriptions-item label="餐饮评分">{{feedbackDetail.evaluationForDining}}</el-descriptions-item><el-descriptions-item label="交通评分">{{feedbackDetail.evaluationForTransportation}}</el-descriptions-item><el-descriptions-item label="方案">{{feedbackDetail.schemeName}}</el-descriptions-item></el-descriptions><el-alert class="mt20" :closable="false" :title="feedbackDetail.feedbackContent||'暂无评价建议'" type="info"></el-alert></el-dialog>
<guava ref="guava">
<template>
<el-card shadow="never">
<div class="btn-group tool-button">
<el-date-picker
v-model="year"
type="year"
value-format="yyyy"
placeholder="选择年"
style="width: 100%" @change="doSearch">
</el-date-picker>
</div>
</el-card>
<el-card shadow="never" class="mt20">
<div class="travel-agency-summary">
<div class="travel-agency-summary-item" v-for="item in travelAgencyCompositeRatings" :key="item.travelAgencyName">
<div class="travel-agency-summary-icon">
<i class="el-icon-guide"></i>
</div>
<div>
<div class="travel-agency-summary-label">{{item.travelAgencyName}}</div>
<div class="travel-agency-summary-score" v-if="hasRatingScore(item.avg)">{{formatRatingScore(item.avg)}}<span></span></div>
<div class="travel-agency-summary-score" v-else>暂无评价</div>
<div class="travel-agency-summary-count">评价数 {{item.ratingCount}}</div>
</div>
</div>
</div>
</el-card>
<el-card shadow="never" class="mt20 scheme-service-card">
<div class="scheme-service-title">方案服务评分</div>
<el-radio-group class="scheme-type-switch" :value="schemeType" @input="changeSchemeType" size="medium">
<el-radio-button label="线路"></el-radio-button>
<el-radio-button label="灵活组团"></el-radio-button>
<el-radio-button label="定点"></el-radio-button>
</el-radio-group>
<div class="scheme-selector" v-if="getSchemeRatingsByType().length">
<el-button
v-for="item in getSchemeRatingsByType()"
:key="item.schemeId"
class="scheme-selector-item"
:type="isSelectedScheme(item) ? 'primary' : 'default'"
:plain="!isSelectedScheme(item)"
@click="selectScheme(item)">
<span class="scheme-selector-name">{{item.schemeName}}</span>
<span class="scheme-selector-score">{{formatRatingScore(item.compositeAvg)}} 分</span>
</el-button>
</div>
<div class="scheme-detail" v-if="getSelectedSchemeRating()">
<div class="scheme-detail-header">
<div>
<div class="scheme-detail-name">{{getSelectedSchemeRating().schemeName}}</div>
<div class="scheme-detail-agency">旅行社:{{getSelectedSchemeRating().travelAgencyName || '暂无旅行社信息'}}</div>
</div>
<div class="scheme-detail-composite">
<div class="scheme-detail-composite-label">综合评分</div>
<div class="scheme-detail-composite-score">{{formatRatingScore(getSelectedSchemeRating().compositeAvg)}}<span></span></div>
<div class="scheme-detail-composite-count">评价数 {{getSelectedSchemeRating().ratingCount || 0}}</div>
</div>
</div>
<div class="scheme-metric-list">
<div class="scheme-metric-item" v-for="item in getSelectedSchemeMetrics()" :key="item.key">
<i class="scheme-metric-icon" :class="item.icon"></i>
<div class="scheme-metric-label">{{item.label}}</div>
<div class="scheme-metric-score">{{formatRatingScore(item.avg)}}<span></span></div>
<div class="scheme-metric-count">评价数 {{item.count || 0}}</div>
</div>
</div>
<div class="scheme-feedback-action">
<el-button type="primary" plain @click="openSchemeFeedbackDialog">查看评价建议</el-button>
</div>
</div>
<div class="scheme-empty" v-else>暂无可查看的方案评分</div>
</el-card>
<el-card shadow="never" class="mt20">
<div class="col-title">
评价明细
<div style="float: right">
<el-button size="medium" type="primary" icon="el-icon-download" style="margin-right: 10px" @click="exportFeedbackDetails">导出</el-button>
<el-radio-group :value="feedbackType" @input="changeFeedbackType" size="medium">
<el-radio-button label="全部"></el-radio-button>
<el-radio-button label="线路"></el-radio-button>
<el-radio-button label="灵活组团"></el-radio-button>
<el-radio-button label="定点"></el-radio-button>
</el-radio-group>
</div>
</div>
<el-table :data="getPagedFeedbackDetails()" style="width: 100%">
<el-table-column align="center" header-align="center" type="index" label="序号" :index="feedbackIndexMethod" width="80"></el-table-column>
<el-table-column align="center" header-align="center" prop="typeName" label="报名方式" width="100"></el-table-column>
<el-table-column align="center" header-align="center" prop="projectName" label="方案名称" min-width="160" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" prop="travelAgencyName" label="旅行社" min-width="140" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" prop="userName" label="评价人" width="100"></el-table-column>
<el-table-column align="center" header-align="center" prop="unitName" label="所属单位" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" label="综合评分" width="110">
<template slot-scope="scope">
{{formatFeedbackScore(scope.row.compositeScore)}}
</template>
</el-table-column>
<el-table-column align="left" header-align="center" label="评价建议" min-width="250">
<template slot-scope="scope">
<span class="feedback-summary">{{getFeedbackContentSummary(scope.row.feedbackContent)}}</span>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="操作" width="90">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openFeedbackDetail(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="feedback-pagination"
:current-page="feedbackPage"
:page-size="feedbackPageSize"
:page-sizes="[10, 20, 50, 100]"
:total="getFilteredFeedbackDetails().length"
layout="total, sizes, prev, pager, next, jumper"
@current-change="changeFeedbackPage"
@size-change="changeFeedbackPageSize">
</el-pagination>
</el-card>
</template>
</guava>
<el-dialog title="评价详情" :visible="feedbackDetailVisible" width="620px" @close="closeFeedbackDetail">
<div class="feedback-score-grid">
<div class="feedback-score-item">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">综合评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.compositeScore)}}</div>
</div>
</div>
<div class="feedback-score-item">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">旅行社评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForTravelAgency)}}</div>
</div>
</div>
<div class="feedback-score-item" v-if="feedbackDetail.typeName === '线路'">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">行程评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForJourney)}}</div>
</div>
</div>
<div class="feedback-score-item">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">{{feedbackDetail.typeName === '线路' ? '住宿评分' : '酒店评分'}}</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForAccommodation)}}</div>
</div>
</div>
<div class="feedback-score-item" v-if="feedbackDetail.typeName === '线路'">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">餐饮评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForDining)}}</div>
</div>
</div>
<div class="feedback-score-item" v-if="feedbackDetail.typeName === '线路'">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">交通评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForTransportation)}}</div>
</div>
</div>
</div>
<div class="feedback-content">{{feedbackDetail.feedbackContent || '暂无评价建议'}}</div>
</el-dialog>
<el-dialog title="评价建议" :visible="schemeFeedbackVisible" width="720px" @close="closeSchemeFeedbackDialog">
<div class="scheme-feedback-list" v-if="getSelectedSchemeFeedbackDetails().length">
<div class="scheme-feedback-row" v-for="item in getSelectedSchemeFeedbackDetails()" :key="item.id">
<div class="scheme-feedback-user">
{{item.userName || '匿名评价人'}}
<span class="scheme-feedback-union">{{item.unitName || '暂无所属单位'}}</span>
</div>
<div class="scheme-feedback-content">{{item.feedbackContent}}</div>
</div>
</div>
<div class="scheme-empty" v-else>当前方案暂无评价建议</div>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({el:'#app',mixins:[initTableMixins],data(){return{activeTab:'summary',pageForm:{pageNumber:1,pageSize:10,totalCount:0,year:new Date().getFullYear().toString(),lineId:'',searchKeyword:'',unionId:'',unitId:'',evaluateScore:''},unionList:[],unitList:[],lineList:[],summaryLoading:false,feedbackLoading:false,agencyRatings:[],schemeRatings:[],feedbackDetails:[],feedbackType:'全部',feedbackVisible:false,feedbackDetail:{}}},methods:{
score(value){return value===null||value===undefined||value===''?'—':Number(value).toFixed(1)},yearChange(){this.$set(this.pageForm,'lineId','');this.loadLines();this.loadSummary();if(this.activeTab==='feedback')this.loadFeedback();if(this.activeTab==='users')this.doSearch()},tabChange(){if(this.activeTab==='summary')this.loadSummary();if(this.activeTab==='feedback')this.loadFeedback();if(this.activeTab==='users')this.pageData()},
loadSummary(){this.$set(this,'summaryLoading',true);this.$axios.post(loc()+'/getSatisfactionRatingStatistics',{year:this.pageForm.year}).then((res)=>{if(res.code===0){this.$set(this,'agencyRatings',(res.data&&res.data.travelAgencyCompositeRatings)||[]);this.$set(this,'schemeRatings',(res.data&&res.data.schemeServiceRatings)||[])}}).finally(()=>{this.$set(this,'summaryLoading',false)})},
loadFeedback(){this.$set(this,'feedbackLoading',true);this.$axios.post(loc()+'/getSatisfactionFeedbackDetails',{year:this.pageForm.year,typeName:this.feedbackType}).then((res)=>{if(res.code===0)this.$set(this,'feedbackDetails',res.data||[])}).finally(()=>{this.$set(this,'feedbackLoading',false)})},exportFeedback(){this.$downLoad(loc()+'/exportSatisfactionFeedbackDetails',{year:this.pageForm.year,typeName:this.feedbackType})},viewFeedback(row){this.$set(this,'feedbackDetail',row);this.$set(this,'feedbackVisible',true)},
loadLines(){this.$axios.post(loc()+'/lineList',{year:this.pageForm.year}).then((res)=>{if(res.code===0)this.$set(this,'lineList',res.data||[])})},pageData(){this.$set(this,'tableLoading',true);this.$axios.post(loc()+'/pageData',this.pageForm).then((res)=>{if(res.code===0){this.$set(this,'tableData',res.data.list||[]);this.$set(this.pageForm,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'tableLoading',false)})},exportEvaluate(){this.$downLoad(loc()+'/exportEvaluate',this.pageForm)}
},created(){this.$businessTool.listUnion().then((res)=>this.$set(this,'unionList',res||[]));this.$businessTool.listUnit().then((res)=>this.$set(this,'unitList',res||[]));this.loadLines();this.loadSummary()}})
new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
return {
year: new Date().getFullYear().toString(),
summaryLoading: false,
feedbackLoading: false,
travelAgencyCompositeRatings: [],
schemeServiceRatings: [],
schemeType: '线路',
selectedSchemeId: '',
feedbackDetails: [],
feedbackType: '全部',
feedbackPage: 1,
feedbackPageSize: 10,
feedbackDetail: {},
feedbackDetailVisible: false,
schemeFeedbackVisible: false
}
},
methods: {
doSearch() {
this.getSatisfactionRatingStatistics()
this.getSatisfactionFeedbackDetails()
},
getSatisfactionRatingStatistics() {
this.$set(this, 'summaryLoading', true)
this.$axios.post(loc() + '/getSatisfactionRatingStatistics', {year: this.year}).then((res) => {
if (res.code === 0) {
const statisticData = res.data || {}
this.$set(this, 'travelAgencyCompositeRatings', statisticData.travelAgencyCompositeRatings || [])
this.$set(this, 'schemeServiceRatings', statisticData.schemeServiceRatings || [])
this.resetSelectedScheme()
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, 'summaryLoading', false)
})
},
getSatisfactionFeedbackDetails() {
this.$set(this, 'feedbackLoading', true)
this.$axios.post(loc() + '/getSatisfactionFeedbackDetails', {year: this.year}).then((res) => {
if (res.code === 0) {
this.$set(this, 'feedbackDetails', res.data || [])
this.$set(this, 'feedbackPage', 1)
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, 'feedbackLoading', false)
})
},
formatRatingScore(score) {
if (score === null || score === undefined || score === '') return '-'
return Number(score).toFixed(1)
},
hasRatingScore(score) {
return score !== null && score !== undefined && score !== ''
},
formatFeedbackScore(score) {
if (score === null || score === undefined || score === '') return '-'
return Number(score).toFixed(1) + ' 分'
},
getFeedbackContentSummary(content) {
const feedbackContent = String(content || '').trim()
if (!feedbackContent) return '暂无评价建议'
return feedbackContent.length > 36 ? feedbackContent.substring(0, 36) + '…' : feedbackContent
},
changeSchemeType(value) {
this.$set(this, 'schemeType', value)
this.resetSelectedScheme()
},
selectScheme(schemeRating) {
this.$set(this, 'selectedSchemeId', schemeRating.schemeId)
},
isSelectedScheme(schemeRating) {
return String(this.selectedSchemeId) === String(schemeRating.schemeId)
},
getSchemeRatingsByType() {
return this.schemeServiceRatings.filter((item) => item.typeName === this.schemeType)
},
getSelectedSchemeRating() {
return this.getSchemeRatingsByType().find((item) => this.isSelectedScheme(item))
},
resetSelectedScheme() {
const schemeRatings = this.getSchemeRatingsByType()
this.$set(this, 'selectedSchemeId', schemeRatings.length ? schemeRatings[0].schemeId : '')
},
getSelectedSchemeMetrics() {
const schemeRating = this.getSelectedSchemeRating() || {}
const metrics = [
{key: 'travelAgency', label: '旅行社', icon: 'el-icon-guide', avg: schemeRating.travelAgencyAvg, count: schemeRating.travelAgencyCount},
{key: 'journey', label: '行程', icon: 'el-icon-map-location', avg: schemeRating.journeyAvg, count: schemeRating.journeyCount},
{key: 'accommodation', label: '住宿', icon: 'el-icon-house', avg: schemeRating.accommodationAvg, count: schemeRating.accommodationCount},
{key: 'dining', label: '餐饮', icon: 'el-icon-food', avg: schemeRating.diningAvg, count: schemeRating.diningCount},
{key: 'transportation', label: '交通', icon: 'el-icon-truck', avg: schemeRating.transportationAvg, count: schemeRating.transportationCount}
]
if (schemeRating.typeName === '线路') return metrics
return [
metrics[0],
{key: 'accommodation', label: '酒店', icon: 'el-icon-house', avg: schemeRating.accommodationAvg, count: schemeRating.accommodationCount}
]
},
getSelectedSchemeFeedbackDetails() {
const schemeRating = this.getSelectedSchemeRating()
if (!schemeRating) return []
return this.feedbackDetails.filter((item) => item.typeName === schemeRating.typeName
&& String(item.schemeId) === String(schemeRating.schemeId)
&& String(item.feedbackContent || '').trim())
},
openSchemeFeedbackDialog() {
this.$set(this, 'schemeFeedbackVisible', true)
},
closeSchemeFeedbackDialog() {
this.$set(this, 'schemeFeedbackVisible', false)
},
changeFeedbackType(value) {
this.$set(this, 'feedbackType', value)
this.$set(this, 'feedbackPage', 1)
},
exportFeedbackDetails() {
this.$downLoad(loc() + '/exportSatisfactionFeedbackDetails', {
year: this.year,
typeName: this.feedbackType
})
},
getFilteredFeedbackDetails() {
if (this.feedbackType === '全部') return this.feedbackDetails
return this.feedbackDetails.filter((item) => item.typeName === this.feedbackType)
},
getPagedFeedbackDetails() {
const start = (this.feedbackPage - 1) * this.feedbackPageSize
return this.getFilteredFeedbackDetails().slice(start, start + this.feedbackPageSize)
},
feedbackIndexMethod(index) {
return (this.feedbackPage - 1) * this.feedbackPageSize + index + 1
},
changeFeedbackPage(page) {
this.$set(this, 'feedbackPage', page)
},
changeFeedbackPageSize(pageSize) {
this.$set(this, 'feedbackPageSize', pageSize)
this.$set(this, 'feedbackPage', 1)
},
openFeedbackDetail(feedbackDetail) {
this.$set(this, 'feedbackDetail', feedbackDetail)
this.$set(this, 'feedbackDetailVisible', true)
},
closeFeedbackDetail() {
this.$set(this, 'feedbackDetailVisible', false)
this.$set(this, 'feedbackDetail', {})
}
},
created() {
this.doSearch()
}
})
</script>
<!--# } #-->
<!--#
}
#-->