initial commit

This commit is contained in:
xiaomai
2025-10-01 16:52:52 +08:00
commit e93b931288
35 changed files with 12784 additions and 0 deletions

7
app/app.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>

6
app/assets/css/app.css Normal file
View File

@@ -0,0 +1,6 @@
@import "tailwindcss";
@theme {
--color-primary: #fcef91;
--color-secondary: #fb9e3a;
}

2
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,2 @@
@import "./app.css";
@import "./markdown.css";

565
app/assets/css/markdown.css Normal file
View File

@@ -0,0 +1,565 @@
/* markdown.css
默认:明亮 / 白色背景主题
同时提供:.dark .prose 覆盖(如需启用 class-based dark 模式)
依赖:全局定义的 CSS 变量 --color-primary 和 --color-secondary
*/
/* 平滑滚动 */
html {
scroll-behavior: smooth;
}
/* ---------- 默认明亮主题Light ---------- */
/* 美化 prose 内容样式 */
.prose {
@apply text-gray-800 leading-relaxed;
font-feature-settings:
"kern" 1,
"liga" 1;
color: inherit;
}
/* 标题层级 */
.prose h1 {
@apply text-4xl font-bold mt-8 mb-6 pb-4 border-b border-gray-200;
background-image: linear-gradient(
90deg,
var(--color-primary),
var(--color-secondary)
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
background-size: 200% 200%;
animation: gradientShift 3s ease infinite;
}
.prose h2 {
@apply text-3xl font-bold text-gray-900 mt-10 mb-5 pb-3 border-b border-gray-200 relative;
}
.prose h2::before {
content: "";
@apply absolute bottom-0 left-0 w-12 h-0.5 rounded-full;
background-image: linear-gradient(
90deg,
var(--color-primary),
var(--color-secondary)
);
}
.prose h3 {
@apply text-2xl font-semibold text-gray-800 mt-8 mb-4;
color: rgba(31, 41, 55, 0.95);
}
.prose h4 {
@apply text-xl font-semibold text-gray-700 mt-7 mb-3;
}
.prose h5 {
@apply text-lg font-medium text-gray-700 mt-6 mb-3;
}
.prose h6 {
@apply text-base font-medium text-gray-600 mt-5 mb-2 italic;
}
/* 段落和文本 */
.prose p {
@apply text-gray-700 leading-relaxed mb-5;
text-align: justify;
}
.prose strong {
@apply font-bold px-1 rounded;
background: linear-gradient(
90deg,
rgba(252, 239, 145, 0.22),
rgba(251, 158, 58, 0.12)
);
color: rgba(31, 41, 55, 0.95);
}
.prose em {
@apply italic px-1 rounded;
color: var(--color-secondary);
background: rgba(251, 158, 58, 0.08);
}
.prose del {
@apply line-through px-1 rounded;
color: #ef4444;
background: rgba(239, 68, 68, 0.06);
}
/* 链接 */
.prose a {
@apply font-medium relative transition-all duration-300;
color: var(--color-secondary);
text-decoration: underline;
text-underline-offset: 3px;
}
.prose a:hover {
color: var(--color-primary);
transform: translateY(1px);
}
.prose a::after {
content: "";
@apply absolute bottom-0 left-0 w-0 h-0.5 transition-all duration-300;
background: linear-gradient(
90deg,
var(--color-primary),
var(--color-secondary)
);
}
.prose a:hover::after {
@apply w-full;
}
/* 外部链接图标 */
.prose a[href^="http"]::before {
content: "↗";
@apply inline-block mr-1 text-xs translate-y-[2px] opacity-70;
}
.prose h1 a,
.prose h2 a,
.prose h3 a,
.prose h4 a,
.prose h5 a,
.prose h6 a {
text-decoration: none;
}
/* 列表 */
.prose ul {
@apply list-none space-y-3 mb-6;
}
.prose ul li {
@apply relative pl-6;
}
.prose ul li::before {
content: "";
@apply absolute left-0 top-3 w-1.5 h-1.5 rounded-full;
background: var(--color-secondary);
}
.prose ol {
@apply list-decimal list-inside space-y-3 mb-6;
counter-reset: list-counter;
}
.prose ol li {
@apply relative pl-8;
counter-increment: list-counter;
}
.prose ol li::before {
content: counter(list-counter);
@apply absolute left-0 top-0 w-6 h-6 text-white text-xs rounded-full flex items-center justify-center font-bold;
background-image: linear-gradient(
135deg,
var(--color-primary),
var(--color-secondary)
);
}
/* 代码块(浅色风格) */
.prose pre {
@apply rounded-xl p-6 my-8 border shadow-sm overflow-x-auto;
border: 1px solid rgba(229, 231, 235, 1); /* gray-200 */
background: linear-gradient(180deg, #ffffff, #f8fafc);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
backdrop-filter: blur(6px);
}
.prose code {
@apply px-2 py-1 rounded text-sm font-mono;
background: rgba(243, 244, 246, 0.8); /* gray-50-ish */
border: 1px solid rgba(229, 231, 235, 1);
color: #0f172a;
}
.prose pre code {
@apply bg-transparent p-0 text-current border-none;
color: #0f172a;
}
/* 引用块(浅色友好) */
.prose blockquote {
@apply pl-6 italic text-gray-700 my-8 py-4 pr-6 rounded-r-xl relative overflow-hidden;
border-left: 4px solid transparent;
border-image: linear-gradient(
to bottom,
rgba(252, 239, 145, 0.9),
rgba(251, 158, 58, 0.9)
)
1;
background: linear-gradient(
90deg,
rgba(252, 239, 145, 0.06),
rgba(255, 255, 255, 0)
);
}
.prose blockquote::before {
content: '"';
@apply absolute -top-4 -left-2 text-6xl opacity-20 font-serif;
color: var(--color-primary);
}
.prose blockquote p {
@apply mb-3 last:mb-0 relative z-10;
}
/* 图片 */
.prose img {
@apply rounded-2xl my-8 mx-auto transition-all duration-500 border-2;
border-color: rgba(226, 232, 240, 0.6); /* gray-200 */
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.04);
}
.prose img:hover {
@apply scale-[1.02];
border-color: var(--color-secondary);
}
.prose figure {
@apply my-8 text-center;
}
.prose figcaption {
@apply text-sm text-gray-500 mt-3 italic;
}
/* 表格 */
.prose table {
@apply w-full border-collapse my-8 text-sm rounded-xl overflow-hidden shadow-sm;
}
.prose thead {
background-image: linear-gradient(
90deg,
rgba(252, 239, 145, 0.18),
rgba(251, 158, 58, 0.12)
);
}
.prose th {
@apply border px-6 py-4 text-left font-bold text-gray-800 text-sm uppercase tracking-wider;
border-color: rgba(226, 232, 240, 0.6);
background: rgba(250, 250, 250, 0.8);
}
.prose td {
@apply border px-6 py-4 text-gray-700;
border-color: rgba(226, 232, 240, 0.4);
}
.prose tr:nth-child(even) {
@apply bg-gray-50;
}
.prose tr:hover {
background: rgba(251, 158, 58, 0.06);
transition: background 0.2s;
}
/* 分割线 */
.prose hr {
@apply border-gray-200 my-12 relative;
}
.prose hr::before {
content: "";
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 rounded-full opacity-20;
background-image: linear-gradient(
90deg,
var(--color-primary),
var(--color-secondary)
);
}
/* 任务列表checkbox */
.prose input[type="checkbox"] {
@apply mr-3 rounded w-5 h-5 transition-all duration-200;
background: #fff;
border: 1px solid rgba(226, 232, 240, 0.8);
}
.prose input[type="checkbox"]:checked {
background-image: linear-gradient(
90deg,
var(--color-primary),
var(--color-secondary)
);
border: none;
}
.prose .task-list-item {
@apply list-none pl-0 flex items-start;
}
.prose .task-list-item input[type="checkbox"] {
@apply mt-0.5 flex-shrink-0;
}
/* 强调和标记 */
.prose mark {
@apply px-2 py-1 rounded font-medium;
background: linear-gradient(
120deg,
rgba(252, 239, 145, 0.22),
rgba(251, 158, 58, 0.18)
);
color: #8a4b00;
}
/* 键盘按键 */
.prose kbd {
@apply border rounded-lg px-3 py-1.5 text-sm font-mono shadow-sm;
background: rgba(247, 249, 250, 0.9);
border-color: rgba(226, 232, 240, 0.8);
color: #0f172a;
}
/* 动画定义 */
@keyframes gradientShift {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
/* 滚动条(浅色) */
.prose pre::-webkit-scrollbar {
@apply h-2;
}
.prose pre::-webkit-scrollbar-track {
@apply rounded-full;
background: rgba(243, 244, 246, 0.9);
}
.prose pre::-webkit-scrollbar-thumb {
border-radius: 9999px;
background-image: linear-gradient(
90deg,
var(--color-primary),
var(--color-secondary)
);
}
/* 移动端优化 */
@media (max-width: 768px) {
.prose {
@apply text-base;
}
.prose h1 {
@apply text-3xl;
}
.prose h2 {
@apply text-2xl;
}
.prose h3 {
@apply text-xl;
}
.prose pre {
@apply p-4;
}
}
/* 打印优化 */
@media print {
.prose {
@apply text-black;
}
.prose a {
@apply text-black no-underline;
}
.prose pre {
@apply bg-gray-100 border border-gray-300;
}
}
/* ---------- 可选:深色模式覆盖(.dark class 优先) ---------- */
/* 如果你使用 Tailwind 的 class-based dark 模式(<html class="dark">.dark .prose 会生效 */
/* 也可替换为 @media (prefers-color-scheme: dark) {...} 来自动跟随系统 dark 模式 */
.dark .prose {
@apply text-gray-200;
color: inherit;
}
.dark .prose h1 {
border-color: rgba(55, 65, 81, 0.5);
}
.dark .prose h2 {
color: #fff;
border-color: rgba(55, 65, 81, 0.4);
}
.dark .prose p {
@apply text-gray-300;
}
.dark .prose pre {
border: 1px solid rgba(55, 65, 81, 0.6);
background: linear-gradient(
135deg,
rgba(17, 24, 39, 0.9),
rgba(31, 41, 55, 0.9)
);
box-shadow: 0 8px 30px rgba(2, 6, 23, 0.6);
}
.dark .prose code {
background: rgba(31, 41, 55, 0.6);
border: 1px solid rgba(55, 65, 81, 0.5);
color: #e6eef8;
}
.dark .prose blockquote {
border-image: linear-gradient(
to bottom,
var(--color-primary),
var(--color-secondary)
)
1;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.02),
rgba(255, 255, 255, 0)
);
color: rgba(255, 255, 255, 0.9);
}
.dark .prose table thead {
background-image: linear-gradient(
90deg,
rgba(252, 239, 145, 0.06),
rgba(251, 158, 58, 0.04)
);
}
.dark .prose tr:nth-child(even) {
background: rgba(255, 255, 255, 0.02);
}
.dark .prose td,
.dark .prose th {
color: rgba(255, 255, 255, 0.9);
}
.dark .prose img {
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.6);
border-color: rgba(55, 65, 81, 0.5);
}
.dark .prose kbd {
background: rgba(31, 41, 55, 0.7);
border-color: rgba(55, 65, 81, 0.6);
color: #e6eef8;
}
/* 滚动条(深色) */
.dark .prose pre::-webkit-scrollbar-track {
background: rgba(17, 24, 39, 0.8);
}
.dark .prose pre::-webkit-scrollbar-thumb {
background-image: linear-gradient(
90deg,
var(--color-primary),
var(--color-secondary)
);
}
@media (prefers-color-scheme: dark) {
.prose {
@apply text-gray-200;
color: inherit;
}
.prose h1 {
border-color: rgba(55, 65, 81, 0.5);
}
.prose h2 {
color: #fff;
border-color: rgba(55, 65, 81, 0.4);
}
.prose p {
@apply text-gray-300;
}
.prose pre {
border: 1px solid rgba(55, 65, 81, 0.6);
background: linear-gradient(
135deg,
rgba(17, 24, 39, 0.9),
rgba(31, 41, 55, 0.9)
);
box-shadow: 0 8px 30px rgba(2, 6, 23, 0.6);
}
.prose code {
background: rgba(31, 41, 55, 0.6);
border: 1px solid rgba(55, 65, 81, 0.5);
color: #e6eef8;
}
.prose blockquote {
border-image: linear-gradient(
to bottom,
var(--color-primary),
var(--color-secondary)
)
1;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.02),
rgba(255, 255, 255, 0)
);
color: rgba(255, 255, 255, 0.9);
}
.prose table thead {
background-image: linear-gradient(
90deg,
rgba(252, 239, 145, 0.06),
rgba(251, 158, 58, 0.04)
);
}
.prose tr:nth-child(even) {
background: rgba(255, 255, 255, 0.02);
}
.prose td,
.prose th {
color: rgba(255, 255, 255, 0.9);
}
.prose img {
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.6);
border-color: rgba(55, 65, 81, 0.5);
}
.prose kbd {
background: rgba(31, 41, 55, 0.7);
border-color: rgba(55, 65, 81, 0.6);
color: #e6eef8;
}
/* 滚动条(深色) */
.prose pre::-webkit-scrollbar-track {
background: rgba(17, 24, 39, 0.8);
}
.prose pre::-webkit-scrollbar-thumb {
background-image: linear-gradient(
90deg,
var(--color-primary),
var(--color-secondary)
);
}
}

