diff --git a/src/components/OnThisDay.astro b/src/components/OnThisDay.astro new file mode 100644 index 0000000..b9dd28e --- /dev/null +++ b/src/components/OnThisDay.astro @@ -0,0 +1,348 @@ +
+
+
+
+
+ +
+ + +
+ +
+ +
+ + +
+

圆点表示这一天收录了班级记忆

+
+ +
+
+
+

ON THIS DAY · 那年今日

+

今天,记忆翻到了这一页

+
+

正在寻找同月同日的班级记忆……

+
+ +
+ + + + +
+
+
+ + diff --git a/src/data/on-this-day.ts b/src/data/on-this-day.ts new file mode 100644 index 0000000..44777d2 --- /dev/null +++ b/src/data/on-this-day.ts @@ -0,0 +1,103 @@ +import { articles } from "./articles"; +import { photoTopics } from "./photos"; +import { timeline } from "./timeline"; + +export type MemoryKind = "event" | "photo" | "article"; + +export interface OnThisDayMemory { + id: string; + date: string; + monthDay: string; + year: number; + kind: MemoryKind; + kindLabel: string; + title: string; + text: string; + href: string; + image?: string; + external?: boolean; +} + +const kindLabels: Record = { + event: "班级事件", + photo: "旧照片", + article: "旧文章" +}; + +const toIsoDate = (year: number, month: number, day: number) => + `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + +const getTimelineDates = (label: string) => { + const match = label.match(/(\d{4})年(\d{1,2})月([^日]+)日/); + if (!match) return []; + + const [, yearText, monthText, dayText] = match; + return dayText + .split(/[、,,]/) + .map((value) => Number(value.match(/\d+/)?.[0])) + .filter((value) => Number.isInteger(value) && value >= 1 && value <= 31) + .map((day) => toIsoDate(Number(yearText), Number(monthText), day)); +}; + +const createMemory = ( + memory: Omit +): OnThisDayMemory => ({ + ...memory, + monthDay: memory.date.slice(5), + year: Number(memory.date.slice(0, 4)), + kindLabel: kindLabels[memory.kind] +}); + +const eventMemories = timeline.flatMap((moment, momentIndex) => + getTimelineDates(moment.date).map((date) => + createMemory({ + id: `event-${momentIndex}-${date}`, + date, + kind: "event", + title: moment.title, + text: moment.text, + href: "href" in moment && typeof moment.href === "string" ? moment.href : "/timeline/" + }) + ) +); + +const photoMemories = photoTopics.flatMap((topic) => + topic.photos.flatMap((photo, photoIndex) => + photo.date + ? [ + createMemory({ + id: `photo-${topic.slug}-${photoIndex}-${photo.date}`, + date: photo.date, + kind: "photo", + title: photo.title, + text: photo.caption, + href: `/gallery/${topic.slug}/`, + image: photo.image + }) + ] + : [] + ) +); + +const articleMemories = articles.map((article, articleIndex) => + createMemory({ + id: `article-${articleIndex}-${article.date}`, + date: article.date, + kind: "article", + title: article.title, + text: article.excerpt, + href: article.sourceUrl, + image: article.cover, + external: true + }) +); + +const kindOrder: Record = { + event: 0, + photo: 1, + article: 2 +}; + +export const onThisDayMemories = [...eventMemories, ...photoMemories, ...articleMemories].sort( + (left, right) => right.year - left.year || kindOrder[left.kind] - kindOrder[right.kind] +); diff --git a/src/data/photo-topics/events.ts b/src/data/photo-topics/events.ts index 840cf1e..a7db67d 100644 --- a/src/data/photo-topics/events.ts +++ b/src/data/photo-topics/events.ts @@ -10,7 +10,8 @@ export const eventsTopic: PhotoTopic = { { title: "高一下学期表彰大会", caption: "排练时觉得麻烦,回头看全是可爱。", - image: "https://pic.biss.click/image/50e233d6-dd88-4a10-bb2d-a42ace27cdec.jpg" + image: "https://pic.biss.click/image/50e233d6-dd88-4a10-bb2d-a42ace27cdec.jpg", + date: "2022-07-08" }, { title: "集体出游", @@ -20,13 +21,17 @@ export const eventsTopic: PhotoTopic = { { title: "百日誓师大会", caption: "高三的第一次全体活动,大家都很随意地在听讲,虽然最后还是没能坚持到最后。", - image: "https://pic.biss.click/image/45bfbe79-bd7b-40f3-9670-1b34cc0957b2.png" + image: "https://pic.biss.click/image/45bfbe79-bd7b-40f3-9670-1b34cc0957b2.png", + date: "2024-02-28" }, wechatPhoto(117, "考前舞台", "紧张的日子里,也需要一次大声唱出来。"), wechatPhoto(121, "看台上的我们", "一片红色校服,聚成高三最后的集体活动。"), wechatPhoto(129, "操场热身", "阳光很好,口号和动作都比平时更有力量。"), wechatPhoto(132, "坐在一起", "台上发生什么已经模糊,身边的人还记得。"), - wechatPhoto(176, "元旦歌咏", "整齐站上舞台,是青春里难得的郑重。"), + { + ...wechatPhoto(176, "元旦歌咏", "整齐站上舞台,是青春里难得的郑重。"), + date: "2021-12-31" + }, wechatPhoto(178, "领奖时刻", "证书会褪色,但被认可的那一刻不会。"), wechatPhoto(180, "操场挑战", "跨过栏架,也跨过一个个觉得做不到的瞬间。"), wechatPhoto(181, "百日倒计时", "数字揭开的那一刻,高考忽然变得很近。"), diff --git a/src/data/photo-topics/graduation-day.ts b/src/data/photo-topics/graduation-day.ts index 1817493..c44f891 100644 --- a/src/data/photo-topics/graduation-day.ts +++ b/src/data/photo-topics/graduation-day.ts @@ -32,13 +32,19 @@ export const graduationDayTopic: PhotoTopic = { caption: "那一刻大家都在笑,但心里都知道要分别了。", image: "https://pic.biss.click/image/f8d180c1-4827-4bfd-9550-4bfad4b97640.jpg" }, - wechatPhoto(145, "壮行合影", "红色班服、彩色气球,和出发前最完整的一次站在一起。"), + { + ...wechatPhoto(145, "壮行合影", "红色班服、彩色气球,和出发前最完整的一次站在一起。"), + date: "2024-06-06" + }, wechatPhoto(146, "气球升起之前", "所有人挤进镜头,等一个一起松手的瞬间。"), wechatPhoto(150, "飞向夏天", "气球越过教学楼,属于高中的倒计时也到了最后。"), wechatPhoto(151, "人群里的告别", "欢呼、拥抱和一句句被现场声音盖住的话。"), wechatPhoto(152, "穿过祝福", "从熟悉的人群中走过,前面就是新的路。"), wechatPhoto(153, "鲜花与笑脸", "最后一程依然有人在身旁鼓掌。"), - wechatPhoto(81, "毕业典礼合影", "红色幕布前,老师和同学一起留下毕业的证据。"), + { + ...wechatPhoto(81, "毕业典礼合影", "红色幕布前,老师和同学一起留下毕业的证据。"), + date: "2024-06-09" + }, wechatPhoto(84, "典礼现场", "从看台望向舞台,礼堂装下了整届人的夏天。"), wechatPhoto(85, "把花送给老师", "镜头拼在一起,也拼出那天的感谢。"), wechatPhoto(88, "走过红毯", "熟悉的老师站在两侧,目送这一届学生离开。"), diff --git a/src/data/photo-types.ts b/src/data/photo-types.ts index 27449b5..3df2047 100644 --- a/src/data/photo-types.ts +++ b/src/data/photo-types.ts @@ -2,6 +2,7 @@ export interface PhotoItem { title: string; caption: string; image: string; + date?: string; } export interface PhotoTopic { diff --git a/src/data/timeline.ts b/src/data/timeline.ts index f071066..041f165 100644 --- a/src/data/timeline.ts +++ b/src/data/timeline.ts @@ -3,7 +3,14 @@ export const timelineIntro = { text: "把三年里的班级大事按时间放在这里。军训、分班、网课、百日誓师、高考和毕业,都成为了后来回头看时很亮的节点。" }; -export const timeline = [ +export interface TimelineMoment { + date: string; + title: string; + text: string; + href?: string; +} + +export const timeline: TimelineMoment[] = [ { date: "2021年8月", title: "进入高中", diff --git a/src/pages/index.astro b/src/pages/index.astro index 44f26f9..ea81d9c 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,5 +1,6 @@ --- import BaseLayout from "../layouts/BaseLayout.astro"; +import OnThisDay from "../components/OnThisDay.astro"; import { galleryIntro, photoTopics } from "../data/photos"; import { featuredMessage, messages, messagesIntro } from "../data/messages"; import { people, peopleIntro } from "../data/people"; @@ -41,6 +42,8 @@ const previewPeople = people.slice(0, 6);
+ +
diff --git a/src/styles/global.css b/src/styles/global.css index 4d78cd4..bb83c84 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -198,6 +198,437 @@ h1 { font-size: 14px; } +.on-this-day-band { + position: relative; + overflow: hidden; + background: + radial-gradient(circle at 8% 14%, rgba(232, 168, 76, 0.2), transparent 24%), + linear-gradient(135deg, #f6f0df, #fffdf7 48%, #eef4e9); +} + +.on-this-day-band::after { + content: ""; + position: absolute; + right: -80px; + bottom: -110px; + width: 280px; + height: 280px; + border: 1px solid rgba(55, 109, 90, 0.12); + border-radius: 50%; + box-shadow: + 0 0 0 34px rgba(55, 109, 90, 0.035), + 0 0 0 68px rgba(55, 109, 90, 0.025); + pointer-events: none; +} + +.on-this-day-shell { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(280px, 1fr) minmax(0, 2fr); + gap: clamp(28px, 5vw, 64px); + align-items: start; +} + +.on-this-day-heading { + display: grid; + grid-template-columns: 1fr; + gap: 12px; + align-items: start; + margin-bottom: 30px; +} + +.on-this-day-heading .eyebrow { + color: var(--coral); +} + +.on-this-day-heading p:last-child { + margin: 0; + color: var(--muted); +} + +.memory-calendar { + position: sticky; + top: 26px; + padding: 18px; + border: 1px solid rgba(31, 43, 42, 0.12); + border-radius: 14px; + background: rgba(255, 255, 255, 0.72); + box-shadow: 0 8px 28px rgba(39, 55, 52, 0.06); + backdrop-filter: blur(10px); +} + +.memory-calendar-head, +.memory-calendar-nav { + display: flex; + align-items: center; +} + +.memory-calendar-head { + display: grid; + gap: 10px; + margin-bottom: 16px; +} + +.memory-calendar-nav { + justify-content: space-between; + gap: 10px; +} + +.memory-calendar-nav button, +.memory-calendar-today { + min-height: 38px; + border: 1px solid var(--line); + background: #fffdf7; + color: var(--ink); + font: inherit; + font-weight: 800; + cursor: pointer; + transition: border-color 160ms ease, background 160ms ease, color 160ms ease; +} + +.memory-calendar-step { + width: 38px; + flex: 0 0 38px; + padding: 0; + border-radius: 50%; +} + +.memory-calendar-period { + min-width: 0; + display: flex; + justify-content: center; + gap: 2px; +} + +.memory-calendar-period button { + min-width: 0; + min-height: 38px; + padding: 4px 7px; + border-color: transparent; + border-radius: 7px; + background: transparent; + font-size: 18px; +} + +.memory-calendar-period button::after { + content: "⌄"; + margin-left: 3px; + color: var(--muted); + font-size: 12px; +} + +.memory-calendar-period button:hover, +.memory-calendar-period button[aria-expanded="true"] { + border-color: rgba(55, 109, 90, 0.2); + background: rgba(55, 109, 90, 0.08); + color: var(--green); +} + +.memory-calendar-today { + width: 100%; + padding: 7px 13px; + border-radius: 999px; + color: var(--green); + font-size: 13px; +} + +.memory-calendar-step:hover, +.memory-calendar-today:hover { + border-color: var(--green); + background: var(--green); + color: #fff; +} + +.memory-calendar-period button:hover::after, +.memory-calendar-period button[aria-expanded="true"]::after { + color: currentColor; +} + +.memory-calendar-jump { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin: -4px 0 16px; + padding: 12px; + border: 1px solid rgba(55, 109, 90, 0.2); + border-radius: 10px; + background: #fffdf7; + box-shadow: 0 12px 28px rgba(39, 55, 52, 0.1); +} + +.memory-calendar-jump[hidden] { + display: none; +} + +.memory-calendar-jump label { + display: grid; + gap: 5px; + color: var(--muted); + font-size: 11px; + font-weight: 800; +} + +.memory-calendar-jump input, +.memory-calendar-jump select { + width: 100%; + min-width: 0; + min-height: 38px; + padding: 6px 9px; + border: 1px solid var(--line); + border-radius: 7px; + outline: none; + background: #fff; + color: var(--ink); + font: inherit; + font-size: 14px; +} + +.memory-calendar-jump input:focus, +.memory-calendar-jump select:focus { + border-color: var(--green); + box-shadow: 0 0 0 3px rgba(55, 109, 90, 0.1); +} + +.memory-calendar-jump > button { + grid-column: 1 / -1; + min-height: 38px; + border: 0; + border-radius: 7px; + background: var(--green); + color: #fff; + font: inherit; + font-size: 13px; + font-weight: 800; + cursor: pointer; +} + +.memory-calendar-weekdays, +.memory-calendar-grid { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 7px; +} + +.memory-calendar-weekdays { + margin-bottom: 7px; + color: var(--muted); + text-align: center; + font-size: 12px; + font-weight: 800; +} + +.memory-calendar-day, +.memory-calendar-spacer { + min-width: 0; + aspect-ratio: 1.45; +} + +.memory-calendar-day { + position: relative; + display: grid; + place-items: center; + align-content: center; + gap: 3px; + padding: 4px; + border: 1px solid transparent; + border-radius: 9px; + background: rgba(244, 247, 238, 0.72); + color: var(--muted); + font: inherit; + font-size: 13px; + cursor: pointer; + transition: transform 150ms ease, border-color 150ms ease, background 150ms ease, color 150ms ease; +} + +.memory-calendar-day:hover { + transform: translateY(-1px); + border-color: rgba(55, 109, 90, 0.3); + color: var(--green); + background: #fff; +} + +.memory-calendar-day.has-memory { + color: var(--ink); + background: rgba(232, 168, 76, 0.12); + font-weight: 800; +} + +.memory-calendar-day i { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--coral); +} + +.memory-calendar-day.is-today { + border-color: rgba(55, 109, 90, 0.55); +} + +.memory-calendar-day.is-selected { + border-color: var(--green); + background: var(--green); + color: #fffdf7; + box-shadow: 0 6px 16px rgba(55, 109, 90, 0.2); +} + +.memory-calendar-day.is-selected i { + background: #f3cf8b; +} + +.memory-calendar-legend { + display: flex; + align-items: center; + gap: 7px; + margin: 12px 0 0; + color: var(--muted); + font-size: 12px; +} + +.memory-calendar-legend > span { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--coral); +} + +.memory-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + align-items: start; +} + +.memory-card { + height: 220px; + min-height: 0; + display: grid; + grid-template-columns: 120px minmax(0, 1fr); + overflow: hidden; + border: 1px solid rgba(31, 43, 42, 0.12); + border-radius: 12px; + background: rgba(255, 255, 255, 0.82); + box-shadow: 0 8px 28px rgba(39, 55, 52, 0.06); + transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease; +} + +.memory-card:hover { + transform: translateY(-3px); + border-color: rgba(55, 109, 90, 0.34); + box-shadow: 0 16px 38px rgba(39, 55, 52, 0.12); +} + +.memory-card > img, +.memory-card-visual { + width: 100%; + height: 100%; + min-height: 0; + object-fit: cover; +} + +.memory-card-visual { + display: grid; + place-items: center; + color: rgba(255, 253, 247, 0.9); + background: linear-gradient(135deg, rgba(55, 109, 90, 0.94), rgba(69, 111, 148, 0.88)); + font-family: STKaiti, KaiTi, serif; + font-size: 48px; +} + +.memory-card-copy { + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + overflow: hidden; + padding: 20px; +} + +.memory-card-meta { + color: var(--coral); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.04em; +} + +.memory-card-copy > strong { + display: -webkit-box; + overflow: hidden; + margin-top: 7px; + font-size: 19px; + line-height: 1.35; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.memory-card-text { + display: -webkit-box; + overflow: hidden; + margin-top: 8px; + color: var(--muted); + font-size: 13px; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.memory-card-action { + margin-top: auto; + padding-top: 14px; + color: var(--green); + font-size: 12px; + font-weight: 800; +} + +.memory-empty { + min-height: 190px; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 22px; + align-items: center; + padding: clamp(24px, 4vw, 38px); + border: 1px dashed rgba(55, 109, 90, 0.3); + border-radius: 14px; + background: rgba(255, 255, 255, 0.56); +} + +.memory-empty[hidden], +.memory-grid[hidden] { + display: none; +} + +.memory-empty-mark { + color: var(--sun); + font-size: 48px; +} + +.memory-empty h3 { + margin: 0; + font-size: 22px; +} + +.memory-empty p, +.memory-noscript { + margin: 6px 0 0; + color: var(--muted); +} + +.memory-empty-links { + display: flex; + flex-wrap: wrap; + gap: 9px; + margin-top: 18px; +} + +.memory-empty-links a { + padding: 7px 12px; + border: 1px solid var(--line); + border-radius: 999px; + background: #fff; + color: var(--green); + font-size: 13px; + font-weight: 800; +} + main { overflow: hidden; } @@ -2147,7 +2578,21 @@ h2 { transform: translate(-50%, 0); } +@media (max-width: 1040px) and (min-width: 821px) { + .memory-grid { + grid-template-columns: 1fr; + } + + .memory-calendar { + position: static; + } +} + @media (max-width: 820px) { + .memory-calendar { + position: static; + } + body:has(.score-sidebar) { padding-bottom: 86px; } @@ -2227,6 +2672,8 @@ h2 { .stats, .section-title, + .on-this-day-shell, + .on-this-day-heading, .message-wall, .twikoo-sticky-wall .tk-comments, .twikoo-sticky-wall .tk-comments-container, @@ -2428,6 +2875,45 @@ h2 { padding-inline: 14px; } + .memory-card { + height: auto; + grid-template-columns: 1fr; + } + + .memory-card > img, + .memory-card-visual { + height: 190px; + min-height: 0; + } + + .memory-calendar { + margin-inline: -4px; + padding: 13px 9px; + } + + .memory-calendar-head { + align-items: stretch; + flex-direction: column; + } + + .memory-calendar-nav { + justify-content: space-between; + } + + .memory-calendar-today { + width: 100%; + } + + .memory-calendar-weekdays, + .memory-calendar-grid { + gap: 4px; + } + + .memory-calendar-day, + .memory-calendar-spacer { + aspect-ratio: 1; + } + .hero-actions { display: grid; } @@ -2442,6 +2928,10 @@ h2 { min-height: auto; } + .memory-grid { + grid-template-columns: 1fr; + } + .photo-card, .photo-card:nth-child(2), .photo-card:nth-child(4) {