mirror of
https://github.com/grey-cat-1908/formaptix-web.git
synced 2024-11-11 18:47:27 +03:00
more question types support
This commit is contained in:
parent
751f9eabad
commit
1e2ee1c2a3
4 changed files with 320 additions and 14 deletions
|
@ -46,7 +46,7 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
isRequired: {
|
isRequired: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
121
src/components/forms/SelectorQuestion.vue
Normal file
121
src/components/forms/SelectorQuestion.vue
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<template>
|
||||||
|
<div class="selector-component">
|
||||||
|
<div class="options">
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="index"
|
||||||
|
class="option"
|
||||||
|
@click="toggleSelection(index)"
|
||||||
|
:class="{ selected: isSelected(index) }"
|
||||||
|
>
|
||||||
|
<span>{{ option.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
minValues: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
maxValues: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
isRequired: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['input'])
|
||||||
|
const selectedIndexes = ref([...props.value])
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
function toggleSelection(index) {
|
||||||
|
const isSelected = selectedIndexes.value.includes(index)
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
selectedIndexes.value = selectedIndexes.value.filter((i) => i !== index)
|
||||||
|
} else if (!props.maxValues || selectedIndexes.value.length < props.maxValues) {
|
||||||
|
selectedIndexes.value.push(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
validateSelection()
|
||||||
|
emit('input', selectedIndexes.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected(index) {
|
||||||
|
return selectedIndexes.value.includes(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSelection() {
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
if (selectedIndexes.value.length < props.minValues) {
|
||||||
|
error.value = `Необходимо выбрать минимум ${props.minValues} вариантов.`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.maxValues && selectedIndexes.value.length > props.maxValues) {
|
||||||
|
error.value = `Необходимо выбрать не больше ${props.maxValues} вариантов.`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.isRequired && selectedIndexes.value.length === 0) {
|
||||||
|
error.value = `Выбор обязателен.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedIndexes, validateSelection)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.selector-component {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option.selected {
|
||||||
|
border-color: #42b983;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option img {
|
||||||
|
max-width: 50px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
148
src/components/forms/TextQuestion.vue
Normal file
148
src/components/forms/TextQuestion.vue
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-question-component">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="inputValue"
|
||||||
|
:maxlength="maxLength"
|
||||||
|
:required="isRequired"
|
||||||
|
@input="validateInput"
|
||||||
|
@blur="validateInput"
|
||||||
|
/>
|
||||||
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
minLength: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
validator: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
isRequired: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['input'])
|
||||||
|
const inputValue = ref(props.value)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
function validateTIN(value) {
|
||||||
|
const len = value.length
|
||||||
|
if (len !== 10 && len !== 12) return false
|
||||||
|
|
||||||
|
const digits = value.split('').map(Number)
|
||||||
|
|
||||||
|
if (len === 10) {
|
||||||
|
const checksum =
|
||||||
|
((2 * digits[0] +
|
||||||
|
4 * digits[1] +
|
||||||
|
10 * digits[2] +
|
||||||
|
3 * digits[3] +
|
||||||
|
5 * digits[4] +
|
||||||
|
9 * digits[5] +
|
||||||
|
4 * digits[6] +
|
||||||
|
6 * digits[7] +
|
||||||
|
8 * digits[8]) %
|
||||||
|
11) %
|
||||||
|
10
|
||||||
|
return digits[9] === checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len === 12) {
|
||||||
|
const checksum1 =
|
||||||
|
((7 * digits[0] +
|
||||||
|
2 * digits[1] +
|
||||||
|
4 * digits[2] +
|
||||||
|
10 * digits[3] +
|
||||||
|
3 * digits[4] +
|
||||||
|
5 * digits[5] +
|
||||||
|
9 * digits[6] +
|
||||||
|
4 * digits[7] +
|
||||||
|
6 * digits[8] +
|
||||||
|
8 * digits[9]) %
|
||||||
|
11) %
|
||||||
|
10
|
||||||
|
const checksum2 =
|
||||||
|
((3 * digits[0] +
|
||||||
|
7 * digits[1] +
|
||||||
|
2 * digits[2] +
|
||||||
|
4 * digits[3] +
|
||||||
|
10 * digits[4] +
|
||||||
|
3 * digits[5] +
|
||||||
|
5 * digits[6] +
|
||||||
|
9 * digits[7] +
|
||||||
|
4 * digits[8] +
|
||||||
|
6 * digits[9] +
|
||||||
|
8 * digits[10]) %
|
||||||
|
11) %
|
||||||
|
10
|
||||||
|
return digits[10] === checksum1 && digits[11] === checksum2
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSNILS(value) {
|
||||||
|
if (value.length !== 11) return false
|
||||||
|
|
||||||
|
const digits = value.slice(0, 9).split('').map(Number)
|
||||||
|
const checksum = digits.reduce((sum, digit, index) => sum + digit * (9 - index), 0)
|
||||||
|
|
||||||
|
let controlNumber = checksum % 101
|
||||||
|
if (controlNumber === 100 || controlNumber === 101) {
|
||||||
|
controlNumber = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return controlNumber === Number(value.slice(-2))
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateInput() {
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
if (props.minLength && inputValue.value.length < props.minLength) {
|
||||||
|
error.value = `Минимальная длина ${props.minLength} символов`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.validator === 1 && !validateTIN(inputValue.value)) {
|
||||||
|
error.value = 'Некорректный ИНН'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.validator === 2 && !validateSNILS(inputValue.value)) {
|
||||||
|
error.value = 'Некорректный СНИЛС'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('input', inputValue.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-question-component {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { makeAPIRequest } from '@/utils/http'
|
import { makeAPIRequest } from '@/utils/http'
|
||||||
import Scale from '@/components/forms/Scale.vue'
|
import Scale from '@/components/forms/ScaleQuestion.vue'
|
||||||
|
import TextQuestion from '@/components/forms/TextQuestion.vue'
|
||||||
|
import SelectorQuestion from '@/components/forms/SelectorQuestion.vue'
|
||||||
|
|
||||||
const data = ref({})
|
const data = ref({})
|
||||||
const currentPageNumber = ref(0)
|
const currentPageNumber = ref(0)
|
||||||
|
@ -18,7 +20,20 @@ async function prepareNewPage() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function beforeSubmitValidate() {
|
||||||
|
for (let question_id of currentPage.value.questions.keys()) {
|
||||||
|
const question = currentPage.value.questions[question_id]
|
||||||
|
const answer = answers.value[question.id]
|
||||||
|
|
||||||
|
if (question.question_type === 2) {
|
||||||
|
return question.required && answer.values.length >= question.min_values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
async function submitForm() {
|
async function submitForm() {
|
||||||
|
if (beforeSubmitValidate()) {
|
||||||
if (currentPageNumber.value !== data.value.pages.length - 1) {
|
if (currentPageNumber.value !== data.value.pages.length - 1) {
|
||||||
currentPageNumber.value += 1
|
currentPageNumber.value += 1
|
||||||
await prepareNewPage()
|
await prepareNewPage()
|
||||||
|
@ -32,6 +47,7 @@ async function submitForm() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
@ -46,7 +62,28 @@ onMounted(async () => {
|
||||||
|
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div class="" v-for="question in currentPage.questions">
|
<div class="" v-for="question in currentPage.questions">
|
||||||
|
<h3>{{ question.label }}</h3>
|
||||||
|
<p>{{ question.description }}</p>
|
||||||
|
<TextQuestion
|
||||||
|
v-if="question.question_type === 1"
|
||||||
|
:minLength="question.min_length"
|
||||||
|
:maxLength="question.max_length"
|
||||||
|
:validator="question.validator"
|
||||||
|
:isRequired="question.required"
|
||||||
|
v-model="answers[question.id].value"
|
||||||
|
@input="answers[question.id].value = $event"
|
||||||
|
/>
|
||||||
|
<SelectorQuestion
|
||||||
|
v-if="question.question_type === 2"
|
||||||
|
:minValues="question.min_values"
|
||||||
|
:maxValues="question.max_values"
|
||||||
|
:options="question.options"
|
||||||
|
:isRequired="question.required"
|
||||||
|
v-model="answers[question.id].values"
|
||||||
|
@input="answers[question.id].values = $event"
|
||||||
|
/>
|
||||||
<Scale
|
<Scale
|
||||||
|
v-if="question.question_type === 3"
|
||||||
:min="question.min_value"
|
:min="question.min_value"
|
||||||
:max="question.max_value"
|
:max="question.max_value"
|
||||||
:minLabel="question.min_label"
|
:minLabel="question.min_label"
|
||||||
|
|
Loading…
Reference in a new issue