forked from mns/mini-skamja
form
This commit is contained in:
parent
bad63f37e9
commit
8d8c0ead07
3
app.vue
3
app.vue
|
@ -1,8 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import '@/assets/main.scss'
|
import "@/assets/main.scss";
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<Modal />
|
||||||
<Header />
|
<Header />
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
|
@ -455,7 +455,7 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-icons {
|
.canvas-icons {
|
||||||
@apply absolute text-3xl top-0 left-0 flex flex-col;
|
@apply absolute text-3xl flex flex-col;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@apply cursor-pointer;
|
@apply cursor-pointer;
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const {
|
||||||
|
isModalOpen,
|
||||||
|
modal_data,
|
||||||
|
modal_form,
|
||||||
|
modal_state,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
toggleModal,
|
||||||
|
openForm,
|
||||||
|
validateInput,
|
||||||
|
submit,
|
||||||
|
} = useModalState();
|
||||||
|
|
||||||
|
const modalStatus = {
|
||||||
|
loading: ["mdi:circle-arrows", "Отправляем данные"],
|
||||||
|
success: ["mdi:check-circle-outline", "Данные отправлены"],
|
||||||
|
error: ["mdi:close-circle-outline", "Ошибка отправки"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const policy = () => {
|
||||||
|
navigateTo("/policy", {
|
||||||
|
open: {
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="isModalOpen" class="modal-backdrop" @click.self="toggleModal">
|
||||||
|
<div class="modal">
|
||||||
|
<span class="modal-close" @click="toggleModal">
|
||||||
|
<Icon name="mdi:close" />
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
v-if="modal_state.show_status"
|
||||||
|
class="modal-status"
|
||||||
|
:class="[modal_state.show_status]"
|
||||||
|
>
|
||||||
|
<div class="modal-status-icon">
|
||||||
|
<Icon :name="modalStatus[modal_state.show_status][0]" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-status-text">
|
||||||
|
{{ modalStatus[modal_state.show_status][1] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else-if="modal_state.show_form">
|
||||||
|
<h2>Оставьте контакты для связи</h2>
|
||||||
|
<form @submit.prevent="submit" ref="form" class="modal-content">
|
||||||
|
<input type="text" placeholder="Ваше имя" v-model="modal_data.name" />
|
||||||
|
<input
|
||||||
|
type="phone"
|
||||||
|
placeholder="Ваш номер телефона"
|
||||||
|
v-model="modal_data.phone"
|
||||||
|
@keypress="validateInput"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Ваш e-mail"
|
||||||
|
v-model="modal_data.email"
|
||||||
|
@keypress="validateInput"
|
||||||
|
/>
|
||||||
|
<div class="flex gap-4 justify-between items-center">
|
||||||
|
<input type="checkbox" id="policy" v-model="modal_data.policy" />
|
||||||
|
<label for="policy">
|
||||||
|
Нажимая кнопку "Отправить", вы даете согласие на
|
||||||
|
<NuxtLink to="/policy" @click="policy">
|
||||||
|
обработку персональных данных.
|
||||||
|
</NuxtLink>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button class="not-prose" :disabled="modal_form.disabled" type="submit">
|
||||||
|
Отправить
|
||||||
|
</button>
|
||||||
|
<button class="not-prose" type="reset" @click="toggleModal">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex gap-4 justify-center">
|
||||||
|
<button class="not-prose" @click="openForm">
|
||||||
|
Отправить расчет на <span class="whitespace-nowrap">e-mail</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,69 +1,66 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Vector3 } from 'three'
|
import { Vector3 } from "three";
|
||||||
|
|
||||||
|
const { openModal } = useModalState();
|
||||||
|
|
||||||
const types = {
|
const types = {
|
||||||
bench: defineAsyncComponent(() => import('./bench.vue')),
|
bench: defineAsyncComponent(() => import("./bench.vue")),
|
||||||
table: defineAsyncComponent(() => import('./bench-table.vue'))
|
table: defineAsyncComponent(() => import("./bench-table.vue")),
|
||||||
}
|
};
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
type: {
|
type: {
|
||||||
type: String as PropType<keyof typeof types>,
|
type: String as PropType<keyof typeof types>,
|
||||||
default: 'bench',
|
default: "bench",
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const camera = ref()
|
const camera = ref();
|
||||||
const controls = ref()
|
const controls = ref();
|
||||||
|
|
||||||
const controlsState = reactive({
|
const controlsState = reactive({
|
||||||
minDistance: 2,
|
minDistance: 0.5,
|
||||||
maxDistance: 20,
|
maxDistance: 5,
|
||||||
enablePan: false
|
enablePan: false,
|
||||||
// enableZoom: false,
|
enableZoom: false,
|
||||||
// maxPolarAngle: Math.PI / 2 - 0.2
|
// maxPolarAngle: Math.PI / 2 - 0.2
|
||||||
})
|
});
|
||||||
|
|
||||||
const toggleModal = () => {}
|
|
||||||
|
|
||||||
const changeDistance = (v = 1) => {
|
const changeDistance = (v = 1) => {
|
||||||
if (camera.value && controls.value) {
|
if (camera.value && controls.value) {
|
||||||
const distance = camera.value.position.distanceTo(new Vector3(0, 0, 0))
|
const distance = camera.value.position.distanceTo(new Vector3(0, 0, 0));
|
||||||
const r = distance + v
|
const r = distance + v;
|
||||||
camera.value.position.normalize().multiplyScalar(r)
|
camera.value.position.normalize().multiplyScalar(r);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<div class="relative h-[600px] max-h-screen">
|
<div class="relative h-[600px] max-h-screen">
|
||||||
<ClientOnly fallback-tag="div">
|
<ClientOnly fallback-tag="div">
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="fallback">Загрузка 3D модели</div>
|
<div class="fallback">Загрузка 3D модели</div>
|
||||||
</template>
|
</template>
|
||||||
<TresCanvas height="600" clear-color="#e2e8f0">
|
<TresCanvas height="600" clear-color="#e2e8f0">
|
||||||
<TresPerspectiveCamera
|
<TresPerspectiveCamera :position="new Vector3(-7, 2, 4)" ref="camera" />
|
||||||
:position="new Vector3(-7, 2, 4)"
|
|
||||||
ref="camera"
|
|
||||||
/>
|
|
||||||
<OrbitControls v-bind="controlsState" ref="controls" make-default />
|
<OrbitControls v-bind="controlsState" ref="controls" make-default />
|
||||||
<ModelEnv />
|
<ModelEnv />
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<component :is="types[props.type]" />
|
<component :is="types[props.type]" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TresCanvas>
|
</TresCanvas>
|
||||||
<div class="canvas-icons">
|
<div class="canvas-icons top-4 left-4">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@click.prevent="changeDistance(-0.5)"
|
@click.prevent="changeDistance(-0.5)"
|
||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
disabled: camera
|
disabled: camera
|
||||||
? camera.position.distanceTo(new Vector3(0, 0, 0)) <= controlsState.minDistance
|
? camera.position.distanceTo(new Vector3(0, 0, 0)) <=
|
||||||
: null
|
controlsState.minDistance
|
||||||
}
|
: null,
|
||||||
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:plus-circle-outline" />
|
<Icon name="mdi:plus-circle-outline" />
|
||||||
|
@ -74,16 +71,18 @@ const changeDistance = (v = 1) => {
|
||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
disabled: camera
|
disabled: camera
|
||||||
? camera.position.distanceTo(new Vector3(0, 0, 0)) >= controlsState.maxDistance
|
? camera.position.distanceTo(new Vector3(0, 0, 0)) >=
|
||||||
: null
|
controlsState.maxDistance
|
||||||
}
|
: null,
|
||||||
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:minus-circle-outline" />
|
<Icon name="mdi:minus-circle-outline" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
<div class="absolute bottom-4 left-1/2 -translate-x-1/2">
|
||||||
|
<button @click.prevent="openModal">Рассчитать</button>
|
||||||
</div>
|
</div>
|
||||||
<button @click.prevent="toggleModal">Рассчитать</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
// useModalState.ts
|
||||||
|
import { ref, reactive, watch } from 'vue';
|
||||||
|
import { useRuntimeConfig } from '#app';
|
||||||
|
|
||||||
|
type ModalDataType = {
|
||||||
|
phone?: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
policy: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Состояние открытия модального окна
|
||||||
|
const isModalOpen = ref(false);
|
||||||
|
// Реактивные данные формы
|
||||||
|
const modal_data = reactive<ModalDataType>({
|
||||||
|
email: undefined,
|
||||||
|
phone: undefined,
|
||||||
|
name: undefined,
|
||||||
|
policy: false,
|
||||||
|
});
|
||||||
|
// Реактивные данные состояния формы
|
||||||
|
const modal_form = reactive({
|
||||||
|
disabled: true,
|
||||||
|
errors: [] as string[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Реактивные данные состояния модального окна
|
||||||
|
const modal_state = reactive({
|
||||||
|
show_form: true,
|
||||||
|
show_status: null as null | 'loading' | 'success' | 'error',
|
||||||
|
});
|
||||||
|
export function useModalState(initialState = false) {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const apiBase = config.public.apiBase;
|
||||||
|
|
||||||
|
// Функция для открытия модального окна
|
||||||
|
const openModal = () => {
|
||||||
|
isModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для закрытия модального окна
|
||||||
|
const closeModal = () => {
|
||||||
|
resetModalData();
|
||||||
|
isModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для переключения состояния модального окна
|
||||||
|
const toggleModal = () => {
|
||||||
|
resetModalData();
|
||||||
|
isModalOpen.value = !isModalOpen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для сброса данных формы
|
||||||
|
const resetModalData = () => {
|
||||||
|
modal_data.email = undefined;
|
||||||
|
modal_data.phone = undefined;
|
||||||
|
modal_data.name = undefined;
|
||||||
|
modal_data.policy = false;
|
||||||
|
|
||||||
|
modal_state.show_form = true;
|
||||||
|
modal_state.show_status = null;
|
||||||
|
|
||||||
|
modal_form.disabled = true;
|
||||||
|
modal_form.errors = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для открытия формы
|
||||||
|
const openForm = () => {
|
||||||
|
modal_state.show_form = !modal_state.show_form;
|
||||||
|
goal('open_form');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Валидация ввода
|
||||||
|
const validateInput = (evt: KeyboardEvent) => {
|
||||||
|
const valid_symbols = /[a-zA-Z0-9\+(\\)\ @\.]/;
|
||||||
|
if (!valid_symbols.test(evt.key)) {
|
||||||
|
evt.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Валидация данных формы
|
||||||
|
const validate = () => {
|
||||||
|
const phone_regexp = /^\+?[\d\s-()]{0,14}\d{11}$/;
|
||||||
|
const email_regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
|
||||||
|
|
||||||
|
modal_form.disabled = true;
|
||||||
|
if (
|
||||||
|
((modal_data.phone && phone_regexp.test(modal_data.phone)) ||
|
||||||
|
(modal_data.email && email_regex.test(modal_data.email))) &&
|
||||||
|
modal_data.policy === true
|
||||||
|
) {
|
||||||
|
modal_form.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправка данных на сервер
|
||||||
|
const submit = async () => {
|
||||||
|
goal('submit_form', modal_data);
|
||||||
|
modal_state.show_status = 'loading';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${apiBase}/custom_request/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: modal_data.name || `ref from site ${new Date()}`,
|
||||||
|
phone: modal_data.phone,
|
||||||
|
email: modal_data.email,
|
||||||
|
privacy: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
modal_state.show_status = 'success';
|
||||||
|
} catch (error) {
|
||||||
|
modal_state.show_status = 'error';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Наблюдение за изменениями данных формы
|
||||||
|
watch(modal_data, validate);
|
||||||
|
|
||||||
|
// Наблюдение за состоянием модального окна
|
||||||
|
watch(isModalOpen, (newValue) => {
|
||||||
|
console.log(isModalOpen)
|
||||||
|
if (newValue) {
|
||||||
|
document.body.classList.add('modal-opened');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('modal-opened');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isModalOpen,
|
||||||
|
modal_data,
|
||||||
|
modal_form,
|
||||||
|
modal_state,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
toggleModal,
|
||||||
|
openForm,
|
||||||
|
validateInput,
|
||||||
|
validate,
|
||||||
|
submit,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default (target: string, params: object = {}) => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
if (nuxtApp.$metrika) {
|
||||||
|
(nuxtApp.$metrika as any).reachGoal(target, params || {})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue