189 lines
6.7 KiB
Vue
189 lines
6.7 KiB
Vue
<template>
|
|
<section id="pricing" 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-14">
|
|
Прозрачные тарифы
|
|
</h2>
|
|
|
|
<!-- Состояние загрузки -->
|
|
<div v-if="loading" class="flex justify-center py-12">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
|
|
<!-- Состояние ошибки -->
|
|
<div v-else-if="error" class="text-center text-red-500 py-12">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<!-- Основной контент -->
|
|
<template v-else>
|
|
<!-- Переключатель периода -->
|
|
<div class="flex justify-center gap-2 mb-10 flex-wrap">
|
|
<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 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8"
|
|
style="grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));"
|
|
>
|
|
<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 relative"
|
|
:class="[
|
|
tariff.isPopular
|
|
? 'border-2 border-blue-600 shadow-xl 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 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>
|
|
|
|
<a
|
|
:href="'https://app.uvpn.shop'"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="w-full transition py-3 rounded-xl font-medium text-white block text-center"
|
|
: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 ? 'Выбрать тариф' : 'Выбрать' }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
</template>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { Check } from '@lucide/vue'
|
|
|
|
// Состояния
|
|
const loading = ref(true)
|
|
const error = ref(null)
|
|
const tariffsData = ref([]) // исходные тарифы (из API)
|
|
const defaultTariffKey = ref('') // ключ популярного тарифа
|
|
const selectedPeriod = ref(1) // выбранный период (месяцев)
|
|
|
|
// Доступные периоды (вычисляются динамически)
|
|
const availablePeriods = computed(() => {
|
|
const periodsSet = new Set()
|
|
for (const tariff of tariffsData.value) {
|
|
if (tariff.enabled && tariff.enabled_periods) {
|
|
tariff.enabled_periods.forEach(p => periodsSet.add(p))
|
|
}
|
|
}
|
|
const sorted = Array.from(periodsSet).sort((a, b) => a - b)
|
|
return sorted.map(value => ({
|
|
value,
|
|
label: value === 1 ? '1 мес' : `${value} мес`
|
|
}))
|
|
})
|
|
|
|
// Активные (включённые) тарифы, отфильтрованные по выбранному периоду
|
|
const activeTariffs = computed(() => {
|
|
return tariffsData.value.filter(tariff =>
|
|
tariff.enabled && tariff.enabled_periods?.includes(selectedPeriod.value)
|
|
)
|
|
})
|
|
|
|
// Карточки для отображения (с ценами, фичами и флагом популярности)
|
|
const tariffCards = computed(() => {
|
|
return activeTariffs.value.map(tariff => {
|
|
const totalPrice = tariff.prices_rub?.[String(selectedPeriod.value)]
|
|
let monthlyPrice = '—'
|
|
if (totalPrice && totalPrice > 0) {
|
|
const monthly = totalPrice / selectedPeriod.value
|
|
monthlyPrice = Number.isInteger(monthly) ? monthly : monthly.toFixed(2)
|
|
}
|
|
|
|
// Формируем список особенностей (features)
|
|
const features = []
|
|
|
|
// 1. Описание из JSON (если есть)
|
|
const description = tariff.descriptions?.ru?.trim()
|
|
if (description) {
|
|
features.push(description)
|
|
}
|
|
|
|
// 2. Лимит устройств
|
|
if (tariff.hwid_device_limit) {
|
|
features.push(`До ${tariff.hwid_device_limit} устройств`)
|
|
}
|
|
|
|
// 3. Безлимитный трафик (всегда)
|
|
features.push('Безлимитный трафик')
|
|
|
|
// 4. Поддержка 24/7
|
|
features.push('Поддержка 24/7')
|
|
|
|
return {
|
|
key: tariff.key,
|
|
name: tariff.names?.ru || tariff.key,
|
|
price: `${monthlyPrice} ₽`,
|
|
features,
|
|
isPopular: tariff.key === defaultTariffKey.value
|
|
}
|
|
})
|
|
})
|
|
|
|
// Загрузка данных с API
|
|
const fetchTariffs = async () => {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const response = await fetch('/api/v1/tariffs')
|
|
if (!response.ok) {
|
|
throw new Error(`Ошибка загрузки: ${response.status}`)
|
|
}
|
|
const data = await response.json()
|
|
tariffsData.value = data.tariffs || []
|
|
defaultTariffKey.value = data.default_tariff || ''
|
|
|
|
// Установка начального периода (первый доступный)
|
|
if (availablePeriods.value.length > 0) {
|
|
const exists = availablePeriods.value.some(p => p.value === selectedPeriod.value)
|
|
if (!exists) {
|
|
selectedPeriod.value = availablePeriods.value[0].value
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load tariffs:', err)
|
|
error.value = 'Не удалось загрузить тарифы. Попробуйте позже.'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchTariffs()
|
|
})
|
|
</script> |