style(homepage): 优化代码格式和卡片布局

This commit is contained in:
cc 2026-03-19 20:41:57 +08:00
parent 11a31c3745
commit a799afebab
1 changed files with 196 additions and 103 deletions

View File

@ -1,92 +1,92 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from "vue";
import { useRouter } from 'vue-router' import { useRouter } from "vue-router";
const router = useRouter() const router = useRouter();
const features = ref([ const features = ref([
{ {
id: 1, id: 1,
title: 'AI听力考试音频生成', title: "听力考试音频生成",
desc: '基于先进TTS技术根据文本快速合成全真模拟的英语听力考试音频。', desc: "输入听力文本,一键调用 AI 合成标准英语考试音频,支持多种题型场景,快速生成可用于备考的模拟听力材料。",
class: 'card-1', class: "card-1",
icon: 'audio', icon: "audio",
url: 'https://jz5k88k7vv.coze.site' url: "https://jz5k88k7vv.coze.site",
}, },
{ {
id: 2, id: 2,
title: 'AI口语对话', title: "口语对话",
desc: '全天候1对1外教级对练实时纠正发音与语法快速提升口语表达。', desc: "提供校园生活、兴趣爱好、英语考试、影视讨论、旅游英语五大场景,与 AI 外教实时对话,每条回复自动语音朗读,支持英语音色切换。",
class: 'card-2', class: "card-2",
icon: 'mic', icon: "mic",
route: '/speaking' route: "/speaking",
}, },
{ {
id: 3, id: 3,
title: 'AI作文原图批改', title: "作文原图批改",
desc: '一键上传手写作文图片AI智能OCR识别并提供词汇、语法深度批改。', desc: "上传手写英语作文的 JPG/PNG 图片AI 自动识别内容并在原图上标注语法、拼写及表达问题,并排对比原图与批改结果,一目了然。",
class: 'card-3', class: "card-3",
icon: 'edit', icon: "edit",
route: '/essay-correction' route: "/essay-correction",
}, },
{ {
id: 4, id: 4,
title: '英语试题AI分析', title: "英语试题AI分析",
desc: '深度解析长难句、阅读理解及语法考点,为您提供专属错题讲解。', desc: "支持文本粘贴或图片上传两种方式AI 流式输出题干理解、考点识别、解题思路、正确答案、详细解析五个维度,全面拆解每道英语题目。",
class: 'card-4', class: "card-4",
icon: 'analytics', icon: "analytics",
route: '/exam-analysis' route: "/exam-analysis",
}, },
{ {
id: 5, id: 5,
title: 'AI单词听写', title: "单词听写",
desc: '自适应发音报词,智能追踪拼写薄弱点,让词汇记忆更高效。', desc: "自适应发音报词,智能追踪拼写薄弱点,让词汇记忆更高效。",
class: 'card-5', class: "card-5",
icon: 'spell', icon: "spell",
route: null route: null,
}, },
{ {
id: 6, id: 6,
title: 'AI英语发音', title: "英语发音",
desc: '输入英语单词或长难句一键调用AI为您进行标准地道的语音朗读示范。', desc: "输入英语单词或句子,从 36 种音色(含美音、英音)中自由选择,一键合成标准发音并支持在线播放与下载保存。",
class: 'card-6', class: "card-6",
icon: 'speaker', icon: "speaker",
route: '/pronunciation' route: "/pronunciation",
} },
]) ]);
// Hover effect for glassmorphism glare // Hover effect for glassmorphism glare
const handleMouseMove = (e) => { const handleMouseMove = (e) => {
const cards = document.querySelectorAll('.homepage-feature-card') const cards = document.querySelectorAll(".homepage-feature-card");
for (const card of cards) { for (const card of cards) {
const rect = card.getBoundingClientRect() const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left const x = e.clientX - rect.left;
const y = e.clientY - rect.top const y = e.clientY - rect.top;
card.style.setProperty('--mouse-x', `${x}px`) card.style.setProperty("--mouse-x", `${x}px`);
card.style.setProperty('--mouse-y', `${y}px`) card.style.setProperty("--mouse-y", `${y}px`);
} }
} };
const handleCardClick = (feature) => { const handleCardClick = (feature) => {
if (feature.url) { if (feature.url) {
window.open(feature.url, '_blank') window.open(feature.url, "_blank");
} else if (feature.route) { } else if (feature.route) {
router.push(feature.route) router.push(feature.route);
} }
} };
onMounted(() => { onMounted(() => {
const container = document.querySelector('.homepage-container') const container = document.querySelector(".homepage-container");
if (container) { if (container) {
container.addEventListener('mousemove', handleMouseMove) container.addEventListener("mousemove", handleMouseMove);
} }
}) });
onUnmounted(() => { onUnmounted(() => {
const container = document.querySelector('.homepage-container') const container = document.querySelector(".homepage-container");
if (container) { if (container) {
container.removeEventListener('mousemove', handleMouseMove) container.removeEventListener("mousemove", handleMouseMove);
} }
}) });
</script> </script>
<template> <template>
@ -97,45 +97,128 @@ onUnmounted(() => {
</header> </header>
<main class="grid-container"> <main class="grid-container">
<div v-for="feature in features" :key="feature.id" @click="handleCardClick(feature)" class="homepage-feature-card" :class="feature.class"> <div
v-for="feature in features"
<div class="icon-wrapper"> :key="feature.id"
<!-- Audio Icon (Headphones) --> @click="handleCardClick(feature)"
<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"> class="homepage-feature-card"
<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" /> :class="feature.class"
</svg> >
<div class="card-header">
<div class="icon-wrapper">
<!-- Audio Icon (Headphones) -->
<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>
<!-- Mic Icon (Chat/Mic) --> <!-- Mic Icon (Chat/Mic) -->
<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"> <svg
<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" /> v-else-if="feature.icon === 'mic'"
</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="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>
<!-- Edit Icon (Document Text / Pen) --> <!-- Edit Icon (Document Text / Pen) -->
<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"> <svg
<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" /> v-else-if="feature.icon === 'edit'"
</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="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>
<!-- Analytics Icon (Chart/Sparkles) --> <!-- Analytics Icon (Chart/Sparkles) -->
<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"> <svg
<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" /> v-else-if="feature.icon === 'analytics'"
</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="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>
<!-- Spell Icon (Book Open) --> <!-- Spell Icon (Book Open) -->
<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"> <svg
<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" /> v-else-if="feature.icon === 'spell'"
</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="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>
<!-- Speaker Icon (Megaphone/Speaker) --> <!-- Speaker Icon (Megaphone/Speaker) -->
<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"> <svg
<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" /> v-else-if="feature.icon === 'speaker'"
</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="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>
<h2 class="card-title">{{ feature.title }}</h2>
</div> </div>
<h2 class="card-title">{{ feature.title }}</h2>
<p class="card-desc">{{ feature.desc }}</p> <p class="card-desc">{{ feature.desc }}</p>
<svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /> class="arrow-icon"
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="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/>
</svg> </svg>
</div> </div>
</main> </main>
@ -200,13 +283,17 @@ h1 {
} }
.homepage-feature-card::before { .homepage-feature-card::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: radial-gradient(800px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(255, 255, 255, 0.06), transparent 40%); background: radial-gradient(
800px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
rgba(255, 255, 255, 0.06),
transparent 40%
);
opacity: 0; opacity: 0;
transition: opacity 0.4s; transition: opacity 0.4s;
z-index: 1; z-index: 1;
@ -224,18 +311,25 @@ h1 {
box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.5); box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.5);
} }
.card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
z-index: 2;
position: relative;
}
.icon-wrapper { .icon-wrapper {
width: 60px; width: 60px;
height: 60px; height: 60px;
flex-shrink: 0;
border-radius: 16px; border-radius: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 1.5rem;
font-size: 1.75rem; font-size: 1.75rem;
color: #fff; color: #fff;
position: relative;
z-index: 2;
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
@ -282,9 +376,8 @@ h1 {
.card-title { .card-title {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
margin: 0 0 0.75rem 0; margin: 0;
color: #f8fafc; color: #f8fafc;
z-index: 2;
} }
.card-desc { .card-desc {
@ -315,11 +408,11 @@ h1 {
h1 { h1 {
font-size: 2.5rem; font-size: 2.5rem;
} }
.homepage-container { .homepage-container {
padding: 2rem 1rem; padding: 2rem 1rem;
} }
.homepage-feature-card { .homepage-feature-card {
padding: 2rem 1.5rem; padding: 2rem 1.5rem;
} }