Карточки в слайдер
Creado el: 24 de enero de 2025
Creado el: 24 de enero de 2025
есть код компонента : <template>
<v-container>
<v-row justify="center">
<v-col
cols="9"
xxl="2"
xl="2"
lg="3"
md="3"
sm="3"
v-for="(i, index) in internetTariffs"
:key="index"
>
<v-card :class="['pa-3 price-card card-animate', getTariffClass(i.name)]">
<v-card-subtitle class="text-center title mb-3">
<v-icon large :color="getIconColor(i.name)" class="mr-2">{{ i.icon }}</v-icon>
<span>{{ i.name }}</span>
</v-card-subtitle>
</v-container> </template> <script> export default { data() { return { iptvEnabled: false, selectedIptvTariff: null, internetTariffs: [ { id: 1, name: "«Економ»", speed: 50, price: 200, iptv: false, icon: "mdi-microsoft-internet-explorer", features: [], externalIpPrice: 75, externalIpEnabled: false, }, { id: 2, name: "«Стандарт»", speed: 100, price: 250, iptv: true, icon: "mdi-wifi", features: [], externalIpPrice: 75, externalIpEnabled: false, }, { id: 3, name: "«Люкс»", speed: 1000, price: 350, iptv: true, icon: "mdi-wifi-star", features: [], externalIpPrice: 0, externalIpEnabled: true, }, ], iptvTariffs: [ { id: 1, name: "S ", tv_count: 245, price: 80 }, { id: 1, name: "M ", tv_count: 333, price: 120 }, { id: 2, name: "L ", tv_count: 347, price: 180 }, ], }; }, watch: { iptvEnabled(iptvEnabled) { const currentScroll = window.scrollY; if (iptvEnabled) { window.scrollTo({ top: currentScroll + 110, behavior: "smooth", }); } else { window.scrollTo({ top: currentScroll - 110, behavior: "smooth", }); } } }, methods: { totalPrice(tariff) { let total = 0; if (tariff.externalIpEnabled) total += tariff.externalIpPrice; if (tariff.iptv && this.iptvEnabled && this.selectedIptvTariff) total += this.selectedIptvTariff.price; return total; }, getTariffClass(name) { switch (name) { case "«Стандарт»": return "standard-tariff"; case "«Люкс»": return "lux-tariff"; case "«Економ»": return "economy-tariff"; default: return ""; } }, getIconColor(name) { switch (name) { case "«Стандарт»": return "blue"; case "«Люкс»": return "#ff8f00"; case "«Економ»": return "green"; default: return "primary"; } }, getFormData(tariff) { const selectedFeatures = tariff.features.filter((f) => f).join(", "); // Проверка на наличие IPTV в тарифе, чтобы не включать его в расчёт, если он не поддерживается const iptvText = tariff.iptv && this.iptvEnabled && this.selectedIptvTariff ? ` + IPTV (${this.selectedIptvTariff.name})` : ""; // Проверка на наличие externalIp в тарифе, чтобы добавить эту услугу, если она включена const externalIpText = tariff.externalIpEnabled ? this.$t('prices.external-ip') : ""; // Формируем строку данных для передачи, добавляя информацию о тарифах, функциях, IPTV и externalIp, если они доступны return `${this.$t("prices.rates")}: ${tariff.name} ${selectedFeatures}${iptvText} ${externalIpText} = ${this.$t("prices.total")}: ₴${tariff.price + this.totalPrice(tariff)} / ${this.$t("prices.month")}`; } }, }; </script> <style scoped> .card-animate { transition: all 0.3s ease; } .card-animate-out { transform: scale(0.95); opacity: 0.7; } .card-animate:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); transform: scale(1.02); } .features-list { background-color: transparent; } .v-btn { font-size: 1rem; text-transform: none; } :deep(.v-input__control) { display: block; } :deep(.v-selection-control .v-label) { align-items: start !important; } :deep(.blue-label .v-label) { font-size: 1.08rem; font-weight: 500; font-family: Roboto-Medium; } :deep(.iptv-switch .v-input__control) { margin-bottom: 8px; } .v-card-subtitle.title { font-size: 1.25rem; font-weight: bold; color: black; } </style>text<v-card-subtitle class="text-center mb-4 price-container"> <div class="price d-flex justify-center align-center"> <span class="currency">₴</span> <span class="amount">{{ i.price }}</span> <span class="frequency">/ {{ $t("prices.month") }}</span> </div> <div class="speed text-h6 mt-2"> <v-icon color="green darken-3" size="24">mdi-web</v-icon> {{ $t("prices.up-to") }} {{ i.speed }} {{ $t("prices.Mbps") }} </div> </v-card-subtitle> <v-divider></v-divider> <v-card-text v-if="i.features.some(f => f !== '')" class="text-center features-list pa-2"> <v-list dense> <v-list-item v-for="(f, index) in i.features" :key="index"> <v-icon color="primary" class="mr-2">mdi-check-circle-outline</v-icon> <span>{{ f }}</span> </v-list-item> </v-list> </v-card-text> <v-switch class="blue-label iptv-switch" v-model="i.externalIpEnabled" hide-details inset color="teal-lighten-1" :label="`${$t('prices.external-ip')}`" ></v-switch> <template v-if="i.iptv"> <v-switch class="blue-label iptv-switch" v-model="iptvEnabled" hide-details inset color="teal-lighten-1" label="+ IPTV" > <template v-slot:append> <v-img lazy-src="@/assets/prices/logo_sweettv_light.svg" min-height="70" min-width="70" max-height="75" max-width="75" src="@/assets/prices/logo_sweettv_light.svg" contain ></v-img> </template> </v-switch> <v-radio-group v-model="selectedIptvTariff" v-if="iptvEnabled"> <v-radio v-for="tariff in iptvTariffs" :key="tariff.id" :label="tariff.name + tariff.tv_count + ' TV'" :value="tariff" color="teal-lighten-1" ></v-radio> </v-radio-group> </template> <v-alert v-if="totalPrice(i) >= 0" color="teal-lighten-1" class="total-price-alert"> {{ $t("prices.total") }}: {{ i.price + totalPrice(i) }} ₴/{{ $t("prices.month") }} </v-alert> <v-card-actions class="justify-center"> <RequestForm :FormData="getFormData(i)" :ButtonTitle="$t('prices.to-buy')" ButtonColor="teal-lighten-1" /> </v-card-actions> </v-card> </v-col> </v-row>
мне надо чтобы прайсы сохранили свою логику и дизайн но были в виде слайдера
Ниже приведён один из вариантов, как можно превратить ваши карточки в слайдер, используя встроенный в Vuetify компонент <v-carousel>
. Принцип такой: вместо ряда с колонками (<v-row>
, <v-col>
для каждой карточки) мы используем слайдер <v-carousel>
и помещаем каждую карточку в отдельный <v-carousel-item>
.
Обратите внимание:
<v-col>
и настроить стили/ширину так, чтобы карусель могла пролистывать их «поштучно» или группами.<v-carousel-item>
будет одна карточка, занимающая всю ширину слайдера.<v-carousel-item>
.vue<template> <v-container fluid> <!-- Обёртка самого слайдера --> <v-carousel show-arrows cycle height="auto" hide-delimiter-background class="my-5" > <!-- Генерация слайдов по данным из массива internetTariffs --> <v-carousel-item v-for="(tariff, index) in internetTariffs" :key="index" > <!-- Можно обернуть в row/col, если нужно центрировать карточки в рамках слайда --> <v-row justify="center"> <v-col cols="12" sm="8" md="6" lg="4" > <v-card :class="['pa-3 price-card card-animate', getTariffClass(tariff.name)]"> <v-card-subtitle class="text-center title mb-3"> <v-icon large :color="getIconColor(tariff.name)" class="mr-2" > {{ tariff.icon }} </v-icon> <span>{{ tariff.name }}</span> </v-card-subtitle> <v-card-subtitle class="text-center mb-4 price-container"> <div class="price d-flex justify-center align-center"> <span class="currency">₴</span> <span class="amount">{{ tariff.price }}</span> <span class="frequency">/ {{ $t("prices.month") }}</span> </div> <div class="speed text-h6 mt-2"> <v-icon color="green darken-3" size="24">mdi-web</v-icon> {{ $t("prices.up-to") }} {{ tariff.speed }} {{ $t("prices.Mbps") }} </div> </v-card-subtitle> <v-divider></v-divider> <v-card-text v-if="tariff.features.some(f => f !== '')" class="text-center features-list pa-2" > <v-list dense> <v-list-item v-for="(f, featureIndex) in tariff.features" :key="featureIndex" > <v-icon color="primary" class="mr-2"> mdi-check-circle-outline </v-icon> <span>{{ f }}</span> </v-list-item> </v-list> </v-card-text> <v-switch class="blue-label iptv-switch" v-model="tariff.externalIpEnabled" hide-details inset color="teal-lighten-1" :label="`${$t('prices.external-ip')}`" ></v-switch> <template v-if="tariff.iptv"> <v-switch class="blue-label iptv-switch" v-model="iptvEnabled" hide-details inset color="teal-lighten-1" label="+ IPTV" > <template v-slot:append> <v-img lazy-src="@/assets/prices/logo_sweettv_light.svg" min-height="70" min-width="70" max-height="75" max-width="75" src="@/assets/prices/logo_sweettv_light.svg" contain ></v-img> </template> </v-switch> <v-radio-group v-model="selectedIptvTariff" v-if="iptvEnabled" > <v-radio v-for="tariffItem in iptvTariffs" :key="tariffItem.id" :label="tariffItem.name + tariffItem.tv_count + ' TV'" :value="tariffItem" color="teal-lighten-1" ></v-radio> </v-radio-group> </template> <!-- Итоговая цена --> <v-alert v-if="totalPrice(tariff) >= 0" color="teal-lighten-1" class="total-price-alert" > {{ $t("prices.total") }}: {{ tariff.price + totalPrice(tariff) }} ₴/{{ $t("prices.month") }} </v-alert> <v-card-actions class="justify-center"> <RequestForm :FormData="getFormData(tariff)" :ButtonTitle="$t('prices.to-buy')" ButtonColor="teal-lighten-1" /> </v-card-actions> </v-card> </v-col> </v-row> </v-carousel-item> </v-carousel> </v-container> </template> <script> export default { data() { return { iptvEnabled: false, selectedIptvTariff: null, internetTariffs: [ { id: 1, name: "«Економ»", speed: 50, price: 200, iptv: false, icon: "mdi-microsoft-internet-explorer", features: [], externalIpPrice: 75, externalIpEnabled: false, }, { id: 2, name: "«Стандарт»", speed: 100, price: 250, iptv: true, icon: "mdi-wifi", features: [], externalIpPrice: 75, externalIpEnabled: false, }, { id: 3, name: "«Люкс»", speed: 1000, price: 350, iptv: true, icon: "mdi-wifi-star", features: [], externalIpPrice: 0, externalIpEnabled: true, }, ], iptvTariffs: [ { id: 1, name: "S ", tv_count: 245, price: 80 }, { id: 1, name: "M ", tv_count: 333, price: 120 }, { id: 2, name: "L ", tv_count: 347, price: 180 }, ], }; }, watch: { iptvEnabled(iptvEnabled) { // Логика скролла при переключении iptvEnabled const currentScroll = window.scrollY; if (iptvEnabled) { window.scrollTo({ top: currentScroll + 110, behavior: "smooth", }); } else { window.scrollTo({ top: currentScroll - 110, behavior: "smooth", }); } } }, methods: { totalPrice(tariff) { let total = 0; if (tariff.externalIpEnabled) total += tariff.externalIpPrice; if (tariff.iptv && this.iptvEnabled && this.selectedIptvTariff) { total += this.selectedIptvTariff.price; } return total; }, getTariffClass(name) { switch (name) { case "«Стандарт»": return "standard-tariff"; case "«Люкс»": return "lux-tariff"; case "«Економ»": return "economy-tariff"; default: return ""; } }, getIconColor(name) { switch (name) { case "«Стандарт»": return "blue"; case "«Люкс»": return "#ff8f00"; case "«Економ»": return "green"; default: return "primary"; } }, getFormData(tariff) { const selectedFeatures = tariff.features.filter((f) => f).join(", "); const iptvText = tariff.iptv && this.iptvEnabled && this.selectedIptvTariff ? ` + IPTV (${this.selectedIptvTariff.name})` : ""; const externalIpText = tariff.externalIpEnabled ? this.$t('prices.external-ip') : ""; return `${this.$t("prices.rates")}: ${tariff.name} ${selectedFeatures}${iptvText} ${externalIpText} = ${this.$t("prices.total")}: ₴${tariff.price + this.totalPrice(tariff)} / ${this.$t("prices.month")}`; }, }, }; </script> <style scoped> .card-animate { transition: all 0.3s ease; } .card-animate-out { transform: scale(0.95); opacity: 0.7; } .card-animate:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); transform: scale(1.02); } .features-list { background-color: transparent; } .v-btn { font-size: 1rem; text-transform: none; } /* Следующие стили, где используется :deep, нужны для переопределения внутренних элементов Vuetify (v-input, v-label и т.д.) */ :deep(.v-input__control) { display: block; } :deep(.v-selection-control .v-label) { align-items: start !important; } :deep(.blue-label .v-label) { font-size: 1.08rem; font-weight: 500; font-family: Roboto-Medium; } :deep(.iptv-switch .v-input__control) { margin-bottom: 8px; } .v-card-subtitle.title { font-size: 1.25rem; font-weight: bold; color: black; } /* Пример адаптации высоты слайдера для карточек */ .v-carousel-item { display: flex !important; align-items: center; justify-content: center; } </style>
Что произошло в коде:
<v-carousel>
вместо контейнера/строки для карточек.<v-carousel-item>
— это делает её отдельным «слайдом».<v-row justify="center">
и <v-col>
.Так вы получите слайдер, в котором каждая карточка (тариф) — это один из «слайдов» карусели. Если хотите, чтобы на одном слайде выводилось сразу несколько тарифов (например, по 3 штуки), можно настроить ширину карточек и использовать, к примеру, <v-col cols="12" md="4">
и хранить несколько карточек в одном <v-carousel-item>
.
Таким образом вы сохраните и внешний вид, и логику, но вместо статичного ряда тарифов получите возможность их пролистывать.
очень плохо с весткой стрелочки слайдера надо расположить над слайдаими сверху и можно стилизировать их убрать кружки сделай стильно и адапртивно
Ниже приведён пример, как можно превратить ваши карточки в слайдер и при этом красиво разместить стрелки навигации в верхней части (над слайдами), убрать «точки» (делимитеры) и сделать более «стильный» внешний вид.
Основные моменты:
show-arrows
.hide-delimiters
..v-carousel__controls
, .v-carousel__prev
, .v-carousel__next
и т.д.height="auto"
.vue<template> <v-container fluid> <!-- Слайдер --> <v-carousel cycle height="auto" class="styled-carousel my-5" show-arrows hide-delimiters hide-delimiter-background > <!-- Генерация слайдов по данным из массива internetTariffs --> <v-carousel-item v-for="(tariff, index) in internetTariffs" :key="index" > <v-row justify="center" no-gutters> <v-col cols="12" sm="8" md="6" lg="4" > <v-card :class="['pa-3 price-card card-animate', getTariffClass(tariff.name)]"> <v-card-subtitle class="text-center title mb-3"> <v-icon large :color="getIconColor(tariff.name)" class="mr-2" > {{ tariff.icon }} </v-icon> <span>{{ tariff.name }}</span> </v-card-subtitle> <v-card-subtitle class="text-center mb-4 price-container"> <div class="price d-flex justify-center align-center"> <span class="currency">₴</span> <span class="amount">{{ tariff.price }}</span> <span class="frequency">/ {{ $t("prices.month") }}</span> </div> <div class="speed text-h6 mt-2"> <v-icon color="green darken-3" size="24">mdi-web</v-icon> {{ $t("prices.up-to") }} {{ tariff.speed }} {{ $t("prices.Mbps") }} </div> </v-card-subtitle> <v-divider></v-divider> <v-card-text v-if="tariff.features.some(f => f !== '')" class="text-center features-list pa-2" > <v-list dense> <v-list-item v-for="(f, featureIndex) in tariff.features" :key="featureIndex" > <v-icon color="primary" class="mr-2"> mdi-check-circle-outline </v-icon> <span>{{ f }}</span> </v-list-item> </v-list> </v-card-text> <v-switch class="blue-label iptv-switch" v-model="tariff.externalIpEnabled" hide-details inset color="teal-lighten-1" :label="`${$t('prices.external-ip')}`" ></v-switch> <template v-if="tariff.iptv"> <v-switch class="blue-label iptv-switch" v-model="iptvEnabled" hide-details inset color="teal-lighten-1" label="+ IPTV" > <template v-slot:append> <v-img lazy-src="@/assets/prices/logo_sweettv_light.svg" min-height="70" min-width="70" max-height="75" max-width="75" src="@/assets/prices/logo_sweettv_light.svg" contain ></v-img> </template> </v-switch> <v-radio-group v-model="selectedIptvTariff" v-if="iptvEnabled" > <v-radio v-for="tariffItem in iptvTariffs" :key="tariffItem.id" :label="tariffItem.name + tariffItem.tv_count + ' TV'" :value="tariffItem" color="teal-lighten-1" ></v-radio> </v-radio-group> </template> <!-- Итоговая цена --> <v-alert v-if="totalPrice(tariff) >= 0" color="teal-lighten-1" class="total-price-alert" > {{ $t("prices.total") }}: {{ tariff.price + totalPrice(tariff) }} ₴/{{ $t("prices.month") }} </v-alert> <v-card-actions class="justify-center"> <RequestForm :FormData="getFormData(tariff)" :ButtonTitle="$t('prices.to-buy')" ButtonColor="teal-lighten-1" /> </v-card-actions> </v-card> </v-col> </v-row> </v-carousel-item> </v-carousel> </v-container> </template> <script> export default { data() { return { iptvEnabled: false, selectedIptvTariff: null, internetTariffs: [ { id: 1, name: "«Економ»", speed: 50, price: 200, iptv: false, icon: "mdi-microsoft-internet-explorer", features: [], externalIpPrice: 75, externalIpEnabled: false, }, { id: 2, name: "«Стандарт»", speed: 100, price: 250, iptv: true, icon: "mdi-wifi", features: [], externalIpPrice: 75, externalIpEnabled: false, }, { id: 3, name: "«Люкс»", speed: 1000, price: 350, iptv: true, icon: "mdi-wifi-star", features: [], externalIpPrice: 0, externalIpEnabled: true, }, ], iptvTariffs: [ { id: 1, name: "S ", tv_count: 245, price: 80 }, { id: 1, name: "M ", tv_count: 333, price: 120 }, { id: 2, name: "L ", tv_count: 347, price: 180 }, ], }; }, watch: { iptvEnabled(iptvEnabled) { // Логика скролла при переключении iptvEnabled const currentScroll = window.scrollY; if (iptvEnabled) { window.scrollTo({ top: currentScroll + 110, behavior: "smooth", }); } else { window.scrollTo({ top: currentScroll - 110, behavior: "smooth", }); } }, }, methods: { totalPrice(tariff) { let total = 0; if (tariff.externalIpEnabled) { total += tariff.externalIpPrice; } if (tariff.iptv && this.iptvEnabled && this.selectedIptvTariff) { total += this.selectedIptvTariff.price; } return total; }, getTariffClass(name) { switch (name) { case "«Стандарт»": return "standard-tariff"; case "«Люкс»": return "lux-tariff"; case "«Економ»": return "economy-tariff"; default: return ""; } }, getIconColor(name) { switch (name) { case "«Стандарт»": return "blue"; case "«Люкс»": return "#ff8f00"; case "«Економ»": return "green"; default: return "primary"; } }, getFormData(tariff) { const selectedFeatures = tariff.features.filter((f) => f).join(", "); const iptvText = tariff.iptv && this.iptvEnabled && this.selectedIptvTariff ? ` + IPTV (${this.selectedIptvTariff.name})` : ""; const externalIpText = tariff.externalIpEnabled ? this.$t("prices.external-ip") : ""; return `${this.$t("prices.rates")}: ${tariff.name} ${selectedFeatures}${iptvText} ${externalIpText} = ${this.$t( "prices.total" )}: ₴${tariff.price + this.totalPrice(tariff)} / ${this.$t( "prices.month" )}`; }, }, }; </script> <style scoped> /* Карточки */ .card-animate { transition: all 0.3s ease; } .card-animate:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); transform: scale(1.02); } .features-list { background-color: transparent; } .v-btn { font-size: 1rem; text-transform: none; } :deep(.v-input__control) { display: block; } :deep(.v-selection-control .v-label) { align-items: start !important; } :deep(.blue-label .v-label) { font-size: 1.08rem; font-weight: 500; font-family: Roboto-Medium; } :deep(.iptv-switch .v-input__control) { margin-bottom: 8px; } .v-card-subtitle.title { font-size: 1.25rem; font-weight: bold; color: black; } /* ==== Стилизация карусели ==== */ .styled-carousel { position: relative; /* Убираем «градиентный» фон по умолчанию на точках */ .v-carousel__controls__delimiter { display: none; } /* Контейнер для стрелок - делаем его вверху */ .v-carousel__controls { top: 0; bottom: auto; left: 0; right: 0; height: 60px; background: linear-gradient(to bottom, rgba(0, 0, 0, 0.4), transparent); display: flex; justify-content: space-between; align-items: center; } /* Сами кнопки (стрелки) */ .v-carousel__prev, .v-carousel__next { position: static; transform: none; /* Если нужно скруглённое полупрозрачное обрамление при ховере, можно так: border-radius: 50%; &:hover { background: rgba(255, 255, 255, 0.2); } */ } /* Иконки стрелок: меняем размер, цвет и т.д. */ .v-carousel__prev .v-icon, .v-carousel__next .v-icon { font-size: 32px; color: #fff; /* Светлые, чтобы выделялись на тёмном фоне */ } } /* Для адаптивности у нас всё уже "резиновое": - <v-container fluid> - <v-row no-gutters> - <v-col ...> Если нужно больше конкретики, можно добавлять медиа-запросы. */ </style>
<v-carousel>
/ <v-carousel-item>
, чтобы превратить обычный список тарифов в слайдер.show-arrows
— показывает стрелки навигации «вперёд/назад».hide-delimiters
и hide-delimiter-background
— убирают индикаторы («точки») и фоновую полоску под ними.cycle
— делает так, чтобы слайды листались «по кругу».height="auto"
— позволяет карточкам подстраивать высоту в зависимости от их контента..v-carousel__controls { top: 0; ... }
.
fluid
-контейнер, v-row
и v-col
. Для разных размеров экранов можно менять ширину колонки (к примеру, lg="4"
, md="6"
, sm="8"
, xs="12"
и т.д.).Таким образом, вы получите стильный, адаптивный, компактный слайдер с тарифами, где стрелки размещены сверху, а «точки» навигации скрыты.