speakeloudest e86cfd0881
All checks were successful
site-dist-deploy / build-and-deploy (push) Successful in 1m12s
样式修改
2026-01-05 02:56:39 -08:00

291 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
class="relative min-h-screen overflow-hidden bg-black bg-[url('@/pages/Home/bg-mobile.webp')] bg-cover bg-center bg-no-repeat pb-[calc(1rem+env(safe-area-inset-bottom))] font-sans text-white md:flex md:flex-col md:bg-[url('@/pages/Home/bg-desktop.webp')] md:pb-0"
>
<!-- Full Width Header -->
<div class="h-[60px] md:h-[125px]">
<div class="fixed top-[20px] z-50 w-full md:top-[45px]">
<div class="container">
<header
class="flex h-[40px] items-center justify-between rounded-[90px] pr-[5px] pl-5 transition-all duration-300 md:h-[60px] md:pr-[10px]"
style="
backdrop-filter: blur(36px);
box-shadow:
0px 0px 33px 0px #f2f2f280 inset,
-1px -1.5px 1.5px -3px #b3b3b3 inset,
3px 4.5px 1.5px -3px #b3b3b333 inset,
4.5px 4.5px 1.5px -5.25px #ffffff80 inset,
-4.5px -4.5px 1.5px -5.25px #ffffff80 inset;
border-image-source: linear-gradient(
135deg,
rgba(255, 255, 255, 0.4) 0%,
rgba(255, 255, 255, 0) 30%
);
border-image-slice: 1;
"
>
<router-link to="/" class="flex items-center gap-2">
<!-- Desktop Logo -->
<Logo alt="Hi快VPN" class="h-[18px] w-auto md:ml-8 md:h-[29px]" />
</router-link>
<div v-if="isLoggedIn" class="flex items-center">
<router-link
to="/user-center"
class="flex size-[30px] items-center justify-center rounded-full bg-[#2E3427] text-xl font-bold text-white shadow-lg transition hover:scale-105 md:size-[40px] md:text-3xl"
>
{{ userLetter }}
</router-link>
</div>
<button
v-else
@click="openLoginModal"
class="flex h-[30px] cursor-pointer items-center justify-center rounded-full bg-[#78788029] px-6 text-sm font-bold backdrop-blur-md transition hover:brightness-110 md:h-[60px] md:w-[220px] md:text-2xl"
>
登录 / 注册
</button>
</header>
</div>
</div>
</div>
<!-- Main Content Container -->
<div class="container mx-auto flex flex-1 flex-col">
<main class="pt-10 md:grid md:flex-1 md:grid-cols-2 md:pt-0">
<!-- Left Column: Text & Downloads -->
<div class="md:flex md:w-[432px] md:flex-col md:justify-center">
<div class="mb-[20px] ml-[42px] md:ml-[17px]">
<h2 class="mb-2 text-2xl font-black md:text-8xl">
<Logo class="h-[34px] md:h-[66px]" />
</h2>
<p class="font-600 text-3xl md:text-[48px]">网在我在, 网快我快</p>
</div>
<!-- Screenshot Image -->
<div class="relative z-10 mx-auto mb-9 w-full max-w-[182px] md:hidden">
<img
:src="ScreenshotMobile"
alt="App Screenshot"
class="h-auto w-full drop-shadow-2xl"
/>
<div class="absolute right-[8%] bottom-[97px] z-50 aspect-square w-[58%]">
<!-- Ripple Animation (Background) -->
<div class="ripple-container absolute inset-0 top-[7px]">
<div class="ripple ripple-outer-3"></div>
<div class="ripple ripple-outer-2"></div>
<div class="ripple ripple-outer-1"></div>
<div class="ripple ripple-core size-[80%]!"></div>
</div>
<!-- Center Image (Foreground/Top Layer) -->
<div
class="absolute inset-0 bottom-0 z-100 bg-[url('@/pages/Home/connected-mobile-bg.png')] bg-cover bg-center bg-no-repeat"
></div>
</div>
</div>
<!-- Download Buttons Grid -->
<div
class="mb-6 flex grid-cols-2 flex-wrap gap-[10px] px-[24px] md:mt-[124px] md:mb-[65px] md:ml-[17px] md:px-0"
>
<DownloadButton />
</div>
<!-- Features / Footer Info -->
<div
class="mb-5 w-full text-center text-[10px] leading-5 font-[300] md:ml-[17px] md:text-left md:text-sm"
>
<p>最新加密协议-安全有保障</p>
<p>IEPL专线-纯净稳定</p>
<p>不限速/不限流-网速多快Hi快多快</p>
<p>极速闪连-永远快人一步</p>
<div class="flex justify-center md:justify-start">
<a
href="https://x.com/hifasttech"
target="_blank"
class="mt-[8px] flex h-[30px] w-[100px] shrink-0 items-center justify-center space-x-2 rounded-full transition-transform hover:brightness-110 md:mt-[16px]"
style="
backdrop-filter: blur(36px);
box-shadow:
0px 0px 33px 0px #f2f2f280 inset,
-1px -1.5px 1.5px -3px #b3b3b3 inset,
3px 4.5px 1.5px -3px #b3b3b333 inset,
4.5px 4.5px 1.5px -5.25px #ffffff80 inset,
-4.5px -4.5px 1.5px -5.25px #ffffff80 inset;
border-image-source: linear-gradient(
135deg,
rgba(255, 255, 255, 0.4) 0%,
rgba(255, 255, 255, 0) 30%
);
border-image-slice: 1;
"
>
<div class="flex items-center justify-center">
<img src="./x-logo.png" class="h-[16px] w-[77px]" />
</div>
</a>
</div>
</div>
<div class="mx-auto mb-1 h-[20px] w-[352px] md:ml-[17px]">
<img src="./image-1.png" alt="image" />
</div>
<div class="text-center text-[10px] leading-[14px] font-[300] md:ml-[17px] md:text-left">
<span class="font-[600]">Hi快VPN</span> &copy; All rights reserved.<br />
<router-link to="/terms-of-service" class="underline">Terms of Service</router-link>
<router-link to="/privacy-policy" class="ml-2 underline">Privacy Policy</router-link>
</div>
</div>
<!-- Right Column: Phone Screenshot -->
<div class="relative hidden w-full max-w-[632px] md:mt-0 md:block">
<!-- Screenshot with Ripple Effect -->
<div class="absolute right-[10%] bottom-0 z-50 aspect-square w-[58%] overflow-hidden">
<!-- Ripple Animation (Background) -->
<div class="ripple-container absolute inset-0">
<div class="ripple ripple-outer-3"></div>
<div class="ripple ripple-outer-2"></div>
<div class="ripple ripple-outer-1"></div>
<div class="ripple ripple-core"></div>
</div>
<!-- Center Image (Foreground/Top Layer) -->
<div
class="absolute inset-0 bottom-[36px] z-100 bg-[url('@/pages/Home/connected-bg.png')] bg-cover bg-center bg-no-repeat"
></div>
</div>
<div class="absolute right-[2vw] bottom-0 max-w-[632px]">
<img :src="ScreenshotDesktop" alt="App Screenshot" class="relative z-10 w-full" />
</div>
</div>
</main>
</div>
<LoginFormModal ref="loginModalRef" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useLocalStorage } from '@vueuse/core'
import type { LocationQueryValue } from 'vue-router'
import LoginFormModal from './components/LoginFormModal.vue'
import DownloadButton from './components/DownloadButton.vue'
import Logo from './logo.svg?component'
import MobileLogo from './mobile-logo.svg?component'
import ScreenshotMobile from './screenshot-mobile.png'
import ScreenshotDesktop from './screenshot-desktop.webp'
import request from '@/utils/request'
const route = useRoute()
const router = useRouter()
const token = useLocalStorage('Authorization', '')
const userEmail = useLocalStorage('UserEmail', '')
const isLoggedIn = computed(() => !!token.value)
const userLetter = computed(() => {
if (!userEmail.value) return '?'
return userEmail.value.charAt(0).toUpperCase()
})
const fetchUserInfo = async () => {
if (!token.value) return
try {
const res = (await request.get('/api/v1/public/user/info')) as any
const emailInfo = res.auth_methods?.find((item: any) => item.auth_type === 'email')
if (emailInfo) {
userEmail.value = emailInfo.auth_identifier
}
} catch (error: any) {
console.error('Failed to fetch user info:', error)
if (error?.code === 401 || error?.status === 401) {
token.value = ''
userEmail.value = ''
}
}
}
onMounted(() => {
fetchUserInfo()
if (route.query.login === 'true') {
openLoginModal()
router.replace({ query: { ...route.query, login: undefined } })
}
})
watch(
() => route.query.login,
(newVal) => {
if (newVal === 'true') {
openLoginModal()
router.replace({ query: { ...route.query, login: undefined } })
}
},
)
const loginModalRef = ref<InstanceType<typeof LoginFormModal> | null>(null)
const openLoginModal = () => {
loginModalRef.value?.show()
}
</script>
<style scoped>
.ripple-container {
position: relative;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 50;
}
.ripple {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
background: #a8ff53; /* Brand neon green */
will-change: width, height, opacity;
}
/* Core: Fixed minimum diameter (~57% of max) */
.ripple-core {
width: 70%;
height: 70%;
opacity: 1;
z-index: 4;
}
/* Outer Layer 1: Opacity 0.8, 5.4s */
.ripple-outer-1 {
animation: breathe 5.4s ease-in-out infinite alternate;
opacity: 0.8;
z-index: 3;
}
/* Outer Layer 2: Opacity 0.6, 4.2s */
.ripple-outer-2 {
animation: breathe 4.2s ease-in-out infinite alternate;
opacity: 0.6;
z-index: 2;
}
/* Outer Layer 3 (Outer-most): Opacity 0.4, 3s */
.ripple-outer-3 {
animation: breathe 3s ease-in-out infinite alternate;
opacity: 0.4;
z-index: 1;
}
@keyframes breathe {
0% {
width: 57%;
height: 57%;
}
100% {
width: 100%;
height: 100%;
}
}
</style>