refactor(ui): adopt Nuxt UI components for homepage and layout

This commit refactors the main layout and homepage to utilize components from the Nuxt UI library, simplifying the
codebase and reducing custom boilerplate.

- The default layout is now built with `UPage`, `UHeader`, `UMain`, and `UFooter`.
- The homepage (`pages/index.vue`) has been updated to use `UPageHero` and `UBlogPosts`.
- The `IndexHero` and `IndexNews` components have been removed as their functionality is now integrated directly into
the index page.
- `Donate`, `Events`, and `HallOfFame` components are now wrapped with `UPageCTA` or `UPageSection`.
This commit is contained in:
xiaomai
2025-10-23 09:16:59 +08:00
parent e7f2bc2c47
commit 6473bdcc15
10 changed files with 188 additions and 183 deletions

View File

@@ -1,16 +1,14 @@
<template> <template>
<div> <UPageCTA
<!-- 捐赠模块 --> class="bg-secondary"
<section id="donate" class="py-16 text-center bg-secondary"> title="支持与捐赠"
<h3 class="text-2xl font-bold text-gray-900 mb-4">支持与捐赠功能未开放</h3> description="您的捐赠将用于奖学金、校园建设及校友活动发展。感谢您对母校的支持!"
<p class="max-w-2xl mx-auto text-gray-700 mb-6">您的捐赠将用于奖学金校园建设及校友活动发展感谢您对母校的支持</p> :links="donationLinks"
<a href="#" class="bg-primary text-white px-8 py-3 rounded-xl shadow hover:opacity-90">立即捐赠</a> />
</section>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const donationLinks = ref([{ label: "立即捐赠", icon: "mdi:cash" }]);
</script> </script>
<style></style> <style></style>

View File

@@ -1,32 +1,35 @@
<template> <template>
<div>
<!-- 活动模块 --> <!-- 活动模块 -->
<section id="events" class="bg-gray-100 py-16"> <UPageSection title="校友活动" class="bg-gray-100">
<div class="max-w-6xl mx-auto px-4">
<h3 class="text-2xl font-bold text-gray-900 mb-6">校友活动</h3>
<div class="grid md:grid-cols-3 gap-6"> <div class="grid md:grid-cols-3 gap-6">
<div v-for="event in events" :key="event.id" class="bg-white shadow rounded-xl"> <div
v-for="event in events"
:key="event.id"
class="bg-white shadow rounded-xl"
>
<img :src="event.cover" :alt="event.title" class="rounded-xl" /> <img :src="event.cover" :alt="event.title" class="rounded-xl" />
<div class="p-6"> <div class="p-6">
<h4 class="font-semibold text-lg mb-2">{{ event.title }}</h4> <h4 class="font-semibold text-lg mb-2">{{ event.title }}</h4>
<p class="text-sm text-gray-600 mb-1">日期{{ useChineseDateFormat(event.date) }}</p> <p class="text-sm text-gray-600 mb-1">
日期{{ useChineseDateFormat(event.date) }}
</p>
<p class="text-sm text-gray-600 mb-4">地点{{ event.location }}</p> <p class="text-sm text-gray-600 mb-4">地点{{ event.location }}</p>
<a :href="event.path" class="bg-primary text-white px-5 py-2 rounded-lg hover:opacity-90">阅读详情</a> <!-- <a
:href="event.path"
class="bg-primary text-white px-5 py-2 rounded-lg hover:opacity-90"
>阅读详情</a
> -->
<UButton label="阅读详情" :to="event.path" trailing-icon="mdi:glasses"/>
</div> </div>
</div> </div>
</div> </div>
</div> </UPageSection>
</section>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const { data: events } = await useAsyncData('events', () => const { data: events } = await useAsyncData("events", () =>
queryCollection('events') queryCollection("events").order("date", "DESC").limit(3).all()
.order("date", "DESC") );
.limit(3)
.all()
)
</script> </script>
<style></style> <style></style>

View File

