Compare commits
23 Commits
3254926c43
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6de61c24b2 | ||
|
|
f6bbd95b77 | ||
|
|
a28bb3a54d | ||
|
|
9bca019b50 | ||
|
|
6288a1b01b | ||
|
|
c7da09d327 | ||
|
|
2ac1428c34 | ||
|
|
3da20d0097 | ||
|
|
f5d9963f3c | ||
|
|
cf6dfac6a3 | ||
|
|
2649cca69d | ||
|
|
59fc6cb13d | ||
|
|
33334a7515 | ||
|
|
c468da8780 | ||
|
|
c3e05d790c | ||
|
|
0a46c3e591 | ||
|
|
567c9ef9c9 | ||
|
|
a864ffd9cf | ||
|
|
7bcabb0c71 | ||
|
|
6473bdcc15 | ||
|
|
e7f2bc2c47 | ||
|
|
1fedf7094c | ||
|
|
cc7b2a7398 |
4
.gitignore
vendored
@@ -23,4 +23,6 @@ logs
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
repomix-output.xml
|
||||
repomix-output.xml
|
||||
|
||||
content/members/members.csv
|
||||
10
.repomixignore
Normal file
@@ -0,0 +1,10 @@
|
||||
# Add patterns to ignore here, one per line
|
||||
# Example:
|
||||
# *.log
|
||||
# tmp/
|
||||
pnpm-workspace.yaml
|
||||
public/
|
||||
app/assets/
|
||||
docs/
|
||||
*.md
|
||||
countries.ts
|
||||
27
.vscode/admin-templates.vue
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar :title="pageTitle" toggle>
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
<template #body>
|
||||
管理员界面模板
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const pageTitle = "管理员界面模板"
|
||||
definePageMeta({
|
||||
layout: "admin-dashboard",
|
||||
title: pageTitle
|
||||
})
|
||||
useHead({
|
||||
title: pageTitle
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"nuxtr.nuxt-vscode-extentions",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
30
.vscode/settings.json
vendored
@@ -1,8 +1,24 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.page-template": "vue",
|
||||
"*.layout-template": "vue",
|
||||
"*.vue": "vue",
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
}
|
||||
"files.associations": {
|
||||
"*.page-template": "vue",
|
||||
"*.layout-template": "vue",
|
||||
"*.vue": "vue",
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
"editor.quickSuggestions": {
|
||||
"other": "on",
|
||||
"comments": "off",
|
||||
"strings": "on"
|
||||
},
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"className",
|
||||
"ngClass",
|
||||
"class:list",
|
||||
"ui"
|
||||
],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
],
|
||||
"css.lint.unknownAtRules": "ignore"
|
||||
}
|
||||
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 [Tootaio Studio]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
264
README.md
@@ -1,75 +1,229 @@
|
||||
# Nuxt Minimal Starter
|
||||
# 永平中学校友会官网(YPHS Alumni)
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
一个基于 Nuxt 4 与 Nuxt Content 打造的内容型网站,面向永平中学校友与社会公众,提供新闻公告、活动预告、名人堂与校友组织介绍等信息。项目内置基础的后台界面脚手架,支持后续扩展会员管理与内容发布流程。
|
||||
|
||||
## Setup
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Make sure to install dependencies:
|
||||
- 线上地址:https://yphsalumni.org
|
||||
- 技术栈:Nuxt 4(Vue 3 + Nitro)、TypeScript、Tailwind CSS 4、@nuxt/ui、@nuxt/content、@nuxt/image、SEO/Sitemap/Robots 模块
|
||||
|
||||
|
||||
**项目亮点**
|
||||
- 内容优先:使用 Markdown + 前置属性(Frontmatter)管理新闻、活动、名人堂。
|
||||
- 现代 UI:采用 @nuxt/ui + Tailwind CSS 4,内置响应式导航与主题样式。
|
||||
- SEO 友好:内置 @nuxtjs/seo、@nuxtjs/sitemap、@nuxtjs/robots 与合理的默认 `<head>` 元信息。
|
||||
- 可扩展的后台:提供基础的「仪表盘 / 内容列表 / 模态编辑器」结构,便于后续对接真实后端。
|
||||
- 零后端即可上线:作为纯内容站点可静态导出部署,也可运行 SSR 部署。
|
||||
|
||||
|
||||
## 目录结构
|
||||
|
||||
关键目录说明:
|
||||
|
||||
- `app/`
|
||||
- `pages/` 路由页面(首页、新闻、活动、名人堂、关于、入会申请、后台等)
|
||||
- `layouts/` 页面布局(`default`、`admin-dashboard`)
|
||||
- `components/` 组件(首页模块、后台弹窗、输入组件等)
|
||||
- `assets/css/` 样式(Tailwind 入口与 Markdown 样式)
|
||||
- `composables/` 可复用逻辑(日期格式化、国家数据等)
|
||||
- `data/` 静态数据(如国家拨号表)
|
||||
- `content/` Markdown 内容(`news/`、`events/`、`hall-of-fames/`)
|
||||
- `content.config.ts` 内容集合与字段 Schema 定义
|
||||
- `public/` 公开静态资源(Logo、封面图、音频、图片等)
|
||||
- `nuxt.config.ts` Nuxt 主配置(模块、SEO、站点信息等)
|
||||
- `docs/` 产品与信息架构文档
|
||||
|
||||
|
||||
## 功能概览
|
||||
|
||||
- 首页:英雄区 + 最新新闻 + 活动卡片 + 名人堂预览 + 捐赠占位模块
|
||||
- 新闻:列表与详情(Markdown 渲染,支持封面、标签、SEO 字段)
|
||||
- 活动:列表与详情(日期/地点/封面,支持嵌入媒体)
|
||||
- 名人堂:人物详情 + 图集 + 外链视频/媒体嵌入
|
||||
- 关于页面:会徽含义、校歌音频等
|
||||
- 入会申请:UI 表单(当前为占位,未连通后端)
|
||||
- 后台(占位):仪表盘、会员籍管理(示例数据)、新闻/活动/名人堂入口与新建弹窗(`md-editor-v3`)
|
||||
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js ≥ 18.20 或 20.x(推荐 20 LTS)
|
||||
- 包管理器:pnpm(推荐)/ npm / yarn / bun
|
||||
|
||||
启用 pnpm(建议):
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
corepack enable
|
||||
corepack prepare pnpm@latest --activate
|
||||
```
|
||||
|
||||
# pnpm
|
||||
|
||||
## 本地开发
|
||||
|
||||
安装依赖:
|
||||
```bash
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
启动开发服务器(默认 http://localhost:3000):
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
## 构建与预览
|
||||
|
||||
SSR 构建与本地预览:
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
静态导出(适合静态站部署,如 Netlify/Cloudflare Pages/Nginx 静态目录):
|
||||
```bash
|
||||
pnpm generate
|
||||
# 产物在 .output/public (仓库中有 dist -> .output/public 的软链)
|
||||
```
|
||||
|
||||
|
||||
## 部署指引
|
||||
|
||||
- 静态托管:执行 `pnpm generate` 后,将 `.output/public`(或 `dist` 软链)上传到任意静态空间(如 Cloudflare Pages、Netlify、GitHub Pages、Nginx 静态目录)。
|
||||
- Node SSR:执行 `pnpm build` 后,以 Node 方式运行 `.output/server/index.mjs`,或使用平台的 Nuxt 运行时适配(Vercel/Netlify Functions/自托管 Node)。
|
||||
- SEO:请在 `nuxt.config.ts` 的 `site.url` 与 `app.head` 中更新为你自己的域名与元信息。
|
||||
|
||||
|
||||
## 内容写作与数据结构
|
||||
|
||||
项目使用 `@nuxt/content` 管理内容,字段规则见 `content.config.ts`。在对应目录新增 Markdown 文件即可出现在站点中。
|
||||
|
||||
示例:新闻(`content/news/20251001-official-web-launch.md`)
|
||||
```md
|
||||
---
|
||||
title: "永中校友会官网正式上线"
|
||||
date: "2025-10-01"
|
||||
updated: "2025-10-01"
|
||||
author: "作者名"
|
||||
description: "用于列表摘要与 SEO 的简短说明"
|
||||
cover: "/news/20251001-official-web-launch/Screenshot.png"
|
||||
tags: ["活动", "公告"]
|
||||
category: "通知"
|
||||
highlight: true
|
||||
seoTitle: "SEO 标题"
|
||||
seoDescription: "SEO 描述"
|
||||
ogImage: "/images/og/news-launch.jpg"
|
||||
---
|
||||
|
||||
正文使用 Markdown 书写,支持图片、表格、引用与 HTML 块。
|
||||
```
|
||||
|
||||
示例:活动(`content/events/20250927-return-to-school.md`)
|
||||
```md
|
||||
---
|
||||
title: "活动标题"
|
||||
subtitle: "活动副标题"
|
||||
date: "2025-09-27"
|
||||
location: "地点名称"
|
||||
cover: "/events/20250927-return-to-school/event-photo-1.jpg"
|
||||
---
|
||||
|
||||
活动详情正文...
|
||||
```
|
||||
|
||||
示例:名人堂(`content/hall-of-fames/he-si-rong.md`)
|
||||
```md
|
||||
---
|
||||
name: "人物姓名"
|
||||
photo: "/hall-of-fame/he-si-rong/人物头像.png"
|
||||
title: "人物头衔"
|
||||
description: "简短介绍"
|
||||
gallery:
|
||||
[
|
||||
"/hall-of-fame/he-si-rong/图1.webp",
|
||||
"/hall-of-fame/he-si-rong/图2.webp"
|
||||
]
|
||||
---
|
||||
|
||||
正文可包含媒体嵌入。
|
||||
```
|
||||
|
||||
|
||||
## 定制与配置
|
||||
|
||||
- 站点信息与 SEO:`nuxt.config.ts`
|
||||
- `app.head.titleTemplate` 与默认 `title`
|
||||
- Meta、OG、Twitter 卡片默认值
|
||||
- `site.url`(用于站点地图与 SEO)
|
||||
- 导航与页脚:`app/layouts/default.vue`
|
||||
- 样式与主题色:`app/assets/css/main.css`(Tailwind v4 与自定义 CSS 变量)
|
||||
- Markdown 渲染样式:`app/assets/css/markdown.css`
|
||||
- 首页区块:`app/components/index/*.vue`
|
||||
- TikTok 嵌入组件:`app/components/TikTokEmbed.vue`
|
||||
- 后台布局与侧边导航:`app/layouts/admin-dashboard.vue`、`app/composables/useDashboardSidebarLinks.ts`
|
||||
- 分析脚本:`public/analytics.js`(默认使用 Umami,若不需要可在 `nuxt.config.ts` 中移除 `<script src="/analytics.js">` 注入)
|
||||
|
||||
|
||||
## 后台与数据(现状)
|
||||
|
||||
- 后台页面为 UI 脚手架,方便未来接入真实后端(当前示例页使用占位数据或仅展示结构)。
|
||||
- 成员管理页使用示例 API(`jsonplaceholder`)占位,待替换为校友会实际后端服务。
|
||||
- 入会申请页面为 UI 表单与校验演示,暂未接入存储与流程(页面顶部已加“功能未开放”提示遮罩)。
|
||||
|
||||
若需接入真实后端,建议:
|
||||
- 认证与权限:OAuth/JWT + 角色权限
|
||||
- 数据库存储:PostgreSQL / MySQL;缓存:Redis
|
||||
- 内容发布:保留 Markdown/Content 模式,或引入 Headless CMS(如 Strapi/Directus)
|
||||
|
||||
|
||||
## 开发者指南
|
||||
|
||||
- 代码风格:TypeScript + Nuxt 约定式目录;组件与页面尽量保持职责单一
|
||||
- 内容 schema:若新增内容类型,请在 `content.config.ts` 中定义并校验字段
|
||||
- 资源放置:公共图片/音频/图标放在 `public/`,内容相关配图走 `public/{模块}/...`
|
||||
- 性能与无障碍:优先图片懒加载、合理的语义化标签与可访问性属性
|
||||
|
||||
|
||||
## 路线图(Roadmap)
|
||||
|
||||
- [ ] 入会申请流程与后台审核对接
|
||||
- [ ] 后台:内容 CRUD(新闻/活动/名人堂)与草稿/发布流
|
||||
- [ ] 捐赠模块与支付对接
|
||||
- [ ] 多语言与国际化(i18n)
|
||||
- [ ] 校友企业/招聘板块
|
||||
- [ ] 活动报名、签到与相册
|
||||
|
||||
|
||||
## 贡献指南
|
||||
|
||||
欢迎 Issue 与 PR:
|
||||
1) Fork 本仓库并新建分支
|
||||
2) 本地开发与验证(`pnpm dev`)
|
||||
3) 提交 PR 并说明修改动机与影响范围
|
||||
|
||||
内容贡献:
|
||||
- 新增/修改 Markdown 请遵循 `content.config.ts` 中的 schema 字段
|
||||
- 提交前请自查封面路径与日期格式是否规范
|
||||
|
||||
|
||||
## 安全与隐私
|
||||
|
||||
- 如发现安全问题,请通过电子邮件私下报告给维护者,并避免公开披露(可在此处填写邮箱或表单链接)。
|
||||
- 分析脚本(Umami)可选启用,默认文件位于 `public/analytics.js`,请替换为你自己的域名与 `website-id`,或移除注入脚本。
|
||||
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目计划以开源方式发布。建议选择并添加合适的开源许可证(如 MIT、Apache-2.0)。
|
||||
- 若你决定使用 MIT:请在仓库根目录新增 `LICENSE` 文件并写明 MIT 许可条款。
|
||||
- 在未选择明确许可证前,请勿对外分发未授权的二进制或闭源衍生物。
|
||||
|
||||
|
||||
## 致谢
|
||||
|
||||
- 设计与技术支持:Tootaio Studio
|
||||
- 内容与资料:永平中学校友会
|
||||
- 技术框架与生态:Nuxt 团队与社区、@nuxt/ui、Tailwind CSS
|
||||
|
||||
—— 连接校友 · 传承精神 ——
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<UApp>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
@import "./markdown.css";
|
||||
/* @import "./markdown.css"; */
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme {
|
||||
--color-primary: #fb9e3a;
|
||||
--color-primary-50: #fff8f0;
|
||||
--color-primary-100: #feecd8;
|
||||
--color-primary-200: #fdd4ab;
|
||||
--color-primary-300: #fbb674;
|
||||
--color-primary-400: #fb9e3a;
|
||||
--color-primary-500: #f9840a;
|
||||
--color-primary-600: #dd6b06;
|
||||
--color-primary-700: #b74f07;
|
||||
--color-primary-800: #943e0d;
|
||||
--color-primary-900: #7a340e;
|
||||
--color-primary-950: #461902;
|
||||
|
||||
--color-secondary: #fcef91;
|
||||
--color-secondary-50: #fffeea;
|
||||
--color-secondary-100: #fffbc5;
|
||||
--color-secondary-200: #fcef91;
|
||||
--color-secondary-300: #f9df53;
|
||||
--color-secondary-400: #f6ca24;
|
||||
--color-secondary-500: #e6b010;
|
||||
--color-secondary-600: #c6880a;
|
||||
--color-secondary-700: #9e610c;
|
||||
--color-secondary-800: #834d12;
|
||||
--color-secondary-900: #6f3f15;
|
||||
--color-secondary-950: #412008;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-gray-800: oklch(85% 0 275);
|
||||
}
|
||||
|
||||
@@ -1,565 +0,0 @@
|
||||
/* 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
64
app/components/PhoneInput.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<!-- ~/components/PhoneInput.vue -->
|
||||
<script setup lang="ts">
|
||||
import { USelectMenu, UInput } from "#components";
|
||||
import { useCountries } from "~/composables/useCountries";
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
defaultDial?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(["update:modelValue", "update:country"]);
|
||||
|
||||
const { getAll } = useCountries();
|
||||
|
||||
// 当前选中国家
|
||||
const selectedCountry = ref(
|
||||
getAll().find((c) => c.dial === props.defaultDial)?.dial || getAll()[0]?.dial
|
||||
);
|
||||
|
||||
console.table(getAll().map(c => ({ name: c.name.cn, dial: c.dial })));
|
||||
|
||||
// 用户输入的电话号码
|
||||
const phone = ref(props.modelValue || "");
|
||||
|
||||
// 计算选项
|
||||
const countryOptions = computed(() =>
|
||||
getAll()
|
||||
.filter((c) => c.dial && c.dial.trim() !== "")
|
||||
.map((c) => ({
|
||||
label: `${c.name.cn} (${c.dial || "未知"})`,
|
||||
id: c.dial || "unknown",
|
||||
}))
|
||||
);
|
||||
|
||||
// 完整号码输出
|
||||
const fullNumber = computed(() => {
|
||||
const dial = selectedCountry.value || "";
|
||||
return `${dial}${phone.value}`;
|
||||
});
|
||||
|
||||
// 双向绑定
|
||||
watch(phone, () => emit("update:modelValue", fullNumber.value));
|
||||
watch(selectedCountry, () => emit("update:country", selectedCountry.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 国家区号选择 -->
|
||||
<USelectMenu
|
||||
v-model="selectedCountry"
|
||||
value-key="id"
|
||||
:items="countryOptions"
|
||||
/>
|
||||
|
||||
<!-- 电话号码输入 -->
|
||||
<UInput
|
||||
v-model="phone"
|
||||
placeholder="输入电话号码"
|
||||
type="tel"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,29 +1,36 @@
|
||||
<template>
|
||||
<UModal v-model:open="open" title="撰写新闻" description="在数据库中添加一篇新闻">
|
||||
<UModal v-model:open="open" title="撰写新闻" description="在数据库中添加一篇新闻" fullscreen :ui="{
|
||||
body: 'overflow-y-auto p-6', // 防止溢出 + 内边距
|
||||
footer: 'justify-end'
|
||||
}">
|
||||
<UButton label="撰写新闻" icon="mdi:newspaper-plus" />
|
||||
|
||||
<template #body>
|
||||
<UForm :schema="newsSchema" :state="newsState" @submit="onSubmit">
|
||||
<UForm ref="form" :schema="newsSchema" :state="newsState" @submit="onSubmit" class="space-y-4">
|
||||
<UFormField label="新闻标题" name="title">
|
||||
<UInput v-model="newsState.title" placeholder="请输入新闻标题" class="w-full" />
|
||||
</UFormField>
|
||||
<UFormField label="新闻内容" name="contents">
|
||||
<UTextarea v-model="newsState.content" class="w-full" />
|
||||
<ClientOnly>
|
||||
<MdEditor v-model="newsState.content" />
|
||||
</ClientOnly>
|
||||
</UFormField>
|
||||
这里应该是要有一个 Markdown 编辑界面
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton label="Cancel" color="neutral" variant="subtle" @click="open = false" />
|
||||
<UButton label="Create" color="primary" variant="solid" type="submit" />
|
||||
</div>
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UButton label="Cancel" color="neutral" variant="subtle" @click="open = false" />
|
||||
<UButton label="Create" color="primary" variant="solid" @click="form.submit()" />
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormSubmitEvent } from '@nuxt/ui';
|
||||
import { MdEditor } from 'md-editor-v3';
|
||||
import * as z from 'zod'
|
||||
|
||||
const form = ref();
|
||||
const open = ref(false);
|
||||
|
||||
const newsSchema = z.object({
|
||||
|
||||
271
app/components/admin/manage/members/AddModal.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<UModal
|
||||
v-model:open="open"
|
||||
title="新增会员"
|
||||
description="新增一名会员信息"
|
||||
:ui="{
|
||||
body: 'overflow-y-auto p-6', // 防止溢出 + 内边距
|
||||
footer: 'justify-end',
|
||||
}"
|
||||
>
|
||||
<UButton label="新增会员" icon="mdi:account-plus" />
|
||||
|
||||
<template #body>
|
||||
<UForm
|
||||
ref="alumniForm"
|
||||
:schema="alumniSchema"
|
||||
:state="alumniState"
|
||||
@submit="onSubmit"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="two-col">
|
||||
<UFormField label="中文姓名" name="chineseName">
|
||||
<UInput
|
||||
v-model="alumniState.chineseName"
|
||||
placeholder="张三"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="英文姓名" name="englishName">
|
||||
<UInput
|
||||
v-model="alumniState.englishName"
|
||||
placeholder="John Doe"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UFormField label="身份证号码(支持旧 IC)" name="icNumber">
|
||||
<div class="flex items-center gap-2">
|
||||
<UCheckbox
|
||||
v-model="alumniState.icWithA"
|
||||
label="带 A?"
|
||||
class="whitespace-nowrap"
|
||||
/>
|
||||
<div class="relative w-full">
|
||||
<UInput
|
||||
v-model="alumniState.icNumber"
|
||||
placeholder="1234567 或 123456-78-9101"
|
||||
class="w-full pl-6"
|
||||
v-maska="{ mask: icNumberMask }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="three-col">
|
||||
<UFormField label="加入年份" name="joinedYear">
|
||||
<UInput
|
||||
v-model="alumniState.joinedYear"
|
||||
type="number"
|
||||
placeholder="例如:2015"
|
||||
:min="1986"
|
||||
:max="new Date().getFullYear()"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="收据" name="receiptNumber">
|
||||
<UInput
|
||||
v-model="alumniState.receiptNumber"
|
||||
placeholder="1234 或 8888/1234"
|
||||
v-maska="{ mask: ['####', '####/####'] }"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="收据类型" name="receiptType">
|
||||
<USelect
|
||||
v-model="alumniState.receiptType"
|
||||
:items="receiptType"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="three-col">
|
||||
<UFormField label="毕业 / 离校" name="educationStatus">
|
||||
<USelect
|
||||
v-model="alumniState.educationStatus"
|
||||
:items="educationStatus"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField
|
||||
:label="`${alumniState.educationStatus?.endsWith('graduated') ? '毕业' : alumniState.educationStatus?.endsWith('dropout') ? '离校' : '毕业 / 离校'}年份`"
|
||||
name="leaveYear"
|
||||
>
|
||||
<UInput
|
||||
v-model="alumniState.leaveYear"
|
||||
type="number"
|
||||
placeholder="例如: 1998"
|
||||
:min="1957"
|
||||
:max="new Date().getFullYear()"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<!-- 计算毕业届别 / 输入离校年级 -->
|
||||
<div
|
||||
v-if="
|
||||
alumniState.educationStatus == 'junior_graduated' ||
|
||||
alumniState.educationStatus == 'senior_graduated'
|
||||
"
|
||||
class="flex items-center h-full"
|
||||
>
|
||||
{{
|
||||
alumniState.educationStatus == "junior_graduated" ? "初" : "高"
|
||||
}}中第 {{ graduatedLevel }} 届
|
||||
</div>
|
||||
<UFormField
|
||||
v-else-if="
|
||||
alumniState.educationStatus == 'junior_dropout' ||
|
||||
alumniState.educationStatus == 'senior_dropout'
|
||||
"
|
||||
:label="`${alumniState.educationStatus == 'junior_dropout' ? '初' : '高'}中?年级离校`"
|
||||
name="dropout_level"
|
||||
>
|
||||
<UInput type="number" :min="1" :max="3" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<div class="two-col">
|
||||
<UFormField label="联系方式( WhatsApp 优先)" name="phone">
|
||||
<UInput
|
||||
v-model="alumniState.phoneNumber"
|
||||
placeholder="国外电话需要加上区号"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="电子邮箱" name="email">
|
||||
<UInput
|
||||
type="email"
|
||||
v-model="alumniState.email"
|
||||
placeholder="请输入您的电子邮箱"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UButton
|
||||
label="Cancel"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
@click="open = false"
|
||||
/>
|
||||
<UButton
|
||||
label="Create"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
@click="alumniForm.submit()"
|
||||
/>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { vMaska } from "maska/vue";
|
||||
import type { SelectItem, FormSubmitEvent } from "@nuxt/ui";
|
||||
import * as z from "zod";
|
||||
|
||||
// 根据是否带 A,动态调整 mask
|
||||
const icNumberMask = computed(() => {
|
||||
return alumniState.icWithA ? ["A#######"] : ["#######", "######-##-####"];
|
||||
});
|
||||
|
||||
const receiptType = ref<SelectItem[]>([
|
||||
{ label: "---请选择---", value: "unknown" },
|
||||
{ label: "表格", value: "form" },
|
||||
{ label: "收据", value: "receipt" },
|
||||
]);
|
||||
|
||||
const educationStatus = ref<SelectItem[]>([
|
||||
{ label: "---请选择---", value: "unknown" },
|
||||
{ label: "初中离校", value: "junior_dropout" },
|
||||
{ label: "初中毕业", value: "junior_graduated" },
|
||||
{ label: "高中离校", value: "senior_dropout" },
|
||||
{ label: "高中毕业", value: "senior_graduated" },
|
||||
]);
|
||||
|
||||
const graduatedLevel = computed<number | string>(() => {
|
||||
const leaveYear = Number(alumniState.leaveYear);
|
||||
switch (alumniState.educationStatus) {
|
||||
case "junior_graduated":
|
||||
return leaveYear > 1958 ? leaveYear - 1958 : "-";
|
||||
case "senior_graduated":
|
||||
return leaveYear > 1965 ? leaveYear - 1965 : "-";
|
||||
default:
|
||||
return "-";
|
||||
}
|
||||
});
|
||||
|
||||
const alumniForm = ref();
|
||||
const open = ref(false);
|
||||
|
||||
const alumniSchema = z.object({
|
||||
chineseName: z.string().min(2, "名字似乎太短了哦").max(4, "名字似乎太长了哦"),
|
||||
englishName: z.string().nullable(),
|
||||
icWithA: z.boolean(),
|
||||
icNumber: z
|
||||
.string()
|
||||
.min(7, "身份证号码最少得 7 位")
|
||||
.max(14, "身份证号码最多支持 12 位")
|
||||
.nullable(),
|
||||
joinedYear: z.number().nullable(),
|
||||
receiptNumber: z.string().nullable(),
|
||||
receiptType: z.enum(["unknown", "form", "receipt"]), // 要和上面的 receiptType 相匹配
|
||||
leaveYear: z.number().nullable(),
|
||||
educationStatus: z.enum([
|
||||
"unknown",
|
||||
"junior_dropout",
|
||||
"junior_graduated",
|
||||
"senior_dropout",
|
||||
"senior_graduated",
|
||||
]),
|
||||
dropout_level: z.string().min(1).max(1).nullable(),
|
||||
phoneNumber: z.string(),
|
||||
email: z.string().email().nullable(),
|
||||
});
|
||||
|
||||
type AlumniSchema = z.output<typeof alumniSchema>;
|
||||
|
||||
const alumniState = reactive<Partial<AlumniSchema>>({
|
||||
chineseName: undefined,
|
||||
englishName: null,
|
||||
icWithA: false,
|
||||
icNumber: null,
|
||||
joinedYear: new Date().getFullYear(),
|
||||
receiptNumber: null,
|
||||
receiptType: "unknown",
|
||||
leaveYear: null,
|
||||
educationStatus: "unknown",
|
||||
dropout_level: null,
|
||||
phoneNumber: undefined,
|
||||
email: null,
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const onSubmit = async (event: FormSubmitEvent<AlumniSchema>) => {
|
||||
console.table(event.data);
|
||||
toast.add({
|
||||
title: "成功",
|
||||
description: `${event.data.chineseName} 新闻稿已经撰写完毕`,
|
||||
color: "success",
|
||||
});
|
||||
open.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference 'tailwindcss';
|
||||
|
||||
.two-col {
|
||||
@apply grid grid-cols-2 gap-4;
|
||||
}
|
||||
|
||||
.three-col {
|
||||
@apply grid grid-cols-3 gap-4;
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +0,0 @@
|
||||
<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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 捐赠模块 -->
|
||||
<section id="donate" class="py-16 text-center bg-secondary">
|
||||
<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-primary text-white px-8 py-3 rounded-xl shadow hover:opacity-90">立即捐赠</a>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,32 +1,42 @@
|
||||
<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">
|
||||
<img :src="event.cover" :alt="event.title" class="rounded-xl" />
|
||||
<div class="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-primary text-white px-5 py-2 rounded-lg hover:opacity-90">阅读详情</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 活动模块 -->
|
||||
<UPageSection title="校友活动" class="bg-gray-100 dark:bg-slate-800">
|
||||
<UPageGrid>
|
||||
<div
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
class="bg-white dark:bg-slate-700 shadow rounded-xl"
|
||||
>
|
||||
<img
|
||||
:src="event.cover"
|
||||
:alt="event.title"
|
||||
class="w-full aspect-video object-cover rounded-t-xl"
|
||||
/>
|
||||
<div class="p-6">
|
||||
<h4 class="font-semibold text-lg mb-2">{{ event.title }}</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||
日期:{{ useChineseDateFormat(event.date) }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">地点:{{ event.location }}</p>
|
||||
<UButton
|
||||
label="阅读详情"
|
||||
:to="event.path"
|
||||
trailing-icon="mdi:book-open-blank-variant-outline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</UPageGrid>
|
||||
</UPageSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { data: events } = await useAsyncData('events', () =>
|
||||
queryCollection('events')
|
||||
const { data: events } = await useAsyncData("events", () =>
|
||||
queryCollection("events")
|
||||
.where("draft", "=", false)
|
||||
.order("date", "DESC")
|
||||
.limit(3)
|
||||
.all()
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style></style>
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<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 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>
|
||||
<p class="text-sm text-gray-500">{{ person.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<UPageSection title="名人堂">
|
||||
<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)"
|
||||
>
|
||||
<img
|
||||
:src="person.photo"
|
||||
:alt="person.name"
|
||||
class="w-40 rounded-full border-primary-400 border-4"
|
||||
/>
|
||||
<h4 class="text-lg font-bold">{{ person.name }}</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ person.title }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</UPageSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { data: persons } = await useAsyncData('hall-of-fames', () =>
|
||||
queryCollection('hallOfFames')
|
||||
.limit(4)
|
||||
.all()
|
||||
)
|
||||
const { data: persons } = await useAsyncData("hall-of-fames", () =>
|
||||
queryCollection("hallOfFames").limit(4).all()
|
||||
);
|
||||
|
||||
var router = useRouter()
|
||||
var router = useRouter();
|
||||
const jumpToPersonIntro = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
router.push(path);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style></style>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
76
app/composables/useCountries.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { countries, type Country } from "~/data/countries";
|
||||
|
||||
/**
|
||||
* 🌍 useCountries composable
|
||||
* 提供国家相关的搜索、过滤、分组、查找等功能
|
||||
*/
|
||||
export const useCountries = () => {
|
||||
/**
|
||||
* 获取全部国家
|
||||
*/
|
||||
const getAll = (): Country[] => countries;
|
||||
|
||||
/**
|
||||
* 按洲分组
|
||||
*/
|
||||
const groupedByContinent = computed(() => {
|
||||
const groups: Record<string, Country[]> = {};
|
||||
for (const c of countries) {
|
||||
const key = c.continent || "Unknown";
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(c);
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
/**
|
||||
* 按名称搜索(支持多语言字段)
|
||||
* @param query 搜索关键字
|
||||
*/
|
||||
const search = (query: string) => {
|
||||
if (!query) return countries;
|
||||
const q = query.toLowerCase();
|
||||
return countries.filter((c) =>
|
||||
Object.values(c.name).some((name) => name.toLowerCase().includes(q))
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据国家代码查找
|
||||
* @param code ISO Alpha-2 或 Alpha-3 代码
|
||||
*/
|
||||
const findByCode = (code: string) => {
|
||||
const upper = code.toUpperCase();
|
||||
return countries.find((c) => c.code === upper || c.iso3 === upper);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取特定语言的显示名称
|
||||
* @param country 国家对象
|
||||
* @param lang 语言代码(默认为 en)
|
||||
*/
|
||||
const getDisplayName = (
|
||||
country: Country,
|
||||
lang: keyof Country["name"] = "en"
|
||||
) => {
|
||||
return country.name?.[lang] || country.name.en;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据洲名筛选
|
||||
* @param continent 洲名(如 'Asia'、'Europe')
|
||||
*/
|
||||
const filterByContinent = (continent: string) => {
|
||||
const key = continent.trim().toLowerCase();
|
||||
return countries.filter((c) => c.continent.toLowerCase() === key);
|
||||
};
|
||||
|
||||
return {
|
||||
getAll,
|
||||
groupedByContinent,
|
||||
search,
|
||||
findByCode,
|
||||
filterByContinent,
|
||||
getDisplayName,
|
||||
};
|
||||
};
|
||||
@@ -3,18 +3,24 @@ import type { NavigationMenuItem } from "@nuxt/ui";
|
||||
export const useDashboardSidebarLinks = () => {
|
||||
const sidebarLinks = [
|
||||
[
|
||||
{
|
||||
label: "回到主站",
|
||||
icon: "mdi:home",
|
||||
type: "link",
|
||||
to: "/",
|
||||
target: "_blank"
|
||||
},
|
||||
{
|
||||
label: "仪表盘",
|
||||
icon: "mdi:view-dashboard",
|
||||
to: "/admin/dashboard",
|
||||
},
|
||||
{
|
||||
label: "信息管理",
|
||||
icon: "mdi:file-document-outline",
|
||||
defaultOpen: true,
|
||||
type: "trigger",
|
||||
children: [
|
||||
{
|
||||
label: "会员籍管理",
|
||||
icon: "mdi:account",
|
||||
to: "/admin/manage/members",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "内容管理",
|
||||
icon: "mdi:bookshelf",
|
||||
@@ -50,6 +56,15 @@ export const useDashboardSidebarLinks = () => {
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
[
|
||||
{
|
||||
label: "回到主站",
|
||||
icon: "mdi:home",
|
||||
type: "link",
|
||||
to: "/",
|
||||
target: "_blank",
|
||||
},
|
||||
],
|
||||
] satisfies NavigationMenuItem[][];
|
||||
|
||||
return { sidebarLinks };
|
||||
|
||||
3003
app/data/countries.ts
Normal file
271
app/error.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<script setup lang="ts">
|
||||
import type { NuxtError } from "#app";
|
||||
|
||||
// 定义扩展错误类型
|
||||
interface ExtendedError extends NuxtError {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{ error?: ExtendedError }>();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const status = computed(() => props.error?.statusCode ?? 500);
|
||||
const statusMessage = computed(() => props.error?.statusMessage ?? "");
|
||||
|
||||
const title = computed(() => {
|
||||
switch (status.value) {
|
||||
case 404:
|
||||
return "页面未找到";
|
||||
case 403:
|
||||
return "禁止访问";
|
||||
case 500:
|
||||
return "服务器内部错误";
|
||||
default:
|
||||
return `${status.value} 错误`;
|
||||
}
|
||||
});
|
||||
|
||||
useHead({ title: title.value });
|
||||
|
||||
const friendlyMessage = computed(() => {
|
||||
if (status.value === 404)
|
||||
return "抱歉,我们找不到您要访问的页面。它可能已被移动、删除或从未存在过。";
|
||||
if (status.value === 403)
|
||||
return "抱歉,您没有权限访问此页面。请检查您的账户权限或联系管理员。";
|
||||
if (status.value === 500)
|
||||
return "服务器出了点问题,我们正在努力修复。请稍后再试。";
|
||||
return props.error?.message ?? "发生了未知错误,我们的技术团队已经收到通知。";
|
||||
});
|
||||
|
||||
const goHome = () => router.push("/");
|
||||
const goBack = () => {
|
||||
if (import.meta.client && window.history.length > 1) router.back();
|
||||
else router.push("/");
|
||||
};
|
||||
const reloadPage = () => {
|
||||
if (import.meta.client) window.location.reload();
|
||||
};
|
||||
const contactSupport = () => {
|
||||
if (!import.meta.client) return;
|
||||
const subject = encodeURIComponent(
|
||||
`网站错误反馈 — ${title.value} — ${route.fullPath}`
|
||||
);
|
||||
const body = encodeURIComponent(`错误信息:
|
||||
Status: ${status.value} ${statusMessage.value}
|
||||
Path: ${route.fullPath}
|
||||
|
||||
请描述您在做什么以及复现步骤:`);
|
||||
// 请替换为真实的支持邮箱或工单链接
|
||||
window.location.href = `mailto:hello@example.com?subject=${subject}&body=${body}`;
|
||||
};
|
||||
|
||||
const isDev = import.meta.dev;
|
||||
|
||||
// 格式化错误信息
|
||||
const formattedDebugInfo = computed(() => {
|
||||
if (!props.error) return isDev ? "无错误对象传入。" : "错误详情已隐藏。";
|
||||
|
||||
if (isDev) {
|
||||
try {
|
||||
return {
|
||||
status: props.error.statusCode,
|
||||
message: props.error.message || props.error.statusMessage,
|
||||
url: props.error.url,
|
||||
stack: props.error.stack,
|
||||
};
|
||||
} catch (e) {
|
||||
return { error: String(props.error) };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: props.error.statusCode || "N/A",
|
||||
message: props.error.message || props.error.statusMessage || "N/A",
|
||||
};
|
||||
});
|
||||
|
||||
// 错误图标
|
||||
const errorIcon = computed(() => {
|
||||
switch (status.value) {
|
||||
case 404:
|
||||
return "mdi:magnify-close";
|
||||
case 403:
|
||||
return "mdi:lock-alert";
|
||||
case 500:
|
||||
return "mdi:server-off";
|
||||
default:
|
||||
return "mdi:alert-circle";
|
||||
}
|
||||
});
|
||||
|
||||
// 错误颜色
|
||||
const errorColor = computed(() => {
|
||||
switch (status.value) {
|
||||
case 404:
|
||||
return "#fb9e3a"; // 使用主题色
|
||||
case 403:
|
||||
return "#e74c3c";
|
||||
case 500:
|
||||
return "#c0392b";
|
||||
default:
|
||||
return "#fb9e3a"; // 使用主题色
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4"
|
||||
>
|
||||
<div
|
||||
class="max-w-3xl w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg overflow-hidden"
|
||||
>
|
||||
<!-- 顶部状态栏 -->
|
||||
<div class="h-2 w-full" :style="{ backgroundColor: errorColor }"></div>
|
||||
|
||||
<div class="p-8 md:p-10">
|
||||
<div class="flex flex-col md:flex-row items-center gap-8">
|
||||
<!-- 错误代码和图标 -->
|
||||
<div class="flex-none text-center">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="w-32 h-32 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
:style="{ backgroundColor: errorColor + '20' }"
|
||||
>
|
||||
<div class="text-4xl" :style="{ color: errorColor }">
|
||||
<span v-if="status === 404">404</span>
|
||||
<span v-else-if="status === 403">403</span>
|
||||
<span v-else-if="status === 500">500</span>
|
||||
<span v-else>{{ status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm text-gray-500 dark:text-gray-400 font-medium mt-2"
|
||||
>
|
||||
{{ statusMessage || title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息和操作 -->
|
||||
<div class="flex-1">
|
||||
<h1
|
||||
class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-3"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
|
||||
{{ friendlyMessage }}
|
||||
</p>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<button
|
||||
@click="goHome"
|
||||
class="px-5 py-2.5 rounded-md text-white font-medium transition-colors flex items-center gap-2"
|
||||
style="background-color: var(--color-primary-400)"
|
||||
>
|
||||
<span>返回首页</span>
|
||||
</button>
|
||||
<button
|
||||
@click="goBack"
|
||||
class="px-5 py-2.5 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
返回上页
|
||||
</button>
|
||||
<button
|
||||
@click="reloadPage"
|
||||
class="px-5 py-2.5 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 报告问题按钮 -->
|
||||
<div class="mb-6">
|
||||
<button
|
||||
@click="contactSupport"
|
||||
class="px-5 py-2.5 rounded-md bg-gray-800 dark:bg-gray-700 text-white font-medium hover:bg-gray-700 dark:hover:bg-gray-600 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span>报告问题</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||
如果这是由链接或书签导致的,请尝试返回或访问主页。
|
||||
</div>
|
||||
|
||||
<!-- 错误详情 -->
|
||||
<details
|
||||
class="border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden"
|
||||
>
|
||||
<summary
|
||||
class="cursor-pointer p-3 bg-gray-50 dark:bg-gray-900 font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
错误详情({{ isDev ? "开发模式" : "精简信息" }})
|
||||
</summary>
|
||||
<div
|
||||
class="p-4 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 overflow-auto max-h-64"
|
||||
>
|
||||
<div v-if="typeof formattedDebugInfo === 'object'">
|
||||
<div v-if="isDev" class="space-y-4">
|
||||
<div v-if="formattedDebugInfo.status">
|
||||
<strong>状态码:</strong> {{ formattedDebugInfo.status }}
|
||||
</div>
|
||||
<div v-if="formattedDebugInfo.message">
|
||||
<strong>消息:</strong> {{ formattedDebugInfo.message }}
|
||||
</div>
|
||||
<div v-if="formattedDebugInfo.url">
|
||||
<strong>URL:</strong> {{ formattedDebugInfo.url }}
|
||||
</div>
|
||||
<div v-if="formattedDebugInfo.stack" class="mt-3">
|
||||
<strong>堆栈跟踪:</strong>
|
||||
<pre
|
||||
class="mt-2 text-xs bg-gray-50 dark:bg-gray-900 p-3 rounded overflow-x-auto"
|
||||
>{{ formattedDebugInfo.stack }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="formattedDebugInfo.status">
|
||||
<strong>状态码:</strong> {{ formattedDebugInfo.status }}
|
||||
</div>
|
||||
<div v-if="formattedDebugInfo.message">
|
||||
<strong>消息:</strong> {{ formattedDebugInfo.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ formattedDebugInfo }}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式补充 */
|
||||
details[open] summary {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* 暗色模式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
details[open] summary {
|
||||
border-bottom-color: #4b5563;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 640px) {
|
||||
.text-4xl {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +1,214 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 导航栏 -->
|
||||
<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">
|
||||
<UPage>
|
||||
<UBanner
|
||||
title="永中校友会 40 周年庆典活动火热筹募中!"
|
||||
icon="hugeicons:party"
|
||||
close
|
||||
:actions="bannerActions"
|
||||
/>
|
||||
<UHeader>
|
||||
<template #left>
|
||||
<NuxtLink to="/">
|
||||
<div class="flex gap-4 items-center">
|
||||
<img class="inline h-12 w-auto" src="/Logo.svg" alt="YPHS Alumni" />
|
||||
<h1
|
||||
class="text-xl font-bold text-gray-900 hover:text-primary-400 hidden md:inline"
|
||||
>
|
||||
永平中学校友会
|
||||
</h1>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<UNavigationMenu :items="items" content-orientation="vertical" />
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<UColorModeButton />
|
||||
<UButton
|
||||
v-for="link in socialLinks"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
v-bind="link"
|
||||
/>
|
||||
<UButton
|
||||
icon="line-md:account-add"
|
||||
to="/join-us"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
label="加入"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UNavigationMenu
|
||||
:items="items"
|
||||
orientation="vertical"
|
||||
class="-mx-2.5"
|
||||
/>
|
||||
</template>
|
||||
</UHeader>
|
||||
|
||||
<UMain>
|
||||
<Transition name="page" mode="out-in">
|
||||
<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-primary">永平中学校友会</a>
|
||||
</h1>
|
||||
<NuxtPage />
|
||||
</div>
|
||||
<nav class="space-x-6 hidden md:flex items-center">
|
||||
<a href="#news" class="hover:text-primary">新闻</a>
|
||||
<a href="#events" class="hover:text-primary">活动</a>
|
||||
<a href="#donate" class="hover:text-primary">捐赠(未开放)</a>
|
||||
<a href="#about" class="hover:text-primary">关于</a>
|
||||
<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" />
|
||||
</Transition>
|
||||
</UMain>
|
||||
|
||||
<UFooter>
|
||||
<template #left>
|
||||
<p>© 2025 永平中学校友会. 保留所有权利.</p>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<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">2018 级毕业学长(麦祖奕)</span>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<slot />
|
||||
|
||||
<!-- 页脚 -->
|
||||
<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">2018 级毕业学长(麦祖奕)</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>
|
||||
</div>
|
||||
<template #right>
|
||||
<UButton
|
||||
v-for="link in socialLinks"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
v-bind="link"
|
||||
/>
|
||||
</template>
|
||||
</UFooter>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem, ButtonProps } from "@nuxt/ui";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const bannerActions = ref<ButtonProps[]>([
|
||||
{
|
||||
label: "查看策划书",
|
||||
variant: "outline",
|
||||
icon: "lucide:arrow-right",
|
||||
to: "/40th-anniversary/proposal",
|
||||
},
|
||||
]);
|
||||
|
||||
const items = computed<NavigationMenuItem[]>(() => [
|
||||
{ label: "首页", to: "/" },
|
||||
{ label: "新闻", to: "/news", active: route.path.startsWith("/news") },
|
||||
{
|
||||
label: "会员",
|
||||
to: "/members",
|
||||
active: route.path.startsWith("/members"),
|
||||
},
|
||||
{
|
||||
label: "活动",
|
||||
to: "/events",
|
||||
active: route.path.startsWith("/events"),
|
||||
children: [
|
||||
{
|
||||
label: "40 周年庆",
|
||||
to: "/40th-anniversary",
|
||||
active: route.path.startsWith("/40th-anniversary"),
|
||||
badge: {
|
||||
label: "Hot",
|
||||
color: "error",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "关于",
|
||||
to: "/about",
|
||||
active: route.path.startsWith("/about"),
|
||||
children: [
|
||||
{
|
||||
label: "创会简史",
|
||||
to: "/about/founded-history",
|
||||
active: route.path.startsWith("/about/founded-history"),
|
||||
icon: "mdi:history",
|
||||
},
|
||||
{
|
||||
label: "组织架构",
|
||||
to: "/about/org-structure/20",
|
||||
active: route.path.startsWith("/about/org-structure"),
|
||||
icon: "mdi:account-group",
|
||||
},
|
||||
{
|
||||
label: "特殊校友:中补班",
|
||||
description: "永平中学补习班(1956年):一封迟来的贴文",
|
||||
to: "/about/middle-highschool-tuition-class",
|
||||
active: route.path.startsWith("/about/middle-highschool-tuition-class"),
|
||||
icon: "mdi:mail",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "链接",
|
||||
children: [
|
||||
{
|
||||
label: "永平中学官网",
|
||||
icon: "mdi:book-education",
|
||||
to: "https://yphs.edu.my/",
|
||||
target: "_blank",
|
||||
},
|
||||
{
|
||||
label: "桃李教育网",
|
||||
icon: "mdi:web",
|
||||
to: "https://efuxi.vtour.my/",
|
||||
target: "_blank",
|
||||
},
|
||||
{
|
||||
label: "董总 Dong Zong",
|
||||
icon: "mdi:web",
|
||||
to: "https://www.dongzong.my/",
|
||||
target: "_blank",
|
||||
},
|
||||
{
|
||||
label: "永中校友网 - 旧站",
|
||||
icon: "mdi:web-clock",
|
||||
to: "https://vtour.my/yphsalumni/",
|
||||
target: "_blank",
|
||||
},
|
||||
],
|
||||
},
|
||||
// { label: "捐赠", to: "#donate", active: route.path.startsWith("#donate") },
|
||||
// { label: "关于", to: "#about", active: route.path.startsWith("#about") },
|
||||
]);
|
||||
|
||||
const socialLinks = ref<ButtonProps[]>([
|
||||
{
|
||||
icon: "line-md:tiktok",
|
||||
to: "https://www.tiktok.com/@yphs.alumni",
|
||||
target: "_blank",
|
||||
},
|
||||
{
|
||||
icon: "line-md:facebook",
|
||||
to: "https://www.facebook.com/YPHS.Alumni/",
|
||||
target: "_blank",
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style lang="css" scoped>
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: all 0.4s;
|
||||
}
|
||||
.page-enter-from,
|
||||
.page-leave-to {
|
||||
opacity: 0;
|
||||
filter: blur(1rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
19
app/pages/40th-anniversary.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<UContainer>
|
||||
<UDashboardToolbar>
|
||||
<UNavigationMenu :items="subPages" />
|
||||
</UDashboardToolbar>
|
||||
<NuxtPage />
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { NavigationMenuItem } from "@nuxt/ui";
|
||||
|
||||
const subPages = ref<NavigationMenuItem[]>([
|
||||
{ label: "庆祝四十载", to: "/40th-anniversary", exact: true },
|
||||
{ label: "策划案", to: "/40th-anniversary/proposal" },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot />
|
||||
40 周年庆纪念页
|
||||
</div>
|
||||
</template>
|
||||
|
||||
55
app/pages/40th-anniversary/proposal.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<template #default>
|
||||
<UStepper :items="proposalSteps" size="xs" disabled />
|
||||
<UPageHeader
|
||||
title="永平中学校友会 40 周年纪念册筹划"
|
||||
description="四十载薪火相传,情系永平,共创未来"
|
||||
/>
|
||||
|
||||
<UPageSection title="纪念册定位">
|
||||
<h3>目标受众</h3>
|
||||
<ul>
|
||||
<li>各届校友(老中青三代)</li>
|
||||
<li>教职员工与校董会</li>
|
||||
<li>在校学生与家长</li>
|
||||
<li>地区社会贤达 / 赞助商 / 友校代表</li>
|
||||
</ul>
|
||||
<h3>风格方向</h3>
|
||||
<p>庄重 × 情感 × 历史厚度 × 现代视觉感</p>
|
||||
<p>
|
||||
→ 类似大学纪念刊风格,不是单纯的活动册,而是一部 *时代见证作品*。
|
||||
</p>
|
||||
</UPageSection>
|
||||
|
||||
<UPageSection
|
||||
title="总体结构规划"
|
||||
description="建议页数:120~160 页"
|
||||
>
|
||||
</UPageSection>
|
||||
</template>
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { StepperItem } from "@nuxt/ui";
|
||||
|
||||
useSeoMeta({
|
||||
title: "40 周年纪念册筹划",
|
||||
});
|
||||
|
||||
const proposalSteps = ref<StepperItem[]>([
|
||||
{ title: "筹划期", description: "成立编辑组,确定风格、预算、印刷规格" },
|
||||
{ title: "资料收集期", description: "访谈、征文、收照片、整理档案" },
|
||||
{ title: "撰写与设计期", description: "文稿成稿、图片修复、初稿排版" },
|
||||
{ title: "审校与赞助期", description: "校对、内容确认、广告页洽谈" },
|
||||
{ title: "印刷准备期", description: "定稿送印、样书确认" },
|
||||
{ title: "发布期", description: "校庆活动同步发行、媒体推广、线上版本上线" },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
393
app/pages/about/founded-history.vue
Normal file
@@ -0,0 +1,393 @@
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<UPageHeader title="校友会创立简史" description="2017 年整理" />
|
||||
</UContainer>
|
||||
<UPageSection title="源起与经过">
|
||||
<p>
|
||||
一所学校的发展, 校友可扮演着重要的角色, 而校友会的成立,
|
||||
则能更有效的凝聚众人的力量, 回馈母校的培育恩情,
|
||||
永平中学建校三十多年, 培育了数以千计的莘莘学子,
|
||||
毕业校友遍布全国各地, 在文教界、工商界各领域均卓有成就。
|
||||
为凝聚校友情谊, 加强与母校的联系, 这一股力量开始在校友心中醖酿,
|
||||
盼着有校友会的成立, 期盼透过群体的力量, 共同协助母校的建设与发展,
|
||||
群策群力, 回馈母校。
|
||||
</p>
|
||||
<p>
|
||||
自七十年代始便有成立校友会的构想, 李文鍊校长也数次为此而南北奔波,
|
||||
号召校友发起组织, 可是因为种种原因而胎死腹中。
|
||||
直到一九八四年十月十日方正式产生了筹委会,
|
||||
成员有:刘响华、杨顺发、刘振昌、罗玉珍、黄保成、范志清、郑祥才、刘用周、许莉云、陈美英、刘建万、陈家发、陈仁芳、周国盛、余养耕、何民荣、陈崠甡、陈亚瑞。
|
||||
由于筹委会成立伊始, 百事待举, 惟以起草章程和招收会员为主。
|
||||
因此这一群热心的校友即展开工作, 访问友会收集资料,
|
||||
并通过郭进财先生协助申请注册工作。
|
||||
</p>
|
||||
<p>
|
||||
然而立意草创, 始业惟艰。 由于个人因素致使筹委会未能有效的协调与配合,
|
||||
因此进度缓慢。 鉴于早日落实校友会成立的意愿,
|
||||
众校友在一九八四年十二月十八日决定重组筹委会, 结果杨顺发荣任主席,
|
||||
副主席:范志清, 财政:王飞兴, 秘书:罗玉珍, 助理秘书:陈宋丽,
|
||||
委员:许志毅、黄保成、陈美英、郑祥才、刘建万、陈仁芳,
|
||||
查账:罗细妹、吴恒发。
|
||||
</p>
|
||||
<p>
|
||||
在彼等的热诚推动与互助之下, 各项工作顺利展开,
|
||||
一九八六年一月二十三日, 永中校友会终于获得社团注册官批准成立,
|
||||
同年三月九日即召开会员大会选出了第一届理事会, 这是历史性的一刻,
|
||||
在永中校友的心中写下辉煌灿烂的一页。 自此校友会立下了根基,
|
||||
藉此迈向新的里程碑, 为母语、母校、中华文化做出贡献。
|
||||
</p>
|
||||
<p>
|
||||
由于经费不足, 一直未能自置会所,
|
||||
直到一九八九年方与永平树胶公会合租永平种植合作社商业大厦四楼一单位,
|
||||
并获得名誉主席马文清报效装修费用, 才落实有一个“家”的愿望。
|
||||
</p>
|
||||
<p>
|
||||
至2010年校友会庆祝创会25周年, 在余深智学长及中马区学长陈亚龙,
|
||||
郑惠民, 吴恒灿, 陈杰民, 何宗荣等推动下,
|
||||
由余和安主席与刘建万筹委会主席及理事, 校友们的踊跃支持下,
|
||||
成功筹得46万零吉的会所基金(参见征信录附表),
|
||||
其中马文清学长捐献10万元。 2011年也成功购置现址之会所。
|
||||
并于2012年6月23日举办本会庆祝27周年庆会所启用仪式“623校友团圆日”暨第14届理事就职典礼晚宴,
|
||||
当晚也推介了永中校友全球服务网,
|
||||
让校友从网站获取更多有关母校发展的最新资讯。
|
||||
</p>
|
||||
</UPageSection>
|
||||
<UPageSection title="组织与结构">
|
||||
<p>
|
||||
自永中校友会成立之后, 依章程第四条拟定, 凡曾在永中就读,
|
||||
年龄达十八岁以上, 而现已离校,
|
||||
不在任何中学肆业之校友均可申请为会员;并规定每年四月底之前必须召开会员大会一次,
|
||||
每二年得举行改选, 产生理事会。 以下为各股职责:
|
||||
</p>
|
||||
<ul>
|
||||
<li>主席:为会员大会及理事会主持人, 执行会务及其他事项。</li>
|
||||
<li>
|
||||
秘书:执行会员大会及理事会依章程所达致的议决案的行政工作,
|
||||
处理及保管本会的一切文件。
|
||||
</li>
|
||||
<li>财政:负责会计, 出纳及一切财务事宜。</li>
|
||||
<li>总务:负责一切庶务事项。</li>
|
||||
<li>康乐股:负责一切育乐活动。</li>
|
||||
<li>文教股:负责学术, 出版等事项。</li>
|
||||
<li>福利股:谋求会员福利。</li>
|
||||
<li>会员局:收集及整理会员资料档案。</li>
|
||||
</ul>
|
||||
<p>
|
||||
本会常年规划举办各项健康文艺表演, 各种专题讲座等活动,
|
||||
让乡民增进知识、发扬优良的传统文化及提倡健康的社会风气。
|
||||
并举办了许多活动,
|
||||
包括“新春大团拜”、“校友回校日”、“卡拉ok歌唱比赛”、“小型旅游”,
|
||||
“庆生会”等, 都能获得热烈的响应。
|
||||
</p>
|
||||
<p>
|
||||
2015年5月份成立本会属下永平合唱团, 2017年成立本会属下永中校友排球组。
|
||||
</p>
|
||||
<p>
|
||||
为加强各界校友联系, 本会在Whatsapp成立了历届校友联系人群组,
|
||||
并架设<UTooltip
|
||||
text="该域名已经下线,现由当前网站进行代替,结合全新站点的上线,代代传承,生生不息!"
|
||||
:delay-duration="0"
|
||||
><span>永中校友网站(yphs-alumni.org)</span></UTooltip
|
||||
>及在面子书成立永中校友网(YongPeng
|
||||
Yphsalumni)以不时分享各项活动资讯。
|
||||
</p>
|
||||
<p>
|
||||
本会目前的会务稳健发展, 规模日宏, 会员人数不断增加。 作为华教先锋,
|
||||
本会始终立场鲜明, 坚持立会宗旨, 也配合董教总方针维护华教。
|
||||
本会向来与时并进, 走在时代尖端, 关注时局及各类社会问题,
|
||||
并提出针砭。
|
||||
</p>
|
||||
<p>
|
||||
历届理事们凭着一股强烈的使命感, 对校友会不离不弃,
|
||||
默默地为校友会付出, 贡献良多。 包括老中青三代的理事紧密结合,
|
||||
和谐共处如一家人。 在大家通力合作之下, 必将秉承一贯的光辉传统,
|
||||
谱下更美好的篇章。
|
||||
</p>
|
||||
</UPageSection>
|
||||
<UPageSection
|
||||
title="永平中学校友会二十五周年购置会所筹募基金征信录"
|
||||
description="(1986年-2010年)制成牌匾, 置挂会所"
|
||||
headline="2012年6月23日"
|
||||
>
|
||||
<div class="overflow-x-auto rounded-2xl shadow-lg">
|
||||
<table class="min-w-full border-collapse bg-white text-gray-800">
|
||||
<thead
|
||||
class="bg-gray-100 text-left text-sm font-semibold uppercase tracking-wide"
|
||||
>
|
||||
<tr>
|
||||
<th class="w-32 border-b border-gray-200 px-4 py-3">金额</th>
|
||||
<th class="border-b border-gray-200 px-4 py-3">捐献者名单</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 text-sm leading-relaxed">
|
||||
<tr
|
||||
v-for="(donor, idx) in donors"
|
||||
:key="donor.amount"
|
||||
class="hover:bg-gray-50"
|
||||
>
|
||||
<td
|
||||
:class="[
|
||||
'px-4',
|
||||
'py-2',
|
||||
idx < 2 ? 'font-bold text-primary-400' : '',
|
||||
]"
|
||||
>
|
||||
{{ donor.amount }}
|
||||
</td>
|
||||
<td class="px-4 py-2">{{ donor.peoples.join(", ") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>总筹得:马币四十六万元正</p>
|
||||
<div class="overflow-x-auto rounded-2xl shadow-lg mt-10">
|
||||
<table class="min-w-full border-collapse bg-white text-gray-800">
|
||||
<thead
|
||||
class="bg-gray-100 text-left text-sm font-semibold uppercase tracking-wide"
|
||||
>
|
||||
<tr>
|
||||
<th class="w-40 border-b border-gray-200 px-4 py-3">项目</th>
|
||||
<th class="border-b border-gray-200 px-4 py-3">详情</th>
|
||||
<th class="w-40 border-b border-gray-200 px-4 py-3">
|
||||
价值(马币)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 text-sm leading-relaxed">
|
||||
<tr
|
||||
class="hover:bg-gray-50"
|
||||
v-for="serviceDonor in serviceDonations"
|
||||
:key="serviceDonor.name"
|
||||
>
|
||||
<td class="px-4 py-2">{{ serviceDonor.name }}</td>
|
||||
<td class="px-4 py-2">{{ serviceDonor.item }}</td>
|
||||
<td class="px-4 py-2">
|
||||
{{ money.format(serviceDonor.amount) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-amber-50 font-semibold text-primary-400">
|
||||
<td class="px-4 py-2">总额</td>
|
||||
<td class="px-4 py-2">专业服务及装修项目</td>
|
||||
<td class="px-4 py-2">{{ money.format(33170) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</UPageSection>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
useSeoMeta({
|
||||
title: "校友会创立简史",
|
||||
ogTitle: "校友会创立简史",
|
||||
});
|
||||
|
||||
const money = new Intl.NumberFormat("zh-CN", {
|
||||
style: "currency",
|
||||
currency: "MYR",
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
const donors = ref<{ amount: string; peoples: string[] }[]>([
|
||||
{ amount: "十万元", peoples: ["马文清"] },
|
||||
{ amount: "五万元", peoples: ["余深智太平局绅"] },
|
||||
{ amount: "一万八千元", peoples: ["余和安"] },
|
||||
{ amount: "一万七千元", peoples: ["郑惠民"] },
|
||||
{ amount: "一万五千元", peoples: ["陈亚龙", "刘建万"] },
|
||||
{ amount: "一万二千元", peoples: ["吴恒灿", "郑惠顺"] },
|
||||
{
|
||||
amount: "一万元",
|
||||
peoples: [
|
||||
"YB拿督魏家祥博士",
|
||||
"刘文兴",
|
||||
"蓝群华",
|
||||
"陈月丽",
|
||||
"林俊伟",
|
||||
"黄振兴",
|
||||
],
|
||||
},
|
||||
{ amount: "六千元", peoples: ["盛添寿"] },
|
||||
{ amount: "五千五百元", peoples: ["何宗荣"] },
|
||||
{
|
||||
amount: "五千元",
|
||||
peoples: ["杨文松", "吴荣贵", "黄宗海", "余根业", "陈德福"],
|
||||
},
|
||||
{
|
||||
amount: "三千元",
|
||||
peoples: [
|
||||
"拿督张玉兴",
|
||||
"黄金斗",
|
||||
"王飞兴",
|
||||
"周隆发",
|
||||
"黄世纯",
|
||||
"陈正龙",
|
||||
"美术广告",
|
||||
"富华冷气酒家",
|
||||
],
|
||||
},
|
||||
{
|
||||
amount: "二千五百元",
|
||||
peoples: [
|
||||
"陈伟权",
|
||||
"余清安",
|
||||
"陈杰民",
|
||||
"陈芳龙",
|
||||
"许赐福",
|
||||
"林玻璃有限公司",
|
||||
],
|
||||
},
|
||||
{ amount: "两千元", peoples: ["周厚钿", "刘振昌", "刘用周"] },
|
||||
{ amount: "一千六百元", peoples: ["余东照"] },
|
||||
{ amount: "一千五百元", peoples: ["YB林其妹", "余新礼", "林建宝"] },
|
||||
{
|
||||
amount: "一千元",
|
||||
peoples: [
|
||||
"拿督黄仰白",
|
||||
"陈宋丽",
|
||||
"郑惠忠",
|
||||
"黄荣发",
|
||||
"江仁瑞",
|
||||
"杨惜平",
|
||||
"黄招兴",
|
||||
"林家祥",
|
||||
"杨顺发",
|
||||
"江义顺",
|
||||
"陈俊浩",
|
||||
"黄仰惠",
|
||||
"江先利",
|
||||
"吴福华",
|
||||
"余养耕",
|
||||
"范志清",
|
||||
"黎星堂",
|
||||
"邱财德",
|
||||
"邱祥春",
|
||||
"覃庆星",
|
||||
"陈秀彩",
|
||||
"张珠英",
|
||||
"吴恒发",
|
||||
"余赐喜",
|
||||
"林仁钦",
|
||||
"黄潮明",
|
||||
"谭丕光",
|
||||
"郑双",
|
||||
"林有兴",
|
||||
"颜丰樑",
|
||||
"周卓开",
|
||||
"江梅香",
|
||||
"谭信飞",
|
||||
"邬焕铭",
|
||||
"王振现",
|
||||
"汤孝森",
|
||||
"王辉恩",
|
||||
"许秀莉",
|
||||
"潘瑞平",
|
||||
"王启耕",
|
||||
"瞿军圣",
|
||||
"余养廉",
|
||||
"Yeak Nai Siew",
|
||||
"许永隆有限公司",
|
||||
"永平合作社",
|
||||
"永平留台同学会",
|
||||
],
|
||||
},
|
||||
{ amount: "八百元", peoples: ["余清洋"] },
|
||||
{
|
||||
amount: "五百元",
|
||||
peoples: [
|
||||
"黄楚茵",
|
||||
"余莉莉",
|
||||
"陈仁芳",
|
||||
"郑惠国",
|
||||
"王桂友",
|
||||
"赖致圣",
|
||||
"陈亚友",
|
||||
"陈成德",
|
||||
"阮文琼",
|
||||
"李万同",
|
||||
"刘素贞",
|
||||
"林宝益",
|
||||
"陈芳桂",
|
||||
"余明瑞",
|
||||
"郑金顺",
|
||||
"汤孝式",
|
||||
"林远来",
|
||||
"苏继仁",
|
||||
"张森錪",
|
||||
"盛永錥",
|
||||
"陈增华",
|
||||
"胡少菲",
|
||||
"邱亚泉",
|
||||
"王文发",
|
||||
"瞿夏莲",
|
||||
"林奕用",
|
||||
"王奕琪",
|
||||
"王亚勇",
|
||||
"林炳坤",
|
||||
"郑宇曙",
|
||||
"财兴铁厂",
|
||||
"萧茶餐室",
|
||||
],
|
||||
},
|
||||
{ amount: "四百元", peoples: ["福名氏"] },
|
||||
{
|
||||
amount: "三百元",
|
||||
peoples: [
|
||||
"许木春",
|
||||
"杜丕石",
|
||||
"张东升",
|
||||
"陈祖利",
|
||||
"余惠敏",
|
||||
"余光云",
|
||||
"余云志",
|
||||
"福州会馆",
|
||||
"张德满",
|
||||
"黄锦彪",
|
||||
"黄福顺",
|
||||
"吴云景",
|
||||
"黄奕洲",
|
||||
"林新桦",
|
||||
"廖国华",
|
||||
"黄金星",
|
||||
"和合汽车服务中心",
|
||||
"顺兴车厂",
|
||||
"闽南公会",
|
||||
"友爱慈善社",
|
||||
"林氏宗亲会",
|
||||
"通达柅轮",
|
||||
"和隆金铺",
|
||||
"万裕兴有限公司",
|
||||
"永平马来亚银行",
|
||||
"super yes computer",
|
||||
],
|
||||
},
|
||||
{
|
||||
amount: "二百元",
|
||||
peoples: [
|
||||
"罗联永",
|
||||
"王仕德",
|
||||
"黄祖严",
|
||||
"蔡晓芳",
|
||||
"黄美新",
|
||||
"胡述良",
|
||||
"萧月英",
|
||||
"蔡立义",
|
||||
"林瑞平",
|
||||
"张秀兰",
|
||||
],
|
||||
},
|
||||
{ amount: "一百元", peoples: ["林春英", "黄友和", "象棋公会", "永平歌友会"] },
|
||||
]);
|
||||
|
||||
const serviceDonations = ref<{ name: string; item: string; amount: number }[]>([
|
||||
{ name: "美珠", item: "报效地砖", amount: 25000 },
|
||||
{ name: "刘文兴", item: "报效漆料", amount: 3000 },
|
||||
{ name: "何世荣", item: "报效画作", amount: 3000 },
|
||||
{ name: "余云志律师", item: "报效律师费", amount: 2170 },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
61
app/pages/about/index.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<UPageHeader
|
||||
title="关于校友会"
|
||||
description="永平中学校友会成立的宗旨是连接全球校友,传承母校精神,支持在校学生成长。我们定期举办活动,推动交流与合作,并积极回馈母校。"
|
||||
/>
|
||||
|
||||
<UPageSection
|
||||
title="会徽"
|
||||
description="你了解校友会会徽的含义吗"
|
||||
:features="logoFeatures"
|
||||
orientation="horizontal"
|
||||
>
|
||||
<img class="size-88" src="/Logo.svg" alt="YPHS Alumni" />
|
||||
</UPageSection>
|
||||
|
||||
<UPageSection
|
||||
title="永平中学校歌"
|
||||
description="这首伴随着我们整个中学生涯的曲子,想必能勾起您不少的青春回忆吧!"
|
||||
orientation="horizontal"
|
||||
reverse
|
||||
>
|
||||
<div class="flex flex-col gap-y-8">
|
||||
<img class="w-full" src="/about/校歌.webp" alt="YPHS Alumni" />
|
||||
<div class="flex items-center gap-x-8">
|
||||
<p>伴奏版</p>
|
||||
<audio
|
||||
controls
|
||||
class="flex-1"
|
||||
src="/about/永平中学校歌.mp3"
|
||||
></audio>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-8">
|
||||
<p>咏唱版</p>
|
||||
<audio
|
||||
controls
|
||||
class="flex-1"
|
||||
src="/about/YongPing_SchoolSong_V2.mp3"
|
||||
></audio>
|
||||
</div>
|
||||
</div>
|
||||
</UPageSection>
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PageFeatureProps } from "@nuxt/ui";
|
||||
|
||||
const logoFeatures = ref<PageFeatureProps[]>([
|
||||
{ title: "九轮", description: "永久随时代齿轮前进" },
|
||||
{ title: "太阳九条线", description: "久久发出光芒照耀华友" },
|
||||
{ title: "三个菱形", description: "代表校友、董事会及社会人士团结一致" },
|
||||
{ title: "两道绿叶", description: "代表组织团结、不断茁壮成长" },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
146
app/pages/about/middle-highschool-tuition-class.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<UPage class="bg-gradient-to-br from-amber-50 to-amber-100 p-2 md:p-8">
|
||||
<UPageHero title="特殊校友" description="中补班 (1956年): 一封迟来的貼文" />
|
||||
<UContainer>
|
||||
<div class="flex justify-center items-center">
|
||||
<div
|
||||
class="p-4 md:p-12 text-justify relative max-w-2xl paper-texture bg-amber-50 shadow-md border border-amber-200 rounded-xl font-letter text-gray-800 leading-relaxed handwritten tracking-wide"
|
||||
>
|
||||
<UModal
|
||||
v-model:open="imagePopup"
|
||||
title="中补班乙班合影"
|
||||
description="永平学校中辅乙同学合影 - 一九五六年八月九日"
|
||||
fullscreen
|
||||
>
|
||||
<img
|
||||
src="/about/中补班乙.jpg"
|
||||
alt="中补班乙班"
|
||||
class="sm:absolute sm:-top-25 sm:-right-60 md:right-0 sm:rotate-12 md:w-60 lg:w-80 transition-all cursor-pointer"
|
||||
/>
|
||||
<template #body>
|
||||
<img
|
||||
src="/about/中补班乙.jpg"
|
||||
alt="中补班乙班"
|
||||
class="max-w-full max-h-full object-contain mx-auto"
|
||||
/>
|
||||
</template>
|
||||
</UModal>
|
||||
<div class="text-xl mb-2 font-bold">致:1956年永中中补班</div>
|
||||
<div class="text-lg mb-6 font-semibold">“诚” “毅”两班老校友:</div>
|
||||
|
||||
<p class="indent-8 mb-4">
|
||||
各位老同学:别来无恙?这句问候的话迟到了六十多年。想当年大家还是青涩年少,六十五年后的今天,如果还健在人间,想必齿危发秃,两鬓斑白,如果早已驾鹤西去,回顾一坯黄土,会否感慨此生:凡是尘土必归于尘土!
|
||||
</p>
|
||||
|
||||
<p class="indent-8 mb-4">
|
||||
但这个迟到的讯息,现在发出,将穿越65年的时间和空间,一定要,也一定会传到你们的心弦,引起共鸣,因为它共同的曲调是永中的校歌:黉舍巍峨著令名,时闻弦诵声。。。
|
||||
</p>
|
||||
|
||||
<p class="indent-8 mb-4">
|
||||
还健在的老同学,请响应这个号召,已经移民的,请魂兮归来,让我们老同学聚在一起,见证和欣赏生命天空最后的夕阳红,莫道桑榆晚,红霞尚满天!
|
||||
</p>
|
||||
|
||||
<p class="indent-8 mb-6">
|
||||
召集人:刘用周 电话:江回潮 电话:马文钦 电话:杨青发 电话:可以再提名
|
||||
</p>
|
||||
|
||||
<p class="text-right mt-8">2022年4月15日</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="my-4 text-center font-bold">
|
||||
中补班同学名册就記忆如下:马文钦提供
|
||||
</p>
|
||||
<UCard>
|
||||
<div class="space-x-4 space-y-2 text-center">
|
||||
<UBadge
|
||||
v-for="name in schoolMates"
|
||||
size="xl"
|
||||
variant="subtle"
|
||||
class="hover:scale-120 transition-all cursor-pointer"
|
||||
>{{ name }}</UBadge
|
||||
>
|
||||
</div>
|
||||
</UCard>
|
||||
<p class="my-4 text-center font-bold">
|
||||
黄绍楠校长,李华光等老师的墨宝赠言,尤为珍贵
|
||||
</p>
|
||||
<UCarousel
|
||||
v-slot="{ item }"
|
||||
:items="giveWords"
|
||||
class="w-full max-w-xs mx-auto"
|
||||
arrows
|
||||
dots
|
||||
>
|
||||
<img :src="item" width="320" height="320" class="rounded-lg" />
|
||||
</UCarousel>
|
||||
</UContainer>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
useSeoMeta({
|
||||
title: "致:1956年永中中补班 — 永平中学校友会",
|
||||
ogTitle: "致中补班的一封信",
|
||||
ogDescription:
|
||||
"致亲爱的1956年‘诚’、‘毅’两班老校友:虽迟六十余年,愿这条讯息穿越时间,召集大家重聚,共叙曾经校园情怀与夕阳红岁月。",
|
||||
ogUrl: "https://yphsalumni.org/about/middle-highschool-tuition-class/",
|
||||
ogType: "article",
|
||||
ogImage: "/about/中补班乙.jpg",
|
||||
ogImageAlt: "永平学校中辅乙同学合影",
|
||||
articlePublishedTime: "2022-04-15T00:00:00+08:00",
|
||||
author: "永平中学校友会",
|
||||
});
|
||||
|
||||
const imagePopup = ref<boolean>(false);
|
||||
|
||||
const schoolMates = ref<string[]>([
|
||||
"萧泽金",
|
||||
"黄荣发",
|
||||
"彭海恩",
|
||||
"掦楚玉",
|
||||
"揚楚英",
|
||||
"林进财",
|
||||
"马文钦",
|
||||
"陳玉英",
|
||||
"黄清蘭",
|
||||
"池伟青",
|
||||
"冯金才",
|
||||
"張波香",
|
||||
"陈红金",
|
||||
"黄鳳娇",
|
||||
"戴成雄",
|
||||
"黄光兴",
|
||||
"余新礼",
|
||||
"刘用周",
|
||||
"杨青发",
|
||||
"江回潮",
|
||||
]);
|
||||
|
||||
const giveWords = ref<string[]>([
|
||||
"/about/黄绍南校长赠言.jpg",
|
||||
"/about/李华光赠言.jpg",
|
||||
"/about/某老师赠言.jpg",
|
||||
"/about/赵老师赠言.jpg",
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* @import url("https://fonts.googleapis.com/css2?family=Great+Vibes&family=Noto+Serif+SC:wght@400;500&family=Zhi+Mang+Xing&display=swap"); */
|
||||
/* 自定义字体风格组合 */
|
||||
.font-letter {
|
||||
/* font-family: "Zhi Mang Xing", "Great Vibes", "Noto Serif SC", serif; */
|
||||
font-weight: 400;
|
||||
}
|
||||
/* 添加信纸背景纹理(可换 URL 为你自己的纹理图)*/
|
||||
.paper-texture {
|
||||
background-image: radial-gradient(
|
||||
rgba(255, 255, 255, 0.5) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 4px 4px;
|
||||
}
|
||||
/* 微微倾斜文字,模拟手写不规整 */
|
||||
.handwritten {
|
||||
transform: rotate(-0.3deg);
|
||||
}
|
||||
</style>
|
||||
16
app/pages/about/org-structure.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<UContainer>
|
||||
<UDashboardToolbar>
|
||||
<UNavigationMenu :items="subPages" />
|
||||
</UDashboardToolbar>
|
||||
<NuxtPage />
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { NavigationMenuItem } from "@nuxt/ui";
|
||||
|
||||
const subPages = ref<NavigationMenuItem[]>([{ label: "20 届", to: "/about/org-structure/20" }]);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
228
app/pages/about/org-structure/[slug].vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHeader
|
||||
:title="`第 ${generation} 届组织架构`"
|
||||
description="精英汇聚,携手共创卓越未来"
|
||||
/>
|
||||
<UPageBody>
|
||||
<UPageSection
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
:title="category"
|
||||
>
|
||||
<!-- <h2 class="text-3xl font-extrabold text-center mb-10">领导团队</h2> -->
|
||||
<UPageGrid class="!grid-cols-1 sm:!grid-cols-2 lg:!grid-cols-4">
|
||||
<UCard
|
||||
v-for="person in orgStructure.filter((p) => p.category == category)"
|
||||
:key="person.name"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="text-xl font-semibold mb-2">{{ person.position }}</h3>
|
||||
<div class="text-2xl font-bold">{{ person.name }}</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="text-center">
|
||||
<img
|
||||
class="h-40 w-auto object-contain inline rounded-md"
|
||||
:src="person.photo"
|
||||
:alt="person.name"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer
|
||||
><p class="text-muted">{{ person.description }}</p></template
|
||||
>
|
||||
</UCard>
|
||||
</UPageGrid>
|
||||
</UPageSection>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
useSeoMeta({
|
||||
title: "组织架构",
|
||||
description:
|
||||
"永平中学校友会组织架构,领导团队、职能部门与专项部门精英汇聚,携手共创卓越未来。",
|
||||
keywords:
|
||||
"永平中学校友会, 校友会组织架构, 领导团队, 职能部门, 专项部门, 理事会成员",
|
||||
ogTitle: "永平中学校友会组织架构",
|
||||
ogDescription:
|
||||
"了解永平中学校友会的领导团队、职能与专项部门,见证校友情谊与组织力量。",
|
||||
ogImage: "https://img.yphsalumni.org/i/2025/11/28/qq4k4z.png",
|
||||
ogType: "website",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const generation = route.params.slug;
|
||||
|
||||
const categories = ref(["领导团队", "职能部门", "专项部门"]);
|
||||
|
||||
// TODO: Fetch from api
|
||||
const orgStructure = ref([
|
||||
{
|
||||
name: "李煜斌",
|
||||
position: "主席",
|
||||
category: "领导团队",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qm2sjz.png",
|
||||
description:
|
||||
"全面领导与战略决策,统筹理事会整体工作,确保组织愿景与使命的实现。",
|
||||
},
|
||||
{
|
||||
name: "郑惠忠",
|
||||
position: "副主席",
|
||||
category: "领导团队",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qm3nod.png",
|
||||
description: "协助主席制定战略方向,分管外部关系与合作伙伴发展。",
|
||||
},
|
||||
{
|
||||
name: "陈月丽",
|
||||
position: "副主席",
|
||||
category: "领导团队",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qm41qp.png",
|
||||
description: "协助主席处理内部事务,分管人力资源与组织文化建设。",
|
||||
},
|
||||
{
|
||||
name: "余东照",
|
||||
position: "副主席",
|
||||
category: "领导团队",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qm4izj.png",
|
||||
description: "协助主席推动重点项目,分管创新业务与发展规划。",
|
||||
},
|
||||
{
|
||||
name: "蓝宜宏",
|
||||
position: "秘书",
|
||||
category: "职能部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qmrsai.png",
|
||||
description: "文件管理、会议组织、行政协调与日常事务处理。",
|
||||
},
|
||||
{
|
||||
name: "陈冠宇",
|
||||
position: "副秘书",
|
||||
category: "职能部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qms4el.png",
|
||||
description: "协助秘书处理文档、会议安排与行政支持工作。",
|
||||
},
|
||||
{
|
||||
name: "余粝栎",
|
||||
position: "财政",
|
||||
category: "职能部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qmsnhy.png",
|
||||
description: "全面财务管理、预算控制、资金运作与财务报告。",
|
||||
},
|
||||
{
|
||||
name: "曾国书",
|
||||
position: "副财政",
|
||||
category: "职能部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qmslmm.jpg",
|
||||
description: "协助财政处理账务、报销与日常财务管理工作。",
|
||||
},
|
||||
{
|
||||
name: "颜志宝",
|
||||
position: "总务",
|
||||
category: "职能部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qnfyb0.png",
|
||||
description: "物资采购、资产管理、场地协调与后勤保障服务。",
|
||||
},
|
||||
{
|
||||
name: "温敬富",
|
||||
position: "副总务",
|
||||
category: "职能部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qnglal.png",
|
||||
description: "协助总务处理物资管理、设备维护与后勤支持。",
|
||||
},
|
||||
{
|
||||
name: "胡少菲",
|
||||
position: "康乐",
|
||||
category: "职能部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qnre6r.png",
|
||||
description: "文化活动策划、康乐项目组织与会员联谊活动。",
|
||||
},
|
||||
{
|
||||
name: "林剑宝",
|
||||
position: "副康乐",
|
||||
category: "职能部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qns52y.png",
|
||||
description: "协助文康组织文体活动、兴趣小组与社交聚会。",
|
||||
},
|
||||
{
|
||||
name: "余美枫",
|
||||
position: "福利主任",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qodchd.png",
|
||||
description: "会员福利规划、实施与评估,提升会员满意度。",
|
||||
},
|
||||
{
|
||||
name: "郑惠国",
|
||||
position: "副福利主任",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qoe3ua.png",
|
||||
description: "协助会员福利规划、实施与评估,提升会员满意度。",
|
||||
},
|
||||
{
|
||||
name: "程靖原",
|
||||
position: "会员籍暨咨询管理",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qof326.png",
|
||||
description: "会员信息管理、咨询服务提供与会员关系维护。",
|
||||
},
|
||||
{
|
||||
name: "许福源",
|
||||
position: "副会员籍暨咨询管理",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qog4aa.png",
|
||||
description: "协助会员管理、咨询响应与信息系统维护。",
|
||||
},
|
||||
{
|
||||
name: "麦祖奕",
|
||||
position: "副会员籍暨咨询管理",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qoo8lx.png",
|
||||
description: "协助会员管理、咨询响应与信息系统开发。",
|
||||
},
|
||||
{
|
||||
name: "许浩铭",
|
||||
position: "奖励金主任",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qoos64.png",
|
||||
description: "奖励金制度设计、评审组织与发放管理。",
|
||||
},
|
||||
{
|
||||
name: "黄楚茵",
|
||||
position: "副奖励金主任",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qoxrim.png",
|
||||
description: "协助奖励金评审、资料审核与发放流程管理。",
|
||||
},
|
||||
{
|
||||
name: "陈廷添",
|
||||
position: "理事",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qoyjo8.png",
|
||||
description: "参与理事会决策、专项工作推进与建议提出。",
|
||||
},
|
||||
{
|
||||
name: "邱康勤",
|
||||
position: "理事",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qp8tni.png",
|
||||
description: "参与理事会决策、专项工作推进与建议提出。",
|
||||
},
|
||||
{
|
||||
name: "张键国",
|
||||
position: "稽查",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qp99ia.png",
|
||||
description: "内部审计、合规监督与风险控制管理。",
|
||||
},
|
||||
{
|
||||
name: "许皓杰",
|
||||
position: "稽查",
|
||||
category: "专项部门",
|
||||
photo: "https://img.yphsalumni.org/i/2025/11/28/qp9l0z.png",
|
||||
description: "内部审计、合规监督与风险控制管理。",
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -19,6 +19,7 @@ const pageTitle = "仪表盘"
|
||||
definePageMeta({
|
||||
layout: "admin-dashboard",
|
||||
title: pageTitle,
|
||||
alias: ['/admin']
|
||||
})
|
||||
useHead({
|
||||
title: pageTitle
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
Login Page
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
layout: "admin-login"
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
165
app/pages/admin/manage/members/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar :title="pageTitle" toggle>
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<AdminManageMembersAddModal />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
<template #body>
|
||||
<UTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="status === 'pending'"
|
||||
class="flex-1 h-80"
|
||||
/>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TableColumn } from "@nuxt/ui";
|
||||
import type { Row } from '@tanstack/vue-table'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const pageTitle = "会员籍管理";
|
||||
definePageMeta({
|
||||
layout: "admin-dashboard",
|
||||
title: pageTitle,
|
||||
});
|
||||
useHead({
|
||||
title: pageTitle,
|
||||
});
|
||||
|
||||
const UAvatar = resolveComponent("UAvatar");
|
||||
const UButton = resolveComponent("UButton");
|
||||
const UBadge = resolveComponent("UBadge");
|
||||
const UDropdownMenu = resolveComponent("UDropdownMenu");
|
||||
|
||||
const toast = useToast()
|
||||
const { copy } = useClipboard()
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: { src: string };
|
||||
company: { name: string };
|
||||
};
|
||||
|
||||
const { data, status } = await useFetch<User[]>(
|
||||
"https://jsonplaceholder.typicode.com/users",
|
||||
{
|
||||
key: "table-users",
|
||||
transform: (data) => {
|
||||
return (
|
||||
data?.map((user) => ({
|
||||
...user,
|
||||
avatar: {
|
||||
src: `https://i.pravatar.cc/120?img=${user.id}`,
|
||||
alt: `${user.name} avatar`,
|
||||
},
|
||||
})) || []
|
||||
);
|
||||
},
|
||||
lazy: true,
|
||||
}
|
||||
);
|
||||
|
||||
const columns: TableColumn<User>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
cell: ({ row }) => {
|
||||
return h("div", { class: "flex items-center gap-3" }, [
|
||||
h(UAvatar, {
|
||||
...row.original.avatar,
|
||||
size: "lg",
|
||||
}),
|
||||
h("div", undefined, [
|
||||
h("p", { class: "font-medium text-highlighted" }, row.original.name),
|
||||
h("p", { class: "" }, `@${row.original.username}`),
|
||||
]),
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
},
|
||||
{
|
||||
accessorKey: "company",
|
||||
header: "Company",
|
||||
cell: ({ row }) => row.original.company.name,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "text-right" },
|
||||
h(
|
||||
UDropdownMenu,
|
||||
{
|
||||
content: {
|
||||
align: "end",
|
||||
},
|
||||
items: getRowItems(row),
|
||||
"aria-label": "Actions dropdown",
|
||||
},
|
||||
() =>
|
||||
h(UButton, {
|
||||
icon: "i-lucide-ellipsis-vertical",
|
||||
color: "neutral",
|
||||
variant: "ghost",
|
||||
class: "ml-auto",
|
||||
"aria-label": "Actions dropdown",
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function getRowItems(row: Row<object>) {
|
||||
return [
|
||||
{
|
||||
type: 'label',
|
||||
label: 'Actions'
|
||||
},
|
||||
{
|
||||
label: 'Copy payment ID',
|
||||
onSelect() {
|
||||
copy(row.original.id)
|
||||
|
||||
toast.add({
|
||||
title: 'Payment ID copied to clipboard!',
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'View customer'
|
||||
},
|
||||
{
|
||||
label: 'View payment details'
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,24 +1,19 @@
|
||||
<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>
|
||||
|
||||
<div class="min-h-screen">
|
||||
<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="absolute inset-0 bg-linear-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="relative 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="h-1 bg-linear-to-r from-blue-400 via-purple-400 to-cyan-400"
|
||||
></div>
|
||||
|
||||
<div class="p-8 sm:p-10 lg:p-12">
|
||||
<!-- 内容渲染器 -->
|
||||
@@ -26,16 +21,32 @@
|
||||
<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>
|
||||
<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-700 text-lg font-medium">
|
||||
内容加载中...
|
||||
</p>
|
||||
<p class="text-gray-400 text-sm mt-2">请稍等片刻</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -50,16 +61,43 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const route = useRoute()
|
||||
const { data: event } = await useAsyncData('event-detail', () =>
|
||||
queryCollection('events')
|
||||
.path(`/events/${route.params.slug}`)
|
||||
.first()
|
||||
)
|
||||
const route = useRoute();
|
||||
const { data: event } = await useAsyncData("event-detail", () =>
|
||||
queryCollection("events").path(`/events/${route.params.slug}`).first()
|
||||
);
|
||||
|
||||
useHead({
|
||||
title: event.value?.title
|
||||
})
|
||||
if (event.value) {
|
||||
// 1. 确定图片:优先用 ogImage,没有就用 cover
|
||||
const shareImage = event.value.ogImage || event.value.cover;
|
||||
|
||||
// 2. 确定标题和描述:优先用 seoTitle,没有就用 title
|
||||
const shareTitle = event.value.seoTitle || event.value.title;
|
||||
const shareDesc = event.value.seoDescription || event.value.description;
|
||||
|
||||
// 3. 注入 SEO
|
||||
useSeoMeta({
|
||||
// 基础
|
||||
title: shareTitle,
|
||||
description: shareDesc,
|
||||
|
||||
// Open Graph (Facebook / WhatsApp)
|
||||
ogTitle: shareTitle,
|
||||
ogDescription: shareDesc,
|
||||
ogImage: shareImage,
|
||||
ogType: "article",
|
||||
|
||||
// Twitter Card
|
||||
twitterCard: "summary_large_image",
|
||||
twitterTitle: shareTitle,
|
||||
twitterDescription: shareDesc,
|
||||
twitterImage: shareImage,
|
||||
});
|
||||
|
||||
// 如果你用了 nuxt-og-image 模块生成动态图
|
||||
if (shareImage) {
|
||||
defineOgImage({ url: shareImage });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
34
app/pages/events/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<UChangelogVersions :versions="newsPost" />
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ChangelogVersionProps } from "@nuxt/ui";
|
||||
|
||||
const { data: events } = await useAsyncData("events", () =>
|
||||
queryCollection("events")
|
||||
.where("draft", "=", false)
|
||||
.order("date", "DESC")
|
||||
.limit(3)
|
||||
.all()
|
||||
);
|
||||
|
||||
// 将 news 数据转换成 UBlogPosts 可用格式
|
||||
const newsPost = computed<ChangelogVersionProps[]>(() =>
|
||||
(events.value || []).map((evt: any) => ({
|
||||
title: evt.title,
|
||||
description: evt.description,
|
||||
image: evt.cover,
|
||||
date: evt.date,
|
||||
to: evt.path, // ✅ 建议加路由跳转
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -1,16 +1,85 @@
|
||||
<template>
|
||||
<div>
|
||||
<IndexHero />
|
||||
<IndexNews />
|
||||
<UPage>
|
||||
<!-- Hero Banner -->
|
||||
<UPageHero
|
||||
:class="heroClass"
|
||||
:style="heroStyle"
|
||||
title="连接校友 · 传承精神"
|
||||
description="马来西亚柔佛永平中学校友会官方网站"
|
||||
:links="heroCta"
|
||||
/>
|
||||
|
||||
<!-- News Blog Posts -->
|
||||
<UPageSection title="新闻与公告">
|
||||
<UBlogPosts :posts="newsPost" />
|
||||
</UPageSection>
|
||||
|
||||
<IndexEvents />
|
||||
<IndexHallOfFame />
|
||||
<IndexDonate />
|
||||
<IndexAbout />
|
||||
</div>
|
||||
|
||||
<!-- 捐赠模块 -->
|
||||
<UPageCTA
|
||||
class="bg-secondary-200 dark:bg-secondary-900"
|
||||
title="支持与捐赠(功能暂未开放)"
|
||||
description="您的捐赠将用于奖学金、校园建设及校友活动发展。感谢您对母校的支持!"
|
||||
:links="donationLinks"
|
||||
/>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { BlogPostProps } from "@nuxt/ui";
|
||||
|
||||
useSeoMeta({
|
||||
title: "首页",
|
||||
});
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const heroClass = 'bg-cover bg-center'
|
||||
const heroStyle = computed(() => ({
|
||||
backgroundImage: colorMode.value === 'dark'
|
||||
? 'url("https://img.yphsalumni.org/i/2025/11/28/qzxrpq.png")'
|
||||
: 'url("https://img.yphsalumni.org/i/2025/11/28/qk9fe8.png")',
|
||||
backgroundPositionY: '-40px',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundColor: colorMode.value === 'dark'
|
||||
? 'rgba(0, 0, 0, 0.5)'
|
||||
: 'rgba(255, 255, 255, 0.5)',
|
||||
backgroundBlendMode: colorMode.value === 'dark' ? 'darken' : 'lighten'
|
||||
}))
|
||||
|
||||
const heroCta = ref([
|
||||
{
|
||||
label: "立即加入我们",
|
||||
to: "/join-us",
|
||||
icon: "mdi:account-plus",
|
||||
"data-umami-event": "Join Us Btn Click",
|
||||
},
|
||||
]);
|
||||
|
||||
// ========================================================
|
||||
// 新闻模块
|
||||
// ========================================================
|
||||
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 donationLinks = ref([{ label: "立即捐赠", icon: "mdi:cash" }]);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style></style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, defineComponent, h } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { ref, reactive, computed, defineComponent, h } from "vue";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import { vMaska } from "maska/vue";
|
||||
|
||||
// Reka primitive parts we actually need
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
RadioGroupRoot,
|
||||
RadioGroupItem,
|
||||
RadioGroupIndicator,
|
||||
} from 'reka-ui';
|
||||
} from "reka-ui";
|
||||
|
||||
/**
|
||||
* Local lightweight FormField wrapper:
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
* - renders: <Label for="..."> + slot(default) + error paragraph
|
||||
*/
|
||||
const FormField = defineComponent({
|
||||
name: 'FormField',
|
||||
name: "FormField",
|
||||
props: {
|
||||
label: { type: String, required: false },
|
||||
error: { type: String, required: false },
|
||||
@@ -28,13 +28,13 @@ const FormField = defineComponent({
|
||||
setup(props, { slots }) {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{ class: 'grid gap-2' },
|
||||
"div",
|
||||
{ class: "grid gap-2" },
|
||||
[
|
||||
props.label ? h(Label, { for: props.for }, () => props.label) : null,
|
||||
slots.default ? slots.default() : null,
|
||||
props.error
|
||||
? h('p', { class: 'text-sm text-red-600 mt-1' }, () => props.error)
|
||||
? h("p", { class: "text-sm text-red-600 mt-1" }, () => props.error)
|
||||
: null,
|
||||
].filter(Boolean)
|
||||
);
|
||||
@@ -45,30 +45,30 @@ const FormField = defineComponent({
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const form = reactive({
|
||||
chineseName: '',
|
||||
englishName: '',
|
||||
ic: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
chineseName: "",
|
||||
englishName: "",
|
||||
ic: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
gradYear: null as number | null,
|
||||
unknownGradYear: false,
|
||||
educationLevel: '',
|
||||
maritalStatus: '',
|
||||
country: '',
|
||||
address: '',
|
||||
educationLevel: "",
|
||||
maritalStatus: "",
|
||||
country: "",
|
||||
address: "",
|
||||
});
|
||||
|
||||
const errors = reactive<Record<string, string>>({});
|
||||
|
||||
const toUpperCaseEnglish = () => {
|
||||
form.englishName = (form.englishName || '').toUpperCase();
|
||||
form.englishName = (form.englishName || "").toUpperCase();
|
||||
};
|
||||
|
||||
const graduationBatch = computed(() => {
|
||||
if (form.gradYear) {
|
||||
if (form.educationLevel === '高中毕业') {
|
||||
if (form.educationLevel === "高中毕业") {
|
||||
return form.gradYear - 1965;
|
||||
} else if (form.educationLevel === '初中毕业') {
|
||||
} else if (form.educationLevel === "初中毕业") {
|
||||
return form.gradYear - 1958;
|
||||
}
|
||||
}
|
||||
@@ -76,25 +76,28 @@ const graduationBatch = computed(() => {
|
||||
});
|
||||
|
||||
const validate = () => {
|
||||
errors.chineseName = !form.chineseName ? '请输入中文姓名' : '';
|
||||
errors.englishName = !form.englishName ? '请输入英文姓名' : '';
|
||||
errors.ic = /^\d{6}-\d{2}-\d{4}$/.test(form.ic) ? '' : '格式应为 000000-00-0000';
|
||||
errors.chineseName = !form.chineseName ? "请输入中文姓名" : "";
|
||||
errors.englishName = !form.englishName ? "请输入英文姓名" : "";
|
||||
errors.ic = /^\d{6}-\d{2}-\d{4}$/.test(form.ic)
|
||||
? ""
|
||||
: "格式应为 000000-00-0000";
|
||||
errors.email =
|
||||
(!form.email && form.country !== '马来西亚')
|
||||
? '国外居住必须填写电邮'
|
||||
!form.email && form.country !== "马来西亚"
|
||||
? "国外居住必须填写电邮"
|
||||
: form.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)
|
||||
? '请输入有效的电邮地址'
|
||||
: '';
|
||||
? "请输入有效的电邮地址"
|
||||
: "";
|
||||
errors.phone =
|
||||
!/^01\d{1}-\d{7,8}$/.test(form.phone) && !/^\+\d{1,3}\s?\d+$/.test(form.phone)
|
||||
? '请输入马来西亚号 (01x-xxxxxxx) 或带区号的号码'
|
||||
: '';
|
||||
errors.educationLevel = !form.educationLevel ? '请选择毕业层次' : '';
|
||||
!/^01\d{1}-\d{7,8}$/.test(form.phone) &&
|
||||
!/^\+\d{1,3}\s?\d+$/.test(form.phone)
|
||||
? "请输入马来西亚号 (01x-xxxxxxx) 或带区号的号码"
|
||||
: "";
|
||||
errors.educationLevel = !form.educationLevel ? "请选择毕业层次" : "";
|
||||
errors.gradYear =
|
||||
!form.unknownGradYear && !form.gradYear ? '请输入毕业年份或勾选“不详”' : '';
|
||||
errors.maritalStatus = !form.maritalStatus ? '请选择婚姻状态' : '';
|
||||
errors.country = !form.country ? '请选择国家' : '';
|
||||
errors.address = !form.address ? '请输入详细地址' : '';
|
||||
!form.unknownGradYear && !form.gradYear ? "请输入毕业年份或勾选“不详”" : "";
|
||||
errors.maritalStatus = !form.maritalStatus ? "请选择婚姻状态" : "";
|
||||
errors.country = !form.country ? "请选择国家" : "";
|
||||
errors.address = !form.address ? "请输入详细地址" : "";
|
||||
|
||||
return Object.values(errors).every((e) => !e);
|
||||
};
|
||||
@@ -102,204 +105,254 @@ const validate = () => {
|
||||
const handleSubmit = () => {
|
||||
if (validate()) {
|
||||
// 如果你已在根组件挂载 Reka 的 ToastProvider + useToast,可替换下面 alert 的实现(见备注)
|
||||
alert('提交成功!理事会将尽快联系您。');
|
||||
alert("提交成功!理事会将尽快联系您。");
|
||||
} else {
|
||||
alert('请完善表单信息');
|
||||
alert("请完善表单信息");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cursor-not-allowed fixed flex items-center justify-center min-h-screen min-w-screen bg-black opacity-50">
|
||||
<p class="text-white text-2xl">此功能尚未开放,敬请期待。谢谢</p>
|
||||
</div>
|
||||
<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>
|
||||
<UPage class="bg-primary-400">
|
||||
<div
|
||||
class="cursor-not-allowed fixed flex items-center justify-center min-h-screen min-w-screen bg-black opacity-50"
|
||||
>
|
||||
<p class="text-white text-2xl">此功能尚未开放,敬请期待。谢谢</p>
|
||||
</div>
|
||||
<UPageBody>
|
||||
<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>
|
||||
<p class="text-sm text-gray-600 my-6 text-center leading-relaxed">
|
||||
兹申请加入成为永平中学校友会会员,愿遵守会规及议决案,并填此表为据。<br />
|
||||
入会费 <span class="font-bold text-secondary">RM60 / 年</span>。<br />
|
||||
填写此表格之后,会有理事联系您协商入会费事宜。
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<!-- 中文姓名 -->
|
||||
<FormField label="中文姓名" :error="errors.chineseName" for="chineseName">
|
||||
<input
|
||||
id="chineseName"
|
||||
v-model="form.chineseName"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="请输入中文姓名"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<!-- 英文姓名 -->
|
||||
<FormField label="英文姓名" :error="errors.englishName" for="englishName">
|
||||
<input
|
||||
id="englishName"
|
||||
v-model="form.englishName"
|
||||
@input="toUpperCaseEnglish"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="请输入英文姓名"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<!-- IC -->
|
||||
<FormField label="IC" :error="errors.ic" for="ic">
|
||||
<input
|
||||
id="ic"
|
||||
v-model="form.ic"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="000000-00-0000"
|
||||
v-maska="'######-##-####'"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<!-- 电邮 -->
|
||||
<FormField label="电邮" :error="errors.email" for="email">
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="选填 / 国外必填"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<!-- 电话 -->
|
||||
<FormField label="电话" :error="errors.phone" for="phone">
|
||||
<input
|
||||
id="phone"
|
||||
v-model="form.phone"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="请输入电话(WhatsApp 号码为佳)"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<!-- 毕业层次 (使用 Reka Radio primitives) -->
|
||||
<FormField label="毕业层次" :error="errors.educationLevel" for="educationLevel">
|
||||
<RadioGroupRoot
|
||||
v-model="form.educationLevel"
|
||||
class="flex flex-col gap-2"
|
||||
name="educationLevel"
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<!-- 中文姓名 -->
|
||||
<FormField
|
||||
label="中文姓名"
|
||||
:error="errors.chineseName"
|
||||
for="chineseName"
|
||||
>
|
||||
<RadioGroupItem value="初中毕业" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator class="w-4 h-4 rounded-full border flex items-center justify-center">
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>初中毕业</span>
|
||||
</RadioGroupItem>
|
||||
|
||||
<RadioGroupItem value="高中毕业" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator class="w-4 h-4 rounded-full border flex items-center justify-center">
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>高中毕业</span>
|
||||
</RadioGroupItem>
|
||||
|
||||
<RadioGroupItem value="辍学/转学肄业" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator class="w-4 h-4 rounded-full border flex items-center justify-center">
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>辍学/转学肄业</span>
|
||||
</RadioGroupItem>
|
||||
|
||||
<RadioGroupItem value="不确定" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator class="w-4 h-4 rounded-full border flex items-center justify-center">
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>不确定</span>
|
||||
</RadioGroupItem>
|
||||
</RadioGroupRoot>
|
||||
</FormField>
|
||||
|
||||
<!-- 毕业年份 -->
|
||||
<FormField label="毕业年份" :error="errors.gradYear" for="gradYear">
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
id="gradYear"
|
||||
type="number"
|
||||
v-model="form.gradYear"
|
||||
:min="1957"
|
||||
:max="currentYear"
|
||||
:disabled="form.unknownGradYear"
|
||||
class="w-32 border rounded px-3 py-2"
|
||||
id="chineseName"
|
||||
v-model="form.chineseName"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="请输入中文姓名"
|
||||
/>
|
||||
<label class="flex items-center gap-2 select-none">
|
||||
<CheckboxRoot v-model="form.unknownGradYear" class="w-5 h-5 rounded border flex items-center justify-center">
|
||||
<CheckboxIndicator class="flex items-center justify-center w-full h-full">
|
||||
<Icon icon="radix-icons:check" class="h-4 w-4 text-secondary" />
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
<span>毕业年份不详</span>
|
||||
</label>
|
||||
</FormField>
|
||||
|
||||
<span class="text-sm text-gray-500" v-if="graduationBatch">
|
||||
您是第 <span class="font-bold">{{ graduationBatch }}</span> 届毕业生
|
||||
</span>
|
||||
</div>
|
||||
</FormField>
|
||||
<!-- 英文姓名 -->
|
||||
<FormField
|
||||
label="英文姓名"
|
||||
:error="errors.englishName"
|
||||
for="englishName"
|
||||
>
|
||||
<input
|
||||
id="englishName"
|
||||
v-model="form.englishName"
|
||||
@input="toUpperCaseEnglish"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="请输入英文姓名"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<!-- 婚姻状态 -->
|
||||
<FormField label="婚姻状态" :error="errors.maritalStatus" for="maritalStatus">
|
||||
<div class="flex flex-col gap-2">
|
||||
<RadioGroupRoot v-model="form.maritalStatus" name="maritalStatus">
|
||||
<RadioGroupItem value="未婚" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator class="w-4 h-4 rounded-full border flex items-center justify-center">
|
||||
<!-- IC -->
|
||||
<FormField label="IC" :error="errors.ic" for="ic">
|
||||
<input
|
||||
id="ic"
|
||||
v-model="form.ic"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="000000-00-0000"
|
||||
v-maska="'######-##-####'"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<!-- 电邮 -->
|
||||
<FormField label="电邮" :error="errors.email" for="email">
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="选填 / 国外必填"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<!-- 电话 -->
|
||||
<FormField label="电话" :error="errors.phone" for="phone">
|
||||
<input
|
||||
id="phone"
|
||||
v-model="form.phone"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="请输入电话(WhatsApp 号码为佳)"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<!-- 毕业层次 (使用 Reka Radio primitives) -->
|
||||
<FormField
|
||||
label="毕业层次"
|
||||
:error="errors.educationLevel"
|
||||
for="educationLevel"
|
||||
>
|
||||
<RadioGroupRoot
|
||||
v-model="form.educationLevel"
|
||||
class="flex flex-col gap-2"
|
||||
name="educationLevel"
|
||||
>
|
||||
<RadioGroupItem value="初中毕业" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator
|
||||
class="w-4 h-4 rounded-full border flex items-center justify-center"
|
||||
>
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>未婚</span>
|
||||
<span>初中毕业</span>
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem value="已婚" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator class="w-4 h-4 rounded-full border flex items-center justify-center">
|
||||
|
||||
<RadioGroupItem value="高中毕业" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator
|
||||
class="w-4 h-4 rounded-full border flex items-center justify-center"
|
||||
>
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>已婚</span>
|
||||
<span>高中毕业</span>
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem value="其他" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator class="w-4 h-4 rounded-full border flex items-center justify-center">
|
||||
|
||||
<RadioGroupItem
|
||||
value="辍学/转学肄业"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<RadioGroupIndicator
|
||||
class="w-4 h-4 rounded-full border flex items-center justify-center"
|
||||
>
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>其他</span>
|
||||
<span>辍学/转学肄业</span>
|
||||
</RadioGroupItem>
|
||||
|
||||
<RadioGroupItem value="不确定" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator
|
||||
class="w-4 h-4 rounded-full border flex items-center justify-center"
|
||||
>
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>不确定</span>
|
||||
</RadioGroupItem>
|
||||
</RadioGroupRoot>
|
||||
</div>
|
||||
</FormField>
|
||||
</FormField>
|
||||
|
||||
<!-- 国家(原生 select,简单且稳定) -->
|
||||
<FormField label="国家" :error="errors.country" for="country">
|
||||
<select id="country" v-model="form.country" class="w-full border rounded px-3 py-2">
|
||||
<option value="" disabled>请选择国家</option>
|
||||
<option>马来西亚</option>
|
||||
<option>新加坡</option>
|
||||
<option>中国</option>
|
||||
<option>美国</option>
|
||||
<option>其他</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<!-- 毕业年份 -->
|
||||
<FormField label="毕业年份" :error="errors.gradYear" for="gradYear">
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
id="gradYear"
|
||||
type="number"
|
||||
v-model="form.gradYear"
|
||||
:min="1957"
|
||||
:max="currentYear"
|
||||
:disabled="form.unknownGradYear"
|
||||
class="w-32 border rounded px-3 py-2"
|
||||
/>
|
||||
<label class="flex items-center gap-2 select-none">
|
||||
<CheckboxRoot
|
||||
v-model="form.unknownGradYear"
|
||||
class="w-5 h-5 rounded border flex items-center justify-center"
|
||||
>
|
||||
<CheckboxIndicator
|
||||
class="flex items-center justify-center w-full h-full"
|
||||
>
|
||||
<Icon
|
||||
icon="radix-icons:check"
|
||||
class="h-4 w-4 text-secondary"
|
||||
/>
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
<span>毕业年份不详</span>
|
||||
</label>
|
||||
|
||||
<!-- 详细地址 -->
|
||||
<FormField label="详细地址" :error="errors.address" for="address">
|
||||
<textarea
|
||||
id="address"
|
||||
v-model="form.address"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="请输入现居详细地址"
|
||||
rows="4"
|
||||
/>
|
||||
</FormField>
|
||||
<span class="text-sm text-gray-500" v-if="graduationBatch">
|
||||
您是第
|
||||
<span class="font-bold">{{ graduationBatch }}</span> 届毕业生
|
||||
</span>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<div class="text-center mt-8">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-secondary text-white font-bold px-8 py-2 rounded-xl shadow hover:scale-105 transition"
|
||||
<!-- 婚姻状态 -->
|
||||
<FormField
|
||||
label="婚姻状态"
|
||||
:error="errors.maritalStatus"
|
||||
for="maritalStatus"
|
||||
>
|
||||
提交申请
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<RadioGroupRoot v-model="form.maritalStatus" name="maritalStatus">
|
||||
<RadioGroupItem value="未婚" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator
|
||||
class="w-4 h-4 rounded-full border flex items-center justify-center"
|
||||
>
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>未婚</span>
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem value="已婚" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator
|
||||
class="w-4 h-4 rounded-full border flex items-center justify-center"
|
||||
>
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>已婚</span>
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem value="其他" class="flex items-center gap-3">
|
||||
<RadioGroupIndicator
|
||||
class="w-4 h-4 rounded-full border flex items-center justify-center"
|
||||
>
|
||||
<span class="block w-2 h-2 rounded-full bg-secondary" />
|
||||
</RadioGroupIndicator>
|
||||
<span>其他</span>
|
||||
</RadioGroupItem>
|
||||
</RadioGroupRoot>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<!-- 国家(原生 select,简单且稳定) -->
|
||||
<FormField label="国家" :error="errors.country" for="country">
|
||||
<select
|
||||
id="country"
|
||||
v-model="form.country"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
>
|
||||
<option value="" disabled>请选择国家</option>
|
||||
<option>马来西亚</option>
|
||||
<option>新加坡</option>
|
||||
<option>中国</option>
|
||||
<option>美国</option>
|
||||
<option>其他</option>
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
<!-- 详细地址 -->
|
||||
<FormField label="详细地址" :error="errors.address" for="address">
|
||||
<textarea
|
||||
id="address"
|
||||
v-model="form.address"
|
||||
class="w-full border rounded px-3 py-2"
|
||||
placeholder="请输入现居详细地址"
|
||||
rows="4"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div class="text-center mt-8">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-secondary text-white font-bold px-8 py-2 rounded-xl shadow hover:scale-105 transition"
|
||||
>
|
||||
提交申请
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
116
app/pages/members/index.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<UPage>
|
||||
<UContainer>
|
||||
<UPageHeader title="会员总览" description="查询每个会员的信息" />
|
||||
<UPageBody>
|
||||
<UTable :data="members" :columns="columns" />
|
||||
</UPageBody>
|
||||
</UContainer>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TableColumn } from "@nuxt/ui";
|
||||
import { z } from "zod";
|
||||
|
||||
useSeoMeta({
|
||||
title: "会员总览",
|
||||
description:
|
||||
"永平中学校友会会员总览,查询每位校友的毕业年份、届别、加入年份与现居国家。",
|
||||
keywords: "永平中学校友会, 校友会员, 毕业届别, 会员名录, 校友查询",
|
||||
ogTitle: "永平中学校友会会员总览",
|
||||
ogDescription: "浏览永平中学校友会会员资料,了解各届校友的分布与加入年份。",
|
||||
// ogImage: "/members/ogImage.png",
|
||||
ogType: "website",
|
||||
});
|
||||
|
||||
const MemberSchema = z.object({
|
||||
memberId: z.string(),
|
||||
chineseName: z.string(),
|
||||
englishName: z.string(),
|
||||
// ic: z.string(),
|
||||
// mobile: z.string(),
|
||||
// home: z.string(),
|
||||
// email: z.string(),
|
||||
graduateLevel: z.string(),
|
||||
graduateYear: z.string(),
|
||||
// marriageNtatus: z.string(),
|
||||
livingCountry: z.string(),
|
||||
// addressLine1: z.string(),
|
||||
// addressLine2: z.string(),
|
||||
// addressLine3: z.string(),
|
||||
joinedYear: z.string(),
|
||||
// receiptNumber: z.string(),
|
||||
});
|
||||
|
||||
type Member = z.infer<typeof MemberSchema>;
|
||||
|
||||
const { data: members } = await useAsyncData("members", async () => {
|
||||
const file = await queryCollection("members").first();
|
||||
// ✅ 关键点:取 meta.body
|
||||
return MemberSchema.array().parse(file?.meta.body);
|
||||
});
|
||||
|
||||
const columns: TableColumn<Member>[] = [
|
||||
{
|
||||
accessorKey: "memberId",
|
||||
header: "会员编号",
|
||||
},
|
||||
{
|
||||
accessorKey: "chineseName",
|
||||
header: "中文姓名",
|
||||
},
|
||||
{
|
||||
accessorKey: "englishName",
|
||||
header: "英文姓名",
|
||||
},
|
||||
{
|
||||
accessorKey: "graduateYear",
|
||||
header: "毕业/离校年份",
|
||||
},
|
||||
{
|
||||
accessorKey: "graduateLevel",
|
||||
header: "毕业/离校届别",
|
||||
cell: ({ row }) => {
|
||||
switch (row.original.graduateLevel) {
|
||||
case "j":
|
||||
// 初中毕业
|
||||
// 如果 row.original.graduateYear 不能转换成数字,就写成初中毕业
|
||||
// 否则计算届别
|
||||
return isNaN(Number(row.original.graduateYear)) || row.original.graduateYear.trim() == ""
|
||||
? "初中毕业"
|
||||
: `初中第 ${Number(row.original.graduateYear) - 1958} 届`;
|
||||
case "s":
|
||||
// 高中毕业
|
||||
return isNaN(Number(row.original.graduateYear)) || row.original.graduateYear.trim() == ""
|
||||
? "高中毕业"
|
||||
: `高中第 ${Number(row.original.graduateYear) - 1965} 届`;
|
||||
case "dj1":
|
||||
return "初一肆业";
|
||||
case "dj2":
|
||||
return "初二肆业";
|
||||
case "dj3":
|
||||
return "初三肆业";
|
||||
case "ds1":
|
||||
return "高一肆业";
|
||||
case "ds2":
|
||||
return "高二肆业";
|
||||
case "ds3":
|
||||
return "高三肆业";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "joinedYear",
|
||||
header: "加入年份",
|
||||
},
|
||||
{
|
||||
accessorKey: "livingCountry",
|
||||
header: "现居国家",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -18,16 +18,43 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const route = useRoute()
|
||||
const { data: n } = await useAsyncData('new-detail', () =>
|
||||
queryCollection('news')
|
||||
.path(`/news/${route.params.slug}`)
|
||||
.first()
|
||||
)
|
||||
const route = useRoute();
|
||||
const { data: n } = await useAsyncData("new-detail", () =>
|
||||
queryCollection("news").path(`/news/${route.params.slug}`).first()
|
||||
);
|
||||
|
||||
useHead({
|
||||
title: n.value?.title
|
||||
})
|
||||
if (n.value) {
|
||||
// 1. 确定图片:优先用 ogImage,没有就用 cover
|
||||
const shareImage = n.value.ogImage || n.value.cover;
|
||||
|
||||
// 2. 确定标题和描述:优先用 seoTitle,没有就用 title
|
||||
const shareTitle = n.value.seoTitle || n.value.title;
|
||||
const shareDesc = n.value.seoDescription || n.value.description;
|
||||
|
||||
// 3. 注入 SEO
|
||||
useSeoMeta({
|
||||
// 基础
|
||||
title: shareTitle,
|
||||
description: shareDesc,
|
||||
|
||||
// Open Graph (Facebook / WhatsApp)
|
||||
ogTitle: shareTitle,
|
||||
ogDescription: shareDesc,
|
||||
ogImage: shareImage,
|
||||
ogType: "article",
|
||||
|
||||
// Twitter Card
|
||||
twitterCard: "summary_large_image",
|
||||
twitterTitle: shareTitle,
|
||||
twitterDescription: shareDesc,
|
||||
twitterImage: shareImage,
|
||||
});
|
||||
|
||||
// 如果你用了 nuxt-og-image 模块生成动态图
|
||||
if (shareImage) {
|
||||
defineOgImage({ url: shareImage });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style></style>
|
||||
|
||||
30
app/pages/news/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<UChangelogVersions :versions="newsPost" />
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ChangelogVersionProps } from "@nuxt/ui";
|
||||
|
||||
const { data: news } = await useAsyncData("news", () =>
|
||||
queryCollection("news").order("date", "DESC").all()
|
||||
);
|
||||
|
||||
// 将 news 数据转换成 UBlogPosts 可用格式
|
||||
const newsPost = computed<ChangelogVersionProps[]>(() =>
|
||||
(news.value || []).map((n: any) => ({
|
||||
title: n.title,
|
||||
description: n.description,
|
||||
image: n.cover,
|
||||
date: n.date,
|
||||
to: n.path, // ✅ 建议加路由跳转
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
6
app/plugins/md-editor.client.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { MdEditor } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.component('MdEditor', MdEditor)
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
|
||||
import { asSeoCollection } from "@nuxtjs/seo/content";
|
||||
|
||||
export default defineContentConfig({
|
||||
collections: {
|
||||
@@ -12,31 +13,35 @@ export default defineContentConfig({
|
||||
date: z.coerce.date(),
|
||||
location: z.string(),
|
||||
cover: z.string().url(),
|
||||
draft: z.boolean().optional().default(false),
|
||||
}),
|
||||
}),
|
||||
// 新闻集合
|
||||
news: defineCollection({
|
||||
type: "page",
|
||||
source: "news/*.md",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
date: z.coerce.date(),
|
||||
updated: z.coerce.date().optional(),
|
||||
author: z.string(),
|
||||
description: z.string(),
|
||||
cover: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
category: z.enum(["活动", "通知", "招聘", "博客"]).optional(),
|
||||
highlight: z.boolean().optional(),
|
||||
seoTitle: z.string().optional(),
|
||||
seoDescription: z.string().optional(),
|
||||
ogImage: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
news: defineCollection(
|
||||
asSeoCollection({
|
||||
type: "page",
|
||||
source: "news/*.md",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
date: z.coerce.date(),
|
||||
updated: z.coerce.date().optional(),
|
||||
author: z.string(),
|
||||
description: z.string(),
|
||||
cover: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
category: z.enum(["活动", "通知", "招聘", "博客"]).optional(),
|
||||
highlight: z.boolean().optional(),
|
||||
seoTitle: z.string().optional(),
|
||||
seoDescription: z.string().optional(),
|
||||
ogImage: z.string().optional(),
|
||||
draft: z.boolean().optional().default(false),
|
||||
}),
|
||||
})
|
||||
),
|
||||
// 名人堂
|
||||
hallOfFames: defineCollection({
|
||||
type: "page",
|
||||
source: "hall-of-fames/*md",
|
||||
source: "hall-of-fames/*.md",
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
photo: z.string().url(),
|
||||
@@ -45,5 +50,28 @@ export default defineContentConfig({
|
||||
gallery: z.array(z.string()),
|
||||
}),
|
||||
}),
|
||||
// 会员名册
|
||||
members: defineCollection({
|
||||
type: "data",
|
||||
source: "members/members.csv",
|
||||
schema: z.object({
|
||||
// id: z.number(),
|
||||
// chinese_name: z.string(),
|
||||
// english_name: z.string(),
|
||||
// ic: z.string(),
|
||||
// mobile: z.string(),
|
||||
// home: z.string(),
|
||||
// email: z.string(),
|
||||
// graduate_level: z.string(),
|
||||
// graduate_year: z.string(),
|
||||
// marriage_status: z.string(),
|
||||
// living_country: z.string(),
|
||||
// address_line_1: z.string(),
|
||||
// address_line_2: z.string(),
|
||||
// address_line_3: z.string(),
|
||||
// joined_year: z.string(),
|
||||
// receipt_number: z.string(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,16 +3,16 @@ title: "927 永中 • 钟意你"
|
||||
subtitle: "永中校友会 39周年庆"
|
||||
date: "2025-09-27"
|
||||
location: "永平中学校园"
|
||||
cover: "/events/20250927-return-to-school/event-photo-1.jpg"
|
||||
cover: "https://img.yphsalumni.org/i/2025/11/28/qr1l7k.jpg"
|
||||
---
|
||||
# 永中校友会39周年会庆午宴 温馨欢聚350人
|
||||
|
||||

|
||||

|
||||
来宾打卡照(星洲日报记者,邹智敏摄)
|
||||
|
||||
永平中学校友会于 9 月 27 日举办“927 永中·钟意你”39 周年会庆午宴,吸引约 350 位校友与社会贤达齐聚一堂,共度温馨时光,场面热闹盛大。
|
||||
|
||||

|
||||

|
||||
与会嘉宾向永中校友会献上祝福,左起:许敏捷、王飞兴、林添顺、李煜斌、刘镇东、蓝宜宏、傅庆隆、张嘉群及郑凯聪(星洲日报记者,邹智敏摄)
|
||||
|
||||
本次会庆出席嘉宾包括:
|
||||
@@ -42,6 +42,31 @@ cover: "/events/20250927-return-to-school/event-photo-1.jpg"
|
||||
|
||||
午宴当天,永中二十四节令鼓队与舞蹈社学员带来精彩表演,“永中之星 1.0”歌唱赛校友组冠军 **黄秋慧** 与学生组冠军 **林妤桐** 亦倾情献唱。现场欢声笑语不断,气氛热烈温馨,为会庆增添了浓厚的节日色彩。
|
||||
|
||||
## 午宴 • 流程表
|
||||
|
||||
| 时间 | 流程 |
|
||||
| ------- | ----------------------------------------------------------------------- |
|
||||
| 12.30pm | 二十四节令鼓迎宾/校友交流 |
|
||||
| 01.00pm | 仪式开始 |
|
||||
| 01.02pm | 唱国歌州歌 |
|
||||
| 01.05pm | 大会主席李煜斌致欢迎词 |
|
||||
| 01.10pm | 大会开幕嘉宾-投资贸易及工业部副部长暨伊斯干达公主城国会议员YB刘镇东致词 |
|
||||
| | 赠送纪念品给予大会开幕嘉宾 |
|
||||
| 01.20pm | 大会荣誉嘉宾-亚依淡区国会议员拿督斯里YB魏家祥博士工程师致辞 |
|
||||
| | 赠送纪念品给予大会荣誉嘉宾 |
|
||||
| 01.25pm | 大会荣誉嘉宾-柔佛州行政议员暨永平区州议员 YB 林添顺致词 |
|
||||
| | 赠送纪念品给予大会荣誉嘉宾 |
|
||||
| 01.40pm | 永平中学-舞蹈团表演 |
|
||||
| 01.50pm | 永平中学张嘉群校长致词 |
|
||||
| 01.55pm | 切生日蛋糕 |
|
||||
| 02.00pm | 播放上午校友回校活动 |
|
||||
| 02.05pm | 永中之星1.0冠军得主表演(校友组-黄秋慧,及在籍学生组-林妤桐) |
|
||||
| 02.35pm | 工委会主席蓝宜宏学长致谢词 |
|
||||
| 02.40pm | 永中校友会理事会带领唱校歌 |
|
||||
| 02.45pm | 欢送嘉宾离席 |
|
||||
|
||||
我们 40 周年庆再会!感谢大家的莅临!感恩!
|
||||
|
||||
---
|
||||
|
||||
<iframe
|
||||
|
||||
81
content/events/20251009-roulian-xiaoyouhui-20th.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "柔联校友会 20 届理事就职典礼"
|
||||
subtitle: "中学生写作比赛颁奖"
|
||||
date: "2025-10-09"
|
||||
location: "富华冷气酒家 2 楼"
|
||||
cover: "https://img.yphsalumni.org/i/2025/11/28/qryfft.jpg"
|
||||
---
|
||||
# 柔联校友会 20 届理事就职典礼
|
||||
|
||||

|
||||
|
||||
柔佛州华校校友联合会日前举办第 20 届理事就职典礼与 2025 年 “林赛花教育基金” 柔佛州中学生现场写作比赛颁奖典礼,由马来西亚华校校友会联合总会会长萧成兴担任监誓人。
|
||||
|
||||
联合会主席陈星和在致词时强调,华教事业的发展离不开团结与传承,新一届理事会将继续致力于推动华文教育,弘扬中华文化。
|
||||
|
||||
## 柔佛州华校校友会联合会
|
||||
|
||||
### 第20届理事会
|
||||
|
||||
| 职位 | 姓名 |
|
||||
| ---------- | ---------------------------------------------- |
|
||||
| 主席 | 陈星和 |
|
||||
| 副主席 | 陈月丽、陈重存、颜青积 |
|
||||
| 总务 | 李秀琴 |
|
||||
| 副总务 | 李煜斌 |
|
||||
| 财政 | 陈保妤 |
|
||||
| 副财政 | 吴沺成 |
|
||||
| 文书 | 莫文豪 |
|
||||
| 副文书 | 林道民 |
|
||||
| 教育主任 | 姚理介 |
|
||||
| 副教育主任 | 陈成祖 |
|
||||
| 文娱主任 | 郑臻董 |
|
||||
| 副文娱主任 | 梁德荣 |
|
||||
| 联络主任 | 郑伟亮 |
|
||||
| 副联络主任 | 郑清华 |
|
||||
| 查账 | 涂馨尹、谢祥庆 |
|
||||
| 理事 | 何国光、吕哲明、罗升隆、黄如龙、郑明裕、温维华 |
|
||||
|
||||
---
|
||||
|
||||
赞助金移交仪式上,妙妙机构执行董事荘坡政将赞助金移交给峇株文艺协会,以支持文艺出版与推广工作,随后颁发奖状与奖金给 26 名获奖学生。
|
||||
|
||||
## 🏆 2025年“林賽花教育基金”柔佛州中學生現場寫作比賽得獎名單
|
||||
|
||||
### 🥇 初中组
|
||||
|
||||
| 奖项 | 姓名 | 学校 |
|
||||
| ------ | ------ | ------------------ |
|
||||
| 特优奖 | 彭艺元 | 居銮中华中学 |
|
||||
| 特优奖 | 黄守蒽 | 居銮中华中学 |
|
||||
| 特优奖 | 方乐颖 | 居銮中华中学 |
|
||||
| 优秀奖 | 余姿亿 | 永平中学 |
|
||||
| 优秀奖 | 廖玮乐 | 永平中学 |
|
||||
| 优秀奖 | 卓婧琳 | 峇株吧辖华仁中学 |
|
||||
| 优秀奖 | 叶贯均 | 峇株吧辖华仁中学 |
|
||||
| 优秀奖 | 董清清 | 峇株吧辖华仁中学 |
|
||||
| 优秀奖 | 林婉仪 | 峇株吧辖华仁中学 |
|
||||
| 优秀奖 | 陈惠敏 | 峇株吧辖华仁中学 |
|
||||
| 优秀奖 | 曾愉峻 | 居銮中华中学 |
|
||||
| 优秀奖 | 曾佳滢 | 利丰港培华独立中学 |
|
||||
| 优秀奖 | 陆颖霏 | 新山宽柔中学 |
|
||||
|
||||
---
|
||||
|
||||
### 🥈 高中组
|
||||
|
||||
| 奖项 | 姓名 | 学校 |
|
||||
| ------ | ------ | ---------------- |
|
||||
| 特优奖 | 王祺齐 | 永平中学 |
|
||||
| 特优奖 | 汤滢菲 | 居銮中华中学 |
|
||||
| 特优奖 | 陈思宇 | 居銮中华中学 |
|
||||
| 优秀奖 | 黄婉芯 | 拿督国中 |
|
||||
| 优秀奖 | 林欣慧 | 拿督国中 |
|
||||
| 优秀奖 | 蔡欣艳 | 拿督国中 |
|
||||
| 优秀奖 | 许恩芮 | 永平中学 |
|
||||
| 优秀奖 | 陈佳萱 | 宽柔中学古来分校 |
|
||||
| 优秀奖 | 苏新致 | 永平中学 |
|
||||
| 优秀奖 | 刘祈悦 | 峇株吧辖华仁中学 |
|
||||
| 优秀奖 | 林淳希 | 峇株吧辖华仁中学 |
|
||||
| 优秀奖 | 黄子宸 | 新山宽柔中学 |
|
||||
| 优秀奖 | 苏祐萱 | 新山宽柔中学 |
|
||||
113
content/events/20251115-graduation-ceremony-60.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: "永平中学第 60 届毕业典礼"
|
||||
subtitle: "暨初中第 67 届毕业典礼"
|
||||
date: "2025-11-15"
|
||||
location: "永平中学邱廉书礼堂"
|
||||
cover: "https://img.yphsalumni.org/i/2025/11/27/st6hzt.jpg"
|
||||
draft: false
|
||||
|
||||
# SEO 主要字段
|
||||
description: "永平中学高中第 60 届与初中第 67 届毕业典礼圆满举行,师生与家长齐聚邱廉书礼堂,共同见证学子的重要里程碑,并向荣休教师刘连升老师致以诚挚祝福。"
|
||||
keywords:
|
||||
- 永平中学
|
||||
- 毕业典礼
|
||||
- 高中第60届
|
||||
- 初中第67届
|
||||
- 校园活动
|
||||
- 刘连升老师
|
||||
- 永中校友会
|
||||
|
||||
# --- SEO 专用字段 (对应你的 Zod Schema) ---
|
||||
|
||||
# 对应 schema: seoTitle
|
||||
# 如果不填,代码里会默认使用 title
|
||||
seoTitle: "永平中学第 60 届毕业典礼|初中第 67 届毕业典礼"
|
||||
|
||||
# 对应 schema: seoDescription
|
||||
# 如果不填,代码里会默认使用 description
|
||||
seoDescription: "2025 年永平中学毕业典礼隆重举行,包含师长致辞、奖学金颁发、荣休老师欢送,以及学生精彩演出等精彩环节。"
|
||||
|
||||
# 对应 schema: ogImage
|
||||
# 只有当你想要分享的图片和封面图不一样时才填,否则代码里会默认用 cover
|
||||
ogImage: "https://img.yphsalumni.org/i/2025/11/27/st6hzt.jpg"
|
||||
---
|
||||
|
||||
# 永平中学高中第 60 届、初中第 67 届毕业典礼圆满举行
|
||||
|
||||

|
||||
|
||||
2025 年 11 月 15 日,永平中学邱廉书礼堂见证了一个充满祝福与感动的重要时刻:高中第 60 届与初中第 67 届毕业典礼隆重举行。礼堂内座无虚席,毕业生、家长、师长与嘉宾齐聚一堂,共同庆祝学子们学习旅程的重要里程碑。
|
||||
|
||||
经过六年的努力与成长,高三学生正式迎来人生的新阶段;初三毕业生亦迈向更高层次的挑战。典礼在庄严的国州歌声中启幕,掀开充满意义的一天。
|
||||
|
||||
## 董事长马彣清博士致词(由代表宣读)
|
||||
|
||||

|
||||
|
||||
因公务繁忙未克出席,马彣清董事长由董事会秘书长黄仰力董事代表致辞。他转达董事长的寄语:“成功从来不是侥幸,而是毅力不断累积的成果。” 同时肯定毕业生在求学路上的坚持,并勉励他们继续以积极态度面对未来。
|
||||
|
||||
## 张嘉群校长三项叮咛:以品格与眼界开创未来
|
||||
|
||||

|
||||
|
||||
张嘉群校长在致辞中寄语毕业生,并提出三项叮咛,期许他们在往后的道路上继续发光发热:
|
||||
|
||||
### 叮咛一:永怀初心
|
||||
|
||||
* 保持对学习的好奇与热情
|
||||
* 无论未来走得多远,都不要忘记自己最初的信念
|
||||
|
||||
### 叮咛二:中正致远
|
||||
|
||||
* 既要正直谦和,也要坚守原则
|
||||
* 不只追求速度,更追求稳健与长远
|
||||
|
||||
### 叮咛三:站在巨人肩膀上成为巨人
|
||||
|
||||
* 以董事、父母与师长为榜样
|
||||
* 持续成长,努力成为对国家、社会与家庭有贡献的人才
|
||||
|
||||
## 柔佛州董联会主席兼校务顾问陈大锦先生致辞
|
||||
|
||||

|
||||
|
||||
陈大锦先生分享了他访问上海中医药大学的见闻,特别是该校完善的奖助学金制度。他希望本校辅导处与升学资讯处能协助学生把握机会,踊跃申请明年 9 月开课的相关奖学金,为升学之路开拓更多可能。
|
||||
|
||||
## 欢送刘连升老师光荣荣休
|
||||
|
||||

|
||||
|
||||
在典礼上,校方特别举行了“欢送刘连升老师荣休”环节,为这位服务永平中学 35 年、深受师生爱戴的资深教师献上诚挚祝福。
|
||||
|
||||
刘连升老师在任职期间,以其和蔼可亲的性格与渊博的历史知识,陪伴无数学生走过求学阶段。他不仅课堂上“聊古论今”,从古代帝王先烈谈到世界变迁,更在课余时与学生畅谈时事,从传统文化一路聊到新兴科技与 AI 技术,是学生眼中既严谨又风趣的良师益友。
|
||||
|
||||
许多毕业生回忆,与刘老师的关系早已超越师生,成为无话不谈的朋友。他的教学风格温和细腻,待人真诚,使无数学子在成长路上深受启发。
|
||||
|
||||
典礼现场气氛温馨感人,全体与会者向刘老师献上热烈掌声,感谢他 35 年来在校园里的付出与贡献。校方也祝愿刘老师退休生活悠然自得、健康安康,继续以他的人生智慧照亮身边的人。
|
||||
|
||||
## 颁发董事长设立的奖学金
|
||||
|
||||
典礼上同步进行董事长设立的各项奖学金颁发仪式,以表扬在学业、操行与综合表现上有卓越成绩的学生。
|
||||
(详见[图集](https://drive.google.com/drive/folders/1n3aRxxBwn-k6jzAOogV2ChBHKDA0c0qG))
|
||||
|
||||
## 特别演出:永中之星冠军林妤桐献唱
|
||||
|
||||

|
||||
|
||||
在典礼进入高潮之前,校方安排了一场温馨惊喜表演 —— 第一届“永中之星”冠军、来自初三诚的林妤桐学妹献唱两首歌曲:《Forever Young》以及《凤凰花开的路口》。她以清亮而富感染力的歌声,向高三学长姐献上最真挚的祝福,也为典礼增添一份青春与深情的色彩。
|
||||
|
||||
## 高三教师团队呈献毕业回忆视频
|
||||
|
||||
随后,由高三各班班导师亲自策划的毕业回忆视频在礼堂大屏幕播出。画面记录了学生们在校园中的点点滴滴,从努力备考到日常生活,从欢笑到奋斗,皆在此刻化为珍贵回忆。
|
||||
|
||||
校方希望学生们在未来面对困难与挫折时,也能记起这份勇敢、乐观与纯真,继续向前迈进。
|
||||
|
||||
## 歌声中道别,在掌声中启航
|
||||
|
||||

|
||||
|
||||
典礼尾声,高三毕业生深情演唱《再见》,而初三与高三学生共同合唱《毕业歌》,歌声回荡礼堂,为今年的毕业典礼写下温馨句点。现场气氛在歌声中变得格外动人,许多毕业生眼眶泛红,纷纷表示将铭记这一刻。
|
||||
|
||||
上午 11 时正,典礼圆满结束。为感谢师生与来宾的参与,马彣清董事长特别准备了供 600 人享用的自助餐,设宴于礼堂楼下。现场气氛融洽温馨。同时,本校友会也成功招收 14 名新会员,为未来的发展注入新的力量。
|
||||
|
||||
[2025 年毕业典礼图集](https://drive.google.com/drive/folders/1n3aRxxBwn-k6jzAOogV2ChBHKDA0c0qG)
|
||||
1
content/members/members-sample.csv
Normal file
@@ -0,0 +1 @@
|
||||
memberId,chineseName,englishName,ic,mobile,home,email,graduateLevel,,graduateYear,marriageStatus,livingCountry,addressLine1,addressLine2,addressLine3,joinedYear,receiptNumber
|
||||
|
@@ -4,13 +4,13 @@ date: "2025-10-01"
|
||||
updated: "2025-10-01"
|
||||
author: "麦祖奕学长"
|
||||
description: "永中校友会官网正式上线,为校友提供最新资讯、活动报名及互动交流平台。"
|
||||
cover: "/news/20251001-official-web-launch/Screenshot.png"
|
||||
cover: "https://img.yphsalumni.org/i/2025/11/28/qqc3ft.png"
|
||||
tags: ["活动", "公告", "产品更新"]
|
||||
category: "通知"
|
||||
highlight: true
|
||||
seoTitle: "永中校友会官网上线 | 最新活动与资讯平台"
|
||||
seoDescription: "永中校友会官网正式上线,校友可在平台获取最新资讯、报名活动及参与互动。"
|
||||
ogImage: "/images/og/news-launch.jpg"
|
||||
ogImage: "https://img.yphsalumni.org/i/2025/11/28/qqc3ft.png"
|
||||
---
|
||||
永中校友会官网正式上线啦!🎉
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "永平中学校友会与校友联络处的区别"
|
||||
date: "2025-11-28"
|
||||
updated: "2025-11-28"
|
||||
author: "永平中学校友会"
|
||||
description: "毕业校友常说“我都加入校友群组了”,实则误入了校友联络处。本文厘清其与永平中学校友会的区别与使命。"
|
||||
cover: "https://img.yphsalumni.org/i/2025/11/28/sbk8ue.jpeg"
|
||||
tags: ["永平中学", "校友会", "校友联络处", "区别", "公告"]
|
||||
category: "公告"
|
||||
highlight: true
|
||||
seoTitle: "永平中学校友会与校友联络处的区别 | 官方说明"
|
||||
seoDescription: "毕业校友常说“我都加入校友群组了”,实则误入了校友联络处。本文厘清其与永平中学校友会的区别与使命。"
|
||||
ogImage: "https://img.yphsalumni.org/i/2025/11/28/sbk8ue.jpeg"
|
||||
---
|
||||
|
||||
**永平中学校友会致广大校友的一封信**
|
||||
|
||||
亲爱的永平中学校友们:
|
||||
|
||||
在近期的毕业典礼上,我们欣喜地听到许多新校友分享“我已加入校友群组”的喜悦。这份对母校的深切情谊,令我们深深感动。
|
||||
|
||||
我们同时也注意到,部分校友可能产生了一个美丽的误会:大家加入的“校友联络处”群组,是由学校董事会设立的官方联络平台,主要负责校友信息的收集与传达。而我们所熟知的“永平中学校友会”,则是一个由历届毕业生**自发创建、独立运作的校友社区组织**。
|
||||
|
||||
自成立以来,永平中学校友会始终秉承以下宗旨,由校友共建、为校友服务:
|
||||
|
||||
- **联结校友情谊**:搭建纯粹、活跃的交流平台,让友谊跨越届别。
|
||||
- **支持母校发展**:凝聚校友力量,回馈我们共同的永平中学。
|
||||
- **推动教育、文化与社区活动**:举办丰富多彩的活动,丰富校友生活。
|
||||
- **建立校友与在校生之间的桥梁**:传承经验与关怀,助力学弟学妹成长。
|
||||
|
||||
校友联络处与校友会,二者服务校友的初心一致,是相辅相成的伙伴。我们由衷欢迎每一位热心校友加入我们这个充满活力的毕业生社区,共同为母校的发展注入来自校友群体的独特力量!
|
||||
|
||||
**加入我们,共同书写校友故事:**
|
||||
|
||||
校友会官方平台:https://yphsalumni.org/
|
||||
|
||||
<!-- 联系我们:[请在此处填入邮箱或具体负责人联系方式] -->
|
||||
|
||||
永平中学校友会<br/>
|
||||
2025年11月28日
|
||||
54
docs/20251128-misunderstanding-yphsalumni-and-alumnigroup.md
Normal file
@@ -0,0 +1,54 @@
|
||||
下面给你一个**实用、可直接交给设计师落地的公告封面设计方案**,基于你们「校友会 vs 校友联络处」容易混淆这一情境,重点是**清晰、正式、易识别、避免再次误会**。
|
||||
|
||||
---
|
||||
|
||||
# 🎨 公告封面设计方案(适用于官网首页 / Banner / WhatsApp 分享图)
|
||||
|
||||
## **1. 视觉主题**
|
||||
|
||||
**关键词:清晰区隔、正式感、组织可信度**
|
||||
色调保持学校主色系(如#fb9e3a、#fcef91),并加上一条亮色做强调,提升官方感。
|
||||
|
||||
---
|
||||
|
||||
## **2. 版式构成**
|
||||
|
||||
### ### **A. 顶部:醒目的组织身份**
|
||||
|
||||
**「永平中学校友会」**
|
||||
|
||||
* 使用学校和校友会 logo(左) + 标题文字(右)
|
||||
* 字体使用较厚重、带权威感的 Serif / Sans Serif
|
||||
* 加上小字「官方公告」标签
|
||||
|
||||
---
|
||||
|
||||
### **B. 中间:公告主标题(视觉核心)**
|
||||
|
||||
标题建议直接点出“澄清 & 提醒”的意图:
|
||||
|
||||
* **《校友会与校友联络处群组区分公告》**
|
||||
|
||||
字体加粗,居中,字号大,确保在手机上也能一眼看懂。
|
||||
|
||||
---
|
||||
|
||||
### **C. 配图 / 图标建议**
|
||||
|
||||
#### **徽章背景淡化**
|
||||
|
||||
校徽做 20% 透明大背景,体现官方权威。
|
||||
|
||||
---
|
||||
|
||||
## **3. 尺寸 & 格式(设计师直接可用)**
|
||||
|
||||
* Website banner: **1920×1080**
|
||||
|
||||
---
|
||||
|
||||
## **4. 文案建议(印在封面下方的小字)**
|
||||
|
||||
> “近期部分校友误将校友联络处群组误认为校友会官方群组,特此澄清。”
|
||||
|
||||
---
|
||||
148
docs/40 周年纪念册策划案.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 40 周年纪念册策划案
|
||||
|
||||
## 🏛 一、纪念册定位
|
||||
|
||||
**主题定位**:
|
||||
|
||||
> 「四十载薪火相传,情系永平,共创未来」
|
||||
|
||||
**目标受众**:
|
||||
|
||||
* 各届校友(老中青三代)
|
||||
* 教职员工与校董会
|
||||
* 在校学生与家长
|
||||
* 地区社会贤达 / 赞助商 / 友校代表
|
||||
|
||||
**风格方向**:
|
||||
庄重 × 情感 × 历史厚度 × 现代视觉感
|
||||
→ 类似大学纪念刊风格,不是单纯的活动册,而是一部 *时代见证作品*。
|
||||
|
||||
---
|
||||
|
||||
## 📘 二、总体结构规划(建议页数:120~160 页)
|
||||
|
||||
### **封面**
|
||||
|
||||
* 主视觉设计:校徽 × 火焰 / 时光流线 / 数字“40”标志
|
||||
* 副标题:“1986—2026 四十周年纪念册”
|
||||
* 封底:赞助单位 + 出版声明 + QR码(导向线上影像纪录)
|
||||
|
||||
---
|
||||
|
||||
### **前序部分(约 10 页)**
|
||||
|
||||
1. **题词与献词**
|
||||
|
||||
* 校友会主席致辞
|
||||
* 校长致辞
|
||||
* 董事长致辞
|
||||
* 特邀嘉宾寄语(如地方议员 / 教育界代表)
|
||||
|
||||
2. **编委会名单**
|
||||
|
||||
* 筹委会 / 编辑组 / 摄影 / 设计 / 出版等名单
|
||||
|
||||
3. **大事记总览**
|
||||
|
||||
* 用年表形式列出 1986–2026 的重要节点
|
||||
* 例如:创会、历届理事更替、重大活动、奖学金设立、母校建设捐助等
|
||||
|
||||
---
|
||||
|
||||
### **第一章|溯源篇:起点与精神(约 20 页)**
|
||||
|
||||
* 成立背景(1980s 马来西亚华教环境)
|
||||
* 永平中学历史简介
|
||||
* 校友会成立缘起、第一届理事成员介绍
|
||||
* “校友情”的精神与口号传承
|
||||
* 早期活动影像、报纸剪报、珍贵照片扫描件
|
||||
|
||||
---
|
||||
|
||||
### **第二章|成长篇:四十年的足迹(约 40 页)**
|
||||
|
||||
分阶段叙述,每 10 年为一章:
|
||||
|
||||
* **1986–1996**:创业维艰期
|
||||
* **1997–2006**:稳定扩展期
|
||||
* **2007–2016**:数码化与社会联结期
|
||||
* **2017–2026**:复兴与再创辉煌
|
||||
|
||||
每个阶段包含:
|
||||
|
||||
* 代表性活动介绍(如校庆、义演、体育赛、募捐项目等)
|
||||
* 重要人物访谈(理事长 / 校友代表)
|
||||
* 历史照片 & 当年文宣再现
|
||||
* 时代感对比(旧照片 vs 现今校园)
|
||||
|
||||
---
|
||||
|
||||
### **第三章|人物篇:传承与榜样(约 25 页)**
|
||||
|
||||
* **杰出校友特写**(5–8 人)
|
||||
* 各领域代表:教育、企业、科技、文化、公益、艺术等
|
||||
* 每人一页访谈式报道 + 肖像照 + 人生金句
|
||||
* **幕后人物**:长期服务校友会者(秘书、摄影、活动志工)
|
||||
* **纪念人物**:已故贡献校友与老师纪念
|
||||
|
||||
---
|
||||
|
||||
### **第四章|情感篇:故事与回忆(约 25 页)**
|
||||
|
||||
* “我们的青春”主题征文(精选 10–15 篇)
|
||||
* “老照片背后的故事”——由投稿校友自述
|
||||
* 班级回忆录(部分毕业届集体投稿)
|
||||
* 手写留言页 / QR码链接至音视频寄语
|
||||
|
||||
---
|
||||
|
||||
### **第五章|未来篇:传承与愿景(约 15 页)**
|
||||
|
||||
* 永平中学未来建设规划(教育蓝图)
|
||||
* 校友会未来十年计划
|
||||
* 青年校友接班计划
|
||||
* “数字校友会”构想/推介:网站 / 校友数据库
|
||||
|
||||
---
|
||||
|
||||
### **第六章|花絮与活动特辑(约 15 页)**
|
||||
|
||||
> 如果本书是在庆典活动前几天印刷好,然后活动当天发出
|
||||
|
||||
* 本次四十周年庆典活动全记录
|
||||
|
||||
* 开幕典礼
|
||||
* 校友之夜
|
||||
* 义跑 / 晚宴 / 展览 / 演出
|
||||
* 纪念品设计展示
|
||||
|
||||
---
|
||||
|
||||
### **附录**
|
||||
|
||||
* 校友会历届理事名录
|
||||
* 捐款鸣谢名单(依金额分级)
|
||||
* 赞助与广告页
|
||||
* 联系方式与 QR Link(线上相册、纪录片、网站)
|
||||
|
||||
---
|
||||
|
||||
## 🧭 三、工作时间规划(8 个月执行建议)
|
||||
|
||||
| 阶段 | 时长 | 工作内容 |
|
||||
| -------------- | ------- | ---------------------------------------- |
|
||||
| 1️⃣ 筹划期 | 第1月 | 成立编辑组,确定风格、预算、印刷规格 |
|
||||
| 2️⃣ 资料收集期 | 第2–3月 | 访谈、征文、收照片、整理档案 |
|
||||
| 3️⃣ 撰写与设计期 | 第4–5月 | 文稿成稿、图片修复、初稿排版 |
|
||||
| 4️⃣ 审校与赞助期 | 第6月 | 校对、内容确认、广告页洽谈 |
|
||||
| 5️⃣ 印刷准备期 | 第7月 | 定稿送印、样书确认 |
|
||||
| 6️⃣ 发布期 | 第8月 | 校庆活动同步发行、媒体推广、线上版本上线 |
|
||||
|
||||
---
|
||||
|
||||
## 💡 四、可拓展创意(让纪念册更现代)
|
||||
|
||||
* 📱 **AR互动页**:扫描校徽出现 3D 校史影片
|
||||
* 🌐 **线上纪念册版**(在校友会官网嵌入同步信息)
|
||||
* 🎬 **纪录片二维码嵌入**(对应章节)
|
||||
* 🕊️ **数字留言墙**(供校友写下祝福)
|
||||
BIN
docs/40 周年纪念册策划案.pdf
Normal file
56
docs/Onboarding Message.md
Normal file
@@ -0,0 +1,56 @@
|
||||
**主题:欢迎加入永平中学校友会!**
|
||||
|
||||
亲爱的校友,您好!
|
||||
|
||||
感谢您加入 **永平中学校友会(Yong Peng High School Alumni Association)**!
|
||||
自您成为会员的这一刻起,我们正式迎来了一位新的伙伴,也多了一份共同守护母校与社区的力量。
|
||||
|
||||
---
|
||||
|
||||
## 🎉 **我们是谁?**
|
||||
|
||||
永平中学校友会成立以来,一直致力于:
|
||||
|
||||
* **联结校友情谊**
|
||||
* **支持母校发展**
|
||||
* **推动教育、文化与社区活动**
|
||||
* **建立校友与在校生之间的桥梁**
|
||||
|
||||
您的加入,让我们更有力量继续前进。
|
||||
|
||||
---
|
||||
|
||||
## 📌 **作为会员,您可享有:**
|
||||
|
||||
* 活动、讲座、聚会的**优先参与权**
|
||||
* 校友资讯、重要公告的**第一时间通知**
|
||||
* 参与校友会项目、社区计划的**贡献机会**
|
||||
* 与来自各领域校友建立联系的**交流平台**
|
||||
|
||||
---
|
||||
|
||||
## 🗓️ **接下来能做什么?**
|
||||
|
||||
为帮助您更快融入校友会生态,我们建议:
|
||||
|
||||
1. **保存本会的官方联系方式**(WhatsApp / 社媒)
|
||||
2. **访问我们的官网**
|
||||
3. **留意即将发布的活动资讯**
|
||||
4. 若愿意参与志工 / 筹委,请随时回复我们
|
||||
|
||||
---
|
||||
|
||||
## ❤️ **我们欢迎您与我们同行**
|
||||
|
||||
无论您身处何地,校友会的大门永远为您敞开。
|
||||
让我们一同继续延续永中的精神,传承、回馈、成长。
|
||||
|
||||
若您有任何疑问,欢迎随时与我们联系。
|
||||
|
||||
谢谢您再次加入我们,
|
||||
**欢迎回家!**
|
||||
|
||||
**永平中学校友会**
|
||||
Yong Peng High School Alumni Association
|
||||
https://yphsalumni.org
|
||||
|
||||
89
docs/PPT Designs v1.md
Normal file
@@ -0,0 +1,89 @@
|
||||
目标时长
|
||||
|
||||
- 5–7 分钟(6–8 页),节奏轻快,现场可插入 60–90 秒演示
|
||||
|
||||
推荐结构(6–8 页)
|
||||
|
||||
- 封面:项目名称、口号、日期与主办单位
|
||||
- 为什么做:痛点与目标
|
||||
- 核心功能:面向校友和公众的主要板块
|
||||
- 设计与技术:品牌视觉与技术栈亮点
|
||||
- 运营与上线:内容节奏、参与方式与上线计划
|
||||
- 路线图:后续可期与合作方向
|
||||
- 现场演示:30–90 秒 Demo 流程说明
|
||||
- 行动号召与致谢:访问方式、参与方式、鸣谢
|
||||
|
||||
每页文案示例(可直接放入 PPT)
|
||||
|
||||
- 封面
|
||||
- 标题:永平中学校友会官网上线发布
|
||||
- 口号:连接校友 · 传承精神
|
||||
- 时间地点:2025.10 永平中学
|
||||
- 主办:永平中学校友会 | 技术支持:Tootaio Studio
|
||||
- 右侧放二维码(yphsalumni.org)+ public/Logo.svg
|
||||
- 为什么做(愿景与价值)
|
||||
- 信息更集中:新闻公告、活动安排、校友故事统一发布
|
||||
- 连接更紧密:线上平台增强校友互联与参与感
|
||||
- 传承更长久:记录校史、人事与精神资产
|
||||
- 数字化基建:为后续报名、征文、捐赠等打基础
|
||||
- 核心功能(首发)
|
||||
- 新闻与公告:重要信息一处直达
|
||||
- 校友活动:预告、报名与回顾
|
||||
- 名人堂:优秀校友故事与图集
|
||||
- 关于校友会:宗旨、会徽、校歌
|
||||
- 入会申请:表单已就绪(当前为占位,逐步开通)
|
||||
- 后台脚手架:内容与会员管理后续接通
|
||||
- 设计与技术(可信与可持续)
|
||||
- 视觉基因:主色 #fb9e3a、副色 #fcef91(温暖、亲和、纪念感)
|
||||
- 易读排版:专属 Markdown 样式,移动优先
|
||||
- 技术栈:Nuxt 4、TypeScript、Tailwind CSS、@nuxt/ui、@nuxt/content
|
||||
- SEO 就绪:Sitemap、Robots、Meta/OG/Twitter 卡片
|
||||
- 部署灵活:可静态托管,也可 SSR 托管
|
||||
- 运营与上线(如何用起来)
|
||||
- 内容节奏:每周1–2篇新闻/活动;名人堂按策划发布
|
||||
- 投稿机制:校友/老师/班级提供图文,统一编审发布
|
||||
- 渠道联动:官网首发,同步 Facebook/TikTok
|
||||
- 数据分析:尊重隐私的访问统计,用于优化内容结构
|
||||
- 路线图(Roadmap)
|
||||
- Q4:入会表单接通审核流程、内容 CRUD 后台
|
||||
- Q1:活动报名/签到、相册;捐赠模块调研
|
||||
- Q2:校友企业与招聘、专题档案与时间轴
|
||||
- 长期:多语言、校友地图、移动端优化
|
||||
- 现场演示(30–90 秒)
|
||||
- 首页开场 → 打开一条新闻 → 切到活动详情 → 看名人堂图集
|
||||
- 快速展示入会表单(目前“功能未开放”提示)
|
||||
- 如需:展示后台仪表盘框架(结构就绪,等接入)
|
||||
- 行动号召与致谢
|
||||
- 访问:yphsalumni.org(扫码关注)
|
||||
- 参与:投稿、提供史料、报名志愿者、提供赞助与合作
|
||||
- 关注我们:Facebook「永平中学校友会」、TikTok「@yphs.alumni」
|
||||
- 致谢:校董会/校方/校友与志愿者、Tootaio Studio 技术支持
|
||||
|
||||
视觉与排版建议
|
||||
|
||||
- 版式:16:9,黑/深灰背景或浅色质感底,突出橙色点缀
|
||||
- 字体:中文优先思源黑体/Noto Sans SC;标题粗体、正文中等
|
||||
- 统一元素:使用 public/Logo.svg 与 public/hero-image-2.jpg 作封面/过渡图
|
||||
- 图片素材:新闻/活动封面取自 public/news/*、public/events/*、public/hall-of-fame/*
|
||||
- 最少字多图:每页 3–5 条要点,每条不超过一行半
|
||||
|
||||
演示备选方案(网络不稳时)
|
||||
|
||||
- 准备 30–60 秒无声/配乐屏录视频(首页→新闻→活动→名人堂→入会)
|
||||
- 关键页面截图备选:主页、新闻详情、活动详情、名人堂人物页、后台仪表盘
|
||||
- 本地二维码图片(指向官网),避免临场生成失败
|
||||
|
||||
可选加页(视场合增减)
|
||||
|
||||
- 用户故事:校友/老师/在校生各 1 个使用场景
|
||||
- 隐私与安全:不采集敏感信息、分析工具与停用开关
|
||||
- 合作与赞助:纪念册/活动共创与鸣谢方式
|
||||
- FAQ:如何投稿、如何加入志愿者、如何反馈问题
|
||||
|
||||
素材清单(制作 PPT 前先备齐)
|
||||
|
||||
- Logo 与主视觉:public/Logo.svg、public/hero-image-2.jpg
|
||||
- 二维码:yphsalumni.org(建议白底黑码,配校色边框)
|
||||
- 页面截图:主页、新闻、活动、名人堂、入会、后台
|
||||
- 文案确认:宗旨口号、愿景 1 句话、路线图 3–5 条
|
||||
- 联系方式:官方邮箱/表单链接、官方社媒链接
|
||||
109
docs/PPT Designs v2.md
Normal file
@@ -0,0 +1,109 @@
|
||||
呈现原则
|
||||
|
||||
- 低调而清晰:把“报效 + 价值 + 开源”讲清楚,不喧宾夺主。
|
||||
- 一致口径:PPT、主持人口播、新闻通稿用同一措辞。
|
||||
- 可验证与可持续:展示交付范围和开源地址,强调长期可维护。
|
||||
|
||||
推荐版式(7–9 页)
|
||||
|
||||
- 封面:项目名、口号、日期、主办单位、技术支持
|
||||
- 背景与目标:为什么要做官网
|
||||
- 亮点与模块:新闻/活动/名人堂/关于/入会(占位)/后台脚手架
|
||||
- 交付与价值:高定项目、价值、交付清单
|
||||
- 开源与授权:开源协议、仓库地址、版权说明
|
||||
- 运营与上线:内容节奏、参与方式、渠道联动
|
||||
- 路线图:后续工作与可期
|
||||
- 现场演示:流程提示(30–90 秒)
|
||||
- 鸣谢与行动号召:访问方式、参与方式、致谢
|
||||
|
||||
关键页完整文案(可直接放入 PPT)
|
||||
|
||||
- 封面
|
||||
- 标题:永平中学校友会官网上线发布
|
||||
- 口号:连接校友 · 传承精神
|
||||
- 时间地点:2025.10 永平中学
|
||||
- 主办单位:永平中学校友会
|
||||
- 技术支持(赞助):Tootaio Studio
|
||||
- 右侧放站点二维码(yphsalumni.org)与 public/Logo.svg
|
||||
- 背景与目标
|
||||
- 信息更集中:新闻公告、活动、校友故事统一发布
|
||||
- 连接更紧密:线上平台增强凝聚力与参与感
|
||||
- 长期可持续:标准化内容、可复用组件、开源治理
|
||||
- 数字化基建:为报名、征文、捐赠等后续功能打基础
|
||||
- 亮点与模块
|
||||
- 新闻/公告:首发权威信息,SEO 友好
|
||||
- 校友活动:预告、回顾与多媒体内容
|
||||
- 名人堂:人物故事与图集
|
||||
- 关于校友会:宗旨、会徽、校歌
|
||||
- 入会申请:表单就绪(将逐步开通流程)
|
||||
- 管理后台:内容与会员管理脚手架,随时对接后端
|
||||
- 交付与价值(高定与报效)
|
||||
- 本项目由 Tootaio Studio 报效永平中学校友会
|
||||
- 价值:RM 30,000(高定项目)
|
||||
- 交付清单(概览)
|
||||
- 产品与信息架构:栏目与内容模型设计
|
||||
- 视觉与 UI:主题风格、导航、Markdown 样式与组件库适配
|
||||
- 前端开发:Nuxt 4、TypeScript、Tailwind、@nuxt/ui、@nuxt/content 集成
|
||||
- 内容迁移与示例:新闻/活动/名人堂样例、封面与图集
|
||||
- SEO 与分析:Sitemap/Robots/OG 元信息、可选 Umami 统计接入
|
||||
- 构建与部署:静态导出/SSR 双方案、上线脚本与文档
|
||||
- 说明:以上价值不含域名、服务器及第三方服务费用
|
||||
- 开源与授权(透明与共建)
|
||||
- 代码开源:项目源代码公开,便于校友与开发者共建
|
||||
- 开源协议:MIT/Apache-2.0(二选一,建议在仓库添加 LICENSE)
|
||||
- 仓库地址:Github 仓库链接与二维码(上线后填入)
|
||||
- 版权说明:代码遵循开源协议;文字与图片等内容版权归原作者/校友会所有
|
||||
- 贡献方式:提交 Issue/PR,按 content.config.ts 字段规范撰写内容
|
||||
- 运营与上线
|
||||
- 内容节奏:每周 1–2 篇新闻/活动;名人堂按策划发布
|
||||
- 参与方式:投稿、提供史料、报名志愿者、技术共建
|
||||
- 渠道联动:官网首发,同步 Facebook/TikTok
|
||||
- 数据与隐私:尊重用户隐私,分析仅用于内容优化
|
||||
- 路线图(示例)
|
||||
- Q4:入会表单接通审核流程、内容后台 CRUD
|
||||
- Q1:活动报名/签到、相册;捐赠模块调研
|
||||
- Q2:校友企业与招聘、专题档案与时间轴
|
||||
- 长期:多语言、校友地图、移动端优化
|
||||
- 现场演示(30–90 秒)
|
||||
- 首页 → 新闻详情 → 活动详情 → 名人堂图集 → 入会表单(现提示“功能未开放”)
|
||||
- 若网络不稳:准备 45 秒屏录视频与关键页面截图备用
|
||||
- 鸣谢与行动号召
|
||||
- 访问:yphsalumni.org(扫码)
|
||||
- 参与:投稿/志愿者/合作/赞助(留邮箱或表单链接)
|
||||
- 鸣谢:校董会/校方/校友与志愿者、Tootaio Studio 技术支持
|
||||
- 结束语:连接校友 · 传承精神
|
||||
|
||||
金额与赞助信息的呈现技巧
|
||||
|
||||
- 将“报效/价值/开源”分散露出,避免单页过度“商业化”:封面页署名(技术支持)、中段“交付与价值”页清晰展现、高尾页鸣谢再次确认。
|
||||
- “RM30,000”用一行大数字配副标题“高定项目(报效)”,旁边放交付清单,强调价值而不过度营销。
|
||||
- 视觉上弱化工作室 Logo(相对校友会 Logo 约 1/2–1/3 宽度),保持庄重。
|
||||
|
||||
主持人口播(15–20 秒)
|
||||
|
||||
- “本次官网由 Tootaio Studio 报效搭建,项目价值 RM 三万,为高定定制并全面开源。感谢工作室团队的专业支持,也欢迎校友与开发者共同参与建设。”
|
||||
|
||||
新闻通稿段落(可复用)
|
||||
|
||||
- “永平中学校友会官网今日正式上线。该项目由 Tootaio Studio 报效支持,项目价值 RM30,000,为高定定制网站并以开源方式发布。平台将持续发布新闻活动、名人堂故事等内容,欢迎校友访问
|
||||
yphsalumni.org 并参与共建。”
|
||||
|
||||
素材清单
|
||||
|
||||
- 官网二维码(yphsalumni.org)
|
||||
- 仓库二维码(开源后补充)
|
||||
- Logo:public/Logo.svg、主视觉 public/hero-image-2.jpg
|
||||
- 页面截图:主页、新闻、活动、名人堂、入会(与后台仪表盘)
|
||||
- 一页式“交付清单与价值”图(可用饼图/图标矩阵)
|
||||
|
||||
合规与落地建议
|
||||
|
||||
- 确认开源协议(建议 MIT)并在仓库根目录添加 LICENSE
|
||||
- 在 README.md 保留“报效/开源”说明与致谢,PPT 与通稿用同一表述
|
||||
- 金额声明附说明:不含域名/服务器/第三方服务费
|
||||
- 现场背景板与屏幕页脚统一“技术支持:Tootaio Studio”
|
||||
|
||||
需要的话,我可以:
|
||||
|
||||
- 直接为你生成一份可放映的 Marp/Reveal.js Markdown 幻灯,或输出 PPTX 模板(含上述文案与占位图)。
|
||||
- 创建“交付与价值”信息图一页(AI 矢量),用于 PPT 与对外物料。
|
||||
57
docs/Statistic Card 建议.md
Normal file
@@ -0,0 +1,57 @@
|
||||
### 🎓 校友群体类(展示规模)
|
||||
|
||||
* **注册会员人数**(基础指标)
|
||||
* **校友分布地区数**(如「分布于 12 个国家」)
|
||||
* **历届毕业生总数**
|
||||
* **校友企业数**(若可统计,代表社会影响力)
|
||||
* **理事人数 / 活跃志工人数**
|
||||
|
||||
---
|
||||
|
||||
### 💰 贡献与资源类(展示凝聚力)
|
||||
|
||||
* **教育基金总额**(例如「林赛花教育基金已累计 RM XXX,XXX」)
|
||||
* **奖助学金受惠人数**
|
||||
* **年度捐款总额 / 参与人数**
|
||||
* **历年活动赞助商数量**
|
||||
|
||||
---
|
||||
|
||||
### 🏛️ 历史与传承类(展示深度)
|
||||
|
||||
* **成立年份 / 周年数**(基础指标)
|
||||
* **举办活动次数**(历届聚会 / 座谈 / 校庆)
|
||||
* **出版刊物 / 纪念册数量**
|
||||
* **历届理事会届数**
|
||||
|
||||
---
|
||||
|
||||
### 🌏 影响与传播类(展示影响面)
|
||||
|
||||
* **官方网站访问量 / 月均访问数**
|
||||
* **社交媒体关注人数 / 互动量**
|
||||
* **媒体报道次数**(可简化为“媒体曝光数”)
|
||||
|
||||
---
|
||||
|
||||
### 💡 进阶玩法(让页面更有活力)
|
||||
|
||||
* **“活跃率”**:例如「本年度活动参与率 72%」
|
||||
* **“成长曲线”**:每年会员人数变化趋势(用小图表示)
|
||||
* **“校友情谊值”**:趣味指标,比如根据活动签到 / 捐赠 / 投稿自动算出的综合分数(可 Gamify)
|
||||
|
||||
---
|
||||
|
||||
### ✅ 推荐组合(简洁又有格调)
|
||||
|
||||
| 分类 | 指标 | 示例展示 |
|
||||
| -------- | ------------- | ---------------- |
|
||||
| 校友群体 | 会员总数 | 1,237 位注册会员 |
|
||||
| 成立历程 | 成立时间 | 创立于 1985 年 |
|
||||
| 贡献力量 | 教育基金累计 | RM 245,000 |
|
||||
| 活动热度 | 年度活动次数 | 12 场活动 |
|
||||
| 社群影响 | Facebook 关注 | 3.4k Followers |
|
||||
|
||||
---
|
||||
|
||||
要我帮你把这五个指标设计成一个统一风格的 **Statistic Card UI**(适合放在 Nuxt + @nuxt/ui 项目里)吗?我可以直接给出结构和样式建议 🔥
|
||||
@@ -5,14 +5,15 @@ export default defineNuxtConfig({
|
||||
compatibilityDate: "2025-07-15",
|
||||
devtools: { enabled: true },
|
||||
modules: [
|
||||
"@nuxt/ui",
|
||||
"@nuxtjs/seo",
|
||||
"@nuxt/content",
|
||||
"@nuxt/image",
|
||||
"@nuxt/ui",
|
||||
"reka-ui/nuxt",
|
||||
"@nuxtjs/robots",
|
||||
"@nuxtjs/seo",
|
||||
"@nuxtjs/sitemap",
|
||||
],
|
||||
ssr: true,
|
||||
css: ["~/assets/css/main.css"],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
@@ -29,28 +30,46 @@ export default defineNuxtConfig({
|
||||
],
|
||||
meta: [
|
||||
// 基础 SEO
|
||||
{ name: "description", content: "永平中学校友会官网 - 连接校友,共享资源,传承母校精神。" },
|
||||
{ name: "keywords", content: "永平中学, 校友会, 永平中学校友, 永平校友, 同学会" },
|
||||
{
|
||||
name: "description",
|
||||
content: "永平中学校友会官网 - 连接校友,共享资源,传承母校精神。",
|
||||
},
|
||||
{
|
||||
name: "keywords",
|
||||
content: "永平中学, 校友会, 永平中学校友, 永平校友, 同学会",
|
||||
},
|
||||
{ name: "author", content: "永平中学校友会" },
|
||||
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||
|
||||
// Open Graph(Facebook/LinkedIn)
|
||||
{ property: "og:title", content: "永平中学校友会" },
|
||||
{ property: "og:description", content: "永平中学校友会官网 - 连接校友,共享资源,传承母校精神。" },
|
||||
{
|
||||
property: "og:description",
|
||||
content: "永平中学校友会官网 - 连接校友,共享资源,传承母校精神。",
|
||||
},
|
||||
{ property: "og:type", content: "website" },
|
||||
{ property: "og:url", content: "https://yphsalumni.org" }, // ✅ 换成你网站的真实域名
|
||||
{ property: "og:image", content: "https://yphsalumni.org/hero-image.jpg" }, // ✅ 上传一张封面图
|
||||
{
|
||||
property: "og:image",
|
||||
content: "https://img.yphsalumni.org/i/2025/11/28/qk9fe8.png",
|
||||
}, // ✅ 上传一张封面图
|
||||
|
||||
// Twitter Card
|
||||
{ name: "twitter:card", content: "summary_large_image" },
|
||||
{ name: "twitter:title", content: "永平中学校友会" },
|
||||
{ name: "twitter:description", content: "连接校友,共享资源,传承母校精神。" },
|
||||
{ name: "twitter:image", content: "https://yphsalumni.org/hero-image.jpg" },
|
||||
{
|
||||
name: "twitter:description",
|
||||
content: "连接校友,共享资源,传承母校精神。",
|
||||
},
|
||||
{
|
||||
name: "twitter:image",
|
||||
content: "https://img.yphsalumni.org/i/2025/11/28/qk9fe8.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
site: {
|
||||
url: "https://yphsalumni.com",
|
||||
name: "永中校友会 YPHS Alumni"
|
||||
}
|
||||
});
|
||||
url: "https://yphsalumni.org",
|
||||
name: "永中校友会 YPHS Alumni",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"element-plus": "^2.11.4",
|
||||
"html2pdf.js": "^0.12.1",
|
||||
"maska": "^3.2.0",
|
||||
"nuxt": "^4.1.3",
|
||||
"md-editor-v3": "^6.0.1",
|
||||
"nuxt": "^4.2.0",
|
||||
"reka-ui": "^2.5.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -30,6 +31,7 @@
|
||||
"vue-sonner": "^2.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.70",
|
||||
"sass-embedded": "^1.93.2"
|
||||
}
|
||||
}
|
||||
|
||||
4316
pnpm-lock.yaml
generated
BIN
public/about/YongPing_SchoolSong_V2.mp3
Normal file
BIN
public/about/中补班乙.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
public/about/李华光赠言.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
public/about/某老师赠言.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
public/about/校歌.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
public/about/永平中学校歌.mp3
Normal file
BIN
public/about/赵老师赠言.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
public/about/黄绍南校长赠言.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
42
repomix.config.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "https://repomix.com/schemas/latest/schema.json",
|
||||
"input": {
|
||||
"maxFileSize": 52428800
|
||||
},
|
||||
"output": {
|
||||
"filePath": "repomix-output.xml",
|
||||
"style": "xml",
|
||||
"parsableStyle": false,
|
||||
"fileSummary": true,
|
||||
"directoryStructure": true,
|
||||
"files": true,
|
||||
"removeComments": false,
|
||||
"removeEmptyLines": false,
|
||||
"compress": false,
|
||||
"topFilesLength": 5,
|
||||
"showLineNumbers": false,
|
||||
"truncateBase64": false,
|
||||
"copyToClipboard": false,
|
||||
"includeFullDirectoryStructure": false,
|
||||
"tokenCountTree": false,
|
||||
"git": {
|
||||
"sortByChanges": true,
|
||||
"sortByChangesMaxCommits": 100,
|
||||
"includeDiffs": false,
|
||||
"includeLogs": false,
|
||||
"includeLogsCount": 50
|
||||
}
|
||||
},
|
||||
"include": [],
|
||||
"ignore": {
|
||||
"useGitignore": true,
|
||||
"useDefaultPatterns": true,
|
||||
"customPatterns": []
|
||||
},
|
||||
"security": {
|
||||
"enableSecurityCheck": true
|
||||
},
|
||||
"tokenCount": {
|
||||
"encoding": "o200k_base"
|
||||
}
|
||||
}
|
||||