Compare commits
10
Commits
45e9338f01
...
d42aafaad7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d42aafaad7 | ||
|
|
fa8498ef4c | ||
|
|
eb17a3cd67 | ||
|
|
fee07ce458 | ||
|
|
7cded67631 | ||
|
|
3fb0d198a2 | ||
|
|
e4d73becc9 | ||
|
|
eef1ba929f | ||
|
|
055aaa20a1 | ||
|
|
3e68af7c58 |
Generated
+96
-3
@@ -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
@@ -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"
|
||||
|
||||
@@ -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), Пользователь подтверждает, что ознакомлен с настоящим Соглашением и принимает его условия в полном объёме.**
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@@ -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. Все права защищены.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">Современные протоколы защищают ваши данные в любой сети, включая публичный Wi‑Fi</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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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')
|
||||
@@ -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
|
||||
@@ -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,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: {
|
||||
|
||||
Reference in New Issue
Block a user