@@ -1,31 +1,33 @@
<template> <template>
<div> <UPageSection title="名人堂">
<section id="hall-of-fame" class="py-16">
<div class="max-w-6xl mx-auto px-4">
<h3 class="text-2xl font-bold text-center text-gray-900 mb-6">名人堂</h3>
<div class="grid md:grid-cols-4 gap-6"> <div class="grid md:grid-cols-4 gap-6">
<div v-for="person in persons" :key="person.id" class="flex flex-col items-center cursor-pointer transition hover:scale-105 hover:drop-shadow-2xl hover:-translate-y-1" @click="jumpToPersonIntro(person.path)"> <div
<img :src="person.photo" :alt="person.name" class="w-40 rounded-full border-primary border-4" /> v-for="person in persons"
:key="person.id"
class="flex flex-col items-center cursor-pointer transition hover:scale-105 hover:drop-shadow-2xl hover:-translate-y-1"
@click="jumpToPersonIntro(person.path)"
>
<img
:src="person.photo"
:alt="person.name"
class="w-40 rounded-full border-primary border-4"
/>
<h4 class="text-lg font-bold">{{ person.name }}</h4> <h4 class="text-lg font-bold">{{ person.name }}</h4>
<p class="text-sm text-gray-500">{{ person.title }}</p> <p class="text-sm text-gray-500">{{ person.title }}</p>
</div> </div>
</div> </div>
</div> </UPageSection>
</section>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const { data: persons } = await useAsyncData('hall-of-fames', () => const { data: persons } = await useAsyncData("hall-of-fames", () =>
queryCollection('hallOfFames') queryCollection("hallOfFames").limit(4).all()
.limit(4) );
.all()
)
var router = useRouter() var router = useRouter();
const jumpToPersonIntro = (path: string) => { const jumpToPersonIntro = (path: string) => {
router.push(path) router.push(path);
} };
</script> </script>
<style></style> <style></style>

View File

@@ -1,35 +0,0 @@
<template>
<div>
<!-- Hero Banner -->
<section class="relative bg-secondary py-32 md:py-48 lg:py-64 text-center bg-cover bg-center"
style="background-image: url('/hero-image.jpg');">
<!-- 遮罩 -->
<div class="absolute inset-0 bg-black/40"></div>
<!-- 内容 -->
<div class="relative z-10 max-w-3xl mx-auto px-4">
<h2 class="text-3xl md:text-5xl font-extrabold text-white drop-shadow-lg">
连接校友 · 传承精神
</h2>
<p class="mt-4 text-lg text-gray-100">
马来西亚柔佛永平中学校友会官方网站
</p>
<div class="mt-6 space-x-4">
<a href="/join-us" class="bg-primary text-white px-6 py-3 rounded-xl shadow hover:opacity-90">
立即加入我们
</a>
<!-- <a href="#donate"
class="bg-white border-2 border-secondary text-secondary px-6 py-3 rounded-xl hover:bg-secondary hover:text-white">
支持捐赠
</a> -->
</div>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
</script>
<style></style>

View File

@@ -1,37 +0,0 @@
<template>
<div>
<!-- 新闻公告 -->
<section id="news" class="max-w-6xl mx-auto py-16 px-4">
<h2 class="text-2xl font-bold text-gray-900 mb-6">最新新闻与公告</h2>
<div class="grid md:grid-cols-3 gap-6">
<article v-for="n in news" :key="n.id" @click="jumpToNewsDetail(n.stem)"
class="bg-secondary/10 rounded-xl shadow cursor-pointer transition transform hover:-translate-y-1 hover:scale-105 hover:shadow-xl duration-300 ease-in-out">
<img class="rounded-xl" :src="n.cover" :alt="n.title">
<div class="p-5">
<h3 class="font-semibold mb-2">{{ n.title }}</h3>
<div class="px-1 w-max bg-secondary/25 border-secondary border-2 rounded-xl text-primary text-sm mb-2">{{
useChineseDateFormat(n.date) }}</div>
<p class="text-sm text-gray-600">{{ n.description }}</p>
</div>
</article>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
const { data: news } = await useAsyncData('news', () =>
queryCollection('news')
.order("date", "DESC")
.limit(3)
.all()
)
const router = useRouter();
const jumpToNewsDetail = (newsPath: string) => {
router.push(newsPath);
}
</script>
<style></style>

View File

