Compare commits
11 Commits
2a59747de5
...
fb2cfaebf5
@@ -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
|
||||
@@ -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],
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "class-anniversary",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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)}`);
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
@@ -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: ""
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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: ""
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -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: ""
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -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: ""
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -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: ""
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
@@ -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: "我们毕业了!"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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"
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user