/** * 问卷设计中涉及到的一些 【逻辑】 中公共方法 */ import config from '@/config'; import { showDialog } from 'vant'; const advancedQuesTypeList = [ { name: 'Maxdiff', icon: '', check: false, type: 105 }, { name: 'CBC', icon: '', check: false, type: 103 }, { name: 'BPTO', icon: '', check: false, type: 104 }, { name: 'PSM', icon: '', check: false, type: 101 }, { name: 'KANO', icon: '', check: false, type: 102 } ]; // 高级题型的 question_type const advancedTypes = advancedQuesTypeList.map((i) => i.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} */ 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)) { showDialog({ title: '无法操作', width: '450px', message: 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; // 计数,在 questions 有几个分页的对象 let pageObjectCount = 0; 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; const 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]; // 跳转逻辑的方向,true 为从前向后跳转 const isPlainSequence = parsedRange1[0] === start1; 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) => [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); } }