Merge remote-tracking branch 'origin/main'

This commit is contained in:
@jyuhsin
2025-06-03 10:08:32 +08:00
6 changed files with 2082 additions and 26 deletions
@@ -1,28 +1,39 @@
package io.v.nutz.zhgh.therapyRecuperation.controller.satisfactionEvaluate;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.wizzer.framework.base.Result;
import cn.wizzer.framework.page.Pagination;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.PictureRenderData;
import io.v.nutz.base.query.PageForm;
import io.v.nutz.base.utils.OfficeTemplateUtil;
import io.v.nutz.sys.models.Sys_user;
import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.zhgh.therapyRecuperation.model.*;
import io.v.nutz.zhgh.therapyRecuperation.service.TheRapyRecuperationEnrollService;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.boot.starter.ftp.FtpService;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.time.ZoneId;
import java.util.*;
@IocBean
@At("/platform/theRapyRecuperation/satisfactionEvaluate")
@@ -33,6 +44,10 @@ public class TheRapyRecuperationSatisfactionEvaluateController {
private Dao dao;
@Inject
private TheRapyRecuperationEnrollService enrollService;
@Inject
private FtpService ftpService;
@Inject
private OfficeTemplateUtil officeTemplateUtil;
@At("")
@@ -44,14 +59,69 @@ public class TheRapyRecuperationSatisfactionEvaluateController {
@At
@RequiresPermissions("theRapyRecuperation.satisfactionEvaluate")
public Result pageData(PageForm pageForm) {
return Result.success();
public Result pageData(PageForm pageForm, Integer year, String lineId, String playTime, String destinationId, String currentCategory) {
Pagination pagination = null;
if (currentCategory.equals("line")) {
Sql sql = Sqls.create("""
SELECT
t1.id,
t1.lineId,
concat(DATE_FORMAT(t1.playStartTime, '%Y-%m-%d'), ' 至 ', DATE_FORMAT(t1.playEndTime, '%Y-%m-%d')) AS playTime,
t1.playStartTime,
t1.playEndTime,
t2.lineName,
(SELECT count(1) FROM the_rapy_recuperation_enroll WHERE takePartInLineId = t1.id AND isTakePartIn = 1) totalNum,
(SELECT count(1) FROM the_rapy_recuperation_satisfaction_evaluate WHERE selectLineId = t1.id) evaluatedNum,
(
SELECT
SUM(
IFNULL(accommodationScore, 0) + IFNULL(cateringScore, 0) + IFNULL(travelScore, 0) + IFNULL(guideScore, 0) + IFNULL(safetyScore, 0)) / (COUNT(*) * 100) * 100 AS averageScore
FROM
the_rapy_recuperation_satisfaction_evaluate
WHERE
selectLineId = t1.id) averageScore
FROM
the_rapy_recuperation_line_union_select t1
LEFT JOIN the_rapy_recuperation_line t2 ON t2.id = t1.lineId
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("YEAR(t1.selectTime)", "=", year);
cnd.andEX("t1.lineId", "=", lineId);
sql.setCondition(cnd);
pagination = enrollService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
} else {
Sql sql = Sqls.create("""
SELECT
t1.id,
t1.baseName,
(SELECT count(1) FROM the_rapy_recuperation_enroll WHERE takePartInBaseManagementId = t1.id AND isTakePartIn = 1) totalNum,
(SELECT count(1) FROM the_rapy_recuperation_satisfaction_evaluate WHERE hotelId = t1.id) evaluatedNum,
(
SELECT
SUM(
IFNULL(accommodationScore, 0) + IFNULL(cateringScore, 0) + IFNULL(travelScore, 0) + IFNULL(guideScore, 0) + IFNULL(safetyScore, 0)) / (COUNT(*) * 100) * 100 AS averageScore
FROM
the_rapy_recuperation_satisfaction_evaluate
WHERE
hotelId = t1.id) averageScore
FROM
`the_rapy_recuperation_base_management` t1
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("t1.year", "=", year);
cnd.andEX("t1.id", "=", destinationId);
sql.setCondition(cnd);
pagination = enrollService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
return Result.success(pagination);
}
/**
* 查询线路
*
* @param year
* @return
*/
@@ -78,6 +148,7 @@ public class TheRapyRecuperationSatisfactionEvaluateController {
/**
* 查询线路的出行时间会有多条
*
* @param lineId
* @param year
* @return
@@ -96,13 +167,387 @@ public class TheRapyRecuperationSatisfactionEvaluateController {
ORDER BY
playStartTime ASC
""");
sql.setParam("lineId",lineId);
sql.setParam("lineId", lineId);
sql.setParam("year", year);
List<NutMap> list = enrollService.listMap(sql);
return Result.success(list);
}
/**
* 查询目的地
*
* @param year
* @return
*/
@At
@RequiresPermissions("theRapyRecuperation.satisfactionEvaluate")
public Result listDestinations(Integer year) {
Sql sql = Sqls.create("""
SELECT
t1.id,
t1.baseName
FROM
`the_rapy_recuperation_base_management` t1
WHERE
year = @year
""");
sql.setParam("year", year);
List<NutMap> list = enrollService.listMap(sql);
return Result.success(list);
}
/**
* 年度总体满意度详情
*
* @param year
* @return
*/
@At
@RequiresPermissions("theRapyRecuperation.satisfactionEvaluate")
public Result totalStat(Integer year) {
NutMap result = NutMap.NEW();
// 参加总人数
int totalNum = dao.count(TheRapyRecuperationEnroll.class, Cnd.where("YEAR(signingUptime)", "=", year));
// 已评价人数
int evaluatedNum = dao.count(TheRapyRecuperationSatisfactionEvaluate.class, Cnd.where("YEAR(evaluateDate)", "=", year));
// 完成率
int evaluateRate = (int) (evaluatedNum / (double) totalNum * 100);
// 总体满意度
Sql sql = Sqls.create("""
SELECT
COUNT(*) as totalPeople,
AVG(IFNULL(accommodationScore, 0)) as avgAccommodationScore,
AVG(IFNULL(cateringScore, 0)) as avgCateringScore,
AVG(IFNULL(travelScore, 0)) as avgTravelScore,
AVG(IFNULL(guideScore, 0)) as avgGuideScore,
AVG(IFNULL(safetyScore, 0)) as avgSafetyScore,
SUM(
IFNULL(accommodationScore, 0) +
IFNULL(cateringScore, 0) +
IFNULL(travelScore, 0) +
IFNULL(guideScore, 0) +
IFNULL(safetyScore, 0)
) as totalScore,
SUM(
IFNULL(accommodationScore, 0) +
IFNULL(cateringScore, 0) +
IFNULL(travelScore, 0) +
IFNULL(guideScore, 0) +
IFNULL(safetyScore, 0)
) / (COUNT(*) * 100) * 100 as averageScore
FROM
the_rapy_recuperation_satisfaction_evaluate
WHERE
YEAR(evaluateDate) = @year
""");
sql.setParam("year", year);
NutMap map = enrollService.fetch(sql);
result.put("totalNum", totalNum);
result.put("evaluatedNum", evaluatedNum);
result.put("evaluateRate", evaluateRate);
result.put("averageScore", map.getInt("averageScore"));
return Result.success(result);
}
/**
* 某一条线路的满意度详情
*
* @param selectLineId
* @return
*/
@At
@RequiresPermissions("theRapyRecuperation.satisfactionEvaluate")
public Result lineStat(String selectLineId) {
Sql sql = Sqls.create("""
SELECT
t1.id,
t1.lineId,
concat(DATE_FORMAT(t1.playStartTime, '%Y-%m-%d'), ' 至 ', DATE_FORMAT(t1.playEndTime, '%Y-%m-%d')) AS playTime,
t1.playStartTime,
t1.playEndTime,
t2.lineName,
(SELECT count(1) FROM the_rapy_recuperation_enroll WHERE takePartInLineId = t1.id AND isTakePartIn = 1) totalNum,
(SELECT count(1) FROM the_rapy_recuperation_satisfaction_evaluate WHERE selectLineId = t1.id) evaluatedNum,
(
SELECT
SUM(
IFNULL(accommodationScore, 0) + IFNULL(cateringScore, 0) + IFNULL(travelScore, 0) + IFNULL(guideScore, 0) + IFNULL(safetyScore, 0)) / (COUNT(*) * 100) * 100 AS averageScore
FROM
the_rapy_recuperation_satisfaction_evaluate
WHERE
selectLineId = t1.id) averageScore
FROM
the_rapy_recuperation_line_union_select t1
LEFT JOIN the_rapy_recuperation_line t2 ON t2.id = t1.lineId
WHERE t1.id = @selectLineId
""");
sql.setParam("selectLineId", selectLineId);
NutMap basicInfo = enrollService.fetch(sql);
int totalNum = basicInfo.getInt("totalNum", 0);
List<TheRapyRecuperationSatisfactionEvaluate> evaluates = dao.query(TheRapyRecuperationSatisfactionEvaluate.class, Cnd.where("selectLineId", "=", selectLineId));
List<NutMap> dimensionsList = new ArrayList<>();
List<String> list = List.of("住宿条件", "餐饮状况", "行程景点安排", "车辆状况和司机服务", "导游服务", "安全措施");
for (String s : list) {
NutMap row = NutMap.NEW();
row.put("dimensionName", s);
if (s.equals("住宿条件")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getAccommodationScore() >= 18 && e.getAccommodationScore() <= 20).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getAccommodationScore() >= 16 && e.getAccommodationScore() <= 17).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getAccommodationScore() >= 12 && e.getAccommodationScore() <= 15).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getAccommodationScore() < 12).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getAccommodationScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getAccommodationScore).sum();
row.put("averageScore", sum / (goodNum + betterNum + generalNum + badNum));
} else if (s.equals("餐饮状况")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getCateringScore() >= 18 && e.getCateringScore() <= 20).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getCateringScore() >= 16 && e.getCateringScore() <= 17).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getCateringScore() >= 12 && e.getCateringScore() <= 15).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getCateringScore() < 12).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getCateringScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getCateringScore).sum();
row.put("averageScore", sum / (goodNum + betterNum + generalNum + badNum));
} else if (s.equals("行程景点安排")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getTravelScore() >= 25 && e.getTravelScore() <= 30).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getTravelScore() >= 20 && e.getTravelScore() <= 24).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getTravelScore() >= 15 && e.getTravelScore() <= 19).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getTravelScore() < 15).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getTravelScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getTravelScore).sum();
row.put("averageScore", sum / (goodNum + betterNum + generalNum + badNum));
} else if (s.equals("车辆状况和司机服务")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getCarDriverScore() >= 9 && e.getCarDriverScore() <= 10).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getCarDriverScore() >= 7 && e.getCarDriverScore() <= 8).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getCarDriverScore() >= 5 && e.getCarDriverScore() <= 6).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getCarDriverScore() < 5).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getCarDriverScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getCarDriverScore).sum();
row.put("averageScore", sum / (goodNum + betterNum + generalNum + badNum));
} else if (s.equals("导游服务")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getGuideScore() >= 9 && e.getGuideScore() <= 10).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getGuideScore() >= 7 && e.getGuideScore() <= 8).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getGuideScore() >= 5 && e.getGuideScore() <= 6).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getGuideScore() < 5).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getGuideScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getGuideScore).sum();
row.put("averageScore", sum / (goodNum + betterNum + generalNum + badNum));
} else if (s.equals("安全措施")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getSafetyScore() >= 9 && e.getSafetyScore() <= 10).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getSafetyScore() >= 7 && e.getSafetyScore() <= 8).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getSafetyScore() >= 5 && e.getSafetyScore() <= 6).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getSafetyScore() < 5).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getSafetyScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getSafetyScore).sum();
row.put("averageScore", sum / (goodNum + betterNum + generalNum + badNum));
}
dimensionsList.add(row);
}
return Result.success(Map.of("basicInfo", basicInfo, "dimensionsList", dimensionsList, "evaluates", evaluates));
}
@At
@RequiresPermissions("theRapyRecuperation.satisfactionEvaluate")
public Result destinationStat(String destinationId) {
Sql sql = Sqls.create("""
SELECT
t1.id,
t1.baseName,
(SELECT count(1) FROM the_rapy_recuperation_enroll WHERE takePartInBaseManagementId = t1.id AND isTakePartIn = 1) totalNum,
(SELECT count(1) FROM the_rapy_recuperation_satisfaction_evaluate WHERE hotelId = t1.id) evaluatedNum,
(
SELECT
SUM(
IFNULL(accommodationScore, 0) + IFNULL(cateringScore, 0) + IFNULL(travelScore, 0) + IFNULL(guideScore, 0) + IFNULL(safetyScore, 0)) / (COUNT(*) * 100) * 100 AS averageScore
FROM
the_rapy_recuperation_satisfaction_evaluate
WHERE
hotelId = t1.id) averageScore
FROM
`the_rapy_recuperation_base_management` t1
WHERE t1.id = @destinationId
""");
sql.setParam("destinationId", destinationId);
NutMap basicInfo = enrollService.fetch(sql);
int totalNum = basicInfo.getInt("totalNum", 1);
List<TheRapyRecuperationSatisfactionEvaluate> evaluates = dao.query(TheRapyRecuperationSatisfactionEvaluate.class, Cnd.where("hotelId", "=", destinationId));
List<NutMap> dimensionsList = new ArrayList<>();
List<String> list = List.of("住宿条件", "餐饮状况", "行程景点安排", "安全措施");
for (String s : list) {
NutMap row = NutMap.NEW();
row.put("dimensionName", s);
if (s.equals("住宿条件")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getAccommodationScore() >= 25 && e.getAccommodationScore() <= 30).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", totalNum == 0 ? 0 : goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getAccommodationScore() >= 20 && e.getAccommodationScore() <= 24).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", totalNum == 0 ? 0 : betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getAccommodationScore() >= 15 && e.getAccommodationScore() <= 19).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", totalNum == 0 ? 0 : generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getAccommodationScore() < 15).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", totalNum == 0 ? 0 : badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getAccommodationScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getAccommodationScore).sum();
row.put("averageScore", (goodNum + betterNum + generalNum + badNum) == 0 ? 0 : sum / (goodNum + betterNum + generalNum + badNum));
} else if (s.equals("餐饮状况")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getCateringScore() >= 25 && e.getCateringScore() <= 30).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", totalNum == 0 ? 0 : goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getCateringScore() >= 20 && e.getCateringScore() <= 24).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", totalNum == 0 ? 0 : betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getCateringScore() >= 15 && e.getCateringScore() <= 19).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", totalNum == 0 ? 0 : generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getCateringScore() < 15).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", totalNum == 0 ? 0 : badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getCateringScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getCateringScore).sum();
row.put("averageScore", (goodNum + betterNum + generalNum + badNum) == 0 ? 0 : sum / (goodNum + betterNum + generalNum + badNum));
} else if (s.equals("行程景点安排")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getTravelScore() >= 25 && e.getTravelScore() <= 30).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", totalNum == 0 ? 0 : goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getTravelScore() >= 20 && e.getTravelScore() <= 24).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", totalNum == 0 ? 0 : betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getTravelScore() >= 15 && e.getTravelScore() <= 19).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", totalNum == 0 ? 0 : generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getTravelScore() < 15).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", totalNum == 0 ? 0 : badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getTravelScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getTravelScore).sum();
row.put("averageScore", (goodNum + betterNum + generalNum + badNum) == 0 ? 0 : sum / (goodNum + betterNum + generalNum + badNum));
} else if (s.equals("安全措施")) {
List<NutMap> distribution = new ArrayList<>();
// 好
long goodNum = evaluates.stream().filter(e -> e.getSafetyScore() >= 9 && e.getSafetyScore() <= 10).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", goodNum).addv("rate", totalNum == 0 ? 0 : goodNum * 100 / totalNum));
// 较好
long betterNum = evaluates.stream().filter(e -> e.getSafetyScore() >= 7 && e.getSafetyScore() <= 8).count();
distribution.add(NutMap.NEW().addv("label", "较好").addv("count", betterNum).addv("rate", totalNum == 0 ? 0 : betterNum * 100 / totalNum));
// 一般
long generalNum = evaluates.stream().filter(e -> e.getSafetyScore() >= 5 && e.getSafetyScore() <= 6).count();
distribution.add(NutMap.NEW().addv("label", "一般").addv("count", generalNum).addv("rate", generalNum * 100 / totalNum));
// 差
long badNum = evaluates.stream().filter(e -> e.getSafetyScore() < 5).count();
distribution.add(NutMap.NEW().addv("label", "").addv("count", badNum).addv("rate", badNum * 100 / totalNum));
row.put("distribution", distribution);
// 平均分
int sum = evaluates.stream().filter(e -> e.getSafetyScore() != null).mapToInt(TheRapyRecuperationSatisfactionEvaluate::getSafetyScore).sum();
row.put("averageScore", (goodNum + betterNum + generalNum + badNum) == 0 ? 0 : sum / (goodNum + betterNum + generalNum + badNum));
}
dimensionsList.add(row);
}
return Result.success(Map.of("basicInfo", basicInfo, "dimensionsList", dimensionsList, "evaluates", evaluates));
}
/**
* 提交评价
*/
@@ -168,8 +613,16 @@ public class TheRapyRecuperationSatisfactionEvaluateController {
result.put("teaNum", teaNum);
result.put("hotelId", hotel.getId());
result.put("travelAgencyName", travelAgency.getTravelAgencyName());
result.put("playStartTime", hotel.getActivityStartTime());
result.put("playEndTime", hotel.getActivityEndTime());
String specificTime = enroll.getSpecificTime();
String[] times = specificTime.split("-");
int year = enroll.getSigningUptime().toInstant().atZone(ZoneId.systemDefault()).getYear();
String playStartTime = (year + "" + times[0]).replace("", "-").replace("", "-").replace("", "");
String playEndTime = (year + "" + times[1]).replace("", "-").replace("", "-").replace("", "");
result.put("playStartTime", playStartTime);
result.put("playEndTime", playEndTime);
}
TheRapyRecuperationSatisfactionEvaluate evaluate = dao.fetch(TheRapyRecuperationSatisfactionEvaluate.class, Cnd.where("enrollId", "=", enrollId)
@@ -183,4 +636,372 @@ public class TheRapyRecuperationSatisfactionEvaluateController {
}
@At
@RequiresPermissions("theRapyRecuperation.satisfactionEvaluate")
public void export(Integer year, HttpServletResponse response) throws Exception {
// 创建临时目录
File tempDir = new File(System.getProperty("java.io.tmpdir") + "/satisfaction_export_" + System.currentTimeMillis());
File lineDir = new File(tempDir, "线路评价");
File destinationDir = new File(tempDir, "目的地评价");
lineDir.mkdirs();
destinationDir.mkdirs();
try {
// 线路
Sql sql = Sqls.create("""
SELECT
t1.id,
t2.playStartTime,
t2.playEndTime,
t3.lineName,
t3.travelAgencyPlace,
t1.accommodationScore,
t1.cateringScore,
t1.travelScore,
t1.guideScore,
t1.safetyScore,
t1.carDriverScore,
t1.mobile,
t1.signature,
t1.evaluateText,
t4.travelAgencyName,
t5.loginName,
t5.userName
FROM
the_rapy_recuperation_satisfaction_evaluate t1
LEFT JOIN the_rapy_recuperation_line_union_select t2 ON t2.id = t1.selectLineId
LEFT JOIN the_rapy_recuperation_line t3 ON t3.id = t2.lineId
LEFT JOIN the_rapy_recuperation_travel_agency t4 ON t4.id = t3.travelAgencyId
LEFT JOIN the_rapy_recuperation_enroll t5 on t5.id = t1.enrollId
where t1.selectLineId is not null and t1.selectLineId != '' and YEAR(t1.evaluateDate) = @year
""");
sql.setParam("year", year);
List<NutMap> list = enrollService.listMap(sql);
for (NutMap row : list) {
int accommodationScore = row.getInt("accommodationScore", 0);
if (accommodationScore >= 18 && accommodationScore <= 20) {
row.put("a1", accommodationScore);
} else if (accommodationScore >= 16 && accommodationScore <= 17) {
row.put("a2", accommodationScore);
} else if (accommodationScore >= 12 && accommodationScore <= 15) {
row.put("a3", accommodationScore);
} else if (accommodationScore < 12) {
row.put("a4", accommodationScore);
}
int cateringScore = row.getInt("cateringScore", 0);
if (cateringScore >= 18 && cateringScore <= 20) {
row.put("b1", cateringScore);
} else if (cateringScore >= 16 && cateringScore <= 17) {
row.put("b2", cateringScore);
} else if (cateringScore >= 12 && cateringScore <= 15) {
row.put("b3", cateringScore);
} else if (cateringScore < 12) {
row.put("b4", cateringScore);
}
int travelScore = row.getInt("travelScore", 0);
if (travelScore >= 25 && travelScore <= 30) {
row.put("c1", travelScore);
} else if (travelScore >= 20 && travelScore <= 24) {
row.put("c2", travelScore);
} else if (travelScore >= 15 && travelScore <= 19) {
row.put("c3", travelScore);
} else if (travelScore < 15) {
row.put("c4", travelScore);
}
int carDriverScore = row.getInt("carDriverScore", 0);
if (carDriverScore >= 9 && carDriverScore <= 10) {
row.put("d1", carDriverScore);
} else if (carDriverScore >= 7 && carDriverScore <= 8) {
row.put("d2", carDriverScore);
} else if (carDriverScore >= 5 && carDriverScore <= 6) {
row.put("d3", carDriverScore);
} else if (carDriverScore < 5) {
row.put("d4", carDriverScore);
}
int guideScore = row.getInt("guideScore", 0);
if (guideScore >= 9 && guideScore <= 10) {
row.put("e1", guideScore);
} else if (guideScore >= 7 && guideScore <= 8) {
row.put("e2", guideScore);
} else if (guideScore >= 5 && guideScore <= 6) {
row.put("e3", guideScore);
} else if (guideScore < 5) {
row.put("e4", guideScore);
}
int safetyScore = row.getInt("safetyScore", 0);
if (safetyScore >= 9 && safetyScore <= 10) {
row.put("f1", safetyScore);
} else if (safetyScore >= 7 && safetyScore <= 8) {
row.put("f2", safetyScore);
} else if (safetyScore >= 5 && safetyScore <= 6) {
row.put("f3", safetyScore);
} else if (safetyScore < 5) {
row.put("f4", safetyScore);
}
// 总分
int totalScore = accommodationScore + cateringScore + travelScore + carDriverScore + guideScore + safetyScore;
row.put("totalScore", totalScore);
Date evaluateDate = row.getTime("evaluateDate");
String formatDate = DateUtil.format(evaluateDate, "yyyy年MM月dd日");
row.put("formatDate", formatDate);
// 教职工人数
int totalNum = dao.count(TheRapyRecuperationEnroll.class, Cnd.where("takePartInLineId", "=", row.getString("selectLineId"))
.and("isTakePartIn", "=", 1));
row.put("totalNum", totalNum);
// 时间
Date playStartTime = row.getTime("playStartTime");
String formatPlayStartTime = DateUtil.format(playStartTime, "yyyy年MM月dd日");
Date playEndTime = row.getTime("playEndTime");
String formatPlayEndTime = DateUtil.format(playEndTime, "MM月dd日");
row.put("playDate", formatPlayStartTime + "" + formatPlayEndTime);
// 签名
try (ByteArrayOutputStream byteOs = new ByteArrayOutputStream()) {
ftpService.download(row.getString("signature"), byteOs);
byte[] imageBytes = byteOs.toByteArray();
PictureRenderData pictureRenderData = new PictureRenderData(70, 30, ".png", imageBytes);
row.put("qm", pictureRenderData);
} catch (Exception e) {
// 处理异常
}
// 生成文件名
String fileName = row.getString("loginName") + "-" + row.getString("lineName") + ".docx";
// 替换文件名中的非法字符
fileName = fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
File outputFile = new File(lineDir, fileName);
try (FileOutputStream os = new FileOutputStream(outputFile)) {
Configure config = Configure.builder().build();
XWPFTemplate.compile(officeTemplateUtil.getInputStream("lxyjz"), config).render(row).writeAndClose(os);
} catch (Exception e) {
e.printStackTrace();
}
}
// 目的地
Sql sql2 = Sqls.create("""
SELECT
t1.id,
t1.accommodationScore,
t1.cateringScore,
t1.travelScore,
t1.guideScore,
t1.safetyScore,
t1.carDriverScore,
t1.mobile,
t1.signature,
t1.evaluateText,
t1.enrollId,
t2.baseName,
t3.travelAgencyName,
t4.specificTime,
t4.loginName,
t4.userName,
t4.specificTime
FROM
the_rapy_recuperation_satisfaction_evaluate t1
LEFT JOIN the_rapy_recuperation_base_management t2 ON t2.id = t1.hotelId
LEFT JOIN the_rapy_recuperation_travel_agency t3 ON t3.id = t2.travelAgencyId
LEFT JOIN the_rapy_recuperation_enroll t4 ON t4.id = t1.enrollId
WHERE
t1.hotelId IS NOT NULL && t1.hotelId != '' and YEAR(t1.evaluateDate) = @year
""");
sql2.setParam("year", year);
List<NutMap> list2 = enrollService.listMap(sql2);
for (NutMap row : list2) {
int accommodationScore = row.getInt("accommodationScore", 0);
if (accommodationScore >= 25 && accommodationScore <= 30) {
row.put("a1", accommodationScore);
} else if (accommodationScore >= 20 && accommodationScore <= 24) {
row.put("a2", accommodationScore);
} else if (accommodationScore >= 15 && accommodationScore <= 19) {
row.put("a3", accommodationScore);
} else if (accommodationScore < 15) {
row.put("a4", accommodationScore);
}
int cateringScore = row.getInt("cateringScore", 0);
if (cateringScore >= 25 && cateringScore <= 30) {
row.put("b1", cateringScore);
} else if (cateringScore >= 20 && cateringScore <= 24) {
row.put("b2", cateringScore);
} else if (cateringScore >= 15 && cateringScore <= 19) {
row.put("b3", cateringScore);
} else if (cateringScore < 15) {
row.put("b4", cateringScore);
}
int travelScore = row.getInt("travelScore", 0);
if (travelScore >= 25 && travelScore <= 30) {
row.put("c1", travelScore);
} else if (travelScore >= 20 && travelScore <= 24) {
row.put("c2", travelScore);
} else if (travelScore >= 15 && travelScore <= 19) {
row.put("c3", travelScore);
} else if (travelScore < 15) {
row.put("c4", travelScore);
}
int safetyScore = row.getInt("safetyScore", 0);
if (safetyScore >= 9 && safetyScore <= 10) {
row.put("d1", safetyScore);
} else if (safetyScore >= 7 && safetyScore <= 8) {
row.put("d2", safetyScore);
} else if (safetyScore >= 5 && safetyScore <= 6) {
row.put("d3", safetyScore);
} else if (safetyScore < 5) {
row.put("d4", safetyScore);
}
// 总分
int totalScore = accommodationScore + cateringScore + travelScore + safetyScore;
row.put("totalScore", totalScore);
// 评价时间
Date evaluateDate = row.getTime("evaluateDate");
String formatDate = DateUtil.format(evaluateDate, "yyyy年MM月dd日");
row.put("formatDate", formatDate);
// 教职工人数
int totalNum = dao.count(TheRapyRecuperationEnroll.class, Cnd.where("takePartInBaseManagementId", "=", row.getString("hotelId"))
.and("isTakePartIn", "=", 1));
row.put("totalNum", totalNum);
// 时间
TheRapyRecuperationEnroll enroll = dao.fetch(TheRapyRecuperationEnroll.class, row.getString("enrollId"));
String specificTime = enroll.getSpecificTime();
String[] times = specificTime.split("-");
int year2 = enroll.getSigningUptime().toInstant().atZone(ZoneId.systemDefault()).getYear();
row.put("playDate", year2 + "" + times[0] + "" + times[1]);
// 签名
try (ByteArrayOutputStream byteOs = new ByteArrayOutputStream()) {
ftpService.download(row.getString("signature"), byteOs);
byte[] imageBytes = byteOs.toByteArray();
PictureRenderData pictureRenderData = new PictureRenderData(70, 30, ".png", imageBytes);
row.put("qm", pictureRenderData);
} catch (Exception e) {
// 处理异常
}
// 生成文件名(使用目的地名称和评价ID)
String fileName = row.getString("userName") + "(" + row.getString("specificTime") + ")" + ".docx";
// 替换文件名中的非法字符
fileName = fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
File outputFile = new File(destinationDir, fileName);
try (FileOutputStream os = new FileOutputStream(outputFile)) {
// 获取模板文件路径
Configure config = Configure.builder().build();
XWPFTemplate.compile(officeTemplateUtil.getInputStream("lxyfd"), config).render(row).writeAndClose(os);
} catch (Exception e) {
e.printStackTrace();
}
}
// 创建ZIP文件
String zipFileName = "满意度评价_" + year + "年.zip";
File zipFile = new File(tempDir.getParentFile(), zipFileName);
// 压缩文件
try (java.util.zip.ZipOutputStream zipOut = new java.util.zip.ZipOutputStream(new FileOutputStream(zipFile))) {
// 添加线路文件夹
addFolderToZip(lineDir, lineDir.getName(), zipOut);
// 添加目的地文件夹
addFolderToZip(destinationDir, destinationDir.getName(), zipOut);
}
// 设置响应头
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=" + java.net.URLEncoder.encode(zipFileName, "UTF-8"));
// 将ZIP文件写入响应
try (java.io.FileInputStream fis = new java.io.FileInputStream(zipFile);
java.io.OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
}
} finally {
// 清理临时文件
deleteDirectory(tempDir);
File zipFile = new File(tempDir.getParentFile(), "满意度评价_" + year + "年.zip");
if (zipFile.exists()) {
zipFile.delete();
}
}
}
/**
* 将文件夹添加到ZIP中
*/
private void addFolderToZip(File folder, String folderName, java.util.zip.ZipOutputStream zipOut) throws Exception {
if (folder.exists() && folder.isDirectory()) {
File[] files = folder.listFiles();
if (files != null) {
// 添加空文件夹
if (files.length == 0) {
java.util.zip.ZipEntry zipEntry = new java.util.zip.ZipEntry(folderName + "/");
zipOut.putNextEntry(zipEntry);
zipOut.closeEntry();
return;
}
// 添加文件
for (File file : files) {
if (file.isDirectory()) {
addFolderToZip(file, folderName + "/" + file.getName(), zipOut);
} else {
byte[] buffer = new byte[4096];
try (java.io.FileInputStream fis = new java.io.FileInputStream(file)) {
java.util.zip.ZipEntry zipEntry = new java.util.zip.ZipEntry(folderName + "/" + file.getName());
zipOut.putNextEntry(zipEntry);
int length;
while ((length = fis.read(buffer)) > 0) {
zipOut.write(buffer, 0, length);
}
zipOut.closeEntry();
}
}
}
}
}
}
/**
* 递归删除目录
*/
private void deleteDirectory(File directory) {
if (directory.exists()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
file.delete();
}
}
}
directory.delete();
}
}
}
@@ -65,6 +65,11 @@ public class TheRapyRecuperationSatisfactionEvaluate extends BaseModel {
@ColDefine(type = ColType.INT)
private Integer travelScore;
@Column
@Comment("车辆司机服务得分")
@ColDefine(type = ColType.INT)
private Integer carDriverScore;
@Column
@Comment("导游服务得分")
@ColDefine(type = ColType.INT)
@@ -56,7 +56,6 @@ const SatisfactionEvaluate = {
:key="idx"
class="option-cell"
:class="{'selected': getScoreLevel(formData[item.field], item) === idx}"
@click="selectOption(index, idx)"
>
{{option.range}}
</td>
@@ -127,7 +126,7 @@ const SatisfactionEvaluate = {
</form>
</div>
</div>
`,
`,
data() {
return {
show: false,
@@ -170,7 +169,7 @@ const SatisfactionEvaluate = {
{
title: "车辆状况和司机服务",
maxScore: 10,
field: "guideScore",
field: "carDriverScore",
options: [
{ label: "好", range: "10-9" },
{ label: "较好", range: "8-7" },
@@ -256,9 +255,11 @@ const SatisfactionEvaluate = {
}, 0);
},
currentEvaluationItems() {
return this.formData.type === 1
? this.evaluationItems
: this.evaluationItemsType2;
if (this.formData && this.formData.selectLineId) {
return this.evaluationItems;
} else {
return this.evaluationItemsType2;
}
},
},
methods: {
@@ -267,17 +268,6 @@ const SatisfactionEvaluate = {
this.getBasicInfo();
this.show = true;
},
selectOption(itemIndex, optionIndex) {
const item = this.currentEvaluationItems[itemIndex];
const range = item.options[optionIndex].range;
if (range.includes("-")) {
const highScore = Number(range.split("-")[0]);
this.formData[item.field] = highScore;
} else if (range.includes("<")) {
const threshold = Number(range.replace("<", ""));
this.formData[item.field] = threshold - 1;
}
},
onSubmit() {
this.$dialog
.confirm({
@@ -0,0 +1,249 @@
const evaluateChart = {
template: /*language=HTML*/ `
<el-dialog
title="满意度评价详情"
:visible.sync="visible"
width="80%"
>
<div class="details-header">
<div class="details-title">
{{currentRoute.lineName}} ({{currentRoute.playTime}})
</div>
<div class="details-stats">
<span>参加人数: {{currentRoute.totalNum}}人</span>
<span>已评价人数: {{currentRoute.evaluatedNum}}人</span>
<span>评价完成率: {{currentRoute.evaluateRate}}%</span>
<span>平均分:
<span :class="getSatisfactionClass(currentRoute.averageScore)">
{{currentRoute.averageScore}}分
</span>
</span>
</div>
</div>
<!-- 维度评分表格 -->
<el-table :data="dimensionsList" style="width: 100%" border>
<el-table-column prop="dimensionName" label="评价维度" width="180">
</el-table-column>
<el-table-column
prop="averageScore"
label="平均分"
width="100"
align="center"
>
<template slot-scope="scope">
<span :class="getSatisfactionClass(scope.row.averageScore)">
{{scope.row.averageScore}}
</span>
</template>
</el-table-column>
<el-table-column label="评价分布">
<template slot-scope="scope">
<el-row :gutter="20" class="distribution-row">
<el-col
:span="6"
v-for="(item, index) in scope.row.distribution"
:key="index"
>
<div class="distribution-item">
<div class="level-label">{{item.label}}</div>
<div class="level-count">{{item.count}}人</div>
<div class="level-rate">{{item.rate}}%</div>
<el-progress
:percentage="item.rate"
:color="getLevelColor(item.label)"
:show-text="false"
></el-progress>
</div>
</el-col>
</el-row>
</template>
</el-table-column>
</el-table>
<div class="chart-container" v-if="dimensionsList.length > 0">
<div class="chart-title">各维度评分对比</div>
<div ref="dimensionsChart" class="dimensions-chart"></div>
</div>
<div class="chart-container" v-if="dimensionsList.length > 0">
<div class="chart-title">满意度分布</div>
<div ref="distributionChart" class="distribution-chart"></div>
</div>
</el-dialog>
`,
data() {
return {
visible: false,
currentRoute: {},
dimensionsList: []
};
},
methods: {
onOpen(row, tableData) {
this.currentRoute = row;
this.dimensionsList = tableData.filter(
(item) => item.lineName === row.lineName
);
this.visible = true;
// 在下一个DOM更新周期渲染图表
this.$nextTick(() => {
this.renderCharts();
});
},
getSatisfactionClass(score) {
if (score >= 90) return "text-success";
if (score >= 80) return "text-warning";
return "text-danger";
},
getLevelColor(level) {
const colors = {
: "#67C23A",
较好: "#409EFF",
一般: "#E6A23C",
: "#F56C6C",
};
return colors[level] || "#909399";
},
renderCharts() {
// 需要引入echarts库
if (window.echarts && this.dimensionsList.length > 0) {
this.renderDimensionsChart();
this.renderDistributionChart();
}
},
renderDimensionsChart() {
const chartDom = this.$refs.dimensionsChart;
const myChart = echarts.init(chartDom);
const dimensions = this.dimensionsList.map(
(item) => item.dimensionName
);
const scores = this.dimensionsList.map((item) => item.averageScore);
const option = {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: "category",
data: dimensions,
axisLabel: {
interval: 0,
rotate: 30,
},
},
yAxis: {
type: "value",
min: 70,
max: 100,
},
series: [
{
name: "平均分",
type: "bar",
data: scores,
itemStyle: {
color: function (params) {
const score = params.value;
if (score >= 90) return "#67C23A";
if (score >= 80) return "#E6A23C";
return "#F56C6C";
},
},
label: {
show: true,
position: "top",
formatter: "{c}分",
},
},
],
};
myChart.setOption(option);
},
renderDistributionChart() {
const chartDom = this.$refs.distributionChart;
const myChart = echarts.init(chartDom);
// 统计各个评价等级的总人数
const distribution = {
: 0,
较好: 0,
一般: 0,
: 0,
};
this.dimensionsList.forEach((dimension) => {
dimension.distribution.forEach((item) => {
distribution[item.label] += item.count;
});
});
const option = {
tooltip: {
trigger: "item",
formatter: "{b}: {c}人 ({d}%)",
},
legend: {
orient: "vertical",
left: "left",
},
series: [
{
name: "满意度分布",
type: "pie",
radius: "70%",
center: ["50%", "50%"],
data: [
{
value: distribution["好"],
name: "好",
itemStyle: { color: "#67C23A" },
},
{
value: distribution["较好"],
name: "较好",
itemStyle: { color: "#409EFF" },
},
{
value: distribution["一般"],
name: "一般",
itemStyle: { color: "#E6A23C" },
},
{
value: distribution["差"],
name: "差",
itemStyle: { color: "#F56C6C" },
},
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)",
},
},
label: {
formatter: "{b}: {c}人 ({d}%)",
},
},
],
};
myChart.setOption(option);
}
}
};
export default evaluateChart;
@@ -0,0 +1,375 @@
const evaluateChart = {
template: /*language=HTML*/ `
<el-dialog
title="满意度评价详情"
:visible.sync="visible"
width="80%"
top="50px"
v-loading="loading"
>
<div class="details-header" v-if="currentRoute">
<div class="details-title">
<template v-if="currentCategory==='line'">
{{currentRoute.lineName}} ({{currentRoute.playTime}})
</template>
<template v-else>
{{currentRoute.baseName}}
</template>
</div>
<div class="details-stats">
<span>参加人数: {{currentRoute.totalNum}}人</span>
<span>已评价人数: {{currentRoute.evaluatedNum}}人</span>
<span>评价完成率: {{currentRoute.totalNum === 0 ? 0 : Math.round(currentRoute.evaluatedNum / currentRoute.totalNum * 100)}}%</span>
<span>平均分:
<span :class="getSatisfactionClass(currentRoute.averageScore)">
{{currentRoute.averageScore || 0}}分
</span>
</span>
</div>
</div>
<!-- 维度评分表格 -->
<el-table :data="dimensionsList" style="width: 100%" border>
<el-table-column prop="dimensionName" label="评价维度" width="180">
</el-table-column>
<el-table-column
prop="averageScore"
label="平均分"
width="100"
align="center"
>
<template slot-scope="scope">
<span :class="getSatisfactionClass(scope.row.averageScore)">
{{scope.row.averageScore}}
</span>
</template>
</el-table-column>
<el-table-column label="评价分布">
<template slot-scope="scope">
<el-row :gutter="20" class="distribution-row">
<el-col
:span="6"
v-for="(item, index) in scope.row.distribution"
:key="index"
>
<div class="distribution-item">
<div class="level-label">{{item.label}}</div>
<div class="level-count">{{item.count}}人</div>
<div class="level-rate">{{item.rate}}%</div>
<el-progress
:percentage="item.rate"
:color="getLevelColor(item.label)"
:show-text="false"
></el-progress>
</div>
</el-col>
</el-row>
</template>
</el-table-column>
</el-table>
<div class="chart-container" v-if="dimensionsList.length > 0">
<div class="chart-title">满意度分布</div>
<div ref="distributionChart" class="distribution-chart"></div>
</div>
<!-- 用户评价详情表格 -->
<div class="user-ratings-container" v-if="evaluates.length > 0">
<div class="chart-title">用户评价详情</div>
<el-table :data="evaluates" style="width: 100%" border stripe>
<el-table-column prop="userName" label="用户姓名" width="120" sortable></el-table-column>
<el-table-column prop="mobile" label="手机号" width="120" sortable></el-table-column>
<el-table-column label="住宿" width="80" align="center" sortable="custom"
:sort-method="(a, b) => a.accommodationScore - b.accommodationScore">
<template slot-scope="scope">
<span :class="getSatisfactionClass(scope.row.accommodationScore)">{{scope.row.accommodationScore}}分</span>
</template>
</el-table-column>
<el-table-column label="餐饮" width="80" align="center" sortable="custom"
:sort-method="(a, b) => a.cateringScore - b.cateringScore">
<template slot-scope="scope">
<span :class="getSatisfactionClass(scope.row.cateringScore)">{{scope.row.cateringScore}}分</span>
</template>
</el-table-column>
<el-table-column label="交通" width="80" align="center" sortable="custom"
:sort-method="(a, b) => a.travelScore - b.travelScore">
<template slot-scope="scope">
<span :class="getSatisfactionClass(scope.row.travelScore)">{{scope.row.travelScore}}分</span>
</template>
</el-table-column>
<el-table-column label="司机" width="80" align="center" sortable="custom"
:sort-method="(a, b) => a.carDriverScore - b.carDriverScore">
<template slot-scope="scope">
<span :class="getSatisfactionClass(scope.row.carDriverScore)">{{scope.row.carDriverScore}}分</span>
</template>
</el-table-column>
<el-table-column label="导游" width="80" align="center" sortable="custom"
:sort-method="(a, b) => a.guideScore - b.guideScore">
<template slot-scope="scope">
<span :class="getSatisfactionClass(scope.row.guideScore)">{{scope.row.guideScore}}分</span>
</template>
</el-table-column>
<el-table-column label="安全" width="80" align="center" sortable="custom"
:sort-method="(a, b) => a.safetyScore - b.safetyScore">
<template slot-scope="scope">
<span :class="getSatisfactionClass(scope.row.safetyScore)">{{scope.row.safetyScore}}分</span>
</template>
</el-table-column>
<el-table-column prop="evaluateText" label="评价意见" min-width="200">
<template slot-scope="scope">
<div v-if="scope.row.evaluateText" class="user-comment">{{scope.row.evaluateText}}</div>
<div v-else class="no-comment">暂无评价意见</div>
</template>
</el-table-column>
<el-table-column prop="evaluateDate" label="评价时间" width="160" sortable></el-table-column>
</el-table>
</div>
</el-dialog>
`,
data() {
return {
visible: false,
loading: false,
currentRoute: null,
dimensionsList: [],
evaluates: [],
currentCategory: null,
};
},
methods: {
onOpen(id, currentCategory) {
this.visible = true;
this.loading = true;
this.currentCategory = currentCategory;
if (this.currentCategory === "line") {
$.post("/platform/theRapyRecuperation/satisfactionEvaluate/lineStat", {
selectLineId: id,
})
.then((res) => {
if (res.code === 0) {
this.currentRoute = res.data.basicInfo;
this.dimensionsList = res.data.dimensionsList;
this.evaluates = res.data.evaluates;
this.$nextTick(() => {
this.renderCharts();
});
} else {
this.$toast(res.msg);
}
})
.always(() => {
this.loading = false;
});
} else {
$.post(
"/platform/theRapyRecuperation/satisfactionEvaluate/destinationStat",
{
destinationId: id,
},
)
.then((res) => {
if (res.code === 0) {
this.currentRoute = res.data.basicInfo;
this.dimensionsList = res.data.dimensionsList;
this.evaluates = res.data.evaluates;
this.$nextTick(() => {
this.renderCharts();
});
} else {
this.$toast(res.msg);
}
})
.always(() => {
this.loading = false;
});
}
},
getSatisfactionClass(score) {
// 根据评分范围返回不同的样式类
if (score >= 25) return "text-success"; // 对于 30 分的项目
if (score >= 15) return "text-success"; // 对于 20 分的项目
if (score >= 8) return "text-success"; // 对于 10 分的项目
if (score >= 4) return "text-success"; // 对于 5 分的项目
if (score >= 20 && score < 25) return "text-warning"; // 对于 30 分的项目
if (score >= 12 && score < 15) return "text-warning"; // 对于 20 分的项目
if (score >= 6 && score < 8) return "text-warning"; // 对于 10 分的项目
if (score >= 3 && score < 4) return "text-warning"; // 对于 5 分的项目
return "text-danger";
},
getLevelColor(level) {
const colors = {
: "#67C23A",
较好: "#409EFF",
一般: "#E6A23C",
: "#F56C6C",
};
return colors[level] || "#909399";
},
renderCharts() {
if (window.echarts && this.dimensionsList.length > 0) {
this.renderDistributionChart();
}
},
renderDistributionChart() {
const chartDom = this.$refs.distributionChart;
const myChart = echarts.init(chartDom);
// 统计各个评价等级的总人数
const distribution = {
: 0,
较好: 0,
一般: 0,
: 0,
};
this.dimensionsList.forEach((dimension) => {
dimension.distribution.forEach((item) => {
distribution[item.label] += item.count;
});
});
const option = {
tooltip: {
trigger: "item",
formatter: "{b}: {c}人 ({d}%)",
},
legend: {
orient: "vertical",
left: "left",
},
series: [
{
name: "满意度分布",
type: "pie",
radius: "70%",
center: ["50%", "50%"],
data: [
{
value: distribution["好"],
name: "好",
itemStyle: { color: "#67C23A" },
},
{
value: distribution["较好"],
name: "较好",
itemStyle: { color: "#409EFF" },
},
{
value: distribution["一般"],
name: "一般",
itemStyle: { color: "#E6A23C" },
},
{
value: distribution["差"],
name: "差",
itemStyle: { color: "#F56C6C" },
},
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)",
},
},
label: {
formatter: "{b}: {c}人 ({d}%)",
},
},
],
};
myChart.setOption(option);
},
},
};
// 添加组件样式
document.head.insertAdjacentHTML(
"beforeend",
`
<style>
.details-header {
margin-bottom: 20px;
}
.details-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.details-stats {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.chart-container {
margin-top: 30px;
margin-bottom: 30px;
}
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
padding-left: 10px;
border-left: 4px solid #409EFF;
}
.dimensions-chart, .distribution-chart {
height: 400px;
width: 100%;
}
.distribution-row {
padding: 10px 0;
}
.distribution-item {
padding: 5px 10px;
}
.level-label {
font-weight: bold;
margin-bottom: 5px;
}
.level-count, .level-rate {
display: inline-block;
margin-right: 10px;
margin-bottom: 5px;
}
.text-success {
color: #67C23A;
font-weight: bold;
}
.text-warning {
color: #E6A23C;
font-weight: bold;
}
.text-danger {
color: #F56C6C;
font-weight: bold;
}
.user-ratings-container {
margin-top: 30px;
}
.rating-item {
margin-bottom: 5px;
line-height: 1.5;
}
.dimension-name {
font-weight: bold;
margin-right: 5px;
}
.user-comment {
white-space: pre-line;
line-height: 1.5;
}
.no-comment {
color: #909399;
font-style: italic;
}
.pagination-container {
margin-top: 20px;
text-align: right;
}
</style>
`,
);
@@ -0,0 +1,616 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<!-- 页面标题和分类切换 -->
<div class="page-header">
<div class="page-title">满意度评价统计</div>
<el-radio-group
v-model="pageForm.currentCategory"
@change="handleCategoryChange"
size="medium"
>
<el-radio-button label="line">线路评价</el-radio-button>
<el-radio-button label="destination">目的地评价</el-radio-button>
</el-radio-group>
</div>
<!-- 搜索区域 -->
<el-card shadow="never" class="search-card">
<div class="search">
<div class="search-item">
<div class="search-item-label">年度:</div>
<div class="search-item-option">
<el-date-picker
style="width: 100%"
placeholder="选择年度"
type="year"
:clearable="false"
@change="doSearch"
v-model="pageForm.year"
value-format="yyyy"
>
</el-date-picker>
</div>
</div>
<!-- 线路评价搜索项 -->
<template v-if="pageForm.currentCategory === 'line'">
<div class="search-item">
<div class="search-item-label">线路:</div>
<div class="search-item-option">
<el-select
v-model="pageForm.lineId"
clearable
@change="lineIdChange"
style="width: 100%"
placeholder="请选择线路"
>
<el-option
v-for="item in lineOptions"
:key="item.id"
:label="item.lineName"
:value="item.id"
>
</el-option>
</el-select>
</div>
</div>
<div class="search-item" v-if="pageForm.lineId">
<div class="search-item-label">出行时间:</div>
<div class="search-item-option">
<el-select
v-model="pageForm.playTime"
clearable
style="width: 100%"
placeholder="请选择出行时间"
>
<el-option
v-for="item in lineTimeOptions"
:key="item.id"
:label="item.label"
:value="item.id"
>
</el-option>
</el-select>
</div>
</div>
</template>
<!-- 目的地评价搜索项 -->
<template v-else>
<div class="search-item">
<div class="search-item-label">目的地:</div>
<div class="search-item-option">
<el-select
v-model="pageForm.destinationId"
clearable
style="width: 100%"
placeholder="请选择目的地"
>
<el-option
v-for="item in destinationOptions"
:key="item.id"
:label="item.baseName"
:value="item.id"
>
</el-option>
</el-select>
</div>
</div>
</template>
<div class="search-query">
<el-button
@click="doSearch"
icon="el-icon-search"
type="primary"
size="medium"
>搜索
</el-button>
<!-- <el-button @click="resetSearch" icon="el-icon-refresh" plain size="medium"-->
<!-- >重置-->
<!-- </el-button>-->
</div>
</div>
</el-card>
<!-- 整体统计信息卡片 -->
<div class="stats-cards" v-if="totalStat.totalNum">
<el-row :gutter="20">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card-inner">
<div class="stat-icon participants">
<i class="el-icon-user"></i>
</div>
<div class="stat-content">
<div class="stat-value">{{totalStat.totalNum}}</div>
<div class="stat-label">参加总人数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card-inner">
<div class="stat-icon evaluated">
<i class="el-icon-s-check"></i>
</div>
<div class="stat-content">
<div class="stat-value">{{totalStat.evaluatedNum}}</div>
<div class="stat-label">已评价人数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card-inner">
<div class="stat-icon completion">
<i class="el-icon-data-line"></i>
</div>
<div class="stat-content">
<div class="stat-value">
{{totalStat.totalNum ? totalStat.evaluateRate : 0}}%
</div>
<div class="stat-label">评价完成率</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card-inner">
<div class="stat-icon satisfaction">
<i class="el-icon-star-on"></i>
</div>
<div class="stat-content">
<div class="stat-value">{{totalStat.averageScore}}</div>
<div class="stat-label">总体满意度</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 数据列表 -->
<el-card class="mt20" shadow="never">
<div slot="header" class="clearfix">
<div class="card-header">
<div class="card-title-wrapper">
<i class="el-icon-s-data"></i>
<span class="card-title"
>{{pageForm.currentCategory === 'line' ? '线路' :
'目的地'}}评价列表</span
>
</div>
<div class="card-header-actions">
<el-button
type="primary"
size="small"
icon="el-icon-download"
@click="handleExport"
>导出数据</el-button
>
</div>
</div>
</div>
<!-- 线路列表表格 -->
<el-table
:data="tableData"
style="width: 100%"
border
stripe
v-loading="loading"
>
<el-table-column
prop="lineName"
label="线路名称"
v-if="pageForm.currentCategory === 'line'"
></el-table-column>
<el-table-column
prop="baseName"
label="目的地名称"
v-if="pageForm.currentCategory === 'destination'"
></el-table-column>
<el-table-column
prop="playTime"
label="出行时间"
v-if="pageForm.currentCategory === 'line'"
></el-table-column>
<el-table-column
prop="totalNum"
label="参加人数"
width="100"
align="center"
></el-table-column>
<el-table-column
prop="evaluatedNum"
label="已评价人数"
width="100"
align="center"
></el-table-column>
<el-table-column
prop="evaluateRate"
label="评价完成率"
width="120"
align="center"
>
<template slot-scope="scope">
<el-progress
:percentage="scope.row.totalNum ? Math.round(scope.row.evaluatedNum / scope.row.totalNum * 100) : 0"
:color="getProgressColor(scope.row.totalNum ? Math.round(scope.row.evaluatedNum / scope.row.totalNum * 100) : 0)"
></el-progress>
</template>
</el-table-column>
<el-table-column
prop="averageScore"
label="平均分"
width="100"
align="center"
>
<template slot-scope="scope">
<span class="score-badge"> {{scope.row.averageScore}} </span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template slot-scope="scope">
<el-button
type="primary"
size="mini"
@click="showDetails(scope.row)"
>查看详情
</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<!-- 使用评价详情组件 -->
<evaluate-chart ref="evaluateChartRef"></evaluate-chart>
</guava>
</div>
<script>
<!--#include('evaluateChart.js'){}#-->
new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"evaluate-chart": evaluateChart,
},
data() {
return {
pageForm: {
year: new Date().getFullYear().toString(),
currentCategory: "line",
lineId: null,
playTime: null,
destinationId: null,
},
lineOptions: [],
lineTimeOptions: [],
destinationOptions: [],
loading: false,
totalStat: {
totalNum: 0,
evaluatedNum: 0,
evaluateRate: 0,
averageScore: 0,
},
detailsVisible: false,
currentRoute: {},
dimensionsList: [],
tableData: [],
};
},
methods: {
listLines() {
$.post(
"/platform/theRapyRecuperation/satisfactionEvaluate/listLines",
this.pageForm,
).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data;
} else {
this.$message.error(res.msg);
}
});
},
lineIdChange(id) {
if (id) {
$.post(
"/platform/theRapyRecuperation/satisfactionEvaluate/listTimeByLineId",
this.pageForm,
).then((res) => {
if (res.code === 0) {
this.lineTimeOptions = res.data;
} else {
this.$message.error(res.msg);
}
});
}
},
getTotalStat() {
$.post(
"/platform/theRapyRecuperation/satisfactionEvaluate/totalStat",
this.pageForm,
).then((res) => {
if (res.code === 0) {
this.totalStat = res.data;
} else {
this.$message.error(res.msg);
}
});
},
showDetails(row) {
this.$refs.evaluateChartRef.onOpen(
row.id,
this.pageForm.currentCategory,
);
},
handleCategoryChange() {
// 重置表单
this.pageForm.lineId = null;
this.pageForm.playTime = null;
this.pageForm.destinationId = null;
// 重新加载数据
if (this.pageForm.currentCategory === "line") {
this.listLines();
} else {
this.listDestinations();
}
this.doSearch();
},
listDestinations() {
$.post(
"/platform/theRapyRecuperation/satisfactionEvaluate/listDestinations",
this.pageForm,
).then((res) => {
if (res.code === 0) {
this.destinationOptions = res.data;
} else {
this.$message.error(res.msg);
}
});
},
resetSearch() {
this.pageForm = {
year: new Date().getFullYear().toString(),
currentCategory: this.pageForm.currentCategory,
lineId: null,
playTime: null,
destinationId: null,
};
this.doSearch();
},
getProgressColor(percentage) {
if (percentage < 30) return "#F56C6C";
if (percentage < 70) return "#E6A23C";
return "#67C23A";
},
handleExport() {
// 构建查询参数
const params = {
year: this.pageForm.year,
currentCategory: this.pageForm.currentCategory,
lineId: this.pageForm.lineId,
playTime: this.pageForm.playTime,
destinationId: this.pageForm.destinationId
};
// 使用window.open直接下载文件
let queryString = '';
const keys = Object.keys(params);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (params[key] != null) {
if (queryString !== '') {
queryString += '&';
}
queryString += key + '=' + encodeURIComponent(params[key]);
}
}
window.open('/platform/theRapyRecuperation/satisfactionEvaluate/export?' + queryString);
},
},
created() {
this.pageData();
this.listLines();
this.getTotalStat();
},
});
</script>
<style>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 22px;
font-weight: 600;
color: #303133;
}
.search-card {
margin-bottom: 20px;
border-radius: 8px;
}
.search {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
}
.search-item {
flex: 1;
min-width: 250px;
}
.search-item-label {
font-weight: 500;
margin-bottom: 8px;
color: #606266;
}
.search-query {
display: flex;
align-items: flex-end;
gap: 10px;
}
.stats-cards {
margin-bottom: 20px;
}
.stat-card {
border-radius: 8px;
height: 100%;
transition: all 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.stat-card-inner {
display: flex;
align-items: center;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
}
.stat-icon i {
font-size: 30px;
color: white;
}
.stat-icon.participants {
background-color: #409eff;
}
.stat-icon.evaluated {
background-color: #67c23a;
}
.stat-icon.completion {
background-color: #e6a23c;
}
.stat-icon.satisfaction {
background-color: #f56c6c;
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #303133;
line-height: 1.2;
}
.stat-label {
color: #909399;
font-size: 14px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 0;
}
.card-title-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.card-title-wrapper i {
font-size: 20px;
color: #409eff;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
position: relative;
top: 1px;
}
.card-header-actions {
display: flex;
gap: 10px;
}
.card-header-actions .el-button {
padding: 8px 16px;
}
.card-header-actions .el-button [class^="el-icon-"] {
margin-right: 4px;
}
.mt20 {
margin-top: 20px;
}
.score-badge {
background-color: #409eff;
color: white;
padding: 5px 10px;
border-radius: 12px;
font-weight: 500;
}
.text-success {
color: #67c23a;
}
.text-warning {
color: #e6a23c;
}
.text-danger {
color: #f56c6c;
}
.clearfix:after {
content: "";
display: table;
clear: both;
}
</style>
<!--#
}
#-->