feat: 优化首页个人名片与内容入口
This commit is contained in:
+171
@@ -0,0 +1,171 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user