View File

@@ -0,0 +1,39 @@
<template>
<!-- 页脚 -->
<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>
</div>
<div class="">
<p class="mt-1">
Powered by:
<a href="https://tootaio.com" target="_blank" class="font-semibold hover:underline" style="color: #e24545;">
Tootaio Studio
</a>
<span class="mt-1 text-sm text-gray-400">18 级毕业学长麦祖奕</span>
</p>
</div>
<!-- 右侧社交链接 -->
<div class="flex space-x-4 mt-3 md:mt-0">
<a href="#">
<Icon name="mdi-facebook" />
</a>
<a href="#">
<Icon name="mdi-instagram" />
</a>
<a href="#">
<Icon name="mdi-gmail" />
</a>
</div>
</div>
</footer>
</template>
<script lang="ts" setup>
</script>
<style></style>

View File

@@ -0,0 +1,25 @@
<template>
<!-- 导航栏 -->
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-4 py-3 flex justify-between items-center">
<div>
<img class="inline w-16" src="/Logo.svg" alt="YPHS Alumni">
<h1 class="inline text-xl font-bold text-gray-900">
<a href="/" class="ml-4 hover:text-secondary">永平中学校友会</a>
</h1>
</div>
<nav class="space-x-6 hidden md:flex items-center">
<a href="#news" class="hover:text-secondary">新闻</a>
<a href="#events" class="hover:text-secondary">活动</a>
<a href="#donate" class="hover:text-secondary">捐赠</a>
<a href="#about" class="hover:text-secondary">关于</a>
<a href="/join-us"
class="inline-flex items-center gap-2 bg-secondary text-white px-4 py-2 rounded-xl shadow hover:opacity-90">
加入
<Icon name="mdi:account-plus" class="w-5 h-5" />
</a>
</nav>
</div>
</header>
</template>

