mirror of
https://github.com/m1ngsama/FUJI.git
synced 2025-12-24 10:51:27 +00:00
Merge branch 'main' into use-openapi-fetch
This commit is contained in:
commit
87f8909283
7 changed files with 98 additions and 114 deletions
|
|
@ -8,8 +8,8 @@ export default function App() {
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
link: "/archive",
|
link: "/blog",
|
||||||
name: "目录",
|
name: "博客",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: "/calendar",
|
link: "/calendar",
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,5 @@ export const SITE_EMAIL = "contact@nbtca.space"
|
||||||
export const SITE_NAME = "NingboTech University, Computer Association"
|
export const SITE_NAME = "NingboTech University, Computer Association"
|
||||||
export const SITE_DESCRIPTION = "Computer Association"
|
export const SITE_DESCRIPTION = "Computer Association"
|
||||||
export const SITE_URL = import.meta.env.PUBLIC_SITE_URL
|
export const SITE_URL = import.meta.env.PUBLIC_SITE_URL
|
||||||
|
export const CA_LOGO_URL = "https://oss.nbtca.space/CA-logo.svg"
|
||||||
|
export const CA_PUBLIC_ICAL_URL = "https://ical.nbtca.space"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
---
|
---
|
||||||
|
import { CA_LOGO_URL } from "../consts"
|
||||||
import { formatDate } from "../utils"
|
import { formatDate } from "../utils"
|
||||||
const { title, href, cover, tags, date } = Astro.props
|
const { title, href, cover, tags, date } = Astro.props
|
||||||
const dateFormatted = formatDate(date)
|
const dateFormatted = formatDate(date)
|
||||||
const type = tags?.[0] ?? "默认"
|
const type = tags?.[0] ?? "默认"
|
||||||
|
|
||||||
const image = cover?.url ?? cover ?? "https://oss.nbtca.space/CA-logo.svg"
|
const image = cover?.url ?? cover ?? CA_LOGO_URL
|
||||||
|
|
||||||
const label = `${title} - ${type} - 发表时间 ${dateFormatted}`;
|
const label = `${title} - ${type} - 发表时间 ${dateFormatted}`;
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
---
|
|
||||||
import BaseLayout from "../layouts/BaseLayout.astro"
|
|
||||||
import ArchivePostList from "../layouts/ArchivePostList.astro"
|
|
||||||
const allPosts = await Astro.glob("./posts/*.md")
|
|
||||||
const Blogs = await Astro.glob("../pages/posts/blogs/**/*.md")
|
|
||||||
const tags = ["寝室", "技术", "学习", "活动", "其他"]
|
|
||||||
const posts = []
|
|
||||||
|
|
||||||
tags.forEach((tag) => {
|
|
||||||
const filteredPosts = allPosts.filter(post =>
|
|
||||||
post.frontmatter.tags.includes(tag),
|
|
||||||
)
|
|
||||||
const filteredBlogs = Blogs.filter(
|
|
||||||
post => post.frontmatter.tags?.includes(tag) ?? tag === "其他",
|
|
||||||
)
|
|
||||||
posts.push([...filteredPosts, ...filteredBlogs])
|
|
||||||
})
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout primaryTitle="归档">
|
|
||||||
<section class="archive">
|
|
||||||
<div class="section-content section-tag">
|
|
||||||
{
|
|
||||||
tags.map((tag, index) => {
|
|
||||||
return (
|
|
||||||
<div class="archive-tag">
|
|
||||||
<h2 class="tag-header">{tag}</h2>
|
|
||||||
<div class="tag-post-list">
|
|
||||||
{posts[index].length !== 0
|
|
||||||
? (
|
|
||||||
<ArchivePostList posts={posts[index]} />
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<div class="no-posts">暂无文章</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</BaseLayout>
|
|
||||||
87
src/pages/blog.astro
Normal file
87
src/pages/blog.astro
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from "../layouts/BaseLayout.astro"
|
||||||
|
import type { MarkdownInstance } from "astro"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import { CA_LOGO_URL } from "../consts"
|
||||||
|
|
||||||
|
type Cover = {
|
||||||
|
url: string
|
||||||
|
square: string
|
||||||
|
alt: string
|
||||||
|
} | string
|
||||||
|
|
||||||
|
type Post = {
|
||||||
|
layout: string
|
||||||
|
title: string
|
||||||
|
pubDate: string // ISO 字符串,可选改为 `Date` 类型
|
||||||
|
description: string
|
||||||
|
author: string
|
||||||
|
cover?: Cover
|
||||||
|
tags: string[]
|
||||||
|
theme: string
|
||||||
|
featured: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts: MarkdownInstance<Post>[] = await Astro.glob("./posts/*.md")
|
||||||
|
const blogs: MarkdownInstance<Post>[] = await Astro.glob("../pages/posts/blogs/**/*.md")
|
||||||
|
|
||||||
|
const allPosts = [...posts, ...blogs].sort((a, b) => {
|
||||||
|
return new Date(b.frontmatter.pubDate).getTime() - new Date(a.frontmatter.pubDate).getTime()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getCover = (post: MarkdownInstance<Post>) => {
|
||||||
|
if (typeof post.frontmatter.cover === "string") {
|
||||||
|
return { url: post.frontmatter.cover, square: post.frontmatter.cover, alt: "" }
|
||||||
|
}
|
||||||
|
return post.frontmatter.cover || { url: "", square: "", alt: "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// const tags = allPosts.reduce((acc, post) => {
|
||||||
|
// post.frontmatter.tags.forEach(tag => {
|
||||||
|
// if (!acc.includes(tag)) {
|
||||||
|
// acc.push(tag)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// return acc
|
||||||
|
// }, [] as string[]).sort((a, b) => a.localeCompare(b))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout primaryTitle="归档">
|
||||||
|
<div class="box-border border-b sticky top-0 bg-white/80 backdrop-blur z-20 h-12">
|
||||||
|
<div class="h-full flex items-center justify-between text-lg max-w-[1024px] mx-auto px-[22px]">
|
||||||
|
<span id="repair-header" class="font-semibold select-none cursor-default">博客</span>
|
||||||
|
<div class="flex items-center">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section class="mb-12">
|
||||||
|
<div class="section-content">
|
||||||
|
<div class="flex flex-col gap-2 group">
|
||||||
|
{
|
||||||
|
allPosts.map((v) => {
|
||||||
|
return (
|
||||||
|
<a class="flex items-start sm:items-center gap-4 sm:gap-6 py-8 no-underline appearance-none text-gray-900 cursor-pointer" style="text-decoration:none" href={v.url}>
|
||||||
|
<img
|
||||||
|
class="rounded-xl sm:rounded-[20px] w-[105px] sm:w-[264px] xl:w-[295px] shrink-0 aspect-video overflow-hidden bg-gray-100 object-cover object-center archive-cover"
|
||||||
|
src={getCover(v).url || CA_LOGO_URL}
|
||||||
|
alt={getCover(v).alt || "fallback cover"}
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-1 sm:gap-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{v.frontmatter.tags.slice(0, 3).map((tag) => {
|
||||||
|
return <span class="text-xs text-gray-700">{tag}</span>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="sm:text-xl font-bold">{v.frontmatter.title}</div>
|
||||||
|
<div class="text-sm sm:font-semibold text-gray-600 sm:mt-1">{dayjs(v.frontmatter.pubDate).format("YYYY-MM-DD")}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="w-full h-[0.5px] bg-gray-600 my-1 flex-shrink-0 last:hidden"></div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -5,6 +5,7 @@ import { Alert, Button, Calendar, Link, Spinner, Select, SelectItem } from "@her
|
||||||
import { today, getLocalTimeZone } from "@internationalized/date"
|
import { today, getLocalTimeZone } from "@internationalized/date"
|
||||||
|
|
||||||
import "dayjs/locale/zh-cn"
|
import "dayjs/locale/zh-cn"
|
||||||
|
import { CA_PUBLIC_ICAL_URL } from "../../consts"
|
||||||
|
|
||||||
dayjs.locale("zh-cn")
|
dayjs.locale("zh-cn")
|
||||||
|
|
||||||
|
|
@ -17,7 +18,7 @@ type ScheduleEvent = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseCal = async (): Promise<ICAL.Component> => {
|
const parseCal = async (): Promise<ICAL.Component> => {
|
||||||
const res = await fetch("https://ical.nbtca.space/").then(res => res.text())
|
const res = await fetch(CA_PUBLIC_ICAL_URL).then(res => res.text())
|
||||||
const jcalData = ICAL.parse(res)
|
const jcalData = ICAL.parse(res)
|
||||||
return new ICAL.Component(jcalData)
|
return new ICAL.Component(jcalData)
|
||||||
}
|
}
|
||||||
|
|
@ -110,62 +111,6 @@ const expandEventOccurrences = (
|
||||||
return occurrences
|
return occurrences
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractScheduleEvents = (icalComp: ICAL.Component): ScheduleEvent[] => {
|
|
||||||
const vevents = icalComp.getAllSubcomponents("vevent")
|
|
||||||
const rangeEnd = ICAL.Time.fromDateString("2026-01-01")
|
|
||||||
const rangeStart = ICAL.Time.fromDateString("2020-01-01")
|
|
||||||
|
|
||||||
// First, collect all exception events (events with RECURRENCE-ID)
|
|
||||||
const exceptions = new Map<string, Set<string>>() // uid -> set of recurrence-id strings
|
|
||||||
const exceptionEvents: ScheduleEvent[] = []
|
|
||||||
|
|
||||||
vevents.forEach((vevent) => {
|
|
||||||
const recurrenceId = vevent.getFirstPropertyValue("recurrence-id")
|
|
||||||
if (recurrenceId) {
|
|
||||||
const uid = vevent.getFirstPropertyValue("uid")
|
|
||||||
const event = new ICAL.Event(vevent)
|
|
||||||
|
|
||||||
// Check if exception is in range
|
|
||||||
if (event.startDate.compare(rangeEnd) <= 0 && event.endDate.compare(rangeStart) >= 0) {
|
|
||||||
exceptionEvents.push({
|
|
||||||
start: event.startDate.toJSDate(),
|
|
||||||
end: event.endDate.toJSDate(),
|
|
||||||
summary: event.summary,
|
|
||||||
description: event.description,
|
|
||||||
recurrenceId: recurrenceId.toString(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track exception for filtering recurring events
|
|
||||||
if (!exceptions.has(uid)) {
|
|
||||||
exceptions.set(uid, new Set())
|
|
||||||
}
|
|
||||||
exceptions.get(uid)!.add(recurrenceId.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process regular and recurring events
|
|
||||||
const regularEvents = vevents
|
|
||||||
.filter(vevent => !vevent.getFirstPropertyValue("recurrence-id"))
|
|
||||||
.flatMap<ScheduleEvent>((vevent) => {
|
|
||||||
const event = new ICAL.Event(vevent)
|
|
||||||
if (event.iterator().complete) {
|
|
||||||
return [{
|
|
||||||
start: event.startDate.toJSDate(),
|
|
||||||
end: event.endDate.toJSDate(),
|
|
||||||
summary: event.summary,
|
|
||||||
description: event.description,
|
|
||||||
recurrenceId: event.uid || event.startDate.toString(),
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
const eventExceptions = exceptions.get(event.uid) || new Set()
|
|
||||||
return expandEventOccurrences(event, rangeStart, rangeEnd, eventExceptions)
|
|
||||||
})
|
|
||||||
|
|
||||||
return [...regularEvents, ...exceptionEvents]
|
|
||||||
.sort((a, b) => b.start.getTime() - a.start.getTime())
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTimePair = (s: Date, e: Date): string => {
|
const formatTimePair = (s: Date, e: Date): string => {
|
||||||
const start = dayjs(s)
|
const start = dayjs(s)
|
||||||
const end = dayjs(e)
|
const end = dayjs(e)
|
||||||
|
|
@ -216,14 +161,6 @@ export default function Schedule() {
|
||||||
}
|
}
|
||||||
}, [focusedDate])
|
}, [focusedDate])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true)
|
|
||||||
parseCal().then((icalComp) => {
|
|
||||||
setScheduledEvents(extractScheduleEvents(icalComp))
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const groupedEvents = useMemo(() => {
|
const groupedEvents = useMemo(() => {
|
||||||
const grouped = new Map<string, ScheduleEvent[]>()
|
const grouped = new Map<string, ScheduleEvent[]>()
|
||||||
scheduledEvents.forEach((event) => {
|
scheduledEvents.forEach((event) => {
|
||||||
|
|
@ -380,7 +317,7 @@ export default function Schedule() {
|
||||||
<span className="text-sm text-gray-600">{formatTimePair(event.start, event.end)}</span>
|
<span className="text-sm text-gray-600">{formatTimePair(event.start, event.end)}</span>
|
||||||
{
|
{
|
||||||
event.description && (
|
event.description && (
|
||||||
<p className="mt-2">{event.description}</p>
|
<p className="mt-2 whitespace-pre-wrap">{event.description}</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -110,10 +110,10 @@ Blogs.sort(
|
||||||
</ul>
|
</ul>
|
||||||
<div class="view-archive-wrapper">
|
<div class="view-archive-wrapper">
|
||||||
<a
|
<a
|
||||||
href="/archive"
|
href="/blog"
|
||||||
class="cta-primary-light"
|
class="cta-primary-light"
|
||||||
data-analytics-region="router"
|
data-analytics-region="router"
|
||||||
data-analytics-title="view archive"
|
data-analytics-title="view blog"
|
||||||
>
|
>
|
||||||
阅读历史文章
|
阅读历史文章
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue