This commit is contained in:
Kseninia Mikhaylova 2025-03-14 12:59:47 +03:00
parent bad63f37e9
commit 8d8c0ead07
6 changed files with 311 additions and 70 deletions

View File

@ -1,8 +1,9 @@
<script setup lang="ts">
import '@/assets/main.scss'
import "@/assets/main.scss";
</script>
<template>
<div>
<Modal />
<Header />
<NuxtPage />
<Footer />

View File

@ -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;

90
components/modal.vue Normal file
View File

@ -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>

View File

@ -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>

145
composables/modal.ts Normal file
View File

@ -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,
};
}

6
utils/goal.ts Normal file
View File

@ -0,0 +1,6 @@
export default (target: string, params: object = {}) => {
const nuxtApp = useNuxtApp()
if (nuxtApp.$metrika) {
(nuxtApp.$metrika as any).reachGoal(target, params || {})
}
}