View File

@@ -0,0 +1,17 @@
<template>
<div>
<!-- 关于我们 -->
<section id="about" class="max-w-6xl mx-auto py-16 px-4">
<h3 class="text-2xl font-bold text-gray-900 mb-4">关于校友会</h3>
<p class="text-gray-700 leading-relaxed">
永平中学校友会成立的宗旨是连接全球校友传承母校精神支持在校学生成长我们定期举办活动推动交流与合作并积极回馈母校
</p>
</section>
</div>
</template>
<script lang="ts" setup>
</script>
<style></style>

View File

@@ -0,0 +1,16 @@
<template>
<div>
<!-- 捐赠模块 -->
<section id="donate" class="py-16 text-center bg-[var(--color-primary)]">
<h3 class="text-2xl font-bold text-gray-900 mb-4">支持与捐赠</h3>
<p class="max-w-2xl mx-auto text-gray-700 mb-6">您的捐赠将用于奖学金校园建设及校友活动发展感谢您对母校的支持</p>
<a href="#" class="bg-[var(--color-secondary)] text-white px-8 py-3 rounded-xl shadow hover:opacity-90">立即捐赠</a>
</section>
</div>
</template>
<script lang="ts" setup>
</script>
<style></style>

View File

@@ -0,0 +1,29 @@
<template>
<div>
<!-- 活动模块 -->
<section id="events" class="bg-gray-100 py-16">
<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 v-for="event in events" :key="event.id" class="bg-white shadow rounded-xl p-6">
<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-4">地点{{ event.location }}</p>
<a :href="event.path" class="bg-secondary text-white px-5 py-2 rounded-lg hover:opacity-90">阅读详情</a>
</div>
</div>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
const { data: events } = await useAsyncData('events', () =>
queryCollection('events')
.order("date", "DESC")
.limit(3)
.all()
)
</script>
<style></style>

