This commit is contained in:
zhouhefeng
2026-06-15 14:18:01 +08:00
parent 449428b762
commit 873213de53
35 changed files with 4044 additions and 701 deletions
@@ -310,7 +310,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt());
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
// if (Globals.sso) {
throw new BaseException("用户名或者密码不正确");
// throw new BaseException("用户名或者密码不正确");
// }
}
user = this.fetchLinks(user, "unit");
@@ -59,18 +59,12 @@ public class QsvActivityController {
//保存问卷基础信息
@At
@SaCheckPermission("qsv.activity")
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("保存问卷基础信息")
@SLog(type = "qsv.activity", tag = "保存问卷基础信息", msg = "保存问卷基础信息")
public Result save(QsvActivity qsvActivity) {
if (qsvActivity.getCategory().equals("QUIZ")) {
if (qsvActivity.getMode().equals("SCHEDULED")) {
qsvActivity.setRepeatMode("DAILY");
} else if (qsvActivity.getMode().equals("REGULAR")) {
qsvActivity.setRepeatMode("TOTAL");
}
}
dao.insertOrUpdate(qsvActivity);
return Result.success();
QsvActivity activity = qsvActivityService.saveActivity(qsvActivity);
return Result.success(activity);
}
@At
@@ -83,13 +77,25 @@ public class QsvActivityController {
// 删除问卷
@At
@SaCheckPermission("qsv.activity")
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("删除问卷")
@SLog(type = "qsv.activity", tag = "删除问卷", msg = "删除问卷")
public Result delete(@Valid String id) {
dao.delete(QsvActivity.class, id);
qsvActivityService.deleteActivity(id);
return Result.success();
}
// 更新活动开启状态
@At
@SaCheckPermission("qsv.activity")
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("更新活动开启状态")
@SLog(type = "qsv.activity", tag = "更新活动开启状态", msg = "更新活动开启状态")
public Result updateEnabled(@Valid String id, Boolean enabled) {
qsvActivityService.updateEnabled(id, enabled);
return Result.success(enabled ? "开启成功" : "关闭成功");
}
// 保存问卷题目
@At
@@ -1,16 +1,27 @@
package com.budwk.app.zhgh.dayofficework.qsv.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvSurveyService;
import io.swagger.annotations.ApiOperation;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
@@ -21,6 +32,10 @@ import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Base64;
import java.util.List;
@IocBean
@@ -43,12 +58,19 @@ public class QsvSurveyController {
}
@At("/reportPage")
@Ok("beetl:/platform/zhgh/dayofficework/qsv/survey/reportPage.html")
@SaCheckPermission("qsv.survey")
public void reportPage() {
}
@At
@SaCheckPermission("qsv.survey")
@ApiOperation("分页查询")
public Result pageData(@Valid PageForm pageForm, Integer year) {
Cnd cnd = Cnd.where("category", "=", "SURVEY");
Cnd cnd = Cnd.where("category", "in", new String[]{"SURVEY", "VOTE"});
cnd.andEX("YEAR(startTime)", "=", year);
cnd.desc("category");
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
@@ -63,6 +85,43 @@ public class QsvSurveyController {
return Result.success(report);
}
@At
@SaCheckPermission("qsv.survey")
@ApiOperation("查询调查信息")
public Result activityInfo(@Valid String activityId) {
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
NutMap data = NutMap.NEW();
if (activity != null) {
data.setv("id", activity.getId());
data.setv("title", activity.getTitle());
}
return Result.success(data);
}
@At
@SaCheckPermission("qsv.survey")
@ApiOperation("生成分类报告")
public Result categoryReport(@Valid String activityId, String conditions) {
List<NutMap> report = qsvSurveyService.categoryReport(activityId, conditions);
return Result.success(report);
}
@At
@SaCheckPermission("qsv.survey")
@ApiOperation("生成交叉分析报告")
public Result crossReport(@Valid String activityId, @Valid String xSubjectIds, @Valid String ySubjectIds, String conditions) {
NutMap report = qsvSurveyService.crossReport(activityId, xSubjectIds, ySubjectIds, conditions);
return Result.success(report);
}
@At
@SaCheckPermission("qsv.survey")
@ApiOperation("生成对比分析报告")
public Result compareReport(@Valid String activityId, @Valid String subjectId, @Valid String optionId) {
NutMap report = qsvSurveyService.compareReport(activityId, subjectId, optionId);
return Result.success(report);
}
@At
@Ok("void")
@@ -72,13 +131,236 @@ public class QsvSurveyController {
qsvSurveyService.exportReportXlsx(activityId, response);
}
@At
@Ok("void")
@SaCheckPermission("qsv.survey")
@ApiOperation("下载PDF报告")
public void exportReportPdf(String reportData, HttpServletResponse response) {
if (StrUtil.isBlank(reportData)) {
throw new IllegalArgumentException("报告数据不能为空");
}
JSONObject report = JSONUtil.parseObj(reportData);
String title = StrUtil.blankToDefault(report.getStr("title"), "分析报告");
String tabLabel = StrUtil.blankToDefault(report.getStr("tabLabel"), "分析报告");
try {
CommonDownloadUtil.download(safeFileName(title + "-" + tabLabel) + ".pdf", buildReportPdf(report), response);
} catch (IOException e) {
throw new RuntimeException("生成PDF报告失败", e);
}
}
private byte[] buildReportPdf(JSONObject report) throws IOException {
try (PDDocument document = new PDDocument(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
PdfReportWriter writer = new PdfReportWriter(document);
writer.title(StrUtil.blankToDefault(report.getStr("title"), "分析报告"));
writer.text("报告类型:" + StrUtil.blankToDefault(report.getStr("tabLabel"), "分析报告"), 10);
writer.text("生成时间:" + DateUtil.now(), 10);
writer.gap(8);
JSONArray sections = report.getJSONArray("sections");
if (sections == null || sections.isEmpty()) {
writer.text("暂无可下载内容", 12);
} else {
for (int i = 0; i < sections.size(); i++) {
JSONObject section = JSONUtil.parseObj(sections.get(i));
writer.section(StrUtil.blankToDefault(section.getStr("title"), "统计项"));
String description = section.getStr("description");
if (StrUtil.isNotBlank(description)) {
writer.text(description, 10);
}
String image = section.getStr("image");
if (StrUtil.isNotBlank(image)) {
writer.image(image);
}
writer.table(section.getJSONArray("rows"));
writer.gap(8);
}
}
writer.close();
document.save(out);
return out.toByteArray();
}
}
private String safeFileName(String fileName) {
return StrUtil.blankToDefault(fileName, "分析报告").replaceAll("[\\\\/:*?\"<>|\\r\\n]", "_");
}
private PDType0Font loadChineseFont(PDDocument document, boolean bold) throws IOException {
String[] paths = bold ? new String[]{
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/simsunb.ttf",
"C:/Windows/Fonts/msyhbd.ttf"
} : new String[]{
"C:/Windows/Fonts/simfang.ttf",
"C:/Windows/Fonts/simsun.ttf",
"C:/Windows/Fonts/msyh.ttf"
};
for (String path : paths) {
File file = new File(path);
if (file.exists()) {
return PDType0Font.load(document, file);
}
}
throw new IOException("未找到可用的中文字体");
}
private class PdfReportWriter {
private final PDDocument document;
private final PDType0Font regularFont;
private final PDType0Font boldFont;
private final float margin = 42F;
private final float pageWidth = PDRectangle.A4.getWidth();
private final float pageHeight = PDRectangle.A4.getHeight();
private final float contentWidth = pageWidth - margin * 2;
private PDPageContentStream content;
private float y;
PdfReportWriter(PDDocument document) throws IOException {
this.document = document;
this.regularFont = loadChineseFont(document, false);
this.boldFont = loadChineseFont(document, true);
newPage();
}
void newPage() throws IOException {
if (content != null) {
content.close();
}
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
content = new PDPageContentStream(document, page);
y = pageHeight - margin;
}
void close() throws IOException {
if (content != null) {
content.close();
content = null;
}
}
void ensure(float height) throws IOException {
if (y - height < margin) {
newPage();
}
}
void gap(float height) throws IOException {
ensure(height);
y -= height;
}
void title(String text) throws IOException {
ensure(36);
drawText(text, margin, y, 18, boldFont);
y -= 32;
}
void section(String text) throws IOException {
ensure(30);
drawText(text, margin, y, 13, boldFont);
y -= 24;
}
void text(String text, float fontSize) throws IOException {
List<String> lines = wrap(StrUtil.blankToDefault(text, ""), fontSize, contentWidth, regularFont);
for (String line : lines) {
ensure(fontSize + 8);
drawText(line, margin, y, fontSize, regularFont);
y -= fontSize + 7;
}
}
void image(String dataUrl) throws IOException {
int commaIndex = dataUrl.indexOf(",");
if (commaIndex < 0) {
return;
}
byte[] bytes = Base64.getDecoder().decode(dataUrl.substring(commaIndex + 1));
PDImageXObject image = PDImageXObject.createFromByteArray(document, bytes, "chart");
float width = contentWidth;
float height = width * image.getHeight() / image.getWidth();
if (height > 260) {
height = 260;
width = height * image.getWidth() / image.getHeight();
}
ensure(height + 12);
content.drawImage(image, margin, y - height, width, height);
y -= height + 12;
}
void table(JSONArray rows) throws IOException {
if (rows == null || rows.isEmpty()) {
text("暂无统计数据", 10);
return;
}
float[] widths = new float[]{contentWidth * 0.46F, contentWidth * 0.15F, contentWidth * 0.17F, contentWidth * 0.22F};
drawTableRow(new String[]{"选项/内容", "数量", "占比", "备注"}, widths, true);
for (int i = 0; i < rows.size(); i++) {
JSONObject row = JSONUtil.parseObj(rows.get(i));
drawTableRow(new String[]{
StrUtil.blankToDefault(row.getStr("name"), ""),
StrUtil.blankToDefault(row.getStr("count"), ""),
StrUtil.blankToDefault(row.getStr("percent"), ""),
StrUtil.blankToDefault(row.getStr("remark"), "")
}, widths, false);
}
}
void drawTableRow(String[] values, float[] widths, boolean header) throws IOException {
float rowHeight = 24F;
ensure(rowHeight);
float x = margin;
for (int i = 0; i < values.length; i++) {
content.addRect(x, y - rowHeight, widths[i], rowHeight);
content.stroke();
drawText(clip(values[i], header ? 16 : 28), x + 5, y - 16, 9, header ? boldFont : regularFont);
x += widths[i];
}
y -= rowHeight;
}
void drawText(String text, float x, float y, float fontSize, PDType0Font font) throws IOException {
content.beginText();
content.setFont(font, fontSize);
content.newLineAtOffset(x, y);
content.showText(StrUtil.blankToDefault(text, "").replaceAll("[\\r\\n\\t]", " "));
content.endText();
}
List<String> wrap(String text, float fontSize, float maxWidth, PDType0Font font) throws IOException {
List<String> lines = new java.util.ArrayList<>();
StringBuilder line = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
String next = line.toString() + ch;
if (font.getStringWidth(next) / 1000 * fontSize > maxWidth && line.length() > 0) {
lines.add(line.toString());
line.setLength(0);
}
line.append(ch);
}
if (line.length() > 0 || lines.isEmpty()) {
lines.add(line.toString());
}
return lines;
}
String clip(String text, int maxLength) {
if (text == null || text.length() <= maxLength) {
return StrUtil.blankToDefault(text, "");
}
return text.substring(0, maxLength) + "...";
}
}
@At
@SaCheckPermission("qsv.survey")
@ApiOperation("选项选择详情")
public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId) {
List<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId));
List<QsvUserAnswerRecord> selectOptionUsers = answerRecords.stream().filter(ext -> ObjectUtil.isNotNull(ext.getExtJson().get(subjectId, JSONObject.class)) && ext.getExtJson().get(subjectId, JSONObject.class).getJSONArray("optionIds").contains(optionId)).toList();
public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId, String conditions) {
List<NutMap> selectOptionUsers = qsvSurveyService.selectOptionUsers(activityId, subjectId, optionId, conditions);
return Result.success(selectOptionUsers);
}
@@ -1,19 +1,29 @@
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean
@At("/platform/h5/qsv")
@@ -23,6 +33,8 @@ public class H5QsvController {
@Inject
private QsvActivityService qsvActivityService;
@Inject
private Dao dao;
@At("")
@Ok("beetl:/platform/zhghh5/dayofficework/qsv/index.html")
@@ -33,13 +45,69 @@ public class H5QsvController {
@At
@SaCheckPermission("h5.qsv")
public Result pageData(@Valid PageForm pageForm, @Valid String category) {
public Result pageData(@Valid PageForm pageForm, @Valid String category, @Param("title") String title) {
Cnd cnd = Cnd.NEW();
cnd.andEX("category", "=", category);
cnd.and(Cnd.likeEX("title", title));
cnd.and(Cnd.exps("enabled", "=", true).or("enabled", "is", null));
LocalDate currentYearFirstDay = LocalDate.now().withDayOfYear(1);
Date currentYearStart = Date.from(currentYearFirstDay.atStartOfDay(ZoneId.systemDefault()).toInstant());
Date nextYearStart = Date.from(currentYearFirstDay.plusYears(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
cnd.and("startTime", ">=", currentYearStart);
cnd.and("startTime", "<", nextYearStart);
cnd.desc("startTime");
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
fillAnsweredStatus(pagination);
return Result.success(pagination);
}
private void fillAnsweredStatus(Pagination pagination) {
List<NutMap> rows = pagination.getList();
if (rows == null || rows.isEmpty()) {
return;
}
List<String> activityIds = rows.stream()
.map(this::getRowActivityId)
.filter(id -> id != null && !id.isBlank())
.collect(Collectors.toList());
if (activityIds.isEmpty()) {
return;
}
Set<String> answeredActivityIds = dao.query(QsvUserAnswerRecord.class, Cnd.where("userId", "=", SecurityUtil.getUserId())
.and("activityId", "in", activityIds)
.and("isFinish", "=", true))
.stream()
.map(QsvUserAnswerRecord::getActivityId)
.collect(Collectors.toSet());
rows.forEach(row -> {
boolean isAnswered = answeredActivityIds.contains(getRowActivityId(row));
row.put("isAnswered", isAnswered);
row.put("answeredText", isAnswered ? getAnsweredText(row.getString("category")) : "");
});
}
private String getRowActivityId(NutMap row) {
String id = row.getString("id");
if (id == null || id.isBlank()) {
id = row.getString("ID");
}
if (id == null || id.isBlank()) {
id = row.getString("activityId");
}
return id;
}
private String getAnsweredText(String category) {
if ("QUIZ".equals(category)) {
return "已答题";
}
if ("SURVEY".equals(category)) {
return "已填写";
}
if ("VOTE".equals(category)) {
return "已投票";
}
return "已完成";
}
}
@@ -1,7 +1,6 @@
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
@@ -9,7 +8,7 @@ import cn.hutool.json.JSONObject;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.result.Result;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
import com.budwk.app.zhgh.dayofficework.qsv.dto.QsvCheckAnswerResult;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
@@ -44,8 +43,6 @@ public class H5QsvQuizController {
@Inject
private Dao dao;
@Inject
private ActivityBasicScopeService activityBasicScopeService;
@Inject
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
@Inject
private QsvQuizService qsvQuizService;
@@ -71,7 +68,9 @@ public class H5QsvQuizController {
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
if (activity.getGroupId() != null) {
if (!activityBasicScopeService.isUserInGroup(activity.getGroupId(), SecurityUtil.getUserId())) {
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
.and("userId", "=", SecurityUtil.getUserId()));
if (count != 1) {
return Result.error("您无需参加此次答题,感谢您的关注!");
}
}
@@ -103,7 +102,7 @@ public class H5QsvQuizController {
List<String> subjectIds = lastRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = lastRecordOptional.get().getId();
} else {
@@ -115,7 +114,7 @@ public class H5QsvQuizController {
List<String> subjectIds = answerRecord.getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
}
} else {
@@ -125,7 +124,9 @@ public class H5QsvQuizController {
//首次进来生成答题记录
if ("ALL".equals(displayMode)) {
resultSubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
answerRecordId = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList())).getId();
QsvUserAnswerRecord answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList()));
answerRecordId = answerRecord.getId();
resultSubjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds());
} else if ("RANDOM".equals(displayMode)) {
//单次随机抽取题目数量
Integer randomCount = activity.getRandomCount();
@@ -144,7 +145,7 @@ public class H5QsvQuizController {
List<String> subjectIds = answerRecord.getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
}
} else {
@@ -170,7 +171,7 @@ public class H5QsvQuizController {
}
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
repeatTips = true;
} else {
@@ -180,7 +181,7 @@ public class H5QsvQuizController {
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = maxAnswerRecordOptional.get().getId();
} else {
@@ -193,7 +194,7 @@ public class H5QsvQuizController {
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = maxAnswerRecordOptional.get().getId();
} else {
@@ -207,7 +208,7 @@ public class H5QsvQuizController {
List<String> subjectIds = notFinishRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = notFinishRecordOptional.get().getId();
} else {
@@ -245,7 +246,7 @@ public class H5QsvQuizController {
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = maxAnswerRecordOptional.get().getId();
} else {
@@ -281,7 +282,7 @@ public class H5QsvQuizController {
List<String> subjectIds = answerRecord.getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
} else {
//今天最大那次的记录
@@ -292,7 +293,7 @@ public class H5QsvQuizController {
List<String> subjectIds = maxTodayRecord.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
} else {
throw new RuntimeException("业务异常");
@@ -306,12 +307,9 @@ public class H5QsvQuizController {
answerRecordId = answerRecord.getId();
List<String> subjectIds = answerRecord.getSubjectIds();
if(CollectionUtil.isEmpty(subjectIds)){
throw new RuntimeException("题目列表为空!请检查答题显示日期");
}
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
}
}
@@ -344,13 +342,6 @@ public class H5QsvQuizController {
float totalScore = 0;
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
QsvSubject dbSubject = dao.fetch(QsvSubject.class, subject.getId());
if (ObjectUtil.isNotEmpty(dbSubject) && "checkbox".equals(dbSubject.getType())
&& ObjectUtil.isNotEmpty(dbSubject.getMaxMulti()) && dbSubject.getMaxMulti() > 0
&& CollectionUtil.size(subject.getUserSelectOptionIds()) > dbSubject.getMaxMulti()) {
// 后端兜底校验最大可选数,避免绕过前端直接提交超限答案。
return Result.error("题目【" + dbSubject.getTitle() + "】最多只能选择" + dbSubject.getMaxMulti() + "");
}
JSONObject entries = extJson.get(subject.getId(), JSONObject.class);
if (ObjectUtil.isNotEmpty(entries)) {
entries.set("optionIds", subject.getUserSelectOptionIds());
@@ -395,7 +386,7 @@ public class H5QsvQuizController {
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.orderBy("sortNum","asc");
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
List<QsvSubject> subjects = dao.query(QsvSubject.class, cnd);
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
@@ -407,4 +398,17 @@ public class H5QsvQuizController {
return Result.success(result);
}
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
if (subjectIds == null || subjectIds.isEmpty()) {
return new ArrayList<>();
}
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
Map<String, QsvSubject> subjectMap = subjects.stream()
.collect(Collectors.toMap(QsvSubject::getId, java.util.function.Function.identity(), (oldValue, newValue) -> oldValue));
return subjectIds.stream()
.map(subjectMap::get)
.filter(ObjectUtil::isNotEmpty)
.collect(Collectors.toList());
}
}
@@ -4,7 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.result.Result;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
@@ -12,6 +12,8 @@ import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam;
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@@ -22,7 +24,11 @@ import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@IocBean
@At("/platform/h5/qsv/survey")
@@ -32,8 +38,6 @@ public class H5QsvSurveyController {
@Inject
private Dao dao;
@Inject
private ActivityBasicScopeService activityBasicScopeService;
@Inject
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
@At("")
@@ -42,29 +46,37 @@ public class H5QsvSurveyController {
}
@At
@Aop(TransAop.READ_COMMITTED)
public Result subjects(String activityId) {
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
Result activityStatusResult = checkActivityStatus(activity);
if (activityStatusResult != null) {
return activityStatusResult;
}
if (!activityBasicScopeService.isUserInGroup(activity.getGroupId(), SecurityUtil.getUserId())) {
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
.and("userId", "=", SecurityUtil.getUserId()));
if (count != 1) {
return Result.error("您无需参加此次投票,感谢您的关注!");
}
String answerRecordId = null;
List<QsvSubject> activitySubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("userId", "=", SecurityUtil.getUserId()));
if (answerRecord == null) {
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).toList());
qsvUserAnswerRecordService.insertRecord(activityId, activitySubjects.stream().map(QsvSubject::getId).toList());
}
QsvUserAnswerRecord answerRecord2 = dao.fetch(QsvUserAnswerRecord.class,
Cnd.where("activityId", "=", activityId)
.and("userId", "=", SecurityUtil.getUserId()));
syncAnswerRecordSubjects(answerRecord2, activitySubjects);
answerRecordId = answerRecord2.getId();
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", answerRecord2.getSubjectIds()).asc("sortNum"));
List<QsvSubject> subjects = querySubjectsByRecordOrder(answerRecord2.getSubjectIds());
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
for (QsvSubject subject : subjects) {
@@ -77,6 +89,95 @@ public class H5QsvSurveyController {
return Result.success(result);
}
/**
* 校验调查活动是否处于可答题状态。activity 为空表示活动不存在;
* enabled 为 false 表示活动已关闭,当前时间早于 startTime 表示未开始,当前时间晚于 endTime 表示已结束。
*
* @param activity 当前调查活动,包含开启状态、开始时间和结束时间
* @return 状态或时间不允许答题时返回错误结果,允许答题时返回 null
*/
private Result checkActivityStatus(QsvActivity activity) {
if (activity == null) {
return Result.error("调查不存在");
}
if (Boolean.FALSE.equals(activity.getEnabled())) {
return Result.error("活动已关闭,暂不能参与");
}
Date now = new Date();
if (activity.getStartTime() != null && activity.getStartTime().after(now)) {
return Result.error("调查尚未开始,请在开始后再参与");
}
if (activity.getEndTime() != null && activity.getEndTime().before(now)) {
return Result.error("调查已结束");
}
return null;
}
/**
* 同步问卷当前题目到未完成答题记录。后台新增题目后,已进入过问卷的用户记录里没有新题ID,
* 这里补齐 subjectIds 和 extJson,保证手机端能拿到最新题目并正常提交答案。
*
* @param answerRecord 用户当前问卷答题记录
* @param activitySubjects 当前活动下按排序查询出的最新题目列表
*/
private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, List<QsvSubject> activitySubjects) {
if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) {
return;
}
List<String> currentSubjectIds = activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList());
List<String> recordSubjectIds = answerRecord.getSubjectIds();
if (recordSubjectIds == null) {
recordSubjectIds = new ArrayList<>();
}
JSONObject extJson = answerRecord.getExtJson();
if (extJson == null) {
extJson = new JSONObject();
}
LinkedHashSet<String> currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds);
List<String> normalizedSubjectIds = recordSubjectIds.stream()
.filter(currentSubjectIdSet::contains)
.collect(Collectors.toList());
for (String subjectId : currentSubjectIds) {
if (!normalizedSubjectIds.contains(subjectId)) {
normalizedSubjectIds.add(subjectId);
}
}
boolean changed = !recordSubjectIds.equals(normalizedSubjectIds);
for (String subjectId : currentSubjectIds) {
if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) {
JSONObject entry = new JSONObject();
entry.set("optionIds", new ArrayList<>());
entry.set("text", null);
entry.set("optionFillContents", new JSONObject());
extJson.set(subjectId, entry);
changed = true;
}
}
if (changed) {
answerRecord.setSubjectIds(normalizedSubjectIds);
answerRecord.setExtJson(extJson);
dao.update(answerRecord);
}
}
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
if (subjectIds == null || subjectIds.isEmpty()) {
return new ArrayList<>();
}
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
Map<String, QsvSubject> subjectMap = subjects.stream()
.collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue));
return subjectIds.stream()
.map(subjectMap::get)
.filter(ObjectUtil::isNotEmpty)
.collect(Collectors.toList());
}
@At
public Result answerRecord(@Valid String answerRecordId) {
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
@@ -84,7 +185,14 @@ public class H5QsvSurveyController {
}
@At
@Aop(TransAop.READ_COMMITTED)
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId());
Result activityStatusResult = checkActivityStatus(activity);
if (activityStatusResult != null) {
return activityStatusResult;
}
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
JSONObject extJson = answerRecord.getExtJson();
@@ -93,6 +201,7 @@ public class H5QsvSurveyController {
if (ObjectUtil.isNotEmpty(entries)) {
entries.set("optionIds", subject.getUserSelectOptionIds());
entries.set("text", subject.getUserFillContent());
entries.set("optionFillContents", subject.getOptionFillContents());
}
extJson.set(subject.getId(), entries);
}
@@ -1,6 +1,8 @@
package com.budwk.app.zhgh.dayofficework.qsv.models;
import com.budwk.app.base.model.BaseModel;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.services.SysHomeConvert;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
@@ -14,7 +16,7 @@ import java.util.Date;
@Table("qsv_activity")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("问卷调查投票活动表")
public class QsvActivity extends BaseModel implements Serializable {
public class QsvActivity extends BaseModel implements Serializable, SysHomeConvert {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@@ -46,6 +48,12 @@ public class QsvActivity extends BaseModel implements Serializable {
@ColDefine(type = ColType.DATETIME)
private Date endTime;
@Column
@Comment("是否开启")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enabled;
@Column
@Comment("活动分组ID")
@ColDefine(type = ColType.INT)
@@ -107,4 +115,35 @@ public class QsvActivity extends BaseModel implements Serializable {
@Comment("封面图片")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String cover;
@Override
public Sys_home_activity covertToSysHomeActivity() {
Sys_home_activity sysHomeActivity = new Sys_home_activity();
sysHomeActivity.setId(this.getId());
sysHomeActivity.setName(this.getTitle());
sysHomeActivity.setCover(this.getCover());
sysHomeActivity.setContent(this.getDescription());
sysHomeActivity.setUrl("/platform/qsv/activity");
sysHomeActivity.setH5Url(getH5Url());
sysHomeActivity.setStartDate(this.getStartTime());
sysHomeActivity.setEndDate(this.getEndTime());
sysHomeActivity.setAllowUserGroupId(this.getGroupId());
sysHomeActivity.setEnable(!Boolean.FALSE.equals(this.getEnabled()));
sysHomeActivity.setClassPath(this.getClass().getPackageName() + this.getClass().getName());
return sysHomeActivity;
}
/**
* 根据活动类型生成移动端首页跳转地址。
* QUIZ 跳转答题页,SURVEY 跳转调查页,VOTE 预留投票页地址,返回值为移动端路由字符串。
*/
private String getH5Url() {
if ("SURVEY".equals(this.getCategory())) {
return "/platform/h5/qsv/survey?id=" + this.getId();
}
if ("VOTE".equals(this.getCategory())) {
return "/platform/h5/qsv/vote?id=" + this.getId();
}
return "/platform/h5/qsv/quiz?id=" + this.getId();
}
}
@@ -51,6 +51,16 @@ public class QsvOption extends BaseModel implements Serializable {
@ColDefine(customType = "longtext")
private String description;
@Column
@Comment("选中后是否需要填写补充内容")
@ColDefine(type = ColType.BOOLEAN)
private Boolean fillRequired;
@Column
@Comment("补充内容输入提示")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String fillPlaceholder;
@Column
@Comment("排序")
@ColDefine(type = ColType.INT)
@@ -58,11 +58,41 @@ public class QsvSubject extends BaseModel implements Serializable {
@ColDefine(type = ColType.INT, width = 1)
private Integer maxMulti;
@Column
@Comment("投票选项排列方式")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String optionLayout;
@Column
@Comment("投票选项横向列数")
@ColDefine(type = ColType.INT, width = 1)
private Integer optionColumns;
@Column
@Comment("排序")
@ColDefine(type = ColType.INT, width = 1)
private Integer sortNum;
@Column
@Comment("是否启用题目隐显逻辑")
@ColDefine(type = ColType.BOOLEAN)
private Boolean visibleRuleEnabled;
@Column
@Comment("隐显逻辑处理方式(show显示,hide隐藏)")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String visibleRuleAction;
@Column
@Comment("隐显逻辑条件关系(AND并且,OR或者)")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String visibleRuleLogic;
@Column
@Comment("隐显逻辑条件列表")
@ColDefine(type = ColType.MYSQL_JSON)
private List<VisibleRuleCondition> visibleRuleConditions;
/**
* 选项
*/
@@ -79,4 +109,52 @@ public class QsvSubject extends BaseModel implements Serializable {
*/
private String userFillContent;
/**
* 题目隐显逻辑条件,一条条件表示选择指定题目的指定选项后参与显示或隐藏判断。
*/
@Data
public static class VisibleRuleCondition implements Serializable {
/**
* 作为触发条件的题目ID。
*/
private String subjectId;
/**
* 作为触发条件的选项ID。
*/
private String optionId;
/**
* 条件节点类型:group 分组,item 条件项。
*/
private String type;
/**
* 分组内条件关系:and 并且,or 或者。
*/
private String logic;
/**
* 条件项引用的题目ID。
*/
private String field;
/**
* 比较符:==、!=、contains、>、<、>=、<=。
*/
private String operator;
/**
* 比较值,选择题为选项ID,填空题可为文本或数字。
*/
private Object value;
/**
* 子条件节点,支持条件分组嵌套。
*/
private List<VisibleRuleCondition> children;
}
}
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.dayofficework.qsv.param;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
//答题参数
@@ -21,6 +22,7 @@ public class QsvAnswerParam {
private String id;
private List<String> userSelectOptionIds;
private String userFillContent;
private Map<String, String> optionFillContents;
}
}
@@ -5,4 +5,10 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
public interface QsvActivityService extends BaseService<QsvActivity> {
QsvActivity saveActivity(QsvActivity qsvActivity);
void deleteActivity(String id);
void updateEnabled(String id, Boolean enabled);
}
@@ -12,6 +12,14 @@ public interface QsvSurveyService extends BaseService<QsvUserAnswerRecord> {
List<NutMap> report(String activityId);
List<NutMap> categoryReport(String activityId, String conditions);
NutMap crossReport(String activityId, String xSubjectIds, String ySubjectIds, String conditions);
NutMap compareReport(String activityId, String subjectId, String optionId);
List<NutMap> selectOptionUsers(String activityId, String subjectId, String optionId, String conditions);
void exportReportXlsx(String activityId, HttpServletResponse response);
void exportUserAnswerXlsx(String activityId, HttpServletResponse response);
@@ -1,15 +1,82 @@
package com.budwk.app.zhgh.dayofficework.qsv.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class QsvActivityServiceImpl extends BaseServiceImpl<QsvActivity> implements QsvActivityService {
public QsvActivityServiceImpl(Dao dao) {
super(dao);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public QsvActivity saveActivity(QsvActivity qsvActivity) {
fillQuizRepeatMode(qsvActivity);
dao().insertOrUpdate(qsvActivity);
syncHomeActivity(qsvActivity);
return qsvActivity;
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteActivity(String id) {
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", id));
List<String> subjectIds = subjects.stream().map(QsvSubject::getId).collect(Collectors.toList());
if (!subjectIds.isEmpty()) {
dao().clear(QsvOption.class, Cnd.where("subjectId", "in", subjectIds));
dao().clear(QsvSubject.class, Cnd.where("id", "in", subjectIds));
}
dao().clear(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", id));
dao().delete(QsvActivity.class, id);
dao().delete(Sys_home_activity.class, id);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateEnabled(String id, Boolean enabled) {
QsvActivity activity = dao().fetch(QsvActivity.class, id);
if (activity == null) {
return;
}
activity.setEnabled(enabled);
dao().update(activity);
syncHomeActivity(activity);
}
/**
* 答题活动的重复模式由出题模式决定。定时定题按天统计,常规模式按活动总次数统计。
*/
private void fillQuizRepeatMode(QsvActivity qsvActivity) {
if (!"QUIZ".equals(qsvActivity.getCategory())) {
return;
}
if ("SCHEDULED".equals(qsvActivity.getMode())) {
qsvActivity.setRepeatMode("DAILY");
} else if ("REGULAR".equals(qsvActivity.getMode())) {
qsvActivity.setRepeatMode("TOTAL");
}
}
/**
* 同步移动端首页活动。问卷活动保存或启停后,都以活动自身 enabled、时间、人员范围生成首页记录。
*/
private void syncHomeActivity(QsvActivity activity) {
Sys_home_activity sysHomeActivity = activity.covertToSysHomeActivity();
dao().insertOrUpdate(sysHomeActivity);
}
}
@@ -119,17 +119,11 @@ public class QsvQuizRankServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord>
cnd.andEX("t1.unionId","=",pageForm.getUnionId());
cnd.andEX("t1.unitId","=",pageForm.getUnitId());
if ("SCHEDULED".equals(mode)) {
if ("HIGH".equals(scoreStatisticsMode)) {
cnd.and("t1.isHighestScore","=",1);
} else if ("LAST".equals(scoreStatisticsMode)) {
cnd.and("t1.isLatestScore","=",1);
}
} else if ("REGULAR".equals(mode)) {
if ("HIGH".equals(scoreStatisticsMode)) {
cnd.and("t1.isHighestScore","=",1);
} else if ("LAST".equals(scoreStatisticsMode)) {
cnd.and("t1.isLatestScore","=",1);
if ("SCHEDULED".equals(mode) || "REGULAR".equals(mode)) {
if ("HIGH".equals(scoreStatisticsMode) || "HIGHEST".equals(scoreStatisticsMode)) {
cnd.and("t1.isHighestScore", "=", 1);
} else {
cnd.and("t1.isLatestScore", "=", 1);
}
}
@@ -8,6 +8,7 @@ import cn.afterturn.easypoi.excel.export.ExcelExportService;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
@@ -68,47 +69,47 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
// 处理每个题目
for (NutMap subject : subjects) {
// 获取当前题目的选项列表
List<NutMap> subjectOptions = Lang.collection2list(optionsGroup.get(subject.getString("id")), NutMap.class);
List<QsvOption> currentOptions = optionsGroup.get(subject.getString("id"));
List<NutMap> subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class);
// 获取题目类型
String subjectType = subject.getString("type");
// 处理文本类型题目
if ("text".equals(subjectType)) {
List<String> texts = answerExtList.stream()
.filter(ext -> ObjectUtil.isNull(ext.get(subject.getString("id"), JSONObject.class)))
.map(ext -> ext.get(subject.getString("id"), JSONObject.class).getStr("text"))
List<NutMap> textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id"));
List<String> texts = textAnswers.stream()
.map(textAnswer -> textAnswer.getString("text"))
.toList();
subject.put("texts", texts);
subject.put("textAnswers", textAnswers);
}
// 处理单选类型题目 处理多选类型题目
else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) {
long selectTotal = answerExtList.stream()
.filter(ext -> ext.containsKey(subject.getString("id")))
.filter(ext -> ObjectUtil.isNotEmpty(ext) && ext.containsKey(subject.getString("id")))
.count();
subjectOptions.forEach(subjectOption -> {
long selectCount = answerExtList.stream()
.filter(ext ->
{
if (ObjectUtil.isEmpty(ext)) {
return false;
}
JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class);
return jsonObject != null
&& jsonObject.getJSONArray("optionIds") != null
&& jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id"));
})
.count();
long round = Math.round((double) selectCount / selectTotal * 100);
long round = selectTotal == 0 ? 0 : Math.round((double) selectCount / selectTotal * 100);
subjectOption.put("selectPercent", round + "%");
subjectOption.put("selectCount", selectCount);
});
subjectOptions.sort((o1, o2) -> {
int count1 = o1.getInt("selectCount");
int count2 = o2.getInt("selectCount");
if (count1 == count2) {
return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum"));
}
return Integer.compare(count2, count1);
return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum"));
});
subject.put("selectTotal", selectTotal);
@@ -125,6 +126,228 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
}
@Override
public List<NutMap> categoryReport(String activityId, String conditions) {
if (activityId == null || activityId.isEmpty()) {
throw new IllegalArgumentException("activityId不能为空");
}
try {
List<NutMap> conditionList = parseCategoryConditions(conditions);
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("isFinish", "=", true));
return buildReport(activityId, filterAnswerRecords(answerRecords, conditionList));
} catch (Exception e) {
log.error("分类报告生成失败", e);
throw new RuntimeException("分类报告生成失败", e);
}
}
@Override
public NutMap crossReport(String activityId, String xSubjectIds, String ySubjectIds, String conditions) {
if (activityId == null || activityId.isEmpty()) {
throw new IllegalArgumentException("activityId不能为空");
}
List<String> xSubjectIdList = parseSubjectIds(xSubjectIds);
List<String> ySubjectIdList = parseSubjectIds(ySubjectIds);
if (ObjectUtil.isEmpty(xSubjectIdList) || ObjectUtil.isEmpty(ySubjectIdList)) {
throw new IllegalArgumentException("请选择自变量X和因变量Y");
}
if (xSubjectIdList.stream().anyMatch(ySubjectIdList::contains)) {
throw new IllegalArgumentException("自变量X和因变量Y不能选择相同变量");
}
try {
List<NutMap> subjects = querySubjects(activityId);
Map<String, NutMap> subjectMap = subjects.stream()
.collect(Collectors.toMap(subject -> subject.getString("id"), subject -> subject));
if (!subjectMap.keySet().containsAll(xSubjectIdList) || !subjectMap.keySet().containsAll(ySubjectIdList)) {
throw new IllegalArgumentException("题目不存在");
}
List<String> allSubjectIds = new ArrayList<>();
allSubjectIds.addAll(xSubjectIdList);
allSubjectIds.addAll(ySubjectIdList);
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", allSubjectIds).asc("sortNum"));
Map<String, List<QsvOption>> optionGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
List<NutMap> xOptionMaps = buildVariableOptionCombinations(xSubjectIdList, subjectMap, optionGroup);
List<NutMap> conditionList = parseCategoryConditions(conditions);
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("isFinish", "=", true));
List<QsvUserAnswerRecord> filteredRecords = filterAnswerRecords(answerRecords, conditionList);
List<NutMap> reports = new ArrayList<>();
for (String ySubjectId : ySubjectIdList) {
NutMap ySubject = subjectMap.get(ySubjectId);
List<QsvOption> yOptions = optionGroup.get(ySubjectId);
if (ObjectUtil.isEmpty(yOptions)) {
yOptions = new ArrayList<>();
}
List<NutMap> yOptionMaps = ObjectUtil.isEmpty(yOptions) ? new ArrayList<>() : Lang.collection2list(yOptions, NutMap.class);
List<NutMap> rows = new ArrayList<>();
for (NutMap xOption : xOptionMaps) {
List<NutMap> xConditions = xOption.getList("conditions", NutMap.class);
List<QsvUserAnswerRecord> xMatchedRecords = filteredRecords.stream()
.filter(record -> isRecordMatched(record, xConditions))
.toList();
long rowTotal = xMatchedRecords.size();
List<NutMap> cells = new ArrayList<>();
for (QsvOption yOption : yOptions) {
List<NutMap> yCondition = List.of(NutMap.NEW()
.addv("subjectId", ySubjectId)
.addv("optionId", yOption.getId()));
long count = xMatchedRecords.stream()
.filter(record -> isRecordMatched(record, yCondition))
.count();
double percent = rowTotal == 0 ? 0 : Math.round((double) count / rowTotal * 10000) / 100.0;
cells.add(NutMap.NEW()
.addv("yOptionId", yOption.getId())
.addv("yOptionText", yOption.getText())
.addv("count", count)
.addv("percent", percent));
}
rows.add(NutMap.NEW()
.addv("xOptionId", xOption.getString("id"))
.addv("xOptionText", xOption.getString("text"))
.addv("cells", cells)
.addv("total", rowTotal));
}
reports.add(NutMap.NEW()
.addv("ySubject", ySubject)
.addv("yOptions", yOptionMaps)
.addv("rows", rows));
}
return NutMap.NEW()
.addv("xSubject", buildVariableSubject(xSubjectIdList, subjectMap))
.addv("xOptions", xOptionMaps)
.addv("reports", reports)
.addv("total", filteredRecords.size());
} catch (Exception e) {
log.error("交叉分析报告生成失败", e);
throw new RuntimeException("交叉分析报告生成失败", e);
}
}
@Override
public NutMap compareReport(String activityId, String subjectId, String optionId) {
if (activityId == null || activityId.isEmpty()) {
throw new IllegalArgumentException("activityId不能为空");
}
if (ObjectUtil.isEmpty(subjectId) || ObjectUtil.isEmpty(optionId)) {
throw new IllegalArgumentException("请选择对比变量和选项");
}
try {
List<NutMap> subjects = querySubjects(activityId);
Map<String, NutMap> subjectMap = subjects.stream()
.collect(Collectors.toMap(subject -> subject.getString("id"), subject -> subject));
NutMap compareSubject = subjectMap.get(subjectId);
if (compareSubject == null) {
throw new IllegalArgumentException("对比变量不存在");
}
List<String> subjectIds = subjects.stream().map(subject -> subject.getString("id")).toList();
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
Map<String, List<QsvOption>> optionGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
QsvOption compareOption = optionGroup.getOrDefault(subjectId, new ArrayList<>()).stream()
.filter(option -> optionId.equals(option.getId()))
.findFirst()
.orElse(null);
if (compareOption == null) {
throw new IllegalArgumentException("对比选项不存在");
}
List<NutMap> compareCondition = List.of(NutMap.NEW()
.addv("subjectId", subjectId)
.addv("optionId", optionId));
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("isFinish", "=", true));
List<QsvUserAnswerRecord> filteredRecords = filterAnswerRecords(answerRecords, compareCondition);
List<NutMap> reports = new ArrayList<>();
for (NutMap subject : subjects) {
String currentSubjectId = subject.getString("id");
String subjectType = subject.getString("type");
if (subjectId.equals(currentSubjectId) || (!"radio".equals(subjectType) && !"checkbox".equals(subjectType))) {
continue;
}
List<QsvOption> currentOptions = optionGroup.get(currentSubjectId);
if (ObjectUtil.isEmpty(currentOptions)) {
continue;
}
List<NutMap> cells = new ArrayList<>();
long rowTotal = filteredRecords.size();
for (QsvOption option : currentOptions) {
List<NutMap> optionCondition = List.of(NutMap.NEW()
.addv("subjectId", currentSubjectId)
.addv("optionId", option.getId()));
long count = filteredRecords.stream()
.filter(record -> isRecordMatched(record, optionCondition))
.count();
double percent = rowTotal == 0 ? 0 : Math.round((double) count / rowTotal * 10000) / 100.0;
cells.add(NutMap.NEW()
.addv("yOptionId", option.getId())
.addv("yOptionText", option.getText())
.addv("count", count)
.addv("percent", percent));
}
reports.add(NutMap.NEW()
.addv("ySubject", subject)
.addv("yOptions", Lang.collection2list(currentOptions, NutMap.class))
.addv("rows", List.of(NutMap.NEW()
.addv("xOptionId", optionId)
.addv("xOptionText", compareOption.getText())
.addv("cells", cells)
.addv("total", rowTotal))));
}
return NutMap.NEW()
.addv("compareSubject", compareSubject)
.addv("compareOption", Lang.obj2nutmap(compareOption))
.addv("reports", reports)
.addv("total", filteredRecords.size());
} catch (Exception e) {
log.error("对比分析报告生成失败", e);
throw new RuntimeException("对比分析报告生成失败", e);
}
}
@Override
public List<NutMap> selectOptionUsers(String activityId, String subjectId, String optionId, String conditions) {
List<NutMap> conditionList = parseCategoryConditions(conditions);
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("isFinish", "=", true));
return filterAnswerRecords(answerRecords, conditionList).stream().map(record -> {
JSONObject extJson = record.getExtJson();
if (ObjectUtil.isEmpty(extJson)) {
return null;
}
JSONObject subjectAnswer = extJson.get(subjectId, JSONObject.class);
if (ObjectUtil.isEmpty(subjectAnswer)) {
return null;
}
JSONArray optionIds = subjectAnswer.getJSONArray("optionIds");
if (ObjectUtil.isEmpty(optionIds) || !optionIds.contains(optionId)) {
return null;
}
JSONObject optionFillContents = subjectAnswer.getJSONObject("optionFillContents");
String fillContent = ObjectUtil.isEmpty(optionFillContents) ? "" : optionFillContents.getStr(optionId);
return NutMap.NEW()
.addv("userName", record.getUserName())
.addv("loginName", record.getLoginName())
.addv("unionName", record.getUnionName())
.addv("unitName", record.getUnitName())
.addv("attemptDate", record.getAttemptDate())
.addv("submitTime", record.getSubmitTime())
.addv("fillContent", fillContent);
})
.filter(ObjectUtil::isNotNull)
.toList();
}
@Override
public void exportReportXlsx(String activityId, HttpServletResponse response) {
List<NutMap> report = report(activityId);
@@ -134,10 +357,8 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
exportEntities.add(new ExcelExportEntity("选择人数", "selectCount", 20));
exportEntities.add(new ExcelExportEntity("选择比例", "selectPercent", 20));
Map<String, List<NutMap>> listMap = report.stream().collect(Collectors.groupingBy(v -> v.getString("id")));
Workbook workbook = new XSSFWorkbook();
listMap.forEach((k, v) -> {
NutMap nutMap = v.get(0);
report.forEach(nutMap -> {
ExcelExportService service = new ExcelExportService();
ExportParams exportParams = new ExportParams();
exportParams.setTitle(nutMap.getString("title"));
@@ -171,7 +392,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
// 查询活动题目列表
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
// 将题目列表转换为Map
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
// 获取题目ID列表
@@ -185,7 +406,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
// 构建Excel导出实体
List<ExcelExportEntity> excelExportEntities = buildExcelExportEntities(subjects);
// 构建答题记录列表
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, options);
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, optionMap);
// 设置导出参数
ExportParams exportParams = new ExportParams();
@@ -223,7 +444,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
// List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
// 查询活动题目列表
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
// 将题目列表转换为Map
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
// 获取题目ID列表
@@ -245,18 +466,15 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
.addv("unitName", record.getString("unitName"))
.addv("unionName", record.getString("unionName"));
JSONObject extJson = record.getAs("extJson", JSONObject.class);
if (ObjectUtil.isEmpty(extJson)) {
return map;
}
extJson.forEach((k, v) -> {
JSONObject jsonVal = (JSONObject) v;
String type = subjectMap.get(k).getType();
if (type.equals("text")) {
map.addv(k, jsonVal.getStr("text"));
} else if (type.equals("radio") || type.equals("checkbox")) {
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
String selectOptionTexts = options.stream().filter(option -> optionIds.contains(option.getId()))
.map(QsvOption::getText).collect(Collectors.joining(";"));
map.addv(k, selectOptionTexts);
QsvSubject subject = subjectMap.get(k);
if (subject == null || !(v instanceof JSONObject jsonVal)) {
return;
}
map.addv(k, buildAnswerText(jsonVal, subject, optionMap));
});
return map;
}).toList();
@@ -278,6 +496,171 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
}
}
private List<NutMap> buildReport(String activityId, List<QsvUserAnswerRecord> answerRecords) {
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
List<NutMap> subjects = querySubjects(activityId);
List<String> subjectIds = subjects.stream().map(v -> v.getString("id")).toList();
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
Map<String, List<QsvOption>> optionsGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
for (NutMap subject : subjects) {
List<QsvOption> currentOptions = optionsGroup.get(subject.getString("id"));
List<NutMap> subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class);
String subjectType = subject.getString("type");
if ("text".equals(subjectType)) {
List<NutMap> textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id"));
List<String> texts = textAnswers.stream()
.map(textAnswer -> textAnswer.getString("text"))
.toList();
subject.put("texts", texts);
subject.put("textAnswers", textAnswers);
} else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) {
long selectTotal = answerExtList.stream()
.filter(ext -> ObjectUtil.isNotEmpty(ext) && ext.containsKey(subject.getString("id")))
.count();
subjectOptions.forEach(subjectOption -> {
long selectCount = answerExtList.stream()
.filter(ext -> {
if (ObjectUtil.isEmpty(ext)) {
return false;
}
JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class);
return jsonObject != null
&& jsonObject.getJSONArray("optionIds") != null
&& jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id"));
})
.count();
long round = selectTotal == 0 ? 0 : Math.round((double) selectCount / selectTotal * 100);
subjectOption.put("selectPercent", round + "%");
subjectOption.put("selectCount", selectCount);
});
subjectOptions.sort((o1, o2) -> Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum")));
subject.put("selectTotal", selectTotal);
}
subject.addv("options", subjectOptions);
}
return subjects;
}
private List<NutMap> parseCategoryConditions(String conditions) {
if (ObjectUtil.isEmpty(conditions)) {
return new ArrayList<>();
}
return JSONUtil.parseArray(conditions).toList(NutMap.class).stream()
.filter(condition -> ObjectUtil.isNotEmpty(condition.getString("subjectId"))
&& ObjectUtil.isNotEmpty(condition.getString("optionId")))
.toList();
}
private List<String> parseSubjectIds(String subjectIds) {
if (ObjectUtil.isEmpty(subjectIds)) {
return new ArrayList<>();
}
String value = subjectIds.trim();
List<String> result = new ArrayList<>();
if (value.startsWith("[")) {
for (Object subjectId : JSONUtil.parseArray(value)) {
if (ObjectUtil.isNotEmpty(subjectId)) {
result.add(String.valueOf(subjectId));
}
}
return result.stream().distinct().toList();
}
for (String subjectId : value.split(",")) {
if (ObjectUtil.isNotEmpty(subjectId)) {
result.add(subjectId.trim());
}
}
return result.stream().distinct().toList();
}
private NutMap buildVariableSubject(List<String> subjectIds, Map<String, NutMap> subjectMap) {
String title = subjectIds.stream()
.map(subjectId -> subjectMap.get(subjectId).getString("title"))
.collect(Collectors.joining(" / "));
return NutMap.NEW()
.addv("id", String.join("|", subjectIds))
.addv("title", title);
}
private List<NutMap> buildVariableOptionCombinations(List<String> subjectIds, Map<String, NutMap> subjectMap, Map<String, List<QsvOption>> optionGroup) {
List<List<NutMap>> subjectOptionGroups = new ArrayList<>();
for (String subjectId : subjectIds) {
NutMap subject = subjectMap.get(subjectId);
List<QsvOption> options = optionGroup.get(subjectId);
if (ObjectUtil.isEmpty(options)) {
continue;
}
List<NutMap> optionMaps = options.stream()
.map(option -> NutMap.NEW()
.addv("subjectId", subjectId)
.addv("subjectTitle", subject.getString("title"))
.addv("optionId", option.getId())
.addv("optionText", option.getText()))
.toList();
subjectOptionGroups.add(optionMaps);
}
if (subjectOptionGroups.size() != subjectIds.size()) {
return new ArrayList<>();
}
List<NutMap> result = new ArrayList<>();
buildVariableOptionCombinations(subjectOptionGroups, 0, new ArrayList<>(), result);
return result;
}
private void buildVariableOptionCombinations(List<List<NutMap>> subjectOptionGroups, int index, List<NutMap> current, List<NutMap> result) {
if (index >= subjectOptionGroups.size()) {
String id = current.stream()
.map(option -> option.getString("subjectId") + ":" + option.getString("optionId"))
.collect(Collectors.joining("|"));
String text = current.stream()
.map(option -> option.getString("optionText"))
.collect(Collectors.joining(" / "));
List<NutMap> conditions = current.stream()
.map(option -> NutMap.NEW()
.addv("subjectId", option.getString("subjectId"))
.addv("optionId", option.getString("optionId")))
.toList();
result.add(NutMap.NEW()
.addv("id", id)
.addv("text", text)
.addv("conditions", conditions));
return;
}
for (NutMap option : subjectOptionGroups.get(index)) {
current.add(option);
buildVariableOptionCombinations(subjectOptionGroups, index + 1, current, result);
current.remove(current.size() - 1);
}
}
private List<QsvUserAnswerRecord> filterAnswerRecords(List<QsvUserAnswerRecord> answerRecords, List<NutMap> conditions) {
if (ObjectUtil.isEmpty(conditions)) {
return answerRecords;
}
return answerRecords.stream()
.filter(record -> isRecordMatched(record, conditions))
.toList();
}
private boolean isRecordMatched(QsvUserAnswerRecord record, List<NutMap> conditions) {
JSONObject extJson = record.getExtJson();
if (ObjectUtil.isEmpty(extJson)) {
return false;
}
for (NutMap condition : conditions) {
JSONObject subjectAnswer = extJson.get(condition.getString("subjectId"), JSONObject.class);
if (ObjectUtil.isEmpty(subjectAnswer)) {
return false;
}
JSONArray optionIds = subjectAnswer.getJSONArray("optionIds");
if (ObjectUtil.isEmpty(optionIds) || !optionIds.contains(condition.getString("optionId"))) {
return false;
}
}
return true;
}
private List<NutMap> querySubjects(String activityId) {
Sql subjectSql = Sqls.create("""
SELECT
@@ -309,7 +692,37 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
return excelExportEntities;
}
private List<NutMap> buildAnswerRecords(List<QsvUserAnswerRecord> answerRecords, Map<String, QsvSubject> subjectMap, List<QsvOption> options) {
/**
* 构建填空题填写详情。分析弹窗需要同时展示填写内容和答题人信息,
* 因此这里从已完成答题记录中提取当前题目的文本答案,并携带姓名、工号、所属工会和单位。
*
* @param answerRecords 当前活动已完成的答题记录
* @param subjectId 当前填空题ID,用于从 extJson 中取出该题填写内容
* @return 填空题填写详情列表,每条包含 loginName、userName、unionName、unitName、submitTime、text
*/
private List<NutMap> buildTextAnswerDetails(List<QsvUserAnswerRecord> answerRecords, String subjectId) {
List<NutMap> textAnswers = new ArrayList<>();
for (QsvUserAnswerRecord answerRecord : answerRecords) {
JSONObject extJson = answerRecord.getExtJson();
if (ObjectUtil.isEmpty(extJson)) {
continue;
}
JSONObject subjectAnswer = extJson.get(subjectId, JSONObject.class);
if (ObjectUtil.isEmpty(subjectAnswer) || ObjectUtil.isEmpty(subjectAnswer.getStr("text"))) {
continue;
}
textAnswers.add(NutMap.NEW()
.addv("loginName", answerRecord.getLoginName())
.addv("userName", answerRecord.getUserName())
.addv("unionName", answerRecord.getUnionName())
.addv("unitName", answerRecord.getUnitName())
.addv("submitTime", answerRecord.getSubmitTime())
.addv("text", subjectAnswer.getStr("text")));
}
return textAnswers;
}
private List<NutMap> buildAnswerRecords(List<QsvUserAnswerRecord> answerRecords, Map<String, QsvSubject> subjectMap, Map<String, QsvOption> optionMap) {
return answerRecords.stream().map(record -> {
NutMap map = NutMap.NEW()
.addv("id", record.getId())
@@ -318,20 +731,61 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
.addv("unitName", record.getUnitName())
.addv("unionName", record.getUnionName());
JSONObject extJson = record.getExtJson();
if (ObjectUtil.isEmpty(extJson)) {
return map;
}
extJson.forEach((k, v) -> {
JSONObject jsonVal = (JSONObject) v;
String type = subjectMap.get(k).getType();
if (type.equals("text")) {
map.addv(k, jsonVal.getStr("text"));
} else if (type.equals("radio") || type.equals("checkbox")) {
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
String selectOptionTexts = options.stream().filter(option -> optionIds.contains(option.getId()))
.map(QsvOption::getText).collect(Collectors.joining(";"));
map.addv(k, selectOptionTexts);
QsvSubject subject = subjectMap.get(k);
if (subject == null || !(v instanceof JSONObject jsonVal)) {
return;
}
map.addv(k, buildAnswerText(jsonVal, subject, optionMap));
});
return map;
}).collect(Collectors.toList());
}
/**
* 组装单题答案展示内容。填空题返回填写文本;选择题返回选项文本,若选项配置了补充填写,
* 则追加对应补充内容,保证后台查看答卷和导出能看到用户填写的说明。
*
* @param jsonVal 答题记录 extJson 中当前题目的答案对象,包含 optionIds、text、optionFillContents
* @param subject 当前题目,用于判断题目类型
* @param optionMap 当前活动所有选项,key 为选项ID,value 为选项实体
* @return 当前题目的答案展示文本
*/
private String buildAnswerText(JSONObject jsonVal, QsvSubject subject, Map<String, QsvOption> optionMap) {
String type = subject.getType();
if ("text".equals(type)) {
return jsonVal.getStr("text");
}
if (!"radio".equals(type) && !"checkbox".equals(type)) {
return "";
}
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
if (ObjectUtil.isEmpty(optionIds)) {
return "";
}
JSONObject optionFillContents = jsonVal.getJSONObject("optionFillContents");
List<String> answerTexts = new ArrayList<>();
for (Object optionIdObj : optionIds) {
String optionId = String.valueOf(optionIdObj);
QsvOption option = optionMap.get(optionId);
if (option == null) {
continue;
}
String answerText = option.getText();
if (ObjectUtil.isNotEmpty(optionFillContents)) {
String fillContent = optionFillContents.getStr(optionId);
if (ObjectUtil.isNotEmpty(fillContent)) {
answerText = answerText + "" + fillContent;
}
}
answerTexts.add(answerText);
}
return String.join(";", answerTexts);
}
}
@@ -121,11 +121,12 @@ public class QsvUserAnswerRecordServiceImpl extends BaseServiceImpl<QsvUserAnswe
String mode = activity.getMode();
String scoreMode = activity.getScoreMode();
boolean highestScoreMode = "HIGH".equals(scoreMode) || "HIGHEST".equals(scoreMode);
if (mode.equals("SCHEDULED")) {
processScoreRecords(activityId, SecurityUtil.getUserId(), true, scoreMode.equals("HIGH"));
processScoreRecords(activityId, SecurityUtil.getUserId(), true, highestScoreMode);
} else if (mode.equals("REGULAR")) {
processScoreRecords(activityId, SecurityUtil.getUserId(), false, scoreMode.equals("HIGH"));
processScoreRecords(activityId, SecurityUtil.getUserId(), false, highestScoreMode);
}
}
+10 -10
View File
@@ -21,7 +21,7 @@ jetty:
#结合ftp使用,或用nginx代理ftp路径
#staticPath: /Users/wizzer/temp/files
#开发模式静态资源
staticPathLocal: E:/projects/zhgh_cug_v4/src/main/resources/static
staticPathLocal: D:/project/zhgh_cug_v4/src/main/resources/static
security:
tokenName: saToken
@@ -71,7 +71,7 @@ redis:
#nodes=192.168.6.31:6377,192.168.6.31:6378,192.168.6.28:6377,192.168.6.28:6378,192.168.6.34:6377,192.168.6.34:6378
jdbc:
url: jdbc:mysql://192.168.21.215:3306/zhgh_cug_v4?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
url: jdbc:mysql://127.0.0.1:3306/zhgh_cug_v4?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&sessionVariables=sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'
username: root
password: root
validationQuery: select 1
@@ -87,7 +87,7 @@ xsssql:
beetl:
RESOURCE:
#本地路径,开发时设置本地路径,便于调试
rootLocal: E:/projects/zhgh_cug_v4/src/main/resources/views/
rootLocal: D:/project/zhgh_cug_v4/src/main/resources/views/
root: views/
DELIMITER_STATEMENT_START: "<!--#"
DELIMITER_STATEMENT_END: "#-->"
@@ -162,16 +162,16 @@ quartz:
minio:
accessKey: minioadmin
secretKey: minioadmin
endPoint: http://192.168.21.214:9000
bucket: njupt
endPoint: http://127.0.0.1:9000
bucket: jshvc
data-center:
grant_type: password
scope: read
username: 3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD
password: 3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD
token-url: https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/oauth/token
username:
password:
token-url:
urls:
teacher: https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/get/JZGJCSJZLB
unit: https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/get/YXSDWJBSJZLB
teacher:
unit:
+24 -2
View File
@@ -365,6 +365,7 @@ layout("/layouts/v4/baseLayout.html"){
// 地址选中
hrefSelect() {
const targetHref = this.getMenuOwnerPath()
function findMenuPath(menus, targetHref) {
function search(items, path) {
for (const item of items) {
@@ -383,7 +384,7 @@ layout("/layouts/v4/baseLayout.html"){
return search(menus, []);
}
const result = findMenuPath(this.menus, window.location.pathname)
const result = findMenuPath(this.menus, targetHref)
if(result){
this.activeMenuIndex = result[result.length-1]['id']
this.openedMenus = result.map(v=>v.id)
@@ -396,6 +397,27 @@ layout("/layouts/v4/baseLayout.html"){
// 默认选中
getMenuOwnerPath() {
const pathname = window.location.pathname
if (pathname === "/platform/qsv/survey/reportPage") {
return "/platform/qsv/survey"
}
if (pathname === "/platform/qsv/new") {
return "/platform/qsv/activity"
}
return pathname
},
getMenuOwnerFullPath() {
if (window.location.pathname === "/platform/qsv/survey/reportPage") {
return "/platform/qsv/survey"
}
if (window.location.pathname === "/platform/qsv/new") {
return "/platform/qsv/activity"
}
return getFullSubAppPath()
},
defaultSelect() {
// 查找第一个有href的子菜单
function findFirstChildWithHref(menus) {
@@ -445,7 +467,7 @@ layout("/layouts/v4/baseLayout.html"){
// 页面加载时获取到正确的菜单
const pathname = window.location.pathname
if (!pathname.startsWith("/platform/v4/subApp")) {
const path = getFullSubAppPath()
const path = this.getMenuOwnerFullPath()
this.$axios.post("/platform/sys/user/rootMenuByPath", {pathname: path}).then((res) => {
if (res.code === 0) {
this.setAppInfo(res.data)
@@ -58,6 +58,17 @@ const basicForm = {
</div>
</el-form-item>
<el-form-item label="封面" prop="cover">
<file-upload
:upload_number="1"
:value.sync="formData.cover"
accept=".jpg,.jpeg,.png"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
</el-form-item>
<template v-if="formData.category==='QUIZ'">
<el-form-item label="出题模式" prop="mode">
<span slot="label">
@@ -171,17 +182,6 @@ const basicForm = {
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="封面" prop="cover">-->
<!-- <file-upload-->
<!-- :upload_number="1"-->
<!-- :value.sync="formData.cover"-->
<!-- accept=".jpg,.jpeg,.png"-->
<!-- complete_result-->
<!-- upload_mode="image"-->
<!-- upload_result_category="interval"-->
<!-- ></file-upload>-->
<!-- </el-form-item>-->
</template>
</el-form>
</div>
@@ -204,7 +204,8 @@ const basicForm = {
return {
dialogVisible: false,
formData: {
category: ''
category: '',
cover: ''
},
formRules: {
category: [{ required: true, message: "请选择类型", trigger: "change" }],
@@ -229,7 +230,7 @@ const basicForm = {
})
} else {
this.formData = {
category: 'QUIZ'
cover: ''
}
}
},
@@ -2,6 +2,382 @@
layout("/layouts/platform.html"){
#-->
<style>
.qsv-logic-drawer .el-drawer__header {
margin-bottom: 0;
padding: 18px 24px;
border-bottom: 1px solid #ebeef5;
color: #303133;
font-weight: 600;
}
.qsv-logic-drawer {
top: 64px !important;
right: 0 !important;
bottom: auto !important;
width: calc(100% - 250px) !important;
height: calc(100vh - 64px) !important;
}
.qsv-logic-drawer .el-drawer__body {
height: calc(100% - 58px);
background: #f0f2f5;
overflow: hidden;
}
.qsv-logic-workspace {
height: 100%;
display: grid;
grid-template-columns: minmax(520px, 46%) minmax(0, 1fr);
gap: 20px;
padding: 20px;
box-sizing: border-box;
background-color: #f0f2f5;
}
.qsv-logic-section {
min-width: 0;
min-height: 0;
background: #ffffff;
border: 1px solid #ebeef5;
border-radius: 4px;
display: flex;
flex-direction: column;
box-shadow: none;
}
.qsv-logic-section-header {
min-height: 58px;
padding: 0 20px;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
box-sizing: border-box;
}
.qsv-logic-section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.qsv-logic-section-header .el-button--success {
min-width: 88px;
height: 32px;
font-weight: 600;
}
.qsv-logic-question-list,
.qsv-logic-preview-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 16px;
box-sizing: border-box;
}
.qsv-logic-question-card {
position: relative;
min-height: 66px;
border: 1px solid #e4e7ed;
border-radius: 12px;
margin-bottom: 12px;
padding: 16px 20px 16px 54px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
cursor: grab;
transition: all .2s ease-in-out;
box-sizing: border-box;
background-color: #ffffff;
}
.qsv-logic-question-card:hover,
.qsv-logic-question-card.active {
border-color: #409eff;
box-shadow: 0 6px 16px rgba(64, 158, 255, .12);
transform: translateY(-2px);
background: #ffffff;
}
.qsv-logic-question-card:active {
cursor: grabbing;
}
.qsv-question-card-main {
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.qsv-question-index {
position: absolute;
left: 16px;
width: 26px;
height: 26px;
border-radius: 50%;
background: #f0f2f5;
border: 1px solid #e4e7ed;
color: #909399;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 12px;
font-weight: 600;
}
.qsv-question-type {
height: 24px;
line-height: 24px;
min-width: 70px;
padding: 0 10px;
border-radius: 4px;
color: #ffffff;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
text-align: center;
}
.qsv-question-type.single {
background: #409eff;
}
.qsv-question-type.multiple {
background: #67c23a;
}
.qsv-question-type.text {
background: #e6a23c;
}
.qsv-question-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #303133;
font-weight: 600;
font-size: 15px;
}
.qsv-question-title.has-visible-rule {
color: #f56c6c;
}
.qsv-question-actions {
display: flex;
align-items: center;
gap: 8px;
opacity: 0;
transform: scale(.9);
pointer-events: none;
transition: all .2s cubic-bezier(.175, .885, .32, 1.275);
flex-shrink: 0;
}
.qsv-logic-question-card:hover .qsv-question-actions,
.qsv-logic-question-card.active .qsv-question-actions {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
.qsv-question-actions .el-button-group .el-button {
padding: 10px 15px;
}
.qsv-preview-controls {
display: flex;
align-items: center;
gap: 12px;
}
.qsv-preview-question {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 4px 8px 24px;
margin-bottom: 8px;
box-sizing: border-box;
transition: all .2s ease;
}
.qsv-preview-question.active {
background: #f8fbff;
}
.qsv-preview-index {
min-width: 22px;
line-height: 22px;
color: #409eff;
font-size: 14px;
font-weight: 700;
text-align: right;
flex-shrink: 0;
padding-top: 1px;
}
.qsv-preview-content {
flex: 1;
min-width: 0;
}
.qsv-preview-title {
margin-bottom: 14px;
line-height: 22px;
color: #4a5568;
font-size: 14px;
}
.qsv-preview-options .el-radio,
.qsv-preview-options .el-checkbox {
margin: 0 32px 12px 0;
color: #3f4a5a;
font-weight: 500;
}
.qsv-preview-options .el-radio__label,
.qsv-preview-options .el-checkbox__label {
font-size: 14px;
}
.qsv-condition-dialog .el-dialog__body {
padding: 18px 20px 8px;
}
.qsv-condition-group {
background-color: #f9f9f5;
border: 1px solid #e8e8e0;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
box-sizing: border-box;
}
.qsv-condition-group.is-root {
box-shadow: none;
}
.qsv-condition-root-title {
margin-bottom: -4px;
font-size: 13px;
color: #606266;
}
.qsv-condition-group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 12px;
border-bottom: 1px dashed #dcdcdc;
gap: 12px;
}
.qsv-condition-group-actions {
display: flex;
align-items: center;
gap: 10px;
}
.qsv-condition-children {
display: flex;
flex-direction: column;
gap: 12px;
}
.qsv-condition-item-card {
background: #ffffff;
border: 1px solid #e4e7ed;
border-radius: 10px;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 12px;
transition: all .2s ease-in-out;
}
.qsv-condition-item-card:hover {
border-color: #dcdfe6;
box-shadow: 0 2px 10px rgba(0, 0, 0, .03);
}
.qsv-condition-item-index {
width: 24px;
height: 24px;
border-radius: 50%;
background: #f0f2f5;
border: 1px solid #e4e7ed;
color: #909399;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 12px;
}
.qsv-condition-item-content {
min-width: 0;
flex: 1;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.qsv-nested-group-wrapper {
background: #ffffff;
border: 1px solid #e4e7ed;
border-radius: 10px;
padding: 12px;
}
.qsv-nested-group-label {
margin-bottom: 10px;
padding-left: 4px;
display: flex;
align-items: center;
gap: 4px;
color: #909399;
font-size: 12px;
}
.qsv-condition-empty-tip {
padding: 28px;
text-align: center;
color: #909399;
background: #ffffff;
border: 1px dashed #dcdfe6;
border-radius: 10px;
font-size: 13px;
}
.qsv-condition-group-footer {
padding-top: 12px;
border-top: 1px dashed #dcdcdc;
display: flex;
gap: 12px;
}
.qsv-add-group-text {
color: #67c23a;
}
@media (max-width: 1200px) {
.qsv-logic-workspace {
grid-template-columns: 1fr;
}
}
</style>
<div id="app">
<guava ref="guava">
<el-card shadow="never">
@@ -10,32 +386,54 @@ layout("/layouts/platform.html"){
<el-date-picker placeholder="请选择年度" style="width: 100%" type="year" v-model="pageForm.year"
value-format="yyyy"></el-date-picker>
</search-item>
<search-item label="标题">
<el-input placeholder="请输入标题" v-model="pageForm.title" clearable></el-input>
<search-item label="标题名称">
<el-input placeholder="请输入标题名称" v-model="pageForm.title" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="管理列表">
<el-button @click="$refs.basicFormRef.onOpen()" size="small" type="primary" class="mr5">新增</el-button>
<table-tool label="标题列表">
</table-tool>
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="标题" prop="title" sortable></el-table-column>
<el-table-column label="标题名称" prop="title" sortable></el-table-column>
<el-table-column label="类型" prop="category" sortable width="200">
<template scope="{row}">
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
<el-tag size="mini" :type="getCategoryTagType(row.category)">
{{getCategoryText(row.category)}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="开始时间" prop="startTime" sortable></el-table-column>
<el-table-column label="结束时间" prop="endTime" sortable></el-table-column>
<el-table-column label="操作" fixed="right" width="450px">
<el-table-column label="状态" width="100">
<template scope="{row}">
<el-tag v-if="row.enabled !== false" type="success">开启</el-tag>
<el-tag v-else type="info">关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="580px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="openQrCode(row)">二维码</el-button>
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
<el-button
v-if="row.category === 'SURVEY'"
size="mini"
type="primary"
@click="openLogic(row.id)">逻辑</el-button>
<el-button
v-if="row.enabled === false"
size="mini"
type="success"
@click="updateEnabled(row, true)">开启</el-button>
<el-button
v-else
size="mini"
type="warning"
@click="updateEnabled(row, false)">关闭</el-button>
<el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button>
</template>
</el-table-column>
@@ -48,22 +446,22 @@ layout("/layouts/platform.html"){
</template>
</guava>
<basic-form ref="basicFormRef" @refresh="pageData"></basic-form>
<logic-form ref="logicFormRef" @refresh="pageData"></logic-form>
<qr-code-plus :url="signatureAddress" ref="qrCodePlusRef"></qr-code-plus>
</div>
<script nonce="${cspNonce!}">
<!--#include('basicForm.js'){}#-->
<!--#include('subjectForm.js'){}#-->
<!--#include('logicForm.js'){}#-->
new Vue({
el: "#app",
dicts: ["ACTIVITY_QSV_CATEGORY"],
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
"subject-form": subjectForm
"subject-form": subjectForm,
"logic-form": logicForm
},
data() {
return {
@@ -71,9 +469,24 @@ layout("/layouts/platform.html"){
}
},
methods: {
getCategoryText(category) {
const options = this.dict && this.dict.type ? (this.dict.type.ACTIVITY_QSV_CATEGORY || []) : []
const item = options.find(item => item.code === category)
return item ? item.name : category
},
getCategoryTagType(category) {
const map = {
SURVEY: "success",
QUIZ: "primary",
VOTE: "danger"
}
return map[category] || "info"
},
// 编辑
openEdit(row) {
this.$refs.basicFormRef.onOpen(row.id)
location.href = "/platform/qsv/new?id=" + row.id
},
// 打开二维码
@@ -94,9 +507,34 @@ layout("/layouts/platform.html"){
})
},
// 调查类型独立维护题目之间的显隐逻辑
openLogic(id) {
this.$refs.logicFormRef.onOpen(id)
},
// 开启或关闭活动,关闭后移动端首页不展示,直达手机端问卷也不能答题
updateEnabled(row, enabled) {
const actionText = enabled ? "开启" : "关闭"
this.$confirm("您确认" + actionText + "该活动吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
$.post("/platform/qsv/activity/updateEnabled", {
id: row.id,
enabled
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
// 删除
onDelete(id) {
this.$confirm("您确认删除吗, 是否继续?", "提示", {
this.$confirm("删除后将同步删除该任务的题目、选项、用户答题记录、统计分析和成绩数据,且不可恢复。确定删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
@@ -8,6 +8,20 @@ const setting = {
<el-form-item label="详情" prop="description">
<text-editor v-model="formData.description"></text-editor>
</el-form-item>
<el-form-item label="补充填写" prop="fillRequired">
<el-switch
v-model="formData.fillRequired"
active-text="需要填写"
inactive-text="无需填写">
</el-switch>
</el-form-item>
<el-form-item label="填写提示" prop="fillPlaceholder" v-if="formData.fillRequired">
<el-input
maxlength="255"
placeholder="例如:请填写原因"
v-model="formData.fillPlaceholder">
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="visible = false">取 消</el-button>
@@ -26,7 +40,11 @@ const setting = {
methods:{
onOpen(option,ext){
console.log(option)
this.formData = { ...option }
this.formData = {
...option,
fillRequired: !!option.fillRequired,
fillPlaceholder: option.fillPlaceholder || ""
}
this.ext = ext
this.visible = true
},
@@ -5,20 +5,20 @@
const subjectForm = {
template: /*language=HTML*/ `
<div class="subject-form-dialog">
<div class="subject-form-dialog qsv-subject-editor">
<el-form ref="form" :model="formData" :rules="formRules" label-width="120px">
<div style="min-height:50vh;overflow-y: auto">
<draggable v-model="subjects" handle=".drag-handler">
<transition-group>
<div v-for="(subject, subjectIndex) in subjects" :key="subject.id" class="subject-item">
<div class="subject-header">
<div style="display: flex; align-items: center;">
<div class="subject-title-row">
<i class="el-icon-rank drag-handler"></i>
<span style="font-weight: bold; margin-right: 10px;">第 {{subjectIndex + 1}} 题</span>
<el-input
class="subject-title-input"
v-model="subject.title"
placeholder="请输入题目"
style="flex: 1">
placeholder="请输入题目">
</el-input>
</div>
@@ -49,6 +49,27 @@ const subjectForm = {
最大选择数:
<el-input-number v-model="subject.maxMulti" placeholder="最多可选数量"></el-input-number>
</div>
<div v-if="activity.category==='VOTE'">
排列方式:
<el-select
v-model="subject.optionLayout"
@change="onVoteLayoutChange(subject)"
placeholder="请选择排列方式"
style="width: 120px;">
<el-option label="竖向排列" value="VERTICAL"></el-option>
<el-option label="横向排列" value="HORIZONTAL"></el-option>
</el-select>
</div>
<div v-if="activity.category==='VOTE' && subject.optionLayout === 'HORIZONTAL'">
横向列数:
<el-select
v-model="subject.optionColumns"
placeholder="请选择列数"
style="width: 100px;">
<el-option label="横向2列" :value="2"></el-option>
<el-option label="横向3列" :value="3"></el-option>
</el-select>
</div>
<!-- <el-input-->
<!-- v-model="subject.hint"-->
<!-- placeholder="题目提示信息"-->
@@ -91,7 +112,7 @@ const subjectForm = {
<div class="option-content">
<el-input
v-model="option.text"
:placeholder="'选项' + (optionIndex + 1)">
:placeholder="'选项'+optionIndex + 1">
<template slot="prepend">选项{{optionIndex + 1}}</template>
</el-input>
<!-- <el-input-->
@@ -119,8 +140,7 @@ const subjectForm = {
<el-switch
v-if="activity.category==='QUIZ'"
v-model="option.isCorrect"
active-text="正确答案"
@change="handleCorrectAnswerChange(subject, optionIndex)">
active-text="正确答案">
</el-switch>
<el-button
@@ -193,7 +213,7 @@ const subjectForm = {
</div>
</el-form>
<div style="border-top: 1px solid var(--border-color-lighter);padding: 12px;text-align: right;">
<div class="qsv-subject-footer">
<!-- <el-button @click="visible = false">取消</el-button>-->
<!--<el-button type="primary" icon="el-icon-plus" @click="openBank">
@@ -201,12 +221,15 @@ const subjectForm = {
</el-button>-->
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
从文本导入
导入题目
</el-button>
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
添加题目
</el-button>
<el-button type="primary" icon="el-icon-check" @click="saveQuestionnaire">
<el-button type="primary" icon="el-icon-collection" @click="openSaveBankDialog">
存到题库
</el-button>
<el-button type="primary" icon="el-icon-check" @click="saveQuestionnaire" v-if="!hideSaveButton">
保存题目
</el-button>
</div>
@@ -215,6 +238,22 @@ const subjectForm = {
<setting ref="settingRef" @confirm="onSettingConfirm"></setting>
<txt-import ref="txtImportRef" @confirm="onTxtImportConfirm"></txt-import>
<bank ref="bankRef" @confirm="onBankConfirm"></bank>
<el-dialog
:visible.sync="saveBankDialog.visible"
title="存到题库"
width="420px"
append-to-body
custom-class="save-bank-dialog">
<el-form :model="saveBankDialog.formData" :rules="saveBankDialog.rules" ref="saveBankFormRef" label-width="90px">
<el-form-item label="标题名称" prop="title">
<el-input v-model="saveBankDialog.formData.title" maxlength="50" show-word-limit placeholder="请输入题库标题"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="saveBankDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="saveBankDialog.loading" @click="saveToBank">确定</el-button>
</div>
</el-dialog>
</div>
`,
components: {
@@ -223,6 +262,12 @@ const subjectForm = {
'txt-import': txtImport,
'bank': subjectBank
},
props: {
hideSaveButton: {
type: Boolean,
default: false
}
},
data() {
return {
id: null,
@@ -231,7 +276,20 @@ const subjectForm = {
subjects: [],
previewDialogVisible: false,
activity: {},
formRules: {}
formRules: {},
saveBankDialog: {
visible: false,
loading: false,
formData: {
title: ""
},
rules: {
title: [
{ required: true, message: "请输入题库标题", trigger: "blur" },
{ max: 50, message: "标题名称不能超过50个字", trigger: "blur" }
]
}
}
}
},
methods: {
@@ -239,14 +297,19 @@ const subjectForm = {
this.visible = true
if (id) {
this.id = id
$.post("/platform/qsv/activity/findOne", {id}).then((res) => {
if (res.code === 0) {
this.activity = res.data
Promise.all([
$.post("/platform/qsv/activity/findOne", {id}),
$.post("/platform/qsv/activity/listSubjects", {activityId: id})
]).then(([activityRes, subjectsRes]) => {
if (activityRes.code === 0) {
this.activity = activityRes.data
}
})
$.post("/platform/qsv/activity/listSubjects", {activityId: id}).then((res) => {
if (res.code === 0) {
this.subjects = res.data
if (subjectsRes.code === 0) {
this.subjects = subjectsRes.data || []
this.normalizeVoteSubjectLayouts()
}
if (this.activity.category === "VOTE" && this.subjects.length === 0) {
this.addSubject()
}
})
}
@@ -266,6 +329,8 @@ const subjectForm = {
type: "radio",
score: 0,
displayDate: "",
optionLayout: "VERTICAL",
optionColumns: 1,
options: [
{
id: this.generateId(),
@@ -285,14 +350,41 @@ const subjectForm = {
})
},
//题目类型切换
subjectTypeChange(subject, subjectIndex) {
// 切换题型时先清空当前题目的正确答案状态,避免沿用旧题型下的答案配置。
;(subject.options || []).forEach((option) => {
if (option) {
this.$set(option, 'isCorrect', false)
}
// 投票选项排列默认值
normalizeVoteSubjectLayout(subject) {
if (!subject) {
return
}
if (!subject.optionLayout) {
this.$set(subject, "optionLayout", "VERTICAL")
}
if (subject.optionLayout === "HORIZONTAL") {
const columns = Number(subject.optionColumns)
this.$set(subject, "optionColumns", columns === 3 ? 3 : 2)
} else {
this.$set(subject, "optionLayout", "VERTICAL")
this.$set(subject, "optionColumns", 1)
}
},
normalizeVoteSubjectLayouts() {
if (this.activity.category !== "VOTE") {
return
}
this.subjects.forEach((subject) => {
this.normalizeVoteSubjectLayout(subject)
})
},
onVoteLayoutChange(subject) {
if (subject.optionLayout === "HORIZONTAL") {
this.$set(subject, "optionColumns", Number(subject.optionColumns) === 3 ? 3 : 2)
} else {
this.$set(subject, "optionColumns", 1)
}
},
subjectTypeChange(subject, subjectIndex) {
if (subject.type === 'text') {
subject.options = []
}
@@ -317,7 +409,7 @@ const subjectForm = {
addOption(subject) {
subject.options.push({
id: this.generateId(),
text: "选项" + (subject.options.length + 1),
text: "选项" + subject.options.length + 1,
hint: "",
imgUrl: null,
isCorrect: false
@@ -358,7 +450,7 @@ const subjectForm = {
//选项设置确认
onSettingConfirm({option, ext}) {
this.subjects[ext.subjectIndex].options[ext.optionIndex] = option
this.$set(this.subjects[ext.subjectIndex].options, ext.optionIndex, option)
console.log(this.subjects[ext.subjectIndex].options[ext.optionIndex])
// this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "isCorrect", option.isCorrect)
@@ -367,7 +459,7 @@ const subjectForm = {
//从文本导入
openTxtImport() {
this.$refs.txtImportRef.visible = true;
this.$refs.txtImportRef.onOpen(this.activity.category);
},
//处理导入的题目
@@ -382,6 +474,9 @@ const subjectForm = {
// 更新导入题目的序号
importedQuestions.forEach((question, index) => {
question.sortNum = startSortNum + index;
if (this.activity.category === "VOTE") {
this.normalizeVoteSubjectLayout(question)
}
});
// 将导入的题目添加到现有题目列表
@@ -400,67 +495,129 @@ const subjectForm = {
},
// 单选题切换正确答案时保持只有一个正确选项,避免界面状态与题型规则不一致。
handleCorrectAnswerChange(subject, optionIndex) {
if (!subject || subject.type !== 'radio') {
// 打开存到题库对话框
openSaveBankDialog() {
if (!this.subjects || this.subjects.length === 0) {
this.$message.warning("请先添加题目")
return
}
const options = subject.options || []
const currentOption = options[optionIndex]
if (!currentOption || !currentOption.isCorrect) {
return
}
for (let index = 0; index < options.length; index++) {
if (index !== optionIndex) {
this.$set(options[index], 'isCorrect', false)
this.saveBankDialog.formData.title = this.activity && this.activity.title ? this.activity.title : ""
this.saveBankDialog.visible = true
this.$nextTick(() => {
if (this.$refs.saveBankFormRef) {
this.$refs.saveBankFormRef.clearValidate()
}
}
})
},
// 保存前校验选项内容,避免题目存在空选项或缺少正确答案时仍然提交到后端。
validateSubjectOptions() {
for (let subjectIndex = 0; subjectIndex < this.subjects.length; subjectIndex++) {
const subject = this.subjects[subjectIndex]
if (!subject || subject.type === 'text') {
continue
}
const options = subject.options || []
if (options.length === 0) {
this.$message.warning('请设置第' + (subjectIndex + 1) + '题的选项')
return false
}
let correctAnswerCount = 0
for (let optionIndex = 0; optionIndex < options.length; optionIndex++) {
const option = options[optionIndex]
const optionText = option && option.text ? option.text.trim() : ''
if (!optionText) {
this.$message.warning('第' + (subjectIndex + 1) + '题的第' + (optionIndex + 1) + '个选项不能为空')
return false
}
if (option && option.isCorrect) {
correctAnswerCount++
}
}
if (this.activity.category === 'QUIZ') {
if (subject.type === 'radio' && correctAnswerCount > 1) {
this.$message.warning('第' + (subjectIndex + 1) + '题为单选题,只能设置一个正确答案')
return false
}
if (correctAnswerCount === 0) {
this.$message.warning('第' + (subjectIndex + 1) + '题请至少设置一个正确答案')
return false
}
// 复制题目到题库时重新生成ID,避免与活动题目关联
buildBankSubjects() {
const subjects = JSON.parse(JSON.stringify(this.subjects || []))
const subjectIdMap = {}
const optionIdMap = {}
const remapValue = (value, map) => {
if (Array.isArray(value)) {
return value.map((item) => map[item] || item)
}
return map[value] || value
}
return true
const remapVisibleConditions = (conditions) => {
if (!Array.isArray(conditions)) {
return conditions
}
return conditions.map((condition) => {
if (condition.subjectId) {
condition.subjectId = subjectIdMap[condition.subjectId] || condition.subjectId
}
if (condition.field) {
condition.field = subjectIdMap[condition.field] || condition.field
}
if (condition.optionId) {
condition.optionId = optionIdMap[condition.optionId] || condition.optionId
}
if (Object.prototype.hasOwnProperty.call(condition, "value")) {
condition.value = remapValue(condition.value, optionIdMap)
}
if (Array.isArray(condition.children)) {
condition.children = remapVisibleConditions(condition.children)
}
return condition
})
}
subjects.forEach((subject) => {
const oldSubjectId = subject.id
const newSubjectId = this.generateId()
subject._bankNewSubjectId = newSubjectId
if (oldSubjectId) {
subjectIdMap[oldSubjectId] = newSubjectId
}
if (Array.isArray(subject.options)) {
subject.options.forEach((option) => {
const oldOptionId = option.id
const newOptionId = this.generateId()
option._bankNewOptionId = newOptionId
if (oldOptionId) {
optionIdMap[oldOptionId] = newOptionId
}
})
}
})
return subjects.map((subject, index) => {
const newSubjectId = this.generateId()
subject.id = subject._bankNewSubjectId || newSubjectId
delete subject._bankNewSubjectId
subject.activityId = null
subject.sortNum = index + 1
if (Array.isArray(subject.options)) {
subject.options = subject.options.map((option, optionIndex) => {
option.id = option._bankNewOptionId || this.generateId()
delete option._bankNewOptionId
option.subjectId = subject.id
option.sortNum = optionIndex + 1
return option
})
}
if (Array.isArray(subject.correctAnswer)) {
subject.correctAnswer = remapValue(subject.correctAnswer, optionIdMap)
}
if (Array.isArray(subject.visibleRuleConditions)) {
subject.visibleRuleConditions = remapVisibleConditions(subject.visibleRuleConditions)
}
return subject
})
},
// 保存当前全部题目到题库
saveToBank() {
this.$refs.saveBankFormRef.validate((valid) => {
if (!valid) {
return
}
const qsvBank = {
title: this.saveBankDialog.formData.title,
category: this.activity.category,
subjects: this.buildBankSubjects()
}
this.saveBankDialog.loading = true
$.post("/platform/qsv/bank/save", { qsvBank: JSON.stringify(qsvBank) }).then((res) => {
this.saveBankDialog.loading = false
if (res.code === 0) {
this.$message.success("已存到题库")
this.saveBankDialog.visible = false
}
}).fail(() => {
this.saveBankDialog.loading = false
})
})
},
//保存
saveQuestionnaire() {
if (!this.validateSubjectOptions()) {
return
}
$.post("/platform/qsv/activity/saveSubjects", {
this.normalizeVoteSubjectLayouts()
$
.post("/platform/qsv/activity/saveSubjects", {
activityId: this.id,
subjects: JSON.stringify(this.subjects)
})
@@ -474,6 +631,38 @@ const subjectForm = {
}
},
style: /*language=CSS*/ `
/deep/ .qsv-subject-editor {
padding-bottom: 72px;
}
/deep/ .qsv-subject-footer {
position: fixed;
left: 0;
width: 100%;
right: 0;
bottom: 0;
z-index: 900;
padding: 12px 24px;
border-top: 1px solid #ebeef5;
background: #ffffff;
box-shadow: 0 -2px 10px rgba(0, 0, 0, .04);
text-align: right;
box-sizing: border-box;
}
@media (max-width: 768px) {
/deep/ .qsv-subject-footer {
left: 0;
width: 100%;
}
}
/deep/ .save-bank-dialog {
top: 50%;
margin-top: 0 !important;
transform: translateY(calc(-50% + 250px));
}
/*.subject-form-dialog,.subject-form-dialog .el-dialog__body {*/
/* background: rgb(248, 249, 250);*/
/*}*/
@@ -513,6 +702,15 @@ const subjectForm = {
margin-bottom: 15px;
}
/deep/ .subject-title-row {
display: flex;
align-items: center;
}
/deep/ .subject-title-input {
flex: 1;
}
/deep/ .subject-toolbar {
display: flex;
justify-content: space-between;
File diff suppressed because it is too large Load Diff
@@ -1,40 +1,52 @@
const basicForm = {
template: /*language=HTML*/ `
<el-dialog :visible.sync="dialogVisible" title="基础设置" width="70%">
<div style="overflow-y: auto">
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="150px">
<el-form-item label="类型" prop="category">
<el-radio-group v-model="formData.category" size="small">
<el-radio v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :label="item.code"
:key="item.code"
border>
{{item.label}}
</el-radio>
</el-radio-group>
</el-form-item>
<el-drawer
:title="formData.id ? '编辑题库' : '新增题库'"
:visible.sync="dialogVisible"
direction="rtl"
size="1024px"
custom-class="qsv-bank-basic-drawer"
append-to-body
:wrapper-closable="false"
:close-on-press-escape="false">
<div class="qsv-bank-basic-drawer-page" v-loading="saveLoading">
<el-card shadow="never" class="qsv-bank-basic-drawer-card">
<h3 class="qsv-bank-sub-title">基础信息</h3>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="96px">
<el-form-item label="类型" prop="category">
<el-radio-group v-model="formData.category" size="small">
<el-radio v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :label="item.code"
:key="item.code"
border>
{{item.label || item.name}}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" maxlength="50"
show-word-limit></el-input>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" maxlength="50"
show-word-limit></el-input>
</el-form-item>
<el-form-item label="说明" prop="description">
<text-editor v-model="formData.description" :height="150"></text-editor>
</el-form-item>
</el-form>
<el-form-item label="说明" prop="description">
<text-editor v-model="formData.description" :height="180"></text-editor>
</el-form-item>
</el-form>
</el-card>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="onSubmit">确 定</el-button>
<div class="qsv-bank-basic-drawer-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saveLoading" @click="onSubmit">保存</el-button>
</div>
</el-dialog>
</el-drawer>
`,
dicts: ["ACTIVITY_QSV_CATEGORY", "ACTIVITY_QSV_MODE", "ACTIVITY_QSV_REPEAT_MODE", "ACTIVITY_QSV_SCORE_MODE"],
data() {
return {
dialogVisible: false,
saveLoading: false,
formData: {
category: ''
category: 'QUIZ'
},
formRules: {
category: [{required: true, message: "请选择类型", trigger: "change"}],
@@ -45,27 +57,42 @@ const basicForm = {
methods: {
onOpen(id) {
this.dialogVisible = true
this.saveLoading = false
if (id) {
$.post("/platform/qsv/bank/findOne", {id}).then((res) => {
if (res.code === 0) {
this.formData = res.data
this.formData = res.data || {category: 'QUIZ'}
}
})
} else {
this.formData = {}
this.formData = {
category: 'QUIZ'
}
this.$nextTick(() => {
if (this.$refs.formRef) {
this.$refs.formRef.clearValidate()
}
})
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
$.post("/platform/qsv/bank/" + (this.formData.id ? "update" : "save"), {qsvBank: JSON.stringify(this.formData)}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.dialogVisible = false
this.$emit("refresh", null)
}
})
if (!valid) {
return
}
this.saveLoading = true
$.post("/platform/qsv/bank/" + (this.formData.id ? "update" : "save"), {
qsvBank: JSON.stringify(this.formData)
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg || "保存成功")
this.dialogVisible = false
this.$emit("refresh", null)
}
this.saveLoading = false
}).fail(() => {
this.saveLoading = false
})
})
}
}
@@ -2,30 +2,152 @@
layout("/layouts/platform.html"){
#-->
<style>
.el-drawer.qsv-bank-drawer {
top: 64px !important;
right: 0 !important;
bottom: auto !important;
width: calc(100% - 250px) !important;
height: calc(100vh - 64px) !important;
display: flex;
flex-direction: column;
}
.qsv-bank-drawer .el-drawer__header {
margin-bottom: 0;
padding: 18px 24px;
border-bottom: 1px solid #ebeef5;
color: #303133;
font-weight: 600;
}
.qsv-bank-drawer .el-drawer__body {
flex: 1;
min-height: 0;
padding: 0;
background: #f0f2f5;
overflow: hidden;
}
.qsv-bank-drawer-page {
height: 100%;
padding: 16px 24px 72px;
box-sizing: border-box;
overflow-y: auto;
}
.qsv-bank-drawer-card {
min-height: 100%;
}
.qsv-bank-sub-title {
margin: 0 0 18px;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.qsv-bank-drawer-footer {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
padding: 12px 24px;
border-top: 1px solid #ebeef5;
background: #ffffff;
box-shadow: 0 -2px 10px rgba(0, 0, 0, .04);
text-align: right;
box-sizing: border-box;
}
.el-drawer.qsv-bank-basic-drawer {
top: 64px !important;
right: 0 !important;
bottom: auto !important;
width: 1024px !important;
height: calc(100vh - 64px) !important;
display: flex;
flex-direction: column;
}
.qsv-bank-basic-drawer .el-drawer__header {
margin-bottom: 0;
padding: 18px 24px;
border-bottom: 1px solid #ebeef5;
color: #303133;
font-weight: 600;
}
.qsv-bank-basic-drawer .el-drawer__body {
flex: 1;
min-height: 0;
padding: 0;
background: #f0f2f5;
overflow: hidden;
}
.qsv-bank-basic-drawer-page {
height: 100%;
padding: 16px 16px 72px;
box-sizing: border-box;
overflow-y: auto;
}
.qsv-bank-basic-drawer-card {
min-height: 100%;
}
.qsv-bank-basic-drawer-footer {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
padding: 12px 24px;
border-top: 1px solid #ebeef5;
background: #ffffff;
box-shadow: 0 -2px 10px rgba(0, 0, 0, .04);
text-align: right;
box-sizing: border-box;
}
@media (max-width: 768px) {
.el-drawer.qsv-bank-drawer,
.el-drawer.qsv-bank-basic-drawer {
top: 0 !important;
width: 100% !important;
height: 100vh !important;
}
}
</style>
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="标题">
<el-input placeholder="请输入标题" v-model="pageForm.title" clearable></el-input>
<search-item label="标题名称">
<el-input placeholder="请输入标题名称" v-model="pageForm.title" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="管理列表">
<table-tool label="题库列表">
<el-button @click="$refs.basicFormRef.onOpen()" size="small" type="primary" class="mr5">新增</el-button>
</table-tool>
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="标题" prop="title" sortable></el-table-column>
<el-table-column label="标题名称" prop="title" sortable></el-table-column>
<el-table-column label="类型" prop="category" sortable width="200">
<template scope="{row}">
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
<el-tag size="mini" :type="getCategoryTagType(row.category)">
{{getCategoryText(row.category)}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="450px">
<el-table-column label="操作" width="450px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
@@ -61,6 +183,21 @@ layout("/layouts/platform.html"){
}
},
methods: {
getCategoryText(category) {
const options = this.dict && this.dict.type ? (this.dict.type.ACTIVITY_QSV_CATEGORY || []) : []
const item = options.find(item => item.code === category)
return item ? item.name : category
},
getCategoryTagType(category) {
const map = {
SURVEY: "success",
QUIZ: "primary",
VOTE: "danger"
}
return map[category] || "info"
},
// 编辑
openEdit(row) {
this.$refs.basicFormRef.onOpen(row.id)
@@ -80,7 +217,7 @@ layout("/layouts/platform.html"){
cancelButtonText: "取消",
type: "warning"
}).then(() => {
$.post("/platform/qsv/activity/delete", {id}).then((res) => {
$.post("/platform/qsv/bank/delete", {id}).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
@@ -4,7 +4,7 @@
const subjectForm = {
template: /*language=HTML*/ `
<div class="subject-form-dialog">
<div class="subject-form-dialog qsv-subject-editor">
<el-form ref="form" :model="formData" :rules="formRules" label-width="120px">
<div style="min-height:50vh;overflow-y: auto">
<draggable v-model="subjects" handle=".drag-handler">
@@ -192,10 +192,10 @@ const subjectForm = {
</div>
</el-form>
<div style="border-top: 1px solid var(--border-color-lighter);padding: 12px;text-align: right;">
<div class="qsv-subject-footer">
<!-- <el-button @click="visible = false">取消</el-button>-->
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
从文本导入
导入题目
</el-button>
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
添加题目
@@ -349,7 +349,9 @@ const subjectForm = {
//从文本导入
openTxtImport() {
this.$refs.txtImportRef.visible = true;
this.$refs.txtImportRef.onOpen(this.activity.category, {
excludeBankId: this.id
});
},
//处理导入的题目
@@ -387,6 +389,32 @@ const subjectForm = {
}
},
style: /*language=CSS*/ `
/deep/ .qsv-subject-editor {
padding-bottom: 72px;
}
/deep/ .qsv-subject-footer {
position: fixed;
left: 0;
width: 100%;
right: 0;
bottom: 0;
z-index: 900;
padding: 12px 24px;
border-top: 1px solid #ebeef5;
background: #ffffff;
box-shadow: 0 -2px 10px rgba(0, 0, 0, .04);
text-align: right;
box-sizing: border-box;
}
@media (max-width: 768px) {
/deep/ .qsv-subject-footer {
left: 0;
width: 100%;
}
}
/*.subject-form-dialog,.subject-form-dialog .el-dialog__body {*/
/* background: rgb(248, 249, 250);*/
/*}*/
@@ -54,7 +54,7 @@ layout("/layouts/platform.html"){
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="管理列表">
<table-tool label="统计列表">
<el-button @click="exportXlsx" icon="el-icon-download" size="small" type="primary" class="mr5">
导出xlsx
</el-button>
@@ -5,7 +5,7 @@ const answer = {
<el-row type="flex" justify="end" class="mb10">
<el-button type="primary" size="small" icon="el-icon-download" @click="exportUserAnswerXlsx">导出xlsx</el-button>
</el-row>
<el-table :data="tableData">
<el-table :data="tableData" v-loading="tableLoading">
<el-table-column label="序号" type="index" width="70"></el-table-column>
<el-table-column v-for="item in tableColumns" :key="item.prop" :label="item.label" :prop="item.prop"></el-table-column>
<el-table-column label="操作" fixed="right" width="100px">
@@ -30,17 +30,24 @@ const answer = {
onOpen(activityId) {
this.activityId = activityId
this.dialogVisible = true
this.tableColumns = []
this.tableData = []
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.pageData()
},
pageData() {
this.pageForm.activityId = this.activityId
this.tableLoading = true
$.post("/platform/qsv/survey/userAnswer", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableColumns = res.data.tableColumns
this.tableData = res.data.tableData.list
this.pageForm.totalCount = res.data.tableData.totalCount
}
}).always(() => {
this.tableLoading = false
})
},
@@ -53,7 +60,7 @@ const answer = {
$.post("/platform/qsv/survey/deleteUserAnswer", { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()()
this.pageData()
}
})
})
@@ -2,6 +2,42 @@
layout("/layouts/platform.html"){
#-->
<style>
.survey-report-dialog {
margin-top: 40px !important;
height: calc(100vh - 80px);
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.survey-report-dialog .el-dialog__header {
flex: 0 0 auto;
}
.survey-report-dialog .el-dialog__body {
flex: 1;
min-height: 0;
overflow: hidden;
padding-top: 10px;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.survey-report-toolbar {
flex: 0 0 auto;
}
.survey-report-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 8px;
box-sizing: border-box;
}
</style>
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
@@ -24,17 +60,19 @@ layout("/layouts/platform.html"){
</table-tool>
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="标题" prop="title" width="320" sortable></el-table-column>
<el-table-column label="标题名称" prop="title" width="320" sortable></el-table-column>
<el-table-column label="类型" prop="category" sortable>
<template scope="{row}">
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
<el-tag size="mini" :type="getCategoryTagType(row.category)">
{{getCategoryText(row.category)}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="开始时间" prop="startTime" sortable></el-table-column>
<el-table-column label="结束时间" prop="endTime" sortable></el-table-column>
<el-table-column label="操作" fixed="right" width="200px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="$refs.reportRef.onOpen(row.id)">查看分析
<el-button size="mini" type="primary" @click="openReportPage(row.id)">查看分析
</el-button>
<el-button size="mini" type="primary" @click="$refs.answerRef.onOpen(row.id)">查看答卷
</el-button>
@@ -44,22 +82,38 @@ layout("/layouts/platform.html"){
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<report ref="reportRef"></report>
<answer ref="answerRef"></answer>
</div>
<script nonce="${cspNonce!}">
<!--#include('report.js'){}#-->
<!--#include('answer.js'){}#-->
new Vue({
el: "#app",
dicts: ["ACTIVITY_QSV_CATEGORY"],
components: {report, answer},
components: {answer},
mixins: [initTableMixins],
data() {
return {}
},
methods: {
getCategoryText(category) {
const options = this.dict && this.dict.type ? (this.dict.type.ACTIVITY_QSV_CATEGORY || []) : []
const item = options.find(item => item.code === category)
return item ? item.name : category
},
getCategoryTagType(category) {
const map = {
SURVEY: "success",
QUIZ: "primary",
VOTE: "danger"
}
return map[category] || "info"
},
openReportPage(activityId) {
window.location.href = "/platform/qsv/survey/reportPage?activityId=" + encodeURIComponent(activityId)
},
exportXlsx() {
this.$downLoad("/platform/qsv/survey/exportXlsx", this.pageForm)
}
@@ -1,41 +1,44 @@
const report = {
/*language=HTML*/
template: `
<el-dialog title="分析报告" :visible.sync="dialogVisible" width="50%">
<el-row type="flex" justify="end" class="mb10">
<el-button type="primary" size="small" icon="el-icon-download" @click="exportReportXlsx">导出xlsx</el-button>
</el-row>
<div v-for="(subject,subjectIndex) in subjects" class="subject">
<div>
<div class="title">
第{{subjectIndex+1}}题: {{subject.title}}
<span class="type" v-if="subject.type==='text'">[填空题]</span>
<span class="type" v-else-if="subject.type==='radio'">[单选题]</span>
<span class="type" v-else-if="subject.type==='text'">[多选题]</span>
</div>
<el-dialog title="分析报告" :visible.sync="dialogVisible" width="50%" custom-class="survey-report-dialog">
<div class="survey-report-toolbar">
<el-row type="flex" justify="end" class="mb10">
<el-button type="primary" size="small" icon="el-icon-download" @click="exportReportXlsx">导出xlsx</el-button>
</el-row>
</div>
<div class="survey-report-body">
<div v-for="(subject,subjectIndex) in subjects" class="subject">
<div>
<div class="title">
第{{subjectIndex+1}}题: {{subject.title}}
<span class="type" v-if="subject.type==='text'">[填空题]</span>
<span class="type" v-else-if="subject.type==='radio'">[单选题]</span>
<span class="type" v-else-if="subject.type==='checkbox'">[多选题]</span>
</div>
<div v-if="subject.type==='text'">
{{subject.texts.join('、')}}
</div>
<div v-if="subject.type==='text'">
<el-button type="text" @click="openTextDetail(subject)">查看填写内容</el-button>
</div>
<div v-else-if="subject.type==='radio' || subject.type==='checkbox'">
<el-table :data="subject.options" size="small">
<el-table-column label="选项" prop="text"></el-table-column>
<el-table-column label="小计" prop="selectCount" width="100"></el-table-column>
<el-table-column label="比例" width="500">
<template slot-scope="scope">
<el-progress
:percentage="Math.round(scope.row.selectCount / subject.selectTotal * 100)"></el-progress>
</template>
</el-table-column>
<el-table-column label="详情" width="100px">
<template slot-scope="{row}">
<el-button type="text" @click="openDetail(subject.id,row.id)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div v-else-if="subject.type==='radio' || subject.type==='checkbox'">
<el-table :data="subject.options" size="small">
<el-table-column label="选项" prop="text"></el-table-column>
<el-table-column label="小计" prop="selectCount" width="100"></el-table-column>
<el-table-column label="比例" width="500">
<template slot-scope="scope">
<el-progress
:percentage="Math.round(scope.row.selectCount / subject.selectTotal * 100)"></el-progress>
</template>
</el-table-column>
<el-table-column label="详情" width="100px">
<template slot-scope="{row}">
<el-button type="text" @click="openDetail(subject.id,row.id)">详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
@@ -45,9 +48,22 @@ const report = {
<el-table-column label="工号" prop="loginName"></el-table-column>
<el-table-column label="所属工会" prop="unionName"></el-table-column>
<el-table-column label="所属单位" prop="unitName"></el-table-column>
<el-table-column label="补充理由" prop="fillContent" show-overflow-tooltip></el-table-column>
<el-table-column label="选择时间" prop="attemptDate"></el-table-column>
</el-table>
</el-dialog>
<el-dialog :title="textSubject.title || '填写内容'" :visible.sync="textVisible" width="50%" append-to-body>
<el-table :data="textAnswers" size="small">
<el-table-column label="序号" type="index" width="70"></el-table-column>
<el-table-column label="姓名" prop="userName" width="100"></el-table-column>
<el-table-column label="工号" prop="loginName" width="120"></el-table-column>
<el-table-column label="所属工会" prop="unionName" width="140"></el-table-column>
<el-table-column label="所属单位" prop="unitName" width="180" show-overflow-tooltip></el-table-column>
<el-table-column label="填写内容" prop="text" show-overflow-tooltip></el-table-column>
<el-table-column label="提交时间" prop="submitTime" width="160"></el-table-column>
</el-table>
</el-dialog>
</el-dialog>
`,
@@ -59,7 +75,10 @@ const report = {
report: null,
subjects: [],
optionVisible: false,
optionUsers: []
optionUsers: [],
textVisible: false,
textSubject: {},
textAnswers: []
}
},
methods: {
@@ -73,8 +92,6 @@ const report = {
})
},
openDetail(subjectId, optionId) {
console.log(subjectId)
console.log(optionId)
$.post("/platform/qsv/survey/selectOptionUsers", {
activityId: this.activityId,
subjectId,
@@ -87,6 +104,13 @@ const report = {
}
})
},
openTextDetail(subject) {
this.textSubject = subject
this.textAnswers = subject.textAnswers || (subject.texts || []).map((text) => {
return { text }
})
this.textVisible = true
},
exportReportXlsx(){
this.$downLoad("/platform/qsv/survey/exportReportXlsx", { activityId: this.activityId })
}
@@ -2,30 +2,71 @@
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="问卷列表" placeholder fixed></van-nav-bar>
<div id="app" v-cloak class="qsv-h5-page">
<div class="qsv-h5-header">
<div class="qsv-h5-nav">
<button class="qsv-h5-back-btn" @click="historyBack">
<i class="fa fa-angle-left"></i>
</button>
<div class="qsv-h5-nav-title">问卷服务</div>
<div class="qsv-h5-nav-placeholder"></div>
</div>
<div class="qsv-h5-search-row">
<van-search
v-model="pageForm.title"
shape="round"
background="transparent"
placeholder="搜索标题名称"
clearable
@search="onRefresh"
@clear="onRefresh">
</van-search>
<button class="qsv-h5-search-btn" @click="onRefresh">
<i class="fa fa-search"></i>
</button>
</div>
<div class="qsv-h5-filter-row">
<button
v-for="item in categoryOptions"
:key="item.value"
:class="['qsv-h5-filter-btn', pageForm.category === item.value ? 'active' : '']"
@click="changeCategory(item.value)">
<i :class="pageForm.category === item.value ? 'fa fa-check-circle' : 'fa fa-circle-o'"></i>
<span>{{item.label}}</span>
</button>
</div>
</div>
<van-sticky offset-top="46px">
<van-tabs v-model="pageForm.category" @change="doSearch">
<van-tab v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :name="item.code" :title="item.label"
:key="item.code"></van-tab>
</van-tabs>
</van-sticky>
<table-list api="/platform/h5/qsv/pageData" :page_form.sync="pageForm" ref="tableListRef" title="title"
@ready="doSearch">
<!-- <template #header="{index,row}"></template>-->
<template v-slot="{index,row}">
<table-column label="开始时间">{{row.startTime}}</table-column>
<table-column label="结束时间">{{row.endTime}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onEnter(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" class="qsv-h5-list-wrap">
<van-list v-model="loading" :finished="finished" finished-text="暂无更多内容" @load="onLoad">
<div
v-for="row in list"
:key="row.id"
class="qsv-h5-card"
@click="onEnter(row)">
<div class="qsv-h5-cover">
<img :src="row.cover || defaultCover" alt="">
</div>
<div class="qsv-h5-card-body">
<div class="qsv-h5-title-row">
<div class="qsv-h5-title">{{row.title}}</div>
<div class="qsv-h5-answered" v-if="row.answeredText || row.isAnswered">{{row.answeredText || getAnsweredText(row.category)}}</div>
</div>
<div class="qsv-h5-meta-row">
<div class="qsv-h5-type">
<i :class="getCategoryIcon(row.category)"></i>
<span>{{getCategoryText(row.category)}}</span>
</div>
</div>
<div class="qsv-h5-time">
<div>开始时间:{{formatDateTime(row.startTime)}}</div>
<div>结束时间:{{formatDateTime(row.endTime)}}</div>
</div>
</div>
</div>
</template>
</table-list>
<van-empty v-if="!loading && list.length === 0" description="暂无内容"></van-empty>
</van-list>
</van-pull-refresh>
</div>
<script nonce="${cspNonce!}">
@@ -35,12 +76,26 @@ layout("/layouts/platform_h5.html"){
store,
data() {
return {
loading: false,
requesting: false,
finished: false,
refreshing: false,
cacheRestored: false,
list: [],
defaultCover: "/assets/mobile/svg/qsv/icon.svg",
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
category: "QUIZ"
title: "",
category: ""
},
categoryOptions: [
{label: "全部", value: ""},
{label: "答题", value: "QUIZ"},
{label: "问卷", value: "SURVEY"},
{label: "投票", value: "VOTE"}
],
categoryPaths: {
QUIZ: "/platform/h5/qsv/quiz",
SURVEY: "/platform/h5/qsv/survey",
@@ -50,30 +105,114 @@ layout("/layouts/platform_h5.html"){
},
methods: {
historyBack() {
this.$pjaxReplace('/platform/h5/home')
this.$pjaxReplace('/platform/h5/home')
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
changeCategory(category) {
if (this.pageForm.category === category) {
return
}
this.pageForm.category = category
this.onRefresh()
},
onRefresh() {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.list = []
this.finished = false
this.clearListCache()
this.onLoad()
},
onLoad() {
if (this.requesting || this.finished) {
return
}
this.requesting = true
this.loading = true
this.$axios.post("/platform/h5/qsv/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
const pageData = res.data || {}
const rows = pageData.list || []
this.list = this.list.concat(rows)
this.pageForm.totalCount = pageData.totalCount || 0
if (this.list.length >= this.pageForm.totalCount || rows.length === 0) {
this.finished = true
}
this.pageForm.pageNumber++
this.saveListCache()
}
}).finally(() => {
this.requesting = false
this.loading = false
this.refreshing = false
})
},
async onEnter(item) {
if (item.groupId) {
const {
code,
data
} = await $.get('/platform/activity/basic/scope/getScopeUser', {activityGroupId: item.groupId})
if (code === 0 && data === 0) {
this.$toast.fail("您没有权限参与")
return
}
refreshListSilently() {
if (this.requesting) {
return
}
const requestForm = Object.assign({}, this.pageForm, {
pageNumber: 1
})
this.requesting = true
this.$axios.post("/platform/h5/qsv/pageData", requestForm).then((res) => {
if (res.code === 0) {
const pageData = res.data || {}
const rows = pageData.list || []
this.list = rows
this.pageForm = Object.assign({}, this.pageForm, {
pageNumber: 2,
totalCount: pageData.totalCount || 0
})
this.finished = rows.length >= this.pageForm.totalCount || rows.length === 0
this.saveListCache()
}
}).finally(() => {
this.requesting = false
this.loading = false
this.refreshing = false
})
},
const {startTime, endTime} = item
getCategoryText(category) {
const map = {
QUIZ: "答题",
SURVEY: "问卷",
VOTE: "投票"
}
return map[category] || "活动"
},
getCategoryIcon(category) {
const map = {
QUIZ: "fa fa-check-square-o",
SURVEY: "fa fa-list-alt",
VOTE: "fa fa-bar-chart"
}
return map[category] || "fa fa-tags"
},
getAnsweredText(category) {
const map = {
QUIZ: "已答题",
SURVEY: "已填写",
VOTE: "已投票"
}
return map[category] || "已完成"
},
formatDateTime(value) {
if (!value) {
return "-"
}
return this.$moment(value).format("YYYY-MM-DD HH:mm")
},
onEnter(item) {
const {startTime} = item
if (this.$moment(startTime).unix() > this.$moment().unix()) {
this.$toast("未开始")
return
@@ -81,13 +220,315 @@ layout("/layouts/platform_h5.html"){
const path = this.categoryPaths[item.category]
if (path) {
this.saveListCache()
pjaxReplace(path + "?id=" + item.id)
}
},
getCacheKey() {
return "qsv:h5:list"
},
saveListCache() {
try {
sessionStorage.setItem(this.getCacheKey(), JSON.stringify({
time: Date.now(),
list: this.list,
pageForm: this.pageForm,
finished: this.finished
}))
} catch (e) {
}
},
restoreListCache() {
try {
const cacheText = sessionStorage.getItem(this.getCacheKey())
if (!cacheText) {
return
}
const cache = JSON.parse(cacheText)
if (!cache || Date.now() - cache.time > 5 * 60 * 1000) {
this.clearListCache()
return
}
this.list = cache.list || []
this.pageForm = Object.assign({}, this.pageForm, cache.pageForm || {})
this.finished = !!cache.finished
this.cacheRestored = this.list.length > 0
} catch (e) {
this.clearListCache()
}
},
clearListCache() {
try {
sessionStorage.removeItem(this.getCacheKey())
} catch (e) {
}
}
},
created() {
this.restoreListCache()
},
mounted() {
if (this.cacheRestored) {
setTimeout(() => {
this.refreshListSilently()
}, 200)
}
}
})
</script>
<style>
.qsv-h5-page {
min-height: 100vh;
background: #f4f8ff;
padding-bottom: 18px;
box-sizing: border-box;
}
.qsv-h5-header {
padding: 10px 14px 16px;
background: linear-gradient(180deg, #1688f8 0%, #0f83ee 100%);
box-sizing: border-box;
}
.qsv-h5-nav {
height: 38px;
display: flex;
align-items: center;
margin-bottom: 8px;
color: #ffffff;
}
.qsv-h5-back-btn,
.qsv-h5-nav-placeholder {
width: 36px;
height: 36px;
flex-shrink: 0;
}
.qsv-h5-back-btn {
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, .16);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
padding: 0;
}
.qsv-h5-nav-title {
flex: 1;
min-width: 0;
text-align: center;
font-size: 18px;
font-weight: 700;
line-height: 36px;
}
.qsv-h5-search-row {
display: flex;
align-items: center;
gap: 10px;
}
.qsv-h5-header .van-search {
flex: 1;
min-width: 0;
padding: 0;
}
.qsv-h5-header .van-search__content {
background: rgba(255, 255, 255, .92);
}
.qsv-h5-search-btn {
width: 42px;
height: 42px;
border: 1px solid rgba(255, 255, 255, .45);
border-radius: 50%;
background: rgba(255, 255, 255, .18);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 18px;
box-sizing: border-box;
}
.qsv-h5-filter-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-top: 12px;
padding-bottom: 1px;
}
.qsv-h5-filter-row::-webkit-scrollbar {
display: none;
}
.qsv-h5-filter-btn {
height: 32px;
width: 100%;
padding: 0 6px;
border: 0;
border-radius: 17px;
background: rgba(255, 255, 255, .92);
color: #1688f8;
font-size: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
white-space: nowrap;
}
.qsv-h5-filter-btn.active {
background: #ffffff;
font-weight: 600;
}
.qsv-h5-filter-btn i {
font-size: 15px;
}
.qsv-h5-list-wrap {
min-height: calc(100vh - 170px);
padding: 14px;
box-sizing: border-box;
}
.qsv-h5-card {
display: flex;
gap: 14px;
padding: 14px;
margin-bottom: 14px;
background: #ffffff;
border-radius: 13px;
box-shadow: 0 8px 24px rgba(35, 102, 168, .1);
box-sizing: border-box;
}
.qsv-h5-cover {
width: 116px;
height: 87px;
aspect-ratio: 4 / 3;
border-radius: 8px;
overflow: hidden;
background: #eaf2ff;
flex-shrink: 0;
}
.qsv-h5-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.qsv-h5-card-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
padding: 2px 0;
}
.qsv-h5-title-row {
display: flex;
align-items: flex-start;
gap: 8px;
}
.qsv-h5-title {
flex: 1;
min-width: 0;
color: #1f2f46;
font-size: 16px;
font-weight: 700;
line-height: 1.35;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.qsv-h5-answered {
height: 22px;
min-width: 50px;
padding: 0 8px;
border: 1px solid #19a05f;
border-radius: 11px;
background: #f0fff8;
color: #149356;
font-size: 12px;
font-weight: 700;
line-height: 20px;
text-align: center;
white-space: nowrap;
flex-shrink: 0;
box-sizing: border-box;
}
.qsv-h5-meta-row {
display: flex;
align-items: center;
min-height: 20px;
}
.qsv-h5-type {
display: inline-flex;
align-items: center;
gap: 4px;
height: 20px;
padding: 0 6px;
border: 1px solid #ff9f43;
border-radius: 4px;
background: #fff7ec;
color: #f28b18;
font-size: 12px;
font-weight: 600;
line-height: 18px;
flex-shrink: 0;
box-sizing: border-box;
}
.qsv-h5-type i {
color: #f28b18;
font-size: 12px;
}
.qsv-h5-time {
color: #71829b;
font-size: 12px;
line-height: 1.75;
}
@media (max-width: 360px) {
.qsv-h5-card {
gap: 10px;
padding: 12px;
}
.qsv-h5-cover {
width: 96px;
height: 72px;
}
.qsv-h5-title {
font-size: 16px;
}
}
</style>
<!--#
}
@@ -3,17 +3,19 @@ layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="问卷列表" placeholder fixed></van-nav-bar>
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<van-sticky offset-top="46px">
<!--开启计时 未完成 活动未结束-->
<div class="timer" v-if="activity.timeLimit > 0 && !answerRecord.isFinish && !isEnd">⏰{{ remainingTime }}s
<div class="quiz-hero" :style="heroStyle">
<div class="quiz-hero-mask">
<div class="timer" v-if="activity.timeLimit > 0 && !answerRecord.isFinish && !isEnd">
⏰ {{ remainingTime }}s
</div>
<h2 class="quiz-title">{{ activity.title }}</h2>
</div>
<h2 class="title">{{ activity.title }}</h2>
</van-sticky>
</div>
<div v-if="!answerRecord.isFinish">
<div class="quiz-card-scroll" v-if="!answerRecord.isFinish">
<div v-for="(subject, index) in subjects" :key="index" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<div class="tag">
@@ -84,9 +86,28 @@ layout("/layouts/platform_h5.html"){
return this.activity.timeLimit * 60 - this.answerTime
}
return 0
},
pageTitle() {
return this.getCategoryPageTitle(this.activity && this.activity.category, "在线答题")
},
heroStyle() {
if (this.activity && this.activity.cover) {
return {
backgroundImage: "url(" + this.activity.cover + ")"
}
}
return {}
}
},
methods: {
getCategoryPageTitle(category, defaultTitle) {
const titleMap = {
QUIZ: "在线答题",
SURVEY: "问卷调查",
VOTE: "投票"
}
return titleMap[category] || defaultTitle
},
//获取活动、题目
listSubjects() {
this.$axios.post("/platform/h5/qsv/quiz/subjects", {activityId: this.id}).then((res) => {
@@ -245,14 +266,6 @@ layout("/layouts/platform_h5.html"){
return
}
const selectedOptionIds = subject.userSelectOptionIds || []
const hasSelected = selectedOptionIds.includes(optionId)
// 多选题达到最大可选数量后禁止继续勾选,但允许取消已选项。
if (subject.type === "checkbox" && !hasSelected && subject.maxMulti && subject.maxMulti > 0 && selectedOptionIds.length >= subject.maxMulti) {
this.$toast("本题最多选择" + subject.maxMulti + "项")
return
}
if (subject.type === "radio") {
this.$refs["subject" + subject.id][0].toggleAll(false)
}
@@ -341,20 +354,53 @@ layout("/layouts/platform_h5.html"){
style: /*language=CSS*/ `
.container {
padding: 0;
background: #f4f8ff;
height: calc(100vh - 46px);
overflow: hidden;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/deep/ .container .title {
text-align: center;
padding: 20px;
background: #ffffff;
margin: 0 0 10px 0 !important;
color: var(--color-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/deep/ .quiz-hero {
flex: 0 0 auto;
min-height: 170px;
background: linear-gradient(135deg, #1688f8, #0a55c5);
background-size: cover;
background-position: center;
position: relative;
overflow: hidden;
margin: 12px 12px 14px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(35, 102, 168, .14);
}
.container .card {
padding: 10px;
background: #ffffff;
/deep/ .quiz-hero-mask {
min-height: 170px;
padding: 24px 20px 22px;
display: flex;
flex-direction: column;
justify-content: flex-end;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(0, 0, 0, .16), rgba(0, 0, 0, .56));
}
/deep/ .quiz-title {
margin: 0;
color: #fff;
font-size: 22px;
font-weight: 700;
line-height: 1.35;
text-shadow: 0 2px 8px rgba(0, 0, 0, .28);
}
.quiz-card-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 0 12px;
box-sizing: border-box;
}
.subject {
@@ -377,10 +423,15 @@ layout("/layouts/platform_h5.html"){
}
/deep/ .timer {
text-align: center;
font-size: 18px;
background: #fff;
padding: 10px 0;
align-self: flex-start;
margin-bottom: 12px;
padding: 5px 10px;
border-radius: 14px;
background: rgba(255, 255, 255, .22);
color: #fff;
font-size: 14px;
font-weight: 600;
backdrop-filter: blur(6px);
}
.submit-button {
@@ -6,15 +6,15 @@ layout("/layouts/platform_h5.html"){
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="问卷列表" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<van-sticky offset-top="46px">
<h2 class="title">{{ activity.title }}</h2>
<div class="score-form">
<div class="score-form-total">
<div class="score-font-style">{{totalScore}}</div>
<i class="score-underline"></i>
<div class="quiz-hero" :style="heroStyle">
<div class="quiz-hero-mask">
<h2 class="quiz-title">{{ activity.title }}</h2>
<div class="quiz-score">
<span>{{totalScore}}</span>
<em></em>
</div>
</div>
</van-sticky>
</div>
<div>
<div v-for="(subject, index) in subjects" :key="index" class="subject">
@@ -111,6 +111,14 @@ layout("/layouts/platform_h5.html"){
}
}
return totalScore
},
heroStyle() {
if (this.activity && this.activity.cover) {
return {
backgroundImage: "url(" + this.activity.cover + ")"
}
}
return {}
}
},
filters: {
@@ -199,23 +207,60 @@ layout("/layouts/platform_h5.html"){
style: /*language=CSS*/ `
.container {
padding: 0;
background: #f4f8ff;
min-height: 100vh;
}
/deep/ .container .title {
text-align: center;
padding: 20px;
margin: 0 !important;
background: #ffffff;
color: var(--color-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/deep/ .quiz-hero {
min-height: 190px;
background: linear-gradient(135deg, #1688f8, #0a55c5);
background-size: cover;
background-position: center;
position: relative;
overflow: hidden;
margin: 12px 12px 14px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(35, 102, 168, .14);
}
.container .score {
/deep/ .quiz-hero-mask {
min-height: 190px;
padding: 28px 20px 24px;
display: flex;
flex-direction: column;
justify-content: flex-end;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(0, 0, 0, .18), rgba(0, 0, 0, .58));
}
.container .card {
padding: 10px;
background: #ffffff;
/deep/ .quiz-title {
margin: 0 0 14px;
color: #fff;
font-size: 22px;
font-weight: 700;
line-height: 1.35;
text-shadow: 0 2px 8px rgba(0, 0, 0, .28);
}
/deep/ .quiz-score {
display: flex;
align-items: flex-end;
color: #fff;
text-shadow: 0 2px 10px rgba(0, 0, 0, .3);
}
/deep/ .quiz-score span {
font-size: 48px;
font-weight: 800;
line-height: 1;
}
/deep/ .quiz-score em {
margin-left: 6px;
margin-bottom: 5px;
font-style: normal;
font-size: 18px;
font-weight: 600;
}
.subject {
@@ -237,46 +282,6 @@ layout("/layouts/platform_h5.html"){
top: 0;
}
/deep/ .score-form {
background: #ffffff;
display: flex;
margin-bottom: 10px;
border-top: 1px solid #f1f1f1;
align-items: center;
}
/deep/ .score-form-total {
flex: 1;
background: #fff;
display: flex;
padding: 10px 20px 10px 0;
flex-direction: column;
align-items: flex-end;
}
/deep/ .score-text-news {
flex: 1;
background: #fff;
}
/deep/ .score-font-style {
font-size: 32px;
color: #ff6a00;
word-break: keep-all;
line-height: 38px;
min-width: 47px;
text-align: center;
}
/deep/ .score-underline {
background: url(//image.wjx.cn/images/newimg/score-form/score-underline@2x.png) no-repeat center;
background-size: 47px 16px;
display: inline-block;
height: 16px;
width: 47px;
margin-top: 7px;
}
.van-checkbox__icon--checked .van-icon {
color: #fff !important;
background-color: var(--color-primary) !important;
@@ -5,18 +5,53 @@ layout("/layouts/platform_h5.html"){
<style>
.container {
padding: 0;
background: #f4f8ff;
height: calc(100vh - 46px);
overflow: hidden;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.container .title {
text-align: center;
padding: 20px;
background: #ffffff;
margin: 0 0 10px 0;
.quiz-hero {
flex: 0 0 auto;
min-height: 170px;
background: linear-gradient(135deg, #1688f8, #0a55c5);
background-size: cover;
background-position: center;
position: relative;
overflow: hidden;
margin: 12px 12px 14px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(35, 102, 168, .14);
}
.container .card {
padding: 10px;
background: #ffffff;
.quiz-hero-mask {
min-height: 170px;
padding: 24px 20px 22px;
display: flex;
flex-direction: column;
justify-content: flex-end;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(0, 0, 0, .16), rgba(0, 0, 0, .56));
}
.quiz-title {
margin: 0;
color: #fff;
font-size: 22px;
font-weight: 700;
line-height: 1.35;
text-shadow: 0 2px 8px rgba(0, 0, 0, .28);
}
.quiz-card-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 0 12px;
box-sizing: border-box;
}
.subject {
@@ -49,16 +84,30 @@ layout("/layouts/platform_h5.html"){
background: #fff;
}
.van-checkbox__icon--checked .van-icon {
.van-checkbox__icon--checked .van-icon,
.van-radio__icon--checked .van-icon {
color: #fff !important;
background-color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
}
.van-checkbox__icon--disabled .van-icon {
.van-checkbox__icon--disabled .van-icon,
.van-radio__icon--disabled .van-icon {
background-color: #fff;
}
.survey-option-finished-selected .survey-option-text {
color: #00a000;
font-weight: 600;
}
.survey-option-finished-selected .van-radio__icon--disabled .van-icon,
.survey-option-finished-selected .van-checkbox__icon--disabled .van-icon {
color: #fff !important;
background-color: #d8d8d8 !important;
border-color: #d8d8d8 !important;
}
.ui-input-box {
border: 1px solid #e3e3e3;
margin: 5px 0;
@@ -80,57 +129,145 @@ layout("/layouts/platform_h5.html"){
resize: none;
width: 100%;
}
.option-fill-box {
border: 1px solid #e3e3e3;
margin: 0 16px 8px 46px;
background-color: #fff;
padding: 0;
display: flex;
}
.option-fill-box input {
background-color: #fff;
border: none !important;
padding: 10px;
font-size: 14px;
line-height: 18px;
display: inline-block;
margin: 0;
-webkit-appearance: none;
resize: none;
width: 100%;
}
.required-star {
color: #ee0a24;
margin-left: 4px;
font-weight: bold;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<h2 class="title">{{ activity.title }}</h2>
<div v-if="!isFinished">
<div v-for="(subject, index) in subjects" :key="index" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<van-nav-bar :title="pageTitle" left-arrow left-text="返回" @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': '84px'}">
<div class="quiz-hero" :style="heroStyle">
<div class="quiz-hero-mask">
<h2 class="quiz-title">{{ activity.title }}</h2>
</div>
</div>
<div class="quiz-card-scroll" v-if="!isFinished">
<div v-for="(subject, index) in visibleSubjects" :key="subject.id" class="subject">
<p>{{index+1}}、{{ subject.title }}<span class="required-star">*</span></p>
<div class="tag">
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>
<van-tag v-if="subject.score" type="primary" size="large">{{subject.score}}分</van-tag>
</div>
<!--单选、多选-->
<van-checkbox-group
v-model="subject.userSelectOptionIds"
:ref="'subject'+subject.id"
v-if="['radio','checkbox'].includes(subject.type)"
<!--单选-->
<van-radio-group
:value="getRadioValue(subject)"
v-if="subject.type === 'radio'"
>
<van-cell-group>
<van-cell
v-for="(option,index) in subject.options"
clickable
:key="option.id"
:title="option.text"
@click="cellToggle(subject,option.id)"
>
<template #icon>
<van-checkbox :name="option.id" :ref="'option'+option.id" style="margin-right: 10px"></van-checkbox>
</template>
<img
slot="right-icon"
v-if="option.imgUrl"
:src="option.imgUrl"
alt=""
style="width: 40px; height: 40px"
@click.stop="previewOptionImg(option.imgUrl)"
/>
</van-cell>
<template v-for="(option,index) in subject.options">
<van-cell
clickable
:key="option.id"
:title="option.text"
:class="getFinishedOptionClass(subject, option)"
@click="cellToggle(subject,option.id)"
>
<template #title>
<span class="survey-option-text">{{option.text}}</span>
</template>
<template #icon>
<van-radio :name="option.id" :disabled="answerRecord.isFinish" style="margin-right: 10px"></van-radio>
</template>
<img
slot="right-icon"
v-if="option.imgUrl"
:src="option.imgUrl"
alt=""
style="width: 40px; height: 40px"
@click.stop="previewOptionImg(option.imgUrl)"
/>
</van-cell>
<div
class="option-fill-box"
v-if="isOptionFillVisible(subject, option)"
:key="option.id + '-fill'">
<input
v-model="subject.optionFillContents[option.id]"
:placeholder="option.fillPlaceholder || '请输入补充内容'">
</div>
</template>
</van-cell-group>
</van-radio-group>
<!--多选-->
<van-checkbox-group
v-model="subject.userSelectOptionIds"
v-if="subject.type === 'checkbox'"
>
<van-cell-group>
<template v-for="(option,index) in subject.options">
<van-cell
clickable
:key="option.id"
:title="option.text"
:class="getFinishedOptionClass(subject, option)"
@click="cellToggle(subject,option.id)"
>
<template #title>
<span class="survey-option-text">{{option.text}}</span>
</template>
<template #icon>
<van-checkbox :name="option.id" :ref="'option'+option.id" :disabled="answerRecord.isFinish" style="margin-right: 10px"></van-checkbox>
</template>
<img
slot="right-icon"
v-if="option.imgUrl"
:src="option.imgUrl"
alt=""
style="width: 40px; height: 40px"
@click.stop="previewOptionImg(option.imgUrl)"
/>
</van-cell>
<div
class="option-fill-box"
:key="option.id + '-fill'"
v-if="isOptionFillVisible(subject, option)">
<input
type="text"
v-model="subject.optionFillContents[option.id]"
:placeholder="option.fillPlaceholder || '请填写补充内容'"
:readonly="answerRecord.isFinish"
@click.stop />
</div>
</template>
</van-cell-group>
</van-checkbox-group>
<!--填空题-->
<div class="ui-input-box" v-if="subject.type==='text'">
<input type="text" v-model="subject.userFillContent" :readonly="answerRecord.isFinish" />
<input type="text" v-model="subject.userFillContent" :placeholder="'请输入' + subject.title" :readonly="answerRecord.isFinish" />
</div>
</div>
<div class="button-control" v-if="!answerRecord.isFinish">
<van-button type="primary" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}</van-button>
<div class="button-control">
<van-button v-if="!answerRecord.isFinish" type="primary" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}</van-button>
<van-button v-else type="primary" @click="historyBack" block>返回</van-button>
</div>
</div>
</div>
@@ -158,10 +295,34 @@ layout("/layouts/platform_h5.html"){
return this.$moment().unix() > this.$moment(this.activity.endTime).unix()
}
return true
},
visibleSubjects() {
return this.subjects.filter((subject) => {
return this.isSubjectVisible(subject)
})
},
pageTitle() {
return this.getCategoryPageTitle(this.activity && this.activity.category, "问卷调查")
},
heroStyle() {
if (this.activity && this.activity.cover) {
return {
backgroundImage: "url(" + this.activity.cover + ")"
}
}
return {}
}
},
methods: {
getCategoryPageTitle(category, defaultTitle) {
const titleMap = {
QUIZ: "在线答题",
SURVEY: "问卷调查",
VOTE: "投票"
}
return titleMap[category] || defaultTitle
},
//获取活动、题目
listSubjects() {
this.$axios.post("/platform/h5/qsv/survey/subjects", { activityId: this.id }).then((res) => {
@@ -193,14 +354,214 @@ layout("/layouts/platform_h5.html"){
const answer = this.answerRecord.extJson[subject.id]
if (["radio", "checkbox"].includes(subject.type)) {
this.$refs["subject" + subject.id][0].toggleAll(false)
answer?.optionIds.forEach((optionId) => {
this.$nextTick(() => {
this.$refs["option" + optionId][0].toggle()
})
})
const optionIds = answer?.optionIds || []
this.$set(subject, "userSelectOptionIds", subject.type === "radio" ? optionIds.slice(0, 1) : optionIds)
this.$set(subject, "optionFillContents", answer?.optionFillContents || {})
} else if (subject.type === "text") {
subject.userFillContent = answer?.text
this.$set(subject, "userFillContent", answer?.text)
}
})
if (!this.answerRecord.isFinish) {
this.clearHiddenSubjectAnswers()
}
},
//判断题目是否显示,未配置隐显逻辑的题目默认显示;已配置的题目按条件树递归计算后再执行显示或隐藏动作
isSubjectVisible(subject) {
if (!subject.visibleRuleEnabled) {
return true
}
const rootGroup = this.getVisibleRuleRootGroup(subject)
if (!rootGroup) {
return true
}
const matched = this.evaluateVisibleRuleGroup(rootGroup)
if (subject.visibleRuleAction === "hide") {
return !matched
}
return matched
},
//读取隐显逻辑根分组,兼容旧版 subjectId/optionId 平铺条件
getVisibleRuleRootGroup(subject) {
let conditions = subject.visibleRuleConditions || []
if (typeof conditions === "string") {
try {
conditions = JSON.parse(conditions)
} catch (e) {
conditions = []
}
}
if (!Array.isArray(conditions) || conditions.length === 0) {
return null
}
if (conditions.length === 1 && conditions[0].type === "group") {
return this.normalizeVisibleRuleGroup(conditions[0])
}
const children = conditions.filter((condition) => {
return condition.subjectId && condition.optionId
}).map((condition) => {
return {
type: "item",
field: condition.subjectId,
operator: "contains",
value: condition.optionId
}
})
if (children.length === 0) {
return null
}
return {
type: "group",
logic: subject.visibleRuleLogic === "OR" ? "or" : "and",
children
}
},
//规整条件分组,避免历史数据或空字段影响递归判断
normalizeVisibleRuleGroup(group) {
return {
type: "group",
logic: (group.logic || "and").toLowerCase() === "or" ? "or" : "and",
children: Array.isArray(group.children) ? group.children.map((child) => {
if (child.type === "group") {
return this.normalizeVisibleRuleGroup(child)
}
return {
type: "item",
field: child.field || child.subjectId || "",
operator: child.operator || (child.optionId ? "contains" : "=="),
value: child.value !== undefined && child.value !== null ? child.value : (child.optionId || "")
}
}) : []
}
},
//递归计算条件分组,支持并且、或者和嵌套分组
evaluateVisibleRuleGroup(group) {
const children = (group.children || []).filter((child) => {
return child.type === "group" || (child.field && child.operator)
})
if (children.length === 0) {
return true
}
const results = children.map((child) => {
if (child.type === "group") {
return this.evaluateVisibleRuleGroup(child)
}
return this.evaluateVisibleRuleItem(child)
})
return group.logic === "or"
? results.some((item) => {
return item
})
: results.every((item) => {
return item
})
},
//计算单条条件,选择题按选项ID判断,填空题支持文本包含和数字大小比较
evaluateVisibleRuleItem(condition) {
const triggerSubject = this.subjects.find((item) => {
return item.id === condition.field
})
if (!triggerSubject) {
return false
}
const answer = ["radio", "checkbox"].includes(triggerSubject.type)
? (triggerSubject.userSelectOptionIds || [])
: triggerSubject.userFillContent
return this.compareVisibleRuleValue(answer, condition.operator, condition.value)
},
//比较条件值,和后台逻辑抽屉实时预览保持同一套语义
compareVisibleRuleValue(answer, operator, value) {
if (Array.isArray(answer)) {
if (operator === "!=") {
return !answer.includes(value)
}
return answer.includes(value)
}
if ([">", "<", ">=", "<="].includes(operator)) {
const answerNumber = Number(answer)
const valueNumber = Number(value)
if (isNaN(answerNumber) || isNaN(valueNumber)) {
return false
}
if (operator === ">") {
return answerNumber > valueNumber
}
if (operator === "<") {
return answerNumber < valueNumber
}
if (operator === ">=") {
return answerNumber >= valueNumber
}
return answerNumber <= valueNumber
}
const answerText = answer === undefined || answer === null ? "" : String(answer)
const valueText = value === undefined || value === null ? "" : String(value)
if (operator === "!=") {
return answerText !== valueText
}
if (operator === "contains") {
return answerText.indexOf(valueText) !== -1
}
return answerText === valueText
},
//清空当前隐藏题目的答案,避免隐藏题残留旧选项或填空内容参与后续提交
clearHiddenSubjectAnswers() {
this.subjects.forEach((subject) => {
if (this.isSubjectVisible(subject)) {
return
}
if (["radio", "checkbox"].includes(subject.type)) {
this.$set(subject, "userSelectOptionIds", [])
this.$set(subject, "optionFillContents", {})
} else if (subject.type === "text") {
this.$set(subject, "userFillContent", null)
}
})
},
//判断选项是否已被选择,用于控制选项补充填写框显示和提交校验
isOptionSelected(subject, optionId) {
return subject.userSelectOptionIds && subject.userSelectOptionIds.includes(optionId)
},
getFinishedOptionClass(subject, option) {
return this.answerRecord.isFinish && this.isOptionSelected(subject, option.id)
? "survey-option-finished-selected"
: ""
},
//单选题仍按数组保存,这里只取第一个值用于 radio 组件展示
getRadioValue(subject) {
return subject.userSelectOptionIds && subject.userSelectOptionIds.length > 0
? subject.userSelectOptionIds[0]
: ""
},
//选中需要补充填写的选项后显示输入框,例如选择“否”后填写原因
isOptionFillVisible(subject, option) {
return option.fillRequired && this.isOptionSelected(subject, option.id)
},
//清空未选中选项的补充填写内容,避免取消选择后残留旧内容
clearUnselectedOptionFillContents(subject) {
if (!subject.optionFillContents) {
this.$set(subject, "optionFillContents", {})
}
if (!subject.options) {
return
}
subject.options.forEach((option) => {
if (!this.isOptionSelected(subject, option.id)) {
this.$delete(subject.optionFillContents, option.id)
}
})
},
@@ -212,9 +573,14 @@ layout("/layouts/platform_h5.html"){
}
if (subject.type === "radio") {
this.$refs["subject" + subject.id][0].toggleAll(false)
this.$set(subject, "userSelectOptionIds", [optionId])
} else {
this.$refs["option" + optionId][0].toggle()
}
this.$refs["option" + optionId][0].toggle()
this.$nextTick(() => {
this.clearUnselectedOptionFillContents(subject)
this.clearHiddenSubjectAnswers()
})
},
//预览图片
@@ -231,13 +597,20 @@ layout("/layouts/platform_h5.html"){
}
//提示那些题没有作答
for (let i = 0; i < this.subjects.length; i++) {
const subject = this.subjects[i]
for (let i = 0; i < this.visibleSubjects.length; i++) {
const subject = this.visibleSubjects[i]
if (["radio", "checkbox"].includes(subject.type)) {
if (!subject.userSelectOptionIds || subject.userSelectOptionIds.length === 0) {
this.$toast.fail("第" + (i + 1) + "题未做答")
return
}
for (let j = 0; j < subject.options.length; j++) {
const option = subject.options[j]
if (this.isOptionFillVisible(subject, option) && !subject.optionFillContents[option.id]) {
this.$toast.fail("请填写第" + (i + 1) + "题的补充内容")
return
}
}
} else if ("text" === subject.type) {
if (!subject.userFillContent) {
this.$toast.fail("第" + (i + 1) + "题未作答")
@@ -255,11 +628,12 @@ layout("/layouts/platform_h5.html"){
answer: JSON.stringify({
activityId: this.id,
answerRecordId: this.answerRecordId,
subjects: this.subjects.map((subject) => {
subjects: this.visibleSubjects.map((subject) => {
return {
id: subject.id,
userSelectOptionIds: ["checkbox", "radio"].includes(subject.type) ? subject.userSelectOptionIds : [],
userFillContent: subject.type === "text" ? subject.userFillContent : null
userFillContent: subject.type === "text" ? subject.userFillContent : null,
optionFillContents: ["checkbox", "radio"].includes(subject.type) ? subject.optionFillContents : {}
}
})
})