fix: 修复后台文章计数为0 + 分类编辑补全描述字段 + CSP放行外部图片 + 更新关于页描述

- 文章管理:计数请求补 page=1 参数,命中分页接口返回正确的 total
- 分类管理:编辑模式新增描述输入框,保存时一并提交 description
- CSP:img-src 加入 https: 允许加载外部图片
- 关于页:数据存储描述从 JSON 文件更正为 SQLite 数据库
- Footer:添加 ICP 备案号
This commit is contained in:
胡旭
2026-06-24 13:51:48 +08:00
parent 3707eddfd4
commit 18e915bcbb
69 changed files with 6818 additions and 1422 deletions
+34 -54
View File
@@ -1,19 +1,15 @@
"use client";
import Link from "next/link";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import type { Post } from "@/data/posts";
import type { PublicPost } from "@/lib/store";
import { formatDate, readingTimeLabel } from "@/lib/utils";
import { useGsapAnimation } from "./useGsapAnimation";
gsap.registerPlugin(ScrollTrigger);
function formatDate(dateStr: string) {
const d = new Date(dateStr);
return d.toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric" });
}
function FeaturedCard({ post }: { post: Post }) {
function FeaturedCard({ post }: { post: PublicPost }) {
return (
<Link href={`/posts/${post.slug}`} className="group block featured-card">
<article className="relative p-7 md:p-10 rounded-2xl bg-cream border border-warm-gray/10 hover:border-terracotta/20 hover:shadow-lg hover:shadow-terracotta/5 transition-all duration-500">
@@ -29,7 +25,7 @@ function FeaturedCard({ post }: { post: Post }) {
<div className="flex items-center gap-3 font-sans text-sm text-ink-muted">
<time>{formatDate(post.date)}</time>
<span className="w-1 h-1 rounded-full bg-warm-gray" />
<span>{post.readingTime} min read</span>
<span>{readingTimeLabel(post.readingTime)}</span>
</div>
<div className="absolute top-7 right-7 md:top-10 md:right-10 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-terracotta">
@@ -41,29 +37,23 @@ function FeaturedCard({ post }: { post: Post }) {
);
}
export function FeaturedGrid({ posts }: { posts: Post[] }) {
const ref = useRef<HTMLDivElement>(null);
export function FeaturedGrid({ posts }: { posts: PublicPost[] }) {
const ref = useGsapAnimation<HTMLDivElement>((_, isReduced) => {
if (isReduced || posts.length === 0) return;
gsap.from(".featured-card", {
y: 50,
opacity: 0,
duration: 0.8,
stagger: 0.15,
ease: "power3.out",
scrollTrigger: { trigger: ref.current, start: "top 85%" },
});
}, [posts.length]);
useEffect(() => {
if (!ref.current) return;
const ctx = gsap.context(() => {
gsap.from(".featured-card", {
y: 50,
opacity: 0,
duration: 0.8,
stagger: 0.15,
ease: "power3.out",
scrollTrigger: {
trigger: ref.current,
start: "top 85%",
},
});
}, ref.current);
return () => ctx.revert();
}, []);
if (posts.length === 0) return null;
return (
<section ref={ref} className="px-page max-w-5xl mx-auto pb-16">
<section ref={ref as React.RefObject<HTMLDivElement>} className="px-page max-w-5xl mx-auto pb-16">
<div className="grid md:grid-cols-2 gap-5">
{posts.map((post) => (
<FeaturedCard key={post.slug} post={post} />
@@ -73,39 +63,29 @@ export function FeaturedGrid({ posts }: { posts: Post[] }) {
);
}
export function RecentList({ posts }: { posts: Post[] }) {
const ref = useRef<HTMLDivElement>(null);
export function RecentList({ posts }: { posts: PublicPost[] }) {
const ref = useGsapAnimation<HTMLDivElement>((_, isReduced) => {
if (isReduced || posts.length === 0) return;
gsap.from(".recent-item", {
y: 30,
opacity: 0,
duration: 0.6,
stagger: 0.08,
ease: "power3.out",
scrollTrigger: { trigger: ref.current, start: "top 85%" },
});
}, [posts.length]);
useEffect(() => {
if (!ref.current) return;
const ctx = gsap.context(() => {
gsap.from(".recent-item", {
y: 30,
opacity: 0,
duration: 0.6,
stagger: 0.08,
ease: "power3.out",
scrollTrigger: {
trigger: ref.current,
start: "top 85%",
},
});
}, ref.current);
return () => ctx.revert();
}, []);
if (posts.length === 0) return null;
return (
<section ref={ref} className="px-page max-w-5xl mx-auto pb-24">
<section ref={ref as React.RefObject<HTMLDivElement>} className="px-page max-w-5xl mx-auto pb-24">
<div className="divider-ornament mb-10">
<span className="font-display text-sm italic whitespace-nowrap"></span>
</div>
<div className="space-y-0">
{posts.map((post) => (
<Link
key={post.slug}
href={`/posts/${post.slug}`}
className="recent-item group block"
>
<Link key={post.slug} href={`/posts/${post.slug}`} className="recent-item group block">
<article className="flex items-baseline gap-6 py-7 border-b border-warm-gray/10 hover:bg-cream -mx-4 px-4 rounded-lg transition-all duration-300">
<time className="shrink-0 font-sans text-sm text-ink-muted tabular-nums w-28 pt-0.5">
{formatDate(post.date)}