View File

@@ -0,0 +1,35 @@
<template>
<div>
<!-- Hero Banner -->
<section class="relative bg-primary py-32 md:py-48 lg:py-64 text-center bg-cover bg-center"
style="background-image: url('http://img.tootaio.com/i/2025/10/01/nu13l1.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-secondary 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

@@ -0,0 +1,34 @@
<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-white rounded-xl shadow p-5 cursor-pointer transition transform hover:-translate-y-1 hover:scale-105 hover:shadow-xl duration-300 ease-in-out">
<h3 class="font-semibold mb-2">{{ n.title }}</h3>
<div class="px-1 w-max bg-primary/25 border-primary border-2 rounded-xl text-secondary text-sm mb-2">{{
useChineseDateFormat(n.date) }}</div>
<p class="text-sm text-gray-600">{{ n.description }}</p>
</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

@@ -0,0 +1,9 @@
export const useChineseDateFormat = (input: Date | string) => {
const date = input instanceof Date ? input : new Date(input);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
// 返回一个响应式值
return ref(`${y}${m}${d}`);
};

15
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
<div>
<AppHeader />
<slot />
<AppFooter />
</div>
</template>
<script lang="ts" setup>
</script>
<style>
</style>

View File

@@ -0,0 +1,61 @@
<template>
<div class="min-h-screen bg-white">
<!-- 装饰性背景元素浅色柔和光晕 -->
<div class="fixed inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-40 -right-40 w-80 h-80 bg-blue-100/40 rounded-full blur-3xl"></div>
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-purple-100/40 rounded-full blur-3xl"></div>
<div
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-cyan-100/40 rounded-full blur-3xl">
</div>
</div>
<section class="relative py-16 px-4 sm:px-6 lg:px-8">
<div class="container mx-auto max-w-4xl">
<!-- 内容卡片 -->
<div class="relative">
<!-- 卡片装饰边框浅色渐变 -->
<div class="absolute inset-0 bg-gradient-to-r from-blue-100 to-purple-100 rounded-2xl blur-sm"></div>
<div class="relative bg-white rounded-xl border border-gray-200 shadow-xl overflow-hidden">
<!-- 顶部装饰条明亮渐变 -->
<div class="h-1 bg-gradient-to-r from-blue-400 via-purple-400 to-cyan-400"></div>
<div class="p-8 sm:p-10 lg:p-12">
<!-- 内容渲染器 -->
<div class="prose prose-lg max-w-none text-gray-800">
<ContentRenderer :value="event ?? {}">
<template #empty>
<div class="text-center py-16">
<div class="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4">
<svg class="w-8 h-8 text-blue-500 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
</circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
</div>
<p class="text-gray-700 text-lg font-medium">内容加载中...</p>
<p class="text-gray-400 text-sm mt-2">请稍等片刻</p>
</div>
</template>
</ContentRenderer>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
const route = useRoute()
const { data: event } = await useAsyncData('event-detail', () =>
queryCollection('events')
.path(`/events/${route.params.slug}`)
.first()
)
</script>
<style scoped></style>

