AI.Demo/src/views/HomePage.vue

1511 lines
40 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.

<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { LISTENING_AUDIO_URL } from "@/config/index.js";
const router = useRouter();
// 所有功能数据
const allFeatures = ref([
{
id: 1,
title: "听力考试音频生成",
desc: "输入听力文本或上传Word一键调用 AI 合成标准英语考试音频",
class: "card-1",
icon: "audio",
url: LISTENING_AUDIO_URL,
},
{
id: 2,
title: "口语对话练习",
desc: "五大场景 AI 外教实时对话,每条回复自动语音朗读",
class: "card-2",
icon: "mic",
route: "/speaking",
},
{
id: 3,
title: "作文原图批改",
desc: "上传手写英语作文图片AI 自动标注语法、拼写及表达问题",
class: "card-3",
icon: "edit",
route: "/essay-correction",
},
{
id: 4,
title: "英语试题AI分析",
desc: "五维度全面拆解每道英语题目,流式输出详细解析",
class: "card-4",
icon: "analytics",
route: "/exam-analysis",
},
{
id: 5,
title: "单词听写",
desc: "自适应发音报词,智能追踪拼写薄弱点",
class: "card-5",
icon: "spell",
route: "/spell-practice",
},
{
id: 6,
title: "英语发音",
desc: "36 种音色自由选择,一键合成标准发音",
class: "card-6",
icon: "speaker",
route: "/pronunciation",
},
{
id: 7,
title: "AI引导解题",
desc: "对话式引导思考,卡片式展示解题步骤",
class: "card-7",
icon: "puzzle",
route: "/problem-solving",
},
{
id: 8,
title: "AI试题生成",
desc: "自定义难度、数量和知识点,快速创建题库",
class: "card-8",
icon: "document",
route: "/question-generator",
},
{
id: 9,
title: "题目变式生成",
desc: "AI 深度分析生成同类变式题,快速扩展题库",
class: "card-9",
icon: "variant",
route: "/question-variant",
},
{
id: 10,
title: "音频转文字",
desc: "支持中英日多语种,适用于会议记录、采访整理",
class: "card-10",
icon: "file-audio",
route: "/audio-to-text",
},
{
id: 11,
title: "听读评测",
desc: "AI智能评测发音准确度提供错误标注和改进建议",
class: "card-12",
icon: "mic",
route: "/speaking-evaluation",
},
]);
// 统计数据
const stats = ref([
{ value: "11+", label: "AI 学习工具" },
{ value: "100万+", label: "服务学习者" },
{ value: "36", label: "发音音色" },
{ value: "24/7", label: "AI 在线" },
]);
// 垂直翻页当前页码
const currentPage = ref(0);
const totalPages = 2;
// 横向滚动当前页码
const currentHorizontalPage = ref(0);
const totalHorizontalPages = 2; // 每页6个卡片11个功能需要2页
// 滚动到指定垂直页面
const scrollToPage = (pageIndex) => {
if (pageIndex < 0 || pageIndex >= totalPages) return;
const container = document.querySelector(".snap-container");
if (container) {
container.scrollTo({
top: pageIndex * window.innerHeight,
behavior: "smooth",
});
}
};
// 滚动到功能区域(第二页)
const scrollToFeatures = () => {
scrollToPage(1);
};
// 横向滚动到指定页
const scrollToHorizontalPage = (pageIndex) => {
if (pageIndex < 0 || pageIndex >= totalHorizontalPages) return;
const container = document.querySelector(".horizontal-scroll-container");
if (container) {
const pageWidth = container.clientWidth;
container.scrollTo({
left: pageIndex * pageWidth,
behavior: "smooth",
});
currentHorizontalPage.value = pageIndex;
}
};
// 处理功能卡片点击
const handleCardClick = (feature) => {
if (feature.url) {
window.open(feature.url, "_blank");
} else if (feature.route) {
router.push(feature.route);
}
};
// 鼠标跟随光效
const handleMouseMove = (e) => {
const cards = document.querySelectorAll(".feature-card");
for (const card of cards) {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
card.style.setProperty("--mouse-x", `${x}px`);
card.style.setProperty("--mouse-y", `${y}px`);
}
};
// 监听垂直滚动更新当前页码
const handleScroll = () => {
const container = document.querySelector(".snap-container");
if (container) {
const scrollTop = container.scrollTop;
const pageHeight = window.innerHeight;
currentPage.value = Math.round(scrollTop / pageHeight);
}
};
// 监听横向滚动更新当前页码
const handleHorizontalScroll = () => {
const container = document.querySelector(".horizontal-scroll-container");
if (container) {
const scrollLeft = container.scrollLeft;
const pageWidth = container.clientWidth;
currentHorizontalPage.value = Math.round(scrollLeft / pageWidth);
}
};
// 键盘导航
const handleKeyDown = (e) => {
if (currentPage.value === 1) {
// 在第二页时,左右键控制横向滚动
if (e.key === "ArrowRight") {
e.preventDefault();
scrollToHorizontalPage(currentHorizontalPage.value + 1);
return;
} else if (e.key === "ArrowLeft") {
e.preventDefault();
scrollToHorizontalPage(currentHorizontalPage.value - 1);
return;
}
}
if (e.key === "ArrowDown" || e.key === "PageDown") {
e.preventDefault();
scrollToPage(currentPage.value + 1);
} else if (e.key === "ArrowUp" || e.key === "PageUp") {
e.preventDefault();
scrollToPage(currentPage.value - 1);
}
};
// 垂直滚轮翻页控制
let wheelTimeout = null;
const handleWheel = (e) => {
// 如果在第二页且横向滚动容器存在,检查是否在容器内
if (currentPage.value === 1) {
const horizontalContainer = document.querySelector(".horizontal-scroll-container");
if (horizontalContainer) {
// 让横向滚动容器自己处理滚轮事件
return;
}
}
e.preventDefault();
if (wheelTimeout) return;
wheelTimeout = setTimeout(() => {
wheelTimeout = null;
}, 800);
if (e.deltaY > 0) {
scrollToPage(currentPage.value + 1);
} else if (e.deltaY < 0) {
scrollToPage(currentPage.value - 1);
}
};
// 横向滚轮处理
const handleHorizontalWheel = (e) => {
e.preventDefault();
if (e.deltaY > 0 || e.deltaX > 0) {
scrollToHorizontalPage(currentHorizontalPage.value + 1);
} else if (e.deltaY < 0 || e.deltaX < 0) {
scrollToHorizontalPage(currentHorizontalPage.value - 1);
}
};
// 触摸滑动支持
let touchStartX = 0;
let touchStartY = 0;
let touchStartTime = 0;
let isTouching = false;
const handleTouchStart = (e) => {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
touchStartTime = Date.now();
isTouching = true;
};
const handleTouchMove = (e) => {
if (!isTouching) return;
// 阻止默认行为防止页面滚动冲突
const touchX = e.touches[0].clientX;
const touchY = e.touches[0].clientY;
const deltaX = Math.abs(touchX - touchStartX);
const deltaY = Math.abs(touchY - touchStartY);
// 在功能区域第2页优先处理水平滑动
if (currentPage.value === 1 && deltaX > deltaY && deltaX > 10) {
e.preventDefault();
}
};
const handleTouchEnd = (e) => {
if (!isTouching) return;
isTouching = false;
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
const touchEndTime = Date.now();
const deltaX = touchStartX - touchEndX;
const deltaY = touchStartY - touchEndY;
const deltaTime = touchEndTime - touchStartTime;
// 快速滑动检测250ms内
const isQuickSwipe = deltaTime < 250;
const threshold = isQuickSwipe ? 25 : 60;
// 判断是水平滑动还是垂直滑动
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 水平滑动 - 控制横向滚动
if (Math.abs(deltaX) > threshold) {
if (deltaX > 0) {
scrollToHorizontalPage(currentHorizontalPage.value + 1);
} else {
scrollToHorizontalPage(currentHorizontalPage.value - 1);
}
}
} else {
// 垂直滑动 - 控制页面翻页(仅在非横向滚动区域)
if (Math.abs(deltaY) > threshold && currentPage.value !== 1) {
if (deltaY > 0) {
scrollToPage(currentPage.value + 1);
} else {
scrollToPage(currentPage.value - 1);
}
}
}
};
// 横向滚动容器触摸处理
const handleHorizontalTouchStart = (e) => {
touchStartX = e.touches[0].clientX;
touchStartTime = Date.now();
};
const handleHorizontalTouchEnd = (e) => {
const touchEndX = e.changedTouches[0].clientX;
const deltaTime = Date.now() - touchStartTime;
const deltaX = touchStartX - touchEndX;
const isQuickSwipe = deltaTime < 300;
const threshold = isQuickSwipe ? 30 : 60;
if (Math.abs(deltaX) > threshold) {
if (deltaX > 0) {
scrollToHorizontalPage(currentHorizontalPage.value + 1);
} else {
scrollToHorizontalPage(currentHorizontalPage.value - 1);
}
}
};
onMounted(() => {
const container = document.querySelector(".snap-container");
if (container) {
container.addEventListener("scroll", handleScroll);
container.addEventListener("wheel", handleWheel, { passive: false });
container.addEventListener("touchstart", handleTouchStart, { passive: true });
container.addEventListener("touchmove", handleTouchMove, { passive: false });
container.addEventListener("touchend", handleTouchEnd, { passive: true });
}
window.addEventListener("keydown", handleKeyDown);
const page2 = document.querySelector(".page-2");
if (page2) {
page2.addEventListener("mousemove", handleMouseMove);
}
const horizontalContainer = document.querySelector(".horizontal-scroll-container");
if (horizontalContainer) {
horizontalContainer.addEventListener("scroll", handleHorizontalScroll);
horizontalContainer.addEventListener("touchstart", handleHorizontalTouchStart, { passive: true });
horizontalContainer.addEventListener("touchend", handleHorizontalTouchEnd, { passive: true });
}
});
onUnmounted(() => {
const container = document.querySelector(".snap-container");
if (container) {
container.removeEventListener("scroll", handleScroll);
container.removeEventListener("wheel", handleWheel);
container.removeEventListener("touchstart", handleTouchStart);
container.removeEventListener("touchmove", handleTouchMove);
container.removeEventListener("touchend", handleTouchEnd);
}
window.removeEventListener("keydown", handleKeyDown);
const page2 = document.querySelector(".page-2");
if (page2) {
page2.removeEventListener("mousemove", handleMouseMove);
}
const horizontalContainer = document.querySelector(".horizontal-scroll-container");
if (horizontalContainer) {
horizontalContainer.removeEventListener("scroll", handleHorizontalScroll);
horizontalContainer.removeEventListener("touchstart", handleHorizontalTouchStart);
horizontalContainer.removeEventListener("touchend", handleHorizontalTouchEnd);
}
});
</script>
<template>
<div class="landing-wrapper">
<!-- 背景 -->
<div class="landing-bg">
<div class="bg-gradient"></div>
<div class="bg-grid"></div>
</div>
<!-- 导航栏 -->
<nav class="landing-nav">
<div class="nav-brand">
<div class="brand-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z" />
</svg>
</div>
<span class="brand-text">AI English</span>
</div>
</nav>
<!-- 翻页指示器 -->
<div class="page-indicator">
<div
v-for="page in totalPages"
:key="page"
class="indicator-dot"
:class="{ active: currentPage === page - 1 }"
@click="scrollToPage(page - 1)"
></div>
</div>
<!-- 翻页滚动容器 -->
<div class="snap-container">
<!-- 第一页Hero Section -->
<section class="snap-page page-1">
<div class="page-content">
<div class="hero-badge">
<span class="badge-dot"></span>
全新 AI 驱动的英语学习平台
</div>
<h1 class="hero-title">
让 AI 成为你的
<span class="gradient-text">专属英语教练</span>
</h1>
<p class="hero-subtitle">
11+ 智能学习工具,覆盖听说读写全场景。从口语对话到作文批改,<br>
从试题分析到智能评测,全方位提升你的英语能力。
</p>
<div class="hero-cta">
<button class="cta-button primary" @click="scrollToFeatures">
开始探索
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
</div>
<!-- 统计数据 -->
<div class="hero-stats">
<div v-for="stat in stats" :key="stat.label" class="stat-item">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</div>
<!-- 向下滚动提示 -->
<div class="scroll-hint" @click="scrollToFeatures">
<span>向下滚动</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</div>
</section>
<!-- 第二页:功能区域 -->
<section class="snap-page page-2">
<div class="page-content">
<div class="section-header">
<h2 class="section-title">全部功能</h2>
<p class="section-subtitle">11+ AI 驱动的英语学习工具</p>
</div>
<!-- 横向滚动容器 -->
<div class="horizontal-scroll-wrapper">
<div
class="horizontal-scroll-container"
@wheel="handleHorizontalWheel"
>
<div class="horizontal-page">
<div class="features-grid">
<div
v-for="feature in allFeatures.slice(0, 6)"
:key="feature.id"
@click="handleCardClick(feature)"
class="feature-card"
:class="feature.class"
>
<div class="card-content">
<div class="icon-wrapper">
<svg v-if="feature.icon === 'audio'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
<svg v-else-if="feature.icon === 'mic'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z" />
</svg>
<svg v-else-if="feature.icon === 'edit'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
</svg>
<svg v-else-if="feature.icon === 'analytics'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />
</svg>
<svg v-else-if="feature.icon === 'spell'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" />
</svg>
<svg v-else-if="feature.icon === 'speaker'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />
</svg>
</div>
<h3 class="card-title">{{ feature.title }}</h3>
<p class="card-desc">{{ feature.desc }}</p>
</div>
</div>
</div>
</div>
<div class="horizontal-page">
<div class="features-grid">
<div
v-for="feature in allFeatures.slice(6, 11)"
:key="feature.id"
@click="handleCardClick(feature)"
class="feature-card"
:class="feature.class"
>
<div class="card-content">
<div class="icon-wrapper">
<svg v-if="feature.icon === 'puzzle'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<svg v-else-if="feature.icon === 'document'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
<svg v-else-if="feature.icon === 'variant'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876v9.004c0 .621-.504 1.125-1.125 1.125h-3.75c-.621 0-1.125.504-1.125 1.125v3.876c0 .621.504 1.125 1.125 1.125h3.75c.621 0 1.125-.504 1.125-1.125Z" />
</svg>
<svg v-else-if="feature.icon === 'file-audio'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 12.75l2.25-1.5v4.5l-2.25-1.5v-1.5z" />
</svg>
<svg v-else-if="feature.icon === 'mic'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z" />
</svg>
</div>
<h3 class="card-title">{{ feature.title }}</h3>
<p class="card-desc">{{ feature.desc }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- 横向滚动指示器 -->
<div class="horizontal-indicator">
<div
v-for="page in totalHorizontalPages"
:key="page"
class="h-indicator-dot"
:class="{ active: currentHorizontalPage === page - 1 }"
@click="scrollToHorizontalPage(page - 1)"
></div>
</div>
<!-- 横向滚动箭头 -->
<button
v-if="currentHorizontalPage > 0"
class="scroll-arrow scroll-arrow-left"
@click="scrollToHorizontalPage(currentHorizontalPage - 1)"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</button>
<button
v-if="currentHorizontalPage < totalHorizontalPages - 1"
class="scroll-arrow scroll-arrow-right"
@click="scrollToHorizontalPage(currentHorizontalPage + 1)"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
</div>
<!-- Footer -->
<footer class="landing-footer">
<p>AI 英语学习辅助平台 · 让学习更智能</p>
</footer>
</section>
</div>
</div>
</template>
<style scoped>
/* ========== 基础布局 ========== */
.landing-wrapper {
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
color: #f8fafc;
overflow: hidden;
}
/* ========== 翻页滚动容器 ========== */
.snap-container {
height: 100vh;
width: 100vw;
overflow-y: scroll;
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
position: relative;
z-index: 1;
}
.snap-container::-webkit-scrollbar {
display: none;
}
.snap-page {
height: 100vh;
width: 100vw;
scroll-snap-align: start;
scroll-snap-stop: always;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.page-content {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
box-sizing: border-box;
flex: 1;
display: flex;
flex-direction: column;
}
/* ========== 翻页指示器 ========== */
.page-indicator {
position: fixed;
right: 2rem;
top: 50%;
transform: translateY(-50%);
z-index: 100;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.indicator-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.indicator-dot:hover {
background: rgba(255, 255, 255, 0.4);
}
.indicator-dot.active {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-color: rgba(255, 255, 255, 0.3);
transform: scale(1.2);
}
/* ========== 向下滚动提示 ========== */
.scroll-hint {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
color: #64748b;
font-size: 0.875rem;
cursor: pointer;
animation: bounce 2s infinite;
z-index: 10;
}
.scroll-hint svg {
width: 24px;
height: 24px;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateX(-50%) translateY(0);
}
40% {
transform: translateX(-50%) translateY(-10px);
}
60% {
transform: translateX(-50%) translateY(-5px);
}
}
/* ========== 背景 ========== */
.landing-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
overflow: hidden;
background: #0f172a;
}
.bg-gradient {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99, 102, 241, 0.15), transparent),
radial-gradient(ellipse 60% 40% at 80% 50%, rgba(139, 92, 246, 0.1), transparent),
radial-gradient(ellipse 50% 30% at 20% 80%, rgba(236, 72, 153, 0.08), transparent);
}
.bg-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
background-size: 60px 60px;
mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
}
/* ========== 导航栏 ========== */
.landing-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.nav-brand {
display: flex;
align-items: center;
gap: 0.75rem;
}
.brand-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.brand-icon svg {
width: 22px;
height: 22px;
}
.brand-text {
font-size: 1.25rem;
font-weight: 700;
background: linear-gradient(135deg, #fff 0%, #a5b4fc 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* ========== Hero Section ========== */
.page-1 .page-content {
justify-content: center;
align-items: center;
text-align: center;
padding-top: 5rem;
padding-bottom: 6rem;
}
/* Hero 区域移动端优化 */
@media (max-width: 640px) {
.page-1 .page-content {
padding-top: 4rem;
padding-bottom: 4rem;
justify-content: flex-start;
}
.hero-badge {
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
margin-bottom: 1.5rem;
}
.hero-title {
font-size: clamp(1.75rem, 8vw, 2.5rem);
line-height: 1.2;
margin-bottom: 1rem;
}
.hero-subtitle {
font-size: 0.875rem;
line-height: 1.6;
margin-bottom: 1.5rem;
padding: 0 0.5rem;
}
.hero-subtitle br {
display: none;
}
.hero-cta {
margin-bottom: 2rem;
}
.cta-button {
padding: 0.875rem 1.5rem;
font-size: 0.9375rem;
}
.hero-stats {
gap: 1rem;
padding: 1rem 1.25rem;
border-radius: 16px;
}
.stat-value {
font-size: 1.25rem;
}
.stat-label {
font-size: 0.75rem;
}
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(99, 102, 241, 0.1);
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 9999px;
font-size: 0.875rem;
color: #a5b4fc;
margin-bottom: 2rem;
}
.badge-dot {
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.hero-title {
font-size: clamp(2.5rem, 6vw, 4.5rem);
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.1;
margin: 0 0 1.5rem;
color: #fff;
}
.gradient-text {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-subtitle {
font-size: 1.125rem;
color: #94a3b8;
line-height: 1.8;
max-width: 600px;
margin: 0 0 2.5rem;
}
.hero-cta {
display: flex;
gap: 1rem;
margin-bottom: 4rem;
}
.cta-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 2rem;
font-size: 1rem;
font-weight: 600;
border-radius: 12px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.cta-button.primary {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
box-shadow: 0 10px 30px -10px rgba(99, 102, 241, 0.5);
}
.cta-button.primary:hover {
transform: translateY(-2px);
box-shadow: 0 15px 40px -10px rgba(99, 102, 241, 0.6);
}
.cta-button svg {
width: 20px;
height: 20px;
transition: transform 0.3s ease;
}
.cta-button:hover svg {
transform: translateY(2px);
}
/* ========== Hero Stats ========== */
.hero-stats {
display: flex;
gap: 3rem;
padding: 2rem 3rem;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 20px;
backdrop-filter: blur(10px);
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 800;
background: linear-gradient(135deg, #fff 0%, #a5b4fc 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-label {
font-size: 0.875rem;
color: #64748b;
margin-top: 0.25rem;
}
/* ========== Section Header ========== */
.section-header {
text-align: center;
margin-bottom: 3rem;
}
@media (max-width: 640px) {
.section-header {
margin-bottom: 1.5rem;
}
.section-title {
font-size: 1.5rem;
}
.section-subtitle {
font-size: 0.875rem;
}
}
.section-title {
font-size: 2rem;
font-weight: 700;
color: #fff;
margin: 0 0 0.75rem;
}
.section-subtitle {
font-size: 1rem;
color: #64748b;
margin: 0;
}
/* ========== Features Section ========== */
.page-2 .page-content {
padding-top: 5rem;
padding-bottom: 0;
justify-content: flex-start;
overflow: hidden;
}
@media (max-width: 640px) {
.page-2 .page-content {
padding-top: 4rem;
}
}
/* ========== 横向滚动区域 ========== */
.horizontal-scroll-wrapper {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
.horizontal-scroll-container {
flex: 1;
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
}
.horizontal-scroll-container::-webkit-scrollbar {
display: none;
}
.horizontal-page {
flex: 0 0 100%;
width: 100%;
height: 100%;
scroll-snap-align: start;
scroll-snap-stop: always;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 0;
box-sizing: border-box;
}
.features-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, auto);
gap: 1.5rem;
width: 100%;
max-width: 1000px;
padding: 2rem;
box-sizing: border-box;
}
/* ========== 横向滚动指示器 ========== */
.horizontal-indicator {
display: flex;
justify-content: center;
gap: 0.5rem;
padding: 1rem 0;
}
.h-indicator-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
cursor: pointer;
transition: all 0.3s ease;
}
.h-indicator-dot:hover {
background: rgba(255, 255, 255, 0.4);
}
.h-indicator-dot.active {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
transform: scale(1.2);
}
/* ========== 横向滚动箭头 ========== */
.scroll-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
z-index: 10;
}
.scroll-arrow:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.1);
}
.scroll-arrow svg {
width: 24px;
height: 24px;
}
.scroll-arrow-left {
left: 1rem;
}
.scroll-arrow-right {
right: 1rem;
}
.all-features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
/* 横向滚动中的卡片样式 */
.features-grid .feature-card {
min-height: 200px;
display: flex;
flex-direction: column;
padding: 2rem;
}
@media (max-width: 640px) {
.features-grid .feature-card {
min-height: auto;
padding: 1.25rem;
}
.features-grid .feature-card .icon-wrapper {
width: 44px;
height: 44px;
border-radius: 12px;
}
.features-grid .feature-card .icon-wrapper svg {
width: 22px;
height: 22px;
}
.features-grid .feature-card .card-title {
font-size: 0.9375rem;
margin: 0.5rem 0 0.25rem;
}
.features-grid .feature-card .card-desc {
font-size: 0.75rem;
line-height: 1.5;
}
}
/* ========== Feature Cards ========== */
.feature-card {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 20px;
padding: 1.5rem;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
cursor: pointer;
backdrop-filter: blur(10px);
}
.feature-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(
600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
rgba(255, 255, 255, 0.06),
transparent 40%
);
opacity: 0;
transition: opacity 0.4s;
pointer-events: none;
}
.feature-card:hover::before {
opacity: 1;
}
.feature-card:hover {
transform: translateY(-4px);
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.1);
}
.feature-card {
padding: 1.5rem;
}
.feature-card:hover {
transform: translateY(-4px);
}
.card-content {
position: relative;
z-index: 1;
}
.icon-wrapper {
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
transition: transform 0.3s ease;
}
.icon-wrapper.small {
width: 44px;
height: 44px;
border-radius: 12px;
}
.icon-wrapper svg {
width: 28px;
height: 28px;
}
.icon-wrapper.small svg {
width: 22px;
height: 22px;
}
.feature-card:hover .icon-wrapper {
transform: scale(1.05);
}
/* Card Colors */
.card-1 .icon-wrapper {
background: linear-gradient(135deg, #6366f1, #4f46e5);
box-shadow: 0 8px 20px -6px rgba(99, 102, 241, 0.5);
}
.card-2 .icon-wrapper {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
box-shadow: 0 8px 20px -6px rgba(139, 92, 246, 0.5);
}
.card-3 .icon-wrapper {
background: linear-gradient(135deg, #ec4899, #db2777);
box-shadow: 0 8px 20px -6px rgba(236, 72, 153, 0.5);
}
.card-4 .icon-wrapper {
background: linear-gradient(135deg, #14b8a6, #0d9488);
box-shadow: 0 8px 20px -6px rgba(20, 184, 166, 0.5);
}
.card-5 .icon-wrapper {
background: linear-gradient(135deg, #f59e0b, #d97706);
box-shadow: 0 8px 20px -6px rgba(245, 158, 11, 0.5);
}
.card-6 .icon-wrapper {
background: linear-gradient(135deg, #ef4444, #dc2626);
box-shadow: 0 8px 20px -6px rgba(239, 68, 68, 0.5);
}
.card-7 .icon-wrapper {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
box-shadow: 0 8px 20px -6px rgba(139, 92, 246, 0.5);
}
.card-8 .icon-wrapper {
background: linear-gradient(135deg, #10b981, #059669);
box-shadow: 0 8px 20px -6px rgba(16, 185, 129, 0.5);
}
.card-9 .icon-wrapper {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
box-shadow: 0 8px 20px -6px rgba(99, 102, 241, 0.5);
}
.card-10 .icon-wrapper {
background: linear-gradient(135deg, #06b6d4, #0891b2);
box-shadow: 0 8px 20px -6px rgba(6, 182, 212, 0.5);
}
.card-12 .icon-wrapper {
background: linear-gradient(135deg, #ec4899, #f472b6);
box-shadow: 0 8px 20px -6px rgba(236, 72, 153, 0.5);
}
.card-title {
font-size: 1.125rem;
font-weight: 600;
margin: 0.75rem 0 0.375rem;
color: #f8fafc;
}
.card-desc {
font-size: 0.875rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* ========== Footer ========== */
.landing-footer {
padding: 3rem 0;
text-align: center;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
@media (max-width: 640px) {
.landing-footer {
padding: 1.5rem 0;
}
.landing-footer p {
font-size: 0.75rem;
}
}
.landing-footer p {
color: #64748b;
font-size: 0.875rem;
margin: 0;
}
/* ========== Responsive ========== */
@media (max-width: 968px) {
.all-features-grid {
grid-template-columns: repeat(2, 1fr);
}
.hero-stats {
gap: 2rem;
padding: 1.5rem 2rem;
}
.page-indicator {
right: 1rem;
}
/* 平板端横向滚动优化 */
.horizontal-scroll-wrapper {
padding: 0 0.5rem;
}
.features-grid {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, auto);
gap: 1rem;
padding: 1rem;
}
}
@media (max-width: 640px) {
.page-content {
padding: 0 1rem;
}
.landing-nav {
padding: 0.75rem 1rem;
}
.nav-brand {
gap: 0.5rem;
}
.brand-icon {
width: 32px;
height: 32px;
border-radius: 8px;
}
.brand-icon svg {
width: 18px;
height: 18px;
}
.brand-text {
font-size: 1.125rem;
}
.page-1 .page-content {
padding-top: 3.5rem;
padding-bottom: 4rem;
}
.hero-stats {
flex-wrap: wrap;
gap: 1rem;
padding: 1rem 1.25rem;
}
.stat-item {
flex: 1 1 45%;
}
.page-2 .page-content {
padding-top: 3.5rem;
}
.features-grid {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, auto);
gap: 0.625rem;
padding: 0.5rem;
}
.features-grid .feature-card {
min-height: auto;
padding: 0.875rem;
}
.features-grid .feature-card .icon-wrapper {
width: 36px;
height: 36px;
border-radius: 10px;
}
.features-grid .feature-card .icon-wrapper svg {
width: 18px;
height: 18px;
}
.features-grid .feature-card .card-title {
font-size: 0.8125rem;
margin: 0.375rem 0 0.125rem;
}
.features-grid .feature-card .card-desc {
font-size: 0.6875rem;
line-height: 1.4;
}
.all-features-grid {
grid-template-columns: 1fr;
}
.scroll-arrow {
width: 36px;
height: 36px;
}
.scroll-arrow svg {
width: 18px;
height: 18px;
}
.scroll-arrow-left {
left: 0.25rem;
}
.scroll-arrow-right {
right: 0.25rem;
}
.page-indicator {
right: 0.5rem;
gap: 0.5rem;
}
.indicator-dot {
width: 6px;
height: 6px;
}
.horizontal-indicator {
padding: 0.75rem 0;
}
.h-indicator-dot {
width: 6px;
height: 6px;
}
.scroll-hint {
bottom: 1rem;
font-size: 0.75rem;
}
.scroll-hint svg {
width: 20px;
height: 20px;
}
}
</style>