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">
|
||||
import '@/assets/main.scss'
|
||||
import "@/assets/main.scss";
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<Modal />
|
||||
<Header />
|
||||
<NuxtPage />
|
||||
<Footer />
|
||||
|
|
|
@ -455,7 +455,7 @@ button {
|
|||
}
|
||||
|
||||
.canvas-icons {
|
||||
@apply absolute text-3xl top-0 left-0 flex flex-col;
|
||||
@apply absolute text-3xl flex flex-col;
|
||||
|
||||
a {
|
||||
@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">
|
||||
import { Vector3 } from 'three'
|
||||
import { Vector3 } from "three";
|
||||
|
||||
const { openModal } = useModalState();
|
||||
|
||||
const types = {
|
||||
bench: defineAsyncComponent(() => import('./bench.vue')),
|
||||
table: defineAsyncComponent(() => import('./bench-table.vue'))
|
||||
}
|
||||
bench: defineAsyncComponent(() => import("./bench.vue")),
|
||||
table: defineAsyncComponent(() => import("./bench-table.vue")),
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<keyof typeof types>,
|
||||
default: 'bench',
|
||||
required: true
|
||||
}
|
||||
})
|
||||
default: "bench",
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const camera = ref()
|
||||
const controls = ref()
|
||||
const camera = ref();
|
||||
const controls = ref();
|
||||
|
||||
const controlsState = reactive({
|
||||
minDistance: 2,
|
||||
maxDistance: 20,
|
||||
enablePan: false
|
||||
// enableZoom: false,
|
||||
minDistance: 0.5,
|
||||
maxDistance: 5,
|
||||
enablePan: false,
|
||||
enableZoom: false,
|
||||
// maxPolarAngle: Math.PI / 2 - 0.2
|
||||
})
|
||||
|
||||
const toggleModal = () => {}
|
||||
});
|
||||
|
||||
const changeDistance = (v = 1) => {
|
||||
if (camera.value && controls.value) {
|
||||
const distance = camera.value.position.distanceTo(new Vector3(0, 0, 0))
|
||||
const r = distance + v
|
||||
camera.value.position.normalize().multiplyScalar(r)
|
||||
const distance = camera.value.position.distanceTo(new Vector3(0, 0, 0));
|
||||
const r = distance + v;
|
||||
camera.value.position.normalize().multiplyScalar(r);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="relative h-[600px] max-h-screen">
|
||||
<ClientOnly fallback-tag="div">
|
||||
<template #fallback>
|
||||
<div class="fallback">Загрузка 3D модели</div>
|
||||
</template>
|
||||
<TresCanvas height="600" clear-color="#e2e8f0">
|
||||
<TresPerspectiveCamera
|
||||
:position="new Vector3(-7, 2, 4)"
|
||||
ref="camera"
|
||||
/>
|
||||
<TresPerspectiveCamera :position="new Vector3(-7, 2, 4)" ref="camera" />
|
||||
<OrbitControls v-bind="controlsState" ref="controls" make-default />
|
||||
<ModelEnv />
|
||||
<Suspense>
|
||||
<component :is="types[props.type]" />
|
||||
</Suspense>
|
||||
</TresCanvas>
|
||||
<div class="canvas-icons">
|
||||
<div class="canvas-icons top-4 left-4">
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="changeDistance(-0.5)"
|
||||
:class="[
|
||||
{
|
||||
disabled: camera
|
||||
? camera.position.distanceTo(new Vector3(0, 0, 0)) <= controlsState.minDistance
|
||||
: null
|
||||
}
|
||||
? camera.position.distanceTo(new Vector3(0, 0, 0)) <=
|
||||
controlsState.minDistance
|
||||
: null,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Icon name="mdi:plus-circle-outline" />
|
||||
|
@ -74,16 +71,18 @@ const changeDistance = (v = 1) => {
|
|||
:class="[
|
||||
{
|
||||
disabled: camera
|
||||
? camera.position.distanceTo(new Vector3(0, 0, 0)) >= controlsState.maxDistance
|
||||
: null
|
||||
}
|
||||
? camera.position.distanceTo(new Vector3(0, 0, 0)) >=
|
||||
controlsState.maxDistance
|
||||
: null,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Icon name="mdi:minus-circle-outline" />
|
||||
</a>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2">
|
||||
<button @click.prevent="openModal">Рассчитать</button>
|
||||
</div>
|
||||
<button @click.prevent="toggleModal">Рассчитать</button>
|
||||
</div>
|
||||
</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