Compare commits

13 Commits
50 changed files with 3187 additions and 549 deletions
+16 -1
View File
@@ -1,6 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="nutz" />
<option name="name" value="nutz" />
<option name="url" value="https://jfrog.nutz.cn/artifactory/libs-release" />
</remote-repository>
<remote-repository>
<option name="id" value="nutz-snapshots" />
<option name="name" value="nutz-snapshots" />
<option name="url" value="https://jfrog.nutz.cn/artifactory/snapshots" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
@@ -27,4 +42,4 @@
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
</component>
</project>
</project>
+1 -2
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="KubernetesApiProvider"><![CDATA[{}]]></component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
@@ -10,4 +9,4 @@
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
</project>
</project>
@@ -38,6 +38,7 @@ import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Base64;
@@ -270,10 +271,12 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
}
}
HtmlSaveOptions saveOptions = new HtmlSaveOptions();
// Aspose输出和字节流读取必须固定使用UTF-8,避免不同服务器的JVM默认编码导致中文乱码。
saveOptions.setEncoding(StandardCharsets.UTF_8);
saveOptions.setExportImagesAsBase64(true);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
doc.save(bos, saveOptions);
String htmlContent = Jsoup.parse(bos.toString()).body().html();
String htmlContent = Jsoup.parse(bos.toString(StandardCharsets.UTF_8)).body().html();
Pattern pattern = Pattern.compile(IMG_BASE64_PATTERN, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(htmlContent);
@@ -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);
@@ -165,6 +165,60 @@ public class WelfareListController {
welfareListService.exportXlsx(pageForm, response);
}
/**
* 导出指定福利项目和套餐的报销表。
*
* @param projectId 福利项目 ID
* @param optionId 福利套餐 ID,必须属于 projectId 对应项目
* @param response XLSX 文件下载响应
*/
@At
@Ok("void")
@SaCheckPermission("welfare.list.mange")
@ApiOperation("导出福利报销表")
public void doExcelByOptionId(@Param("projectId") String projectId,
@Param("optionId") String optionId,
HttpServletResponse response) {
welfareListService.doExcelByOptionId(projectId, optionId, response);
}
/**
* 导出供货商使用的套餐选择数据,按套餐拆分工作表。
*
* @param projectId 福利项目 ID
* @param unionId 可选的分工会 ID;为空时按当前用户的数据权限导出
* @param response XLSX 文件下载响应
*/
@At
@Ok("void")
@SaCheckPermission("welfare.list.mange")
@ApiOperation("导出供货商选择数据")
public void exportSelectData(@Param("projectId") String projectId,
@Param("unionId") String unionId,
HttpServletResponse response) {
welfareListService.exportSelectData(projectId, unionId, response);
}
/**
* 导入供应商提供的快递单号文件。
*
* @param file XLS 或 XLSX 文件,包含工号与最多四个快递单号列
* @param projectId 福利项目 ID
* @param optionId 福利套餐 ID,必须属于 projectId 对应项目
* @return 导入结果;成功时返回成功提示,失败时返回具体原因
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("welfare.list.mange")
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
@SLog(tag = "福利名单管理", msg = "导入福利快递单号")
@ApiOperation("导入福利快递单号")
public Result importCourierNumber(@Param("file") TempFile file,
@Param("projectId") String projectId,
@Param("optionId") String optionId) {
return welfareListService.importCourierNumber(file, projectId, optionId);
}
@At
@SaCheckPermission("welfare.list.mange")
@ApiOperation("编辑备注")
@@ -60,7 +60,6 @@ public class WelfareMineController {
END AS isChoose,
GROUP_CONCAT(DISTINCT wuso.optionName ,'',wus.selectNum,'份)') AS gist_list,
MAX(wus.selectTime) AS selectTime,
MAX(wus.deliveryDate) AS deliveryDate,
wus.receiveAddress,
wl.userId
FROM
@@ -98,7 +97,6 @@ public class WelfareMineController {
wpus.receiveAddress,
wpso.optionName,
wpus.selectNum,
wpus.deliveryDate,
wpus.remark,
wpus.userSign,
wpus.courierNumber,
@@ -0,0 +1,47 @@
package com.budwk.app.zhgh.welfare.controller;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.validation.Valid;
/**
* 福利通知附件查看入口。
*/
@IocBean
@Ok("json:full")
@At("/platform/welfare/notice")
@Api(tags = "福利通知")
public class WelfareNoticeController {
@Inject
private WelfareProjectService projectService;
@At
@Ok("beetl:/platform/zhghh5/welfare/notice/detail.html")
@ApiOperation("福利通知附件详情页")
public void index() {
}
/**
* 查询福利通知附件详情。
*
* @param id 福利项目 ID
* @return JSON 结果,data 中包含项目名称、文件名称、下载地址和 PDF 预览地址
*/
@At
@ApiOperation("查询福利通知附件详情")
public Result detailData(@Valid String id) {
if (StrUtil.isBlank(id)) {
return Result.error("福利项目ID不能为空");
}
return Result.success(projectService.getNoticeAttachmentInfo(id));
}
}
@@ -110,8 +110,8 @@ public class WelfareStatisticsController {
@Ok("void")
@SaCheckPermission("welfare.statistics")
@ApiOperation("按福利选项导出")
public void exportByWelfareOptions(String projectId, HttpServletResponse response) {
welfareStatisticsService.exportByWelfareOptions(projectId, response);
public void exportByWelfareOptions(String projectId, String unionId, HttpServletResponse response) {
welfareStatisticsService.exportByWelfareOptions(projectId, unionId, response);
}
@At
@@ -8,7 +8,6 @@ import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.welfare.model.WelfareList;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareUserSelection;
import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
import io.swagger.annotations.ApiOperation;
@@ -24,10 +23,8 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@IocBean
@Ok("json:full")
@@ -57,7 +54,6 @@ public class WelfareUserSelectController {
ELSE 0
END AS isChoose,
wus.selectTime,
MAX(wus.deliveryDate) AS deliveryDate,
MAX(wus.remark) AS remark,
GROUP_CONCAT(DISTINCT wuso.optionName ,'',wus.selectNum,'份)') AS gist_list,
wus.receiveAddress
@@ -93,8 +89,6 @@ public class WelfareUserSelectController {
if (count == 0) {
return Result.error("您没有选择的权限");
}
WelfareProject project = dao.fetch(WelfareProject.class, projectId);
Date selectTime = new Date();
String additionalInfoValidationMessage = welfareProjectService.validateSelectionAdditionalInfo(selections, selectTime);
if (additionalInfoValidationMessage != null) {
@@ -112,9 +106,9 @@ public class WelfareUserSelectController {
return Result.error(groupQuantityValidationMessage);
}
int sum = Arrays.stream(selections).mapToInt(selection -> Objects.requireNonNullElse(selection.getSelectNum(), 0)).sum();
if (sum > project.getMultiSelectNum()) {
return Result.error("选择的数量不能超过" + project.getMultiSelectNum());
String projectQuantityValidationMessage = welfareProjectService.validateProjectSelectionQuantity(projectId, selections);
if (projectQuantityValidationMessage != null) {
return Result.error(projectQuantityValidationMessage);
}
@@ -30,9 +30,19 @@ public class WelfareExportEntityTc {
@Excel(name = "已选福利", width = 80d)
private String optionName;
@Excel(name = "收货信息", width = 100d)
// V4 原“收货信息”导出列保留字段,现按 V3 规则拆分为收货人、联系方式和收货地址。
// @Excel(name = "收货信息", width = 100d)
private String receiveAddress;
@Excel(name = "收货人", width = 15d)
private String recipient;
@Excel(name = "联系方式", width = 20d)
private String phone;
@Excel(name = "收货地址", width = 60d)
private String address;
@Excel(name = "快递单号", width = 40d)
private String courierNumber;
@@ -207,6 +207,14 @@ public class WelfareProject extends BaseModel implements SysHomeConvert {
@ColDefine(type = ColType.VARCHAR, width = 200)
private String cover;
/**
* 福利通知附件下载地址,只允许关联一个 Word 文件。
*/
@Column
@Comment("福利通知附件")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String noticeAttachment;
@Column
@Comment("是否全年生日蛋糕卷")
@ColDefine(type = ColType.BOOLEAN)
@@ -37,4 +37,10 @@ public class WelfareSelectionSituationPageForm extends PageForm {
@ApiModelProperty("人员属性")
private String userAttribute;
@ApiModelProperty("人员类型")
private String[] personTypes;
@ApiModelProperty("在职状态")
private String[] userStates;
}
@@ -13,10 +13,32 @@ import javax.servlet.http.HttpServletResponse;
public interface WelfareListService extends BaseService<WelfareList> {
/**
* 导出一个福利项目下指定套餐的报销表。
*
* @param projectId 福利项目 ID
* @param optionId 福利套餐 ID,必须属于指定福利项目
* @param response XLSX 文件下载响应
*/
void doExcelByOptionId(String projectId, String optionId, HttpServletResponse response);
/**
* 导出供应商所需的选择数据,并按套餐创建工作表。
*
* @param projectId 福利项目 ID
* @param unionId 可选的分工会 ID;为空时按当前登录人的数据范围导出
* @param response XLSX 文件下载响应
*/
void exportSelectData(String projectId, String unionId, HttpServletResponse response);
/**
* 导入指定福利项目、套餐下的快递单号。
*
* @param file XLS 或 XLSX 文件,列为工号、姓名和最多四个快递单号
* @param projectId 福利项目 ID
* @param optionId 福利套餐 ID,必须属于指定福利项目
* @return 成功或失败的导入结果
*/
Result importCourierNumber(TempFile file, String projectId, String optionId);
void doEditWelfareData(String editWelfareData, String welfareOptions);
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.welfare.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareUserSelection;
import org.nutz.lang.util.NutMap;
import java.util.Date;
import java.util.List;
@@ -25,6 +26,14 @@ public interface WelfareProjectService extends BaseService<WelfareProject> {
*/
List<WelfareProject> listHistoryProject();
/**
* 查询福利通知详情页需要的项目和附件信息。
*
* @param projectId 福利项目 ID
* @return 包含 projectName、fileId、fileName、suffix、attachmentUrl 和 previewUrl 的详情数据;未上传附件时文件字段为空
*/
NutMap getNoticeAttachmentInfo(String projectId);
/**
* 校验福利选择是否保留系统默认福利。
*
@@ -44,11 +53,19 @@ public interface WelfareProjectService extends BaseService<WelfareProject> {
String validateGroupSelectionQuantity(String projectId, WelfareUserSelection[] selections);
/**
* 校验福利选择附加信息。配送时间为必填项,必须按日期粒度不早于本次选择时间;
* 同一次提交的全部福利项必须使用相同配送时间和备注。
* 校验福利项目一次提交的总选择份数。
*
* @param selections 用户提交的福利选择数组,每项需包含 deliveryDate,可选包含 remark
* @param selectTime 后端生成的本次选择时间,用于防止客户端伪造日期
* @param projectId 福利项目 ID,用于读取单选或多选模式及多选数量上限
* @param selections 用户提交的福利选择数组,每项需包含 selectOptionId 和 selectNum
* @return 校验通过返回 {@code null};超过项目上限或多选项目未配置上限时返回错误提示
*/
String validateProjectSelectionQuantity(String projectId, WelfareUserSelection[] selections);
/**
* 校验福利选择附加信息。同一次提交的全部福利项必须使用相同备注,配送时间字段已停用。
*
* @param selections 用户提交的福利选择数组,每项可包含 remark
* @param selectTime 后端生成的本次选择时间,保留该参数以兼容现有调用接口
* @return 校验通过返回 {@code null},否则返回可直接展示的错误提示
*/
String validateSelectionAdditionalInfo(WelfareUserSelection[] selections, Date selectTime);
@@ -15,12 +15,25 @@ public interface WelfareSelectionSituationService extends BaseService<WelfareLis
* 根据选择情况页面的全部查询条件发送钉钉消息。
*
* @param pageForm 查询条件,包含项目、福利选项、人员信息、组织范围及选择状态
* @param messageContent 纯文本消息内容,不应包含HTML标签或富文本样式
* @param messageContent 纯文本消息内容,不应包含HTML标签或富文本样式;发送前会补充福利通知详情地址
* @return 实际提交到钉钉消息渠道的去重人员数量
*/
int sendDingTalkMessage(WelfareSelectionSituationPageForm pageForm, String messageContent);
/**
* 按选择情况页面的查询条件导出人员选择明细。
*
* @param pageForm 页面查询条件,包含项目、福利选项、姓名、工号、组织范围、人员类型和选择状态
* @param response HTTP响应,返回包含收货备注、快递单号和签字图片的XLSX文件流
*/
void exportXlsx(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response);
/**
* 导出福利领取表。
*
* @param pageForm 页面查询条件,projectId指定福利项目
* @param response HTTP响应,返回包含福利选项、收货备注和签字图片的XLSX文件流
*/
void receiveXlsx(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response);
/**
@@ -62,7 +62,7 @@ public interface WelfareStatisticsService extends BaseService<WelfareProject> {
* @param projectId
* @param response
*/
void exportByWelfareOptions(String projectId, HttpServletResponse response);
void exportByWelfareOptions(String projectId, String unionId, HttpServletResponse response);
/**
* 导出汇总表
@@ -15,8 +15,8 @@ import javax.servlet.http.HttpServletResponse;
public interface WelfareUserEvaluationService extends BaseService<WelfareUserEvaluation> {
/**
* 校验当前用户是否已到福利配送日期。只有存在选择记录、全部记录已设置配送日期,且当前日期
* 不早于配送日期时才允许打开或提交评价
* 校验当前用户是否已经选择福利且项目活动已经结束。活动结束时间以福利项目的
* choiceTimeEnd 为准,PC、移动端与接口提交使用同一规则
*
* @param projectId 福利项目ID
* @param userId 当前登录用户ID
@@ -61,8 +61,17 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
@Override
public void doExcelByOptionId(String projectId, String optionId, HttpServletResponse response) {
if (StrUtil.isBlank(projectId) || StrUtil.isBlank(optionId)) {
log.warn("导出报销表失败:福利项目或套餐不能为空");
return;
}
WelfareProject project = dao().fetch(WelfareProject.class, projectId);
WelfareProjectSubjectOption subjectOption = dao().fetch(WelfareProjectSubjectOption.class, optionId);
WelfareProjectSubjectOption subjectOption = dao().fetch(WelfareProjectSubjectOption.class,
Cnd.where("id", "=", optionId).and("welfareId", "=", projectId));
if (project == null || subjectOption == null) {
log.warn("导出报销表失败:福利项目或套餐不存在,projectId={}, optionId={}", projectId, optionId);
return;
}
Sql sql = Sqls.create("""
SELECT
@@ -75,30 +84,35 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
wpus.selectNum
FROM
welfare_project_user_selection wpus
LEFT JOIN `welfare_list` wl ON wpus.selectUserId = wl.userId
INNER JOIN `welfare_list` wl ON wl.userId = wpus.selectUserId
AND wl.projectId = wpus.welfareId
LEFT JOIN `sys_user` u ON u.id = wl.userId
LEFT JOIN sys_union un ON un.id = wl.welfareUnitId
LEFT JOIN sys_unit it ON it.id = wl.welfareSecondLevelUnitId
LEFT JOIN sys_union un ON un.id = wl.welfareUnionId
LEFT JOIN sys_unit it ON it.id = wl.welfareUnitId
WHERE
wl.projectId = @welfareId
AND wpus.selectOptionId = @selectOptionId and wpus.selectNum!=0
wpus.welfareId = @welfareId
AND wpus.selectOptionId = @selectOptionId
AND IFNULL(wpus.selectNum, 0) <> 0
ORDER BY wl.welfareUnionName, wl.welfareUnitName, u.loginname
""").setParam("welfareId", projectId).setParam("selectOptionId", optionId);
List<NutMap> mapList = listMap(sql);
List<String> userId = mapList.stream().map(m -> m.getString("id")).collect(Collectors.toList());
List<WelfareCourierNumber> courierNumberList = dao().query(WelfareCourierNumber.class,
List<WelfareCourierNumber> courierNumberList = userId.isEmpty()
? Collections.emptyList()
: dao().query(WelfareCourierNumber.class,
Cnd.where("selectUserId", "in", userId)
.and("welfareId", "=", projectId)
.and("selectOptionId", "=", optionId));
.and("selectOptionId", "=", optionId)
.asc("createdAt"));
List<String> nameList = List.of("one", "two", "three", "four");
for (NutMap map : mapList) {
List<WelfareCourierNumber> userCourierNumbers = courierNumberList.stream()
.filter(c -> c.getSelectUserId().equals(map.getString("id"))).collect(Collectors.toList());
for (int i = 0; i < userCourierNumbers.size(); i++) {
for (int i = 0; i < userCourierNumbers.size() && i < nameList.size(); i++) {
map.setv(nameList.get(i) + "CourierNumber", userCourierNumbers.get(i).getCourierNumber());
}
}
@@ -138,14 +152,13 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
exportParams.setSheetName(un);
service.createSheetForMap(workbook, exportParams, exportEntities, v);
}*/
try {
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle(project.getName() + subjectOption.getOptionName() + "发放名单");
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, mapList);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle(project.getName() + subjectOption.getOptionName() + "报销表");
try (Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, mapList)) {
CommonDownloadUtil.download(project.getName() + subjectOption.getOptionName() + ".xlsx", workbook, response);
} catch (Exception e) {
e.printStackTrace();
log.error("导出报销表失败,projectId={}, optionId={}", projectId, optionId, e);
}
}
@@ -153,6 +166,10 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
@Override
public void exportSelectData(String projectId, String unionId, HttpServletResponse response) {
WelfareProject welfareProject = dao().fetch(WelfareProject.class, Cnd.where("id", "=", projectId));
if (welfareProject == null) {
log.warn("导出供货商选择数据失败:福利项目不存在,projectId={}", projectId);
return;
}
String projectName = welfareProject.getName();
Cnd cnd = Cnd.NEW();
Sql sql = Sqls.create("""
@@ -170,16 +187,18 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
left join `sys_user` u on u.id = wpus.selectUserId
left join welfare_project_subject_option wpso on wpso.id = wpus.selectOptionId
left join welfare_list wl on wl.userId = wpus.selectUserId and wl.projectId=wpus.welfareId
LEFT JOIN sys_union un ON un.id = wl.welfareUnitId
LEFT JOIN sys_unit it ON it.id = wl.welfareSecondLevelUnitId
LEFT JOIN sys_union un ON un.id = wl.welfareUnionId
LEFT JOIN sys_unit it ON it.id = wl.welfareUnitId
$condition
""");
cnd.and("wpus.welfareId", "=", projectId);
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(), RoleConstant.SCHOOL_UNION_WELFARE_ADMIN.name())) {
cnd.and("wl.welfareUnitId", "=", SecurityUtil.getUnionId());
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name())) {
cnd.and("wl.welfareUnionId", "=", SecurityUtil.getUnionId());
}
cnd.andEX("wl.welfareUnitId", "=", unionId);
cnd.andEX("wl.welfareUnionId", "=", unionId);
cnd.groupBy("wpus.selectUserId", "wpus.selectOptionId");
cnd.asc("it.unitcode");
sql.setCondition(cnd);
@@ -207,12 +226,21 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
try {
CommonDownloadUtil.download(projectName + ".xlsx", workbook, response);
} catch (Exception e) {
e.printStackTrace();
log.error("导出供货商选择数据失败,projectId={}", projectId, e);
}
}
@Override
@Aop(TransAop.READ_COMMITTED)
public Result importCourierNumber(TempFile file, String projectId, String optionId) {
if (StrUtil.isBlank(projectId) || StrUtil.isBlank(optionId)) {
return Result.error("福利项目和套餐不能为空");
}
WelfareProjectSubjectOption subjectOption = dao().fetch(WelfareProjectSubjectOption.class,
Cnd.where("id", "=", optionId).and("welfareId", "=", projectId));
if (subjectOption == null) {
return Result.error("福利套餐不存在或不属于当前福利项目");
}
if (Lang.isEmpty(file)) {
return Result.error("上传的文件不能为空");
}
@@ -226,7 +254,7 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
ImportParams importParams = new ImportParams();
excelList = ExcelImportUtil.importExcel(file.getFile(), CourierNumberExcelMode.class, importParams);
} catch (Exception e) {
e.printStackTrace();
log.warn("读取快递单号导入文件失败", e);
return Result.error("读取不到数据,请检查excel文件格式");
}
@@ -236,7 +264,7 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
try {
if (excelList.stream().anyMatch(v -> StrUtil.isBlank(v.getLoginName()))) {
return Result.error("工号和快递单号不能为空");
return Result.error("工号不能为空");
}
List<CourierNumberExcelMode> filterExcelList = excelList.stream().collect(Collectors.collectingAndThen(
@@ -249,6 +277,9 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
Sql loginNameSql = Sqls.create("select id,loginname from sys_user where loginname in (@loginNameList)");
loginNameSql.setParam("loginNameList", loginNameList);
List<NutMap> loginNameMaps = listMap(loginNameSql);
if (loginNameMaps.isEmpty()) {
return Result.error("Excel中的工号均未匹配到系统用户");
}
List<String> userIds = loginNameMaps.stream().map(v -> v.getString("id")).collect(Collectors.toList());
@@ -291,12 +322,13 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
welfareCourierNumbers.add(courierNumber);
}
}
insert(welfareCourierNumbers);
if (!welfareCourierNumbers.isEmpty()) {
insert(welfareCourierNumbers);
}
return Result.success("导入成功");
} catch (Exception e) {
e.printStackTrace();
log.error("导入快递单号失败,projectId={}, optionId={}", projectId, optionId, e);
return Result.error("导入快递单号失败");
}
@@ -685,12 +717,10 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
t2.loginname,
t2.username,
t2.sex,
DATE_FORMAT(t2.birthday, '%Y-%m-%d') AS birthday,
threeUnit.name AS threeUnitName
DATE_FORMAT(t2.birthday, '%Y-%m-%d') AS birthday
FROM
`welfare_list` t1
LEFT JOIN sys_user t2 ON t2.id = t1.userId
LEFT JOIN sys_unit threeUnit ON threeUnit.id = t2.threeUnitId
$condition
""");
Cnd cnd = Cnd.NEW();
@@ -735,7 +765,6 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
entities.add(new ExcelExportEntity("在职状态", "userState", 20));
entities.add(new ExcelExportEntity("所属工会", "welfareUnionName", 20));
entities.add(new ExcelExportEntity("所属单位", "welfareUnitName", 20));
entities.add(new ExcelExportEntity("三级单位", "threeUnitName", 20));
entities.add(new ExcelExportEntity("备注", "remark", 20));
// 设置导出参数
@@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.welfare.model.WelfareList;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
@@ -79,6 +80,25 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
return dao().query(WelfareProject.class, Cnd.NEW().desc(WelfareProject::getYear).desc(WelfareProject::getChoiceTimeStart));
}
@Override
public NutMap getNoticeAttachmentInfo(String projectId) {
WelfareProject project = fetch(projectId);
if (project == null) {
throw new IllegalArgumentException("福利项目不存在");
}
NutMap result = NutMap.NEW().addv("projectName", project.getName());
if (StrUtil.isBlank(project.getNoticeAttachment())) {
return result;
}
Sys_file noticeFile = resolveNoticeAttachmentFile(project.getNoticeAttachment());
return result
.addv("fileId", noticeFile.getId())
.addv("fileName", noticeFile.getName())
.addv("suffix", noticeFile.getSuffix())
.addv("attachmentUrl", noticeFile.getDownloadPath())
.addv("previewUrl", "/platform/sys/file/convertPDF?id=" + noticeFile.getId());
}
@Override
public String validateSystemDefaultOptionSelection(String projectId, WelfareUserSelection[] selections) {
List<WelfareProjectSubjectOption> systemDefaultOptions = dao().query(WelfareProjectSubjectOption.class,
@@ -182,8 +202,42 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
}
/**
* 校验一次选择提交中的配送日期和备注。日期比较统一截断到当天零点,允许选择当天配送;
* 多项福利的附加信息必须一致,避免同一次选择产生互相冲突的评价开放时间
* 校验福利项目总选择份数。单选项目固定只能选择 1 份,不读取仅供多选项目使用的
* multiSelectNum;多选项目必须配置有效的数量上限
*
* @param projectId 福利项目 ID
* @param selections 当前用户提交的福利选择数据
* @return 校验通过返回 {@code null},否则返回可直接展示的错误提示
*/
@Override
public String validateProjectSelectionQuantity(String projectId, WelfareUserSelection[] selections) {
WelfareProject project = fetch(projectId);
if (project == null) {
return "福利项目不存在";
}
int selectedQuantity = Arrays.stream(selections == null ? new WelfareUserSelection[0] : selections)
.filter(Objects::nonNull)
.mapToInt(selection -> Objects.requireNonNullElse(selection.getSelectNum(), 0))
.sum();
boolean multipleSelection = "checkBox".equals(project.getIsCheckBox());
int maxSelectQuantity;
if (multipleSelection) {
if (project.getMultiSelectNum() == null || project.getMultiSelectNum() <= 0) {
return "福利项目未配置最多选择数量";
}
maxSelectQuantity = project.getMultiSelectNum();
} else {
maxSelectQuantity = 1;
}
if (selectedQuantity > maxSelectQuantity) {
return "选择的数量不能超过" + maxSelectQuantity;
}
return null;
}
/**
* 校验一次选择提交中的备注。配送时间字段已停用,新提交数据统一清空该字段;
* 多项福利的备注必须一致,避免同一次选择出现互相冲突的附加信息。
*
* @param selections 当前提交的福利选择数据
* @param selectTime 后端生成的选择时间
@@ -194,30 +248,23 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
if (selections == null || selections.length == 0) {
return "请至少选择一项福利";
}
Date selectionDate = DateUtil.beginOfDay(selectTime == null ? new Date() : selectTime);
Date submittedDeliveryDate = null;
String submittedRemark = null;
boolean firstSelection = true;
for (WelfareUserSelection selection : selections) {
if (selection == null || selection.getDeliveryDate() == null) {
return "请选择配送时间";
}
Date deliveryDate = DateUtil.beginOfDay(selection.getDeliveryDate());
if (deliveryDate.before(selectionDate)) {
return "配送时间不能早于选择时间";
}
if (submittedDeliveryDate == null) {
submittedDeliveryDate = deliveryDate;
submittedRemark = StrUtil.trim(selection.getRemark());
} else if (!submittedDeliveryDate.equals(deliveryDate)) {
return "同一次选择的配送时间必须一致";
} else if (!Objects.equals(submittedRemark, StrUtil.trim(selection.getRemark()))) {
return "同一次选择的备注必须一致";
if (selection == null) {
return "福利选择数据不能为空";
}
String remark = StrUtil.trim(selection.getRemark());
if (firstSelection) {
submittedRemark = remark;
firstSelection = false;
} else if (!Objects.equals(submittedRemark, remark)) {
return "同一次选择的备注必须一致";
}
if (remark != null && remark.length() > 200) {
return "备注不能超过200字";
}
selection.setDeliveryDate(deliveryDate);
selection.setDeliveryDate(null);
selection.setRemark(remark);
}
return null;
@@ -257,6 +304,7 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveProject(WelfareProject project) {
normalizeNoticeAttachment(project);
// 保存前根据福利选项分组去重并校验选择数量范围,防止非法 JSON 配置进入数据库。
normalizeGroupSelectionConfigs(project);
// 插入项目
@@ -272,6 +320,7 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateProject(WelfareProject project) {
normalizeNoticeAttachment(project);
// 修改项目时同样归一化分组配置,确保新增、编辑两条保存链路规则一致。
normalizeGroupSelectionConfigs(project);
// 更新项目
@@ -325,6 +374,55 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
dao().clear(Sys_home_activity.class, Cnd.where("id", "=", id));
}
/**
* 校验福利通知附件数量和真实文件类型,并将提交地址归一化为系统文件下载地址。
*
* @param project 待保存的福利项目,noticeAttachment 应为单个系统文件下载地址
*/
private void normalizeNoticeAttachment(WelfareProject project) {
String attachment = StrUtil.trim(project.getNoticeAttachment());
if (StrUtil.isBlank(attachment)) {
project.setNoticeAttachment(null);
return;
}
if (attachment.contains(",")) {
throw new IllegalArgumentException("福利通知附件只能上传一个文件");
}
Sys_file noticeFile = resolveNoticeAttachmentFile(attachment);
String suffix = StrUtil.blankToDefault(noticeFile.getSuffix(), "").toLowerCase(Locale.ROOT);
if (!"doc".equals(suffix) && !"docx".equals(suffix)) {
throw new IllegalArgumentException("福利通知附件只能上传Word文件");
}
project.setNoticeAttachment(noticeFile.getDownloadPath());
}
/**
* 从系统下载地址解析文件 ID,并确认文件记录真实存在。
*
* @param attachment 系统文件下载地址,格式为 /platform/sys/file/download?id=文件ID
* @return 对应的系统文件记录
*/
private Sys_file resolveNoticeAttachmentFile(String attachment) {
int idIndex = attachment.indexOf("id=");
if (idIndex < 0) {
throw new IllegalArgumentException("福利通知附件地址无效");
}
String fileId = attachment.substring(idIndex + 3);
int parameterIndex = fileId.indexOf('&');
if (parameterIndex >= 0) {
fileId = fileId.substring(0, parameterIndex);
}
fileId = fileId.trim();
if (StrUtil.isBlank(fileId)) {
throw new IllegalArgumentException("福利通知附件地址无效");
}
Sys_file noticeFile = dao().fetch(Sys_file.class, fileId);
if (noticeFile == null) {
throw new IllegalArgumentException("福利通知附件文件不存在,请重新上传");
}
return noticeFile;
}
/**
* 根据福利选项中的非空分组生成最终配置数组,并校验最小、最大选择数量。
* 最大选择数量为 0 时表示不限,不参与最小值与最大值的大小比较。
@@ -14,6 +14,7 @@ import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.sys.models.Sys_file;
@@ -21,6 +22,7 @@ import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.welfare.model.WelfareCourierNumber;
import com.budwk.app.zhgh.welfare.model.WelfareList;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareProjectSubjectOption;
@@ -47,6 +49,7 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
@@ -75,7 +78,7 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
GROUP_CONCAT(DISTINCT t3.optionName ,'',t2.selectNum,'') AS selectedOptions,
GROUP_CONCAT(DISTINCT t2.mobile) AS mobile,
GROUP_CONCAT(DISTINCT t2.receiveAddress) AS receiveAddress,
MAX(t2.deliveryDate) AS deliveryDate,
MAX(t2.remark) AS remark,
t4.username AS userName,
t4.loginname AS loginName,
t4.sex,
@@ -143,13 +146,36 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
String title = project.getName() + "-福利通知";
boolean success = smsService.sendMsg("6", loginNames, null, title, plainContent, null, null);
String detailUrl = StrUtil.removeSuffix(Globals.AppDomain, "/")
+ "/platform/welfare/notice/index?id=" + project.getId();
String detailLine = "查看详情:" + detailUrl;
String messageBody = removeExistingNoticeDetailLinks(plainContent, detailUrl);
String sendContent = StrUtil.isBlank(messageBody) ? detailLine : messageBody + "\n" + detailLine;
// 详情地址已经包含在消息正文中跳转参数保持为空避免消息中心再次追加 PC移动端详情链接
boolean success = smsService.sendMsg("6", loginNames, null, title, sendContent, null, null);
if (!success) {
throw new BaseException("钉钉消息发送失败,请检查消息发送配置或稍后重试");
}
return loginNames.size();
}
/**
* 删除正文中已存在的当前福利详情链接避免管理员在默认链接前继续输入文字后重复追加
*
* @param content 管理员在发送弹窗中编辑的纯文本正文可在任意位置包含查看详情URL
* @param detailUrl 当前福利项目的标准详情地址
* @return 已移除全部当前项目详情链接并清理空白行的消息正文没有其他正文时返回空字符串
*/
private String removeExistingNoticeDetailLinks(String content, String detailUrl) {
String detailPattern = "查看详情\\s*[:]\\s*" + Pattern.quote(detailUrl);
String contentWithoutDetail = content.replaceAll(detailPattern, "");
return Arrays.stream(contentWithoutDetail.split("\\R"))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.collect(Collectors.joining("\n"))
.trim();
}
/**
* 构建选择情况列表与消息接收人共用的筛选条件
* 除页面条件外会主动叠加当前登录人的分工会数据范围
@@ -169,6 +195,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
cnd.andEX("t2.selectOptionId", "=", pageForm.getWelfareOptionId());
cnd.andEX("t4.aidFundMemberUserType", "=", pageForm.getAidFundMemberUserType());
cnd.andEX("t4.userAttribute", "=", pageForm.getUserAttribute());
cnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
if (StrUtil.isNotBlank(pageForm.getUserName())) {
cnd.where().andLike("t4.username", pageForm.getUserName());
}
@@ -188,12 +216,16 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
Sql sql = Sqls.create("""
SELECT
t1.id,
t1.userId,
t1.welfareUnionName,
t1.welfareUnitName,
GROUP_CONCAT(DISTINCT t3.optionName,'',t2.selectNum,'') AS selectedOptions,
GROUP_CONCAT(DISTINCT t2.mobile) AS mobile,
GROUP_CONCAT(DISTINCT t2.userName) AS userName2,
GROUP_CONCAT(DISTINCT t2.receiveAddress) AS receiveAddress,
GROUP_CONCAT(DISTINCT t2.courierNumber) AS legacyCourierNumber,
MAX(t2.userSign) AS userSign,
MAX(t2.remark) AS remark,
t4.username AS userName,
t4.loginname AS loginName,
t4.sex
@@ -221,6 +253,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
cnd.andEX("t1.welfareUnitId", "in", pageForm.getUnitIds());
cnd.andEX("t1.welfareUnionId", "=", pageForm.getUnionId());
cnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
if (pageForm.getIsSelect() != null) {
if (pageForm.getIsSelect()) {
@@ -240,6 +274,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
cnd.groupBy("t1.id");
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
fillExportCourierNumbers(list, pageForm);
list.forEach(this::fillExportUserSign);
// 项目
WelfareProject project = dao().fetch(WelfareProject.class, pageForm.getProjectId());
@@ -259,6 +295,14 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
if(project.getProvideMode() == 3){
entities.add(new ExcelExportEntity("收货地址", "receiveAddress", 40));
}
entities.add(new ExcelExportEntity("备注", "remark", 30));
ExcelExportEntity courierNumberEntity = new ExcelExportEntity("快递单号", "courierNumber", 30);
courierNumberEntity.setWrap(true);
entities.add(courierNumberEntity);
ExcelExportEntity userSignEntity = new ExcelExportEntity("签字信息", "userSignBytes", 20);
userSignEntity.setType(2);
userSignEntity.setExportImageType(2);
entities.add(userSignEntity);
// 设置导出参数
ExportParams exportParams = new ExportParams();
@@ -271,6 +315,73 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
}
/**
* 批量补充选择情况导出的快递单号
* 当前数据从快递单号表读取历史数据仍兼容选择记录中的旧快递单号字段
*
* @param rows 选择情况导出行每行必须包含userId处理后增加courierNumber文本字段
* @param pageForm 页面查询条件welfareOptionId不为空时只导出所选套餐对应的快递单号
*/
private void fillExportCourierNumbers(List<NutMap> rows, WelfareSelectionSituationPageForm pageForm) {
List<String> userIds = rows.stream()
.map(row -> row.getString("userId"))
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
if (userIds.isEmpty()) {
return;
}
Cnd courierCondition = Cnd.where(WelfareCourierNumber::getWelfareId, "=", pageForm.getProjectId())
.and(WelfareCourierNumber::getSelectUserId, "in", userIds)
.andEX(WelfareCourierNumber::getSelectOptionId, "=", pageForm.getWelfareOptionId());
courierCondition.asc(WelfareCourierNumber::getCreatedAt);
Map<String, List<WelfareCourierNumber>> courierNumbersByUser = dao()
.query(WelfareCourierNumber.class, courierCondition)
.stream()
.collect(Collectors.groupingBy(WelfareCourierNumber::getSelectUserId));
for (NutMap row : rows) {
LinkedHashSet<String> courierNumbers = courierNumbersByUser
.getOrDefault(row.getString("userId"), Collections.emptyList())
.stream()
.map(WelfareCourierNumber::getCourierNumber)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
String legacyCourierNumber = row.getString("legacyCourierNumber");
if (StrUtil.isNotBlank(legacyCourierNumber)) {
Arrays.stream(legacyCourierNumber.split(","))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.forEach(courierNumbers::add);
}
row.put("courierNumber", String.join("", courierNumbers));
}
}
/**
* 将签字文件路径转换为选择情况Excel可识别的图片字节
* 文件记录缺失或文件读取失败时保留空白签字列避免单个历史文件影响整份导出
*
* @param row 选择情况导出行输入userSign文件路径输出userSignBytes图片字节
*/
private void fillExportUserSign(NutMap row) {
String userSignPath = row.getString("userSign");
if (StrUtil.isBlank(userSignPath)) {
return;
}
try {
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", userSignPath));
if (file == null) {
log.warn("选择情况导出未找到签字文件记录,userId={}, userSign={}", row.getString("userId"), userSignPath);
return;
}
row.put("userSignBytes", SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath()));
} catch (Exception e) {
log.warn("选择情况导出签字文件读取失败,userId={}", row.getString("userId"), e);
}
}
@Override
public void receiveXlsx(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response) {
@@ -278,6 +389,7 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
SELECT
t1.*,
t2.userSign,
MAX(t2.remark) AS remark,
t3.username userName,
t3.loginname loginName,
GROUP_CONCAT(DISTINCT t2.selectOptionId) AS selectOptionIds
@@ -308,26 +420,29 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
}
}
if (map.get("userSign") != null){
if (StrUtil.isNotBlank(map.getString("userSign"))){
String userSignPath = map.getString("userSign");
try {
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", map.getString("userSign")));
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", userSignPath));
if (file == null) {
// 签字路径没有对应文件记录时保留人员数据仅将签字单元格导出为空
map.put("userSign", null);
log.warn("领取表导出未找到签字文件记录,userId={}, userSign={}", map.getString("userId"), userSignPath);
continue;
}
byte[] userSignBytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
map.put("userSign", userSignBytes);
}catch (Exception e){
e.printStackTrace();
log.error("下载图片失败", e);
// 签字文件读取异常不能阻断整张领取表导出当前人员签字留空
map.put("userSign", null);
log.warn("领取表导出签字文件读取失败,userId={}", map.getString("userId"), e);
}
}
}
List<Map<String, Object>> safeList = list.stream().map(nutMap -> {
Map<String, Object> map = new HashMap<>(nutMap);
return map;
}).toList();
// 分组
Map<String, List<Map<String, Object>>> listMap = safeList.stream()
.collect(Collectors.groupingBy(n -> (String) n.get("welfareUnionName")));
// 保留 NutMap 数据结构避免 EasyPOI Java 17 下反射普通 HashMap 导致工作表创建失败
Map<String, List<NutMap>> listMap = list.stream()
.collect(Collectors.groupingBy(n -> n.getString("welfareUnionName")));
// 构建 Excel
List<ExcelExportEntity> entities = new ArrayList<>();
@@ -336,22 +451,23 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
optionList.forEach(option -> {
entities.add(new ExcelExportEntity(option.getOptionName(), option.getOptionName(), 20));
});
entities.add(new ExcelExportEntity("备注", "remark", 30));
ExcelExportEntity userSignEntity = new ExcelExportEntity("签字", "userSign", 20);
userSignEntity.setType(2);
userSignEntity.setExportImageType(2);
entities.add(userSignEntity);
// 导出
Workbook workbook = new HSSFWorkbook();
Workbook workbook = new XSSFWorkbook();
listMap.forEach((k, v) -> {
ExcelExportService service = new ExcelExportService();
ExportParams exportParams = new ExportParams();
exportParams.setSheetName(k);
exportParams.setType(ExcelType.HSSF);
exportParams.setType(ExcelType.XSSF);
service.createSheetForMap(workbook, exportParams, entities, v);
});
CommonDownloadUtil.download("领取表.xls", workbook, response);
CommonDownloadUtil.download("领取表.xlsx", workbook, response);
}
/**
@@ -4,11 +4,14 @@ 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.afterturn.easypoi.excel.export.ExcelExportService;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.welfare.mode.WelfareExportEntityTc;
@@ -31,9 +34,7 @@ import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -159,9 +160,10 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
u.unitname AS unitName,
u.unionname AS unionName,
u.mobile,
IF(LOCATE('undefined',wpus.receiveAddress)>0,NULL,wpus.receiveAddress) as receiveAddress,
GROUP_CONCAT(DISTINCT wpso.optionName ,'',wpus.selectNum,'') optionName,
wpus.courierNumber
IF(LOCATE('undefined',wpus.receiveAddress)>0,NULL,wpus.receiveAddress) as receiveAddress,
GROUP_CONCAT(DISTINCT wpso.optionName ,'',wpus.selectNum,'') optionName,
wpus.courierNumber,
MAX(wpus.userSign) userSign
FROM
welfare_project_user_selection wpus
LEFT JOIN `vw_user` u ON u.id = wpus.selectUserId
@@ -183,6 +185,8 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
@Override
public Workbook exportReceiveDetail(String projectId, String unionId) {
// V4 原横向套餐签领表逻辑保留当前按 V3 每个套餐一个工作表格式执行
if (false) {
Sql optionSql = Sqls.create("""
SELECT w2.id, w2.optionName
@@ -282,9 +286,148 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, dataList);
return workbook;
}
return exportReceiveDetailV3(projectId, unionId);
}
/**
* 生成参考项目格式的分工会福利签领表一个工作表展示人员套餐份数和签字并在末行汇总套餐份数
*
* @param projectId 福利项目ID
* @param unionId 分工会ID
* @return XLSX工作簿
*/
private Workbook exportReceiveDetailV3(String projectId, String unionId) {
Sql optionSql = Sqls.create("""
SELECT id, optionName
FROM welfare_project_subject_option
WHERE welfareId = @projectId
ORDER BY optionSort, id
""");
optionSql.setParam("projectId", projectId);
List<NutMap> options = listMap(optionSql);
Sql sql = Sqls.create("""
SELECT
wl.userId,
u.username AS userName,
u.loginname AS loginName,
wl.welfareUnitName AS unitName,
wpus.userSign,
wpus.selectOptionId,
wpus.selectNum
FROM welfare_list wl
LEFT JOIN sys_user u ON u.id = wl.userId
LEFT JOIN welfare_project_user_selection wpus ON wpus.selectUserId = wl.userId
AND wpus.welfareId = wl.projectId
WHERE wl.projectId = @projectId AND wl.welfareUnionId = @unionId
ORDER BY wl.welfareUnitName, u.loginname, wpus.selectOptionId
""");
sql.setParam("projectId", projectId);
sql.setParam("unionId", unionId);
List<NutMap> rows = listMap(sql);
rows.forEach(this::fillUserSign);
Map<String, List<NutMap>> userRows = rows.stream().collect(Collectors.groupingBy(
row -> row.getString("userId"), LinkedHashMap::new, Collectors.toList()));
List<NutMap> exportRows = new ArrayList<>();
int index = 1;
for (List<NutMap> selectedRows : userRows.values()) {
NutMap userRow = selectedRows.get(0);
NutMap exportRow = NutMap.NEW();
exportRow.put("no", index++);
exportRow.put("userName", userRow.getString("userName"));
exportRow.put("loginName", userRow.getString("loginName"));
exportRow.put("unitName", userRow.getString("unitName"));
exportRow.put("qzBytes", selectedRows.stream()
.map(row -> row.get("qzBytes"))
.filter(Objects::nonNull)
.findFirst()
.orElse(null));
for (NutMap option : options) {
int selectNum = selectedRows.stream()
.filter(row -> option.getString("id").equals(row.getString("selectOptionId")))
.mapToInt(row -> row.getInt("selectNum", 0))
.sum();
exportRow.put(option.getString("id"), selectNum);
}
exportRows.add(exportRow);
}
NutMap totalRow = NutMap.NEW();
totalRow.put("no", "合计");
for (NutMap option : options) {
int total = rows.stream()
.filter(row -> option.getString("id").equals(row.getString("selectOptionId")))
.mapToInt(row -> row.getInt("selectNum", 0))
.sum();
totalRow.put(option.getString("id"), total);
}
exportRows.add(totalRow);
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("序号", "no", 10));
entities.add(new ExcelExportEntity("姓名", "userName", 15));
entities.add(new ExcelExportEntity("工号", "loginName", 18));
ExcelExportEntity unitEntity = new ExcelExportEntity("所在单位", "unitName", 25);
unitEntity.setWrap(true);
entities.add(unitEntity);
for (NutMap option : options) {
entities.add(new ExcelExportEntity(option.getString("optionName"), option.getString("id"), 12));
}
ExcelExportEntity signEntity = new ExcelExportEntity("签字", "qzBytes", 20);
signEntity.setType(2);
signEntity.setExportImageType(2);
entities.add(signEntity);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
return ExcelExportUtil.exportExcel(exportParams, entities, exportRows);
}
/** 从 V4 文件存储读取签名图片,历史文件缺失时保留空白签字格。 */
private void fillUserSign(NutMap row) {
String userSign = row.getString("userSign");
if (StrUtil.isBlank(userSign)) {
return;
}
try {
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", userSign));
if (file != null) {
row.put("qzBytes", SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath()));
}
} catch (Exception ignored) {
// 单个签名文件异常不能影响整份签领表导出
}
}
/** 身份证仅显示前段信息,保持 V3 签领表的脱敏规则。 */
private String maskIdCard(String idCard) {
if (StrUtil.isBlank(idCard) || idCard.length() <= 4) {
return idCard;
}
return idCard.substring(0, idCard.length() - 4) + "****";
}
/** 将历史收货信息拆为 V3 名单所需的收货人、联系方式和收货地址字段。 */
private void fillV3ReceiveAddress(NutMap row) {
String receiveAddress = row.getString("receiveAddress");
if (StrUtil.isBlank(receiveAddress)) {
return;
}
Map<String, String> values = new HashMap<>();
for (String pair : receiveAddress.split("[,]")) {
String[] keyValue = pair.split("[:]", 2);
if (keyValue.length == 2) {
values.put(keyValue[0].trim(), keyValue[1].trim());
}
}
row.put("recipient", values.get("收件人"));
row.put("phone", values.get("联系方式"));
row.put("address", values.get("收货地址"));
}
@Override
public Object unclaimedData(String projectId, String unionId, Integer pageNumber, Integer pageSize) {
@@ -333,6 +476,7 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
}
List<NutMap> list2 = list.stream().filter(v -> Strings.isNotBlank(v.getString("unionName"))).collect(Collectors.toList());
list2.forEach(this::fillV3ReceiveAddress);
List<WelfareExportEntityTc> welfareExportEntityTcList = list2.stream()
.map(map -> JSONUtil.toBean(Json.toJson(map), WelfareExportEntityTc.class))
@@ -340,15 +484,15 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
welfareExportEntityTcList.forEach(v -> {
if (StrUtil.isNotBlank(v.getUserSign())) {
ByteArrayOutputStream signOs = new ByteArrayOutputStream();
// ftpService.download(v.getUserSign(), signOs);
byte[] imageBytes = signOs.toByteArray();
if (Lang.isNotEmpty(imageBytes)) {
v.setQzBytes(imageBytes);
} else {
v.setQzBytes(null);
try {
Sys_file file = dao().fetch(Sys_file.class,
Cnd.where(Sys_file::getDownloadPath, "=", v.getUserSign()));
if (file != null) {
v.setQzBytes(SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath()));
}
} catch (Exception ignored) {
// 签名文件缺失时保留空白不中断名单导出
}
}
});
ExportParams exportParams = new ExportParams();
@@ -17,6 +17,7 @@ import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareProjectSubjectOption;
import com.budwk.app.zhgh.welfare.service.WelfareSingleService;
import com.budwk.app.zhgh.welfare.service.WelfareStatisticsService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
@@ -32,14 +33,15 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
@Slf4j
public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject> implements WelfareStatisticsService {
@org.nutz.ioc.loader.annotation.Inject
private WelfareSingleService welfareSingleService;
public WelfareStatisticsServiceImpl(Dao dao) {
super(dao);
}
@@ -65,7 +67,7 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
}};
// 选项数据
List<WelfareProjectSubjectOption> welfareOptions = dao().query(WelfareProjectSubjectOption.class, Cnd.where(WelfareProjectSubjectOption::getWelfareId, "=", projectId));
List<WelfareProjectSubjectOption> welfareOptions = getNaturalSortedOptions(projectId);
for (WelfareProjectSubjectOption welfareOption : welfareOptions) {
dynamicTableColumns.add(NutMap.NEW().addv("label", welfareOption.getOptionName()).addv("prop", welfareOption.getId()));
@@ -176,6 +178,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike("u.loginname", pageForm.getSearchKeyword());
seg.orLike("u.username", pageForm.getSearchKeyword());
// 已选人员弹窗仅支持按工号或姓名筛选需将 OR 条件加入查询条件后才会生效
cnd.and(seg);
}
sql.setCondition(cnd);
Pagination pagination = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -184,6 +188,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
@Override
public void exportSelectedUnionUser(String projectId, String unionId, HttpServletResponse response) {
// V4 原已选人员导出逻辑保留当前按 V3 字段与格式执行
if (false) {
Sql sql = Sqls.create("""
SELECT
u.loginname AS loginName,
@@ -245,6 +251,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
} catch (Exception e) {
log.error("导出Excel失败", e);
}
}
welfareSingleService.exportReceiveDetailByUnionId(projectId, unionId, true, response);
}
@Override
@@ -289,6 +297,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
@Override
public void exportUnSelectedUnionUser(String projectId, String unionId, HttpServletResponse response) {
// V4 原未选人员导出逻辑保留当前按 V3 字段与格式执行
if (false) {
Sql sql = Sqls.create("""
SELECT
u.loginname AS loginName,
@@ -344,6 +354,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
} catch (Exception e) {
log.error("导出Excel失败", e);
}
}
welfareSingleService.exportReceiveDetailByUnionId(projectId, unionId, false, response);
}
@@ -377,7 +389,9 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
@Override
public void exportByWelfareOptions(String projectId, HttpServletResponse response) {
public void exportByWelfareOptions(String projectId, String unionId, HttpServletResponse response) {
// V4 原按福利选项导出逻辑保留当前按 V3 的品牌分表字段执行
if (false) {
Workbook workbook = new XSSFWorkbook();
List<ExcelExportEntity> exportEntities = new ArrayList<>();
@@ -434,6 +448,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
} catch (Exception e) {
log.error("导出Excel失败", e);
}
}
exportByWelfareOptionsV3(projectId, unionId, response);
}
@@ -456,7 +472,7 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
entities.add(unSelectedNumEntity);
// 选项数据
List<WelfareProjectSubjectOption> welfareOptions = dao().query(WelfareProjectSubjectOption.class, Cnd.where(WelfareProjectSubjectOption::getWelfareId, "=", projectId));
List<WelfareProjectSubjectOption> welfareOptions = getNaturalSortedOptions(projectId);
for (WelfareProjectSubjectOption welfareOption : welfareOptions) {
ExcelExportEntity entity = new ExcelExportEntity(welfareOption.getOptionName(), welfareOption.getId(), 20);
entity.setType(10);
@@ -543,4 +559,150 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
}
}
/**
* V3 品牌导出结构生成工作簿每个福利选项独立工作表人员多选时在每个对应选项表中均保留一行
*
* @param projectId 福利项目ID
* @param unionId 当前筛选的分工会ID为空时导出当前登录人权限范围内的全部分工会
* @param response HTTP响应返回XLSX格式的品牌分表文件
*/
private void exportByWelfareOptionsV3(String projectId, String unionId, HttpServletResponse response) {
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("工号", "loginName", 20));
entities.add(new ExcelExportEntity("姓名", "userName", 20));
entities.add(new ExcelExportEntity("电话号码", "mobile", 20));
entities.add(new ExcelExportEntity("在职状态", "userState", 20));
entities.add(new ExcelExportEntity("人员类型", "personType", 20));
entities.add(new ExcelExportEntity("所在分工会", "welfareUnionName", 30));
entities.add(new ExcelExportEntity("所在单位", "welfareUnitName", 50));
entities.add(new ExcelExportEntity("选择份数", "selectNum", 10));
entities.add(new ExcelExportEntity("收货人", "recipient", 15));
entities.add(new ExcelExportEntity("联系方式", "phone", 20));
entities.add(new ExcelExportEntity("收货地址", "address", 60));
Sql sql = Sqls.create("""
SELECT
u.loginname AS loginName,
u.username AS userName,
wpus.mobile,
wl.userState,
wl.personType,
wl.welfareUnionName,
wl.welfareUnitName,
wpus.receiveAddress,
wpus.selectNum,
wpus.selectOptionId
FROM welfare_project_user_selection wpus
LEFT JOIN welfare_list wl ON wl.userId = wpus.selectUserId AND wl.projectId = wpus.welfareId
LEFT JOIN sys_user u ON u.id = wpus.selectUserId
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("wpus.welfareId", "=", projectId);
cnd.and("wpus.selectNum", "!=", 0);
cnd.andEX("wl.welfareUnionId", "=", resolveExportUnionId(unionId));
cnd.asc("wl.welfareUnionName").asc("wl.welfareUnitName").asc("u.loginname");
sql.setCondition(cnd);
List<NutMap> rows = listMap(sql);
rows.forEach(this::fillV3ReceiveAddress);
try (Workbook workbook = new XSSFWorkbook()) {
for (WelfareProjectSubjectOption option : getNaturalSortedOptions(projectId)) {
List<NutMap> optionRows = rows.stream()
.filter(row -> option.getId().equals(row.getString("selectOptionId")))
// EasyPOI 生成工作表时会移除已处理行必须传入可修改列表
.collect(Collectors.toCollection(ArrayList::new));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle(option.getOptionName() + "选择情况");
exportParams.setSheetName(option.getOptionName());
new ExcelExportService().createSheetForMap(workbook, exportParams, entities, optionRows);
}
CommonDownloadUtil.download("按品牌导出选择情况表.xlsx", workbook, response);
} catch (Exception e) {
log.error("按品牌导出Excel失败", e);
}
}
/** 分工会角色只能导出本人工会数据,校级角色可按页面筛选导出。 */
private String resolveExportUnionId(String unionId) {
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name())) {
return SecurityUtil.getUnionId();
}
return unionId;
}
/** 将历史收货信息拆为 V3 导出所需的收货人、联系方式和收货地址三列。 */
private void fillV3ReceiveAddress(NutMap row) {
String receiveAddress = row.getString("receiveAddress");
if (StrUtil.isBlank(receiveAddress)) {
return;
}
Map<String, String> values = new HashMap<>();
for (String pair : receiveAddress.split("[,]")) {
String[] keyValue = pair.split("[:]", 2);
if (keyValue.length == 2) {
values.put(keyValue[0].trim(), keyValue[1].trim());
}
}
row.put("recipient", values.get("收件人"));
row.put("phone", values.get("联系方式"));
row.put("address", values.get("收货地址"));
}
/**
* 按套餐名称进行自然排序数字片段按数值比较英文字母忽略大小写比较避免套餐10排在套餐2之前
*/
private List<WelfareProjectSubjectOption> getNaturalSortedOptions(String projectId) {
List<WelfareProjectSubjectOption> options = dao().query(WelfareProjectSubjectOption.class,
Cnd.where(WelfareProjectSubjectOption::getWelfareId, "=", projectId));
options.sort(Comparator.comparing(WelfareProjectSubjectOption::getOptionName, this::compareOptionNames)
.thenComparing(option -> option.getOptionSort() == null ? Integer.MAX_VALUE : option.getOptionSort())
.thenComparing(WelfareProjectSubjectOption::getId));
return options;
}
private int compareOptionNames(String left, String right) {
String leftName = StrUtil.blankToDefault(left, "");
String rightName = StrUtil.blankToDefault(right, "");
int leftIndex = 0;
int rightIndex = 0;
while (leftIndex < leftName.length() && rightIndex < rightName.length()) {
char leftChar = leftName.charAt(leftIndex);
char rightChar = rightName.charAt(rightIndex);
if (Character.isDigit(leftChar) && Character.isDigit(rightChar)) {
int leftEnd = leftIndex;
int rightEnd = rightIndex;
while (leftEnd < leftName.length() && Character.isDigit(leftName.charAt(leftEnd))) {
leftEnd++;
}
while (rightEnd < rightName.length() && Character.isDigit(rightName.charAt(rightEnd))) {
rightEnd++;
}
String leftNumber = leftName.substring(leftIndex, leftEnd).replaceFirst("^0+(?!$)", "");
String rightNumber = rightName.substring(rightIndex, rightEnd).replaceFirst("^0+(?!$)", "");
int numberCompare = Integer.compare(leftNumber.length(), rightNumber.length());
if (numberCompare != 0) {
return numberCompare;
}
numberCompare = leftNumber.compareTo(rightNumber);
if (numberCompare != 0) {
return numberCompare;
}
leftIndex = leftEnd;
rightIndex = rightEnd;
continue;
}
int charCompare = String.valueOf(leftChar).compareToIgnoreCase(String.valueOf(rightChar));
if (charCompare != 0) {
return charCompare;
}
leftIndex++;
rightIndex++;
}
return Integer.compare(leftName.length(), rightName.length());
}
}
@@ -4,7 +4,6 @@ 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.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
@@ -59,8 +58,8 @@ public class WelfareUserEvaluationServiceImpl extends BaseServiceImpl<WelfareUse
}
/**
* 评价开放时间以后端福利选择记录为准避免客户端隐藏按钮后仍可直接调用评价接口
* 配送时间按日期粒度比较到达配送日期当天即可评价
* 评价开放时间以后端福利项目活动结束时间为准避免客户端隐藏按钮后仍可直接调用评价接口
* 当前用户必须已经存在福利选择记录且当前时间达到 choiceTimeEnd 才允许评价
*
* @param projectId 福利项目ID
* @param userId 当前登录用户ID
@@ -77,14 +76,15 @@ public class WelfareUserEvaluationServiceImpl extends BaseServiceImpl<WelfareUse
if (selections.isEmpty()) {
return "当前项目暂无可评价的福利选择";
}
Date currentDate = DateUtil.beginOfDay(new Date());
for (WelfareUserSelection selection : selections) {
if (selection.getDeliveryDate() == null) {
return "当前福利尚未设置配送时间,暂不能评价";
}
if (currentDate.before(DateUtil.beginOfDay(selection.getDeliveryDate()))) {
return "配送时间未到,暂不能评价";
}
WelfareProject project = dao().fetch(WelfareProject.class, projectId);
if (project == null) {
return "福利项目不存在";
}
if (project.getChoiceTimeEnd() == null) {
return "当前福利未设置活动结束时间,暂不能评价";
}
if (new Date().before(project.getChoiceTimeEnd())) {
return "福利活动尚未结束,暂不能评价";
}
return null;
}
@@ -0,0 +1,123 @@
-- 补充旧版疗休养已完成移植、但未写入新版菜单表的两个 PC 端入口。
-- 使用 permission 做幂等判断,脚本可重复执行。
INSERT INTO sys_menu (
id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt,
delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService
)
SELECT
'8a691f4623db4d82927668b05d10b401',
parent_menu.id,
CONCAT(
parent_menu.path,
LPAD(
IFNULL((
SELECT MAX(CAST(RIGHT(child.path, 4) AS UNSIGNED))
FROM sys_menu child
WHERE child.parentId = parent_menu.id
), 0) + 1,
4,
'0'
)
),
'线路成团结果通知',
'Line Group Result Notice',
'menu',
'/platform/recuperation/lineStatistics',
'data-pjax',
'',
1,
0,
'recuperation.lineStatistics',
'查询线路报名统计并发送成团或未成团通知',
IFNULL((
SELECT MAX(child.location)
FROM sys_menu child
WHERE child.parentId = parent_menu.id
), parent_menu.location) + 1,
0,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
0,
'PC',
parent_menu.moduleId,
NULL,
'x',
0,
0
FROM sys_menu parent_menu
WHERE parent_menu.permission = 'recuperation'
AND NOT EXISTS (
SELECT 1
FROM (SELECT id FROM sys_menu WHERE permission = 'recuperation.lineStatistics') existing_menu
);
INSERT INTO sys_menu (
id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt,
delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService
)
SELECT
'3074a944a1554982835793d1c164183f',
parent_menu.id,
CONCAT(
parent_menu.path,
LPAD(
IFNULL((
SELECT MAX(CAST(RIGHT(child.path, 4) AS UNSIGNED))
FROM sys_menu child
WHERE child.parentId = parent_menu.id
), 0) + 1,
4,
'0'
)
),
'省内灵活组团查询',
'Flexible Group Query',
'menu',
'/platform/recuperation/flexibleGroupQuery',
'data-pjax',
'',
1,
0,
'recuperation.flexibleGroupQuery',
'查询省内灵活组团、团队及报名人员',
IFNULL((
SELECT MAX(child.location)
FROM sys_menu child
WHERE child.parentId = parent_menu.id
), parent_menu.location) + 1,
0,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
0,
'PC',
parent_menu.moduleId,
NULL,
's',
0,
0
FROM sys_menu parent_menu
WHERE parent_menu.permission = 'recuperation'
AND NOT EXISTS (
SELECT 1
FROM (SELECT id FROM sys_menu WHERE permission = 'recuperation.flexibleGroupQuery') existing_menu
);
-- 系统管理员默认拥有补充菜单权限,其他业务角色可在角色管理中按需分配。
INSERT INTO sys_role_menu (roleId, menuId)
SELECT role_info.id, menu_info.id
FROM sys_role role_info
JOIN sys_menu menu_info ON menu_info.permission IN (
'recuperation.lineStatistics',
'recuperation.flexibleGroupQuery'
)
LEFT JOIN sys_role_menu role_menu
ON role_menu.roleId = role_info.id
AND role_menu.menuId = menu_info.id
WHERE role_info.code = 'SYSADMIN'
AND role_menu.roleId IS NULL;
@@ -21,8 +21,10 @@
<button v-if="signatureContent" @click="openSignature" class="signature-reset-button"
type="button">重新签名</button>
</div>
<van-popup v-model="showSignaturePanel" :close-on-click-overlay="false"
:style="{ height: '100vh', width: '100vw' }" @close="handleSignaturePopupClose">
<!-- 签字板挂载到 body避免 iOS fixed 子元素裁切在外层带 transform 的滚动 Popup -->
<van-popup v-model="showSignaturePanel" class="signature-full-popup" get-container="body"
:close-on-click-overlay="false" :style="{ height: '100%', width: '100%' }"
@close="handleSignaturePopupClose">
<signature @save="save"></signature>
</van-popup>
</div>
@@ -1,6 +1,6 @@
<template>
<div class="signature-wrap">
<canvas id="canvas"></canvas>
<canvas ref="signatureCanvas"></canvas>
<div :class="isLikelyMobile() ? 'action-buttons' : 'action-buttons pc-action-buttons'">
<button @click="clear" class="action-button-danger" type="button">清空</button>
<button @click="undo" class="action-button-warning" type="button">撤销</button>
@@ -80,15 +80,13 @@ module.exports = {
}
},
mounted() {
const innerWidth = window.innerWidth
const innerHeight = window.innerHeight
const canvas = document.getElementById("canvas")
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// 使 iOS WebView 100vh/innerHeight
const containerWidth = this.$el.clientWidth || window.innerWidth
const containerHeight = this.$el.clientHeight || window.innerHeight
const canvas = this.$refs.signatureCanvas
signature = new SmoothSignature(canvas, {
width: innerWidth - 30,
height: innerHeight - 30,
width: Math.max(containerWidth - 30, 1),
height: Math.max(containerHeight - 30, 1),
scale: 2,
minWidth: 4,
maxWidth: 10,
@@ -101,11 +99,14 @@ module.exports = {
<style scoped>
.signature-wrap {
position: fixed;
left: 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 15px;
overflow: hidden;
box-sizing: border-box;
background: #ffffff;
}
@@ -121,8 +122,9 @@ module.exports = {
transform: rotate(90deg);
display: flex;
column-gap: 10px;
position: fixed;
bottom: 0px;
position: absolute;
z-index: 2;
bottom: calc(15px + env(safe-area-inset-bottom));
left: 60px;
transform: rotate(90deg);
display: flex;
@@ -621,7 +621,7 @@
</script>
<script nonce="${cspNonce!}">
const h5ComponentVersion = "20260821_2"
const h5ComponentVersion = "20260907_1"
Vue.component("rich-text", httpVueLoader("/components/plugins/sysRichTextView/index.vue?v=" + h5ComponentVersion))
Vue.component("h5-file-upload", httpVueLoader("/components/plugins/sysUpload/h5Index.vue?v=" + h5ComponentVersion))
Vue.component("h5-signature", httpVueLoader("/components/plugins/sysSignature/h5Index.vue?v=" + h5ComponentVersion))
@@ -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>
<!--# } #-->
<!--#
}
#-->
@@ -319,6 +319,18 @@ layout("/layouts/platform.html"){
</el-form-item>
</template>
<el-form-item label="福利通知附件" prop="noticeAttachment">
<file-upload
:value.sync="formData.noticeAttachment"
:upload_number="1"
accept=".doc,.docx"
upload_mode="file"
upload_text="上传福利通知附件"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
</el-form-item>
<el-form-item label="封面图片" prop="cover">
<file-upload
:value.sync="formData.cover"
@@ -373,7 +385,6 @@ layout("/layouts/platform.html"){
formRules: {
name: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
festival: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
provideTime: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
choiceTime: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
provideAddress: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
provideMode: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
@@ -486,6 +497,7 @@ layout("/layouts/platform.html"){
noticePushMode: 1,
groupRequired: false,
groupSelectionConfigs: [],
noticeAttachment: "",
options: [],
welfareProjectSubjects: [],
flexibleGifts: [{}]
@@ -43,12 +43,10 @@ const welfareOption = {
<el-table-column align="center" header-align="center" label="系统默认选择" width="140">
<template slot-scope="{row}">
<el-radio
v-model="systemDefaultOption"
:label="row"
@change="setSystemDefault(row)">
&nbsp;
</el-radio>
<el-checkbox
:value="!!row.isSystemDefault"
@change="setSystemDefault(row, $event)">
</el-checkbox>
</template>
</el-table-column>
@@ -170,7 +168,6 @@ const welfareOption = {
data() {
return {
welfareList: [],
systemDefaultOption: null,
localGroupRequired: false,
descriptionDialog: {
visible: false,
@@ -187,10 +184,6 @@ const welfareOption = {
created() {
this.welfareList = this.value ? JSON.parse(JSON.stringify(this.value)) : [];
this.localGroupRequired = Boolean(this.groupRequired);
this.systemDefaultOption = this.welfareList.find((item) => item.isSystemDefault) || null;
if (this.systemDefaultOption) {
this.setSystemDefault(this.systemDefaultOption);
}
},
watch: {
@@ -298,22 +291,16 @@ const welfareOption = {
return true;
},
// 每个福利项目只允许配置一个系统默认选项,保存时会随福利选项一并提交
setSystemDefault(defaultOption) {
this.welfareList.forEach((item) => {
this.$set(item, 'isSystemDefault', item === defaultOption);
});
// 每个福利选项独立维护系统默认状态,支持同时勾选多个选项或取消已有勾选
setSystemDefault(option, selected) {
this.$set(option, 'isSystemDefault', selected);
},
deleteOption(index) {
this.$confirm('确认删除该福利选项?', '提示', {
type: 'warning'
}).then(() => {
const deletedOption = this.welfareList[index];
this.welfareList.splice(index, 1);
if (this.systemDefaultOption === deletedOption) {
this.systemDefaultOption = null;
}
this.updateSortNumbers();
});
},
@@ -442,7 +429,7 @@ const welfareOption = {
transform: translate(-50%, -50%);
}
.welfare-option .el-radio__label {
.welfare-option .el-checkbox__label {
padding-left: 0;
}
@@ -135,11 +135,6 @@ layout("/layouts/platform.html"){
<span v-else>暂无</span>
</template>
<template scope="{row}" v-else-if="column.prop=='deliveryDate'">
<span v-if="row.deliveryDate">{{$moment(row.deliveryDate).format('YYYY-MM-DD')}}</span>
<span v-else>暂无</span>
</template>
<template scope="{row}" v-else-if="column.prop=='remark'">
<span v-if="row.remark">{{row.remark}}</span>
<span v-else>暂无</span>
@@ -224,7 +219,6 @@ layout("/layouts/platform.html"){
{ prop: "choiceTime", label: "选择时间" },
{ prop: "isChoose", label: "是否选择" },
{ prop: "gist_list", label: "所选福利" },
{ prop: "deliveryDate", label: "配送时间" },
{ prop: "remark", label: "备注" }
// { prop: "receiveAddress", label: "收货地址" }
],
@@ -289,15 +283,15 @@ layout("/layouts/platform.html"){
},
/**
* 配送日期到达后才显示评价入口;没有配送日期的历史数据不允许评价
* @param row 福利项目列表行,deliveryDate 为 yyyy-MM-dd 日期
* @return {boolean} 当前日期达到配送日期时返回 true
* 用户已选择福利且活动结束后显示评价入口
* @param row 福利项目列表行,choiceTimeEnd 为活动结束时间
* @return {boolean} 当前时间达到活动结束时间时返回 true
*/
canEvaluate(row) {
if (!row.isChoose || !row.deliveryDate) {
if (!row.isChoose || !row.choiceTimeEnd) {
return false
}
return this.$moment().startOf("day").valueOf() >= this.$moment(row.deliveryDate).startOf("day").valueOf()
return this.$moment().valueOf() >= this.$moment(row.choiceTimeEnd).valueOf()
},
/**
@@ -172,19 +172,6 @@ const optionSelect = {
</el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item prop="deliveryDate" label="配送时间">
<el-date-picker
v-model="contactForm.deliveryDate"
type="date"
value-format="yyyy-MM-dd"
format="yyyy-MM-dd"
placeholder="请选择配送时间"
:picker-options="deliveryDatePickerOptions"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item prop="remark" label="备注">
<el-input
@@ -285,7 +272,6 @@ const optionSelect = {
receiveAddress: "",
userName: "", // 收货人
userSign: "",
deliveryDate: "", // 配送日期,按 yyyy-MM-dd 提交
remark: "" // 用户自选和管理员代选共用的选择备注
},
contactRules: {
@@ -299,15 +285,8 @@ const optionSelect = {
message: "请输入正确的手机号码",
trigger: "blur"
}
],
deliveryDate: [
{required: true, message: "请选择配送时间", trigger: "change"}
]
},
deliveryDatePickerOptions: {
// 配送日期按天校验,选择当天允许提交。
disabledDate: (time) => time.getTime() < new Date().setHours(0, 0, 0, 0)
},
addressOptions: []
}
},
@@ -447,7 +426,6 @@ const optionSelect = {
receiveAddress: "",
userName: "",
userSign: "",
deliveryDate: "",
remark: ""
}
@@ -504,8 +482,6 @@ const optionSelect = {
// 本人选择按字段回退到登录人信息;代选只使用目标人员已有选择数据。
this.$set(this.contactForm, "mobile", firstSelection.mobile || (!this.isProxySelect ? currentUser.mobile || "" : ""))
this.$set(this.contactForm, "userName", firstSelection.userName || (!this.isProxySelect ? currentUser.username || "" : ""))
this.$set(this.contactForm, "deliveryDate", this.userSelection[0].deliveryDate
? this.$moment(this.userSelection[0].deliveryDate).format("YYYY-MM-DD") : "")
this.$set(this.contactForm, "remark", this.userSelection[0].remark || "")
} else {
if (this.isProxySelect) {
@@ -640,7 +616,6 @@ const optionSelect = {
userName: this.showReceivingContact ? this.contactForm.userName : "",
userSign: this.contactForm.userSign,
receiveAddress: this.showReceivingContact ? this.contactForm.receiveAddress : "",
deliveryDate: this.contactForm.deliveryDate,
remark: this.contactForm.remark
}
]
@@ -655,7 +630,6 @@ const optionSelect = {
userName: this.showReceivingContact ? this.contactForm.userName : "",
userSign: this.contactForm.userSign,
receiveAddress: this.showReceivingContact ? this.contactForm.receiveAddress : "",
deliveryDate: this.contactForm.deliveryDate,
remark: this.contactForm.remark
}))
}
@@ -29,8 +29,8 @@ layout("/layouts/platform.html"){
</el-select>
</search-item>
<search-item label="福利选项">
<el-select clearable filterable placeholder="请选择福利选项" style="width: 100%" v-model="pageForm.welfareOptionId">
<search-item label="所选套餐">
<el-select clearable filterable placeholder="请选择所选套餐" style="width: 100%" v-model="pageForm.welfareOptionId">
<el-option :key="item.id" :label="item.optionName" :value="item.id" v-for="item in welfareOptions"></el-option>
</el-select>
</search-item>
@@ -66,10 +66,33 @@ layout("/layouts/platform.html"){
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in unitOptions"></el-option>
</el-select>
</search-item>
<search-item label="人员类">
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>
<search-item label="人员类">
<dict-select
clearable
code="USER_PERSON_TYPE"
multiple
collapse-tags
placeholder="请选择人员类型"
v-model="pageForm.personTypes"
></dict-select>
</search-item>
<search-item label="在职状态">
<dict-select
clearable
code="USER_STATE"
multiple
collapse-tags
placeholder="请选择人员状态"
v-model="pageForm.userStates"
></dict-select>
</search-item>
<!-- <search-item label="人员分类">-->
<!-- <el-select clearable filterable placeholder="请选择人员分类" style="width: 100%" v-model="pageForm.aidFundMemberUserType">-->
<!-- <el-option :key="item.code" :label="item.name" :value="item.code" v-for="item in aidFundMemberUserTypeOptions"></el-option>-->
<!-- </el-select>-->
<!-- </search-item>-->
</search>
</el-card>
@@ -130,6 +153,7 @@ layout("/layouts/platform.html"){
></el-table-column>
<el-table-column label="收货地址" prop="receiveAddress" v-if="provideMode===3"></el-table-column>
<el-table-column label="备注" prop="remark" show-overflow-tooltip></el-table-column>
<el-table-column align="center" fixed="right" header-align="center" label="操作" width="100px">
<template slot-scope="{row}">
@@ -185,10 +209,13 @@ layout("/layouts/platform.html"){
pageForm: {
searchName: "username",
year: new Date().getFullYear().toString(),
isSelect: null
isSelect: null,
personTypes: [],
userStates: []
},
unionOptions: [],
unitOptions: [],
aidFundMemberUserTypeOptions: [],
projectOptions: [],
tableColumns: [
{ prop: "loginName", label: "工号" },
@@ -198,7 +225,6 @@ layout("/layouts/platform.html"){
{ prop: "welfareUnionName", label: "所属工会", sortable: true },
{ prop: "welfareUnitName", label: "所属单位", sortable: true },
{ prop: "selectedOptions", label: "所选福利", sortable: true },
{ prop: "deliveryDate", label: "配送时间", sortable: true },
{ prop: "mobile", label: "联系电话", sortable: true }
],
optionSelectVisible: false,
@@ -213,6 +239,13 @@ layout("/layouts/platform.html"){
}
},
computed: {
noticeDetailUrl() {
if (!this.pageForm.projectId) {
return ""
}
const appDomain = typeof APP_DOMAIN !== "undefined" && APP_DOMAIN ? APP_DOMAIN : window.location.origin
return appDomain.replace(/\/+$/, "") + "/platform/welfare/notice/index?id=" + encodeURIComponent(this.pageForm.projectId)
},
welfareOptions() {
if (this.pageForm.projectId) {
return this.projectOptions.find((item) => item.id === this.pageForm.projectId)?.options
@@ -286,7 +319,7 @@ layout("/layouts/platform.html"){
this.$message.warning("请先选择福利项目")
return
}
this.$set(this.messageDialog, "messageContent", "")
this.$set(this.messageDialog, "messageContent", "查看详情:" + this.noticeDetailUrl)
this.$set(this.messageDialog, "visible", true)
this.$nextTick(() => {
if (this.$refs.messageForm) {
@@ -312,7 +345,7 @@ layout("/layouts/platform.html"){
this.$message.warning("请输入有效的消息内容")
return
}
this.messageSending = true
this.$set(this, "messageSending", true)
this.$axios.post("/platform/welfare/selection/situation/sendMessage", {
pageForm: JSON.stringify(this.pageForm),
messageContent: messageContent
@@ -324,67 +357,7 @@ layout("/layouts/platform.html"){
this.$message.error(res.msg)
}
}).finally(() => {
this.messageSending = false
})
})
},
// 按当前页面全部查询条件导出签收单模版,后台会再次叠加登录人的数据权限。
exportReceiveTemplate() {
if (!this.pageForm.projectId) {
this.$message.warning("请先选择福利项目")
return
}
this.$downLoad("/platform/welfare/selection/situation/exportReceiveTemplate", {
pageForm: JSON.stringify(this.pageForm)
})
},
// 打开发送窗口时清空上次编辑内容,实际接收人由后台按当前查询条件重新计算。
openMessageDialog() {
if (!this.pageForm.projectId) {
this.$message.warning("请先选择福利项目")
return
}
this.$set(this.messageDialog, "messageContent", "")
this.$set(this.messageDialog, "visible", true)
this.$nextTick(() => {
if (this.$refs.messageForm) {
this.$refs.messageForm.clearValidate()
}
})
},
closeMessageDialog(done) {
this.$set(this.messageDialog, "visible", false)
if (typeof done === "function") {
done()
}
},
sendMessage() {
this.$refs.messageForm.validate((valid) => {
if (!valid) {
return
}
const messageContent = (this.messageDialog.messageContent || "").trim()
if (!messageContent) {
this.$message.warning("请输入有效的消息内容")
return
}
this.messageSending = true
this.$axios.post("/platform/welfare/selection/situation/sendMessage", {
pageForm: JSON.stringify(this.pageForm),
messageContent: messageContent
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.closeMessageDialog()
} else {
this.$message.error(res.msg)
}
}).finally(() => {
this.messageSending = false
this.$set(this, "messageSending", false)
})
})
},
@@ -401,6 +374,9 @@ layout("/layouts/platform.html"){
this.getWelfareList()
this.unionOptions = await this.$businessTool.listUnion()
this.unitOptions = await this.$businessTool.listUnit()
// 人员分类下拉仅在当前页面排除离退休人员,不影响全局字典及其他页面。
this.$set(this, "aidFundMemberUserTypeOptions", (await this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE"))
.filter((item) => item.code !== "离退休人员"))
}
})
</script>
@@ -33,15 +33,21 @@ layout("/layouts/platform.html"){
<el-card class="mt10" shadow="never" v-loading="tableLoading">
<table-tool label="工会列表">
<el-button @click="exportByWelfareOptions" icon="el-icon-printer" size="small" type="primary">按福利选项导出</el-button>
<!-- V4 原导出按钮保留,不再作为当前页面入口。 -->
<!-- <el-button @click="exportByWelfareOptions" icon="el-icon-printer" size="small" type="primary">按福利选项导出</el-button> -->
<el-button :disabled="!pageForm.projectId" @click="remindUnSelectedPlaceholder" icon="el-icon-bell" size="small" type="primary">
提醒未选择人员
</el-button>
<template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN','SCHOOL_UNION_WELFARE_ADMIN'])">
<el-button :disabled="!pageForm.projectId" @click="exportSummary" icon="el-icon-printer" size="small" type="primary">
导出汇总表
<!-- V4 原汇总导出按钮保留,不再作为当前页面入口。 -->
<!-- <el-button :disabled="!pageForm.projectId" @click="exportSummary" icon="el-icon-printer" size="small" type="primary">导出汇总表</el-button> -->
<el-button :disabled="!pageForm.projectId" @click="allExportReceiveDetailByUnionIdWord" icon="el-icon-printer" size="small" type="primary">
导出各分工会福利签领表
</el-button>
</template>
<el-button :disabled="!pageForm.projectId" @click="exportByWelfareOptionsV3" icon="el-icon-printer" size="small" type="primary">按品牌导出</el-button>
<el-button :disabled="!pageForm.projectId" @click="exportReceiveDetailByUnionId(true)" icon="el-icon-printer" size="small" type="primary">导出已选名单</el-button>
<el-button :disabled="!pageForm.projectId" @click="exportReceiveDetailByUnionId(false)" icon="el-icon-printer" size="small" type="primary">导出未选名单</el-button>
</table-tool>
<el-table
:data="tableData"
@@ -74,6 +80,12 @@ layout("/layouts/platform.html"){
<el-link @click="openUnSelected(row)" type="primary">{{row.unSelectedNum}}</el-link>
</template>
</el-table-column>
<el-table-column align="center" fixed="right" header-align="center" label="操作" width="260">
<template slot-scope="{row}">
<el-button plain size="mini" type="primary" @click="exportReceiveDetailByUnionIdWord(row)">导出福利签领表</el-button>
<el-button size="mini" type="primary" @click="openSelected(row)">已选人员</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</template>
@@ -113,7 +125,7 @@ layout("/layouts/platform.html"){
remindUnSelectedPlaceholder() {
this.$message.info("提醒未选择人员功能待开发")
},
// 导出汇总表
// V4 原汇总表导出方法保留,页面已改用 V3 风格的导出入口。
exportSummary() {
this.$downLoad("/platform/welfare/statistics/exportSummary", {
projectId: this.pageForm.projectId
@@ -129,7 +141,7 @@ layout("/layouts/platform.html"){
this.$refs.unSelectedUserRef.onOpen(this.pageForm.projectId, row)
},
// 按福利选项导出
// V4 原按福利选项导出方法保留,页面已改用 V3 的按品牌导出入口。
exportByWelfareOptions() {
this.$downLoad("/platform/welfare/statistics/exportByWelfareOptions", {
projectId: this.pageForm.projectId,
@@ -137,6 +149,38 @@ layout("/layouts/platform.html"){
})
},
// V3 按品牌导出:按套餐拆分工作表,并传递当前分工会筛选范围。
exportByWelfareOptionsV3() {
this.$downLoad("/platform/welfare/statistics/exportByWelfareOptions", {
projectId: this.pageForm.projectId,
unionId: this.pageForm.unionId
})
},
// V3 已选、未选名单导出:未选择分工会时由后台导出当前数据权限内的全部人员。
exportReceiveDetailByUnionId(flag) {
this.$downLoad("/platform/welfare/statistics/exportReceiveDetailByUnionId", {
projectId: this.pageForm.projectId,
unionId: this.pageForm.unionId,
flag: flag
})
},
// V3 单个分工会福利签领表导出。
exportReceiveDetailByUnionIdWord(row) {
this.$downLoad("/platform/welfare/statistics/exportReceiveDetailByUnionIdWord", {
projectId: this.pageForm.projectId,
unionId: row.id
})
},
// V3 全部分工会福利签领表导出,后台生成 ZIP 文件。
allExportReceiveDetailByUnionIdWord() {
this.$downLoad("/platform/welfare/statistics/allExportReceiveDetailByUnionIdWord", {
projectId: this.pageForm.projectId
})
},
async pageData() {
this.tableLoading = true
const resp = await this.$axios.post("/platform/welfare/statistics/pageData", this.pageForm)
@@ -6,7 +6,7 @@ const selectedUser = {
<el-input
v-model="pageForm.searchKeyword"
placeholder="请输入姓名或工号"
style="width: 200px;"
style="width: 300px;"
size="small"
clearable
@keyup.enter.native="doSearch"
@@ -51,7 +51,7 @@ const selectedUser = {
{ label: "生日", prop: "birthday" },
{ prop: "userState", label: "在职状态", width: 120, sortable: true },
{ prop: "personType", label: "人员类型", width: 120, sortable: true },
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true },
// { prop: "postDoctoralJoinDate", label: "进站时间", sortable: true },
{ label: "单位", prop: "welfareUnitName" },
{ label: "手机号", prop: "mobile" },
{ label: "所选福利", prop: "selectOptionName" }
@@ -63,7 +63,7 @@ const addUser = {
</search-item>
<search-item label="所属单位">
<el-select @change="unitChange" clearable
<el-select clearable
filterable
placeholder="请选择所属单位" style="width: 100%"
v-model="pageForm.unitId">
@@ -76,18 +76,6 @@ const addUser = {
</el-select>
</search-item>
<search-item label="三级单位">
<el-select clearable filterable placeholder="请先选择所属单位" style="width: 100%"
v-model="pageForm.threeUnitId">
<el-option
:key="item.id"
:label="item.name"
:value="item.id"
v-for="item in threeUnitOptions">
</el-option>
</el-select>
</search-item>
<search-item label="人员类型:">
<dict-select clearable code="USER_PERSON_TYPE"
multiple
@@ -163,7 +151,6 @@ const addUser = {
birthMonths: [],
unionId: null,
unitId: null,
threeUnitId: null,
personTypes: [],
preparedBys: [],
userStates: [],
@@ -172,7 +159,6 @@ const addUser = {
sexOptions: ["男", "女"],
unionOptions: [],
unitOptions: [],
threeUnitOptions: [],
tableColumns: [
{ prop: "loginname", label: "工号" },
{ prop: "username", label: "姓名" },
@@ -181,8 +167,7 @@ const addUser = {
{ prop: "personType", label: "人员类型", sortable: true },
{ prop: "userState", label: "在职状态", sortable: true },
{ prop: "unionName", label: "所属工会", sortable: true },
{ prop: "unitName", label: "所属单位", sortable: true },
{ prop: "threeUnitName", label: "三级单位", sortable: true }
{ prop: "unitName", label: "所属单位", sortable: true }
]
}
},
@@ -204,8 +189,6 @@ const addUser = {
this.$set(this.pageForm, "birthMonths", [])
this.$set(this.pageForm, "unionId", null)
this.$set(this.pageForm, "unitId", null)
this.$set(this.pageForm, "threeUnitId", null)
this.$set(this, "threeUnitOptions", [])
this.$set(this.pageForm, "personTypes", [])
this.$set(this.pageForm, "preparedBys", [])
this.$set(this.pageForm, "userStates", [])
@@ -214,7 +197,6 @@ const addUser = {
async unionIdChange(val) {
this.$set(this.pageForm, "unitId", null)
this.unitChange(null)
if (val) {
this.$set(this, "unitOptions", await this.$businessTool.listUnit(val))
} else {
@@ -222,26 +204,6 @@ const addUser = {
}
},
// 所属单位改变后,仅加载该单位直属的三级单位,避免跨单位筛选。
unitChange(unitId) {
this.$set(this.pageForm, "threeUnitId", null)
this.$set(this, "threeUnitOptions", [])
if (!unitId) {
return
}
this.$axios.post("/platform/sys/unit/child", { pid: unitId })
.then((res) => {
if (res.code === 0) {
this.$set(this, "threeUnitOptions", res.data || [])
} else {
this.$message.error(res.msg || "三级单位查询失败")
}
})
.catch(() => {
this.$message.error("三级单位查询失败,请稍后重试")
})
},
pageData() {
this.tableLoading = true
this.$axios
@@ -25,7 +25,7 @@ layout("/layouts/platform.html"){
</search-item>
<search-item label="福利项目">
<el-select :clearable="false" filterable placeholder="请选择福利项目" style="width: 100%" v-model="pageForm.projectId">
<el-select :clearable="false" filterable placeholder="请选择福利项目" style="width: 100%" v-model="pageForm.projectId" @change="projectChange">
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in projectSelectOptions"></el-option>
</el-select>
</search-item>
@@ -133,11 +133,6 @@ layout("/layouts/platform.html"){
<!-- <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"-->
<!-- code="AIDFUND_MEMBER_USER_TYPE"></dict-select>-->
<!-- </search-item>-->
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" clearable multiple collapse-tags></dict-select>
</search-item>
<!-- <search-item label="所选福利:">-->
<!-- <el-select clearable placeholder="请选择所选福利" style="width: 100%" v-model="pageForm.optionId">-->
<!-- <el-option :key="item.id" :label="item.optionName" :value="item.id" v-for="item in pageFormProject"></el-option>-->
@@ -148,6 +143,35 @@ layout("/layouts/platform.html"){
<el-card class="mt10 welfare-list-card" shadow="never">
<table-tool :app="this" label="福利名单">
<el-select
:disabled="!isExpressWelfareProject"
clearable
filterable
placeholder="请选择套餐"
size="small"
style="width: 180px"
v-model="pageForm.optionId"
>
<el-option :key="item.id" :label="item.optionName" :value="item.id" v-for="item in pageFormProject"></el-option>
</el-select>
<el-button
:disabled="!isExpressWelfareProject || !pageForm.optionId"
@click="doExcelByOptionId"
icon="el-icon-printer"
size="small"
type="primary"
>
导出报销表
</el-button>
<el-button
:disabled="!isExpressWelfareProject"
@click="openImportExpressExcel"
icon="el-icon-upload2"
size="small"
type="primary"
>
导入快递单号
</el-button>
<el-button :disabled="!pageForm.projectId" :loading="batchDeleteLoading" @click="deleteSearchUsers" icon="el-icon-delete" size="small" type="danger">
删除人员
</el-button>
@@ -254,6 +278,8 @@ layout("/layouts/platform.html"){
width="700px"
:extra_params="{projectId:pageForm.projectId}"
></excel-import>
<import-courier-number-dialog ref="importCourierNumberRef"></import-courier-number-dialog>
</div>
<script nonce="${cspNonce!}">
@@ -270,11 +296,15 @@ layout("/layouts/platform.html"){
if (this.pageForm.projectId) {
const projectInfo = this.projectSelectOptions.find((v) => v.id === this.pageForm.projectId)
if (projectInfo) {
return projectInfo.optionList
return projectInfo.options || []
}
return null
return []
}
return null
return []
},
isExpressWelfareProject() {
const projectInfo = this.projectSelectOptions.find((v) => v.id === this.pageForm.projectId)
return projectInfo && projectInfo.provideMode === 3
},
canSyncUnion() {
const projectInfo = this.projectSelectOptions.find((v) => v.id === this.pageForm.projectId)
@@ -288,7 +318,8 @@ layout("/layouts/platform.html"){
add_welfare_list_by_select: ADD_WELFARE_LIST_BY_SELECT,
"file-import": httpVueLoader("/components/plugins/sysImport/index.vue?v=" + new Date().getTime()),
"add-user": addUser,
"update-remark": updateRemark
"update-remark": updateRemark,
"import-courier-number-dialog": IMPORT_COURIER_NUMBER_DIALOG
},
data() {
return {
@@ -326,6 +357,25 @@ layout("/layouts/platform.html"){
}
},
methods: {
// 切换福利项目后清空旧套餐,避免将上一个项目的套餐用于导出或导入。
projectChange() {
this.$set(this.pageForm, "optionId", null)
this.doSearch()
},
// 导出当前福利项目中指定套餐的报销表。
doExcelByOptionId() {
this.$downLoad("/platform/welfare/list/mange/doExcelByOptionId", {
projectId: this.pageForm.projectId,
optionId: this.pageForm.optionId
})
},
// 打开快递单号导入弹窗,并把当前福利项目的套餐列表传给弹窗选择。
openImportExpressExcel() {
this.$refs.importCourierNumberRef.openImportExpressExcel(this.pageForm.projectId, this.pageFormProject)
},
// 导出名单
openExport() {
this.$downLoad("/platform/welfare/list/mange/exportXlsx", {
@@ -430,6 +480,7 @@ layout("/layouts/platform.html"){
getWelfareList() {
this.$axios.post("/platform/welfare/project/mange/getWelfareList", { year: this.pageForm.year }).then((res) => {
this.projectSelectOptions = res.data
this.$set(this.pageForm, "optionId", null)
if (res.data && res.data.length > 0) {
this.$set(this.pageForm, "projectId", res.data[0].id)
} else {
@@ -1351,15 +1351,15 @@ layout("/layouts/platform_h5.html"){
},
/**
* 配送日期到达后才显示移动端评价入口;历史记录未设置配送日期时保持隐藏
* @param row 我的福利列表行,deliveryDate 为 yyyy-MM-dd 日期
* @return {boolean} 当前日期达到配送日期时返回 true
* 当前用户已选择福利且项目活动结束后显示移动端评价入口
* @param row 我的福利列表行,choiceTimeEnd 为活动结束时间
* @return {boolean} 当前时间达到活动结束时间时返回 true
*/
canEvaluate(row) {
if (!row || !row.deliveryDate) {
if (!row || !row.isChoose || !row.choiceTimeEnd) {
return false
}
return this.$moment().startOf("day").valueOf() >= this.$moment(row.deliveryDate).startOf("day").valueOf()
return this.$moment().valueOf() >= this.$moment(row.choiceTimeEnd).valueOf()
},
/**
@@ -0,0 +1,97 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.welfare-notice-page {
min-height: 100vh;
background: #f7f8fa;
}
.welfare-notice-card {
margin: 12px;
padding: 14px;
background: #ffffff;
border-radius: 8px;
}
.welfare-notice-title {
margin-bottom: 12px;
color: #323233;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.welfare-notice-viewer {
width: 100%;
height: calc(100vh - 150px);
border: 0;
background: #ffffff;
}
.welfare-notice-empty {
padding: 50px 0;
}
</style>
<div id="app" v-cloak class="welfare-notice-page" v-loading="pageLoading">
<van-nav-bar title="福利通知" left-text="返回" left-arrow fixed placeholder @click-left="back"></van-nav-bar>
<div class="welfare-notice-card" v-if="notice.fileId">
<div class="welfare-notice-title">{{notice.projectName}}</div>
<iframe class="welfare-notice-viewer" :src="viewerUrl" :title="notice.fileName"></iframe>
<van-button block type="primary" plain @click="download">下载原文件</van-button>
</div>
<van-empty v-else-if="!pageLoading" class="welfare-notice-empty" description="暂无福利通知附件"></van-empty>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
const query = new URLSearchParams(window.location.search)
return {
projectId: query.get("id") || "",
notice: {},
pageLoading: false
}
},
computed: {
viewerUrl() {
if (!this.notice.previewUrl) {
return ""
}
return "/assets/platform/plugins/pdfJs/web/viewer.html?file=" + encodeURIComponent(this.notice.previewUrl)
}
},
methods: {
back() {
window.history.back()
},
loadNotice() {
this.$set(this, "pageLoading", true)
this.$axios.post("/platform/welfare/notice/detailData", {id: this.projectId}).then((res) => {
if (res.code === 0) {
this.$set(this, "notice", res.data || {})
} else {
this.$toast.fail(res.msg)
}
}).finally(() => {
this.$set(this, "pageLoading", false)
})
},
download() {
if (this.notice.attachmentUrl) {
window.location.href = this.notice.attachmentUrl
}
}
},
created() {
this.loadNotice()
}
})
</script>
<!--#
}
#-->
@@ -242,7 +242,8 @@ layout("/layouts/platform_h5.html"){
.welfare-detail-link {
display: inline-flex;
height: 20px;
margin-left: 6px;
margin-left: auto;
padding-left: 6px;
flex: none;
align-items: center;
color: #1989fa;
@@ -827,8 +828,8 @@ layout("/layouts/platform_h5.html"){
<span>{{ optionRankText(option) }}</span>
</div>
<div class="welfare-option-desc-row" v-if="optionBrief(option)">
<div class="welfare-option-desc">
<div class="welfare-option-desc-row">
<div class="welfare-option-desc" v-if="optionBrief(option)">
{{ optionBrief(option) }}
</div>
<div class="welfare-detail-link" @click.stop="showOptionDetail(option)">
@@ -847,7 +848,8 @@ layout("/layouts/platform_h5.html"){
integer
disable-input
:min="option.isSystemDefault ? 1 : 0"
:max="projectInfo.isCheckBox === 'radio' ? 1 : selectionLimit"
:max="getOptionMaxSelectNum(option)"
:disable-plus="isOptionIncreaseDisabled(option)"
:disabled="isDeadlinePassed || (projectInfo.isCheckBox === 'radio' && systemDefaultOption && systemDefaultOption.id !== option.id)"
input-width="40px"
button-size="22px"
@@ -918,16 +920,6 @@ layout("/layouts/platform_h5.html"){
maxlength="11"
required
></van-field>
<van-field
v-model="formData.deliveryDate"
label="配送时间"
placeholder="请选择配送时间"
readonly
clickable
is-link
@click="openDeliveryCalendar"
required
></van-field>
<van-field
v-model="formData.remark"
label="备注"
@@ -990,27 +982,15 @@ layout("/layouts/platform_h5.html"){
</div>
</van-popup>
<!-- 配送日期使用 Vant 日历快捷选择,点击日期后立即确认;弹层同步接入全局历史栈。 -->
<van-calendar
v-model="showDeliveryCalendar"
title="选择配送时间"
type="single"
color="#1989fa"
:show-confirm="false"
:close-on-click-overlay="false"
:min-date="deliveryCalendarMinDate"
:default-date="deliveryCalendarDefaultDate"
@confirm="confirmDeliveryDate"
@close="handleHistoryLayerComponentClose('delivery-calendar')">
</van-calendar>
<!-- 选项详情弹窗 -->
<van-action-sheet v-model="showOptionDetailDialog" class="welfare-detail-sheet"
:style="{ height: '78%' }" :close-on-click-overlay="true"
:title="selectedOption ? selectedOption.optionName : ''" cancel-text="取消"
@close="handleHistoryLayerComponentClose('detail')">
@close="handleHistoryLayerComponentClose('detail')">
<div class="welfare-detail-popup" v-if="selectedOption">
<div class="welfare-detail-content" v-html="selectedOption.description"></div>
<div class="welfare-detail-content" v-if="hasOptionDetail(selectedOption)"
v-html="selectedOption.description"></div>
<van-empty v-else description="暂无详情"></van-empty>
</div>
</van-action-sheet>
@@ -1051,8 +1031,6 @@ layout("/layouts/platform_h5.html"){
projectId: "",
selectedRadioId: "", // 单选模式下选中的选项ID
showConfirmDialog: false,
showDeliveryCalendar: false, // 配送日期日历弹层
deliveryCalendarDefaultDate: new Date(), // 日历打开时默认定位的日期
isSubmitting: false,
hasSubmittedBefore: false, // 是否之前提交过
showOptionDetailDialog: false, // 选项详情弹窗
@@ -1065,7 +1043,6 @@ layout("/layouts/platform_h5.html"){
address: "",// 收货地址
receiveAddress: "", // 确认后的完整收货地址
userName: "",//收货人
deliveryDate: "", // 配送日期,按 yyyy-MM-dd 提交
remark: "" // 用户选择备注
},
@@ -1077,11 +1054,6 @@ layout("/layouts/platform_h5.html"){
computed: {
// Vant 日历最小可选日期为当天零点,允许用户选择当天配送。
deliveryCalendarMinDate() {
return this.$moment().startOf("day").toDate()
},
// 系统默认福利必须被保留,管理端限制每个项目最多配置一个。
systemDefaultOption() {
if (!this.projectInfo.options) return null
@@ -1142,7 +1114,7 @@ layout("/layouts/platform_h5.html"){
return Math.max(0, this.selectionLimit - this.selectedCount)
},
// 意向选择不采集收货人、联系电话和收货地址,配送时间及备注仍按原业务要求保留。
// 意向选择不采集收货人、联系电话和收货地址,备注仍按原业务要求保留。
showReceivingContact() {
return Number(this.projectInfo.provideMode) !== 2
},
@@ -1174,7 +1146,6 @@ layout("/layouts/platform_h5.html"){
// 根据浏览器历史中的弹层栈统一显示或关闭弹框,保证返回键与页面状态一致。
syncHistoryLayerStack(stack) {
this.showConfirmDialog = stack.includes("confirm")
this.$set(this, "showDeliveryCalendar", stack.includes("delivery-calendar"))
this.showAddressPopup = stack.includes("address")
this.showOptionDetailDialog = stack.includes("detail")
},
@@ -1212,36 +1183,10 @@ layout("/layouts/platform_h5.html"){
setHistoryLayerVisible(layerName, visible) {
if (layerName === "confirm") this.showConfirmDialog = visible
if (layerName === "delivery-calendar") this.$set(this, "showDeliveryCalendar", visible)
if (layerName === "address") this.showAddressPopup = visible
if (layerName === "detail") this.showOptionDetailDialog = visible
},
/**
* 打开配送日期日历。已有 deliveryDate 时定位到该日期,否则默认定位当天;无返回值。
* 日历作为确认选择弹层的下一层历史记录,系统返回键只关闭日历。
*/
openDeliveryCalendar() {
const selectedDate = this.formData.deliveryDate
? this.$moment(this.formData.deliveryDate, "YYYY-MM-DD").toDate()
: new Date()
// 历史配送日期早于今天时定位到今天,避免默认日期超出日历可选范围。
const defaultDate = this.$moment(selectedDate).startOf("day").valueOf()
< this.$moment().startOf("day").valueOf() ? new Date() : selectedDate
this.$set(this, "deliveryCalendarDefaultDate", defaultDate)
this.openHistoryLayer("delivery-calendar")
},
/**
* 快捷选中配送日期。date 为 van-calendar 返回的 Date,写入 yyyy-MM-dd 字符串后同步回退日历历史;无返回值。
*
* @param date Vant 日历当前选中的日期
*/
confirmDeliveryDate(date) {
this.$set(this.formData, "deliveryDate", this.$moment(date).format("YYYY-MM-DD"))
this.closeHistoryLayer("delivery-calendar")
},
optionRankText(option) {
if (option.rankNo) {
return "本次福利排行榜第" + option.rankNo + "名"
@@ -1255,6 +1200,20 @@ layout("/layouts/platform_h5.html"){
const text = (div.textContent || div.innerText || "").replace(/\s+/g, " ").trim()
return text
},
/**
* 判断福利选项是否存在可展示的详情内容。
* @param option 福利选项,详情来源为 description 字段。
* @returns {boolean} 存在文字或富媒体内容时返回 true,否则详情弹框显示“暂无详情”。
*/
hasOptionDetail(option) {
if (!option || !option.description) {
return false
}
const div = document.createElement("div")
div.innerHTML = option.description
const text = (div.textContent || div.innerText || "").replace(/\s+/g, " ").trim()
return !!text || !!div.querySelector("img,table,video,audio,iframe")
},
selectNumChange(index, newValue) {
const option = this.projectInfo.options[index];
if (option.isSystemDefault && Number(newValue) < 1) {
@@ -1262,6 +1221,12 @@ layout("/layouts/platform_h5.html"){
this.$toast.fail("系统默认福利不可取消")
return
}
if (!this.projectInfo.singleOptionSupportMultipleSelection && Number(newValue) > 1) {
this.$set(option, "selectNum", 1)
this.$set(option, "selectNumKey", (option.selectNumKey || 0) + 1)
this.$toast.fail("此套餐最多只能选择1份")
return
}
const maxSelect = this.projectInfo.multiSelectNum || this.projectInfo.options.length;
// 计算其他所有选项的总和(不包括当前修改的选项)
@@ -1282,7 +1247,7 @@ layout("/layouts/platform_h5.html"){
// 如果当前选项的值大于允许的最大值,则调整它
if (option.selectNum > maxAllowedForThisOption) {
option.selectNum = maxAllowedForThisOption;
this.$set(option, "selectNum", maxAllowedForThisOption)
}
// 如果当前选项的值小于或等于允许的最大值(例如,用户减少了数量导致总数超标,但实际上减少后并未超过 maxSelect,
// 这种情况理论上不应该在 change 时发生,因为 change 是值改变后触发的,且 change 后的值导致了超标),
@@ -1306,6 +1271,44 @@ layout("/layouts/platform_h5.html"){
return Number(option.selectNum) || 0
},
/**
* 计算当前选项可达到的最大份数。
* 多选项目同时受项目剩余份数和单个套餐是否允许多份控制;单选项目固定为1。
* @param option 当前福利选项,selectNum为已选份数
* @return {number} 传给步进器max属性的最大份数
*/
getOptionMaxSelectNum(option) {
if (this.projectInfo.isCheckBox === "radio") {
return 1
}
const currentQuantity = Number(option.selectNum) || 0
const globalAllowedQuantity = currentQuantity + this.remainingSelectionCount
if (!this.projectInfo.singleOptionSupportMultipleSelection) {
return Math.min(1, globalAllowedQuantity)
}
return globalAllowedQuantity
},
/**
* 判断当前选项是否还能增加份数。
* 达到项目总上限或单个套餐已达到1份且不允许多份时禁用加号,但不影响减号取消。
* @param option 当前福利选项,selectNum为已选份数
* @return {boolean} true表示禁用当前步进器的加号
*/
isOptionIncreaseDisabled(option) {
if (this.isDeadlinePassed) {
return true
}
if (this.projectInfo.isCheckBox === "radio") {
return this.selectedRadioId === option.id
}
const currentQuantity = Number(option.selectNum) || 0
if (!this.projectInfo.singleOptionSupportMultipleSelection && currentQuantity >= 1) {
return true
}
return this.remainingSelectionCount <= 0
},
/**
* 统一处理单选和多选项目的数量变化,保持原有默认项及总数量限制。
* @param index 福利选项索引
@@ -1320,16 +1323,28 @@ layout("/layouts/platform_h5.html"){
return
}
if (option.isSystemDefault) return
this.selectedRadioId = ""
this.projectInfo.options.forEach((item) => {
item.selectNum = 0
})
this.syncRadioOptionSelection("")
return
}
this.$set(option, "selectNum", quantity)
this.selectNumChange(index, quantity)
},
/**
* 同步单选项目的选中项和全部步进器显示值。
* @param optionId 当前选中的福利选项 ID;传空字符串表示取消当前选择
* @return {void} 无返回值,未选项统一显示 0,选中项显示 1
*/
syncRadioOptionSelection(optionId) {
this.$set(this, "selectedRadioId", optionId || "")
if (!this.projectInfo.options) return
this.projectInfo.options.forEach((option) => {
this.$set(option, "selectNum", option.id === optionId ? 1 : 0)
// Vant Stepper 会保留内部显示值,切换单选项时更新 key 强制同步所有控件。
this.$set(option, "selectNumKey", (option.selectNumKey || 0) + 1)
})
},
// 保留原有首次选择方法,兼容其他调用入口。
selectCheckboxOption(index) {
if (this.isDeadlinePassed) {
@@ -1387,10 +1402,8 @@ layout("/layouts/platform_h5.html"){
this.userSelection = resp.data
this.hasSubmittedBefore = this.userSelection.length > 0
// 配送时间和备注属于整次选择,统一从首条选择记录回显。
// 备注属于整次选择,统一从首条选择记录回显。
if (this.userSelection && this.userSelection.length > 0) {
this.$set(this.formData, "deliveryDate", this.userSelection[0].deliveryDate
? this.$moment(this.userSelection[0].deliveryDate).format("YYYY-MM-DD") : "")
this.$set(this.formData, "remark", this.userSelection[0].remark || "")
}
@@ -1427,11 +1440,7 @@ layout("/layouts/platform_h5.html"){
if (this.projectInfo.options && this.userSelection.length > 0) {
// 单选模式
if (this.projectInfo.isCheckBox === "radio" && this.userSelection[0]) {
this.selectedRadioId = this.userSelection[0].selectOptionId
const selectedOption = this.projectInfo.options.find((opt) => opt.id === this.selectedRadioId)
if (selectedOption) {
selectedOption.selectNum = 1
}
this.syncRadioOptionSelection(this.userSelection[0].selectOptionId)
}
// 多选模式
else {
@@ -1543,11 +1552,6 @@ layout("/layouts/platform_h5.html"){
return
}
// 配送时间是评价开放依据,提交前必须确保不早于当前选择日期。
if (!this.validateDeliveryDate()) {
return
}
if (this.isSubmitting) return
this.isSubmitting = true
@@ -1568,7 +1572,6 @@ layout("/layouts/platform_h5.html"){
selectNum: 1,
mobile: this.showReceivingContact ? this.formData.mobile : "",
userName: this.showReceivingContact ? this.formData.userName : "",
deliveryDate: this.formData.deliveryDate,
remark: this.formData.remark
}
]
@@ -1581,7 +1584,6 @@ layout("/layouts/platform_h5.html"){
selectNum: option.selectNum,
mobile: this.showReceivingContact ? this.formData.mobile : "",
userName: this.showReceivingContact ? this.formData.userName : "",
deliveryDate: this.formData.deliveryDate,
remark: this.formData.remark
}))
}
@@ -1653,26 +1655,6 @@ layout("/layouts/platform_h5.html"){
return true;
},
/**
* 校验配送日期。deliveryDate 应传 yyyy-MM-dd,返回布尔值表示是否允许继续提交。
* 日期按天比较,配送日期等于选择当天时允许提交。
*
* @return {boolean} 配送日期有效返回 true,否则提示错误并返回 false
*/
validateDeliveryDate() {
if (!this.formData.deliveryDate) {
this.$toast.fail("请选择配送时间")
return false
}
const deliveryDate = this.$moment(this.formData.deliveryDate).startOf("day").valueOf()
const currentDate = this.$moment().startOf("day").valueOf()
if (deliveryDate < currentDate) {
this.$toast.fail("配送时间不能早于选择时间")
return false
}
return true
},
// 提交选择
submitSelection() {
if (!this.hasSelection) {
@@ -1708,22 +1690,11 @@ layout("/layouts/platform_h5.html"){
return
}
if (this.systemDefaultOption && this.systemDefaultOption.id !== optionId) {
this.selectedRadioId = this.systemDefaultOption.id
this.syncRadioOptionSelection(this.systemDefaultOption.id)
this.$toast.fail("系统默认福利不可取消")
return
}
this.selectedRadioId = optionId
// 重置所有选项的selectNum
if (this.projectInfo.options) {
this.projectInfo.options.forEach((option) => {
if (option.id === optionId) {
option.selectNum = 1
} else {
option.selectNum = 0
}
})
}
this.syncRadioOptionSelection(optionId)
},
// 默认项只在首次选择时自动带入,已有历史选择不自动覆盖。
@@ -505,10 +505,6 @@ layout("/layouts/platform_h5.html"){
<van-icon name="clock-o"></van-icon>
<span>选择时间:未设置</span>
</div>
<div class="welfare-list-card__time" v-if="Number(row.isChoose) === 1">
<van-icon name="logistics"></van-icon>
<span>配送时间:{{ row.deliveryDate ? $moment(row.deliveryDate).format('YYYY-MM-DD') : '暂无' }}</span>
</div>
<div class="welfare-list-card__time" v-if="Number(row.isChoose) === 1 && row.remark">
<van-icon name="notes-o"></van-icon>
<span>备注:{{ row.remark }}</span>