@@ -1,51 +1,51 @@
<template> <template>
<div class="min-h-screen flex flex-col"> <UPage>
<!-- 导航栏 --> <UHeader>
<header class="bg-white shadow-md sticky top-0 z-50"> <template #left>
<div class="max-w-6xl mx-auto px-4 py-3 flex justify-between items-center"> <img class="inline h-12 w-auto" src="/Logo.svg" alt="YPHS Alumni" />
<div>
<img class="inline w-16" src="/Logo.svg" alt="YPHS Alumni" />
<h1 class="inline text-xl font-bold text-gray-900"> <h1 class="inline text-xl font-bold text-gray-900">
<a href="/" class="ml-4 hover:text-primary">永平中学校友会</a> <a href="/" class="ml-4 hover:text-primary">永平中学校友会</a>
</h1> </h1>
</div> </template>
<template #right>
<nav class="space-x-6 hidden md:flex items-center"> <nav class="space-x-6 hidden md:flex items-center">
<a href="#news" class="hover:text-primary">新闻</a> <UNavigationMenu :items="items" />
<a href="#events" class="hover:text-primary">活动</a> <a
<a href="#donate" class="hover:text-primary">捐赠未开放</a> href="/join-us"
<a href="#about" class="hover:text-primary">关于</a> class="inline-flex items-center gap-2 bg-primary text-white px-4 py-2 rounded-xl shadow hover:opacity-90"
<a href="/join-us" >
class="inline-flex items-center gap-2 bg-primary text-white px-4 py-2 rounded-xl shadow hover:opacity-90">
加入 加入
<Icon name="mdi:account-plus" class="w-5 h-5" /> <Icon name="mdi:account-plus" class="w-5 h-5" />
</a> </a>
</nav> </nav>
</div> </template>
</header> </UHeader>
<UMain>
<!-- 主体部分 -->
<main class="flex-1">
<slot /> <slot />
</main> </UMain>
<UFooter>
<!-- 页脚 --> <template #left>
<footer class="bg-gray-900 text-gray-300 py-6">
<div class="max-w-6xl mx-auto px-4 flex flex-col md:flex-row justify-between items-center">
<div class="text-center md:text-left">
<p>© 2025 永平中学校友会. 保留所有权利.</p> <p>© 2025 永平中学校友会. 保留所有权利.</p>
</div> </template>
<div> <template #default>
<p class="mt-1"> <p class="mt-1">
Powered by: Powered by:
<a href="https://tootaio.com" target="_blank" class="font-semibold hover:underline" style="color: #e24545;"> <a
href="https://tootaio.com"
target="_blank"
class="font-semibold hover:underline"
style="color: #e24545"
>
Tootaio Studio Tootaio Studio
</a> </a>
<span class="mt-1 text-sm text-gray-400">2018 级毕业学长麦祖奕</span> <span class="mt-1 text-sm"
>2018 级毕业学长麦祖奕</span
>
</p> </p>
</div> </template>
<div class="flex space-x-4 mt-3 md:mt-0"> <template #right>
<a href="#"> <a href="#">
<Icon name="mdi-facebook" /> <Icon name="mdi-facebook" />
</a> </a>
@@ -55,8 +55,20 @@
<a href="#"> <a href="#">
<Icon name="mdi-gmail" /> <Icon name="mdi-gmail" />
</a> </a>
</div>
</div>
</footer>
</div>
</template> </template>
</UFooter>
</UPage>
</template>
<script setup lang="ts">
import type { NavigationMenuItem } from "@nuxt/ui";
const route = useRoute();
const items = computed<NavigationMenuItem[]>(() => [
{ label: "新闻", to: "#news", active: route.path.startsWith("#news") },
{ label: "活动", to: "#events", active: route.path.startsWith("#events") },
{ label: "捐赠", to: "#donate", active: route.path.startsWith("#donate") },
{ label: "关于", to: "#about", active: route.path.startsWith("#about") },
]);
</script>

View File

