Merge remote-tracking branch 'origin/main'
This commit is contained in:
+830
-9
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -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;
|
||||
+375
@@ -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>
|
||||
`,
|
||||
);
|
||||
+616
@@ -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>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user