适配首页移动端布局
site-dist-deploy / build-and-deploy (push) Successful in 59s

- 提取首页和用户中心共享 Header,统一移动端与桌面端尺寸及登录状态展示
- 调整首页首屏视频、标语和产品图的响应式排列,并适配视频播放弹窗
- 新增移动端合伙人权益图片,优化产品卡片和轮播区域的自适应尺寸
- 修复轮播左右箭头缩放显示,并保持桌面端与线上页面一致
- 统一移动端公共容器左右 24px 留白
This commit is contained in:
2026-09-02 15:19:43 +08:00
parent 70f4f97d53
commit c08b453e91
6 changed files with 341 additions and 168 deletions
+108
View File
@@ -0,0 +1,108 @@
<template>
<div class="h-[60px] md:h-[125px]">
<div class="fixed top-[20px] z-50 w-full md:top-[45px]">
<div class="container">
<header
class="lucid-glass-bar flex h-[40px] items-center justify-between rounded-[90px] p-[4px] transition-all duration-300 md:h-[60px]"
>
<router-link
to="/"
class="ml-[clamp(0.25rem,2vw,0.625rem)] flex min-w-0 items-center gap-[clamp(0.25rem,1vw,0.5rem)] whitespace-nowrap md:ml-[47px]"
>
<Logo
alt="Hi快VPN"
class="h-[clamp(0.875rem,4vw,1.125rem)] w-auto shrink-0 md:h-[29px]"
/>
<span
class="text-[clamp(0.875rem,4.25vw,1.5rem)] leading-none font-black whitespace-nowrap md:ml-3 md:text-2xl"
>
高能合伙人
</span>
</router-link>
<router-link
v-if="isLoggedIn"
to="/user-center"
aria-label="进入用户中心"
class="flex size-[30px] shrink-0 items-center justify-center rounded-full bg-[#78788029] text-xl font-bold text-white shadow-lg transition hover:scale-105 md:size-[40px] md:text-3xl"
>
{{ userLetter }}
</router-link>
<button
v-else
type="button"
class="flex h-[30px] shrink-0 cursor-pointer items-center justify-center rounded-full bg-[#78788029] px-[clamp(0.625rem,3vw,1.5rem)] text-[clamp(0.75rem,3.2vw,0.875rem)] leading-none font-bold whitespace-nowrap backdrop-blur-md transition hover:brightness-110 md:h-[50px] md:w-[220px] md:px-6 md:text-xl"
@click="openLoginModal"
>
登录/注册
</button>
</header>
</div>
</div>
<LoginFormModal ref="loginModalRef" />
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useLocalStorage } from '@vueuse/core'
import { useRoute, useRouter } from 'vue-router'
import LoginFormModal from '@/pages/Home/components/LoginFormModal.vue'
import Logo from '@/pages/Home/logo.svg?component'
import request from '@/utils/request'
const route = useRoute()
const router = useRouter()
const token = useLocalStorage('Authorization', '')
const userEmail = useLocalStorage('UserEmail', '')
const loginModalRef = ref<InstanceType<typeof LoginFormModal> | null>(null)
interface UserInfoResponse {
auth_methods?: Array<{
auth_type?: string
auth_identifier?: string
}>
}
const isLoggedIn = computed(() => !!token.value)
const userLetter = computed(() => userEmail.value?.charAt(0).toUpperCase() || '?')
const openLoginModal = () => {
loginModalRef.value?.show()
}
const handleLoginQuery = () => {
if (route.query.login !== 'true') return
openLoginModal()
router.replace({ query: { ...route.query, login: undefined } })
}
const fetchUserInfo = async () => {
if (route.path !== '/' || !token.value) return
try {
const res = (await request.get('/api/v1/public/user/info')) as UserInfoResponse
const emailInfo = res.auth_methods?.find((item) => item.auth_type === 'email')
if (emailInfo?.auth_identifier) userEmail.value = emailInfo.auth_identifier
} catch (error: unknown) {
console.error('Failed to fetch user info:', error)
const status =
typeof error === 'object' && error !== null
? (error as { code?: number; status?: number })
: {}
if (status.code === 401 || status.status === 401) {
token.value = ''
userEmail.value = ''
}
}
}
onMounted(() => {
fetchUserInfo()
handleLoginQuery()
})
watch(() => route.query.login, handleLoginQuery)
</script>
+227 -118
View File
@@ -2,55 +2,37 @@
<div
class="relative min-h-screen overflow-hidden bg-black bg-cover bg-center bg-no-repeat pb-[calc(4rem+env(safe-area-inset-bottom))] font-sans text-white md:flex md:flex-col"
>
<!-- 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="lucid-glass-bar flex h-[40px] items-center justify-between rounded-[90px] pr-[5px] pl-5 transition-all duration-300 md:h-[60px] md:pr-[10px]"
>
<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]" />
<span class="ml-3 text-2xl font-black">高能合伙人</span>
</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-[#78788029] 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-[40px] md:w-[220px] md:text-xl"
>
代理后台登录/注册
</button>
</header>
</div>
</div>
</div>
<AppHeader />
<!-- Main Content Container -->
<div class="container mx-auto flex max-w-[1220px] flex-col">
<main class="pt-10">
<!-- module0 -->
<div class="mb-[80px] flex justify-between">
<div class="flex flex-col items-center">
<div class="mb-[20px] ml-[42px] md:ml-[17px]">
<div class="home-hero mb-[80px] flex flex-col justify-between md:flex-row">
<div
class="flex w-full items-center justify-between gap-4 md:w-auto md:flex-col md:justify-start md:gap-0"
>
<div class="min-w-0 md:mb-[20px] md:ml-[17px]">
<h2 class="mb-2 text-2xl font-black md:text-8xl">
<Logo class="h-[34px] md:h-[43px]" />
</h2>
<p class="font-600 text-3xl">网在我在, 网快我快</p>
<p
class="font-600 text-[clamp(1.25rem,7vw,1.875rem)] leading-tight md:text-3xl md:leading-9"
>
网在我在, 网快我快
</p>
</div>
<img src="./Group%20133.png" alt="" class="mt-[52px] h-[287px] w-[182px]" />
<img
src="./Group%20133.png"
alt="Hi快VPN 手机应用"
class="home-hero__product w-auto shrink-0"
/>
</div>
<!-- video -->
<div
class="max-w-[800px] flex-1 cursor-pointer overflow-hidden rounded-[40px] transition duration-300 hover:scale-[1.02]"
<button
type="button"
aria-label="播放介绍视频"
class="block aspect-video w-full max-w-[800px] flex-1 cursor-pointer overflow-hidden rounded-[clamp(1.25rem,5vw,2.5rem)] p-0 text-left transition duration-300 hover:scale-[1.02]"
@click="openVideoModal"
>
<img
@@ -58,14 +40,29 @@
class="h-full w-full object-cover object-center"
alt="Video Cover"
/>
</div>
</button>
</div>
<!-- modules1 -->
<div class="mb-[80px] w-full">
<img src="./modules1/Group%20235.png" alt="邀请规则" class="mx-auto w-[1024px]" />
<img
src="./modules1/module2-mobile.png"
alt="高能合伙人权益"
class="mx-auto h-auto w-full md:hidden"
loading="lazy"
decoding="async"
/>
<img
src="./modules1/Group%20235.png"
alt="高能合伙人权益"
class="mx-auto hidden h-auto w-full max-w-[1024px] md:block"
loading="lazy"
decoding="async"
/>
</div>
<!-- modules2 -->
<div class="mb-[80px] w-full rounded-[40px] bg-[#ADFF5B] p-8 pb-2 text-black">
<div
class="mb-[80px] w-full rounded-[40px] bg-[#ADFF5B] px-[clamp(1rem,5vw,2rem)] pt-8 pb-2 text-black md:p-8 md:pb-2"
>
<div class="text-center text-4xl font-black">超强产品力全面秒杀同级app</div>
<div class="mt-2 text-center">为高能合伙人提供强力的的产品背书每个点拿出去都能打</div>
<div class="el-style-scrollbar mt-8 flex gap-2 overflow-x-auto pb-4">
@@ -78,31 +75,44 @@
/>
</div>
</div>
<!-- modules3 -->
<div class="mb-[40px] w-full rounded-[40px] pb-2 text-black">
<div class="text-center text-4xl font-black">
<Group215Icon class="mx-auto" />
<!-- modules3 + modules4 -->
<section class="home-showcase text-black" aria-labelledby="home-showcase-description">
<div class="home-showcase__heading text-center">
<Group215Icon class="home-showcase__title mx-auto h-auto w-full" aria-hidden="true" />
</div>
<div class="text-center text-[#999999]">给您的客户提供专业视角的建议</div>
<div class="mt-[20px] flex justify-center">
<Carousel class="relative w-full max-w-[700px]" :opts="{ loop: true }">
<div class="size-[700px] overflow-hidden rounded-[40px]">
<p id="home-showcase-description" class="home-showcase__description text-center">
给您的客户提供专业视角的建议
</p>
<div class="home-showcase__content flex justify-center">
<Carousel
class="home-showcase__carousel relative w-full"
:opts="{ loop: true }"
aria-label="产品优势展示"
>
<div class="home-showcase__viewport aspect-square w-full overflow-hidden">
<CarouselContent>
<CarouselItem v-for="(img, index) in modules4Images" :key="index">
<img
:src="img"
alt=""
class="h-[700px] w-[700px] object-cover select-none"
:alt="`产品优势展示 ${index + 1}`"
class="aspect-square h-auto w-full object-cover select-none"
draggable="false"
loading="lazy"
/>
</CarouselItem>
</CarouselContent>
</div>
<CarouselPrevious class="-left-[100px]" />
<CarouselNext class="-right-[100px]" />
<CarouselPrevious
class="home-showcase__nav home-showcase__nav--previous"
aria-label="上一张"
/>
<CarouselNext
class="home-showcase__nav home-showcase__nav--next"
aria-label="下一张"
/>
</Carousel>
</div>
</div>
</section>
</main>
</div>
<div class="container flex flex-col items-center justify-center">
@@ -115,19 +125,19 @@
<router-link to="/privacy" class="ml-2 underline">Privacy Policy</router-link>
</div>
</div>
<LoginFormModal ref="loginModalRef" />
<!-- Video Modal -->
<Dialog
v-model:open="videoModalVisible"
@update:open="(val) => !val && handleVideoModalClose()"
>
<DialogContent class="max-w-[60vw]! overflow-hidden rounded-2xl border-none bg-black/90 p-0">
<div class="relative w-full pt-[56.25%]">
<DialogContent
class="home-video-dialog overflow-hidden border-none bg-black/90 p-0 shadow-2xl"
>
<div class="aspect-video w-full overflow-hidden rounded-[inherit] bg-black">
<video
ref="videoRef"
src="/fast-video.MP4"
class="absolute inset-0 h-full w-full"
class="block h-full w-full object-contain"
controls
autoplay
></video>
@@ -138,10 +148,8 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useLocalStorage } from '@vueuse/core'
import LoginFormModal from './components/LoginFormModal.vue'
import { ref } from 'vue'
import AppHeader from '@/components/layout/AppHeader.vue'
import Logo from './logo.svg?component'
import Group215Icon from './modules3/Group 215.svg?component'
import {
@@ -152,7 +160,6 @@ import {
CarouselPrevious,
} from '@/components/ui/carousel'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import request from '@/utils/request'
import Frame98 from './modules2/Frame 98.png'
import Frame99 from './modules2/Frame 99.png'
import Frame100 from './modules2/Frame 100.png'
@@ -183,59 +190,161 @@ const handleVideoModalClose = () => {
videoRef.value.currentTime = 0
}
}
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></style>
<style scoped>
.home-hero {
gap: clamp(1.5rem, 6vw, 2.5rem);
}
.home-hero__product {
block-size: clamp(4rem, 18.133vw, 4.25rem);
}
.home-video-dialog {
inline-size: min(
calc(100vw - 2rem - env(safe-area-inset-left) - env(safe-area-inset-right)),
calc((100dvh - 4rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)) * 16 / 9)
) !important;
max-inline-size: none !important;
border-radius: clamp(0.75rem, 4vw, 1rem);
}
:deep(.home-video-dialog [data-slot='dialog-close']) {
top: clamp(0.375rem, 2vw, 0.75rem);
right: clamp(0.375rem, 2vw, 0.75rem);
z-index: 10;
display: grid;
inline-size: 2.75rem;
block-size: 2.75rem;
place-items: center;
border-radius: 999px;
color: white;
background: rgb(0 0 0 / 55%);
}
:deep(.home-video-dialog [data-slot='dialog-close'] svg) {
inline-size: 1.25rem;
block-size: 1.25rem;
}
@media (width >= 48rem) {
.home-hero {
gap: clamp(2rem, 4vw, 4rem);
}
.home-hero__product {
inline-size: 11.375rem;
block-size: 17.9375rem;
margin-block-start: 3.25rem;
}
.home-video-dialog {
inline-size: min(
60vw,
calc((100dvh - 4rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)) * 16 / 9)
) !important;
}
}
.home-showcase {
inline-size: min(100%, calc(100vw - 2.25rem));
margin-block-end: clamp(2rem, 5vw, 4rem);
padding-block-end: clamp(0.5rem, 1.5vw, 1rem);
border-radius: clamp(1.25rem, 4vw, 2.5rem);
}
.home-showcase__heading {
padding-inline: clamp(0.25rem, 2vw, 1rem);
}
.home-showcase__title {
max-inline-size: min(20.625rem, 100%);
}
.home-showcase__description {
margin-block-start: clamp(0.5rem, 1.5vw, 0.875rem);
padding-inline: clamp(0.75rem, 3vw, 1.5rem);
color: #999;
font-size: clamp(1rem, 2.5vw, 1.125rem);
line-height: 1.6;
}
.home-showcase__content {
margin-block-start: clamp(1rem, 3vw, 1.75rem);
}
.home-showcase__carousel {
max-inline-size: min(43.75rem, 100%, 85dvh);
touch-action: pan-y pinch-zoom;
}
.home-showcase__viewport {
border-radius: clamp(1.25rem, 5vw, 2.5rem);
}
.home-showcase__nav {
display: grid;
inline-size: clamp(2.75rem, 10vw, 4rem);
block-size: clamp(2.75rem, 10vw, 4rem);
place-items: center;
border: 1px solid rgb(255 255 255 / 35%);
border-radius: 999px;
background: rgb(0 0 0 / 48%);
backdrop-filter: blur(0.5rem);
transition:
background-color 180ms ease,
opacity 180ms ease;
}
.home-showcase__nav:hover,
.home-showcase__nav:focus-visible {
background: rgb(0 0 0 / 68%);
}
.home-showcase__nav:focus-visible {
outline: 2px solid white;
outline-offset: 2px;
}
.home-showcase__nav--previous {
left: clamp(0.5rem, 2.5vw, 1rem);
}
.home-showcase__nav--next {
right: clamp(0.5rem, 2.5vw, 1rem);
}
.home-showcase__nav :deep(svg) {
inline-size: auto;
block-size: clamp(1.75rem, 7vw, 2.5rem);
max-inline-size: 100%;
}
@media (width >= 64rem) {
.home-showcase__nav {
border-color: transparent;
background: transparent;
backdrop-filter: none;
}
.home-showcase__nav--previous {
left: clamp(-6.25rem, -7vw, -4rem);
}
.home-showcase__nav--next {
right: clamp(-6.25rem, -7vw, -4rem);
}
.home-showcase__nav :deep(svg) {
inline-size: auto;
block-size: clamp(4rem, 8vw, 7.5rem);
}
}
@media (prefers-reduced-motion: reduce) {
.home-showcase__nav {
transition: none;
}
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

+1 -1
View File
@@ -1,3 +1,3 @@
<svg width="48" height="120" viewBox="0 0 48 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 48 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 59.9033L37.5049 0L47.4883 4.35156L12.8008 59.9033L47.4883 115.456L37.5049 119.808L0 59.9033Z" fill="#ADFF5B"/>
</svg>

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 204 B

+2 -46
View File
@@ -1,35 +1,6 @@
<template>
<div class="flex min-h-screen flex-col bg-black text-white">
<!-- 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="lucid-glass-bar flex h-[40px] items-center justify-between rounded-[90px] pr-[5px] pl-5 transition-all duration-300 md:h-[60px] md:pr-[10px]"
>
<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]" />
<span class="ml-3 text-2xl font-black">高能合伙人</span>
</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-[#78788029] 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
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-[40px] md:w-[220px] md:text-xl"
>
代理后台登录/注册
</button>
</header>
</div>
</div>
</div>
<AppHeader />
<div class="flex flex-1 flex-col">
<!-- Main Neon Green Card -->
<!-- <div class="container md:hidden">
@@ -45,21 +16,6 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import AppHeader from '@/components/layout/AppHeader.vue'
import DesktopLayout from './DesktopLayout/index.vue'
import Logo from '@/pages/Home/logo.svg?component'
import { useLocalStorage } from '@vueuse/core'
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()
})
</script>
<style scoped></style>
<style scoped></style>
+3 -3
View File
@@ -53,9 +53,9 @@ html, body {
margin-left: auto;
margin-right: auto;
/* 1. 移动端默认:左右 18px 边距 */
padding-left: 18px;
padding-right: 18px;
/* 1. 移动端默认:左右保持相同的 24px 边距 */
padding-left: 24px;
padding-right: 24px;
/* 2. 桌面端逻辑:当屏幕达到 1440px 及以上 */
@media (width >= 1440px) {