15
app/pages/index.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
<div>
<IndexHero />
<IndexNews />
<IndexEvents />
<IndexDonate />
<IndexAbout />
</div>
</template>
<script lang="ts" setup>
</script>
<style></style>

212
app/pages/join-us/index.vue Normal file
View File

@@ -0,0 +1,212 @@
<template>
<div class="min-h-screen bg-primary py-10">
<div class="max-w-3xl mx-auto p-8 bg-white rounded-2xl shadow-lg">
<h1 class="text-3xl font-bold mb-6 text-center text-secondary">
永平中学校友会入会申请表
</h1>
<p class="text-sm text-gray-600 my-6 text-center leading-relaxed">
兹申请加入成为永平中学校友会会员愿遵守会规及议决案并填此表为据<br />
入会费 <span class="font-bold text-secondary">RM60 / </span><br />
填写此表格之后会有理事联系您协商入会费事宜
</p>
<el-form :model="form" :rules="rules" ref="formRef" label-width="130px" status-icon>
<!-- 中文姓名 -->
<el-form-item label="中文姓名" prop="chineseName">
<el-input v-model="form.chineseName" placeholder="请输入中文姓名" />
</el-form-item>
<!-- 英文姓名 (自动转大写) -->
<el-form-item label="英文姓名" prop="englishName">
<el-input v-model="form.englishName" placeholder="请输入英文姓名" @input="toUpperCaseEnglish" />
</el-form-item>
<!-- IC -->
<el-form-item label="IC" prop="ic">
<el-input v-model="form.ic" placeholder="000000-00-0000" v-maska="'######-##-####'" />
</el-form-item>
<!-- 电邮 -->
<el-form-item label="电邮" prop="email">
<el-input v-model="form.email" placeholder="选填 / 国外必填"
v-maska="['###-#######', '###-########', '+#############']" />
</el-form-item>
<!-- 电话 -->
<el-form-item label="电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入电话(使用 WhatsApp 号码为佳,可加入校友会群组)" />
</el-form-item>
<!-- 毕业层次 -->
<el-form-item label="毕业层次" prop="educationLevel">
<el-radio-group v-model="form.educationLevel">
<el-radio label="初中毕业">初中毕业</el-radio>
<el-radio label="高中毕业">高中毕业</el-radio>
<el-radio label="辍学/转学肄业">辍学/转学肄业</el-radio>
<el-radio label="不确定">不确定</el-radio>
</el-radio-group>
</el-form-item>
<!-- 毕业年份 -->
<el-form-item label="毕业年份" prop="gradYear">
<el-input-number v-model="form.gradYear" :min="1957" :max="currentYear" :step="1"
:disabled="form.unknownGradYear" />
<el-checkbox v-model="form.unknownGradYear">毕业年份不详</el-checkbox>
<span class="text-sm text-gray-500 ml-2" v-if="graduationBatch">
您是第 <span class="font-bold">{{ graduationBatch }}</span> 届毕业生
</span>
</el-form-item>
<!-- 婚姻状态 -->
<el-form-item label="婚姻状态" prop="maritalStatus">
<el-radio-group v-model="form.maritalStatus">
<el-radio label="未婚">未婚</el-radio>
<el-radio label="已婚">已婚</el-radio>
<el-radio label="其他">其他</el-radio>
</el-radio-group>
</el-form-item>
<!-- 国家选择 -->
<el-form-item label="国家" prop="country">
<el-select v-model="form.country" placeholder="请选择国家">
<el-option label="马来西亚" value="马来西亚" />
<el-option label="新加坡" value="新加坡" />
<el-option label="中国" value="中国" />
<el-option label="美国" value="美国" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<!-- 详细地址 -->
<el-form-item label="详细地址" prop="address">
<el-input type="textarea" v-model="form.address" placeholder="请输入现居详细地址" />
</el-form-item>
<!-- 提交按钮 -->
<div class="text-center mt-8">
<el-button type="warning"
class="bg-secondary border-none px-8 py-2 rounded-xl text-white font-bold shadow hover:scale-105 transition"
@click="handleSubmit">
提交申请
</el-button>
</div>
</el-form>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { vMaska } from 'maska/vue'
const currentYear = new Date().getFullYear();
const formRef = ref(null);
const form = reactive({
chineseName: "",
englishName: "",
ic: "",
email: "",
phone: "",
gradYear: null,
maritalStatus: "",
country: "",
address: "",
});
// 英文名自动转大写
const toUpperCaseEnglish = () => {
form.englishName = form.englishName.toUpperCase();
};
// 1966 年为第一届高中生毕业年份。由此根据 gradYear 计算毕业届别
const graduationBatch = computed(() => {
if (form.gradYear) {
if (form.educationLevel === "高中毕业") {
return form.gradYear - 1965; // 高中届别
} else if (form.educationLevel === "初中毕业") {
return form.gradYear - 1958; // 假设 1959 第一届初中
}
}
return null;
});
// 表单验证规则
const rules = {
chineseName: [{ required: true, message: "请输入中文姓名", trigger: "blur" }],
englishName: [{ required: true, message: "请输入英文姓名", trigger: "blur" }],
ic: [
{ required: true, message: "请输入 IC", trigger: "blur" },
{
pattern: /^\d{6}-\d{2}-\d{4}$/,
message: "格式应为 000000-00-0000",
trigger: "blur",
},
],
email: [
{
validator: (rule, value, callback) => {
if (!value && form.country !== "马来西亚") {
callback(new Error("国外居住必须填写电邮"));
} else if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
callback(new Error("请输入有效的电邮地址"));
} else {
callback();
}
},
trigger: "blur",
},
],
phone: [
{ required: true, message: "请输入电话", trigger: "blur" },
{
validator: (rule, value, callback) => {
if (/^01\d{1}-\d{7,8}$/.test(value)) {
callback();
} else if (/^\+\d{1,3}\s?\d+$/.test(value)) {
callback();
} else {
callback(new Error("请输入马来西亚号 (01x-xxxxxxx) 或带区号的号码"));
}
},
trigger: "blur",
},
],
educationLevel: [
{ required: true, message: "请选择毕业层次", trigger: "change" },
],
gradYear: [
{
validator: (rule, value, callback) => {
if (!form.unknownGradYear && !value) {
callback(new Error("请输入毕业年份或勾选“毕业年份不详”"));
} else {
callback();
}
},
trigger: "blur",
},
],
maritalStatus: [
{ required: true, message: "请选择婚姻状态", trigger: "change" },
],
country: [{ required: true, message: "请选择国家", trigger: "change" }],
address: [{ required: true, message: "请输入详细地址", trigger: "blur" }],
};
// 提交
const handleSubmit = () => {
formRef.value.validate((valid) => {
if (valid) {
ElMessage.success("提交成功!理事会将尽快联系您。");
} else {
ElMessage.error("请完善表单信息");
}
});
};
</script>

29
app/pages/news/[slug].vue Normal file
View File

@@ -0,0 +1,29 @@
<template>
<div>
<section class="py-20 px-4">
<div class="container mx-auto max-w-6xl">
<!-- 内容渲染器 -->
<div class="prose prose-invert prose-lg max-w-none">
<ContentRenderer :value="n ?? {}">
<template #empty>
<div class="text-center py-12">
<p class="text-gray-400 text-xl">内容加载中...</p>
</div>
</template>
</ContentRenderer>
</div>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
const route = useRoute()
const { data: n } = await useAsyncData('new-detail', () =>
queryCollection('news')
.path(`/news/${route.params.slug}`)
.first()
)
</script>
<style></style>