feat: 重构矩阵问卷

- 重构矩阵,现在单选矩阵可以自由左右滚动
This commit is contained in:
Huangzhe
2025-03-19 17:12:21 +08:00
parent 1dd85051bb
commit 62cfc0986e
6 changed files with 201 additions and 198 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { /* useTemplateRef, */ ref, computed, type Component } from 'vue'; import { type Component, computed, defineModel } from 'vue';
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';
@@ -10,6 +10,9 @@ const question = defineModel<question>('element', {
return {}; return {};
} }
}); });
const rowRecord = defineModel<unknown[]>('rowRecord', { required: false, default: () => [] });
const isPreview = defineModel<boolean>('isPreview', { required: false, default: false });
// eslint-disable-next-line // eslint-disable-next-line
const activeComponent = computed<Component>(() => { const activeComponent = computed<Component>(() => {
switch (question.value.question_type) { switch (question.value.question_type) {
@@ -24,16 +27,19 @@ const activeComponent = computed<Component>(() => {
if (question.value?.list) question.value.options = question.value?.list; if (question.value?.list) question.value.options = question.value?.list;
// 行标签 // 行标签
const rows = ref(question.value?.options[0] ?? []); const rows = defineModel<questionOptionType[]>('rows', { required: false, default: () => [] });
rows.value.length < 1 && (rows.value = question.value?.options[0] ?? []);
// 列标签 // 列标签
const cols = ref(question.value?.options[1] ?? []); const cols = defineModel<questionOptionType[]>('cols', { required: false, default: () => [] });
cols.value.length < 1 && (cols.value = question.value?.options[1] ?? []);
// console.log(rows.value, cols.value); // console.log(rows.value, cols.value);
// const columnLabels = useTemplateRef<HTMLElement[]>('columnLabels'); // const columnLabels = useTemplateRef<HTMLElement[]>('columnLabels');
// 注意, element.options 里面的东西是数组,第一项内容是行标签内容,第二项内容是列标签内容 // 注意, element.options 里面的东西是数组,第一项内容是行标签内容,第二项内容是列标签内容
// 类型 AI 生成 切勿盲目相信,以实际为准 // 类型 AI 生成 切勿盲目相信,以实际为准
/* const props = */ defineProps<{ /* const props = */
defineProps<{
index: number; index: number;
active: boolean; active: boolean;
}>(); }>();
@@ -62,54 +68,16 @@ const emitValue = () => {
</template> </template>
<template #input> <template #input>
<!-- <div style="width: 1000px; overflow: scroll">--> <Component
<Component :is="activeComponent" v-model:rows="rows" v-model:cols="cols" /> :is="activeComponent"
<!-- </div>--> v-model:rowRecord="rowRecord"
v-model:element="question"
v-model:rows="rows"
v-model:cols="cols"
:isPreview="isPreview"
style="overflow: scroll; width: 88vw"
/>
</template> </template>
<!-- 使用 label 插槽来自定义标题 -->
<!-- <template #input>-->
<!-- <table class="matrix-table">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- &lt;!&ndash; 第一行内容为空 &ndash;&gt;-->
<!-- <th></th>-->
<!-- &lt;!&ndash; 第二行内容开始填充 &ndash;&gt;-->
<!-- <td v-for="col in element.options[1]" :key="col.option" ref="columnLabels">-->
<!-- <contenteditable-->
<!-- v-model="col.option"-->
<!-- :active="active"-->
<!-- @blur="emitValue"-->
<!-- ></contenteditable>-->
<!-- </td>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr v-for="row in element.options[0]" :key="row.option">-->
<!-- &lt;!&ndash; 编辑状态单次点击出输入框失焦后关闭 &ndash;&gt;-->
<!-- <td>-->
<!-- <contenteditable-->
<!-- v-model="row.option"-->
<!-- :active="active"-->
<!-- @blur="emitValue"-->
<!-- ></contenteditable>-->
<!-- </td>-->
<!-- <td v-for="col in element.options[1]" :key="col.option" class="td-input">-->
<!-- &lt;!&ndash; 编辑状态单次点击出输入框失焦后关闭 &ndash;&gt;-->
<!-- <input :id="col.option" :type="tableInputTypeMapping()" :name="row.option" />-->
<!-- </td>-->
<!-- <td v-if="element.config.is_limit_right_content === 1">-->
<!-- <contenteditable-->
<!-- v-model="row.option_config.limit_right_content"-->
<!-- :active="active"-->
<!-- @blur="emitValue"-->
<!-- >-->
<!-- </contenteditable>-->
<!-- </td>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </template>-->
</van-field> </van-field>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,58 +1,82 @@
<template> <template>
<table class="matrix-table"> <!-- <table class="matrix-table">-->
<thead> <!-- <thead>-->
<tr> <!-- <tr>-->
<th></th> <!-- <th></th>-->
<th v-for="col in cols" :key="col.option"> <!-- <th v-for="col in cols" :key="col.option">-->
<contenteditable v-model="col.option" :active="active" @blur="emitValue" /> <!-- <contenteditable-->
<!-- 编辑状态单次点击出输入框失焦后关闭 --> <!-- v-if="col.option"-->
<!-- <input-->
<!-- v-if="col.editor"-->
<!-- v-model="col.option"--> <!-- v-model="col.option"-->
<!-- v-focus--> <!-- :active="active"-->
<!-- type="text"--> <!-- @blur="emitValue"-->
<!-- @focusout="col.editor = false"-->
<!-- @click="handleRowNameChange(col.option!)"-->
<!-- />--> <!-- />-->
<!-- <span v-else @click="handleRowNameChange(col.option!)" v-html="col.option"></span>--> <!-- <van-field v-else placeholder="请输入" v-model="col.option" v-focus />-->
</th> <!-- </th>-->
</tr> <!-- </tr>-->
</thead> <!-- </thead>-->
<tbody> <!-- <tbody>-->
<tr v-for="(row, rowIndex) in rows" :key="rowIndex"> <!-- <tr v-for="(row, rowIndex) in rows" :key="rowIndex">-->
<contenteditable v-model="row.option" :active="active" @blur="emitValue" /> <!-- <contenteditable v-model="row.option" :active="active" @blur="emitValue" />-->
<td v-for="(col, colIndex) in cols" :key="colIndex"> <!-- <td v-for="(col, colIndex) in cols" :key="colIndex">-->
<!-- <input-->
<!-- type="radio"-->
<!-- class="van-icon matrix-radio"-->
<!-- :name="`R${rowIndex + 1}`"-->
<!-- :value="`${rowIndex + 1}_${colIndex + 1}`"-->
<!-- :checked="isOptionChecked(rowIndex, colIndex)"-->
<!-- @change="handleMatrixRadioChange(rowIndex, colIndex)"-->
<!-- />-->
<!-- </td>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<el-table :data="rows" style="width: 100%">
<el-table-column width="140">
<template #header></template>
<template #default="{ row /*, column, $index*/ }"> <div v-html="row.option"></div></template>
</el-table-column>
<el-table-column v-for="(col, colIndex) in cols" :key="col.option" width="100">
<template #header>
<contenteditable
v-if="col.option"
v-model="col.option"
:active="active"
@blur="emitValue"
/>
<van-field v-else v-model="col.option" placeholder="请输入"></van-field>
</template>
<template #default="{ /*row, column, */ $index: rowIndex }">
<input <input
type="radio" type="radio"
class="van-icon matrix-radio"
:name="`R${rowIndex + 1}`" :name="`R${rowIndex + 1}`"
:value="`${rowIndex + 1}_${colIndex + 1}`"
:checked="isOptionChecked(rowIndex, colIndex)" :checked="isOptionChecked(rowIndex, colIndex)"
@change="handleMatrixRadioChange(rowIndex, colIndex)" @change="handleMatrixRadioChange(rowIndex, colIndex)"
/> />
</td> </template>
</tr> </el-table-column>
</tbody> </el-table>
</table>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// import { defineProps } from 'vue';
// import { vFocus } from '@/utils/directives/useVFocus';
// 记录行和列的索引 // 记录行和列的索引
// import { getDomText } from '@/utils/utils';
const rowRecord = defineModel<number[]>('rowRecord', { required: false, default: () => [] }); const rowRecord = defineModel<number[]>('rowRecord', { required: false, default: () => [] });
// const matrixAnswer = defineModel<{ [key: string]: 1 }>('matrixAnswer', { required: false, default: () => ({}) });
// 检查 rowRecord 是否存在 // 检查 rowRecord 是否存在
// console.log(`rowRecord:`, rowRecord.value); // console.log(`rowRecord:`, rowRecord.value);
/* const isPreview = */ defineModel<boolean>('isPreview', { required: false, default: false }); /* const isPreview = */ defineModel<boolean>('isPreview', { required: false, default: false });
const rows = defineModel<number[]>('rows', { required: false, default: () => [] }); const rows = defineModel<questionOptionType[]>('rows', { required: false, default: () => [] });
const cols = defineModel<number[]>('cols', { required: false, default: () => [] }); const cols = defineModel<questionOptionType[]>('cols', { required: false, default: () => [] });
const active = defineModel<boolean>('active', { required: false, default: true }); /* const active = */ defineModel<boolean>('active', { required: false, default: true });
// console.log(rows.value, cols.value); const element = defineModel<question>('element', {
// const emits = defineEmits(['update:matrixAnswer', 'update:rowRecord']); required: false,
default: () => {
/**/
}
});
const emit = defineEmits(['update:matrixAnswer', 'update:rowRecord', 'update:element']);
// 判断是否选中 // 判断是否选中
const isOptionChecked = (rowIndex: number, colIndex: number): boolean => { const isOptionChecked = (rowIndex: number, colIndex: number): boolean => {
@@ -93,7 +117,8 @@ function handleMatrixRadioChange(row: number, col: number) {
// emits('update:matrixAnswer', props.matrixAnswer); // emits('update:matrixAnswer', props.matrixAnswer);
// emits('update:rowRecord', props.rowRecord); // emits('update:rowRecord', props.rowRecord);
// }; // };
const emitValue = () => { const emitValue = (val: unknown) => {
console.log(val);
emit('update:element', element.value); emit('update:element', element.value);
}; };
</script> </script>

View File

@@ -46,10 +46,10 @@ const rowRecord = defineModel<string[][]>('rowRecord', { required: false, defaul
const active = defineModel<boolean>('active', { required: false, default: true }); const active = defineModel<boolean>('active', { required: false, default: true });
/* const isPreview = */ defineModel<boolean>('isPreview', { required: false, default: false }); /* const isPreview = */ defineModel<boolean>('isPreview', { required: false, default: false });
const rows = defineModel<number[]>('rows', { required: false, default: () => [] }); const rows = defineModel<questionOptionType[]>('rows', { required: false, default: () => [] });
const cols = defineModel<number[]>('cols', { required: false, default: () => [] }); const cols = defineModel<questionOptionType[]>('cols', { required: false, default: () => [] });
// const emits = defineEmits(['update:matrixAnswer', 'update:rowRecord']); const emit = defineEmits(['update:matrixAnswer', 'update:rowRecord', 'update:element']);
// const handleRowNameChange = (/* value: string */) => { // const handleRowNameChange = (/* value: string */) => {
// console.log(`row change: ${value}`); // console.log(`row change: ${value}`);
@@ -94,6 +94,9 @@ function handleMatrixTextChange(row: number, col: number, e: Event) {
// emits('update:matrixAnswer', props.matrixAnswer); // emits('update:matrixAnswer', props.matrixAnswer);
// emits('update:rowRecord', props.rowRecord); // emits('update:rowRecord', props.rowRecord);
// }; // };
const emitValue = () => {
emit('update:element', element.value);
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -11,7 +11,7 @@ interface OptionConfigType {
limit_right_content?: string; limit_right_content?: string;
} }
interface OptionType { interface questionOptionType {
// 包含 HTML 标签的字符串,例如 "<p>选项1</p>" // 包含 HTML 标签的字符串,例如 "<p>选项1</p>"
option?: string; option?: string;
is_other?: number; is_other?: number;
@@ -34,7 +34,7 @@ type questionsList = {
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?: OptionType[]; options?: questionOptionType[];
}; };
type question = { type question = {

View File

@@ -183,6 +183,7 @@
v-else-if="question.question_type === 8" v-else-if="question.question_type === 8"
v-model:answer="question.answer" v-model:answer="question.answer"
:list="question.list" :list="question.list"
:questionIndex="question.question_index"
:config="question.config" :config="question.config"
:stem="question.stem" :stem="question.stem"
:question="question" :question="question"
@@ -195,6 +196,7 @@
v-else-if="question.question_type === 9" v-else-if="question.question_type === 9"
v-model:answer="question.answer" v-model:answer="question.answer"
:list="question.list" :list="question.list"
:questionIndex="question.question_index"
:config="question.config" :config="question.config"
:stem="question.stem" :stem="question.stem"
:answerSn="questionsData.answer.sn" :answerSn="questionsData.answer.sn"
@@ -669,9 +671,9 @@ async function answer(callback, callbackBeforePage) {
question.error = translatedText.value.ThisIsARequiredQuestion; question.error = translatedText.value.ThisIsARequiredQuestion;
} }
} else if ( } else if (
answer && answer
questionType === 1 && && questionType === 1
Object.keys(answer).findIndex((value) => !answer[value]) !== -1 && Object.keys(answer).findIndex((value) => !answer[value]) !== -1
) { ) {
// 单选题 // 单选题
isError = true; isError = true;
@@ -851,16 +853,16 @@ async function answer(callback, callbackBeforePage) {
// eslint-disable-next-line // eslint-disable-next-line
const reg = const reg =
/^[a-zA-Z·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]]+$/; /^[a-zA-Z·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]]+$/;
isError = isError
config.include_mark === 1 = config.include_mark === 1
? !reg.test(newValue) || !newValue.length ? !reg.test(newValue) || !newValue.length
: !/^[a-zA-Z]+$/.test(newValue) || !newValue.length; : !/^[a-zA-Z]+$/.test(newValue) || !newValue.length;
question.error = isError ? translatedText.value.PleaseEnterEnglishLetters : ''; question.error = isError ? translatedText.value.PleaseEnterEnglishLetters : '';
break; break;
// 中文 // 中文
case 4: case 4:
isError = isError
config.include_mark === 1 = config.include_mark === 1
? !/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|[a-zA-Z·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]])+$/.test( ? !/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|[a-zA-Z·~@#¥%…&*()—\-+={}|《》?:“”【】、;‘’,。`!$^()_<>?:",./;'\\[\]])+$/.test(
newValue newValue
) || !newValue.length ) || !newValue.length
@@ -871,8 +873,8 @@ async function answer(callback, callbackBeforePage) {
break; break;
// 邮箱 // 邮箱
case 5: case 5:
isError = isError
!/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( = !/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
value value
); );
question.error = isError ? translatedText.value.PleaseEnterACorrectEmail : ''; question.error = isError ? translatedText.value.PleaseEnterACorrectEmail : '';
@@ -884,8 +886,8 @@ async function answer(callback, callbackBeforePage) {
break; break;
// 身份证号 // 身份证号
case 7: case 7:
isError = isError
!/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/.test( = !/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/.test(
value value
); );
question.error = isError ? translatedText.value.PleaseEnterACorrectID : ''; question.error = isError ? translatedText.value.PleaseEnterACorrectID : '';
@@ -1027,14 +1029,14 @@ async function answer(callback, callbackBeforePage) {
currentQuestions.forEach((question, index) => { currentQuestions.forEach((question, index) => {
if (index >= warnStart && index < warnEnd) { if (index >= warnStart && index < warnEnd) {
if (repeat.repeat_type) { if (repeat.repeat_type) {
question.warning = question.warning
translatedText.value.TheAnswerIsRepeatedMoreThanOneTimesPleaseRevise( = translatedText.value.TheAnswerIsRepeatedMoreThanOneTimesPleaseRevise(
repeat.allow_repeat_num, repeat.allow_repeat_num,
repeat.repeat_type repeat.repeat_type
); );
} else { } else {
question.error = question.error
translatedText.value.TheAnswerIsRepeatedMoreThanOneTimesPleaseRevise( = translatedText.value.TheAnswerIsRepeatedMoreThanOneTimesPleaseRevise(
repeat.allow_repeat_num, repeat.allow_repeat_num,
repeat.repeat_type repeat.repeat_type
); );
@@ -1381,8 +1383,8 @@ function updateAnswer(auto) {
const evt1 = {}; const evt1 = {};
if ([1].includes(question.question_type)) { if ([1].includes(question.question_type)) {
evt1.value = evt1.value
Object.keys(question.answer) = Object.keys(question.answer)
.map((key) => (question.answer[key] ? key : undefined)) .map((key) => (question.answer[key] ? key : undefined))
.filter((i) => !!i)?.[0] || undefined; .filter((i) => !!i)?.[0] || undefined;
evt1.options = question.list.flatMap((i) => i.options); evt1.options = question.list.flatMap((i) => i.options);

View File

@@ -1,16 +1,19 @@
<template> <template>
<matrix-radio <MartrixQuestion
v-model:rowRecord="rowRecord" v-model:rowRecord="rowRecord"
v-model:matrix-radio-answer="answer!"
:rows="rows" :rows="rows"
:cols="cols" :cols="cols"
:index="questionIndex"
:element="question"
:is-preview="true" :is-preview="true"
></matrix-radio> :active="false"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import matrixRadio from '@/views/Design/components/Questions/MatrixRadio.vue';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import MartrixQuestion from '@/views/Design/components/Questions/MartrixQuestion.vue';
// const questionType = defineModel<number>('questionType', { required: false }); // const questionType = defineModel<number>('questionType', { required: false });
// 矩阵单选的答案类型 // 矩阵单选的答案类型
@@ -23,6 +26,8 @@ type answerType = {
// 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'); const question = defineModel<question>('question');
const questionIndex = defineModel<number>('questionIndex', { required: false, default: 0 });
// console.log(question.value);
const emit = defineEmits(['changeAnswer', 'previous', 'next']); const emit = defineEmits(['changeAnswer', 'previous', 'next']);
// 示例 // 示例
// { // {