diff --git a/database/design.md b/database/design.md
index 508dc80..c9446c4 100644
--- a/database/design.md
+++ b/database/design.md
@@ -85,3 +85,88 @@ _一般来说,一个实体对应一张表,多对多的关系也可对应一
| | created_at | timestamp | | |
注:由于使用的是 PostgreSQL,其`text`类型指长度可变的字符串,与其他数据库可能不同([PostgreSQL: Documentation: 16: Chapter 8. Data Types](https://www.postgresql.org/docs/current/datatype.html))
+
+---
+
+## 作业补充:会议便签纸(选项 C)
+
+> 需求:用户可以为每个会议创建一张便签纸,便签纸仅创建者本人可见。
+
+### 1. 分析需求
+
+在原有「用户注册、登录 / 会议创建、加入 / 会议中的聊天室」基础上新增:
+
+- 用户在某会议中拥有一张自己的便签纸,可在上面记录要点与待办
+- 同一用户在同一会议只有一张便签纸;不同用户的便签互相不可见
+
+### 2.1 标识实体
+
+- 用户(用户名、密码)
+- 会议(名称、介绍、邀请码、创建时间)
+- **便签(内容、创建时间、修改时间)** ← 新增
+
+### 2.2 标识关系
+
+- 用户加入会议(用户——会议)
+- 用户在会议中发出消息(用户——会议——消息)
+- **用户在某会议拥有便签纸(用户——会议——便签)** ← 新增
+ - “每个会议一张”意味着约束 `(user_uuid, room_uuid)` 唯一
+
+### 2.3 E-R 图
+
+```mermaid
+erDiagram
+ user{
+ String username
+ String password
+ }
+ room{
+ String name
+ String intro
+ String invite_code
+ Time created_at
+ }
+ message{
+ String content
+ Time created_at
+ }
+ note{
+ String content
+ Time created_at
+ Time updated_at
+ }
+ user }|--o{ room : join
+ user ||--o{ message : send
+ room ||--o{ message : contain
+ user ||--o{ note : own
+ room ||--o{ note : contain
+```
+
+### 3.1 新增表设计
+
+| 表 | 字段 | 数据类型 | 主键 | 外键 | 约束 |
+| ---- | ---------- | --------- | ---- | --------- | --------------------------------------------- |
+| note | id | uuid | 是 | | 默认 `gen_random_uuid()` |
+| | user_uuid | uuid | | user.uuid | 级联删除/更新;`(user_uuid, room_uuid)` 唯一 |
+| | room_uuid | uuid | | room.uuid | 级联删除/更新 |
+| | content | text | | | |
+| | created_at | timestamp | | | 默认当前时间 |
+| | updated_at | timestamp | | | 默认当前时间,更新时自动刷新 |
+
+### 4. 仅自己可见的两种保障
+
+1. 数据库层:便签行包含 `user_uuid`(属主),查询与修改都带上属主条件;
+2. API 层(Hasura):`user` 角色的 `insert/select/update/delete` 权限均绑定会话变量
+ `X-Hasura-User-Id`,且 `insert` 使用 `set: { user_uuid: X-Hasura-User-Id }`,
+ 客户端无法伪造属主。配置文件见
+ `database/hasura/metadata/databases/workshop/tables/public_note.yaml`。
+
+### 5. 落地文件
+
+| 文件 | 说明 |
+| ---- | ---- |
+| `database/sql/note.sql` | PostgreSQL 版建表 + 触发器 + 示例数据(Hasura/Neon 环境执行) |
+| `database/sql/note_mysql.sql` | 同设计的 MySQL 9.x 版(本机实测通过) |
+| `database/graphql/note.graphql` | 创建/查询/修改/删除便签的 GraphQL 操作 |
+| `database/hasura/metadata/databases/workshop/tables/public_note.yaml` | note 表的 Hasura 元数据与权限 |
+
diff --git a/database/graphql/note.graphql b/database/graphql/note.graphql
new file mode 100644
index 0000000..7fbaa88
--- /dev/null
+++ b/database/graphql/note.graphql
@@ -0,0 +1,53 @@
+# 便签纸由 Hasura 的 insert set 自动写入当前登录用户(见 public_note.yaml),
+# 客户端无需也不能传 user_uuid,因此仅需要 room_uuid 与 content。
+
+mutation createNote($room_uuid: uuid!, $content: String!) {
+ insert_note_one(object: {room_uuid: $room_uuid, content: $content}) {
+ id
+ room_uuid
+ content
+ created_at
+ updated_at
+ }
+}
+
+query getMyNoteInRoom($room_uuid: uuid!) {
+ note(where: {room_uuid: {_eq: $room_uuid}}) {
+ id
+ content
+ created_at
+ updated_at
+ room {
+ uuid
+ name
+ }
+ }
+}
+
+query getMyNotes {
+ note(order_by: {updated_at: desc}) {
+ id
+ room_uuid
+ content
+ created_at
+ updated_at
+ room {
+ uuid
+ name
+ }
+ }
+}
+
+mutation updateNote($id: uuid!, $content: String!) {
+ update_note_by_pk(pk_columns: {id: $id}, _set: {content: $content}) {
+ id
+ content
+ updated_at
+ }
+}
+
+mutation deleteNote($id: uuid!) {
+ delete_note_by_pk(id: $id) {
+ id
+ }
+}
diff --git a/database/hasura/metadata/databases/workshop/tables/public_note.yaml b/database/hasura/metadata/databases/workshop/tables/public_note.yaml
new file mode 100644
index 0000000..b19866b
--- /dev/null
+++ b/database/hasura/metadata/databases/workshop/tables/public_note.yaml
@@ -0,0 +1,53 @@
+table:
+ name: note
+ schema: public
+object_relationships:
+ - name: room
+ using:
+ foreign_key_constraint_on: room_uuid
+ - name: user
+ using:
+ foreign_key_constraint_on: user_uuid
+insert_permissions:
+ - role: user
+ permission:
+ check: {}
+ set:
+ user_uuid: X-Hasura-User-Id
+ columns:
+ - content
+ - room_uuid
+ comment: 便签仅能作为当前登录用户创建(user_uuid 由会话变量写入)
+select_permissions:
+ - role: user
+ permission:
+ columns:
+ - content
+ - created_at
+ - updated_at
+ - id
+ - room_uuid
+ - user_uuid
+ filter:
+ user_uuid:
+ _eq: X-Hasura-User-Id
+ comment: 只能看到自己创建的便签(仅自己可见)
+update_permissions:
+ - role: user
+ permission:
+ columns:
+ - content
+ filter:
+ user_uuid:
+ _eq: X-Hasura-User-Id
+ check:
+ user_uuid:
+ _eq: X-Hasura-User-Id
+ comment: 只能修改自己便签的内容
+delete_permissions:
+ - role: user
+ permission:
+ filter:
+ user_uuid:
+ _eq: X-Hasura-User-Id
+ comment: 只能删除自己的便签
diff --git a/database/hasura/metadata/databases/workshop/tables/tables.yaml b/database/hasura/metadata/databases/workshop/tables/tables.yaml
index c4eb8a7..921ee3a 100644
--- a/database/hasura/metadata/databases/workshop/tables/tables.yaml
+++ b/database/hasura/metadata/databases/workshop/tables/tables.yaml
@@ -1,4 +1,5 @@
- "!include public_message.yaml"
+- "!include public_note.yaml"
- "!include public_room.yaml"
- "!include public_user.yaml"
- "!include public_user_room.yaml"
diff --git a/database/sql/note.sql b/database/sql/note.sql
new file mode 100644
index 0000000..4c3cad9
--- /dev/null
+++ b/database/sql/note.sql
@@ -0,0 +1,38 @@
+-- PostgreSQL
+-- 第11讲作业(选项 C:会议便签纸)
+-- 需求:用户可以为每个会议创建一张便签纸,便签纸仅自己可见。
+-- * 每人每会议最多一张:由 UNIQUE(user_uuid, room_uuid) 保证
+-- * 仅创建者本人可见:由 Hasura 权限层(session 变量 X-Hasura-User-Id)
+-- 控制,见 database/hasura/metadata/databases/workshop/tables/public_note.yaml
+-- 本文件依赖 user.sql、room.sql 已先执行(外键引用 user、room)。
+create table if not exists public.note (
+ id uuid default gen_random_uuid() not null,
+ user_uuid uuid not null,
+ room_uuid uuid not null,
+ content text not null,
+ created_at timestamp default current_timestamp not null,
+ updated_at timestamp default current_timestamp not null,
+ primary key (id),
+ unique (user_uuid, room_uuid)
+);
+alter table public.note
+add constraint note_user_uuid_fkey foreign key (user_uuid) references public.user (uuid) on update cascade on delete cascade;
+alter table public.note
+add constraint note_room_uuid_fkey foreign key (room_uuid) references public.room (uuid) on update cascade on delete cascade;
+
+-- 触发器:修改便签内容时自动刷新 updated_at
+create or replace function public.note_set_updated_at() returns trigger language plpgsql as $$
+begin
+ new.updated_at := current_timestamp;
+ return new;
+end;
+$$;
+create trigger trg_note_set_updated_at before update on public.note
+for each row execute function public.note_set_updated_at();
+
+insert into public.note (user_uuid, room_uuid, content) values
+('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000001', '大家好,我是张三!这是咱们房间的便签纸。待办:今天 15:00 讨论暑培答辩分组。'),
+('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-100000000001', '收到!我来准备演示用的投影和电脑。'),
+('00000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-100000000001', '(王五)我负责记录会议纪要~'),
+('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000002', '(张三在聊天室1的私人便签)别忘了下周还书。'),
+('00000000-0000-0000-0000-000000000004', '00000000-0000-0000-0000-100000000003', '(赵六)成语接龙战绩存档:心安理得→得过且过→过目不忘。');
diff --git a/database/sql/note_mysql.sql b/database/sql/note_mysql.sql
new file mode 100644
index 0000000..ab7ad05
--- /dev/null
+++ b/database/sql/note_mysql.sql
@@ -0,0 +1,80 @@
+-- MySQL(9.7 实测可用)——第11讲作业(选项 C:会议便签纸)的 MySQL 版本
+-- 官方讲解环境为 PostgreSQL + Hasura,请优先使用同目录 user.sql、room.sql、note.sql;
+-- 本文件把同一套设计翻译为 MySQL 语法,可在本机 MySQL 直接跑通并验证(已实测)。
+-- 注意:MySQL 9.x 已移除 md5(),故密码示例改用 sha2(),仅作占位演示。
+
+SET NAMES utf8mb4;
+
+CREATE DATABASE IF NOT EXISTS web_workshop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
+USE web_workshop;
+
+-- 1. user(同 user.sql)
+CREATE TABLE IF NOT EXISTS `user` (
+ uuid CHAR(36) NOT NULL,
+ username VARCHAR(64) NOT NULL,
+ password VARCHAR(64) NOT NULL,
+ PRIMARY KEY (uuid),
+ UNIQUE KEY uk_user_username (username)
+) ENGINE = InnoDB;
+
+INSERT INTO `user` (uuid, username, password) VALUES
+ ('00000000-0000-0000-0000-000000000000', 'admin', sha2('123456', 256)),
+ ('00000000-0000-0000-0000-000000000001', '张三', sha2('张三', 256)),
+ ('00000000-0000-0000-0000-000000000002', '李四', sha2('李四', 256)),
+ ('00000000-0000-0000-0000-000000000003', '王五', sha2('王五', 256)),
+ ('00000000-0000-0000-0000-000000000004', '赵六', sha2('赵六', 256));
+
+-- 2. room(同 room.sql)
+CREATE TABLE IF NOT EXISTS room (
+ uuid CHAR(36) NOT NULL,
+ name VARCHAR(64) NOT NULL,
+ intro VARCHAR(255) NOT NULL,
+ invite_code CHAR(6) NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (uuid),
+ UNIQUE KEY uk_room_name (name),
+ UNIQUE KEY uk_room_invite_code (invite_code)
+) ENGINE = InnoDB;
+
+INSERT INTO room (uuid, name, intro, invite_code) VALUES
+ ('00000000-0000-0000-0000-100000000001', '公共聊天室', '欢迎加入公共聊天室', 'gD9jE4'),
+ ('00000000-0000-0000-0000-100000000002', '聊天室1', '这是一个聊天室', 'oC3kY5'),
+ ('00000000-0000-0000-0000-100000000003', '聊天室2', '这是一个聊天室', 'uE8aY9'),
+ ('00000000-0000-0000-0000-100000000004', '聊天室3', '这是一个聊天室', 'aF2jR6');
+
+-- 3. note:会议便签纸(本讲新增表)
+-- * UNIQUE(user_uuid, room_uuid):每个用户在每个会议最多一张便签纸
+-- * 外键级联:用户或会议被删除时其便签随之删除
+-- * updated_at 在修改行时自动刷新(等价于 note.sql 中的触发器)
+CREATE TABLE IF NOT EXISTS note (
+ id CHAR(36) NOT NULL DEFAULT (UUID()),
+ user_uuid CHAR(36) NOT NULL,
+ room_uuid CHAR(36) NOT NULL,
+ content TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_note_user_room (user_uuid, room_uuid),
+ KEY idx_note_room (room_uuid),
+ CONSTRAINT fk_note_user FOREIGN KEY (user_uuid) REFERENCES `user` (uuid)
+ ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT fk_note_room FOREIGN KEY (room_uuid) REFERENCES room (uuid)
+ ON UPDATE CASCADE ON DELETE CASCADE
+) ENGINE = InnoDB;
+
+INSERT INTO note (user_uuid, room_uuid, content) VALUES
+ ('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000001', '大家好,我是张三!这是咱们房间的便签纸。待办:今天 15:00 讨论暑培答辩分组。'),
+ ('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-100000000001', '收到!我来准备演示用的投影和电脑。'),
+ ('00000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-100000000001', '(王五)我负责记录会议纪要~'),
+ ('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000002', '(张三在聊天室1的私人便签)别忘了下周还书。'),
+ ('00000000-0000-0000-0000-000000000004', '00000000-0000-0000-0000-100000000003', '(赵六)成语接龙战绩存档:心安理得→得过且过→过目不忘。');
+
+-- ================= 常见验证查询 =================
+-- 1) 某人某会议的便签(Hasura 中相当于 user_uuid 被 X-Hasura-User-Id 锁定)
+-- SELECT r.name, u.username, n.content, n.updated_at
+-- FROM note n JOIN room r ON n.room_uuid = r.uuid JOIN `user` u ON n.user_uuid = u.uuid
+-- WHERE n.user_uuid = '00000000-0000-0000-0000-000000000001'
+-- AND n.room_uuid = '00000000-0000-0000-0000-100000000001';
+-- 2) 同一个人在另一房间再插一张应报 ERROR 1062(唯一约束生效)
+-- INSERT INTO note (user_uuid, room_uuid, content) VALUES
+-- ('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000001', '重复便签,应失败');
diff --git a/frontend/public/about-me.css b/frontend/public/about-me.css
new file mode 100644
index 0000000..ba2151c
--- /dev/null
+++ b/frontend/public/about-me.css
@@ -0,0 +1,347 @@
+/* about-me.css —— 第9讲作业:关于我页面的样式 */
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
+ color: #333;
+ background: linear-gradient(160deg, #a8edea 0%, #fed6e3 55%, #fbc2eb 100%);
+ background-attachment: fixed;
+}
+
+.page {
+ width: min(920px, 92vw);
+ margin: 0 auto;
+ padding: 32px 8px 24px;
+}
+
+/* 头部 */
+header {
+ text-align: center;
+ margin-bottom: 28px;
+}
+
+header h1 {
+ font-size: 42px;
+ margin: 12px 0 6px;
+ color: #2d4c6b;
+ text-shadow: 0 2px 6px rgba(255, 255, 255, 0.6);
+ letter-spacing: 4px;
+}
+
+.subtitle {
+ color: #556;
+ margin-top: 0;
+}
+
+header nav a {
+ display: inline-block;
+ margin-top: 10px;
+ padding: 8px 20px;
+ border-radius: 999px;
+ background: rgba(45, 76, 107, 0.85);
+ color: #fff;
+ text-decoration: none;
+ transition: background 0.3s;
+}
+
+header nav a:hover {
+ background: #1e3a54;
+}
+
+/* 卡片网格布局 */
+.grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+}
+
+.card {
+ background: rgba(255, 255, 255, 0.72);
+ border-radius: 16px;
+ padding: 20px 24px;
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
+ backdrop-filter: blur(10px);
+}
+
+.card h2 {
+ margin-top: 4px;
+ margin-bottom: 12px;
+ font-size: 20px;
+ color: #2d4c6b;
+ border-bottom: 2px dashed #ffb6c1;
+ padding-bottom: 6px;
+}
+
+.span-2 {
+ grid-column: 1 / -1;
+}
+
+/* 个人简介 */
+.avatar {
+ width: 120px;
+ height: 120px;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 4px solid #fff;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ float: left;
+ margin: 0 18px 12px 0;
+}
+
+blockquote {
+ clear: both;
+ margin: 14px 0 0;
+ padding: 10px 16px;
+ border-left: 4px solid #f093a7;
+ background: rgba(255, 255, 255, 0.5);
+ border-radius: 0 8px 8px 0;
+ color: #5a4a52;
+}
+
+/* 表格 */
+table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 15px;
+}
+
+th,
+td {
+ border: 1px solid #c9d4de;
+ padding: 8px 10px;
+ text-align: left;
+}
+
+th {
+ background: #eef4fa;
+ width: 30%;
+ white-space: nowrap;
+}
+
+a {
+ color: #2778c4;
+}
+
+/* 列表 */
+ul,
+ol {
+ line-height: 1.9em;
+ padding-left: 22px;
+ margin: 6px 0;
+}
+
+/* 技能进度条 */
+meter {
+ width: 120px;
+ height: 16px;
+}
+
+.skill-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.skill-name {
+ font-weight: 600;
+ flex: 1;
+}
+
+/* 表单 */
+form label {
+ display: block;
+ margin: 10px 0 4px;
+ font-weight: 600;
+}
+
+form input[type="text"],
+form input[type="email"],
+form textarea {
+ width: 100%;
+ padding: 8px 10px;
+ border: 1px solid #b9c6d2;
+ border-radius: 8px;
+ font-family: inherit;
+ background: rgba(255, 255, 255, 0.9);
+}
+
+form button {
+ margin-top: 12px;
+ padding: 8px 22px;
+ border: none;
+ border-radius: 999px;
+ background: #2d4c6b;
+ color: #fff;
+ font-size: 15px;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+form button:hover {
+ background: #1e3a54;
+}
+
+/* 音乐播放器区域 */
+audio {
+ width: 100%;
+}
+
+/* 页脚 */
+footer {
+ text-align: center;
+ color: #556;
+ margin-top: 24px;
+ font-size: 13px;
+}
+
+/* ===== 第 10 讲作业新增样式 ===== */
+
+.clock {
+ color: #5a6b7b;
+ margin: 4px 0 0;
+ font-size: 14px;
+ min-height: 1.2em;
+}
+
+header nav button {
+ display: inline-block;
+ margin-top: 10px;
+ margin-left: 8px;
+ padding: 8px 20px;
+ border: none;
+ border-radius: 999px;
+ background: rgba(45, 76, 107, 0.85);
+ color: #fff;
+ cursor: pointer;
+ font: inherit;
+ transition: background 0.3s;
+}
+
+header nav button:hover {
+ background: #1e3a54;
+}
+
+#repo-list {
+ list-style: none;
+ padding: 0;
+ display: grid;
+ gap: 10px;
+ margin: 0 0 6px;
+}
+
+#repo-list li {
+ border: 1px solid #dbe4ec;
+ border-radius: 10px;
+ padding: 10px 14px;
+ background: rgba(255, 255, 255, 0.55);
+}
+
+#repo-list a {
+ font-weight: 600;
+ font-size: 16px;
+}
+
+#repo-list .lang {
+ float: right;
+ color: #6b7680;
+ font-size: 13px;
+}
+
+.muted {
+ color: #6b7680;
+ font-size: 13px;
+ margin: 4px 0 0;
+}
+
+.tip {
+ min-height: 1.4em;
+ margin: 10px 0 0;
+ font-weight: 600;
+ color: #1e7d4f;
+}
+
+/* 夜间模式 */
+body.dark {
+ background: linear-gradient(160deg, #141e30 0%, #243b55 60%, #2b5876 100%);
+ color: #dfe6ee;
+}
+
+body.dark .card {
+ background: rgba(22, 32, 48, 0.82);
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
+}
+
+body.dark header h1,
+body.dark .card h2 {
+ color: #9fc2e8;
+}
+
+body.dark .subtitle,
+body.dark .clock,
+body.dark .muted {
+ color: #9aa7b6;
+}
+
+body.dark .card p,
+body.dark .card li {
+ color: #d3dce6;
+}
+
+body.dark th {
+ background: #1c2b3d;
+ color: #d3dce6;
+}
+
+body.dark th,
+body.dark td {
+ border-color: #3b4f66;
+}
+
+body.dark blockquote {
+ background: rgba(255, 255, 255, 0.05);
+ color: #d3dce6;
+ border-left-color: #5b8db8;
+}
+
+body.dark #repo-list li {
+ background: rgba(255, 255, 255, 0.05);
+ border-color: #3b4f66;
+}
+
+body.dark form input[type="text"],
+body.dark form input[type="email"],
+body.dark form textarea {
+ background: #1c2b3d;
+ color: #e6ecf3;
+ border-color: #3b4f66;
+}
+
+body.dark a {
+ color: #8ec5f0;
+}
+
+body.dark .tip {
+ color: #7ad0a2;
+}
+
+body.dark footer {
+ color: #8a98a8;
+}
+
+@media (max-width: 700px) {
+ .grid {
+ grid-template-columns: 1fr;
+ }
+
+ .avatar {
+ float: none;
+ display: block;
+ margin: 0 auto 12px;
+ }
+}
diff --git a/frontend/public/about-me.html b/frontend/public/about-me.html
new file mode 100644
index 0000000..124ac6c
--- /dev/null
+++ b/frontend/public/about-me.html
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+ 关于我 - 豆梓宁
+
+
+
+
+
+
+
+
+
+
+ 个人简介
+
+
+ 你好!我是 豆梓宁,学号 2025010355。
+ 目前正在参加电子系学生科协的暑期培训,从 Git、Linux、C#/.NET
+ 一路学到现在的 Web 开发,目标是亲手做出一个完整的前后端项目。
+
+
+ 这个页面是暑培 Web Workshop 第 9 讲(HTML & CSS)的作业:
+ 我尝试用最朴素的 HTML + CSS 画出一个属于自己的小天地。
+
+ 「种一棵树最好的时间是十年前,其次是现在。」
+
+
+
+
+ 基本信息
+
+
+ | 姓名 |
+ 豆梓宁 |
+
+
+ | 学号 |
+ 2025010355 |
+
+
+ | 邮箱 |
+ 3094655597@qq.com |
+
+
+ | GitHub |
+ github.com/sililass |
+
+
+ | 所在地 |
+ 北京 · 清华大学 |
+
+
+
+
+
+
+ 兴趣爱好
+
+ - 写代码:喜欢把一个想法从 0 到 1 变成真实可用的东西
+ - 听音乐:最近在循环播放李健《风吹麦浪》
+ - 摄影:喜欢拍校园的四季,尤其是清华的秋天
+ - 桌游:是朋友局的"气氛组"常驻选手
+
+ 最近的小目标
+
+ - 按时完成暑培全部作业并真正理解每一讲
+ - 和小伙伴一起完成一个完整的趣味会议小软件
+ - 维护一个属于自己的个人网站
+
+
+
+
+
+ 技能自评
+
+ C# / .NET(暑培学习中)
+
+
+
+ Git 版本管理
+
+
+
+ Linux 基础
+
+
+
+ HTML / CSS(学习ing)
+
+
+ 数值会随着暑培进度不断上涨,敬请期待~
+
+
+
+
+
+
+
+ 循环中的一首歌
+
+
+
+
+
+ 我的 GitHub 仓库
+
+ 由 JavaScript 调用 api.github.com 实时获取(第 10 讲网络资源作业)。
+
+
+
+
+
+
+
+
diff --git a/frontend/public/about-me.js b/frontend/public/about-me.js
new file mode 100644
index 0000000..c142e0a
--- /dev/null
+++ b/frontend/public/about-me.js
@@ -0,0 +1,101 @@
+// about-me.js —— 第 10 讲(JS & TS)作业
+// 让"关于我"页面"动起来",包含:问候语 + 实时时钟、夜间模式、GitHub 仓库实时加载、表单提交反馈。
+
+/* 1. 动态问候语 + 实时时钟(本地动态) */
+const getGreeting = () => {
+ const h = new Date().getHours();
+ if (h < 6) return "夜深了,记得早点休息";
+ if (h < 12) return "早上好";
+ if (h < 14) return "中午好";
+ if (h < 18) return "下午好";
+ return "晚上好";
+};
+
+const updateClock = () => {
+ const clockDOM = document.getElementById("clock");
+ if (!clockDOM) return;
+ const now = new Date();
+ const date = now.toLocaleDateString("zh-CN");
+ const time = now.toLocaleTimeString("zh-CN");
+ clockDOM.innerText = `${getGreeting()}!现在是 ${date} ${time}(北京时间)`;
+};
+
+updateClock();
+setInterval(updateClock, 1000);
+
+/* 2. 夜间 / 日间模式切换(使用 localStorage 记住偏好) */
+const themeToggleDOM = document.getElementById("theme-toggle");
+const applyTheme = (theme) => {
+ document.body.classList.toggle("dark", theme === "dark");
+ if (themeToggleDOM) {
+ themeToggleDOM.textContent = theme === "dark" ? "☀️ 日间模式" : "🌙 夜间模式";
+ }
+};
+
+let savedTheme = localStorage.getItem("about-me-theme");
+if (!savedTheme) {
+ savedTheme =
+ window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+}
+applyTheme(savedTheme);
+
+if (themeToggleDOM) {
+ themeToggleDOM.addEventListener("click", () => {
+ const nextTheme = document.body.classList.contains("dark") ? "light" : "dark";
+ applyTheme(nextTheme);
+ localStorage.setItem("about-me-theme", nextTheme);
+ });
+}
+
+/* 3. 网络资源:实时加载 GitHub 公开仓库(fetch + Promise + async/await) */
+const GITHUB_USER = "sililass";
+const loadRepos = async () => {
+ const listDOM = document.getElementById("repo-list");
+ if (!listDOM) return;
+ try {
+ const response = await fetch(
+ `https://api.github.com/users/${GITHUB_USER}/repos?sort=updated&per_page=5`
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const repos = await response.json();
+ if (!Array.isArray(repos) || repos.length === 0) {
+ listDOM.innerHTML = "目前还没有公开仓库,期待他发奋图强~";
+ return;
+ }
+ listDOM.innerHTML = repos
+ .map(
+ (repo) =>
+ `
+ ${repo.name}
+ ${repo.language || "其他"} · ⭐ ${repo.stargazers_count}
+ ${repo.description || "暂无简介"}
+ `
+ )
+ .join("");
+ } catch (err) {
+ console.error(err);
+ listDOM.innerHTML = "GitHub 数据加载失败,请检查网络后刷新重试。";
+ }
+};
+
+loadRepos();
+
+/* 4. 留言表单提交反馈(DOM 事件) */
+const contactFormDOM = document.querySelector("#contact form");
+if (contactFormDOM) {
+ contactFormDOM.addEventListener("submit", (event) => {
+ event.preventDefault();
+ const nicknameInput = document.getElementById("nickname");
+ const tipDOM = document.getElementById("form-tip");
+ const nickname = nicknameInput ? nicknameInput.value || "同学" : "同学";
+ const submitTime = new Date().toLocaleTimeString("zh-CN");
+ if (tipDOM) {
+ tipDOM.textContent = `🎉 感谢 ${nickname} 的留言(${submitTime})!这是静态示例,未来接入后端后就能真正送达啦。`;
+ }
+ contactFormDOM.reset();
+ });
+}
diff --git a/frontend/public/index.html b/frontend/public/index.html
index fe08636..46c03c8 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -27,6 +27,8 @@ 这是一个趣味会议软件
进入主页
+ 关于我
+
关于这个工程
当前时间加载中... ...