Представления
В директории /views
будем хранить большие компоненты со сложной логикой, которые будут использовать другие компоненты и
будут являться полноценным представлением для конкретной страницы.
<script setup>
import{ ref, onMounted } from 'vue';
import { fetchWrapper } from '@/helpers';
import { useAuthStore } from '@/stores';
import { LoginButton, LoadingScreen } from "@/components";
const baseUrl = `${import.meta.env.VITE_API_URL}`;
const disabled = ref(false)
const authenticate = async () => {
disabled.value = true;
const response = await fetchWrapper.get(`${baseUrl}/oidc/auth/url/`);
window.location.href = response.url;
};
onMounted(async () => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
if (code) {
disabled.value = true;
await login(code);
}
});
const login = async (code) => {
const authStore = useAuthStore();
return authStore.login(code)
};
</script>
<template>
<LoginButton @click="authenticate" :disabled="disabled"/>
<LoadingScreen v-if="disabled" />
</template>
<style scoped>
</style>
В шаблоне этого компонента будем выводить кнопку входа и загрузочный экран, отключенный по умолчанию.
При клике на кнопку будем отправлять запрос в бэкенд на получение ссылки на авторизации, после чего будем переводить пользователя на неё.
После успешной авторизации пользователь будет переведён обратно на эту же страницу с query параметром code
, который нужно
отправить обратно на бэкенд.
Для этого при загрузке страницы будем проверять наличие code
, и, если он есть, будем отправлять отправлять вызывать
функцию login
хранилища авторизации с этим аргументом.
<script setup>
import { ref, onMounted } from 'vue'
import { fetchWrapper, router } from '@/helpers';
import { useInfoStore } from '@/stores';
import { AdapterSlider, Select, Slider, SubmitButton, TextArea, TrainingSelect } from "@/components";
const baseUrl = `${import.meta.env.VITE_API_URL}`;
let isSending = ref(false);
const onSubmit = async () => {
if (errors.value.length > 0) {
return;
}
isSending.value = true;
const info = useInfoStore();
const formValuesList = Object.keys(formValues).map(key => {
if (typeof formValues[key] === 'object' && formValues[key] !== null) {
return {
id: parseInt(key),
value: Object.keys(formValues[key]).map(innerKey => ({
id: parseInt(innerKey),
value: formValues[key][innerKey]
}))
};
}
else {
return {
id: parseInt(key),
value: formValues[key]
};
}
});
try {
await fetchWrapper.post(`${baseUrl}/api/survey/answers/`, formValuesList);
info.setText('Опрос пройден!\nСпасибо 💙');
} catch (error) {
info.setText('Произошла ошибка при сохранении ответов!');
}
await router.push('/info');
}
const formFields = ref([])
const formValues = {}
const errors = ref([])
onMounted(async () => {
try {
const data = await fetchWrapper.get(`${baseUrl}/api/survey/questions/`)
if(data) {
formFields.value = data
data.forEach((field) => {
formValues[field.id] = ''
if (field.component === 'Select' || field.component === 'TrainingSelect') {
errors.value.push(field.id)
}
})
}
} catch(err) {
console.error(err)
}
})
</script>
<template>
<div class="form-container">
<v-form @submit.prevent="onSubmit" class="form-content">
<div class="field" v-for="(field, index) in formFields" :key="index">
<TextArea
v-if="field.component === 'TextArea'"
:id="`${field.id}`"
:text="field.text"
:helpText="field.help_text"
@update:textArea="val => {
formValues[field.id] = val
}"
/>
<Select
v-else-if="field.component === 'Select'"
:id="`${field.id}`"
:text="field.text"
:helpText="field.help_text"
@update:selectedValue="val => formValues[field.id] = val"
@update:error="val => {
if (val) {
if (!errors.includes(field.id)) {
errors.push(field.id)
}
} else {
const index = errors.indexOf(field.id)
if (index > -1) {
errors.splice(index, 1)
}
}
}"
/>
<TrainingSelect
v-else-if="field.component === 'TrainingSelect'"
:id="`${field.id}`"
:text="field.text"
:helpText="field.help_text"
:training-text="field.training_text"
:training-help-text="field.training_help_text"
@update:selectedValue="val => formValues[field.id] = val"
@update:error="val => {
if (val) {
if (!errors.includes(field.id)) {
errors.push(field.id)
}
} else {
const index = errors.indexOf(field.id)
if (index > -1) {
errors.splice(index, 1)
}
}
}"
/>
<AdapterSlider
v-else-if="field.component === 'AdapterSlider'"
:id="`${field.id}`"
:text="field.text"
:helpText="field.help_text"
:adapters="field.adapters"
@update:sliderValue="val => formValues[field.id] = val"
/>
<Slider
v-else-if="field.component === 'Slider'"
:id="`${field.id}`"
:text="field.text"
:helpText="field.help_text"
@update:sliderValue="val => formValues[field.id] = val"
/>
</div>
<div class="center-button">
<SubmitButton v-if="formFields.length>0" :disabled="errors.length>0 || isSending"/>
</div>
</v-form>
</div>
</template>
<style scoped>
.form-container {
display: flex;
justify-content: center;
}
.form-content {
padding-left: 20px;
padding-right: 20px;
max-width: 600px;
width: 100%;
}
.center-button {
display: flex;
justify-content: center;
padding-bottom: 30px;
}
.field {
padding-top: 30px;
}
</style>
Это наша основная страница.
При загрузке будем запрашивать у бэкенда список вопросов, которые затем поместим в реактивную переменную formFields
.
Также при загрузке в словаре formValues
создадим элементы с пустыми значениями и ключами, равными id вопросов.
Для генерации шаблона воспользуемся директивой v-for
, с помощью которой будем итерироваться по значениям в ранее
созданном списке formFields
. И с помощью директивы v-if
будем проверять значение поля component
каждого вопроса
и сравнивать его с названиями наших компонент, чтобы понять, для какого вопроса какой компонент использовать.
В каждый компонент будем передавать соответсвующие для их вопроса text
и helpText
.
Также будем принимать их emits
и помещать полученные значения в словарь под нужным ключом.
Кнопку отправки будем блокировать, если есть незаполненные поля (длина массива errors
> 0) или форма в процессе отправки.
Клик кнопки будет вызывать функцию onSubmit
, которая перепарсит словарь ответов в список и отправит на бэкенд, затем
обновит текст в хранилище infoStore
и переведёт пользователя на страницу информации.
<script setup>
import {onMounted, ref} from 'vue'
import { useInfoStore } from '@/stores';
import { InfoContainer } from "@/components";
const text = ref("")
onMounted( () => {
const infoStore = useInfoStore();
if (!infoStore.text && localStorage.getItem('info_message')) {
infoStore.setText(localStorage.getItem('info_message'));
}
text.value = infoStore.text
});
</script>
<template>
<div class="image-container">
<div class="bird2-container">
<img src="@/assets/images/birds2.png">
</div>
<InfoContainer :text="text" />
<div class="bird1-container">
<img src="@/assets/images/birds.png">
</div>
</div>
</template>
<style scoped>
.image-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 800px;
height: 600px;
display: flex;
justify-content: center;
align-items: center;
background: url("@/assets/images/full-sun.png") no-repeat center center;
background-size: auto;
}
.bird1-container {
position: absolute;
right: 0;
bottom: 0;
margin-right: 20px;
}
.bird2-container {
position: absolute;
top: 0;
left: 0;
margin-left: 20px;
}
</style>
При загрузке этой страницы из хранилища infoStore
будет доставаться текст и выводиться с помощью компонента InfoContainer
в окружении картинок птиц.