@ -2,31 +2,30 @@
|
||||
<div class="flex w-full flex-col items-center justify-center md:justify-start">
|
||||
<!-- 主下载按钮 -->
|
||||
<div
|
||||
class="mx-auto h-[60px] w-[210px] rounded-full bg-[#ADFF5B] md:-ml-2 md:h-[100px] md:w-[360px]"
|
||||
class="relative mx-auto flex h-[60px] w-[210px] items-center justify-center overflow-hidden rounded-full bg-[#ADFF5B] px-[16px] md:mb-4 md:ml-0 md:h-[83px] md:w-[300px] md:px-[24px]"
|
||||
>
|
||||
<router-link
|
||||
v-if="mainButton?.link && !mainButton.link.startsWith('http')"
|
||||
:to="mainButton.link"
|
||||
class="block transition-transform hover:brightness-110 active:scale-95"
|
||||
>
|
||||
<component :is="mainButton.mainIcon" class="h-full text-black" />
|
||||
</router-link>
|
||||
<a
|
||||
v-else-if="mainButton?.link"
|
||||
v-if="mainButton?.link"
|
||||
:href="mainButton.link"
|
||||
target="_blank"
|
||||
:aria-label="mainButton.label"
|
||||
class="block transition-transform hover:brightness-110 active:scale-95"
|
||||
class="flex h-full w-full items-center justify-center transition-transform hover:brightness-110 active:scale-95"
|
||||
>
|
||||
<component :is="mainButton.mainIcon" class="h-full text-black" />
|
||||
<component
|
||||
:is="mainButton.mainIcon"
|
||||
class="h-auto w-full text-black transition-transform"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
v-else
|
||||
:id="mainButton?.id"
|
||||
:aria-label="mainButton?.label"
|
||||
class="cursor-pointer transition-transform hover:brightness-110 active:scale-95"
|
||||
class="flex h-full w-full cursor-pointer items-center justify-center transition-transform hover:brightness-110 active:scale-95"
|
||||
>
|
||||
<component :is="mainButton?.mainIcon" class="h-full text-black" />
|
||||
<component
|
||||
:is="mainButton?.mainIcon"
|
||||
class="h-auto w-full text-black transition-transform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 140 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="17 0 99 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="17" y="11" width="8.57529" height="8.60876" fill="currentColor"/>
|
||||
<rect x="26.3549" y="11" width="8.57529" height="8.60876" fill="currentColor"/>
|
||||
<rect x="17" y="20.3914" width="8.57529" height="8.60876" fill="currentColor"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 140 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="15 0 96 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1511_3352)">
|
||||
<path d="M28.2965 31C29.0321 31 29.6736 30.3585 29.6736 29.623V26.4069H30.5931C31.1448 26.4069 31.5126 26.0391 31.5126 25.4874V16.302H20.4875V25.4875C20.4875 26.0392 20.8553 26.407 21.407 26.407H22.3265V29.623C22.3265 30.3586 22.9679 31.0001 23.7036 31.0001C24.4391 31.0001 25.0805 30.3586 25.0805 29.623V26.407H26.9195V29.623C26.9195 30.3585 27.5609 31 28.2965 31Z" fill="currentColor"/>
|
||||
<path d="M33.8092 25.4875C34.5448 25.4875 35.1861 24.8461 35.1861 24.1105V17.6785C35.1861 16.9468 34.5448 16.302 33.8092 16.302C33.0735 16.302 32.4321 16.9468 32.4321 17.6785V24.1105C32.4321 24.846 33.0735 25.4875 33.8092 25.4875Z" fill="currentColor"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
@ -1,124 +1,138 @@
|
||||
<template>
|
||||
<div class="review-carousel-container relative">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div
|
||||
v-if="currentReview"
|
||||
:key="currentIndex"
|
||||
class="review-card flex items-center lucid-glass-bar text-white rounded-2xl overflow-hidden p-4 md:p-5 relative"
|
||||
:style="{ height: isMobile ? '114px' : '130px' }"
|
||||
<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"
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div class="flex-shrink-0 mr-4 md:mr-6">
|
||||
<img
|
||||
:src="currentReview.avatar"
|
||||
alt="User Avatar"
|
||||
class="w-[70px] h-[70px] md:w-[84px] md:h-[84px] rounded-full object-cover border-2 border-pink-300/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-grow min-w-0">
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<h3 class="text-base md:text-xl font-bold truncate pr-2">{{ currentReview.username }}</h3>
|
||||
<router-link
|
||||
to="/reviews"
|
||||
class="more-reviews text-[10px] md:text-sm text-white hover:text-[#ADFF5B] transition-colors underline decoration-white underline-offset-4"
|
||||
>
|
||||
More Reviews
|
||||
</router-link>
|
||||
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>
|
||||
|
||||
<!-- Stars and Rating -->
|
||||
<div class="flex items-center gap-1 mb-1 md:mb-2">
|
||||
<div class="flex gap-0.5">
|
||||
<svg v-for="i in 5" :key="i" class="w-3 h-3 md:w-4 md:h-4 text-[#ADFF5B]" 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>
|
||||
<!-- 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>
|
||||
<span class="text-xs md:text-sm text-white/50 font-medium ml-1">4.9</span>
|
||||
<span class="text-xs md:text-sm text-white/30 ml-2">{{ currentReview.reviewCount }}k reviews</span>
|
||||
</div>
|
||||
|
||||
<!-- Comment Text -->
|
||||
<p class="text-xs md:text-sm text-white/90 leading-[1.4] line-clamp-2 md:pr-4">
|
||||
“{{ currentReview.comment }}”
|
||||
</p>
|
||||
<!-- 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>
|
||||
</div>
|
||||
</transition>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
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';
|
||||
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 isMobile = ref(false)
|
||||
|
||||
const reviews = [
|
||||
{
|
||||
username: '庄子不给',
|
||||
avatar: avatar1,
|
||||
comment: '真心不错,比我只前买的那个好用多了。网宿很给力。',
|
||||
reviewCount: '5.7'
|
||||
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'
|
||||
reviewCount: '12.4',
|
||||
},
|
||||
{
|
||||
username: '阿杰',
|
||||
avatar: avatar2,
|
||||
comment: '非常稳定的连接,在高峰时段也没掉过线。UI设计很现代,用着很舒服。',
|
||||
reviewCount: '3.2'
|
||||
reviewCount: '3.2',
|
||||
},
|
||||
{
|
||||
username: 'Lina_Zhang',
|
||||
avatar: avatar3,
|
||||
comment: '客服响应速度很快,配置简单。推荐给需要长期稳定翻墙的朋友们。',
|
||||
reviewCount: '8.9'
|
||||
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'
|
||||
}
|
||||
];
|
||||
reviewCount: '6.1',
|
||||
},
|
||||
]
|
||||
|
||||
const currentIndex = ref(0);
|
||||
const currentReview = computed(() => reviews[currentIndex.value]);
|
||||
const currentIndex = ref(0)
|
||||
const currentReview = computed(() => reviews[currentIndex.value])
|
||||
|
||||
let timer: any = null;
|
||||
let timer: any = null
|
||||
|
||||
const startCarousel = () => {
|
||||
timer = setInterval(() => {
|
||||
currentIndex.value = (currentIndex.value + 1) % reviews.length;
|
||||
}, 5000);
|
||||
};
|
||||
currentIndex.value = (currentIndex.value + 1) % reviews.length
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
isMobile.value = window.innerWidth < 768;
|
||||
};
|
||||
isMobile.value = window.innerWidth < 768
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
startCarousel();
|
||||
});
|
||||
handleResize()
|
||||
window.addEventListener('resize', handleResize)
|
||||
startCarousel()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -134,7 +148,9 @@ onUnmounted(() => {
|
||||
|
||||
.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);
|
||||
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 {
|
||||
@ -163,8 +179,12 @@ onUnmounted(() => {
|
||||
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:
|
||||
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;
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
|
||||
<!-- No more data -->
|
||||
<div v-if="!hasMore && !isLoading" class="py-10 text-center text-sm text-white/30">
|
||||
暂无更多
|
||||
已经到底啦 ~
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@ -234,7 +234,7 @@ const loadMore = () => {
|
||||
hasMore.value = false
|
||||
}
|
||||
isLoading.value = false
|
||||
}, 300)
|
||||
}, 800)
|
||||
}
|
||||
|
||||
useInfiniteScroll(
|
||||
|
||||