initial commit
This commit is contained in:
7
app/app.vue
Normal file
7
app/app.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
6
app/assets/css/app.css
Normal file
6
app/assets/css/app.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-primary: #fcef91;
|
||||
--color-secondary: #fb9e3a;
|
||||
}
|
||||
2
app/assets/css/main.css
Normal file
2
app/assets/css/main.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "./app.css";
|
||||
@import "./markdown.css";
|
||||
565
app/assets/css/markdown.css
Normal file
565
app/assets/css/markdown.css
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
39
app/components/AppFooter.vue
Normal file
39
app/components/AppFooter.vue
Normal 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>
|
||||
25
app/components/AppHeader.vue
Normal file
25
app/components/AppHeader.vue
Normal 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>
|
||||
17
app/components/index/About.vue
Normal file
17
app/components/index/About.vue
Normal 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>
|
||||
16
app/components/index/Donate.vue
Normal file
16
app/components/index/Donate.vue
Normal 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>
|
||||
29
app/components/index/Events.vue
Normal file
29
app/components/index/Events.vue
Normal 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>
|
||||
35
app/components/index/Hero.vue
Normal file
35
app/components/index/Hero.vue
Normal 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>
|
||||
34
app/components/index/News.vue
Normal file
34
app/components/index/News.vue
Normal 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>
|
||||
9
app/composables/useChineseDateFormat.ts
Normal file
9
app/composables/useChineseDateFormat.ts
Normal 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
15
app/layouts/default.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppHeader />
|
||||
<slot />
|
||||
<AppFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
61
app/pages/events/[slug].vue
Normal file
61
app/pages/events/[slug].vue
Normal 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
15
app/pages/index.vue
Normal 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
212
app/pages/join-us/index.vue
Normal 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
29
app/pages/news/[slug].vue
Normal 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>
|
||||
Reference in New Issue
Block a user