Files
ylst-h5/src/layouts/logic.js
2025-03-15 19:25:11 +08:00

425 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 问卷设计中涉及到的一些 【逻辑】 中公共方法
*/
import config from '@/config';
import { Modal } from 'ant-design-vue';
import { advancedQuesTypeList } from './quesInfoList';
const advancedTypes = advancedQuesTypeList.map((i) => i.type); // 高级题型的 question_type
const performance = {
start(label) {
if (config.currentMode === 'dev') {
console.time(`${label} Performance`);
}
},
end(label) {
if (config.currentMode === 'dev') {
console.timeEnd(`${label} Performance`);
}
},
log(...rest) {
if (config.currentMode === 'dev') {
console.log(...rest);
}
}
};
function isNullish(value) {
return value === undefined || value === null;
}
/**
* 修改(添加、移动、删除)问卷 题目、分页,编辑(添加、删除)逻辑 时,判断循环逻辑是否满足
* 的异步方法,感觉题多了这个方法执行会很慢。。。
* @param rest
* @return {Promise<boolean>}
*/
export async function loopingAvailableAsync(...rest) {
return Promise.resolve(loopingAvailable(...rest));
}
/**
* 修改(添加、移动、删除)问卷 题目、分页,编辑(添加、删除)逻辑 时,判断循环逻辑是否满足
* @param cycles 循环逻辑列表
* @param questions 被操作后的问题列表
* @param logics 逻辑列表
* @param isPerPage 是否是每页一道题
* @param reason 提示信息
* @return {boolean} 是否满足循环逻辑
*/
export function loopingAvailable({ cycles, questions, logics, isPerPage, reason } = {}) {
const performanceName = 'CheckLoopingAvailable';
performance.start(performanceName);
let result = true;
let prompt = reason || '';
if (!cycles.length) {
// 没有配置循环逻辑
performance.end(performanceName);
return result;
}
const quizList = generateQuestionPages({ questions, logics, isPerPage });
const cycleList = generateCyclePages(cycles, quizList);
const logicList = generateLogicPages(logics, quizList);
const lastPage = quizList.reduce((prev, curr) => Math.max(prev, curr.$page || 0), 0); // 最后一页的页码
// console.log('questions', questions, quizList);
// console.log('cycles', cycles, cycleList);
// console.log('logics', logics, logicList);
// console.log('last page', lastPage);
for (let i = 0; i < cycleList.length; i += 1) {
const { index, page, startPage, endPage } = cycleList[i] || {};
// 循环关联问题被删除时,才会出现这种情况
if (!quizList.find((j) => j.question_index === index)) {
result = false;
prompt = '问题被循环关联,不能删除';
break;
}
if ((startPage && startPage <= page) || (endPage && endPage <= page)) {
result = false;
prompt = '问题被循环关联,不能位于循环题组后';
break;
}
// 循环关联问题位于自己或其它循环的循环题组分页中间
if (page && cycleList.some((j) => j.startPage < page && page < j.endPage)) {
result = false;
prompt = '问题被循环关联,不能位于循环题组中';
break;
}
// 循环的循环题组,修改问题或分页的时候应该不会出现这种情况
if (startPage && endPage && startPage > endPage) {
result = false;
prompt = '循环题组的起始分页应该小于结束分页';
break;
}
// 删除分页,或者删除自带分页的题,导致分页变化,校验分页是否包含了所有的循环题组分页
if (endPage > lastPage) {
result = false;
prompt = '请勿在循环题组中编辑分页,请调整循环题组后再试';
break;
}
// 循环题组分页和跳转逻辑有冲突
if (
logicList.some((j) => {
// 跳转到这几个的时候,循环不校验,只校验带题的
// 正常完成、提前终止、配额超限
if ([-1, -2, -3].includes(Math.min(...j))) {
return false;
}
return isCross(j, [startPage, endPage]);
})
) {
result = false;
prompt = '循环题组分页区间与跳转逻辑冲突,请修改后再试';
break;
}
}
if (!result && (reason || prompt)) {
Modal.warning({
class: 'custom-modal custom-modal-title-notice hide-ant-icon',
title: '无法操作',
width: '450px',
content: reason || prompt
});
}
performance.end(performanceName);
return result;
}
/**
* 给问题列表中的问题添加,问题所在的页码字段($page
* 说明:
* 1. 需要过滤掉没有题的空白页
* 2. 高级题型的题前后需要加分页
* 3. 添加了逻辑的基础题型前后需要加分页
* @param questions
* @param logics
* @param isPerPage
* @param addon 自定义额外操作,是一个方法
* @return {array} 题目列表,包括题和分页
*/
export function generateQuestionPages({ questions, logics, isPerPage, addon }) {
if (!questions?.length) {
return questions || [];
}
let page = 1;
let pageObjectCount = 0; // 计数,在 questions 有几个分页的对象
return questions.map((item, index, arr) => {
const asBlock = [...advancedTypes, 23]; // 需要自动加分页的题型:高级题型及知情同意书
const isPrevAdv = asBlock.includes(arr[index - 1]?.question_type); // 前一道题是高级题型
const isAdv = asBlock.includes(item?.question_type); // 当前题是高级题型
// const isPrevAdv = advancedTypes.includes(arr[index - 1]?.question_type); // 前一道题是高级题型
// const isAdv = advancedTypes.includes(item?.question_type); // 当前题是高级题型
// 前一道题配置了跳转逻辑
const isPrevLogic = !!logics.find((logic) => logic.question_index === arr[index - 1]?.question_index);
// 当前题配置了跳转逻辑
const isLogic = !!logics.find((logic) => logic.question_index === item.question_index);
const isSurroundedByPage = isAdv || isLogic; // 需要在该题前后添加分页
const isPrevPage = !!arr[index - 1]?.page; // 前一道题是一个分页对象
const prevIsQuestion = !arr[index - 1]?.page; // 前一道题是一道题而不是一个分页对象
if (!index) {
page = 1;
pageObjectCount = 0;
}
if (item.page) {
pageObjectCount += 1;
if (index && prevIsQuestion && !isPrevLogic && !isPrevAdv) {
// 过滤掉没有题的空白页
page += 1;
}
} else {
if (index && !isPrevPage && !isPrevAdv && !isPrevLogic && isSurroundedByPage) {
page += 1;
}
item.$page = isPerPage ? index + 1 - pageObjectCount : page;
if (isSurroundedByPage) {
page += 1;
}
}
if (addon) {
addon(item);
}
return item;
});
}
/**
* 格式化循环列表,便于后续使用
* @param cycles
* @param questions
* @return {array} 格式化后的循环列表
*/
export function generateCyclePages(cycles, questions) {
if (!cycles?.length) {
return cycles || [];
}
return cycles.map((cycle) => ({
index: cycle.question_index,
page: getPageByQuestionIndex(cycle.question_index, questions),
startPage: cycle.first_page,
endPage: cycle.last_page
}));
}
/**
* 逻辑关联的页码,便于后续使用
* @param logics {*[]}
* @param questions {*[]}
* @param fillRangeItem {boolean} 用数字填充满 range[index] 数组
* @return {number[][]} 逻辑关联的页码
*/
export function generateLogicPages(logics, questions, fillRangeItem) {
if (!logics?.length) {
return logics || [];
}
const pages = [];
logics.forEach((logic) => {
if (![0, 2].includes(logic.skip_type)) {
return;
}
let page = [
getPageByQuestionIndex(logic.logic?.[0]?.question_index, questions),
getPageByQuestionIndex(logic.question_index, questions),
getPageByQuestionIndex(logic.skip_question_index, questions) || logic.skip_question_index
];
page = page.filter((i) => !!i);
if (fillRangeItem) {
const min = Math.min(...page.filter((i) => i > 0));
const max = Math.max(...page);
page = generateRange(min, max);
}
pages.push(page);
});
return pages;
}
/**
* 通过问题的 question_index 找到问题在第几页
* @param questionIndex
* @param questions
* @return {number|*|undefined}
*/
export function getPageByQuestionIndex(questionIndex, questions) {
return questions.find((i) => i.question_index === questionIndex)?.$page || undefined;
}
/**
* 生成一个从 start 开始到 end 结束的数组
* @param start {number}
* @param end {number}
* @return {number[]}
*/
export function generateRange(start, end) {
if (isNullish(start) || isNullish(end)) {
return [];
}
if (isNaN(Number(start)) || isNaN(Number(end))) {
return [];
}
let i = start;
let range = [];
while (i <= end) {
range.push(i);
i += 1;
}
return range;
}
/**
* 判断逻辑与循环分组,是否不合理
* @param range1 {array} 跳转逻辑的分页数组
* @param range2 {array} 循环分组的分页数组
* @return {boolean} true 不合理, false 合理
*/
export function isCross(range1, range2) {
if (!range1 || !range2) {
return false;
}
const parsedRange1 = range1.slice(1);
const judge = range1[0];
const start1 = Math.min(...parsedRange1);
const end1 = Math.max(...parsedRange1);
const start2 = range2[0];
const end2 = range2[1];
const isPlainSequence = parsedRange1[0] === start1; // 跳转逻辑的方向true 为从前向后跳转
if (isNullish(start1) || isNullish(end1) || end1 < 0 || (isNullish(start2) && isNullish(end2))) {
return false;
}
// [judge, start1, end1]; // isPlainSequence
// [start1, judge, end1]; // isPlainSequence || !isPlainSequence
//
// [start2, end2];
// 逻辑在循环之前
const isLeft = isNullish(start2) ? end1 < end2 : end1 < start2;
// 逻辑在循环之后
const isRight = isNullish(end2) ? start2 < start1 && start2 < judge : end2 < start1 && end2 < judge;
// 不相交也不包含
const isSibling = isLeft || isRight;
// 逻辑包含循环
const contain = (isPlainSequence
&& (((isNullish(start2) || isSequence(judge, start2, start1))
&& (isNullish(end2) || isSequence(judge, end2, start1)))
|| ((isNullish(start2) || isSequence(start1, start2, end1))
&& (isNullish(end2) || isSequence(start1, end2, end1)))))
|| (!isPlainSequence
&& (judge < start1
? ((isNullish(start2) || isSequence(judge, start2, start1))
&& (isNullish(end2) || isSequence(judge, end2, start1)))
|| ((isNullish(start2) || isSequence(start1, start2, end1))
&& (isNullish(end2) || isSequence(start1, end2, end1)))
: ((isNullish(start2) || isSequence(start1, start2, judge))
&& (isNullish(end2) || isSequence(start1, end2, judge)))
|| ((isNullish(start2) || isSequence(judge, start2, end1))
&& (isNullish(end2) || isSequence(judge, end2, end1)))));
// 循环存在封闭区间,并且循环包含逻辑
const contained = !isNullish(start2)
&& !isNullish(end2)
// [judge, start1, end1];
&& ((isPlainSequence && start2 <= judge && end1 <= end2)
// [judge, start1, end1];
// [start1, judge, end1];
|| (!isPlainSequence && start2 <= start1 && start2 <= judge && end1 <= end2));
// 循环不存在封闭区间
const unCircled = (!isNullish(start2)
&& isNullish(end2)
&& ((isPlainSequence && start2 === judge) || (!isPlainSequence && judge < start1)
? start2 === judge
: start2 === start1))
|| (isNullish(start2) && !isNullish(end2) && end2 === end1);
return !(isSibling || contain || contained || unCircled);
}
function isSequence(s1, s2, s3, equal) {
return equal ? s1 <= s2 && s2 <= s3 : s1 < s2 && s2 < s3;
}
/**
* 调用保存问题接口前,检查是否有问题受到循环影响需要重新保存,如果有则重新保存一下这道题,没有则不需要特殊处理
* bugfix for : 有循环的问卷发布后,再次编辑问卷,将循环题组内的问题移除循环题组,
* 再次发布,后端不处理被移除的问题 title导致该题的 title 仍保持上次发布时的值
* 错误格式一般为B3.1 正确格式一般为B3
* 导致作答出现错误,例如:引用找不到题
* 从 store 里查出修改前和修改后的问题、分页、循环;比较修改前后问题是否被移除了某个循环题组;修改 quesSaveParam.newQuestion
* @param quesSaveParam 将要保存的数据会被此方法修改的字段quesSaveParam.newQuestion
* @param store
*/
export function updateNewQuestionsByLoopingEffect(quesSaveParam, store) {
const { questionInfoBeforeModified = {}, questionInfo = {} } = JSON.parse(JSON.stringify(store.state.common)) || {};
const oldPages = questionInfoBeforeModified.survey.pages;
const newQuestions = questionInfo.questions;
const newPages = questionInfo.survey.is_one_page_one_question
? questionInfo.questions.filter((i) => i.question_index).map((i, idx) => [i.question_index])
: questionInfo.survey.pages;
const cycles = questionInfo.cycle_pages || [];
if (!cycles.length) {
return;
}
const moveOutOfCycleQuestionIndex = [];
const cyclePages = cycles.map((i) => [i.first_page, i.last_page]).filter((i) => i[0] && i[1]);
cyclePages.forEach((i) => {
const start = i[0] - 1;
const end = i[1] - 1;
for (let j = start; j <= end; j += 1) {
if (oldPages[j]?.join(',') !== newPages[j]?.join(',')) {
oldPages[j]?.forEach((k) => {
if (!newPages?.[j]?.includes(k)) {
moveOutOfCycleQuestionIndex.push(k);
}
});
}
}
});
const movedOutOfCycleQuestions = newQuestions.filter((i) => moveOutOfCycleQuestionIndex.includes(i.question_index));
if (movedOutOfCycleQuestions.length) {
if (!quesSaveParam.newQuestion) {
quesSaveParam.newQuestion = [];
}
quesSaveParam.newQuestion.push(...movedOutOfCycleQuestions);
}
}