All checks were successful
site-dist-deploy / build-and-deploy (push) Successful in 1m51s
201 lines
5.7 KiB
Vue
201 lines
5.7 KiB
Vue
<template>
|
||
<div class="review-carousel-container relative">
|
||
<div
|
||
class="review-card lucid-glass-bar relative flex items-center overflow-hidden rounded-2xl p-4 text-white md:p-5"
|
||
:style="{ height: isMobile ? '114px' : '130px' }"
|
||
>
|
||
<!-- Static More Reviews Button, 不受动画影响,随时可点 -->
|
||
<router-link
|
||
to="/reviews"
|
||
class="absolute top-[18px] right-4 z-20 cursor-pointer text-[10px] text-white underline decoration-white underline-offset-4 transition-colors hover:text-[#ADFF5B] md:top-[24px] md:right-5 md:text-sm"
|
||
>
|
||
More Reviews
|
||
</router-link>
|
||
<!-- 动画应用在卡片内部的内容上 -->
|
||
<transition name="fade" mode="out-in">
|
||
<div v-if="currentReview" :key="currentIndex" class="flex h-full w-full items-center">
|
||
<!-- Avatar -->
|
||
<div class="mr-4 flex-shrink-0 md:mr-6">
|
||
<img
|
||
:src="currentReview.avatar"
|
||
alt="User Avatar"
|
||
class="h-[70px] w-[70px] rounded-full border-2 border-pink-300/30 object-cover md:h-[84px] md:w-[84px]"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Content -->
|
||
<div class="min-w-0 flex-grow">
|
||
<div class="mb-1 flex items-start justify-between">
|
||
<h3 class="truncate pr-20 text-base font-bold md:pr-28 md:text-xl">
|
||
{{ currentReview.username }}
|
||
</h3>
|
||
</div>
|
||
|
||
<!-- Stars and Rating -->
|
||
<div class="mb-1 flex items-center gap-1 md:mb-2">
|
||
<div class="flex gap-0.5">
|
||
<svg
|
||
v-for="i in 5"
|
||
:key="i"
|
||
class="h-3 w-3 text-[#ADFF5B] md:h-4 md:w-4"
|
||
fill="currentColor"
|
||
viewBox="0 0 20 20"
|
||
>
|
||
<path
|
||
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<span class="ml-1 text-xs font-medium text-white/50 md:text-sm">4.9</span>
|
||
<span class="ml-2 text-xs text-white/30 md:text-sm"
|
||
>{{ currentReview.reviewCount }}k reviews</span
|
||
>
|
||
</div>
|
||
|
||
<!-- Comment Text -->
|
||
<p class="line-clamp-2 text-xs leading-[1.4] text-white/90 md:pr-4 md:text-sm">
|
||
“{{ currentReview.comment }}”
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||
|
||
// Import avatars
|
||
import avatar1 from './avatars/avatar1.png'
|
||
import avatar2 from './avatars/avatar2.png'
|
||
import avatar3 from './avatars/avatar3.png'
|
||
import avatar4 from './avatars/avatar4.png'
|
||
import avatar6 from './avatars/avatar6.png'
|
||
|
||
const isMobile = ref(false)
|
||
|
||
const reviews = [
|
||
{
|
||
username: '庄子不给',
|
||
avatar: avatar1,
|
||
comment: '真心不错,比我只前买的那个好用多了。网宿很给力。',
|
||
reviewCount: '5.7',
|
||
},
|
||
{
|
||
username: 'TechEnthusiast',
|
||
avatar: avatar6,
|
||
comment: 'Speed is incredible! The best VPN I have used in years for international streaming.',
|
||
reviewCount: '12.4',
|
||
},
|
||
{
|
||
username: '阿杰',
|
||
avatar: avatar2,
|
||
comment: '非常稳定的连接,在高峰时段也没掉过线。UI设计很现代,用着很舒服。',
|
||
reviewCount: '3.2',
|
||
},
|
||
{
|
||
username: 'Lina_Zhang',
|
||
avatar: avatar3,
|
||
comment: '客服响应速度很快,配置简单。推荐给需要长期稳定翻墙的朋友们。',
|
||
reviewCount: '8.9',
|
||
},
|
||
{
|
||
username: 'GlobalNomad',
|
||
avatar: avatar4,
|
||
comment: 'Perfect for my travels. Low latency and high security. A must-have for privacy.',
|
||
reviewCount: '6.1',
|
||
},
|
||
]
|
||
|
||
const currentIndex = ref(0)
|
||
const currentReview = computed(() => reviews[currentIndex.value])
|
||
|
||
let timer: any = null
|
||
|
||
const startCarousel = () => {
|
||
timer = setInterval(() => {
|
||
currentIndex.value = (currentIndex.value + 1) % reviews.length
|
||
}, 5000)
|
||
}
|
||
|
||
const handleResize = () => {
|
||
isMobile.value = window.innerWidth < 768
|
||
}
|
||
|
||
onMounted(() => {
|
||
handleResize()
|
||
window.addEventListener('resize', handleResize)
|
||
startCarousel()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('resize', handleResize)
|
||
if (timer) clearInterval(timer)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.review-carousel-container {
|
||
width: 100%;
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
.review-carousel-container {
|
||
max-width: 400px;
|
||
}
|
||
}
|
||
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition:
|
||
opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
||
transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.fade-enter-from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(-20px);
|
||
}
|
||
|
||
.review-card {
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||
transition: all 0.3s ease;
|
||
border-radius: 16px !important;
|
||
}
|
||
|
||
.review-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
border-radius: inherit;
|
||
padding: 1px;
|
||
background: linear-gradient(135deg, rgba(173, 255, 91, 0.1), transparent 50%);
|
||
-webkit-mask:
|
||
linear-gradient(#fff 0 0) content-box,
|
||
linear-gradient(#fff 0 0);
|
||
mask:
|
||
linear-gradient(#fff 0 0) content-box,
|
||
linear-gradient(#fff 0 0);
|
||
-webkit-mask-composite: xor;
|
||
mask-composite: exclude;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.line-clamp-2 {
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
overflow: hidden;
|
||
}
|
||
</style>
|