mirror of
https://github.com/grey-cat-1908/formaptix-web.git
synced 2024-09-22 19:21:59 +03:00
better types of questions support on answers page
This commit is contained in:
parent
cebd6adbd2
commit
63ccc17cc0
11 changed files with 409 additions and 62 deletions
133
src/components/answers/ScaleValue.vue
Normal file
133
src/components/answers/ScaleValue.vue
Normal file
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div class="default-card" v-if="modelValue">
|
||||
<div class="view-form-q-title">
|
||||
<h3 class="form-q-title">{{ label }}</h3>
|
||||
<p class="form-q-description">{{ description }}</p>
|
||||
</div>
|
||||
<div class="rating">
|
||||
<div class="rating-options">
|
||||
<label v-for="n in range" :key="n" class="rating-option">
|
||||
<input type="radio" :value="n" v-model="selectedValue" class="rating-option--btn" />
|
||||
<span>{{ n }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import '@/styles/form/view.scss'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: Number
|
||||
}
|
||||
})
|
||||
|
||||
const selectedValue = ref(props.modelValue)
|
||||
|
||||
const range = computed(() => {
|
||||
return Array.from({ length: props.max - props.min + 1 }, (_, i) => props.min + i)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.rating {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//align-items: center;
|
||||
|
||||
&-options {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
gap: 0 50px;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
flex-direction: column;
|
||||
gap: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px 0;
|
||||
|
||||
//&--btn {
|
||||
// width: 20px;
|
||||
// height: 20px;
|
||||
// cursor: pointer;
|
||||
//}
|
||||
|
||||
&--btn:after {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 15px;
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
position: relative;
|
||||
background: var(--color-input-background);
|
||||
border: 2px solid var(--color-main-border);
|
||||
content: '';
|
||||
display: inline-block;
|
||||
visibility: visible;
|
||||
cursor: pointer;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
|
||||
&--btn:after:hover {
|
||||
border: 2px solid var(--color-secondary-border);
|
||||
background: var(--color-main-background);
|
||||
}
|
||||
&--btn:checked:after {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 15px;
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
position: relative;
|
||||
border: 2px solid var(--color-main);
|
||||
background: var(--color-main-toned);
|
||||
content: '';
|
||||
display: inline-block;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 810px) {
|
||||
flex-direction: initial;
|
||||
gap: 0 25px;
|
||||
}
|
||||
}
|
||||
|
||||
&-delete {
|
||||
width: 100%;
|
||||
border-radius: 1rem;
|
||||
padding: 10px 30px;
|
||||
margin-top: 25px;
|
||||
border: 1px solid var(--color-third-border);
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--color-main);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
112
src/components/answers/SelectorValue.vue
Normal file
112
src/components/answers/SelectorValue.vue
Normal file
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<div class="default-card" v-if="modelValue.length > 0">
|
||||
<div class="view-form-q-title">
|
||||
<h3 class="form-q-title">{{ label }}</h3>
|
||||
<p class="form-q-description">{{ description }}</p>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<div class="selector-options">
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="selector-option default-button"
|
||||
:class="{ selected: isSelected(index) }"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import '@/styles/form/view.scss'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
function isSelected(index) {
|
||||
return props.modelValue.includes(index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-labels {
|
||||
&-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0 11px;
|
||||
color: var(--color-description);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&--sign {
|
||||
min-width: 23px;
|
||||
min-height: 23px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-options {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&-option {
|
||||
border-radius: 0.5rem;
|
||||
background: var(--color-input-background);
|
||||
border: 1px solid var(--color-main-border);
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--color-secondary-border);
|
||||
background: var(--color-main-background);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: 1px solid var(--color-main);
|
||||
background: var(--color-main-toned);
|
||||
//color: var(--color-alternative-text);
|
||||
}
|
||||
}
|
||||
|
||||
&-error {
|
||||
margin-top: 20px;
|
||||
color: var(--color-red);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0 13px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&--sign {
|
||||
min-width: 23px;
|
||||
min-height: 23px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,24 +1,73 @@
|
|||
<template>
|
||||
<div class="text-question-component">
|
||||
<input type="text" :value="modelValue" readonly />
|
||||
<div class="default-card" v-if="modelValue.length > 0">
|
||||
<div class="view-form-q-title">
|
||||
<h3 class="form-q-title">{{ label }}</h3>
|
||||
<p class="form-q-description">{{ description }}</p>
|
||||
</div>
|
||||
<div class="text-question">
|
||||
<input class="text-question-input" type="text" :value="modelValue" readonly />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import '@/styles/form/view.scss'
|
||||
|
||||
const props = defineProps(['modelValue'])
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-question-component {
|
||||
.text-question {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin-top: 5px;
|
||||
&-error {
|
||||
margin-top: 20px;
|
||||
color: var(--color-red);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0 13px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&--sign {
|
||||
min-width: 23px;
|
||||
min-height: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100% !important;
|
||||
background: var(--color-main-background);
|
||||
border: 1px solid var(--color-main-border);
|
||||
padding: 10px 20px;
|
||||
font-weight: 200;
|
||||
border-radius: 0.5rem;
|
||||
outline: 0;
|
||||
transition:
|
||||
border,
|
||||
background 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--color-secondary-border);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--color-third-border);
|
||||
background: var(--color-input-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { PhCardsThree, PhCaretCircleUpDown } from '@phosphor-icons/vue'
|
||||
|
||||
import '@/styles/form/view.scss';
|
||||
import '@/styles/form/view.scss'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
|
|
|
@ -10,7 +10,14 @@
|
|||
<div class="selector-labels-info">
|
||||
<PhCaretCircleUpDown :size="23" class="selector-labels-info--sign" />
|
||||
<div class="selector-labels-info--text">
|
||||
Выберите от {{ minValues }} до {{ Math.min(maxValues, options.length) }} {{ normalizeCountForm(Math.min(maxValues, options.length), ['варианта', 'вариантов', 'вариантов']) }}
|
||||
Выберите от {{ minValues }} до {{ Math.min(maxValues, options.length) }}
|
||||
{{
|
||||
normalizeCountForm(Math.min(maxValues, options.length), [
|
||||
'варианта',
|
||||
'вариантов',
|
||||
'вариантов'
|
||||
])
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,7 +44,7 @@ import { ref, watch } from 'vue'
|
|||
import { PhCaretCircleUpDown, PhXCircle } from '@phosphor-icons/vue'
|
||||
import { normalizeCountForm } from '@/utils/formation'
|
||||
|
||||
import '@/styles/form/view.scss';
|
||||
import '@/styles/form/view.scss'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
|
@ -99,12 +106,12 @@ function validateSelection() {
|
|||
error.value = ''
|
||||
|
||||
if (selectedIndexes.value.length < props.minValues) {
|
||||
error.value = `Необходимо выбрать минимум ${props.minValues} ${normalizeCountForm(props.minValues, ['вариант', 'варианта', 'вариантов']) }.`
|
||||
error.value = `Необходимо выбрать минимум ${props.minValues} ${normalizeCountForm(props.minValues, ['вариант', 'варианта', 'вариантов'])}.`
|
||||
return
|
||||
}
|
||||
|
||||
if (props.maxValues && selectedIndexes.value.length > props.maxValues) {
|
||||
error.value = `Необходимо выбрать не больше ${props.maxValues} ${normalizeCountForm(props.maxValues, ['варианта', 'вариантов', 'вариантов']) }.`
|
||||
error.value = `Необходимо выбрать не больше ${props.maxValues} ${normalizeCountForm(props.maxValues, ['варианта', 'вариантов', 'вариантов'])}.`
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import { PhXCircle } from '@phosphor-icons/vue'
|
|||
import { validateSNILS, validateTIN } from '@/utils/validators'
|
||||
import { normalizeCountForm } from '@/utils/formation'
|
||||
|
||||
import '@/styles/form/view.scss';
|
||||
import '@/styles/form/view.scss'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
|
@ -75,7 +75,7 @@ function validateInput() {
|
|||
|
||||
if (props.isRequired || inputValue.value) {
|
||||
if (props.minLength && inputValue.value.length < props.minLength) {
|
||||
error.value = `Минимальная длина - ${props.minLength} ${normalizeCountForm(props.minValues, ['символ', 'символа', 'символов']) }`
|
||||
error.value = `Минимальная длина - ${props.minLength} ${normalizeCountForm(props.minValues, ['символ', 'символа', 'символов'])}`
|
||||
}
|
||||
if (props.validator === 1 && !validateTIN(inputValue.value)) {
|
||||
error.value = 'Некорректный ИНН'
|
||||
|
@ -116,7 +116,9 @@ function validateInput() {
|
|||
font-weight: 200;
|
||||
border-radius: 0.5rem;
|
||||
outline: 0;
|
||||
transition: border, background 0.25s ease;
|
||||
transition:
|
||||
border,
|
||||
background 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--color-secondary-border);
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export function normalizeCountForm (number: number, words_arr: Array<string>) {
|
||||
let options = [2, 0, 1, 1, 1, 2];
|
||||
return words_arr[(number % 100 > 4 && number % 100 < 20) ? 2 : options[(number % 10 < 5) ? number % 10 : 5]];
|
||||
export function normalizeCountForm(number: number, words_arr: Array<string>) {
|
||||
let options = [2, 0, 1, 1, 1, 2]
|
||||
return words_arr[
|
||||
number % 100 > 4 && number % 100 < 20 ? 2 : options[number % 10 < 5 ? number % 10 : 5]
|
||||
]
|
||||
}
|
|
@ -7,14 +7,14 @@ export function validateTIN(value: String) {
|
|||
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]) %
|
||||
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
|
||||
|
@ -23,29 +23,29 @@ export function validateTIN(value: String) {
|
|||
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]) %
|
||||
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]) %
|
||||
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
|
||||
|
|
|
@ -4,10 +4,13 @@ import { makeAPIRequest } from '@/utils/http'
|
|||
import FormNotFound from '@/components/FormNotFound.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import TextValue from '@/components/answers/TextValue.vue'
|
||||
import SelectorValue from '@/components/answers/SelectorValue.vue'
|
||||
import ScaleValue from '@/components/answers/ScaleValue.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const data = ref([])
|
||||
const questionsData = ref({})
|
||||
const currentPageNumber = ref(0)
|
||||
const isAnswerNotFound = ref(true)
|
||||
const mode = ref(0)
|
||||
|
@ -27,6 +30,19 @@ onMounted(async () => {
|
|||
if (data.value.length > 0) {
|
||||
isAnswerNotFound.value = false
|
||||
}
|
||||
|
||||
const questionsFormResponse = await makeAPIRequest('/form/get', 'GET', {
|
||||
id: Number(route.params.id)
|
||||
})
|
||||
if (!questionsFormResponse.json || questionsFormResponse.status !== 200) {
|
||||
return
|
||||
}
|
||||
|
||||
questionsFormResponse.json.data.pages.forEach((p: any) => {
|
||||
p.questions.forEach((q: any) => {
|
||||
questionsData.value[q.id] = q
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -41,7 +57,27 @@ onMounted(async () => {
|
|||
</button>
|
||||
|
||||
<div class="" v-for="(value, index) in data[currentPageNumber].data.values">
|
||||
<TextValue v-if="value.question_type === 1" v-model="value.value" />
|
||||
<TextValue
|
||||
v-if="questionsData[value.question_id].question_type === 1"
|
||||
:label="questionsData[value.question_id].label"
|
||||
:description="questionsData[value.question_id].description"
|
||||
v-model="value.value"
|
||||
/>
|
||||
<SelectorValue
|
||||
v-if="questionsData[value.question_id].question_type === 2"
|
||||
:options="questionsData[value.question_id].options"
|
||||
:label="questionsData[value.question_id].label"
|
||||
:description="questionsData[value.question_id].description"
|
||||
v-model="value.values"
|
||||
></SelectorValue>
|
||||
<ScaleValue
|
||||
v-if="questionsData[value.question_id].question_type === 3"
|
||||
:min="questionsData[value.question_id].min_value"
|
||||
:max="questionsData[value.question_id].max_value"
|
||||
:label="questionsData[value.question_id].label"
|
||||
:description="questionsData[value.question_id].description"
|
||||
v-model="value.value"
|
||||
></ScaleValue>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useRoute } from 'vue-router'
|
|||
import { PhInfo, PhCardsThree } from '@phosphor-icons/vue'
|
||||
import { validateSNILS, validateTIN } from '@/utils/validators'
|
||||
|
||||
import '@/styles/form/view.scss';
|
||||
import '@/styles/form/view.scss'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
@ -36,14 +36,18 @@ function beforeSubmitValidate() {
|
|||
for (let question of currentPage.value.questions) {
|
||||
const answer = answers.value[question.id]
|
||||
|
||||
if (!answer) return false;
|
||||
if (question.required && !answer.value && !answer.values) return false;
|
||||
if (!answer) return false
|
||||
if (question.required && !answer.value && !answer.values) return false
|
||||
|
||||
if (question.question_type === 1 && answer.value) {
|
||||
if (!question.validator && question.min_length && answer.value.length < question.min_length) return false;
|
||||
if (question.validator === 1 && !validateTIN(answer.value)) return false;
|
||||
if (question.validator === 2 && !validateSNILS(answer.value)) return false;
|
||||
} else if (question.question_type === 2 && !(question.required && answer.values.length >= question.min_values)) {
|
||||
if (!question.validator && question.min_length && answer.value.length < question.min_length)
|
||||
return false
|
||||
if (question.validator === 1 && !validateTIN(answer.value)) return false
|
||||
if (question.validator === 2 && !validateSNILS(answer.value)) return false
|
||||
} else if (
|
||||
question.question_type === 2 &&
|
||||
!(question.required && answer.values.length >= question.min_values)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -58,9 +62,9 @@ async function submitForm() {
|
|||
} else {
|
||||
for (let page of data.value.pages) {
|
||||
page.questions.forEach((q) => {
|
||||
let answer = answers.value[q.id];
|
||||
let answer = answers.value[q.id]
|
||||
if (!q.required && !(answer.value || answer.values)) {
|
||||
delete answers.value[q.id];
|
||||
delete answers.value[q.id]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -99,7 +103,9 @@ onMounted(async () => {
|
|||
<div class="view-form-title view-form-container default-card">
|
||||
<div class="view-form-info">
|
||||
<PhCardsThree :size="23" class="view-form-info--sign" />
|
||||
<div class="view-form-info--text">Страница {{ currentPageNumber + 1 }} из {{ data.pages.length }}</div>
|
||||
<div class="view-form-info--text">
|
||||
Страница {{ currentPageNumber + 1 }} из {{ data.pages.length }}
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="form-title">{{ data.name }}</h2>
|
||||
<p class="form-description">{{ currentPage.text }}</p>
|
||||
|
|
Loading…
Reference in a new issue