Compare commits

...

11 Commits

24 changed files with 1197 additions and 275 deletions
+31
View File
@@ -0,0 +1,31 @@
name: 申请删除照片墙照片
description: 删除照片墙某个专题里的照片
title: "photo-delete: "
labels:
- photo-delete
body:
- type: markdown
attributes:
value: |
提交后,将进行人工审核,请勿恶意提交,否则禁用账号。
如果该专题里已经有相同 `title` 的照片,会删除它。
- type: dropdown
id: topic
attributes:
label: topic
description: 要删除的照片所属专题。
options:
- classroom
- events
- graduation-day
- candid
validations:
required: true
- type: input
id: title
attributes:
label: title
description: 照片标题;同一专题内标题相同会被视为删除已有照片。
placeholder: 班级节目
validations:
required: true
+53
View File
@@ -0,0 +1,53 @@
name: 更新照片墙照片
description: 新增或修改照片墙某个专题里的照片
title: "photo: "
labels:
- photo-update
body:
- type: markdown
attributes:
value: |
提交后,自动化会根据 `topic` 找到对应的 `src/data/photo-topics/<topic>.ts`。
如果该专题里已经有相同 `title` 的照片,会更新它;否则会追加一张新照片。
- type: dropdown
id: topic
attributes:
label: topic
description: 要写入的照片专题。
options:
- classroom
- events
- graduation-day
- candid
validations:
required: true
- type: input
id: title
attributes:
label: title
description: 照片标题;同一专题内标题相同会被视为修改已有照片。
placeholder: 班级节目
validations:
required: true
- type: textarea
id: caption
attributes:
label: caption
description: 照片说明。
placeholder: 那天大家一起站在灯光下面。
validations:
required: true
- type: input
id: image
attributes:
label: image
description: 图片地址,可以是外链,也可以是站内路径,例如 /photos/events/show.jpg。
placeholder: https://example.com/photo.jpg
validations:
required: true
- type: input
id: cover
attributes:
label: cover
description: 可选。填写后会同时更新该专题封面;留空则不改封面。
placeholder: https://example.com/cover.jpg
@@ -0,0 +1,64 @@
name: 网站建议
description: 对网站首页、同学资料、照片墙、内容文案或交互体验提出建议
title: "suggestion: "
labels:
- site-suggestion
body:
- type: markdown
attributes:
value: |
感谢你愿意帮我们把网站做得更好。
请尽量说明建议涉及的位置、你希望改成什么样,以及这样修改的原因。
- type: dropdown
id: section
attributes:
label: 建议涉及的网站部分
description: 请选择最接近的部分;如果不确定,可以选择「其他」。
options:
- 首页
- 如今的我们
- 照片墙
- 班级故事
- 网站文案
- 页面布局
- 移动端体验
- 交互功能
- 其他
validations:
required: true
- type: textarea
id: current
attributes:
label: 当前情况
description: 目前你看到的问题、觉得不方便的地方,或想改进的位置。
placeholder: 例如:照片墙某个专题的入口不太明显,手机上需要滑很久才能找到。
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: 建议内容
description: 你希望网站如何调整?可以写具体文案、布局、功能或展示方式。
placeholder: 例如:希望在首页增加照片墙入口,并按「教室 / 活动 / 毕业日」展示几个快捷入口。
validations:
required: true
- type: textarea
id: reason
attributes:
label: 建议原因
description: 为什么你觉得这个建议值得做?它会帮助谁,或解决什么问题?
placeholder: 例如:这样第一次访问网站的同学可以更快找到照片,也更容易回忆起不同阶段的活动。
validations:
required: true
- type: input
id: page
attributes:
label: 相关页面或链接
description: 可选。填写具体页面地址、截图链接或相关位置说明。
placeholder: /photos 或 首页顶部
- type: textarea
id: extra
attributes:
label: 补充信息
description: 可选。可以补充截图说明、参考网站、更多背景或其他想法。
placeholder: 其他想补充的内容。
@@ -0,0 +1,71 @@
name: Review Issues Without Template
on:
issues:
types:
- opened
permissions:
issues: write
jobs:
label-without-template:
if: ${{ !github.event.issue.pull_request }}
runs-on: ubuntu-latest
steps:
- name: Add review label when no template is used
uses: actions/github-script@v7
with:
script: |
const reviewLabel = "needs-review";
const templateLabels = new Set([
"people-update",
"photo-update",
"site-suggestion",
]);
const issue = context.payload.issue;
const labels = issue.labels.map((label) => label.name);
const body = issue.body || "";
const title = issue.title || "";
const hasTemplateLabel = labels.some((label) => templateLabels.has(label));
const hasTemplateTitle =
title.startsWith("people:") ||
title.startsWith("photo:") ||
title.startsWith("suggestion:");
const hasTemplateFields =
body.includes("### slug") ||
body.includes("### topic") ||
body.includes("### 建议涉及的网站部分");
if (hasTemplateLabel || hasTemplateTitle || hasTemplateFields) {
return;
}
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: reviewLabel,
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: reviewLabel,
color: "fbca04",
description: "Issue did not use a repository template and needs manual review.",
});
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [reviewLabel],
});
+66
View File
@@ -0,0 +1,66 @@
name: Update Photo Data
on:
issues:
types:
- opened
- edited
- labeled
permissions:
contents: write
issues: write
pull-requests: write
jobs:
update-photo:
if: contains(github.event.issue.labels.*.name, 'photo-update') || startsWith(github.event.issue.title, 'photo:')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Update photo topic data
env:
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
run: node scripts/update-photo-topic-from-issue.mjs
- name: Create pull request
id: create-pr
uses: peter-evans/create-pull-request@v6
with:
commit-message: "chore: update photo data from issue #${{ github.event.issue.number }}"
title: "更新照片墙:#${{ github.event.issue.number }}"
body: |
由 #${{ github.event.issue.number }} 自动生成。
这个 PR 会根据 issue 表单更新 `src/data/photo-topics/<topic>.ts`
- 如果同一专题内已有相同 `title`,则修改对应照片
- 如果没有相同 `title`,则追加一张新照片
- 如果填写了 `cover`,则同步更新专题封面
branch: photo-update/issue-${{ github.event.issue.number }}
delete-branch: true
labels: photo-update
- name: Comment on issue
uses: actions/github-script@v7
with:
script: |
const prUrl = "${{ steps.create-pr.outputs.pull-request-url }}";
const body = prUrl
? `已根据这个 issue 创建照片更新 PR${prUrl}\n\n如果内容还要调整,直接编辑 issue 表单即可更新同一个 PR。`
: "这个 issue 没有产生新的文件改动,所以没有创建 PR。";
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "class-anniversary",
"version": "0.2.0",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "class-anniversary",
"version": "0.2.0",
"version": "0.3.0",
"dependencies": {
"@astrojs/check": "^0.9.9",
"astro": "^6.2.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "class-anniversary",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"type": "module",
"scripts": {
+123
View File
@@ -0,0 +1,123 @@
import { readFileSync, writeFileSync } from "node:fs";
import { relative, resolve } from "node:path";
const issueBody = process.env.ISSUE_BODY ?? "";
const topicRoot = resolve(process.env.PHOTO_TOPIC_ROOT ?? "src/data/photo-topics");
const fields = ["topic", "title", "caption", "image", "cover"];
const requiredFields = ["topic", "title", "caption", "image"];
const sectionAliases = new Map(fields.map((field) => [field.toLowerCase(), field]));
function normalizeValue(value) {
const trimmed = value.trim();
return trimmed === "_No response_" ? "" : trimmed;
}
function parseIssueForm(body) {
const result = {};
const sectionPattern = /^###\s+(.+?)\s*$/gm;
const matches = [...body.matchAll(sectionPattern)];
for (let index = 0; index < matches.length; index += 1) {
const rawLabel = matches[index][1].trim().toLowerCase();
const field = sectionAliases.get(rawLabel);
if (!field) continue;
const start = matches[index].index + matches[index][0].length;
const end = index + 1 < matches.length ? matches[index + 1].index : body.length;
result[field] = normalizeValue(body.slice(start, end));
}
return result;
}
function assertValidInput(input) {
for (const field of requiredFields) {
if (!input[field]) {
throw new Error(`Issue form is missing required field: ${field}`);
}
}
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.topic)) {
throw new Error("topic must use lowercase letters, numbers, and single hyphens only.");
}
}
function topicPathFromSlug(slug) {
return resolve(topicRoot, `${slug}.ts`);
}
function readTopicModule(filePath) {
const source = readFileSync(filePath, "utf8");
const declarationPattern = /export const\s+(\w+)\s*:\s*PhotoTopic\s*=\s*({[\s\S]*?});\s*$/;
const match = source.match(declarationPattern);
if (!match) {
throw new Error(`Could not find PhotoTopic export in ${relative(process.cwd(), filePath)}`);
}
const [, exportName, objectSource] = match;
const topic = Function(`return (${objectSource});`)();
return { exportName, topic };
}
function buildPhoto(input, existing = {}) {
return {
title: input.title || existing.title || "",
caption: input.caption || existing.caption || "",
image: input.image || existing.image || ""
};
}
function quote(value) {
return JSON.stringify(value);
}
function formatPhoto(photo) {
return ` {
title: ${quote(photo.title)},
caption: ${quote(photo.caption)},
image: ${quote(photo.image)}
}`;
}
function formatTopicModule(exportName, topic) {
return `import type { PhotoTopic } from "../photo-types";
export const ${exportName}: PhotoTopic = {
slug: ${quote(topic.slug)},
title: ${quote(topic.title)},
text: ${quote(topic.text)},
cover: ${quote(topic.cover ?? "")},
photos: [
${topic.photos.map(formatPhoto).join(",\n")}
]
};
`;
}
const input = parseIssueForm(issueBody);
assertValidInput(input);
const topicPath = topicPathFromSlug(input.topic);
const { exportName, topic } = readTopicModule(topicPath);
if (topic.slug !== input.topic) {
throw new Error(`Topic slug mismatch: issue asked for ${input.topic}, file contains ${topic.slug}`);
}
if (input.cover) {
topic.cover = input.cover;
}
const existingIndex = topic.photos.findIndex((photo) => photo.title === input.title);
if (existingIndex >= 0) {
topic.photos[existingIndex] = buildPhoto(input, topic.photos[existingIndex]);
console.log(`Updated existing photo "${input.title}" in topic: ${input.topic}`);
} else {
topic.photos.push(buildPhoto(input));
console.log(`Added new photo "${input.title}" to topic: ${input.topic}`);
}
writeFileSync(topicPath, formatTopicModule(exportName, topic), "utf8");
console.log(`Wrote ${relative(process.cwd(), topicPath)}`);
+1 -2
View File
@@ -21,8 +21,7 @@ export const messages = [
export const twikooConfig = {
envId: import.meta.env.PUBLIC_TWIKOO_ENV_ID ?? "",
scriptSrc: "https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.nocss.js",
styleSrc: "https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.css",
scriptSrc: "https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js",
path: "/messages/",
lang: "zh-CN"
};
+17
View File
@@ -20,5 +20,22 @@ export const people = [
messageToClass: "希望下次见面时,我们还能像以前一样很快聊起来。",
favoriteMemory: "晚自习后一起走出教学楼,风吹过来的那几分钟。",
contact: "可填写微信、邮箱或留空"
},
{
slug: "biss",
initial: "BI",
name: "毕爽爽",
photo: "",
location: "山西高平",
school: "山东理工大学",
direction: "测绘工程",
keywords: ["开心"],
text: "迷茫",
currentStatus: "还好还好",
highlight: "似乎没有什么值得开心的",
toPastSelf: "没什么",
messageToClass: "没什么",
favoriteMemory: "all",
contact: ""
}
];
+30
View File
@@ -0,0 +1,30 @@
import type { PhotoTopic } from "../photo-types";
export const candidTopic: PhotoTopic = {
slug: "candid",
title: "没被摆拍的瞬间",
text: "走廊、食堂、晚霞和笑场。真正会让人停下来的,常常是不太整齐的照片。",
cover: "",
photos: [
{
title: "走廊偶遇",
caption: "模糊一点也没关系,像真的从记忆里翻出来。",
image: ""
},
{
title: "食堂那一桌",
caption: "饭菜不一定好吃,但聊天是真的好笑。",
image: ""
},
{
title: "操场晚霞",
caption: "很多故事都发生在天快黑的时候。",
image: ""
},
{
title: "笑场",
caption: "最不端正的照片,往往最像我们。",
image: ""
}
]
};
+35
View File
@@ -0,0 +1,35 @@
import type { PhotoTopic } from "../photo-types";
export const classroomTopic: PhotoTopic = {
slug: "classroom",
title: "教室日常",
text: "黑板、课桌、窗边、试卷,还有那些写在草稿纸边角的小情绪。",
cover: "",
photos: [
{
title: "窗边的座位",
caption: "把真实照片放到 public/photos/classroom/window-seat.jpg 后,再把 image 改成对应路径。",
image: ""
},
{
title: "黑板角落",
caption: "适合放倒计时、值日表、板书和课代表留下的提醒。",
image: ""
},
{
title: "堆满书的桌面",
caption: "那些看起来很乱、后来又很想念的普通一天。",
image: ""
},
{
title: "晚自习灯光",
caption: "可以放教室灯亮着、窗外天色暗下来的照片。",
image: ""
},
{
title: "课间十分钟",
caption: "不用太正式,越像随手拍越有高中味道。",
image: ""
}
]
};
+25
View File
@@ -0,0 +1,25 @@
import type { PhotoTopic } from "../photo-types";
export const eventsTopic: PhotoTopic = {
slug: "events",
title: "班级活动",
text: "运动会、晚会、春游、比赛,所有离开课桌之后还在一起发光的时刻。",
cover: "",
photos: [
{
title: "班级节目",
caption: "排练时觉得麻烦,回头看全是可爱。",
image: "https://pic.biss.click/image/ac8403bf-8732-4f43-bb4d-a1def1fc6fa9.jpg"
},
{
title: "集体出游",
caption: "人群、阳光、背包和没停过的聊天。",
image: "https://pic.biss.click/image/ef56e433-2938-4f96-bc16-b6c973b7621d.png"
},
{
title: "比赛现场",
caption: "赢没赢都记得,站在一起才是重点。",
image: ""
}
]
};
+35
View File
@@ -0,0 +1,35 @@
import type { PhotoTopic } from "../photo-types";
export const graduationDayTopic: PhotoTopic = {
slug: "graduation-day",
title: "毕业那天",
text: "合照、签名、花束、校门和没说完的话,都放在这个专题里。",
cover: "",
photos: [
{
title: "最后一张合照",
caption: "这里最适合放班级毕业照。",
image: ""
},
{
title: "校服签名",
caption: "名字挤在一起,像那天没来得及说完的话。",
image: ""
},
{
title: "校门口",
caption: "出发的地方,也成了回头看的地方。",
image: ""
},
{
title: "花和证书",
caption: "仪式感不用太多,一束花就够亮。",
image: ""
},
{
title: "散场之前",
caption: "那一刻大家都在笑,但心里都知道要分别了。",
image: ""
}
]
};
+13
View File
@@ -0,0 +1,13 @@
export interface PhotoItem {
title: string;
caption: string;
image: string;
}
export interface PhotoTopic {
slug: string;
title: string;
text: string;
cover: string;
photos: PhotoItem[];
}
+11 -123
View File
@@ -1,129 +1,17 @@
import type { PhotoTopic } from "./photo-types";
import { candidTopic } from "./photo-topics/candid";
import { classroomTopic } from "./photo-topics/classroom";
import { eventsTopic } from "./photo-topics/events";
import { graduationDayTopic } from "./photo-topics/graduation-day";
export const galleryIntro = {
title: "照片墙",
text: "照片按专题收纳:教室日常、班级活动、毕业那天、没被摆拍的瞬间。点进专题后,就像把一叠照片随手摊在桌上慢慢翻。"
};
export const photoTopics = [
{
slug: "classroom",
title: "教室日常",
text: "黑板、课桌、窗边、试卷,还有那些写在草稿纸边角的小情绪。",
cover: "",
photos: [
{
title: "窗边的座位",
caption: "把真实照片放到 public/photos/classroom/window-seat.jpg 后,再把 image 改成对应路径。",
image: ""
},
{
title: "黑板角落",
caption: "适合放倒计时、值日表、板书和课代表留下的提醒。",
image: ""
},
{
title: "堆满书的桌面",
caption: "那些看起来很乱、后来又很想念的普通一天。",
image: ""
},
{
title: "晚自习灯光",
caption: "可以放教室灯亮着、窗外天色暗下来的照片。",
image: ""
},
{
title: "课间十分钟",
caption: "不用太正式,越像随手拍越有高中味道。",
image: ""
}
]
},
{
slug: "events",
title: "班级活动",
text: "运动会、晚会、春游、比赛,所有离开课桌之后还在一起发光的时刻。",
cover: "",
photos: [
{
title: "运动会看台",
caption: "喊到嗓子哑的那一天。",
image: ""
},
{
title: "班级节目",
caption: "排练时觉得麻烦,回头看全是可爱。",
image: ""
},
{
title: "集体出游",
caption: "人群、阳光、背包和没停过的聊天。",
image: ""
},
{
title: "比赛现场",
caption: "赢没赢都记得,站在一起才是重点。",
image: ""
}
]
},
{
slug: "graduation-day",
title: "毕业那天",
text: "合照、签名、花束、校门和没说完的话,都放在这个专题里。",
cover: "",
photos: [
{
title: "最后一张合照",
caption: "这里最适合放班级毕业照。",
image: ""
},
{
title: "校服签名",
caption: "名字挤在一起,像那天没来得及说完的话。",
image: ""
},
{
title: "校门口",
caption: "出发的地方,也成了回头看的地方。",
image: ""
},
{
title: "花和证书",
caption: "仪式感不用太多,一束花就够亮。",
image: ""
},
{
title: "散场之前",
caption: "那一刻大家都在笑,但心里都知道要分别了。",
image: ""
}
]
},
{
slug: "candid",
title: "没被摆拍的瞬间",
text: "走廊、食堂、晚霞和笑场。真正会让人停下来的,常常是不太整齐的照片。",
cover: "",
photos: [
{
title: "走廊偶遇",
caption: "模糊一点也没关系,像真的从记忆里翻出来。",
image: ""
},
{
title: "食堂那一桌",
caption: "饭菜不一定好吃,但聊天是真的好笑。",
image: ""
},
{
title: "操场晚霞",
caption: "很多故事都发生在天快黑的时候。",
image: ""
},
{
title: "笑场",
caption: "最不端正的照片,往往最像我们。",
image: ""
}
]
}
export const photoTopics: PhotoTopic[] = [
classroomTopic,
eventsTopic,
graduationDayTopic,
candidTopic
];
+36 -5
View File
@@ -1,13 +1,32 @@
export const site = {
className: "高三 X 班",
title: "毕业一年后,我们依然在场",
className: "2024届612班",
title: "毕业后,我们依然在场",
subtitle:
"把散落在相册、聊天记录和心里的瞬间放回同一个地方。这里先留给高中的三年,也留给一年后的我们。",
anniversary: "2025.06 - 2026.06 · 毕业周年纪念",
anniversary: "2024.06 - 2026.06 · 毕业周年纪念",
heroImage: "/assets/campus-hero.png",
footer: "高三 X 班毕业一周年纪念网站 · 初版"
footer: "2024届612班纪念网站"
};
export const contactLinks = [
{
label: "邮箱",
href: "mailto:class@biss.click",
icon: "fa-solid fa-envelope"
},
{
label: "GitHub",
href: "https://github.com/bishshi/class",
icon: "fa-brands fa-github"
},
{
label: "微信公众号",
href: "#",
icon: "fa-brands fa-weixin",
qrImage: "https://pic.biss.click/image/44a5e576-5cf3-4752-bae2-70d74619324f.webp"
}
];
export const navItems = [
{ label: "首页", href: "/" },
{ label: "三年时间线", href: "/timeline/" },
@@ -16,8 +35,20 @@ export const navItems = [
{ label: "留言墙", href: "/messages/" }
];
const leavingCampusDate = "2024-06-08";
const millisecondsPerDay = 24 * 60 * 60 * 1000;
const getDaysSince = (date: string) => {
const [year, month, day] = date.split("-").map(Number);
const start = Date.UTC(year, month - 1, day);
const now = new Date();
const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
return Math.max(0, Math.floor((today - start) / millisecondsPerDay));
};
export const stats = [
{ value: "1095+", label: "一起经过的高中日子" },
{ value: "365", label: "离开校园后的第一年" },
{ value: String(getDaysSince(leavingCampusDate)), label: "离开校园后的日子" },
{ value: "∞", label: "还会被想起的瞬间" }
];
+59 -18
View File
@@ -1,31 +1,72 @@
export const timelineIntro = {
title: "从第一节课,到最后一次回头",
text: "把班级大事按时间放在这里。现在是示例内容,之后可以替换成军训、运动会、成人礼、百日誓师、毕业照等真实节点。"
title: "从进入高中,到我们毕业",
text: "把三年里的班级大事按时间放在这里。军训、分班、网课、百日誓师、高考和毕业,都成为了后来回头看时很亮的节点。"
};
export const timeline = [
{
date: "高一 · 九月",
title: "第一次点名",
text: "很多名字还对不上脸,但同一间教室已经开始收藏我们的吵闹、紧张和新鲜感。",
href: "/gallery/classroom/"
date: "2021年8月",
title: "进入高中",
text: "2021年8月,我们进入高中,举行军训等活动。"
},
{
date: "高二 · 秋天",
title: "运动会和晚自习后的风",
text: "有人在跑道上冲线,有人在看台上喊到嗓子哑。那天的风,后来吹进了很多照片里。",
href: "/gallery/events/"
date: "2021年12月31日",
title: "元旦歌咏比赛",
text: "2021年12月31日进行元旦歌咏比赛。"
},
{
date: "高三 · 春天",
title: "倒计时牌越来越小",
text: "黑板角落的数字每天少一点,我们一边嫌累,一边悄悄把彼此记得更牢。",
href: "/messages/"
date: "2022年1月",
title: "文理分科、分班",
text: "2022年1月,进行文理分科、分班。"
},
{
date: "毕业 · 六月",
title: "合照里的那个下午",
text: "校服、签名、拥抱和没说完的话,都被按下快门,留在了那一年最亮的地方。",
href: "/gallery/graduation-day/"
date: "2022年6月7日",
title: "部分同学《将进酒》朗诵比赛",
text: "陈昕楠、王雪婧、张帆、王琳柯、谭宇辉等同学参加《将进酒》朗诵比赛。"
},
{
date: "2022年7月8日",
title: "高一下期末考试表彰",
text: "因为成绩不错,牛坤霖老师组织班级内表彰,同时准备了节目。"
},
{
date: "2022年11月18日",
title: "网课时代",
text: "因为一些原因,开启了三个月的网课时代。"
},
{
date: "2023年7月6日",
title: "高二升高三活动",
text: "牛坤霖老师举办了一场最大的活动,各位同学积极参与。"
},
{
date: "2023年8月",
title: "升入高三",
text: "2023年8月,我们正式升入高三。"
},
{
date: "2024年2月",
title: "最短寒假",
text: "高中最后一个寒假,也是最短的一个。"
},
{
date: "2024年2月28日",
title: "百日誓师",
text: "距离高考还有最后一百天!!!"
},
{
date: "2024年6月6日",
title: "高考壮行",
text: "2024年6月6日,我们为高考壮行。"
},
{
date: "2024年6月7、8日",
title: "高考",
text: "2024年6月7日、8日,我们走进高考考场。"
},
{
date: "2024年6月9日",
title: "毕业典礼",
text: "我们毕业了!"
}
];
+59 -2
View File
@@ -1,12 +1,23 @@
---
import "../styles/global.css";
import { navItems, site } from "../data/site";
import packageJson from "../../package.json";
import { contactLinks, navItems, site } from "../data/site";
interface Props {
title?: string;
}
const { title = site.title } = Astro.props;
const buildTime = new Intl.DateTimeFormat("zh-CN", {
timeZone: "Asia/Shanghai",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}).format(new Date());
---
<!doctype html>
@@ -15,6 +26,14 @@ const { title = site.title } = Astro.props;
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
/>
</head>
<body>
<header class="site-header" aria-label="网站导航">
@@ -37,7 +56,35 @@ const { title = site.title } = Astro.props;
<slot />
<footer>{site.footer}</footer>
<footer class="site-footer">
<p>{site.footer}</p>
<p class="footer-meta">v{packageJson.version} · 构建于 {buildTime}</p>
<nav class="footer-contacts" aria-label="联系方式">
{
contactLinks.map((item) => (
<a
href={item.href}
class:list={[item.qrImage && "has-qr"]}
aria-label={item.label}
title={item.label}
target={item.href.startsWith("http") ? "_blank" : undefined}
rel={item.href.startsWith("http") ? "noreferrer" : undefined}
>
<i class={item.icon} aria-hidden="true"></i>
<span>{item.label}</span>
{
item.qrImage && (
<span class="contact-qr" aria-hidden="true">
<img src={item.qrImage} alt="" loading="lazy" />
<small>{item.label}</small>
</span>
)
}
</a>
))
}
</nav>
</footer>
<script>
const header = document.querySelector(".site-header");
const toggle = document.querySelector(".nav-toggle");
@@ -69,5 +116,15 @@ const { title = site.title } = Astro.props;
});
}
</script>
<script
is:inline
src="https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"
></script>
<script is:inline>
window.Fancybox?.bind("[data-fancybox]", {
animated: true,
dragToClose: true
});
</script>
</body>
</html>
+21 -4
View File
@@ -29,17 +29,34 @@ const { topic } = Astro.props;
<div class="scrapbook">
{
topic.photos.map((photo) => (
<article
class="scrap-photo"
style={photo.image ? `--scrap-image: url("${photo.image}")` : ""}
<article class="scrap-photo">
{photo.image ? (
<a
class="scrap-frame scrap-link"
href={photo.image}
data-fancybox={`gallery-${topic.slug}`}
data-caption={`${photo.title} - ${photo.caption}`}
>
<img
class="scrap-image scrap-image-file"
src={photo.image}
alt={photo.title}
loading="lazy"
/>
<div class="scrap-copy">
<strong>{photo.title}</strong>
<span>{photo.caption}</span>
</div>
</a>
) : (
<div class="scrap-frame">
<div class="scrap-image" aria-hidden="true"></div>
<div class="scrap-image scrap-placeholder" aria-hidden="true"></div>
<div class="scrap-copy">
<strong>{photo.title}</strong>
<span>{photo.caption}</span>
</div>
</div>
)}
</article>
))
}
+5 -104
View File
@@ -2,6 +2,7 @@
import BaseLayout from "../layouts/BaseLayout.astro";
import { featuredMessage, messagesIntro, twikooConfig } from "../data/messages";
import { site } from "../data/site";
import twikooThemeUrl from "../styles/twikoo2.css";
---
<BaseLayout title={`${messagesIntro.title} · ${site.className}`}>
@@ -25,7 +26,7 @@ import { site } from "../data/site";
{
twikooConfig.envId ? (
<div
id="tcomment"
id="twikoo"
class="twikoo-sticky-wall"
data-env-id={twikooConfig.envId}
data-path={twikooConfig.path}
@@ -47,111 +48,11 @@ import { site } from "../data/site";
{
twikooConfig.envId && (
<>
<link rel="stylesheet" href={twikooConfig.styleSrc} />
<style is:inline>
#tcomment.twikoo-sticky-wall {
--sticky-1: #fff1a8;
--sticky-2: #dff1c7;
--sticky-3: #d8ebf7;
--sticky-4: #f8d7d0;
--sticky-5: #eadcf4;
--sticky-tape: rgba(255, 255, 255, 0.62);
}
#tcomment.twikoo-sticky-wall .tk-submit {
margin-bottom: 2rem !important;
padding: 1.25rem !important;
border: 1px solid var(--line) !important;
border-radius: 8px !important;
background: #fffdf7 !important;
box-shadow: var(--shadow) !important;
}
#tcomment.twikoo-sticky-wall .tk-send {
border-color: var(--green) !important;
background: var(--green) !important;
color: #fffdf7 !important;
font-weight: 800 !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-title {
margin: 1.5rem 0 0.75rem !important;
color: var(--green) !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment {
position: relative !important;
margin-top: 1.35rem !important;
padding: 1.25rem !important;
border-radius: 3px !important;
background: var(--sticky-1) !important;
box-shadow:
0 16px 30px rgba(39, 55, 52, 0.14),
inset 0 -18px 28px rgba(255, 255, 255, 0.18) !important;
transform: rotate(-0.7deg) !important;
transform-origin: center !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment:nth-of-type(5n + 2) {
background: var(--sticky-2) !important;
transform: translateY(6px) rotate(0.6deg) !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment:nth-of-type(5n + 3) {
background: var(--sticky-3) !important;
transform: rotate(-0.3deg) !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment:nth-of-type(5n + 4) {
background: var(--sticky-4) !important;
transform: translateY(8px) rotate(0.8deg) !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment:nth-of-type(5n) {
background: var(--sticky-5) !important;
transform: rotate(-0.5deg) !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment::before {
content: "" !important;
position: absolute !important;
top: 0.45rem !important;
left: 50% !important;
width: 3.5rem !important;
height: 0.8rem !important;
background: var(--sticky-tape) !important;
box-shadow: 0 1px 4px rgba(39, 55, 52, 0.13) !important;
transform: translateX(-50%) rotate(2deg) !important;
pointer-events: none !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment .tk-content,
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment .tk-content p {
color: var(--ink) !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment .tk-replies .tk-comment {
background: rgba(255, 253, 247, 0.6) !important;
border-radius: 6px !important;
padding: 0.75rem !important;
transform: none !important;
box-shadow: none !important;
}
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment .tk-replies .tk-comment::before {
content: none !important;
}
@media (max-width: 560px) {
#tcomment.twikoo-sticky-wall .tk-comments-container > .tk-comment {
transform: none !important;
}
}
</style>
<link rel="stylesheet" href={twikooThemeUrl} />
<script is:inline src={twikooConfig.scriptSrc} defer></script>
<script is:inline>
window.addEventListener("DOMContentLoaded", function () {
const container = document.querySelector("#tcomment");
const container = document.querySelector("#twikoo");
if (!container || !window.twikoo) {
return;
@@ -159,7 +60,7 @@ import { site } from "../data/site";
window.twikoo.init({
envId: container.dataset.envId,
el: "#tcomment",
el: "#twikoo",
path: container.dataset.path || location.pathname,
lang: container.dataset.lang || "zh-CN"
});
+18
View File
@@ -17,6 +17,24 @@ import { site } from "../data/site";
<section>
<div class="section-inner">
<aside class="data-guide" aria-labelledby="people-data-guide-title">
<div>
<p class="eyebrow">Data Guide</p>
<h2 id="people-data-guide-title">修改或添加同学数据</h2>
<p>
所有同学卡片都来自 <code>src/data/people.ts</code>。修改已有同学时,直接编辑对应对象;添加新同学时,复制一个对象并更换
<code>slug</code>、<code>name</code>、<code>location</code>、<code>school</code>、<code>direction</code>
等字段。
</p>
</div>
<ol>
<li><strong>自动提交:</strong>也可以在 GitHub 新建“更新「如今的我们」”issue,填完表单后会自动创建一个更新数据的 PR。</li>
<li><strong>照片:</strong><code>photo</code> 可留空;如需放照片,将图片放入 <code>public</code> 后填写以 <code>/</code> 开头的路径。</li>
<li><strong>关键词:</strong><code>keywords</code> 是数组,建议保留 1 到 3 个短词,列表页和个人页都会显示。</li>
<li><strong>个人页:</strong><code>currentStatus</code>、<code>highlight</code>、<code>toPastSelf</code>、<code>favoriteMemory</code> 和 <code>messageToClass</code> 会生成详情内容。</li>
</ol>
</aside>
<div class="people-grid page-grid">
{
people.map((person) => (
+191 -8
View File
@@ -419,6 +419,51 @@ h2 {
gap: 16px;
}
.data-guide {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(280px, 1.1fr);
gap: clamp(22px, 4vw, 46px);
align-items: start;
margin-bottom: 26px;
padding: clamp(22px, 4vw, 32px);
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
box-shadow: 0 8px 28px rgba(39, 55, 52, 0.06);
}
.data-guide h2 {
margin-bottom: 14px;
font-size: clamp(24px, 3vw, 34px);
}
.data-guide p {
margin: 0;
color: var(--muted);
}
.data-guide code {
color: var(--green);
font-weight: 800;
}
.data-guide ol {
display: grid;
gap: 12px;
margin: 0;
padding-left: 22px;
color: var(--muted);
}
.data-guide li::marker {
color: var(--green);
font-weight: 800;
}
.data-guide strong {
color: var(--ink);
}
.page-grid {
align-items: stretch;
}
@@ -480,6 +525,7 @@ h2 {
}
.scrap-frame {
display: block;
padding: 10px 10px 16px;
border: 1px solid rgba(31, 43, 42, 0.1);
border-radius: 4px;
@@ -489,23 +535,46 @@ h2 {
inset 0 0 0 1px rgba(255, 255, 255, 0.7);
}
.scrap-link {
cursor: zoom-in;
transition:
box-shadow 180ms ease,
transform 180ms ease;
}
.scrap-link:hover {
transform: translateY(-3px);
box-shadow:
0 24px 50px rgba(39, 55, 52, 0.18),
inset 0 0 0 1px rgba(255, 255, 255, 0.7);
}
.scrap-image {
min-height: clamp(190px, 24vw, 320px);
width: 100%;
border-radius: 3px;
}
.scrap-image-file {
height: auto;
background: #f7f2e7;
}
.scrap-placeholder {
min-height: clamp(190px, 24vw, 320px);
background: var(--scrap-image, linear-gradient(135deg, #376d5a, #e8a84c));
background-size: cover;
background-position: center;
}
.scrap-photo:nth-child(4n + 2) .scrap-image {
.scrap-photo:nth-child(4n + 2) .scrap-placeholder {
background: var(--scrap-image, linear-gradient(135deg, #456f94, #f0c66d));
}
.scrap-photo:nth-child(4n + 3) .scrap-image {
.scrap-photo:nth-child(4n + 3) .scrap-placeholder {
background: var(--scrap-image, linear-gradient(135deg, #c96452, #f1d9a6));
}
.scrap-photo:nth-child(4n) .scrap-image {
.scrap-photo:nth-child(4n) .scrap-placeholder {
background: var(--scrap-image, linear-gradient(135deg, #263e5c, #7ab28b));
}
@@ -790,14 +859,127 @@ h2 {
color: rgba(255, 253, 247, 0.78);
}
footer {
padding: 24px;
.site-footer {
display: grid;
justify-items: center;
gap: 14px;
padding: 28px 24px;
color: rgba(255, 253, 247, 0.58);
background: #1f2b2a;
text-align: center;
font-size: 14px;
}
.site-footer p {
margin: 0;
}
.footer-meta {
color: rgba(255, 253, 247, 0.42);
font-size: 12px;
}
.footer-contacts {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.footer-contacts a {
position: relative;
min-width: 42px;
min-height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid rgba(255, 253, 247, 0.18);
border-radius: 8px;
color: #fffdf7;
background: rgba(255, 253, 247, 0.06);
transition:
background 180ms ease,
border-color 180ms ease,
transform 180ms ease;
}
.footer-contacts .has-qr {
cursor: default;
}
.footer-contacts a:hover {
transform: translateY(-2px);
border-color: rgba(241, 201, 121, 0.58);
background: rgba(255, 253, 247, 0.12);
}
.footer-contacts i {
font-size: 18px;
}
.footer-contacts span {
font-size: 13px;
font-weight: 700;
}
.contact-qr {
position: absolute;
left: 50%;
bottom: calc(100% + 12px);
z-index: 5;
width: 156px;
display: grid;
gap: 8px;
justify-items: center;
padding: 12px;
border: 1px solid rgba(31, 43, 42, 0.12);
border-radius: 8px;
background: #fffdf7;
color: var(--ink);
box-shadow: 0 18px 44px rgba(13, 20, 19, 0.28);
opacity: 0;
pointer-events: none;
transform: translate(-50%, 8px);
transition:
opacity 180ms ease,
transform 180ms ease;
}
.contact-qr::after {
content: "";
position: absolute;
left: 50%;
bottom: -7px;
width: 14px;
height: 14px;
background: #fffdf7;
border-right: 1px solid rgba(31, 43, 42, 0.12);
border-bottom: 1px solid rgba(31, 43, 42, 0.12);
transform: translateX(-50%) rotate(45deg);
}
.contact-qr img {
width: 132px;
height: 132px;
object-fit: contain;
border-radius: 4px;
background: #f4f7ee;
}
.contact-qr small {
color: var(--muted);
font-size: 12px;
line-height: 1.3;
}
.footer-contacts a:hover .contact-qr,
.footer-contacts a:focus-visible .contact-qr {
opacity: 1;
transform: translate(-50%, 0);
}
@media (max-width: 820px) {
.site-header {
position: absolute;
@@ -879,7 +1061,8 @@ footer {
.twikoo-sticky-wall .tk-comments-container,
.twikoo-sticky-wall .tk-comments-list,
.person-hero-inner,
.detail-grid {
.detail-grid,
.data-guide {
grid-template-columns: 1fr;
}
@@ -955,7 +1138,7 @@ footer {
transform: none !important;
}
.scrap-image {
.scrap-placeholder {
min-height: 220px;
}
}
+224
View File
@@ -0,0 +1,224 @@
/* ========================================
Twikoo 便利贴主题 CSS
覆盖原始样式粘贴到博客自定义 CSS 即可
======================================== */
/* 便利贴颜色变量 */
#twikoo {
--tk-note-1: #fff9c4;
--tk-note-2: #d7f7c2;
--tk-note-3: #cce8ff;
--tk-note-4: #f0d6ff;
--tk-note-5: #ffe4cc;
--tk-note-shadow: 2px 3px 0 rgba(0, 0, 0, 0.10), 0 1px 0 rgba(0, 0, 0, 0.06);
--tk-note-hover-shadow: 4px 10px 20px rgba(0, 0, 0, 0.16);
}
/* 评论容器:瀑布流/网格布局 */
#twikoo .tk-comments-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
align-items: start;
min-height: 10rem;
}
/* 隐藏原有竖向分隔线 */
#twikoo .tk-comment {
margin-top: 0;
}
/* ---- 单张便利贴 ---- */
#twikoo .tk-comment {
display: flex;
flex-direction: column;
position: relative;
border-radius: 2px;
padding: 14px 14px 36px;
box-shadow: var(--tk-note-shadow);
transition: transform 0.18s ease, box-shadow 0.18s ease, z-index 0s;
cursor: default;
min-height: 130px;
border: none;
word-break: break-word;
/* 去掉原来的 flex-direction: row */
}
/* 五色轮换 */
#twikoo .tk-comment:nth-child(5n+1) { background: var(--tk-note-1); transform: rotate(-1.2deg); }
#twikoo .tk-comment:nth-child(5n+2) { background: var(--tk-note-2); transform: rotate(0.7deg); }
#twikoo .tk-comment:nth-child(5n+3) { background: var(--tk-note-3); transform: rotate(-0.4deg); }
#twikoo .tk-comment:nth-child(5n+4) { background: var(--tk-note-4); transform: rotate(1.3deg); }
#twikoo .tk-comment:nth-child(5n+5) { background: var(--tk-note-5); transform: rotate(-0.8deg); }
/* 悬停浮起 */
#twikoo .tk-comment:hover {
transform: rotate(0deg) scale(1.04) translateY(-4px) !important;
box-shadow: var(--tk-note-hover-shadow);
z-index: 20;
}
/* 顶部胶带条 */
#twikoo .tk-comment::before {
content: '';
position: absolute;
top: -11px;
left: 50%;
transform: translateX(-50%);
width: 48px;
height: 22px;
background: rgba(255, 255, 255, 0.55);
border-radius: 1px;
border: 0.5px solid rgba(0, 0, 0, 0.08);
box-shadow: none;
}
/* 底部折角效果 */
#twikoo .tk-comment::after {
content: '';
position: absolute;
bottom: 0;
right: 0;
width: 0;
height: 0;
border-style: solid;
border-width: 0 0 16px 16px;
border-color: transparent transparent rgba(0, 0, 0, 0.10) transparent;
}
/* ---- 头像区域 ---- */
#twikoo .tk-comment > .tk-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 0;
margin-bottom: 8px;
flex-shrink: 0;
overflow: hidden;
background: rgba(0, 0, 0, 0.10);
}
#twikoo .tk-comment > .tk-avatar .tk-avatar-img {
height: 30px;
width: 30px;
}
/* 主体内容区 */
#twikoo .tk-comment > .tk-main {
width: 100%;
}
/* 昵称 */
#twikoo .tk-comment .tk-nick-link {
font-size: 12px;
font-weight: 700;
color: rgba(0, 0, 0, 0.6);
}
#twikoo .tk-comment .tk-nick-link:hover {
color: rgba(0, 0, 0, 0.85);
}
/* 操作按钮(点赞等)浮在右侧 */
#twikoo .tk-comment .tk-row {
flex-wrap: wrap;
}
/* 正文内容 */
#twikoo .tk-comment .tk-content {
margin-top: 6px;
font-size: 13px;
line-height: 1.65;
color: rgba(0, 0, 0, 0.75);
max-height: 200px;
overflow: hidden;
}
/* 元信息(时间、地区等) */
#twikoo .tk-comment .tk-extras {
position: absolute;
bottom: 8px;
left: 14px;
right: 20px;
font-size: 10px;
color: rgba(0, 0, 0, 0.38);
flex-wrap: nowrap;
overflow: hidden;
margin: 0;
}
#twikoo .tk-comment .tk-extra {
margin-top: 0;
margin-right: 6px;
}
/* 点赞按钮颜色适配 */
#twikoo .tk-comment .tk-action-link {
color: rgba(0, 0, 0, 0.4);
}
#twikoo .tk-comment .tk-action-icon {
color: rgba(0, 0, 0, 0.4);
}
/* ---- 回复子评论 ---- */
#twikoo .tk-comment .tk-replies {
margin-top: 8px;
max-height: none;
overflow: visible;
padding-left: 8px;
border-left: 2px solid rgba(0, 0, 0, 0.10);
}
/* ---- 评论框 ---- */
#twikoo .tk-submit {
grid-column: 1 / -1;
background: rgba(255, 255, 255, 0.6);
border: 1.5px dashed rgba(0, 0, 0, 0.15);
border-radius: 2px;
padding: 1rem;
box-shadow: none;
}
/* ---- 分页 ---- */
#twikoo .tk-pagination {
grid-column: 1 / -1;
}
/* ---- 深色模式适配 ---- */
@media (prefers-color-scheme: dark) {
#twikoo {
--tk-note-1: #4a4520;
--tk-note-2: #1f3b1a;
--tk-note-3: #1a2e42;
--tk-note-4: #32204a;
--tk-note-5: #3f2b18;
--tk-note-shadow: 2px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 0 rgba(0, 0, 0, 0.3);
}
#twikoo .tk-comment .tk-nick-link { color: rgba(255, 255, 255, 0.75); }
#twikoo .tk-comment .tk-content { color: rgba(255, 255, 255, 0.80); }
#twikoo .tk-comment .tk-extras { color: rgba(255, 255, 255, 0.40); }
#twikoo .tk-comment .tk-action-link,
#twikoo .tk-comment .tk-action-icon { color: rgba(255, 255, 255, 0.45); }
#twikoo .tk-comment::before { background: rgba(255, 255, 255, 0.15); }
}
/* 主题框架深色模式(Hexo/Hugo 常见的 class */
.night #twikoo,
.darkmode #twikoo,
.DarkMode #twikoo,
[data-theme="dark"] #twikoo,
[data-user-color-scheme="dark"] #twikoo {
--tk-note-1: #4a4520;
--tk-note-2: #1f3b1a;
--tk-note-3: #1a2e42;
--tk-note-4: #32204a;
--tk-note-5: #3f2b18;
}
/* ---- 移动端:单列 ---- */
@media screen and (max-width: 600px) {
#twikoo .tk-comments-container {
grid-template-columns: 1fr;
}
#twikoo .tk-comment {
transform: rotate(0deg) !important;
}
}