Карточки в слайдер
Criado em: 24 de janeiro de 2025
Criado em: 24 de janeiro de 2025
Switching layout approach
The user is keen on converting their Vuetify-based card layout into a slider, exploring options like <v-carousel> or <v-slide-group>, and considering a more advanced custom library such as Swiper.
Crafting the slider
I’m working through a Vuetify slider using <v-carousel> and <v-carousel-item>, maintaining the existing card logic and design, and ensuring elements like rows are streamlined.
Utilizing Vuetify 2
I’m piecing together a <v-carousel> example, ensuring arrow navigation and card alignment, while maintaining Vuetify 2</v-carousel> features and logic. Progress is evident in maintaining consistency with Vuetify 2.3.
Thought about slider implementation for pricing for 12 seconds
Ниже приведён один из вариантов, как можно превратить ваши карточки в слайдер, используя встроенный в Vuetify компонент <v-carousel>
. Принцип такой: вместо ряда с колонками (<v-row>
, <v-col>
для каждой карточки) мы используем слайдер <v-carousel>
и помещаем каждую карточку в отдельный <v-carousel-item>
.
Обратите внимание:
<v-col>
и настроить стили/ширину так, чтобы карусель могла пролистывать их «поштучно» или группами.<v-carousel-item>
будет одна карточка, занимающая всю ширину слайдера.<v-carousel-item>
.<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>
.
Таким образом вы сохраните и внешний вид, и логику, но вместо статичного ряда тарифов получите возможность их пролистывать.
Crafting carousel control
I’m detailing a carousel layout with arrows at top corners, removing pagination dots. Styling involves using an arrow slot or overriding default styles, ensuring a polished look.
Refining carousel navigation
Tweaking arrow positions and hiding dots. Styling involves setting top
to 20px
, pointer-events
to auto
, and tweaking background and before
content for custom icons. This boosts usability and aesthetics.
Crafting custom carousel controls
I’m setting up a carousel with top corner arrows, using SCSS for styling overrides. Adjustments include arrow positioning, top alignment, size settings, and ensuring responsiveness.
Tweaking icons
I’m setting icon font size to 30px and color to white. Considering minimal styling with semi-transparent backgrounds and ensuring icons are clickable. This leads to a more intuitive navigation.
Adjusting carousel controls
I’m working on aligning carousel controls at the top, tweaking their styling. Ensuring responsiveness and clickable icons is key. This leads me to fine-tune the visual dynamics.
Thought about slider arrow customization for 14 seconds
Ниже приведён пример, как можно превратить ваши карточки в слайдер и при этом красиво разместить стрелки навигации в верхней части (над слайдами), убрать «точки» (делимитеры) и сделать более «стильный» внешний вид.
Основные моменты:
show-arrows
.hide-delimiters
..v-carousel__controls
, .v-carousel__prev
, .v-carousel__next
и т.д.height="auto"
.<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"
и т.д.).Таким образом, вы получите стильный, адаптивный, компактный слайдер с тарифами, где стрелки размещены сверху, а «точки» навигации скрыты.