Compare commits
13 Commits
2a59747de5
...
master
@@ -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": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -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)}`);
|
||||
@@ -0,0 +1,298 @@
|
||||
export const examIntro = {
|
||||
title: "综合素质检测",
|
||||
eyebrow: "Class 612 Exam",
|
||||
description:
|
||||
"一份写给 2024 届 612 班的特别试卷。网页保留正文、插图与公式,原始版式可下载 Word 原卷查看。",
|
||||
downloadHref: "/assets/exam/2024-exam.docx"
|
||||
};
|
||||
|
||||
export const examMath: Record<string, string> = {
|
||||
setA:
|
||||
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi>A</mi><mo>=</mo><mo>{</mo><mn>6</mn><mo>,</mo><mn>1</mn><mo>,</mo><mn>2</mn><mo>,</mo><mn>9</mn><mo>,</mo><mn>8</mn><mo>,</mo><mn>5</mn><mo>,</mo><mn>2</mn><mo>,</mo><mn>1</mn><mo>,</mo><mn>1,6</mn><mo>,</mo><mn>6,6</mn><mo>}</mo></math>',
|
||||
relation:
|
||||
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mo>-</mo><mn>1</mn><mo>≤</mo><mi>a</mi><mo>+</mo><mi>b</mi><mo>≤</mo><mn>4</mn><mo>,</mo><mo>-</mo><mn>1</mn><mo>≤</mo><mi>a</mi><mo>-</mo><mi>b</mi><mo>≤</mo><mn>2</mn></math>',
|
||||
expression:
|
||||
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mn>4</mn><mi>a</mi><mo>-</mo><mn>2</mn><mi>b</mi></math>',
|
||||
optionA:
|
||||
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mo>[</mo><mrow><mo>-</mo><mn>2,10</mn></mrow><mo>]</mo></mrow></math>',
|
||||
optionB:
|
||||
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mo>(</mo><mrow><mo>-</mo><mn>2,10</mn></mrow><mo>]</mo></mrow></math>',
|
||||
optionC:
|
||||
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi>R</mi></math>',
|
||||
optionD:
|
||||
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mi mathvariant="normal">∅</mi></math>',
|
||||
doubleIntegral:
|
||||
'<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>I</mi><mo>=</mo><mrow><msubsup><mo stretchy="false">∫</mo><mrow><mn>0</mn></mrow><mrow><mn>1</mn></mrow></msubsup><mrow><mrow><msubsup><mo stretchy="false">∫</mo><mrow><mn>0</mn></mrow><mrow><mn>1</mn><mo>-</mo><mi>x</mi></mrow></msubsup><mrow><mrow><mo>(</mo><mrow><mi>x</mi><mo>+</mo><mi>y</mi></mrow><mo>)</mo></mrow></mrow></mrow></mrow></mrow><mo> </mo><mi>d</mi><mi>y</mi><mo> </mo><mi>d</mi><mi>x</mi></math>'
|
||||
};
|
||||
|
||||
export const examScoring = {
|
||||
choiceBonus: {
|
||||
label: "选择题全对奖励",
|
||||
points: 10,
|
||||
appliesTo: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
||||
},
|
||||
choiceQuestions: [
|
||||
{ number: 1, points: 5, answer: "B" },
|
||||
{ number: 2, points: 5, answer: "D" },
|
||||
{ number: 3, points: 5, answer: "B" },
|
||||
{ number: 4, points: 5, answer: "A" },
|
||||
{ number: 5, points: 5, answer: "C" },
|
||||
{ number: 6, points: 5, answer: "C" },
|
||||
{ number: 7, points: 5, answer: "D" },
|
||||
{ number: 8, points: 5, answer: "D" },
|
||||
{ number: 9, points: 5, answer: "C" },
|
||||
{ number: 10, points: 5, answer: "A" },
|
||||
{ number: 11, points: 5, answer: "B" },
|
||||
{ number: 12, points: 5, answer: "" , freebie: true},
|
||||
{ number: 18, points: 5, answer: "A" }
|
||||
],
|
||||
fillQuestions: [
|
||||
{
|
||||
number: 13,
|
||||
points: 10,
|
||||
mode: "ordered-blanks",
|
||||
blankPoints: 2,
|
||||
answers: ["既", "也|仍", "只有", "才", "如果|一旦"],
|
||||
note: "按 ①▲ 到 ⑤▲ 的顺序填写答案。"
|
||||
},
|
||||
{
|
||||
number: 14,
|
||||
points: 30,
|
||||
mode: "unordered-names",
|
||||
blankCount: 56,
|
||||
answers: [
|
||||
"安梦伦",
|
||||
"毕爽爽",
|
||||
"陈可",
|
||||
"陈昕楠",
|
||||
"陈雨鑫",
|
||||
"程怡钊",
|
||||
"崔冬",
|
||||
"崔欣钇",
|
||||
"段星浩",
|
||||
"冯梦雪",
|
||||
"冯彦斌",
|
||||
"郭梦帆",
|
||||
"郭思涵",
|
||||
"郭紫瑜",
|
||||
"贺冰冰",
|
||||
"贺秀芸",
|
||||
"霍馨媛",
|
||||
"姬锦琛",
|
||||
"姜康坤",
|
||||
"焦敬泽",
|
||||
"琚泽浩",
|
||||
"李超鹏",
|
||||
"李琛",
|
||||
"李佳琦",
|
||||
"李丽桃",
|
||||
"李顺利",
|
||||
"李潇逸",
|
||||
"李晓坤",
|
||||
"李艳江",
|
||||
"李怡颖",
|
||||
"李宇川",
|
||||
"李炤锦",
|
||||
"路宗谕",
|
||||
"马明宇",
|
||||
"牛梦茹",
|
||||
"牛润锋",
|
||||
"牛温雅",
|
||||
"秦培源",
|
||||
"秦祎岚",
|
||||
"秦泽宇",
|
||||
"谭宇辉",
|
||||
"王德杰",
|
||||
"王琳柯",
|
||||
"王雪婧",
|
||||
"王宇锦",
|
||||
"王子玉",
|
||||
"卫宇坤",
|
||||
"吴子涵",
|
||||
"张晨雨",
|
||||
"张帆",
|
||||
"张妍忆",
|
||||
"张中华",
|
||||
"赵家祺",
|
||||
"赵壬雨",
|
||||
"周钰浩",
|
||||
"朱学航"
|
||||
],
|
||||
penaltyPerMistake: 1,
|
||||
note: "人名无先后顺序;全对 30 分,每错、漏一个扣 1 分,最低分可以为负分。"
|
||||
},
|
||||
{
|
||||
number: 15,
|
||||
points: 20,
|
||||
mode: "image-titles",
|
||||
blankPoints: 5,
|
||||
answers: ["", "", "", ""],
|
||||
freebie: true,
|
||||
note: "按图片顺序填写标题。"
|
||||
}
|
||||
],
|
||||
manualQuestions: [
|
||||
{ number: 16, points: 5 },
|
||||
{ number: 17, points: 5 },
|
||||
{ number: 19, points: 5 },
|
||||
{ number: 20, points: 10 },
|
||||
{ number: 21, points: 10 },
|
||||
{ number: 22, points: 10 },
|
||||
{ number: 23, points: 10 },
|
||||
{ number: 24, points: 40 },
|
||||
{ number: 25, points: 30 }
|
||||
]
|
||||
};
|
||||
|
||||
export const examText = `
|
||||
姓名:准考证号#########座位号##
|
||||
2024届普通高中毕业班综合测试
|
||||
综合素质检测
|
||||
本试卷共8页,25题。全卷满分260分。考试用时150分钟。
|
||||
注意事项:
|
||||
考生要认真检查试题卷、答题卡有无缺印、漏印,监考员下发条形码上信息是否与本人相符,检查完毕后在试题卷、答题卡、草稿纸的相应位置填写信息。
|
||||
在作答选择题时,用2B铅笔将答题卡上对应的答案标号涂黑,如需更改,请用橡皮擦净后改涂其他选项。在作答非选择题时,使用0.5mm 的黑色碳素笔作答;作答在试题卷、草稿纸以及答题卡非作答区域上的答案无效。
|
||||
考试结束后,请将本试题卷和答题卡自行保管。
|
||||
★祝考试顺利!★
|
||||
一、选择题:在下面的小题中每题至少有一项符合题意,每小题5分,共12小题,共60分。漏选得2分,有选错的不得分,全部正确额外加10分。
|
||||
如图为某班某次考试的成绩雷达图,读图并回答1-2小题:
|
||||
[image:/assets/exam/image2.svg:某班某次考试成绩雷达图]
|
||||
[q]下列说法中正确的有
|
||||
①该班成绩中,英语学科总体成绩较好;
|
||||
②该班所有同学英语科成绩一定高于平均分;
|
||||
③没有足够证据表明语文学科与英语学科成绩的方差相同;
|
||||
④在各科成绩中,平均水平最差的为物理科。
|
||||
A.①④B.①③④C.③④D.①②③④
|
||||
[q]请推断该班同学甲的英语科成绩
|
||||
A. 优秀B. 良好C. 较差D. 无法推断
|
||||
[q]集合 [math:setA] 中,元素的个数为
|
||||
A. 9 B. 6 C. 7 D. 8
|
||||
阅读本段材料,回答4-5 小题:
|
||||
假定你正在参加你校毕业典礼,突然你想起数学教师讲的一道题,题目如下:已知不等关系 [math:relation],求 [math:expression] 的取值范围。
|
||||
[q]解答材料中题目
|
||||
A.[math:optionA] B.[math:optionB] C.[math:optionC] D.[math:optionD]
|
||||
[q]在解答与材料中相类似题目时,往往会出错,其中有一种最典型的错误,原因是
|
||||
A. 水平不足B. 题目错了
|
||||
C. 在解答时扩大了𝑎, bD. 从没错过
|
||||
你将阅读一段文字,并在文中的横线上选出恰当的成语。
|
||||
在高中三年中,我们与老师和同学们▲,虽然历经风雨,但我们不轻言放弃、▲,我们始终相信阳光总在风雨后,▲,我们一定会拥有美好的明天!
|
||||
[q]在文中的横线上选出恰当的成语
|
||||
A. 戮力同心坚持不懈苦尽甘来B. 披荆斩棘戮力同心苦尽甘来
|
||||
C. 坚持不懈苦尽甘来戮力同心D. 披荆斩棘坚持不懈戮力同心
|
||||
高中三年,你是否还记得班主任对我们的教诲?回答7-8 小题。
|
||||
[q]在高二年级,班主任来班级的频率
|
||||
A. 全天都在B. 上午会在C. 下午会在D. 经常不在
|
||||
[q]夏季,班主任经常在教室
|
||||
A. 认真工作B. 玩手机C. 睡觉D. 吹空调
|
||||
[q]At the graduation ceremony, we ▲uniform attire and celebrated graduation together.
|
||||
A. take up B. make up C. dress up D. turn up.
|
||||
[q]元旦晚会上,有一首必唱的歌,为
|
||||
A.《高平一中赞歌》B.《高平一中校歌》C. 流行音乐D.《七律·长征》
|
||||
[q]通过十二年的学习,我们收获了很多。下列说法错误的是
|
||||
A. 我们参加了丰富的实践活动,学会了做人,学会了做事
|
||||
B. 我们在学海中泛舟,懂得了书本知识比实践能力更重要
|
||||
C. 我们结交一群志同道合的朋友,在成长道路上风雨同行
|
||||
D. 我们不断地修炼着道德操守,坚持与德并进,与法同行
|
||||
[q]在毕业后我们都各奔东西,不知你是否过上了自己想要的大学生活,现在请评价一下你的大学生活。
|
||||
A.如鱼得水B.不尽人意C.勉强还行D.一塌糊涂
|
||||
无论你的选择如何,都是满分答案。请记住生活中总有美好的事物。不要被当前的不如意所阻碍!
|
||||
二、填空题:共3小题,共50分。
|
||||
[q]在下列材料的空白处填入合适的关联词语(每空2分,共10分):
|
||||
青年人富有理想和抱负,憧憬着美好的未来,这是青年人的特点,①▲是优点。②▲须懂得,个人的抱负不可能孤立地实现,③▲将个人理想同时代和人民的要求紧密结合起来,用自己的知识和本领为祖国、为人民服务,④▲能使自身价值得到充分体现。⑤▲脱离时代,脱离人民,必将一事无成。
|
||||
[q]默写题:在本题中请你默写你班所有同学的姓名,全对得30分,每错、漏一个扣1分,最低分可以为负分,若为负分,则从总分中倒扣。
|
||||
[q]在本题中你将看到几幅图片请为每幅图片编写一个标题,每处5分,共20分。
|
||||
[gallery:/assets/exam/image3.jpg,/assets/exam/image4.jpeg,/assets/exam/image5.jpg,/assets/exam/image6.jpg]
|
||||
三、读·思:共分3部分,共30分。
|
||||
(一)阅读:本部分共两小题,每小题5分,共10分。
|
||||
①白求恩同志毫不利己专门利人的精神,表现在他对工作的极端的负责任,对同志对人民的极端的热忱。每个共产党员都要学习他。不少的人对工作不负责任,拈轻怕重,把重担子推给人家,自己挑轻的。一事当前,先替自己打算,然后再替别人打算。出了一点力就觉得了不起,喜欢自吹,生怕人家不知道。对同志对人民不是满腔热忱,而是冷冷清清,漠不关心,麻木不仁。这种人其实不是共产党员,至少不能算一个纯粹的共产党员。从前线回来的人说到白求恩,没有一个不佩服,没有一个不为他的精神所感动。晋察冀边区的军民,凡亲身受过白求恩医生的治疗和亲眼看过白求恩医生的工作的,无不为之感动。每一个共产党员,一定要学习白求恩同志的这种真正共产主义者的精神。
|
||||
②白求恩同志是个医生,他以医疗为职业,对技术精益求精;在整个八路军医务系统中,他的医术是很高明的。这对于一班见异思迁的人,对于一班鄙薄技术工作以为不足道、以为无出路的人,也是一个极好的教训。
|
||||
[q]简述作者观点。(100字以内)
|
||||
[q]这段话对你有什么启示?(150字以内)
|
||||
(二)文言文阅读:本部分共两小题,每小题5分,共10分。
|
||||
陆壹贰序【现】谭宇辉
|
||||
三载韶光织锦,聚星芒于庠序;一室肝胆同辉,化春雨润青衿。望流云而思俊采,抚书卷以慕长风。敢竭鄙怀,恭疏短引:
|
||||
观夫师道巍巍,各秉圭璋[1]。坤霖骋骏,驰骋绿茵振木铎;玉先雕龙,吞吐诗骚点碧霄。少南布算,谈笑间星河倒转;雪梅沥血,丹忱处桃李无言。永珍叱咤惊雷[2],口劈物理玄关;海燕呢喃细雨,手润化学幽微。英莉探生命之妙,王琼遗蕙兰之芳。至若张琴,申彤余音绕梁,诸师并耀,皆化春泥护新蕊。
|
||||
至若男儿列岫[3],尽展峥嵘。陈可执牛耳而谐六艺,宇辉扫浊尘以净八方。培源擎纛引龙[4]骧虎步,昕楠振玉和鸾[5]凤清声。怡钊苦诣,似精卫填沧海。明宇逐风,若夸父追高日。子涵,泽浩掌戏乾坤,驭电驰霆[6]。宇锦绝笑如云外钟,琳柯诙谐似通灵鬼。宇川冠绝篮场,爽爽解构天机。晨雨,星浩倚案听蝉,枕流漱石鸣[7]。彦斌称恙犹戏谑,顺利蓄髯[8]自轩昂。宇坤扣篮惊鸿影,潇逸裂石遏云歌。超鹏吐凤嘲群彦,泽宇持正镇佞邪。宗谕戏谑藏锥颖,炤锦通达聚虹霓[9]。壬雨,家祺,润锋挟雷吐烈焰[10],融霜成笑;佳琦禅心映佛光,温婉为歌。李琛魁硕,力拔山兮盖世,梦帆易途,满腹玄鸡梗语。且看敬泽,晓坤,学航,思涵,德杰,雨鑫,崔冬,锦琛,祎岚渊默抱璞,伏枥藏[11]。百态峥嵘尽琳琅也。
|
||||
尤慕娥眉璀璨,独占风华。欣钇执印[12],凛若秋霜裁玉律;子玉挥毫,灿如春日照朱阑[13]。冰冰怀瑾,璞玉终耀荆山彩;中华泼墨,才华直夺阆苑[14]色。梦伦渊默参星斗,怡颖凝脂掬月华。紫瑜拈花说岐黄,嫣然解尽三焦语;梦雪赧颊叱风雨[15],红妆偏胜七尺躯。馨媛眸转银汉,惊鸿一瞥倾人国;艳江笑漾梨涡,豆蔻初妆动帝畿[16]。丽桃译鸾通九译,妍忆容华羞洛神。梦茹凌波涉流芳,张帆莲步自生香。雪婧倩笑,云髻明灭半遮面;丹唇外朗,修眉联娟锁珠帘。温雅热忱,光照两性之友,众星拱月,皎若冰凌之心[17]。观其群芳:或如昆山片玉,或似赤水玄珠,皆钟造化之神秀也。
|
||||
辉,三尺微命,一介农子,难忘师生之情,同窗之悦。既天赐文韬,唯以潘江陆海,铭此少年游——
|
||||
教泽深兮沐春阳,同窗契兮凌雪霜。
|
||||
男儿志在拏云手,女儿心凝韫玉光。
|
||||
待乘长风破巨浪,犹记当时明月廊!
|
||||
(选用时有删改)
|
||||
注释:
|
||||
[1]圭璋:一种玉制礼器,象征着美好的品质。
|
||||
[2]叱咤惊雷:有震撼力和威严。
|
||||
[3]列岫:排列的山峰,这里指依次介绍同学。
|
||||
[4]擎纛引龙:举着旗引着龙,形容先锋作用。
|
||||
[5]振玉和鸾:玉饰,车铃的声响,形容好听的声音。
|
||||
[6]掌戏乾坤,驭电驰霆:戏指游戏,电指电子,总体意思就是玩电子游戏。
|
||||
[7]倚案听蝉,枕流漱石鸣:即瞌睡说的雅称。
|
||||
[8]蓄髯:留着胡须。
|
||||
[9]聚虹霓:聚集光彩,指聚集目光,受同学们爱戴。
|
||||
[10]挟雷吐烈焰:性情刚烈。
|
||||
[11]渊默抱璞,伏枥藏锋:闷声干大事。
|
||||
[12]执印:有官威,这里指能很好地管理班级。
|
||||
[13]朱阑:红色的围栏,也指华丽的建筑。
|
||||
[14]阆苑:神仙居住的地方,五光十色,色彩缤纷。
|
||||
[15]赧颊叱风雨:赧颊指害羞,叱风雨指有气势,形容女汉子。
|
||||
[16]帝畿:代指皇帝。
|
||||
[17]冰凌之心:有柔情似水,也有冰凌刀锋,形容爱憎分明。
|
||||
[q]文中描述“男儿列岫,尽展峥嵘”,这句话表达的意思最接近(请将答案填涂在答题卡上):
|
||||
A. 男儿志向高远,展示出非凡的才华B.男儿在困境中展示自我
|
||||
C. 男儿低调内敛,不张扬D. 男儿注重修身,心境高远
|
||||
[q]文中多次提到“诸师并耀”,请结合文意解释这句话的含义,并谈谈其对作者的影响。
|
||||
[q]阅读与改错:本部分中你将阅读五个名句,若有错误请使用修改符号改正,每处2分,共10 分。
|
||||
①少无世俗韵,性本爱秋山。
|
||||
②####难为听,今夜闻君琵琶语,如听仙乐耳暂明。
|
||||
③故不积硅步无以致千里,不积小流无以成江海。
|
||||
④奈何取之尽zi’zhu,用之如泥砂?
|
||||
⑤寄浮游于天地,渺沧海之一粟。
|
||||
四、解答题:共2小题,每小题10分,共20分。
|
||||
[q]计算以下重积分:
|
||||
[math:doubleIntegral]
|
||||
[q]请你写出某个最令你印象深刻的事件,并简要评析。
|
||||
五、综合操作题:共1小题,共10分。
|
||||
[q]本题共五小题,每小题2分,共10分,请你写出正确且合理的操作:
|
||||
(1)假定英语老师正在检查作业,而你恰好忘记写了,请你写出你的应对方式;
|
||||
(2)牛坤霖老师正在检查迟到,而你姗姗来迟,你会怎么解释(辩解)?
|
||||
(3)周练啦!你肯定会看到同学们互帮互助(抄),对此情况,你会怎么做?
|
||||
(4)假定牛坤霖老师制定一些班规,你会有何反应?
|
||||
(5)毕业啦!你有何感想?
|
||||
六、读·写:共1小题,共40分。
|
||||
[q]请在以下A、B两题中选做一题,并将自己选做的题号填涂在答题卡的相应位置上:若有多做,按第一题计分:若考生多涂、漏涂选作标记,按A题计分。
|
||||
A
|
||||
在高中毕业前,我们总是憧憬着大学生活,但是到大学之后一切似乎和自己想的不太一样,老师也说大学才是开始学习的时候,是更苦更累的时候。一些人发出了“要回高中休息”的想法。
|
||||
读了这则材料,你有什么感想?请你写一篇450字左右的作文。
|
||||
要求:结合材料,选好角度,确定立意,明确文体,自拟标题;不要套作,不得抄袭;不得泄露个人信息;不少于450字。
|
||||
B
|
||||
2020年暑假,高考成绩676分,湖南省全省文科排名第四的留守女孩钟芳蓉因选择了北大考古专业而备受关注。钟芳蓉表示,选择该专业是因为自己从小喜欢历史和文物,希望将来能读研深造,做考古研究。对于她的选择,有人为她浪费了高分而感到惋惜,担忧她毕业后的就业前景和经济状况;也有人支持她的选择。
|
||||
读了这则材料,你有什么感想?请你就未来人生规划写一篇450字左右的作文。
|
||||
要求:结合材料,选好角度,确定立意,明确文体,自拟标题;不要套作,不得抄袭;不得泄露个人信息;不少于450字。
|
||||
Ⅶ.Writing,Full mark:30.
|
||||
Direction: in this section, you'll read a passage and then please you continue the story. The words are not limited.
|
||||
From 612 to the Journey Ahead
|
||||
The 612 class, consisting of fifty-six students, embarked on their journey from the first day of school until graduation. The class name "612" became synonymous (同义的) with unity and camaraderie (友情), as students from diverse backgrounds and families came together as a tightly-knit(紧密结合)group.
|
||||
Over the past few years, the 612-class faced numerous challenges and experienced significant growth. They shared countless mornings under the rising sun, confronted the pressures of exams, and celebrated both successes and failures together. Guided by their teachers, they supported and uplifted one another, forging a unique bond.
|
||||
As time passed, the students of 612 began to showcase(展示) their individual talents and potentials. Some excelled academically, becoming the class's academic achievers (学术佼佼者). Others displayed remarkable artistic talents, becoming the artistic pillars (艺术支柱) of the class. Some actively engaged in social activities, becoming the class's pioneers (先锋) in community service. Each person worked hard in their respective fields, adding vibrant colors to the class.
|
||||
As graduation approached, the students of 612 started contemplating their futures. Some dreamed of becoming doctors, contributing to people's health and well-being. Others aspired to be scientists, exploring the mysteries of the unknown. And there were those who yearned to become artists, using their creativity to convey emotions. Regardless of their dreams, they carried with them confidence and passion for the future.
|
||||
On the final day of 612, the classmates gathered to reminisce about their precious time together. They shared stories of personal growth and achievements, filled with deep emotions. Although parting ways was inevitable, they all understood that this class had provided them with endless support and encouragement, leaving an indelible mark (深刻的印记) in their hearts.
|
||||
The future is an unknown realm, brimming with infinite possibilities. The students of 612 believe that no matter how winding (曲折) the path ahead may be, they will march forward with unwavering determination. They will use their efforts and talents to realize their dreams and make meaningful contributions to society.
|
||||
In the closing chapter of their story, the 612 classmates bid farewell, knowing that it marks the beginning of a new journey. They carry their youthful dreams, venturing into a future that awaits them. They believe that, together, they will create a brighter tomorrow, armed with their dedication and aspirations.
|
||||
[q]Continuation: Several years later, you find it while you're sorting through your things, and your mind flips back to that summer…
|
||||
本试题卷到此结束,试题卷与答题卡均无需上交,请妥善保管
|
||||
青春 席慕容
|
||||
所有的结局都已写好
|
||||
所有的泪水也都已启程
|
||||
却忽然忘了是怎么样的一个开始
|
||||
在那个古老的不再回来的夏日
|
||||
无论我如何地去追索
|
||||
年轻的你只如云影掠过
|
||||
而你微笑的面容极浅极淡
|
||||
逐渐隐没在日落后的群岚
|
||||
遂翻开那发黄的扉页
|
||||
命运将它装订的极为拙劣
|
||||
含着泪我一读再读
|
||||
却不得不承认
|
||||
青春是一本太仓促的书
|
||||
[注]那种对青春远逝的无限伤感、那种对生命短暂的无穷幽怨仿佛立刻遮蔽了天空,紧紧攫住了读者的心;就像那起程的泪水汹涌而来,打湿了每一个敏感而脆弱的生命。
|
||||
`;
|
||||
@@ -5,7 +5,7 @@ export const messagesIntro = {
|
||||
|
||||
export const featuredMessage = {
|
||||
text: "后来我们去了不同的地方,但那间教室像一个坐标,提醒我们曾经一起出发。",
|
||||
author: "写给高三 X 班"
|
||||
author: "写给612班"
|
||||
};
|
||||
|
||||
export const messages = [
|
||||
@@ -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
|
||||
];
|
||||
|
||||
+37
-5
@@ -1,23 +1,55 @@
|
||||
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/" },
|
||||
{ label: "照片墙", href: "/gallery/" },
|
||||
{ label: "如今的我们", href: "/people/" },
|
||||
{ label: "综合试卷", href: "/exam/" },
|
||||
{ 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>
|
||||
|
||||
@@ -0,0 +1,781 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import { examIntro, examMath, examScoring, examText } from "../data/exam";
|
||||
import { site } from "../data/site";
|
||||
|
||||
const lines = examText
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const isSectionLine = (line: string) =>
|
||||
/^(一、|二、|三、|四、|五、|六、|Ⅶ\.)/.test(line);
|
||||
|
||||
const imagePattern = /^\[image:(.+?):(.+?)\]$/;
|
||||
const galleryPattern = /^\[gallery:(.+)\]$/;
|
||||
const questionPattern = /^\[q\](.+)$/;
|
||||
const mathPattern = /\[math:([\w-]+)\]/g;
|
||||
const optionPattern = /^[A-D]\./;
|
||||
|
||||
const renderLine = (line: string) =>
|
||||
line.replace(mathPattern, (_, id: string) => examMath[id] ?? "");
|
||||
|
||||
const getOptions = (line: string) => {
|
||||
if (!optionPattern.test(line)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return line.match(/[A-D]\.\s*.*?(?=\s*[A-D]\.|$)/g) ?? [];
|
||||
};
|
||||
|
||||
const getOptionValue = (option: string) => option.match(/^([A-D])\./)?.[1] ?? "";
|
||||
const fillQuestionMap = new Map(examScoring.fillQuestions.map((question) => [question.number, question]));
|
||||
const circledNumbers = ["①", "②", "③", "④", "⑤"];
|
||||
|
||||
const renderQuestion13Line = (line: string) =>
|
||||
line.replace(/([①②③④⑤])▲/g, (_, marker: string) => {
|
||||
const blankIndex = circledNumbers.indexOf(marker);
|
||||
|
||||
return `${marker}<input class="inline-fill-input" data-fill-answer data-question-number="13" data-blank-index="${blankIndex}" aria-label="第 13 题第 ${blankIndex + 1} 空" />`;
|
||||
});
|
||||
|
||||
const objectiveMax =
|
||||
examScoring.choiceQuestions.reduce((sum, question) => sum + question.points, 0) +
|
||||
examScoring.fillQuestions
|
||||
.filter((question) => question.mode !== "unordered-names")
|
||||
.reduce((sum, question) => sum + question.points, 0) +
|
||||
examScoring.choiceBonus.points;
|
||||
const nameQuestionMax =
|
||||
examScoring.fillQuestions.find((question) => question.mode === "unordered-names")?.points ?? 0;
|
||||
const manualMax = examScoring.manualQuestions.reduce((sum, question) => sum + question.points, 0);
|
||||
const totalMax = objectiveMax + nameQuestionMax + manualMax;
|
||||
const admissionNumberLength = 9;
|
||||
const answerCardQuestions = [
|
||||
...examScoring.choiceQuestions.map((question) => ({ ...question, kind: "choice" })),
|
||||
...examScoring.fillQuestions.map((question) => ({ ...question, kind: "fill" })),
|
||||
...examScoring.manualQuestions.map((question) => ({ ...question, kind: "manual" }))
|
||||
].sort((left, right) => left.number - right.number);
|
||||
|
||||
let questionNumber = 0;
|
||||
---
|
||||
|
||||
<BaseLayout title={`${examIntro.title} · ${site.className}`}>
|
||||
<main class="page-main exam-page">
|
||||
<section class="page-hero">
|
||||
<div class="section-inner">
|
||||
<a class="back-link" href="/">返回首页</a>
|
||||
<p class="eyebrow">{examIntro.eyebrow}</p>
|
||||
<h1>{examIntro.title}</h1>
|
||||
<p>{examIntro.description}</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button primary" href={examIntro.downloadHref} download>下载原卷</a>
|
||||
<a class="button" href="#paper">阅读试卷</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="exam-band" id="paper">
|
||||
<div class="section-inner">
|
||||
<div class="exam-workspace">
|
||||
<article class="exam-paper" aria-label="试卷正文">
|
||||
{
|
||||
lines.map((line, index) => {
|
||||
const image = line.match(imagePattern);
|
||||
const gallery = line.match(galleryPattern);
|
||||
const question = line.match(questionPattern);
|
||||
const options = getOptions(line);
|
||||
|
||||
if (image) {
|
||||
return (
|
||||
<figure class="exam-figure">
|
||||
<img src={image[1]} alt={image[2]} loading="lazy" />
|
||||
<figcaption>{image[2]}</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
if (gallery) {
|
||||
const images = gallery[1].split(",");
|
||||
|
||||
return (
|
||||
<div class="exam-image-grid" aria-label="图片标题题配图">
|
||||
{images.map((src, imageIndex) => (
|
||||
<figure>
|
||||
<img src={src} alt={`图片标题题配图 ${imageIndex + 1}`} loading="lazy" />
|
||||
<figcaption>图 {imageIndex + 1}</figcaption>
|
||||
{
|
||||
questionNumber === 15 && (
|
||||
<label class="image-title-answer">
|
||||
<span>标题</span>
|
||||
<input
|
||||
data-fill-answer
|
||||
data-question-number="15"
|
||||
data-blank-index={imageIndex}
|
||||
aria-label={`第 15 题图 ${imageIndex + 1} 标题`}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (question) {
|
||||
questionNumber += 1;
|
||||
const currentQuestionNumber = questionNumber;
|
||||
const fillQuestion = fillQuestionMap.get(currentQuestionNumber);
|
||||
|
||||
return (
|
||||
<div
|
||||
class="exam-question-block"
|
||||
id={`question-${currentQuestionNumber}`}
|
||||
data-question-block
|
||||
data-question-number={currentQuestionNumber}
|
||||
>
|
||||
<p class="exam-question">
|
||||
<span class="exam-question-number">{currentQuestionNumber}</span>
|
||||
<span class="exam-question-text" set:html={renderLine(question[1])} />
|
||||
</p>
|
||||
{
|
||||
fillQuestion?.mode === "unordered-names" ? (
|
||||
<div class="name-fill-grid" aria-label="第 14 题姓名默写作答">
|
||||
{Array.from({ length: fillQuestion.blankCount }, (_, blankIndex) => (
|
||||
<label>
|
||||
<span>{blankIndex + 1}</span>
|
||||
<input
|
||||
data-fill-answer
|
||||
data-question-number={currentQuestionNumber}
|
||||
data-blank-index={blankIndex}
|
||||
aria-label={`第 ${currentQuestionNumber} 题第 ${blankIndex + 1} 个姓名`}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (options.length > 0) {
|
||||
return (
|
||||
<div class="exam-options" aria-label="选项">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
class="exam-option"
|
||||
type="button"
|
||||
data-select-option
|
||||
data-question-number={questionNumber}
|
||||
data-option-value={getOptionValue(option)}
|
||||
set:html={renderLine(option)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
return (
|
||||
<div class="exam-candidate-info" aria-label="考生信息">
|
||||
<label>
|
||||
<span>姓名</span>
|
||||
<input id="candidate-name" autocomplete="name" />
|
||||
</label>
|
||||
<label>
|
||||
<span>准考证号</span>
|
||||
<input
|
||||
id="admission-number"
|
||||
inputmode="numeric"
|
||||
maxlength={admissionNumberLength}
|
||||
pattern={`\\d{${admissionNumberLength}}`}
|
||||
placeholder={"#".repeat(admissionNumberLength)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>座位号</span>
|
||||
<input id="seat-number" inputmode="numeric" maxlength="2" readonly placeholder="末两位" />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (index < 3) {
|
||||
return (
|
||||
<p
|
||||
class:list={["exam-title-line", index === 1 && "major"]}
|
||||
set:html={renderLine(line)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSectionLine(line)) {
|
||||
return <h2>{line}</h2>;
|
||||
}
|
||||
|
||||
if (line === "注意事项:" || line === "注释:") {
|
||||
return <h3>{line}</h3>;
|
||||
}
|
||||
|
||||
if (questionNumber === 13 && line.includes("▲")) {
|
||||
return <p class="fill-inline-text" set:html={renderQuestion13Line(line)} />;
|
||||
}
|
||||
|
||||
return <p set:html={renderLine(line)} />;
|
||||
})
|
||||
}
|
||||
</article>
|
||||
|
||||
<aside class="score-sidebar" id="scorer" aria-label="答题卡">
|
||||
<form class="score-panel" id="exam-scorer">
|
||||
<div class="score-sidebar-head">
|
||||
<p class="eyebrow">Answer Sheet</p>
|
||||
<h2>答题卡</h2>
|
||||
<p>题号颜色会提示是否作答,点击题号可跳到对应题目。</p>
|
||||
</div>
|
||||
|
||||
<section class="score-group answer-card-profile" aria-label="考生信息">
|
||||
<dl>
|
||||
<div>
|
||||
<dt>姓名</dt>
|
||||
<dd id="card-candidate-name">未填写</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>准考证号</dt>
|
||||
<dd id="card-admission-number">未填写</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>座位号</dt>
|
||||
<dd id="card-seat-number">未填写</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="score-group" aria-labelledby="answer-nav-title">
|
||||
<h3 id="answer-nav-title">题号</h3>
|
||||
<div class="answer-card-grid">
|
||||
{
|
||||
answerCardQuestions.map((question) => (
|
||||
<button
|
||||
class="answer-card-number"
|
||||
type="button"
|
||||
data-answer-jump
|
||||
data-kind={question.kind}
|
||||
data-question-number={question.number}
|
||||
aria-label={`跳到第 ${question.number} 题`}
|
||||
>
|
||||
{question.number}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="answer-card-legend" aria-label="答题状态说明">
|
||||
<span><i class="is-unanswered"></i>未答</span>
|
||||
<span><i class="is-answered"></i>已答</span>
|
||||
<span><i class="is-manual"></i>手评</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="score-group" aria-labelledby="manual-score-title">
|
||||
<h3 id="manual-score-title">主观题</h3>
|
||||
<div class="manual-score-grid">
|
||||
{
|
||||
examScoring.manualQuestions.map((question) => (
|
||||
<label class="manual-score-item">
|
||||
<span>{question.number} 题</span>
|
||||
<input
|
||||
data-manual-score
|
||||
data-points={question.points}
|
||||
type="number"
|
||||
min="0"
|
||||
max={question.points}
|
||||
step="0.5"
|
||||
value="0"
|
||||
/>
|
||||
<small>/ {question.points}</small>
|
||||
</label>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button class="score-submit" type="submit">提交</button>
|
||||
|
||||
<div class="score-total" aria-live="polite">
|
||||
<div>
|
||||
<span>客观题</span>
|
||||
<strong><output id="objective-score">0</output><small> / {objectiveMax}</small></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>姓名题</span>
|
||||
<strong><output id="name-score">0</output><small> / {nameQuestionMax}</small></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>主观题</span>
|
||||
<strong><output id="manual-score">0</output><small> / {manualMax}</small></strong>
|
||||
</div>
|
||||
<div class="score-total-final">
|
||||
<span>总分</span>
|
||||
<strong><output id="total-score">0</output><small> / {totalMax}</small></strong>
|
||||
</div>
|
||||
</div>
|
||||
<p class="score-total-note">14 题为姓名题,单独计算得分;罚分只从姓名题和总分中体现,不计入客观题。</p>
|
||||
</form>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script is:inline>
|
||||
window.MathJax = {
|
||||
startup: {
|
||||
typeset: true
|
||||
},
|
||||
chtml: {
|
||||
matchFontHeight: false
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script
|
||||
is:inline
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/mml-chtml.js"
|
||||
defer
|
||||
></script>
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{
|
||||
choiceBonus: examScoring.choiceBonus,
|
||||
choiceQuestions: examScoring.choiceQuestions,
|
||||
fillQuestions: examScoring.fillQuestions,
|
||||
objectiveMax,
|
||||
nameQuestionMax,
|
||||
manualMax,
|
||||
totalMax
|
||||
}}
|
||||
>
|
||||
const scorer = document.querySelector("#exam-scorer");
|
||||
const candidateName = document.querySelector("#candidate-name");
|
||||
const admissionNumber = document.querySelector("#admission-number");
|
||||
const seatNumber = document.querySelector("#seat-number");
|
||||
const cardCandidateName = document.querySelector("#card-candidate-name");
|
||||
const cardAdmissionNumber = document.querySelector("#card-admission-number");
|
||||
const cardSeatNumber = document.querySelector("#card-seat-number");
|
||||
const draftStorageKey = "examDraft";
|
||||
|
||||
const normalizeAnswer = (value) =>
|
||||
String(value || "")
|
||||
.trim()
|
||||
.replace(/\s+/g, "")
|
||||
.toLowerCase();
|
||||
|
||||
const getAcceptedAnswers = (answer) =>
|
||||
(Array.isArray(answer) ? answer : String(answer || "").split("|"))
|
||||
.map(normalizeAnswer)
|
||||
.filter(Boolean);
|
||||
|
||||
const clampScore = (value, max) => {
|
||||
const score = Number(value);
|
||||
|
||||
if (!Number.isFinite(score)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.min(Math.max(score, 0), max);
|
||||
};
|
||||
|
||||
const getDraft = () => ({
|
||||
candidate: {
|
||||
name: candidateName?.value ?? "",
|
||||
admissionNumber: admissionNumber?.value ?? "",
|
||||
seatNumber: seatNumber?.value ?? ""
|
||||
},
|
||||
choices: Object.fromEntries(
|
||||
[...document.querySelectorAll("[data-select-option].is-selected")].map((option) => [
|
||||
option.dataset.questionNumber,
|
||||
option.dataset.optionValue
|
||||
])
|
||||
),
|
||||
fills: Object.fromEntries(
|
||||
[...document.querySelectorAll("[data-fill-answer]")].map((input) => [
|
||||
`${input.dataset.questionNumber}-${input.dataset.blankIndex || 0}`,
|
||||
input.value
|
||||
])
|
||||
),
|
||||
manualScores: Object.fromEntries(
|
||||
[...document.querySelectorAll("[data-manual-score]")].map((input) => {
|
||||
const label = input.closest(".manual-score-item");
|
||||
const numberText = label?.querySelector("span")?.textContent ?? "";
|
||||
const questionNumber = Number(numberText.match(/\d+/)?.[0] ?? 0);
|
||||
|
||||
return [questionNumber, input.value];
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
const saveDraft = () => {
|
||||
localStorage.setItem(draftStorageKey, JSON.stringify(getDraft()));
|
||||
};
|
||||
|
||||
const restoreDraft = () => {
|
||||
const draft = JSON.parse(localStorage.getItem(draftStorageKey) || "null");
|
||||
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (candidateName) {
|
||||
candidateName.value = draft.candidate?.name || "";
|
||||
}
|
||||
|
||||
if (admissionNumber) {
|
||||
admissionNumber.value = draft.candidate?.admissionNumber || "";
|
||||
}
|
||||
|
||||
if (seatNumber) {
|
||||
seatNumber.value = draft.candidate?.seatNumber || admissionNumber?.value.slice(-2) || "";
|
||||
}
|
||||
|
||||
Object.entries(draft.choices || {}).forEach(([questionNumber, optionValue]) => {
|
||||
const option = document.querySelector(
|
||||
`[data-select-option][data-question-number="${questionNumber}"][data-option-value="${optionValue}"]`
|
||||
);
|
||||
|
||||
option?.classList.add("is-selected");
|
||||
});
|
||||
|
||||
Object.entries(draft.fills || {}).forEach(([key, value]) => {
|
||||
const [questionNumber, blankIndex] = key.split("-");
|
||||
const input = document.querySelector(
|
||||
`[data-fill-answer][data-question-number="${questionNumber}"][data-blank-index="${blankIndex}"]`
|
||||
);
|
||||
|
||||
if (input) {
|
||||
input.value = value;
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-manual-score]").forEach((input) => {
|
||||
const label = input.closest(".manual-score-item");
|
||||
const numberText = label?.querySelector("span")?.textContent ?? "";
|
||||
const questionNumber = Number(numberText.match(/\d+/)?.[0] ?? 0);
|
||||
|
||||
if (draft.manualScores?.[questionNumber] != null) {
|
||||
input.value = draft.manualScores[questionNumber];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const syncSelectedOption = (questionNumber, userAnswer) => {
|
||||
document
|
||||
.querySelectorAll(`[data-select-option][data-question-number="${questionNumber}"]`)
|
||||
.forEach((option) =>
|
||||
option.classList.toggle(
|
||||
"is-selected",
|
||||
Boolean(userAnswer) && option.dataset.optionValue === userAnswer
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const syncCandidateInfo = () => {
|
||||
if (cardCandidateName) {
|
||||
cardCandidateName.textContent = candidateName?.value.trim() || "未填写";
|
||||
}
|
||||
|
||||
if (cardAdmissionNumber) {
|
||||
cardAdmissionNumber.textContent = admissionNumber?.value.trim() || "未填写";
|
||||
}
|
||||
|
||||
if (cardSeatNumber) {
|
||||
cardSeatNumber.textContent = seatNumber?.value.trim() || "未填写";
|
||||
}
|
||||
};
|
||||
|
||||
const setAnswerCardState = (questionNumber, isAnswered) => {
|
||||
const cardNumber = document.querySelector(
|
||||
`[data-answer-jump][data-question-number="${questionNumber}"]`
|
||||
);
|
||||
|
||||
cardNumber?.classList.toggle("is-answered", Boolean(isAnswered));
|
||||
};
|
||||
|
||||
const updateAnswerCardStates = () => {
|
||||
choiceQuestions.forEach((question) => {
|
||||
const selectedOption = document.querySelector(
|
||||
`[data-select-option][data-question-number="${question.number}"].is-selected`
|
||||
);
|
||||
|
||||
setAnswerCardState(question.number, Boolean(selectedOption));
|
||||
});
|
||||
|
||||
fillQuestions.forEach((question) => {
|
||||
const inputs = [...document.querySelectorAll(`[data-fill-answer][data-question-number="${question.number}"]`)];
|
||||
|
||||
setAnswerCardState(question.number, inputs.some((input) => input.value.trim()));
|
||||
});
|
||||
|
||||
scorer?.querySelectorAll("[data-manual-score]").forEach((input) => {
|
||||
const label = input.closest(".manual-score-item");
|
||||
const numberText = label?.querySelector("span")?.textContent ?? "";
|
||||
const questionNumber = Number(numberText.match(/\d+/)?.[0] ?? 0);
|
||||
|
||||
if (questionNumber) {
|
||||
setAnswerCardState(questionNumber, Number(input.value) > 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateScores = () => {
|
||||
if (!scorer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let objectiveScore = 0;
|
||||
let manualScore = 0;
|
||||
let choiceScore = 0;
|
||||
let fillScore = 0;
|
||||
let choiceAnswered = 0;
|
||||
let fillAnswered = 0;
|
||||
let manualAnswered = 0;
|
||||
let namePenalty = 0;
|
||||
let nameScore = 0;
|
||||
let rawTotal = 0;
|
||||
const correctChoiceNumbers = new Set();
|
||||
|
||||
choiceQuestions.forEach((question) => {
|
||||
const selectedOption = document.querySelector(
|
||||
`[data-select-option][data-question-number="${question.number}"].is-selected`
|
||||
);
|
||||
const userAnswer = selectedOption?.dataset.optionValue ?? "";
|
||||
|
||||
if (userAnswer) {
|
||||
choiceAnswered += 1;
|
||||
}
|
||||
|
||||
if (question.freebie && userAnswer) {
|
||||
objectiveScore += question.points;
|
||||
choiceScore += question.points;
|
||||
correctChoiceNumbers.add(question.number);
|
||||
return;
|
||||
}
|
||||
|
||||
if (question.answer && userAnswer === question.answer) {
|
||||
objectiveScore += question.points;
|
||||
choiceScore += question.points;
|
||||
correctChoiceNumbers.add(question.number);
|
||||
}
|
||||
});
|
||||
|
||||
fillQuestions.forEach((question) => {
|
||||
const inputs = [...document.querySelectorAll(`[data-fill-answer][data-question-number="${question.number}"]`)]
|
||||
.sort((left, right) => Number(left.dataset.blankIndex || 0) - Number(right.dataset.blankIndex || 0));
|
||||
const answers = Array.isArray(question.answers) ? question.answers : [];
|
||||
|
||||
if (question.mode === "unordered-names") {
|
||||
const standardNames = answers.map(normalizeAnswer).filter(Boolean);
|
||||
const submittedNames = inputs.map((input) => normalizeAnswer(input.value)).filter(Boolean);
|
||||
|
||||
if (submittedNames.length > 0) {
|
||||
fillAnswered += 1;
|
||||
}
|
||||
|
||||
if (standardNames.length > 0) {
|
||||
const standardSet = new Set(standardNames);
|
||||
const submittedSet = new Set(submittedNames);
|
||||
const correctCount = [...submittedSet].filter((name) => standardSet.has(name)).length;
|
||||
const missingCount = Math.max(0, standardSet.size - correctCount);
|
||||
const wrongCount = submittedNames.filter((name) => !standardSet.has(name)).length;
|
||||
namePenalty = (missingCount + wrongCount) * (question.penaltyPerMistake ?? 1);
|
||||
nameScore = question.points - namePenalty;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let questionHasAnswer = false;
|
||||
|
||||
answers.forEach((answer, index) => {
|
||||
const acceptedAnswers = getAcceptedAnswers(answer);
|
||||
const normalizedUserAnswer = normalizeAnswer(inputs[index]?.value ?? "");
|
||||
const blankPoints = question.blankPoints ?? question.points / Math.max(answers.length, 1);
|
||||
|
||||
if (normalizedUserAnswer) {
|
||||
questionHasAnswer = true;
|
||||
}
|
||||
|
||||
if (question.freebie && normalizedUserAnswer) {
|
||||
objectiveScore += blankPoints;
|
||||
fillScore += blankPoints;
|
||||
return;
|
||||
}
|
||||
|
||||
if (acceptedAnswers.length > 0 && acceptedAnswers.includes(normalizedUserAnswer)) {
|
||||
objectiveScore += blankPoints;
|
||||
fillScore += blankPoints;
|
||||
}
|
||||
});
|
||||
|
||||
if (questionHasAnswer) {
|
||||
fillAnswered += 1;
|
||||
}
|
||||
});
|
||||
|
||||
const bonusApplies =
|
||||
choiceBonus.appliesTo.length > 0 &&
|
||||
choiceBonus.appliesTo.every((number) => correctChoiceNumbers.has(number));
|
||||
|
||||
if (bonusApplies) {
|
||||
objectiveScore += choiceBonus.points;
|
||||
choiceScore += choiceBonus.points;
|
||||
}
|
||||
|
||||
scorer.querySelectorAll("[data-manual-score]").forEach((input) => {
|
||||
const points = Number(input.dataset.points || 0);
|
||||
const score = clampScore(input.value, points);
|
||||
|
||||
if (String(input.value) !== String(score)) {
|
||||
input.value = String(score);
|
||||
}
|
||||
|
||||
manualScore += score;
|
||||
|
||||
if (score > 0) {
|
||||
manualAnswered += 1;
|
||||
}
|
||||
});
|
||||
|
||||
const objectiveOutput = document.querySelector("#objective-score");
|
||||
const nameOutput = document.querySelector("#name-score");
|
||||
const manualOutput = document.querySelector("#manual-score");
|
||||
const totalOutput = document.querySelector("#total-score");
|
||||
const subtotal = objectiveScore + nameScore + manualScore;
|
||||
rawTotal = subtotal;
|
||||
const displayTotal = Math.max(0, rawTotal);
|
||||
|
||||
if (objectiveOutput) {
|
||||
objectiveOutput.value = objectiveScore;
|
||||
objectiveOutput.textContent = objectiveScore;
|
||||
}
|
||||
|
||||
if (nameOutput) {
|
||||
nameOutput.value = nameScore;
|
||||
nameOutput.textContent = nameScore;
|
||||
}
|
||||
|
||||
if (manualOutput) {
|
||||
manualOutput.value = manualScore;
|
||||
manualOutput.textContent = manualScore;
|
||||
}
|
||||
|
||||
if (totalOutput) {
|
||||
totalOutput.value = displayTotal;
|
||||
totalOutput.textContent = displayTotal;
|
||||
}
|
||||
|
||||
return {
|
||||
candidate: {
|
||||
name: candidateName?.value.trim() || "未填写",
|
||||
admissionNumber: admissionNumber?.value.trim() || "未填写",
|
||||
seatNumber: seatNumber?.value.trim() || "未填写"
|
||||
},
|
||||
score: {
|
||||
objective: objectiveScore,
|
||||
name: nameScore,
|
||||
manual: manualScore,
|
||||
subtotal,
|
||||
total: displayTotal,
|
||||
rawTotal,
|
||||
objectiveMax,
|
||||
nameQuestionMax,
|
||||
manualMax,
|
||||
totalMax
|
||||
},
|
||||
breakdown: {
|
||||
choiceScore,
|
||||
fillScore,
|
||||
choiceAnswered,
|
||||
choiceTotal: choiceQuestions.length,
|
||||
fillAnswered,
|
||||
fillTotal: fillQuestions.length,
|
||||
manualAnswered,
|
||||
manualTotal: scorer.querySelectorAll("[data-manual-score]").length,
|
||||
bonusApplied: bonusApplies,
|
||||
namePenalty,
|
||||
totalFloored: rawTotal < 0
|
||||
},
|
||||
submittedAt: new Date().toISOString()
|
||||
};
|
||||
};
|
||||
|
||||
scorer?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
const result = updateScores();
|
||||
|
||||
if (result) {
|
||||
sessionStorage.setItem("examResult", JSON.stringify(result));
|
||||
window.location.href = "/exam/result/";
|
||||
}
|
||||
});
|
||||
document.querySelectorAll("[data-select-option]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const questionNumber = button.dataset.questionNumber;
|
||||
const optionValue = button.dataset.optionValue;
|
||||
|
||||
if (!questionNumber || !optionValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll(`[data-select-option][data-question-number="${questionNumber}"]`)
|
||||
.forEach((option) => option.classList.toggle("is-selected", option === button));
|
||||
updateAnswerCardStates();
|
||||
saveDraft();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("[data-fill-answer]").forEach((input) => {
|
||||
input.addEventListener("input", () => {
|
||||
updateAnswerCardStates();
|
||||
saveDraft();
|
||||
});
|
||||
});
|
||||
scorer?.querySelectorAll("[data-manual-score]").forEach((input) => {
|
||||
input.addEventListener("input", () => {
|
||||
updateAnswerCardStates();
|
||||
saveDraft();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("[data-answer-jump]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const question = document.querySelector(`#question-${button.dataset.questionNumber}`);
|
||||
|
||||
question?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start"
|
||||
});
|
||||
});
|
||||
});
|
||||
admissionNumber?.addEventListener("input", () => {
|
||||
admissionNumber.value = admissionNumber.value.replace(/\D/g, "").slice(0, admissionNumber.maxLength);
|
||||
|
||||
if (seatNumber) {
|
||||
seatNumber.value = admissionNumber.value.slice(-2);
|
||||
}
|
||||
|
||||
syncCandidateInfo();
|
||||
saveDraft();
|
||||
});
|
||||
candidateName?.addEventListener("input", () => {
|
||||
syncCandidateInfo();
|
||||
saveDraft();
|
||||
});
|
||||
seatNumber?.addEventListener("input", () => {
|
||||
syncCandidateInfo();
|
||||
saveDraft();
|
||||
});
|
||||
restoreDraft();
|
||||
syncCandidateInfo();
|
||||
updateAnswerCardStates();
|
||||
</script>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import { site } from "../../data/site";
|
||||
---
|
||||
|
||||
<BaseLayout title={`成绩分析 · ${site.className}`}>
|
||||
<main class="page-main">
|
||||
<section class="page-hero result-hero">
|
||||
<div class="section-inner">
|
||||
<a class="back-link" href="/exam/">返回试卷</a>
|
||||
<p class="eyebrow">Result</p>
|
||||
<h1>成绩分析</h1>
|
||||
<p>这里展示本次答题的得分、完成情况和可下载的分享图。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="result-band">
|
||||
<div class="section-inner">
|
||||
<article class="result-card" id="result-card">
|
||||
<div class="result-empty" id="result-empty">
|
||||
<h2>还没有成绩</h2>
|
||||
<p>请先完成试卷并点击答题卡里的“提交”。</p>
|
||||
<a class="section-link" href="/exam/">去答题</a>
|
||||
</div>
|
||||
|
||||
<div class="result-content" id="result-content" hidden>
|
||||
<div class="result-head">
|
||||
<div>
|
||||
<p class="eyebrow">综合素质检测模拟卷</p>
|
||||
<h2 id="result-title">成绩单</h2>
|
||||
</div>
|
||||
<button class="score-submit" type="button" id="download-share">下载分享图</button>
|
||||
</div>
|
||||
|
||||
<dl class="result-profile">
|
||||
<div>
|
||||
<dt>姓名</dt>
|
||||
<dd id="result-name">未填写</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>准考证号</dt>
|
||||
<dd id="result-admission">未填写</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>座位号</dt>
|
||||
<dd id="result-seat">未填写</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="result-score-grid">
|
||||
<div class="result-total">
|
||||
<span>总分</span>
|
||||
<strong><output id="result-total">0</output></strong>
|
||||
<small id="result-total-max">/ 260</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>客观题</span>
|
||||
<strong><output id="result-objective">0</output></strong>
|
||||
<small id="result-objective-max">/ 135</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>主观题</span>
|
||||
<strong><output id="result-manual">0</output></strong>
|
||||
<small id="result-manual-max">/ 125</small>
|
||||
</div>
|
||||
<div class="result-name-score">
|
||||
<span>姓名题</span>
|
||||
<strong><output id="result-name-score">0</output></strong>
|
||||
<small id="result-name-score-max">/ 30</small>
|
||||
</div>
|
||||
</div>
|
||||
<p class="result-score-note">14 题姓名题单独计算,分数可低于 0;客观题不包含该题分数。</p>
|
||||
|
||||
<div class="result-analysis">
|
||||
<h2>完成情况</h2>
|
||||
<div class="analysis-grid">
|
||||
<p><strong id="choice-progress">0 / 0</strong><span>选择题作答</span></p>
|
||||
<p><strong id="fill-progress">0 / 0</strong><span>填空题作答</span></p>
|
||||
<p><strong id="manual-progress">0 / 0</strong><span>主观题给分</span></p>
|
||||
<p><strong id="bonus-status">未获得</strong><span>选择题奖励</span></p>
|
||||
</div>
|
||||
<div class="penalty-note" id="penalty-note" hidden>
|
||||
<strong>14 题罚分:<span id="name-penalty">0</span> 分</strong>
|
||||
<span id="floor-note" hidden>原始总分为负,最终总分已按 0 分记录。</span>
|
||||
</div>
|
||||
<p class="result-comment" id="result-comment"></p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script is:inline>
|
||||
const result = JSON.parse(sessionStorage.getItem("examResult") || "null");
|
||||
const empty = document.querySelector("#result-empty");
|
||||
const content = document.querySelector("#result-content");
|
||||
|
||||
const setText = (selector, value) => {
|
||||
const element = document.querySelector(selector);
|
||||
|
||||
if (element) {
|
||||
element.textContent = String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const getComment = (score, max) => {
|
||||
const ratio = max > 0 ? score / max : 0;
|
||||
|
||||
if (ratio >= 0.9) return "状态拉满,很有当年考场上那股稳劲。";
|
||||
if (ratio >= 0.75) return "整体发挥不错,很多题都稳稳拿住了。";
|
||||
if (ratio >= 0.6) return "完成度还可以,主观题和填空题还能继续捞分。";
|
||||
return "这张卷子本来就带点纪念性质,答完就已经很有参与感。";
|
||||
};
|
||||
|
||||
const drawShareImage = (data) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const scale = window.devicePixelRatio || 1;
|
||||
const width = 960;
|
||||
const height = 1280;
|
||||
canvas.width = width * scale;
|
||||
canvas.height = height * scale;
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.scale(scale, scale);
|
||||
ctx.fillStyle = "#fffdf7";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.fillStyle = "#24443a";
|
||||
ctx.fillRect(0, 0, width, 240);
|
||||
ctx.fillStyle = "#f3cf8b";
|
||||
ctx.font = "700 28px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText("2024届612班", 72, 86);
|
||||
ctx.fillStyle = "#fffdf7";
|
||||
ctx.font = "800 54px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText("综合素质检测模拟卷", 72, 160);
|
||||
ctx.font = "700 28px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText("成绩分析", 72, 208);
|
||||
|
||||
ctx.fillStyle = "#1f2b2a";
|
||||
ctx.font = "700 30px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText(`姓名:${data.candidate.name}`, 72, 310);
|
||||
ctx.fillText(`准考证号:${data.candidate.admissionNumber}`, 72, 360);
|
||||
ctx.fillText(`座位号:${data.candidate.seatNumber}`, 72, 410);
|
||||
|
||||
ctx.fillStyle = "#376d5a";
|
||||
ctx.font = "900 140px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText(String(data.score.total), 72, 610);
|
||||
ctx.font = "800 38px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText(`/ ${data.score.totalMax}`, 340, 590);
|
||||
ctx.fillStyle = "#62706f";
|
||||
ctx.font = "700 28px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText(`客观题 ${data.score.objective} / ${data.score.objectiveMax}`, 72, 690);
|
||||
ctx.fillText(`主观题 ${data.score.manual} / ${data.score.manualMax}`, 72, 740);
|
||||
ctx.fillText(`姓名题 ${data.score.name ?? 0} / ${data.score.nameQuestionMax ?? 0}`, 72, 790);
|
||||
|
||||
ctx.fillStyle = "#f4f7ee";
|
||||
ctx.fillRect(72, 820, 816, 230);
|
||||
ctx.fillStyle = "#1f2b2a";
|
||||
ctx.font = "800 30px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText("完成情况", 110, 880);
|
||||
ctx.font = "700 24px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText(`选择题:${data.breakdown.choiceAnswered} / ${data.breakdown.choiceTotal}`, 110, 935);
|
||||
ctx.fillText(`填空题:${data.breakdown.fillAnswered} / ${data.breakdown.fillTotal}`, 110, 985);
|
||||
ctx.fillText(`主观题:${data.breakdown.manualAnswered} / ${data.breakdown.manualTotal}`, 500, 935);
|
||||
ctx.fillText(`奖励分:${data.breakdown.bonusApplied ? "已获得" : "未获得"}`, 500, 985);
|
||||
|
||||
if (data.breakdown.namePenalty > 0 || data.breakdown.totalFloored) {
|
||||
ctx.fillStyle = "#c96452";
|
||||
ctx.font = "800 24px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText(`14题罚分:${data.breakdown.namePenalty || 0}分`, 110, 1035);
|
||||
|
||||
if (data.breakdown.totalFloored) {
|
||||
ctx.fillText("原始总分为负,最终总分按0分记录", 500, 1035);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#62706f";
|
||||
ctx.font = "700 24px Microsoft YaHei, sans-serif";
|
||||
ctx.fillText("青春是一本太仓促的书,但这张成绩单刚刚好。", 72, 1140);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.download = `612-score-${Date.now()}.png`;
|
||||
link.href = canvas.toDataURL("image/png");
|
||||
link.click();
|
||||
};
|
||||
|
||||
if (result) {
|
||||
empty.hidden = true;
|
||||
content.hidden = false;
|
||||
setText("#result-title", `${result.candidate.name}的成绩单`);
|
||||
setText("#result-name", result.candidate.name);
|
||||
setText("#result-admission", result.candidate.admissionNumber);
|
||||
setText("#result-seat", result.candidate.seatNumber);
|
||||
setText("#result-total", result.score.total);
|
||||
setText("#result-total-max", `/ ${result.score.totalMax}`);
|
||||
setText("#result-objective", result.score.objective);
|
||||
setText("#result-objective-max", `/ ${result.score.objectiveMax}`);
|
||||
setText("#result-manual", result.score.manual);
|
||||
setText("#result-manual-max", `/ ${result.score.manualMax}`);
|
||||
setText("#result-name-score", result.score.name ?? 0);
|
||||
setText("#result-name-score-max", `/ ${result.score.nameQuestionMax ?? 0}`);
|
||||
setText("#choice-progress", `${result.breakdown.choiceAnswered} / ${result.breakdown.choiceTotal}`);
|
||||
setText("#fill-progress", `${result.breakdown.fillAnswered} / ${result.breakdown.fillTotal}`);
|
||||
setText("#manual-progress", `${result.breakdown.manualAnswered} / ${result.breakdown.manualTotal}`);
|
||||
setText("#bonus-status", result.breakdown.bonusApplied ? "已获得" : "未获得");
|
||||
setText("#result-comment", getComment(result.score.total, result.score.totalMax));
|
||||
|
||||
if (result.breakdown.namePenalty > 0 || result.breakdown.totalFloored) {
|
||||
document.querySelector("#penalty-note").hidden = false;
|
||||
setText("#name-penalty", result.breakdown.namePenalty || 0);
|
||||
|
||||
if (result.breakdown.totalFloored) {
|
||||
document.querySelector("#floor-note").hidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector("#download-share")?.addEventListener("click", () => {
|
||||
if (result) {
|
||||
drawShareImage(result);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</BaseLayout>
|
||||
@@ -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}")` : ""}
|
||||
>
|
||||
<div class="scrap-frame">
|
||||
<div class="scrap-image" aria-hidden="true"></div>
|
||||
<div class="scrap-copy">
|
||||
<strong>{photo.title}</strong>
|
||||
<span>{photo.caption}</span>
|
||||
<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 scrap-placeholder" aria-hidden="true"></div>
|
||||
<div class="scrap-copy">
|
||||
<strong>{photo.title}</strong>
|
||||
<span>{photo.caption}</span>
|
||||
</div>
|
||||
</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?url";
|
||||
---
|
||||
|
||||
<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) => (
|
||||
|
||||
+1202
-8
File diff suppressed because it is too large
Load Diff
@@ -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