diff --git a/src/assets/js/logic.js b/src/assets/js/logic.js new file mode 100644 index 0000000..c2d4dbd --- /dev/null +++ b/src/assets/js/logic.js @@ -0,0 +1,424 @@ +/** + * 问卷设计中涉及到的一些 【逻辑】 中公共方法 + */ +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} + */ +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); + } +} diff --git a/src/layouts/config3d.constant.js b/src/layouts/config3d.constant.js new file mode 100644 index 0000000..d5818c3 --- /dev/null +++ b/src/layouts/config3d.constant.js @@ -0,0 +1,32 @@ +// 问题类型 +export const QUESTION_TYPE = { + // 单选 + CHOICE: 1, + + // 多选 + CHOICE_MULT: 2, + + // 输入 + INPUT_MULT: 4, + + // 图文说明 + IMG_TEXT_EXPLAIN: 6, + + // 热区-开关 + HOTAREA: 25, + + // 热区-喜欢/不喜欢 + HOTAREA_2: 26, + + // 矩阵-填空 + MATRIX_INPUT: 8, + + // 矩阵-单选 + MATRIX_RADIO: 9, + + contains(item) { + return Object.keys(this).some((key) => this[key] == item); + } +}; + +export default {}; diff --git a/src/layouts/logic.js b/src/layouts/logic.js new file mode 100644 index 0000000..c2d4dbd --- /dev/null +++ b/src/layouts/logic.js @@ -0,0 +1,424 @@ +/** + * 问卷设计中涉及到的一些 【逻辑】 中公共方法 + */ +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} + */ +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); + } +} diff --git a/src/layouts/utils.js b/src/layouts/utils.js index d66baa6..91596a9 100644 --- a/src/layouts/utils.js +++ b/src/layouts/utils.js @@ -1,7 +1,9 @@ - import { showConfirmDialog } from 'vant'; import { getQuestionList, getCheckSurvey } from '@/api/survey'; import appBridge from '@/assets/js/appBridge'; +import { QUESTION_TYPE } from '@/layouts/config3d.constant.js'; +import { loopingAvailable } from '@/layouts/logic.js'; +import { getDomText } from '@/utils/utils'; // /** // * 统一的弹窗 // * @param options @@ -39,13 +41,13 @@ function showModal(options) { * @param {*} data * @returns */ -const canPlanetPublishPSM = function(data) { +const canPlanetPublishPSM = function (data) { let isFb = true; let message = ''; let title = '题目设置未完成'; const incompleteQuestionList = []; - data.questions - && data.questions.forEach((s) => { + data.questions && + data.questions.forEach((s) => { if (s.question_type === 101 && s.config.price_gradient.length <= 0) { isFb = false; message = 'psm题目未完成设置,请设置价格区间后投放'; @@ -70,15 +72,15 @@ const canPlanetPublishPSM = function(data) { * @param {*} data * @returns */ -const canPlanetPublishMxdAndHotArea = function(data) { +const canPlanetPublishMxdAndHotArea = function (data) { let isFb = true; let message = ''; const qSteams = []; const incompleteQuestionList = []; let type = 0; let title = '题目设置未完成'; - data.questions - && data.questions.forEach((s) => { + data.questions && + data.questions.forEach((s) => { if (s.question_type === 105 && s.config.design_version <= 0) { isFb = false; message = 'maxdiff题目未完成设置,请生成设计后投放'; @@ -117,14 +119,14 @@ const canPlanetPublishMxdAndHotArea = function(data) { * @param {*} data * @returns */ -const canPlanetPublish3D = function(data) { +const canPlanetPublish3D = function (data) { { let canFB = true; let message = ''; const qSteams = []; let title = ''; - data.questions - && data.questions.forEach((s) => { + data.questions && + data.questions.forEach((s) => { if (QUESTION_TYPE.contains(s.question_type)) { try { if (s.config.is_three_dimensions && !s.config.scene) { @@ -154,17 +156,17 @@ const canPlanetPublish3D = function(data) { let message = ''; const qSteams = []; let title = ''; - data.questions - && data.questions.forEach((s) => { + data.questions && + data.questions.forEach((s) => { if (QUESTION_TYPE.contains(s.question_type)) { try { if (s.config.is_three_dimensions && s.config.is_binding_goods) { const wares = []; - const scene_information = s.config.scene_information; - const sceneInformation - = typeof scene_information === 'string' - ? JSON.parse(scene_information) - : scene_information; + const _sceneInformation = s.config.scene_information; + const sceneInformation = + typeof _sceneInformation === 'string' + ? JSON.parse(_sceneInformation) + : _sceneInformation; sceneInformation.shelves.forEach((shelf) => { shelf.wares.forEach((ware) => { if (!ware.option_index) return; @@ -209,15 +211,15 @@ const canPlanetPublish3D = function(data) { * @param {*} data * @returns */ -const canPlanetPublishImage = function(data) { +const canPlanetPublishImage = function (data) { { let canFB = true; let message = ''; const qSteams = []; let title = ''; - data.questions - && data.questions.forEach((s) => { - if (s.question_type == 13) { + data.questions && + data.questions.forEach((s) => { + if (s.question_type === 13) { try { if (s.options.length <= 0 || s.options.some((y) => y.length <= 0)) { canFB = false; @@ -310,8 +312,8 @@ function canPublishRandom(data, publishType) { if (!isValidated) { errors.push({ message: - field.message - || `请填写"${random.title}"中第${index + 1}组"随机题组"的"${field.name}"` + field.message || + `请填写"${random.title}"中第${index + 1}组"随机题组"的"${field.name}"` }); } }); @@ -371,11 +373,11 @@ function isLoopingLogicValid(data, publishType) { if ( (data?.cycle_pages || []).every((i) => { return ( - i.question_index - && i.relation_type !== undefined - && i.relation_type !== null - && i.first_page - && i.last_page + i.question_index && + i.relation_type !== undefined && + i.relation_type !== null && + i.first_page && + i.last_page ); }) ) { @@ -387,22 +389,15 @@ function isLoopingLogicValid(data, publishType) { }); } -<<<<<<< HEAD - showDialog({ - class: 'custom-modal custom-modal-title-notice show-icon', - title: '修改循环', - content: `循环题组不完全,请前往循环列表修改后${publishStr}` -======= showModal({ title: '修改循环', message: `循环题组不完全,请前往循环列表修改后${publishStr}` ->>>>>>> feature/feature-20250331-h5 }); return false; } -export const canPlanetPublish = async function(sn, publishType) { +export const canPlanetPublish = async function (sn, publishType) { const parsedPublishType = !publishType ? 2 : publishType; const num = window.location.href.indexOf('code='); let code;