@@ -1,7 +1,23 @@
<template> <template>
<div> <div>
<IndexHero /> <!-- Hero Banner -->
<IndexNews /> <UPageHero
class="bg-cover bg-center"
style="
background-image: url(&quot;/hero-image.jpg&quot;);
background-color: rgba(255, 255, 255, 0.5); /* Semi-transparent black */
background-blend-mode: lighten;
"
title="连接校友 · 传承精神"
description="马来西亚柔佛永平中学校友会官方网站"
:links="heroCta"
/>
<!-- News Blog Posts -->
<UPageSection title="新闻与公告">
<UBlogPosts :posts="newsPost" />
</UPageSection>
<IndexEvents /> <IndexEvents />
<IndexHallOfFame /> <IndexHallOfFame />
<IndexDonate /> <IndexDonate />
@@ -10,7 +26,41 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { BlogPostProps, PageCardProps } from "@nuxt/ui";
const heroCta = ref([
{
label: "立即加入我们",
to: "/join-us",
icon: "mdi:account-plus",
},
]);
// ========================================================
// 新闻模块
// ========================================================
const { data: news } = await useAsyncData("news", () =>
queryCollection("news").order("date", "DESC").limit(3).all()
);
// 将 news 数据转换成 UBlogPosts 可用格式
const newsPost = computed<BlogPostProps[]>(() =>
(news.value || []).map((n: any) => ({
title: n.title,
description: n.description,
image: n.cover,
date: n.date,
to: n.path, // ✅ 建议加路由跳转
variant: "subtle",
}))
);
// ========================================================
// 活动模块
// ========================================================
const { data: events } = await useAsyncData("events", () =>
queryCollection("events").order("date", "DESC").limit(3).all()
);
</script> </script>
<style></style> <style></style>

View File

@@ -11,6 +11,7 @@ highlight: true
seoTitle: "永中校友会官网上线 | 最新活动与资讯平台" seoTitle: "永中校友会官网上线 | 最新活动与资讯平台"
seoDescription: "永中校友会官网正式上线,校友可在平台获取最新资讯、报名活动及参与互动。" seoDescription: "永中校友会官网正式上线,校友可在平台获取最新资讯、报名活动及参与互动。"
ogImage: "/images/og/news-launch.jpg" ogImage: "/images/og/news-launch.jpg"
slug: "/news/20251001-official-web-launch"
--- ---
永中校友会官网正式上线啦!🎉 永中校友会官网正式上线啦!🎉

View File

@@ -31,6 +31,7 @@
"vue-sonner": "^2.0.9" "vue-sonner": "^2.0.9"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/lucide": "^1.2.70",
"sass-embedded": "^1.93.2" "sass-embedded": "^1.93.2"
} }
} }

10
pnpm-lock.yaml generated
View File

@@ -66,6 +66,9 @@ importers:
specifier: ^2.0.9 specifier: ^2.0.9
version: 2.0.9(@nuxt/kit@4.1.3(magicast@0.3.5))(@nuxt/schema@4.1.3)(nuxt@4.1.3(@parcel/watcher@2.5.1)(@types/node@24.7.0)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.1)(lightningcss@1.30.1)(magicast@0.3.5)(rollup@4.52.4)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)) version: 2.0.9(@nuxt/kit@4.1.3(magicast@0.3.5))(@nuxt/schema@4.1.3)(nuxt@4.1.3(@parcel/watcher@2.5.1)(@types/node@24.7.0)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.1)(lightningcss@1.30.1)(magicast@0.3.5)(rollup@4.52.4)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))
devDependencies: devDependencies:
'@iconify-json/lucide':
specifier: ^1.2.70
version: 1.2.70
sass-embedded: sass-embedded:
specifier: ^1.93.2 specifier: ^1.93.2
version: 1.93.2 version: 1.93.2
@@ -528,6 +531,9 @@ packages:
'@floating-ui/vue@1.1.9': '@floating-ui/vue@1.1.9':
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==} resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
'@iconify-json/lucide@1.2.70':
resolution: {integrity: sha512-56s9NdBKgshywVY1e4gOcxzAbU1J649e/jLHBJU1tyNqRs7mFLVEGwj2mmzHJ5YAZB5Tsngi4f/ocTBPlG06ZA==}
'@iconify/collections@1.0.602': '@iconify/collections@1.0.602':
resolution: {integrity: sha512-bpHY7qw+a5QbYlsyROx3Ar2jb8hMMvUOrp99a+pQTHDM+pAVR76WMYriRdW0mKcDo9L3uoU5u0uI9LY+jfbkOQ==} resolution: {integrity: sha512-bpHY7qw+a5QbYlsyROx3Ar2jb8hMMvUOrp99a+pQTHDM+pAVR76WMYriRdW0mKcDo9L3uoU5u0uI9LY+jfbkOQ==}
@@ -5959,6 +5965,10 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
'@iconify-json/lucide@1.2.70':
dependencies:
'@iconify/types': 2.0.0
'@iconify/collections@1.0.602': '@iconify/collections@1.0.602':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0