feat[preview]: 抽离矩阵的验证内容
- 抽离 preview 矩阵的验证内容,多选矩阵的校验放到 PreviewMatrixCheckbox 内 - 添加新的错误提示支持 - 增加 矩阵 类型 - 添加矩阵测试文件
This commit is contained in:
4
src/types/question.d.ts
vendored
4
src/types/question.d.ts
vendored
@@ -34,7 +34,7 @@ declare interface IQuestionOption {
|
|||||||
relation_last_scope: number;
|
relation_last_scope: number;
|
||||||
relation_first_scope: number;
|
relation_first_scope: number;
|
||||||
relation_question_index: number;
|
relation_question_index: number;
|
||||||
options?: questionOptionType[];
|
options?: IMatrixCheckboxOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 答案 config 类型
|
// 答案 config 类型
|
||||||
@@ -89,7 +89,7 @@ export declare interface IQuestion<QuestionConfig> {
|
|||||||
stem?: string;
|
stem?: string;
|
||||||
other?: string;
|
other?: string;
|
||||||
// options 列表项,第一个是默认
|
// options 列表项,第一个是默认
|
||||||
list: questionOptionType[];
|
list: questionOptionType[] | IQuestionOption[];
|
||||||
question_index?: number;
|
question_index?: number;
|
||||||
question_type?: number;
|
question_type?: number;
|
||||||
// 如果没有自定义类型,那么就直接用基础 config 类型
|
// 如果没有自定义类型,那么就直接用基础 config 类型
|
||||||
|
|||||||
70
src/types/questions/matrixCheckbox.ts
Normal file
70
src/types/questions/matrixCheckbox.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export declare interface IMatrixCheckboxConfig extends IBaseConfig {
|
||||||
|
/** 是否必选 (1-是 0-否) */
|
||||||
|
is_required: 0 | 1;
|
||||||
|
/** 是否交换行列 */
|
||||||
|
is_change_row_cell?: boolean;
|
||||||
|
/** 暂未得知 */
|
||||||
|
quick_type: 0 | 1;
|
||||||
|
/** 每题选项数 */
|
||||||
|
each_number: number;
|
||||||
|
/** 最大选择数 (空字符串表示无限制) */
|
||||||
|
max_select: number | '';
|
||||||
|
/** 最小选择数 (空字符串表示无限制) */
|
||||||
|
min_select: number | '';
|
||||||
|
/** 随机选择数量 */
|
||||||
|
select_random: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface IMatrixCheckboxOption {
|
||||||
|
/** 选项HTML内容 */
|
||||||
|
option: string;
|
||||||
|
/** 是否为"其他"选项 (0-否 1-是) */
|
||||||
|
is_other: 0 | 1;
|
||||||
|
/** 是否固定位置 (0-否 1-是) */
|
||||||
|
is_fixed: 0 | 1;
|
||||||
|
/** 是否移除其他选项 (0-否 1-是) */
|
||||||
|
is_remove_other: 0 | 1;
|
||||||
|
/** 层级深度 */
|
||||||
|
level: number;
|
||||||
|
/** 选项唯一键 */
|
||||||
|
option_key: string;
|
||||||
|
/** 选项索引(可能不连续) */
|
||||||
|
option_index: string;
|
||||||
|
/** 选项编码 */
|
||||||
|
option_code: string;
|
||||||
|
/** 选项配置 */
|
||||||
|
option_config: IMatrixCheckboxOptionConfig;
|
||||||
|
/** 父选项索引 */
|
||||||
|
parent_option_index: number;
|
||||||
|
/** 子选项(树形结构) */
|
||||||
|
children: null;
|
||||||
|
/** 最小选择数 */
|
||||||
|
min_select: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface IMatrixCheckboxOptionConfig {
|
||||||
|
/** 选项类型 */
|
||||||
|
type: number;
|
||||||
|
/** 关联价格 */
|
||||||
|
price: number;
|
||||||
|
/** 标题 */
|
||||||
|
title: string;
|
||||||
|
/** 渐变样式 */
|
||||||
|
gradient: string;
|
||||||
|
/** 图片URL数组 */
|
||||||
|
image_url: string[];
|
||||||
|
/** 子区域配置 */
|
||||||
|
child_area: null | {
|
||||||
|
// 根据实际业务补充具体结构
|
||||||
|
area_type?: number;
|
||||||
|
coordinates?: number[];
|
||||||
|
};
|
||||||
|
/** 选项类型标记 */
|
||||||
|
option_type: number;
|
||||||
|
/** 说明内容数组 */
|
||||||
|
instructions: string[];
|
||||||
|
/** 绑定商品ID */
|
||||||
|
binding_goods_id: string;
|
||||||
|
/** 右侧限制内容(HTML) */
|
||||||
|
limit_right_content: string;
|
||||||
|
}
|
||||||
@@ -9,13 +9,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IQuestion } from '@/types/question';
|
||||||
|
import type { IMatrixCheckboxConfig } from '@/types/questions/matrixCheckbox';
|
||||||
|
|
||||||
// 接受获取到的 col row 的索引参数
|
// 接受获取到的 col row 的索引参数
|
||||||
const rowIndex = defineModel<number>('rowIndex', { required: true, default: 0 });
|
const rowIndex = defineModel<number>('rowIndex', { required: true, default: 0 });
|
||||||
const colIndex = defineModel<number>('colIndex', { required: true, default: 0 });
|
const colIndex = defineModel<number>('colIndex', { required: true, default: 0 });
|
||||||
|
|
||||||
const rowRecord = defineModel<number[][]>('rowRecord', { required: false, default: () => [] });
|
const rowRecord = defineModel<number[][]>('rowRecord', { required: false, default: () => [] });
|
||||||
|
|
||||||
const element = defineModel<question>('element', {
|
const element = defineModel<IQuestion<IMatrixCheckboxConfig>>('element', {
|
||||||
required: false,
|
required: false,
|
||||||
default: () => {
|
default: () => {
|
||||||
/**/
|
/**/
|
||||||
@@ -61,6 +64,7 @@ const emitValue = (/* val: unknown */) => {
|
|||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '@/assets/css/main';
|
@import '@/assets/css/main';
|
||||||
|
|
||||||
input[type='checkbox'] {
|
input[type='checkbox'] {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
@@ -73,9 +77,11 @@ input[type='checkbox'] {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: border-color 0.4s ease;
|
transition: border-color 0.4s ease;
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
border-color: $theme-color;
|
border-color: $theme-color;
|
||||||
background: $theme-color;
|
background: $theme-color;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '\2713';
|
content: '\2713';
|
||||||
font-family: 'Arial', sans-serif; // 确保符号正常显示
|
font-family: 'Arial', sans-serif; // 确保符号正常显示
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Io5EllipsisVerticalSharp } from 'vue-icons-plus/io5';
|
|||||||
import MatrixCheckbox from '@/views/Design/components/Questions/MatrixCheckbox.vue';
|
import MatrixCheckbox from '@/views/Design/components/Questions/MatrixCheckbox.vue';
|
||||||
import MatrixText from '@/views/Design/components/Questions/MatrixText.vue';
|
import MatrixText from '@/views/Design/components/Questions/MatrixText.vue';
|
||||||
import MatrixRadio from '@/views/Design/components/Questions/MatrixRadio.vue';
|
import MatrixRadio from '@/views/Design/components/Questions/MatrixRadio.vue';
|
||||||
|
import type { IQuestion } from '@/types/question';
|
||||||
|
import type { IMatrixCheckboxConfig } from '@/types/questions/matrixCheckbox';
|
||||||
|
|
||||||
// 添加激活 action 的选项
|
// 添加激活 action 的选项
|
||||||
interface _questionOptionType extends questionOptionType {
|
interface _questionOptionType extends questionOptionType {
|
||||||
@@ -11,7 +13,7 @@ interface _questionOptionType extends questionOptionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 题目
|
// 题目
|
||||||
const element = defineModel<question>('element', {
|
const element = defineModel<IQuestion<IMatrixCheckboxConfig>>('element', {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {
|
default: () => {
|
||||||
return {};
|
return {};
|
||||||
@@ -41,6 +43,7 @@ const emitValue = () => {
|
|||||||
|
|
||||||
// 组件选择
|
// 组件选择
|
||||||
const activeComponent = selectActiveComponent();
|
const activeComponent = selectActiveComponent();
|
||||||
|
|
||||||
function selectActiveComponent(): Component {
|
function selectActiveComponent(): Component {
|
||||||
switch (element.value.question_type) {
|
switch (element.value.question_type) {
|
||||||
case 8:
|
case 8:
|
||||||
@@ -87,6 +90,7 @@ function handleActionSelect(action: { text: string }, axi: any, type: 'row' | 'c
|
|||||||
}
|
}
|
||||||
|
|
||||||
addShowActionOption();
|
addShowActionOption();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 给行或者列选项 添加 showAction 选项
|
* 给行或者列选项 添加 showAction 选项
|
||||||
*/
|
*/
|
||||||
@@ -113,7 +117,7 @@ const errorMessage = defineModel('errorMessage', {
|
|||||||
label-align="top"
|
label-align="top"
|
||||||
class="contenteditable-question-title"
|
class="contenteditable-question-title"
|
||||||
>
|
>
|
||||||
<template #left-icon> {{ isPreview ? element.title : index + 1 }}. </template>
|
<template #left-icon> {{ isPreview ? element.title : index + 1 }}.</template>
|
||||||
<!-- 使用 title 插槽来自定义标题 -->
|
<!-- 使用 title 插槽来自定义标题 -->
|
||||||
<template #label>
|
<template #label>
|
||||||
<contenteditable
|
<contenteditable
|
||||||
@@ -220,6 +224,7 @@ input[type='text'] {
|
|||||||
//top: 15%;
|
//top: 15%;
|
||||||
//right: 0;
|
//right: 0;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -539,6 +539,7 @@ import PreviewCheckbox from '@/views/Survey/views/Preview/components/questions/P
|
|||||||
import PreviewRate from '@/views/Survey/views/Preview/components/questions/PreviewRate.vue';
|
import PreviewRate from '@/views/Survey/views/Preview/components/questions/PreviewRate.vue';
|
||||||
import PreviewSign from '@/views/Survey/views/Preview/components/questions/PreviewSign.vue';
|
import PreviewSign from '@/views/Survey/views/Preview/components/questions/PreviewSign.vue';
|
||||||
import PreviewTextWithImages from '@/views/Survey/views/Preview/components/questions/PreviewTextWithImages.vue';
|
import PreviewTextWithImages from '@/views/Survey/views/Preview/components/questions/PreviewTextWithImages.vue';
|
||||||
|
|
||||||
const isPreview = defineModel('isPreview', {
|
const isPreview = defineModel('isPreview', {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
@@ -697,86 +698,6 @@ async function answer(callback, callbackBeforePage) {
|
|||||||
question.error = translatedText.value.PleaseInputAValue;
|
question.error = translatedText.value.PleaseInputAValue;
|
||||||
} else if (answer && questionType === 2) {
|
} else if (answer && questionType === 2) {
|
||||||
} else if (answer && questionType === 10) {
|
} else if (answer && questionType === 10) {
|
||||||
// 矩阵多选题,列分组时,校验选项数量
|
|
||||||
const cellGroups = (config?.cell_option_groups?.option_group || []).filter(
|
|
||||||
(i) => i.groups?.length
|
|
||||||
);
|
|
||||||
if (cellGroups.length) {
|
|
||||||
const rows = question.list.reduce(
|
|
||||||
(p, c) => [...p, ...(c.type === 1 ? c.options || [] : [])],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const cols = question.list.reduce(
|
|
||||||
(p, c) => [...p, ...(c.type === 2 ? c.options || [] : [])],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const freeCols = cols.filter(
|
|
||||||
(col) => !cellGroups.some((g) => g.groups.find((c) => c.option_key === col.option_key))
|
|
||||||
);
|
|
||||||
|
|
||||||
const arr = rows.map((row) => {
|
|
||||||
return cellGroups.map((group) => {
|
|
||||||
return group.groups.map((col) => {
|
|
||||||
return `${row.option_key}_${col.option_key}`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
arr.forEach((a, idx) =>
|
|
||||||
a.push(freeCols.map((col) => `${rows[idx].option_key}_${col.option_key}`))
|
|
||||||
);
|
|
||||||
const answered = Object.keys(answer);
|
|
||||||
|
|
||||||
cellGroups.forEach((group, idx) => {
|
|
||||||
if (!group.groups?.length || (!group.max && !group.min) || isError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < arr.length; i += 1) {
|
|
||||||
const row = arr[i];
|
|
||||||
const selectedCount = row[idx].filter((key) => answered.includes(key)).length;
|
|
||||||
if (group.min && selectedCount < group.min) {
|
|
||||||
isError = true;
|
|
||||||
question.error = translatedText.value.PleaseSelectAtLeastOneOptionsInTitleGroup(
|
|
||||||
group.min,
|
|
||||||
group.title
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (group.max && selectedCount > group.max) {
|
|
||||||
isError = true;
|
|
||||||
question.error = translatedText.value.PleaseSelectAtMostOneOptionsInTitleGroup(
|
|
||||||
group.max,
|
|
||||||
group.title
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 最少选择数量校验
|
|
||||||
const minSelect = +config.min_select || 0;
|
|
||||||
const perLineSelectedCount = Object.keys(answer).reduce(
|
|
||||||
(p, c) => {
|
|
||||||
if (+answer[c]) {
|
|
||||||
p[+c.split('_')[0] - 1] += 1;
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
},
|
|
||||||
question.list.filter((i) => i.type === 1).flatMap((i) => i.options.map(() => 0))
|
|
||||||
);
|
|
||||||
if (minSelect && minSelect > Math.max(...perLineSelectedCount)) {
|
|
||||||
if (question.config.is_change_row_cell) {
|
|
||||||
question.error = translatedText.value.PleaseSelectAtLeastOneOptionsPerColumn(
|
|
||||||
config.min_select || 1
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
question.error = translatedText.value.PleaseSelectAtLeastOneOptionsPerLine(
|
|
||||||
config.min_select || 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
isError = true;
|
|
||||||
}
|
|
||||||
// console.log('===', minSelect, Object.keys(answer), answer, perLineSelectedCount);
|
|
||||||
} else if (answer && questionType === 12) {
|
} else if (answer && questionType === 12) {
|
||||||
question.error = '';
|
question.error = '';
|
||||||
} else if (answer && questionType === 14 && Object.keys(answer).length < config.min_select) {
|
} else if (answer && questionType === 14 && Object.keys(answer).length < config.min_select) {
|
||||||
@@ -1301,6 +1222,7 @@ function updateAnswer(auto) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选项隐藏
|
// 选项隐藏
|
||||||
function hideOptions(hide) {
|
function hideOptions(hide) {
|
||||||
const questionIndex = hide?.question_index;
|
const questionIndex = hide?.question_index;
|
||||||
@@ -1353,6 +1275,7 @@ function toUrl(url) {
|
|||||||
open(modifiedUrl);
|
open(modifiedUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,18 +14,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MatrixQuestion from '@/views/Design/components/Questions/MatrixQuestion.vue';
|
import MatrixQuestion from '@/views/Design/components/Questions/MatrixQuestion.vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
// const questionType = defineModel<number>('questionType', { required: false });
|
import type { IQuestion, IQuestionOption } from '@/types/question';
|
||||||
|
import { validateMatrixCheckbox } from '@/views/Survey/views/Preview/components/questions/validate/validateMatrixCheckbox';
|
||||||
|
import type { MatrixCheckboxAnswerType } from '@/views/Survey/views/Preview/components/questions/types/previewMatrix';
|
||||||
|
import type { IMatrixCheckboxConfig } from '@/types/questions/matrixCheckbox';
|
||||||
|
|
||||||
// 矩阵多选的答案类型
|
// const questionType = defineModel<number>('questionType', { required: false });
|
||||||
type answerType = {
|
|
||||||
[key: string]: 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// preview props
|
// preview props
|
||||||
// const stem = defineModel('stem');
|
// const stem = defineModel('stem');
|
||||||
// const list = defineModel<questionsList[]>('list', { required: false });
|
// const list = defineModel<questionsList[]>('list', { required: false });
|
||||||
// const config = defineModel<OptionConfigType>('config', { required: false });
|
// const config = defineModel<OptionConfigType>('config', { required: false });
|
||||||
const question = defineModel<question>('question', { default: () => {} });
|
const question = defineModel<IQuestion<IMatrixCheckboxConfig>>('question', {
|
||||||
|
default: () => {}
|
||||||
|
});
|
||||||
|
console.log(`question value: `, question.value);
|
||||||
const answerIndex = computed(() => question.value.title);
|
const answerIndex = computed(() => question.value.title);
|
||||||
const emit = defineEmits(['changeAnswer', 'previous', 'next']);
|
const emit = defineEmits(['changeAnswer', 'previous', 'next']);
|
||||||
// 示例
|
// 示例
|
||||||
@@ -35,7 +38,7 @@ const emit = defineEmits(['changeAnswer', 'previous', 'next']);
|
|||||||
// "2_1": 1,
|
// "2_1": 1,
|
||||||
// "2_2": 1
|
// "2_2": 1
|
||||||
// }
|
// }
|
||||||
const answer = defineModel<answerType>('answer', {
|
const answer = defineModel<MatrixCheckboxAnswerType>('answer', {
|
||||||
// 临时赋值, 用于测试
|
// 临时赋值, 用于测试
|
||||||
// default: () => ({
|
// default: () => ({
|
||||||
// "1_1": 1,
|
// "1_1": 1,
|
||||||
@@ -63,7 +66,7 @@ answer.value && parseAnswer(answer.value);
|
|||||||
/**
|
/**
|
||||||
* 解析 answer
|
* 解析 answer
|
||||||
*/
|
*/
|
||||||
function parseAnswer(answer: answer) {
|
function parseAnswer(answer: MatrixCheckboxAnswerType) {
|
||||||
// console.log(`come in parseAnswer`);
|
// console.log(`come in parseAnswer`);
|
||||||
const rowRecordList: number[][] = [];
|
const rowRecordList: number[][] = [];
|
||||||
Object.entries(answer).forEach(([key]) => {
|
Object.entries(answer).forEach(([key]) => {
|
||||||
@@ -75,6 +78,7 @@ function parseAnswer(answer: answer) {
|
|||||||
|
|
||||||
return rowRecordList;
|
return rowRecordList;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看parseAnswer的返回值
|
// 查看parseAnswer的返回值
|
||||||
// console.log(`parseAnswer value:`, parseAnswer(answer.value!))
|
// console.log(`parseAnswer value:`, parseAnswer(answer.value!))
|
||||||
|
|
||||||
@@ -88,15 +92,32 @@ const cols = computed(() => question.value?.list[1]?.options ?? []);
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
rowRecord,
|
rowRecord,
|
||||||
() => {
|
(value) => {
|
||||||
// console.log(`record has changed`, rowRecord.value);
|
// console.log(`record has changed`, rowRecord.value);
|
||||||
// 重新生成 answer
|
// 重新生成 answer
|
||||||
const newAnswer: answer = {};
|
const newAnswer = {} as MatrixCheckboxAnswerType;
|
||||||
rowRecord.value.forEach((rowOptions, rowIndex) => {
|
rowRecord.value.forEach((rowOptions, rowIndex) => {
|
||||||
rowOptions.forEach((colIndex) => {
|
rowOptions.forEach((colIndex) => {
|
||||||
newAnswer[`${rowIndex + 1}_${colIndex + 1}`] = 1;
|
const key = `${rowIndex + 1}_${colIndex + 1}`;
|
||||||
|
newAnswer[key] = 1;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验
|
||||||
|
*/
|
||||||
|
const res: string = validateMatrixCheckbox(
|
||||||
|
value,
|
||||||
|
question.value.config!,
|
||||||
|
question.value.list as IQuestionOption[]
|
||||||
|
);
|
||||||
|
console.log('res', res);
|
||||||
|
question.value.error = res;
|
||||||
|
// 如果校验失败,清空 answer
|
||||||
|
if (res.length) {
|
||||||
|
question.value.answer = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
answer.value = newAnswer;
|
answer.value = newAnswer;
|
||||||
emit('changeAnswer', newAnswer);
|
emit('changeAnswer', newAnswer);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export type MatrixCheckboxAnswerType = { [key: string]: 1 | number };
|
||||||
|
s;
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { validateMatrixCheckbox } from '../validateMatrixCheckbox';
|
||||||
|
import { getLanguage } from '@/views/Survey/views/Preview/js/language';
|
||||||
|
import type { ITranslatedText } from '@/views/Survey/views/Preview/components/questions/types/translatedText';
|
||||||
|
|
||||||
|
// 模拟翻译函数
|
||||||
|
const mockTranslatedText: ITranslatedText = getLanguage(['zh']);
|
||||||
|
|
||||||
|
describe('validateMatrixCheckbox 函数测试', () => {
|
||||||
|
// 基础测试数据
|
||||||
|
const baseOptions = [
|
||||||
|
{
|
||||||
|
type: 1, // 行选项
|
||||||
|
options: [
|
||||||
|
{ option_key: 'row1', option: '行1' },
|
||||||
|
{ option_key: 'row2', option: '行2' },
|
||||||
|
{ option_key: 'row3', option: '行3' }
|
||||||
|
],
|
||||||
|
cite_type: 0,
|
||||||
|
relation_type: 0,
|
||||||
|
relation_out_scope: [],
|
||||||
|
relation_last_scope: 0,
|
||||||
|
relation_first_scope: 0,
|
||||||
|
relation_question_index: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2, // 列选项
|
||||||
|
options: [
|
||||||
|
{ option_key: 'col1', option: '列1' },
|
||||||
|
{ option_key: 'col2', option: '列2' },
|
||||||
|
{ option_key: 'col3', option: '列3' }
|
||||||
|
],
|
||||||
|
cite_type: 0,
|
||||||
|
relation_type: 0,
|
||||||
|
relation_out_scope: [],
|
||||||
|
relation_last_scope: 0,
|
||||||
|
relation_first_scope: 0,
|
||||||
|
relation_question_index: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const baseConfig = {
|
||||||
|
is_required: 1,
|
||||||
|
quick_type: 0,
|
||||||
|
each_number: 3,
|
||||||
|
max_select: 3,
|
||||||
|
min_select: 1,
|
||||||
|
select_random: 0,
|
||||||
|
is_change_row_cell: false,
|
||||||
|
float_window_content: '',
|
||||||
|
is_show: [],
|
||||||
|
is_brand: 0,
|
||||||
|
is_behavior: 0,
|
||||||
|
is_initialize: 0,
|
||||||
|
is_price_tag: 0,
|
||||||
|
is_three_dimensions: 0,
|
||||||
|
is_default_perspective: 0,
|
||||||
|
popup_window: 0,
|
||||||
|
popup_window_content: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
it('当选项为空数组时,应返回空字符串', () => {
|
||||||
|
const answer = [];
|
||||||
|
const result = validateMatrixCheckbox(answer, baseConfig, [], mockTranslatedText);
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当所有行都满足最小选择数要求时,应返回空字符串', () => {
|
||||||
|
const answer = [
|
||||||
|
[1], // 第1行选择了1个
|
||||||
|
[1, 2], // 第2行选择了2个
|
||||||
|
[1, 2, 3] // 第3行选择了3个
|
||||||
|
];
|
||||||
|
const result = validateMatrixCheckbox(answer, baseConfig, baseOptions, mockTranslatedText);
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当某行选择数小于最小要求时,应返回相应错误信息', () => {
|
||||||
|
const answer = [
|
||||||
|
[1], // 第1行选择了1个
|
||||||
|
[], // 第2行没有选择
|
||||||
|
[1, 2] // 第3行选择了2个
|
||||||
|
];
|
||||||
|
const result = validateMatrixCheckbox(answer, baseConfig, baseOptions, mockTranslatedText);
|
||||||
|
expect(result).toBe('第2行最少选1个。');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当某行选择数超过最大要求时,应返回相应错误信息', () => {
|
||||||
|
const config = { ...baseConfig, max_select: 2 };
|
||||||
|
const answer = [
|
||||||
|
[1], // 第1行选择了1个
|
||||||
|
[1, 2], // 第2行选择了2个
|
||||||
|
[1, 2, 3] // 第3行选择了3个,超过了最大值2
|
||||||
|
];
|
||||||
|
const result = validateMatrixCheckbox(answer, config, baseOptions, mockTranslatedText);
|
||||||
|
expect(result).toBe('第3行最多选2个。');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当设置is_change_row_cell=true时,应返回列相关的错误信息', () => {
|
||||||
|
const config = { ...baseConfig, is_change_row_cell: true };
|
||||||
|
const answer = [
|
||||||
|
[1], // 第1行选择了1个
|
||||||
|
[], // 第2行没有选择
|
||||||
|
[1, 2] // 第3行选择了2个
|
||||||
|
];
|
||||||
|
const result = validateMatrixCheckbox(answer, config, baseOptions, mockTranslatedText);
|
||||||
|
expect(result).toBe('第2列最少选1个。');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当min_select为0时,不应进行最小值校验', () => {
|
||||||
|
const config = { ...baseConfig, min_select: 0 };
|
||||||
|
const answer = [
|
||||||
|
[], // 第1行没有选择
|
||||||
|
[], // 第2行没有选择
|
||||||
|
[] // 第3行没有选择
|
||||||
|
];
|
||||||
|
const result = validateMatrixCheckbox(answer, config, baseOptions, mockTranslatedText);
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当min_select为空字符串时,应将其转换为0并且不进行最小值校验', () => {
|
||||||
|
const config = { ...baseConfig, min_select: '' as any };
|
||||||
|
const answer = [
|
||||||
|
[], // 第1行没有选择
|
||||||
|
[], // 第2行没有选择
|
||||||
|
[] // 第3行没有选择
|
||||||
|
];
|
||||||
|
const result = validateMatrixCheckbox(answer, config, baseOptions, mockTranslatedText);
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当max_select为0或空字符串时,不应进行最大值校验', () => {
|
||||||
|
const config1 = { ...baseConfig, max_select: 0 };
|
||||||
|
const config2 = { ...baseConfig, max_select: '' as any };
|
||||||
|
const answer = [
|
||||||
|
[1, 2, 3, 4, 5], // 第1行选择了5个
|
||||||
|
[1, 2, 3, 4], // 第2行选择了4个
|
||||||
|
[1, 2, 3] // 第3行选择了3个
|
||||||
|
];
|
||||||
|
|
||||||
|
const result1 = validateMatrixCheckbox(answer, config1, baseOptions, mockTranslatedText);
|
||||||
|
const result2 = validateMatrixCheckbox(answer, config2, baseOptions, mockTranslatedText);
|
||||||
|
|
||||||
|
expect(result1).toBe('');
|
||||||
|
expect(result2).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当有多行不满足条件时,应返回第一个检测到的错误', () => {
|
||||||
|
const config = { ...baseConfig, min_select: 2, max_select: 3 };
|
||||||
|
const answer = [
|
||||||
|
[1], // 第1行选择不足
|
||||||
|
[], // 第2行选择不足
|
||||||
|
[1, 2, 3, 4] // 第3行选择过多
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = validateMatrixCheckbox(answer, config, baseOptions, mockTranslatedText);
|
||||||
|
expect(result).toBe('第1行最少选2个。');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当min_select和max_select都有限制时,应先检查最小值再检查最大值', () => {
|
||||||
|
const config = { ...baseConfig, min_select: 2, max_select: 3 };
|
||||||
|
|
||||||
|
// 测试最小值不满足的情况
|
||||||
|
const answer1 = [
|
||||||
|
[1], // 第1行选择不足
|
||||||
|
[1, 2, 3, 4] // 第2行选择过多
|
||||||
|
];
|
||||||
|
const result1 = validateMatrixCheckbox(answer1, config, baseOptions, mockTranslatedText);
|
||||||
|
expect(result1).toBe('第1行最少选2个。');
|
||||||
|
|
||||||
|
// 测试最大值不满足的情况
|
||||||
|
const answer2 = [
|
||||||
|
[1, 2], // 第1行满足最小值
|
||||||
|
[1, 2, 3, 4] // 第2行选择过多
|
||||||
|
];
|
||||||
|
const result2 = validateMatrixCheckbox(answer2, config, baseOptions, mockTranslatedText);
|
||||||
|
expect(result2).toBe('第2行最多选3个。');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { getLanguage } from '@/views/Survey/views/Preview/js/language';
|
||||||
|
import type { IQuestionOption } from '@/types/question';
|
||||||
|
import type { IMatrixCheckboxConfig } from '@/types/questions/matrixCheckbox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 矩阵多选题校验
|
||||||
|
* @param answer 答案
|
||||||
|
* @param config 配置
|
||||||
|
* @param options 选项
|
||||||
|
* @param translatedText 语言
|
||||||
|
*/
|
||||||
|
function validateMatrixCheckbox(
|
||||||
|
answer: number[][],
|
||||||
|
config: IMatrixCheckboxConfig,
|
||||||
|
options: IQuestionOption[],
|
||||||
|
translatedText = getLanguage(['zh'])
|
||||||
|
) {
|
||||||
|
const [rows, cols] = options;
|
||||||
|
// 矩阵多选题,列选项分组时,校验选项数量
|
||||||
|
// const cellGroups = (config?.cell_option_groups?.option_group || []).filter(
|
||||||
|
// (i) => i.groups?.length
|
||||||
|
// );
|
||||||
|
// if (cellGroups.length) {
|
||||||
|
// const rows = question.list.reduce(
|
||||||
|
// (p, c) => [...p, ...(c.type === 1 ? c.options || [] : [])],
|
||||||
|
// []
|
||||||
|
// );
|
||||||
|
// const cols = question.list.reduce(
|
||||||
|
// (p, c) => [...p, ...(c.type === 2 ? c.options || [] : [])],
|
||||||
|
// []
|
||||||
|
// );
|
||||||
|
// const freeCols = cols.filter(
|
||||||
|
// (col) => !cellGroups.some((g) => g.groups.find((c) => c.option_key === col.option_key))
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const arr = rows.map((row) => {
|
||||||
|
// return cellGroups.map((group) => {
|
||||||
|
// return group.groups.map((col) => {
|
||||||
|
// return `${row.option_key}_${col.option_key}`;
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// arr.forEach((a, idx) =>
|
||||||
|
// a.push(freeCols.map((col) => `${rows[idx].option_key}_${col.option_key}`))
|
||||||
|
// );
|
||||||
|
// const answered = Object.keys(answer);
|
||||||
|
//
|
||||||
|
// cellGroups.forEach((group, idx) => {
|
||||||
|
// if (!group.groups?.length || (!group.max && !group.min) || isError) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for (let i = 0; i < arr.length; i += 1) {
|
||||||
|
// const row = arr[i];
|
||||||
|
// const selectedCount = row[idx].filter((key) => answered.includes(key)).length;
|
||||||
|
// if (group.min && selectedCount < group.min) {
|
||||||
|
// isError = true;
|
||||||
|
// question.error = translatedText.value.PleaseSelectAtLeastOneOptionsInTitleGroup(
|
||||||
|
// group.min,
|
||||||
|
// group.title
|
||||||
|
// );
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// if (group.max && selectedCount > group.max) {
|
||||||
|
// isError = true;
|
||||||
|
// question.error = translatedText.value.PleaseSelectAtMostOneOptionsInTitleGroup(
|
||||||
|
// group.max,
|
||||||
|
// group.title
|
||||||
|
// );
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// 最少选择数量校验
|
||||||
|
// 转换配置值为数字
|
||||||
|
const minSelect = +config.min_select;
|
||||||
|
const maxSelect = +config.max_select;
|
||||||
|
// 如果选项为空,直接返回空字符串
|
||||||
|
if (!options.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误信息
|
||||||
|
let errorMessage = '';
|
||||||
|
|
||||||
|
// 最大和最小选择数量校验
|
||||||
|
answer.forEach((row, rowIndex) => {
|
||||||
|
// 如果没有最小选择要求或最小选择为0,不进行最小值校验
|
||||||
|
if (minSelect >= 0 && row.length < minSelect) {
|
||||||
|
// 如果已经有错误信息,不进行最小值校验
|
||||||
|
if (errorMessage) return;
|
||||||
|
// 是否行列转换
|
||||||
|
errorMessage = config.is_change_row_cell
|
||||||
|
? translatedText.PleaseSelectAtLeastOneOptionsPerColumn(minSelect, rowIndex)
|
||||||
|
: translatedText.PleaseSelectAtLeastOneOptionsPerLine(minSelect, rowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有最大选择限制,进行最大值校验
|
||||||
|
if (maxSelect > 0 && row.length > maxSelect) {
|
||||||
|
// 如果已经有错误信息,不进行最大值校验
|
||||||
|
if (errorMessage) return;
|
||||||
|
// 是否行列转换
|
||||||
|
errorMessage = config.is_change_row_cell
|
||||||
|
? translatedText.PleaseSelectLessOptionsPerColumn(maxSelect, rowIndex)
|
||||||
|
: translatedText.PleaseSelectLessOptionsPerLine(maxSelect, rowIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检测所有的答案是否正常选择
|
||||||
|
if (errorMessage.length === 0 && rows.options!.length > answer.length) {
|
||||||
|
errorMessage = translatedText.PleaseSelectAllRows;
|
||||||
|
}
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { validateMatrixCheckbox };
|
||||||
@@ -5,13 +5,36 @@ export const language = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
PleaseSelectAtLeastOneOptionsPerLine: {
|
PleaseSelectAtLeastOneOptionsPerLine: {
|
||||||
en: (count) => `Please select at least ${count} answer option${count > 1 ? 's' : ''} per row.`,
|
en: (count, row) =>
|
||||||
zh: (count) => `每行最少选${count}个。`
|
typeof row === 'number'
|
||||||
|
? `Please select at least ${count} answer option${count > 1 ? 's' : ''} at row ${row + 1}.`
|
||||||
|
: `Please select at least ${count} answer option${count > 1 ? 's' : ''} per row.`,
|
||||||
|
zh: (count, row) =>
|
||||||
|
typeof row === 'number' ? `第${row + 1}行最少选${count}个。` : `每行最少选${count}个。`
|
||||||
|
},
|
||||||
|
PleaseSelectLessOptionsPerLine: {
|
||||||
|
en: (count, row) =>
|
||||||
|
typeof row === 'number'
|
||||||
|
? `Please select no more than ${count} answer option${count > 1 ? 's' : ''} at row ${row + 1}.`
|
||||||
|
: `Please select no more than ${count} answer option${count > 1 ? 's' : ''} per row.`,
|
||||||
|
zh: (count, row) =>
|
||||||
|
typeof row === 'number' ? `第${row + 1}行最多选${count}个。` : `每行最多选${count}个。`
|
||||||
},
|
},
|
||||||
PleaseSelectAtLeastOneOptionsPerColumn: {
|
PleaseSelectAtLeastOneOptionsPerColumn: {
|
||||||
en: (count) =>
|
en: (count, column) =>
|
||||||
`Please select at least ${count} answer option${count > 1 ? 's' : ''} per column.`,
|
typeof column === 'number'
|
||||||
zh: (count) => `每列最少选${count}个。`
|
? `Please select at least ${count} answer option${count > 1 ? 's' : ''} at column ${column + 1}.`
|
||||||
|
: `Please select at least ${count} answer option${count > 1 ? 's' : ''} per column.`,
|
||||||
|
zh: (count, column) =>
|
||||||
|
typeof column === 'number' ? `第${column + 1}列最少选${count}个。` : `每列最少选${count}个。`
|
||||||
|
},
|
||||||
|
PleaseSelectLessOptionsPerColumn: {
|
||||||
|
en: (count, column) =>
|
||||||
|
typeof column === 'number'
|
||||||
|
? `Please select no more than ${count} answer option${count > 1 ? 's' : ''} at column ${column + 1}.`
|
||||||
|
: `Please select no more than ${count} answer option${count > 1 ? 's' : ''} per column.`,
|
||||||
|
zh: (count, column) =>
|
||||||
|
typeof column === 'number' ? `第${column + 1}列最多选${count}个。` : `每列最多选${count}个。`
|
||||||
},
|
},
|
||||||
PleaseCategorizeAllOptions: {
|
PleaseCategorizeAllOptions: {
|
||||||
en: 'Please categorize all answer options.',
|
en: 'Please categorize all answer options.',
|
||||||
@@ -21,6 +44,10 @@ export const language = {
|
|||||||
en: 'This is compulsory.',
|
en: 'This is compulsory.',
|
||||||
zh: '请填写当前题目'
|
zh: '请填写当前题目'
|
||||||
},
|
},
|
||||||
|
PleaseSelectAllRows: {
|
||||||
|
en: 'Please select all rows.',
|
||||||
|
zh: '请完成所有行。'
|
||||||
|
},
|
||||||
PleaseInputAValue: {
|
PleaseInputAValue: {
|
||||||
en: 'Please input a value.',
|
en: 'Please input a value.',
|
||||||
zh: '请输入。'
|
zh: '请输入。'
|
||||||
|
|||||||
Reference in New Issue
Block a user