10 Commits
Author SHA1 Message Date
austnv d42aafaad7 UI patch 2026-06-07 01:47:51 +03:00
austnv fa8498ef4c v0.4.0
add:
- legal .md files
- routers
- home view
- rewrite App.vue
- update footer / header
- update features
2026-06-07 01:19:02 +03:00
austnv eb17a3cd67 fix grid-3 to grid-2 2026-06-07 00:21:32 +03:00
austnv fee07ce458 update pricing and faq 2026-06-07 00:03:16 +03:00
austnv 7cded67631 add ScreenshotButton to development needs 2026-05-27 22:18:30 +03:00
austnv 3fb0d198a2 update pricing section
fix: footer & header
2026-05-27 22:00:42 +03:00
austnv e4d73becc9 update #3 2026-05-24 13:17:32 +03:00
austnv eef1ba929f add FaqSection 2026-05-24 13:17:06 +03:00
austnv 055aaa20a1 add CompatibilitySection 2026-05-24 13:07:48 +03:00
austnv 3e68af7c58 update FeatureSection 2026-05-24 12:34:26 +03:00
20 changed files with 1143 additions and 91 deletions
+96 -3
View File
@@ -1,23 +1,27 @@
{
"name": "uvpn.shop",
"version": "0.1.0",
"version": "0.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "uvpn.shop",
"version": "0.1.0",
"version": "0.3.1",
"dependencies": {
"@lucide/vue": "^1.16.0",
"@tailwindcss/vite": "^4.3.0",
"@tresjs/cientos": "^5.7.1",
"@tresjs/core": "^5.8.1",
"marked": "^18.0.5",
"swiper": "^12.1.4",
"tailwindcss": "^4.3.0",
"three": "^0.184.0",
"vue": "^3.5.32"
"vue": "^3.5.32",
"vue-router": "^4.6.4",
"vue3-simple-icons": "^16.10.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@vitejs/plugin-vue": "^6.0.6",
"vite": "^8.0.8",
"vite-plugin-vue-devtools": "^8.1.1"
@@ -1133,6 +1137,19 @@
"node": ">= 20"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz",
@@ -1664,6 +1681,19 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -2288,6 +2318,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/marked": {
"version": "18.0.5",
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz",
"integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/meshoptimizer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
@@ -2440,6 +2482,20 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/potpack": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
@@ -2798,6 +2854,13 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
@@ -3000,6 +3063,36 @@
}
}
},
"node_modules/vue-router": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/vue-router/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue3-simple-icons": {
"version": "16.10.0",
"resolved": "https://registry.npmjs.org/vue3-simple-icons/-/vue3-simple-icons-16.10.0.tgz",
"integrity": "sha512-i5i4K3R+ZLFLsb+Gjw7jHxOQW5hhH4NcB0U2Xe1v3FKnqFJow8A5sgRglAwFtFDHLw1peDIRRRe8dCxs9yhVdQ==",
"license": "MIT",
"dependencies": {
"vue": "^3"
}
},
"node_modules/wsl-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+6 -2
View File
@@ -1,6 +1,6 @@
{
"name": "uvpn.shop",
"version": "0.2.0",
"version": "0.4.1",
"private": true,
"type": "module",
"scripts": {
@@ -13,12 +13,16 @@
"@tailwindcss/vite": "^4.3.0",
"@tresjs/cientos": "^5.7.1",
"@tresjs/core": "^5.8.1",
"marked": "^18.0.5",
"swiper": "^12.1.4",
"tailwindcss": "^4.3.0",
"three": "^0.184.0",
"vue": "^3.5.32"
"vue": "^3.5.32",
"vue-router": "^4.6.4",
"vue3-simple-icons": "^16.10.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@vitejs/plugin-vue": "^6.0.6",
"vite": "^8.0.8",
"vite-plugin-vue-devtools": "^8.1.1"
+107
View File
@@ -0,0 +1,107 @@
# Пользовательское соглашение
### 1. Общие положения
1.1. Настоящее Пользовательское соглашение (далее — **«Соглашение»** ) регулирует порядок использования онлайн-сервиса (далее — **«Сервис»** ), предоставляемого Администрацией.
1.2. Используя Сервис, включая запуск бота, регистрацию, оплату услуг или получение доступа к материалам, Пользователь подтверждает, что полностью ознакомился с условиями настоящего Соглашения и принимает их в полном объёме.
1.3. В случае несогласия с условиями Соглашения Пользователь обязан прекратить использование Сервиса.
### 2. Характер услуг и цифровых товаров
2.1. Сервис предоставляет цифровые товары и услуги нематериального характера, включая, но не ограничиваясь: информационные материалы, обучающие программы, консультации, цифровые продукты и сервисные услуги.
2.2. Материалы, предоставляемые через Сервис, могут включать:
* информацию из открытых источников;
* авторские материалы Администрации и/или третьих лиц;
* аналитические обзоры, подборки, рекомендации, структурированные данные.
2.3. Пользователь осознаёт и соглашается, что ценность цифровых товаров и услуг Сервиса заключается **в систематизации, анализе, форме подачи, сопровождении, поддержке и обновлениях** , а не в эксклюзивности отдельных фрагментов информации.
2.4. Сервис не заявляет и не гарантирует уникальность, исключительность или недоступность отдельных элементов материалов вне Сервиса.
### 3. Отказ от гарантий и ответственности
3.1. Сервис предоставляется на условиях **«AS IS» («как есть»)** .
3.2. Администрация не гарантирует:
* соответствие Сервиса ожиданиям Пользователя;
* достижение каких-либо финансовых, коммерческих, профессиональных или иных результатов;
* бесперебойную и безошибочную работу Сервиса.
3.3. Администрация не несёт ответственности за:
* любые прямые или косвенные убытки, включая упущенную выгоду;
* последствия применения Пользователем полученных материалов;
* действия или бездействие третьих лиц;
* временные технические сбои и ограничения доступа.
3.4. Все решения о применении материалов, рекомендаций и услуг принимаются Пользователем **самостоятельно и на его риск** .
### 4. Законность использования
4.1. Сервис не предназначен для поощрения, организации или содействия противоправной деятельности.
4.2. Пользователь обязуется использовать Сервис исключительно в рамках применимого законодательства и правил третьих сторон.
4.3. Ответственность за законность использования материалов и услуг Сервиса полностью возлагается на Пользователя.
### 5. Интеллектуальная собственность
5.1. Все материалы, размещённые в Сервисе, охраняются законодательством об интеллектуальной собственности.
5.2. Пользователю запрещается копировать, распространять, перепродавать, передавать третьим лицам или иным образом использовать материалы Сервиса без разрешения правообладателя.
5.3. Нарушение прав интеллектуальной собственности может повлечь ограничение доступа к Сервису без компенсации.
### 6. Ограничение доступа
6.1. Администрация вправе приостановить или ограничить доступ Пользователя к Сервису в случае:
* нарушения условий настоящего Соглашения;
* выявления злоупотреблений;
* требований законодательства или платёжных провайдеров.
6.2. Ограничение доступа не освобождает Пользователя от обязательств, возникших ранее.
6.3. Администрация оставляет за собой право отказывать в обслуживании Пользователям, чьи действия могут создавать повышенные риски для Сервиса, платёжных провайдеров или третьих лиц.
### 7. Платежи и возвраты
7.1. Оплата услуг и цифровых товаров производится на условиях, указанных в Сервисе до момента оплаты.
7.2. В связи с нематериальным характером цифровых товаров и услуг, **возврат денежных средств после предоставления доступа не осуществляется** , за исключением случаев, указанных ниже.
7.3. Возврат средств возможен только если:
* услуга не была оказана по технической вине Сервиса;
* доступ к цифровому товару фактически не был предоставлен.
7.4. Для рассмотрения вопроса о возврате Пользователь обязан обратиться в службу поддержки в течение **24 часов** с момента оплаты.
7.5. Решение о возврате принимается Администрацией индивидуально.
7.6. Пользователь подтверждает, что обязуется **не инициировать возврат платежа (chargeback)** через платёжные системы без предварительного обращения в службу поддержки Сервиса.
### 8. Конфиденциальность
8.1. Администрация может собирать минимально необходимые технические данные для обеспечения работы Сервиса.
8.2. Администрация принимает разумные меры для защиты данных, однако не гарантирует абсолютную безопасность передаваемой информации.
### 9. Изменение условий
9.1. Администрация вправе вносить изменения в настоящее Соглашение.
9.2. Актуальная версия Соглашения публикуется в Сервисе.
9.3. Продолжение использования Сервиса означает согласие Пользователя с обновлёнными условиями.
### 10. Контактная информация
10.1. По всем вопросам Пользователь может обратиться в службу поддержки через форму в самом боте.
**Используя Сервис (в том числе запуская бота и/или вводя команду /start), Пользователь подтверждает, что ознакомлен с настоящим Соглашением и принимает его условия в полном объёме.**
+64
View File
@@ -0,0 +1,64 @@
# Политика конфиденциальности
Политика конфиденциальности регулирует сбор, использование и защиту информации пользователей сервиса. Собираются идентификаторы аккаунта, техническая информация и история взаимодействий. Данные используются для обеспечения работы сервиса, связи с пользователем и анализа. Передача информации третьим лицам возможна только в законодательно установленных случаях или с согласия пользователя. Хранение данных осуществляется в течение необходимого срока, их защита — в разумных пределах. Пользователь самостоятельно несёт ответственность за риски, связанные с передачей данных. Администрация вправе вносить изменения в Политику без уведомления — согласие считается принятым при дальнейшем использовании сервиса.
**1. Общие положения**
1.1. Настоящая Политика конфиденциальности (далее — «Политика») регулирует порядок обработки и защиты информации, которую Пользователь передаёт при использовании сервиса (далее — «Сервис»).
1.2. Используя Сервис, Пользователь подтверждает своё согласие с условиями Политики. Если Пользователь не согласен с условиями — он обязан прекратить использование Сервиса.
---
**2. Сбор информации**
2.1. Сервис может собирать следующие типы данных:
* идентификаторы аккаунта (логин, ID, никнейм и т.п.);
* техническую информацию (IP-адрес, данные о браузере, устройстве и операционной системе);
* историю взаимодействий с Сервисом.
* 2.2. Сервис не требует от Пользователя предоставления паспортных данных, документов, фотографий или другой личной информации, кроме минимально необходимой для работы.
---
**3. Использование информации**
3.1. Сервис может использовать полученную информацию исключительно для:
* обеспечения работы функционала;
* связи с Пользователем (в том числе для уведомлений и поддержки);
* анализа и улучшения работы Сервиса.
---
**4. Передача информации третьим лицам**
4.1. Администрация не передаёт полученные данные третьим лицам, за исключением случаев:
* если это требуется по закону;
* если это необходимо для исполнения обязательств перед Пользователем (например, при работе с платёжными системами);
* если Пользователь сам дал на это согласие.
---
**5. Хранение и защита данных**
5.1. Данные хранятся в течение срока, необходимого для достижения целей обработки.
5.2. Администрация принимает разумные меры для защиты данных, но не гарантирует абсолютную безопасность информации при передаче через интернет.
---
**6. Отказ от ответственности**
6.1. Пользователь понимает и соглашается, что передача информации через интернет всегда сопряжена с рисками.
6.2. Администрация не несёт ответственности за утрату, кражу или раскрытие данных, если это произошло по вине третьих лиц или самого Пользователя.
---
**7. Изменения в Политике**
7.1. Администрация вправе изменять условия Политики без предварительного уведомления.
7.2. Продолжение использования Сервиса после внесения изменений означает согласие Пользователя с новой редакцией Политики.
+7 -12
View File
@@ -1,21 +1,16 @@
<template>
<div class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-white font-sans antialiased min-h-screen">
<div class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-white font-sans antialiased min-h-screen flex flex-col">
<AppHeader />
<HeroSection />
<FeaturesSection />
<PricingSection />
<StepsSection />
<TestimonialsSection />
<main class="grow">
<RouterView />
</main>
<AppFooter />
</div>
</template>
<script setup>
import AppHeader from './components/AppHeader.vue'
import HeroSection from './components/HeroSection.vue'
import FeaturesSection from './components/FeaturesSection.vue'
import PricingSection from './components/PricingSection.vue'
import StepsSection from './components/StepsSection.vue'
import TestimonialsSection from './components/TestimonialsSection.vue'
import AppFooter from './components/AppFooter.vue'
</script>
</script>
+2 -1
View File
@@ -1 +1,2 @@
@import "tailwindcss";
@import "tailwindcss";
@plugin "@tailwindcss/typography";
+3 -3
View File
@@ -6,9 +6,9 @@
<span class="text-lg font-bold">uVPN</span>
</div>
<div class="flex gap-6 text-gray-500 dark:text-gray-400 text-sm">
<a href="#" class="hover:text-gray-900 dark:text-white transition">Политика конфиденциальности</a>
<a href="#" class="hover:text-gray-900 dark:text-white transition">Условия использования</a>
<a href="#" class="hover:text-gray-900 dark:text-white transition">Поддержка</a>
<router-link to="/docs/policy" class="hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400 transition hover:cursor-pointer">Политика конфиденциальности</router-link>
<router-link to="/docs/agreement" class="hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400 hover:cursor-pointer">Условия использования</router-link>
<a href="mailto:support@uvpn.shop" target="_blank" class="hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400 transition">Поддержка</a>
</div>
<p class="text-gray-500 dark:text-gray-400 text-sm">
© 2026 uVPN. Все права защищены.
+16 -18
View File
@@ -1,31 +1,29 @@
<template>
<header class="flex items-center justify-between px-6 py-4 md:px-12 border-b border-gray-200 dark:border-gray-800 sticky top-0 bg-white/80 dark:bg-gray-900/80 backdrop-blur-xs z-50">
<div class="flex items-center gap-3">
<!-- Логотип -->
<router-link to="/" class="flex items-center gap-3 hover:opacity-90 transition">
<img src="/images/logo.png" alt="uVPN logo" class="size-8" />
<span class="text-xl font-bold tracking-tight">uVPN</span>
</div>
</router-link>
<!-- Якорные ссылки -->
<!-- Навигация: теперь это router-link с хэшем секции -->
<nav class="hidden md:flex items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
<a href="#hero" @click.prevent="scrollTo('hero')" class="hover:text-gray-900 dark:hover:text-gray-400 dark:text-white transition">Главная</a>
<a href="#features" @click.prevent="scrollTo('features')" class="hover:text-gray-900 dark:hover:text-gray-400 dark:text-white transition">Преимущества</a>
<a href="#pricing" @click.prevent="scrollTo('pricing')" class="hover:text-gray-900 dark:hover:text-gray-400 dark:text-white transition">Тарифы</a>
<a href="#steps" @click.prevent="scrollTo('steps')" class="hover:text-gray-900 dark:hover:text-gray-400 dark:text-white transition">Как начать</a>
<a href="#testimonials" @click.prevent="scrollTo('testimonials')" class="hover:text-gray-900 dark:hover:text-gray-400 dark:text-white transition">Отзывы</a>
<a href="#footer" @click.prevent="scrollTo('footer')" class="hover:text-gray-900 dark:hover:text-gray-400 dark:text-white transition">Контакты</a>
<router-link :to="{ path: '/', hash: '#hero' }" class="hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400 transition">Главная</router-link>
<router-link :to="{ path: '/', hash: '#features' }" class="hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400 transition">Преимущества</router-link>
<router-link :to="{ path: '/', hash: '#pricing' }" class="hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400 transition">Тарифы</router-link>
<router-link :to="{ path: '/', hash: '#steps' }" class="hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400 transition">Как начать</router-link>
<router-link :to="{ path: '/', hash: '#testimonials' }" class="hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400 transition">Отзывы</router-link>
<router-link :to="{ path: '/', hash: '#faq' }" class="hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400 transition">FAQ</router-link>
</nav>
<button class="bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 transition px-5 py-2 rounded-lg font-medium text-sm text-white">
<!-- Кнопка тарифов -->
<a href="https://app.uvpn.shop" target="_blank" class="bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 transition px-5 py-2 rounded-lg font-medium text-sm text-white">
Попробовать бесплатно
</button>
</a>
</header>
</template>
<script setup>
const scrollTo = (id) => {
const el = document.getElementById(id)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
}
}
</script>
// Скрипты больше не нужны, всю магию сделает роутер!
</script>
+50
View File
@@ -0,0 +1,50 @@
<template>
<section id="compatibility" class="px-6 py-20 md:px-12 max-w-7xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold text-center mb-4">Совместимость</h2>
<p class="text-center text-gray-500 dark:text-gray-400 mb-14">Работает на всех устройствах</p>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
<!-- iOS -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-6 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<AppleIcon class="w-10 h-10 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
<p class="font-medium">iOS</p>
</div>
<!-- Android -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-6 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<AndroidIcon class="w-10 h-10 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
<p class="font-medium">Android</p>
</div>
<!-- Windows -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-6 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<WindowsIcon class="w-10 h-10 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
<p class="font-medium">Windows</p>
</div>
<!-- macOS -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-6 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<MacOsIcon class="w-10 h-10 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
<p class="font-medium">macOS</p>
</div>
<!-- Linux -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-6 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<LinuxIcon class="w-10 h-10 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
<p class="font-medium">Linux</p>
</div>
<!-- Smart TV -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-6 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<AppleTvIcon class="w-10 h-10 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
<p class="font-medium">Smart TV</p>
</div>
</div>
</section>
</template>
<script setup>
// import { Apple, Smartphone, Monitor, Terminal, Server, Tv } from '@lucide/vue'
import { AppleIcon, AndroidIcon, LinuxIcon, AppleTvIcon, MacOsIcon } from 'vue3-simple-icons';
import WindowsIcon from './WindowsIcon.vue';
</script>
+85
View File
@@ -0,0 +1,85 @@
<template>
<div class="max-w-4xl mx-auto px-4 py-12">
<!-- Навигация: теперь используем router-link вместо обычных кнопок -->
<nav class="flex space-x-6 mb-8 pb-2">
<router-link
to="/docs/policy"
class="px-1 py-2 text-sm font-medium transition-colors border-b-2 -mb-2.5"
active-class="border-blue-600 text-blue-600 dark:text-blue-400 dark:border-blue-400"
inactive-class="border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400"
>
Политика конфиденциальности
</router-link>
<router-link
to="/docs/agreement"
class="px-1 py-2 text-sm font-medium transition-colors border-b-2 -mb-2.5"
active-class="border-blue-600 text-blue-600 dark:text-blue-400 dark:border-blue-400"
inactive-class="border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400"
>
Пользовательское соглашение
</router-link>
</nav>
<!-- Область отображения Markdown (Tailwind Typography) -->
<div
v-if="htmlContent"
v-html="htmlContent"
class="prose prose-slate max-w-none dark:prose-invert"
></div>
<div v-else-if="isLoading" class="text-center py-12 text-slate-500 dark:text-slate-400">
Загрузка документа...
</div>
<div v-else class="text-center py-12 text-red-500">
Документ не найден или произошла ошибка.
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { marked } from 'marked'
// Принимаем параметр docName из роутера
const props = defineProps({
docName: {
type: String,
required: true
}
})
const htmlContent = ref('')
const isLoading = ref(true)
// Функция загрузки md-файла
const fetchMarkdown = async (name) => {
isLoading.value = true
htmlContent.value = ''
try {
// Загрузка из папки public/docs/
const response = await fetch(`/docs/${name}.md`)
if (!response.ok) throw new Error('Файл отсутствует')
const markdownText = await response.text()
htmlContent.value = marked(markdownText)
} catch (error) {
console.error('Ошибка отрисовки md:', error)
htmlContent.value = ''
} finally {
isLoading.value = false
}
}
// Загружаем файл при первой инициализации страницы
onMounted(() => {
fetchMarkdown(props.docName)
})
// Отслеживаем изменение ссылки (например, если пользователь перешел с policy на agreement)
watch(() => props.docName, (newName) => {
fetchMarkdown(newName)
})
</script>
+62
View File
@@ -0,0 +1,62 @@
<template>
<section id="faq" class="px-6 py-20 md:px-12 max-w-4xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold text-center mb-14">
Часто задаваемые вопросы
</h2>
<div class="space-y-4">
<details
v-for="(item, index) in faqItems"
:key="index"
class="group bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 overflow-hidden transition-colors hover:border-blue-300 dark:hover:border-blue-700"
>
<summary class="flex items-center justify-between px-6 py-5 cursor-pointer list-none">
<span class="font-medium text-left pr-4">{{ item.question }}</span>
<ChevronDown
class="w-5 h-5 shrink-0 text-gray-400 transition-transform duration-300 group-open:rotate-180"
/>
</summary>
<div class="px-6 pb-5 text-gray-500 dark:text-gray-400 leading-relaxed" v-html="item.answer"></div>
</details>
</div>
</section>
</template>
<script setup>
import { ChevronDown } from '@lucide/vue'
const faqItems = [
{
question: 'Как начать пользоваться uVPN?',
answer: 'Откройте бота <a href="https://t.me/uvpn_official_bot" target="_blank" rel="noopener" class="text-blue-600 dark:text-blue-400">@uVPN</a> в Telegram, нажмите «Старт», выберите тариф или активируйте 7 дней бесплатно. Конфигурация придёт автоматически — подключение займёт 30 секунд. При недоступности Telegram воспользуйтесь <a href="https://app.uvpn.shop" target="_blank" class="text-blue-600 dark:text-blue-400">нашим сайтом</a>.'
},
{
question: 'Нужно ли устанавливать отдельное приложение?',
answer: 'Нет. uVPN работает через стандартные протоколы Xray — настройка в один клик. Поддерживаются iOS, Android, Windows, macOS, Linux и Smart TV.'
},
{
question: 'Какая реальная скорость соединения?',
answer: 'Наши серверы поддерживают до 1 Гбит/с. Для стриминга 4K достаточно 25 Мбит/с — этого у нас с запасом. Реальная скорость зависит от вашего провайдера и выбранного сервера.'
},
{
question: 'Есть ли гарантия возврата денег?',
answer: 'Да. Если uVPN вам не подойдёт по любой причине, мы вернём деньги в течение 30 дней без вопросов — просто напишите в поддержку через Telegram.'
},
{
question: 'Сколько устройств можно подключить одновременно?',
answer: 'Standard — до 3 устройств, Premium — до 10 устройств одновременно. Для семьи или небольшого офиса идеально подойдёт Premium.'
},
{
question: 'Хранит ли uVPN логи моей активности?',
answer: 'Нет. Мы придерживаемся строгой политики no-log: не сохраняем историю посещений, IP-адреса, время подключений и другие персональные данные.'
},
{
question: 'Как оплатить подписку?',
answer: 'Оплата принимается через мини-приложение в Telegram или на <a href="https://app.uvpn.shop" target="_blank">сайте. Доступны банковские карты (Visa, Mastercard, МИР), СБП. Автосписания нет — продление только по вашему желанию.'
},
{
question: 'Что делать, если VPN перестал работать?',
answer: 'Обновите конфиги в вашем клиенте. Если не помогло — напишите в поддержку, мы отвечаем в течение 15 минут.'
}
]
</script>
+28 -7
View File
@@ -4,25 +4,46 @@
Почему выбирают uVPN
</h2>
<div class="grid md:grid-cols-3 gap-8">
<!-- Преимущество 1 -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<Zap class="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-4" />
<h3 class="text-xl font-semibold mb-3">Высокая скорость</h3>
<p class="text-gray-500 dark:text-gray-400">Серверы в 90+ странах с пропускной способностью до 10 Гбит/с.</p>
<h3 class="text-xl font-semibold mb-3">Молниеносная скорость</h3>
<p class="text-gray-500 dark:text-gray-400">Серверы до 1 Гбит/с стримите, играйте и работайте без задержек</p>
</div>
<!-- Преимущество 2 -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<Shield class="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-4" />
<h3 class="text-xl font-semibold mb-3">Полная безопасность</h3>
<p class="text-gray-500 dark:text-gray-400">Шифрование AES-256 и политика отсутствия логов.</p>
<h3 class="text-xl font-semibold mb-3">Надёжное шифрование</h3>
<p class="text-gray-500 dark:text-gray-400">Современные протоколы защищают ваши данные в любой сети, включая публичный WiFi</p>
</div>
<!-- Преимущество 3 -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<Globe class="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-4" />
<h3 class="text-xl font-semibold mb-3">Глобальный доступ</h3>
<p class="text-gray-500 dark:text-gray-400">Обходите цензуру и получайте доступ к любому контенту из любой точки мира.</p>
<h3 class="text-xl font-semibold mb-3">30+ стран</h3>
<p class="text-gray-500 dark:text-gray-400">Серверы в Европе, Азии и Америке. Выберите локацию под нужный сервис</p>
</div>
<!-- Преимущество 4 -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<FileX class="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-4" />
<h3 class="text-xl font-semibold mb-3">Политика No-Log</h3>
<p class="text-gray-500 dark:text-gray-400">Не храним историю посещений, IP-адреса и никакие персональные данные</p>
</div>
<!-- Преимущество 5 -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<MessageCircle class="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-4" />
<h3 class="text-xl font-semibold mb-3">Управление в Telegram</h3>
<p class="text-gray-500 dark:text-gray-400">Подключение, смена сервера, продление всё через удобного бота</p>
</div>
<!-- Преимущество 6 -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 text-center hover:bg-gray-100 dark:hover:bg-gray-800 transition border border-gray-200 dark:border-gray-800">
<RefreshCw class="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-4" />
<h3 class="text-xl font-semibold mb-3">Стабильная работа</h3>
<p class="text-gray-500 dark:text-gray-400">Автоматические обновления конфигураций мы первые подстраиваемся под новые условия</p>
</div>
</div>
</section>
</template>
<script setup>
import { Zap, Shield, Globe } from '@lucide/vue'
import { Zap, Shield, Globe, FileX, MessageCircle, RefreshCw } from '@lucide/vue'
</script>
+12 -5
View File
@@ -9,14 +9,21 @@
<!-- Левая колонка -->
<div class="text-white">
<h1 class="text-4xl md:text-5xl font-extrabold leading-tight mb-6 drop-shadow-lg">
Безопасный и быстрый VPN от <span class="text-blue-400">uVPN</span>
Безопасный и быстрый интернет от <span class="text-blue-400">uVPN</span>
</h1>
<p class="text-gray-200 text-lg mb-8 max-w-md drop-shadow">
Защитите свои данные, обходите блокировки и оставайтесь анонимными в сети с нашим современным VPN-сервисом на базе Xray.
Защитите свои данные и оставайтесь анонимными в сети с нашим современным VPN-сервисом на базе Xray.
</p>
<button class="bg-blue-600 hover:bg-blue-700 transition px-8 py-4 rounded-xl font-bold text-lg shadow-lg shadow-blue-600/30 flex items-center gap-2">
<Zap class="w-5 h-5" /> Начать бесплатно
</button>
<a
href="https://app.uvpn.shop"
target="_blank"
rel="noopener noreferrer"
class="bg-blue-600 hover:bg-blue-700 transition px-8 py-4 rounded-xl font-bold text-lg shadow-lg shadow-blue-600/30 inline-flex items-center gap-2 text-white no-underline"
>
<Zap class="w-5 h-5" />
Начать бесплатно
</a>
<p class="text-gray-300 text-sm mt-4 drop-shadow">30-дневная гарантия возврата денег</p>
</div>
+130 -37
View File
@@ -3,45 +3,59 @@
<h2 class="text-3xl md:text-4xl font-bold text-center mb-14">
Прозрачные тарифы
</h2>
<div class="grid md:grid-cols-3 gap-8">
<!-- Тариф Месяц -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 flex flex-col border border-gray-200 dark:border-gray-800 hover:border-blue-600 transition">
<h3 class="text-xl font-semibold mb-2">Месяц</h3>
<p class="text-4xl font-extrabold mb-4">$11.99<span class="text-lg font-normal text-gray-500 dark:text-gray-400">/мес</span></p>
<!-- Переключатель периода -->
<div class="flex justify-center gap-2 mb-10">
<button
v-for="period in availablePeriods"
:key="period.value"
@click="selectedPeriod = period.value"
class="px-5 py-2 rounded-full text-sm font-medium transition hover:cursor-pointer"
:class="selectedPeriod === period.value
? 'bg-blue-600 text-white shadow-md'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'"
>
{{ period.label }}
</button>
</div>
<!-- Сетка тарифов -->
<div class="grid md:grid-cols-2 gap-12">
<div
v-for="tariff in tariffCards"
:key="tariff.key"
class="bg-white dark:bg-gray-900 rounded-2xl p-8 flex flex-col border transition"
:class="[
tariff.isPopular
? 'border-2 border-blue-600 relative transform scale-105 shadow-2xl shadow-blue-600/20'
: 'border border-gray-200 dark:border-gray-800 hover:border-blue-600'
]"
>
<span
v-if="tariff.isPopular"
class="absolute top-0 right-0 bg-blue-600 dark:bg-blue-500 text-gray-900 dark:text-white text-xs font-bold px-4 py-1 rounded-bl-xl rounded-tr-xl"
>
ПОПУЛЯРНЫЙ
</span>
<h3 class="text-xl font-semibold mb-2">{{ tariff.name }}</h3>
<p class="text-4xl font-extrabold mb-4">
{{ tariff.price }}<span class="text-lg font-normal text-gray-500 dark:text-gray-400">/мес</span>
</p>
<ul class="text-gray-500 dark:text-gray-400 space-y-3 mb-8 flex-1">
<li class="flex items-center gap-2"><Check class="w-4 h-4 text-green-400" /> Все устройства</li>
<li class="flex items-center gap-2"><Check class="w-4 h-4 text-green-400" /> Безлимитный трафик</li>
<li class="flex items-center gap-2"><Check class="w-4 h-4 text-green-400" /> Поддержка 24/7</li>
<li v-for="feature in tariff.features" :key="feature" class="flex items-center gap-2">
<Check class="w-4 h-4 text-green-400" /> {{ feature }}
</li>
</ul>
<button class="w-full bg-gray-800 hover:bg-gray-700 transition py-3 rounded-xl font-medium text-white">
Выбрать
</button>
</div>
<!-- Тариф Год (выделенный) -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 flex flex-col border-2 border-blue-600 relative transform scale-105 shadow-2xl shadow-blue-600/20">
<span class="absolute top-0 right-0 bg-blue-600 dark:bg-blue-500 text-gray-900 dark:text-white text-xs font-bold px-4 py-1 rounded-bl-xl rounded-tr-xl">ПОПУЛЯРНЫЙ</span>
<h3 class="text-xl font-semibold mb-2">Год</h3>
<p class="text-4xl font-extrabold mb-4">$4.99<span class="text-lg font-normal text-gray-500 dark:text-gray-400">/мес</span></p>
<ul class="text-gray-500 dark:text-gray-400 space-y-3 mb-8 flex-1">
<li class="flex items-center gap-2"><Check class="w-4 h-4 text-green-400" /> Все устройства</li>
<li class="flex items-center gap-2"><Check class="w-4 h-4 text-green-400" /> Безлимитный трафик</li>
<li class="flex items-center gap-2"><Check class="w-4 h-4 text-green-400" /> Приоритетная поддержка</li>
</ul>
<button class="w-full bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 transition py-3 rounded-xl font-bold text-white">
Выбрать тариф
</button>
</div>
<!-- Тариф 2 года -->
<div class="bg-white dark:bg-gray-900 rounded-2xl p-8 flex flex-col border border-gray-200 dark:border-gray-800 hover:border-blue-600 transition">
<h3 class="text-xl font-semibold mb-2">2 года</h3>
<p class="text-4xl font-extrabold mb-4">$2.99<span class="text-lg font-normal text-gray-500 dark:text-gray-400">/мес</span></p>
<ul class="text-gray-500 dark:text-gray-400 space-y-3 mb-8 flex-1">
<li class="flex items-center gap-2"><Check class="w-4 h-4 text-green-400" /> Все устройства</li>
<li class="flex items-center gap-2"><Check class="w-4 h-4 text-green-400" /> Безлимитный трафик</li>
<li class="flex items-center gap-2"><Check class="w-4 h-4 text-green-400" /> Максимальная экономия</li>
</ul>
<button class="w-full bg-gray-800 hover:bg-gray-700 transition py-3 rounded-xl font-medium text-white">
Выбрать
<button
class="w-full transition py-3 rounded-xl font-medium text-white"
:class="tariff.isPopular
? 'bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 font-bold'
: 'bg-gray-800 hover:bg-gray-700'"
>
{{ tariff.isPopular ? 'Выбрать тариф' : 'Выбрать' }}
</button>
</div>
</div>
@@ -49,5 +63,84 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { Check } from '@lucide/vue'
// Исходный конфиг
const pricingConfig = {
default_tariff: "premium",
tariffs: [
{
key: "standard",
names: { ru: "Standard", en: "Standard" },
descriptions: { ru: "До 3 устройств", en: "up to 3 devices" },
prices_rub: { "1": 179.0, "3": 387.0, "6": 594.0, "12": 996.0 },
hwid_device_limit: 2,
monthly_gb: 0.0
},
{
key: "premium",
names: { ru: "Premium", en: "Premium"},
descriptions: { ru: "До 10 устройств, сервера РФ и США", en: "Up to 10 devices, RU, USA servers" },
prices_rub: { "1": 349.0, "3": 987.0, "6": 1734.0, "12": 1992.0 },
hwid_device_limit: 10,
monthly_gb: 0.0
}
]
}
// Доступные периоды
const availablePeriods = [
{ value: 1, label: '1 мес' },
{ value: 3, label: '3 мес' },
{ value: 6, label: '6 мес' },
{ value: 12, label: '12 мес' }
]
// Выбранный период (по умолчанию 1 месяц)
const selectedPeriod = ref(1)
// Функция для получения цены за месяц
const getMonthlyPrice = (tariff, period) => {
const total = tariff.prices_rub[String(period)]
if (!total) return '—'
const monthly = total / period
return Number.isInteger(monthly) ? monthly : monthly.toFixed(2)
}
// Подготовка данных для карточек
const tariffCards = computed(() => {
const tariffs = pricingConfig.tariffs
const defaultKey = pricingConfig.default_tariff
// Разделяем тарифы: популярный (премиум) и остальные
const premiumTariff = tariffs.find(t => t.key === defaultKey)
const otherTariffs = tariffs.filter(t => t.key !== defaultKey)
// Порядок: первый обычный, премиум, второй обычный
const ordered = [
{ ...otherTariffs[0], isPopular: false },
{ ...premiumTariff, isPopular: true },
]
return ordered.map(tariff => {
const price = getMonthlyPrice(tariff, selectedPeriod.value)
// Фичи: разбиваем описание по запятой и добавляем общие пункты
const descriptionFeatures = tariff.descriptions.ru
.split(',')
.map(s => s.trim())
.filter(Boolean)
const staticFeatures = ['Безлимитный трафик', 'Поддержка 24/7']
const features = [...new Set([...descriptionFeatures, ...staticFeatures])]
return {
key: tariff.key,
name: tariff.names.ru,
price: `${price}`,
features,
isPopular: tariff.isPopular
}
})
})
</script>
+411
View File
@@ -0,0 +1,411 @@
<template>
<div class="screenshot-controls">
<!-- Основная кнопка скриншота -->
<button
@click="takeScreenshot"
:disabled="isTakingScreenshot || !isModelReady"
class="screenshot-btn"
:class="{ 'screenshot-btn-loading': isTakingScreenshot }"
>
<Camera v-if="!isTakingScreenshot" class="w-5 h-5" />
<Loader2 v-else class="w-5 h-5 animate-spin" />
{{ buttonText }}
</button>
<!-- Уведомление -->
<Transition name="notification">
<div v-if="showNotification" class="screenshot-notification" :class="notificationType">
<CheckCircle v-if="notificationType === 'success'" class="w-4 h-4" />
<AlertCircle v-else-if="notificationType === 'error'" class="w-4 h-4" />
<Download v-else-if="notificationType === 'downloading'" class="w-4 h-4 animate-bounce" />
{{ notificationMessage }}
</div>
</Transition>
<!-- Меню настроек (опционально) -->
<div v-if="showSettings" class="screenshot-settings">
<button @click="toggleSettings" class="settings-btn">
<Settings class="w-4 h-4" />
</button>
<div v-if="settingsOpen" class="settings-dropdown">
<label>
<input type="checkbox" v-model="settings.keepAutoRotate" />
Сохранять авто-вращение
</label>
<label>
<select v-model="settings.format">
<option value="image/png">PNG</option>
<option value="image/jpeg">JPEG</option>
<option value="image/webp">WebP</option>
</select>
</label>
<label>
Качество:
<input type="range" v-model.number="settings.quality" min="0.1" max="1.0" step="0.1" />
{{ Math.round(settings.quality * 100) }}%
</label>
<label>
<input type="checkbox" v-model="settings.transparentBg" />
Прозрачный фон
</label>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, onUnmounted } from 'vue'
import { Camera, Loader2, CheckCircle, AlertCircle, Download, Settings } from '@lucide/vue'
// Пропсы для конфигурации
const props = defineProps({
// Ссылка на Three.js рендерер
renderer: {
type: Object,
required: true
},
// Ссылка на OrbitControls (для управления авто-вращением)
controls: {
type: Object,
default: null
},
// Флаг готовности модели
isModelReady: {
type: Boolean,
default: false
},
// Имя файла по умолчанию
filename: {
type: String,
default: 'screenshot'
},
// Показывать ли настройки
showSettings: {
type: Boolean,
default: false
},
// Текст на кнопке
buttonText: {
type: String,
default: 'Сделать скриншот'
},
// Автоматическое уведомление
autoNotify: {
type: Boolean,
default: true
}
})
// Состояния
const isTakingScreenshot = ref(false)
const showNotification = ref(false)
const notificationMessage = ref('')
const notificationType = ref('success')
const settingsOpen = ref(false)
// Настройки по умолчанию
const settings = ref({
keepAutoRotate: true,
format: 'image/png',
quality: 1.0,
transparentBg: false
})
// Таймер для уведомлений
let notificationTimeout = null
// Показать уведомление
const showNotificationMessage = (message, type = 'success', duration = 3000) => {
if (!props.autoNotify) return
if (notificationTimeout) clearTimeout(notificationTimeout)
notificationMessage.value = message
notificationType.value = type
showNotification.value = true
notificationTimeout = setTimeout(() => {
showNotification.value = false
}, duration)
}
// Переключение настроек
const toggleSettings = () => {
settingsOpen.value = !settingsOpen.value
}
// Основной метод скриншота
const takeScreenshot = async () => {
if (!props.renderer || !props.isModelReady) {
showNotificationMessage('Модель еще не загружена', 'error', 2000)
return
}
isTakingScreenshot.value = true
// Сохраняем состояние авто-вращения
let wasAutoRotating = false
if (props.controls && !settings.value.keepAutoRotate) {
wasAutoRotating = props.controls.autoRotate
if (wasAutoRotating) {
props.controls.autoRotate = false
props.controls.update()
}
}
try {
// Небольшая задержка для стабилизации кадра
await new Promise(resolve => setTimeout(resolve, 50))
const canvas = props.renderer.domElement
// Если нужен прозрачный фон
if (settings.value.transparentBg) {
const originalClearColor = props.renderer.getClearColor()
const originalAlpha = props.renderer.getClearAlpha()
props.renderer.setClearColor(0x000000, 0)
// Делаем скриншот после изменения прозрачности
await new Promise(resolve => setTimeout(resolve, 10))
const blob = await new Promise((resolve) => {
canvas.toBlob(resolve, settings.value.format, settings.value.quality)
})
// Восстанавливаем оригинальный цвет фона
props.renderer.setClearColor(originalClearColor, originalAlpha)
if (blob) {
downloadBlob(blob)
}
} else {
// Обычный скриншот
const blob = await new Promise((resolve) => {
canvas.toBlob(resolve, settings.value.format, settings.value.quality)
})
if (blob) {
downloadBlob(blob)
}
}
showNotificationMessage('Скриншот сохранен!', 'success', 2000)
} catch (error) {
console.error('Ошибка при создании скриншота:', error)
showNotificationMessage('Ошибка при сохранении скриншота', 'error', 3000)
} finally {
// Восстанавливаем авто-вращение
if (props.controls && !settings.value.keepAutoRotate && wasAutoRotating) {
props.controls.autoRotate = wasAutoRotating
props.controls.update()
}
isTakingScreenshot.value = false
}
}
// Скачивание blob
const downloadBlob = (blob) => {
// Показываем уведомление о начале скачивания
showNotificationMessage('Подготовка файла...', 'downloading', 1000)
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
const extension = settings.value.format.split('/')[1]
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-')
link.href = url
link.download = `${props.filename}-${timestamp}.${extension}`
document.body.appendChild(link)
link.click()
// Очищаем
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
// Очистка при размонтировании
onUnmounted(() => {
if (notificationTimeout) clearTimeout(notificationTimeout)
})
// Следим за изменениями прозрачности
watch(() => settings.value.transparentBg, (newVal) => {
if (props.renderer) {
if (newVal) {
props.renderer.setClearColor(0x000000, 0)
} else {
props.renderer.setClearColor(0x000000, 1)
}
}
})
</script>
<style scoped>
.screenshot-controls {
position: relative;
display: inline-block;
}
.screenshot-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.75rem;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.screenshot-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.screenshot-btn:active:not(:disabled) {
transform: translateY(0);
}
.screenshot-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.screenshot-btn-loading {
cursor: wait;
}
.screenshot-notification {
position: fixed;
bottom: 2rem;
right: 2rem;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
background: white;
color: #1f2937;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
z-index: 1000;
font-size: 0.875rem;
font-weight: 500;
}
.screenshot-notification.success {
background: #10b981;
color: white;
}
.screenshot-notification.error {
background: #ef4444;
color: white;
}
.screenshot-notification.downloading {
background: #3b82f6;
color: white;
}
.notification-enter-active,
.notification-leave-active {
transition: all 0.3s ease;
}
.notification-enter-from {
transform: translateX(100%);
opacity: 0;
}
.notification-leave-to {
transform: translateX(100%);
opacity: 0;
}
.screenshot-settings {
position: relative;
display: inline-block;
margin-left: 0.5rem;
}
.settings-btn {
padding: 0.75rem;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.75rem;
color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.settings-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.settings-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 0.5rem;
padding: 1rem;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.75rem;
color: white;
min-width: 200px;
z-index: 100;
}
.settings-dropdown label {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.settings-dropdown select,
.settings-dropdown input[type="range"] {
margin-left: auto;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.25rem;
color: white;
padding: 0.25rem;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
.animate-bounce {
animation: bounce 0.5s ease infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-25%);
}
}
</style>
+3
View File
@@ -0,0 +1,3 @@
<template>
<svg width="800px" height="800px" viewBox="-0.5 0 257 257" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M0 36.357L104.62 22.11l.045 100.914-104.57.595L0 36.358zm104.57 98.293l.08 101.002L.081 221.275l-.006-87.302 104.494.677zm12.682-114.405L255.968 0v121.74l-138.716 1.1V20.246zM256 135.6l-.033 121.191-138.716-19.578-.194-101.84L256 135.6z" fill="#00ADEF"/></svg>
</template>
+6 -1
View File
@@ -1,5 +1,10 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './assets/styles.css'
createApp(App).mount('#app')
const app = createApp(App)
app.use(router)
app.mount('#app')
+34
View File
@@ -0,0 +1,34 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import DocViewer from '../components/DocViewer.vue'
const router = createRouter({
history: createWebHistory(),
scrollBehavior(to, from, savedPosition) {
// Если в ссылке есть хэш (например, #pricing)
if (to.hash) {
return new Promise((resolve) => {
// Даем 200 миллисекунд компонентам (Three.js/Swiper) на полную отрисовку
setTimeout(() => {
resolve({
el: to.hash,
behavior: 'smooth',
// Корректировка отступа сверху, чтобы липкая (sticky) шапка не перекрывала заголовок блока
offset: { x: 0, y: 80 }
})
}, 200)
})
}
// Если хэша нет, при переходе на новую страницу просто скроллим в самый верх
return { top: 0 }
},
routes: [
{ path: '/', name: 'home', component: HomeView },
{ path: '/docs/:docName', name: 'documents', component: DocViewer, props: true }
]
})
export default router
+21
View File
@@ -0,0 +1,21 @@
<template>
<div>
<HeroSection />
<FeaturesSection />
<CompatibilitySection />
<PricingSection />
<StepsSection />
<TestimonialsSection />
<FaqSection />
</div>
</template>
<script setup>
import HeroSection from '../components/HeroSection.vue'
import FeaturesSection from '../components/FeaturesSection.vue'
import CompatibilitySection from '../components/CompatibilitySection.vue'
import PricingSection from '../components/PricingSection.vue'
import StepsSection from '../components/StepsSection.vue'
import TestimonialsSection from '../components/TestimonialsSection.vue'
import FaqSection from '../components/FaqSection.vue'
</script>
-2
View File
@@ -2,7 +2,6 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from '@tailwindcss/vite'
@@ -10,7 +9,6 @@ import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
tailwindcss(),
],
resolve: {