feat(members): add members listing page

This commit introduces a new `/members` page to display a directory of alumni association members.

- Member data is sourced from a CSV file (`content/members/members.csv`) and managed via Nuxt Content.
- The page presents member information in a table, including calculated graduation class (`届别`).
- A link to the new page has been added to the main navigation.
- Minor UI tweaks and data corrections in other sections are also included.
This commit is contained in:
xiaomai
2025-11-02 21:53:33 +08:00
parent f5d9963f3c
commit 34bb2360fd
9 changed files with 152 additions and 8 deletions

4
.gitignore vendored
View File

@@ -23,4 +23,6 @@ logs
.env.*
!.env.example
repomix-output.xml
repomix-output.xml
content/members/members.csv

View File

@@ -105,6 +105,11 @@ const bannerActions = ref<ButtonProps[]>([
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",
@@ -122,7 +127,7 @@ const items = computed<NavigationMenuItem[]>(() => [
],
},
{
label: "关于校友会",
label: "关于",
to: "/about",
active: route.path.startsWith("/about"),
children: [
@@ -143,12 +148,12 @@ const items = computed<NavigationMenuItem[]>(() => [
description: "永平中学补习班1956年一封迟来的贴文",
to: "/about/middle-highschool-tuition-class",
active: route.path.startsWith("/about/middle-highschool-tuition-class"),
icon: "mdi:mail"
}
icon: "mdi:mail",
},
],
},
{
label: "友情链接",
label: "链接",
children: [
{
label: "永平中学官网",

View File

@@ -58,6 +58,7 @@ const generation = route.params.slug;
const categories = ref(["领导团队", "职能部门", "专项部门"]);
// TODO: Fetch from api
const orgStructure = ref([
{
name: "李煜斌",
@@ -132,14 +133,14 @@ const orgStructure = ref([
},
{
name: "胡少菲",
position: "总务",
position: "康乐",
category: "职能部门",
photo: "/org-structure/胡少菲.png",
description: "文化活动策划、康乐项目组织与会员联谊活动。",
},
{
name: "林剑宝",
position: "副总务",
position: "副康乐",
category: "职能部门",
photo: "/org-structure/林剑宝.png",
description: "协助文康组织文体活动、兴趣小组与社交聚会。",

View File

@@ -5,6 +5,8 @@
class="bg-cover bg-center"
style="
background-image: url(&quot;/hero-image.jpg&quot;);
background-position-y: -40px;
background-repeat: no-repeat;
background-color: rgba(255, 255, 255, 0.5); /* Semi-transparent black */
background-blend-mode: lighten;
"

111
app/pages/members/index.vue Normal file
View File

@@ -0,0 +1,111 @@
<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({
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: "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))
? "初中毕业"
: `初中第 ${Number(row.original.graduateYear) - 1958}`;
case "s":
// 高中毕业
return isNaN(Number(row.original.graduateYear))
? "高中毕业"
: `高中第 ${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>

View File

@@ -36,7 +36,7 @@ export default defineContentConfig({
// 名人堂
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 +45,27 @@ export default defineContentConfig({
gallery: z.array(z.string()),
}),
}),
// 会员名册
members: defineCollection({
type: "data",
source: "members/members.csv",
schema: z.object({
// 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(),
}),
}),
},
});

View File

@@ -0,0 +1 @@
chineseName,englishName,ic,mobile,home,email,graduateLevel,,graduateYear,marriageStatus,livingCountry,addressLine1,addressLine2,addressLine3,joinedYear,receiptNumber
1 chineseName englishName ic mobile home email graduateLevel graduateYear marriageStatus livingCountry addressLine1 addressLine2 addressLine3 joinedYear receiptNumber

BIN
public/hero-image-2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 93 KiB