172 lines
4.3 KiB
TypeScript
172 lines
4.3 KiB
TypeScript
export type BlogPost = {
|
|
id?: string
|
|
path?: string
|
|
title?: string
|
|
description?: string
|
|
published?: string
|
|
updated?: string | null
|
|
image?: string | null
|
|
draft?: boolean
|
|
pinned?: boolean
|
|
tags?: string[]
|
|
series?: string | null
|
|
seriesOrder?: number | null
|
|
difficulty?: string | null
|
|
body?: unknown
|
|
}
|
|
|
|
export type ArchiveMonth = {
|
|
month: string
|
|
label: string
|
|
posts: BlogPost[]
|
|
}
|
|
|
|
export type ArchiveYear = {
|
|
year: string
|
|
months: ArchiveMonth[]
|
|
}
|
|
|
|
export type SeriesSummary = {
|
|
name: string
|
|
path: string
|
|
count: number
|
|
posts: BlogPost[]
|
|
latestPost: BlogPost
|
|
latestTime: number
|
|
tags: string[]
|
|
}
|
|
|
|
export function isVisiblePost(post: BlogPost): boolean {
|
|
return !post.draft
|
|
}
|
|
|
|
export function sortPostsByPublished(posts: BlogPost[]): BlogPost[] {
|
|
return [...posts].sort((a, b) => getPostTime(b.published) - getPostTime(a.published))
|
|
}
|
|
|
|
export function sortPostsForSeries(posts: BlogPost[]): BlogPost[] {
|
|
return [...posts].sort((a, b) => {
|
|
const orderA = typeof a.seriesOrder === 'number' ? a.seriesOrder : Number.MAX_SAFE_INTEGER
|
|
const orderB = typeof b.seriesOrder === 'number' ? b.seriesOrder : Number.MAX_SAFE_INTEGER
|
|
if (orderA !== orderB) return orderA - orderB
|
|
return getPostTime(a.published) - getPostTime(b.published)
|
|
})
|
|
}
|
|
|
|
export function getArchiveGroups(posts: BlogPost[]): ArchiveYear[] {
|
|
const years = new Map<string, Map<string, BlogPost[]>>()
|
|
|
|
for (const post of sortPostsByPublished(posts)) {
|
|
const date = getPostDate(post.published)
|
|
if (!date) continue
|
|
|
|
const year = String(date.getFullYear())
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
|
if (!years.has(year)) {
|
|
years.set(year, new Map())
|
|
}
|
|
|
|
const months = years.get(year)!
|
|
if (!months.has(month)) {
|
|
months.set(month, [])
|
|
}
|
|
|
|
months.get(month)!.push(post)
|
|
}
|
|
|
|
return [...years.entries()]
|
|
.sort(([a], [b]) => Number(b) - Number(a))
|
|
.map(([year, months]) => ({
|
|
year,
|
|
months: [...months.entries()]
|
|
.sort(([a], [b]) => Number(b) - Number(a))
|
|
.map(([month, monthPosts]) => ({
|
|
month,
|
|
label: `${Number(month)} 月`,
|
|
posts: monthPosts,
|
|
})),
|
|
}))
|
|
}
|
|
|
|
export function getSeriesSummaries(posts: BlogPost[]): SeriesSummary[] {
|
|
const groups = new Map<string, BlogPost[]>()
|
|
|
|
for (const post of posts) {
|
|
const name = normalizeSeriesName(post.series)
|
|
if (!name) continue
|
|
|
|
if (!groups.has(name)) {
|
|
groups.set(name, [])
|
|
}
|
|
groups.get(name)!.push(post)
|
|
}
|
|
|
|
return [...groups.entries()]
|
|
.map(([name, items]) => {
|
|
const seriesPosts = sortPostsForSeries(items)
|
|
const latestPost = sortPostsByPublished(seriesPosts)[0]
|
|
const tags = [...new Set(seriesPosts.flatMap(post => post.tags || []))].slice(0, 6)
|
|
|
|
return {
|
|
name,
|
|
path: getSeriesPath(name),
|
|
count: seriesPosts.length,
|
|
posts: seriesPosts,
|
|
latestPost,
|
|
latestTime: Math.max(...seriesPosts.map(post => getPostTime(post.updated || post.published))),
|
|
tags,
|
|
}
|
|
})
|
|
.sort((a, b) => b.latestTime - a.latestTime)
|
|
}
|
|
|
|
export function normalizeSeriesName(series?: string | null): string {
|
|
return typeof series === 'string' ? series.trim() : ''
|
|
}
|
|
|
|
export function getSeriesPath(name: string): string {
|
|
return `/series/${encodeURIComponent(name)}`
|
|
}
|
|
|
|
export function decodeRouteParam(value: string): string {
|
|
try {
|
|
return decodeURIComponent(value)
|
|
} catch {
|
|
return value
|
|
}
|
|
}
|
|
|
|
export function formatPostDate(date?: string | null): string {
|
|
if (!date) return ''
|
|
|
|
return new Date(date).toLocaleDateString('zh-CN', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
})
|
|
}
|
|
|
|
export function getWordCount(post: BlogPost): number {
|
|
const text = JSON.stringify(post.body || '').replace(/<[^>]*>/g, '')
|
|
const chinese = text.match(/[一-龥]/g) || []
|
|
const english = text.match(/[a-zA-Z]+/g) || []
|
|
return chinese.length + english.length
|
|
}
|
|
|
|
export function getReadTime(post: BlogPost): number {
|
|
return Math.max(1, Math.ceil(getWordCount(post) / 300))
|
|
}
|
|
|
|
function getPostDate(date?: string | null): Date | null {
|
|
if (!date) return null
|
|
|
|
const value = new Date(date)
|
|
if (Number.isNaN(value.getTime())) return null
|
|
return value
|
|
}
|
|
|
|
function getPostTime(date?: string | null): number {
|
|
return getPostDate(date)?.getTime() || 0
|
|
}
|