feat: 重构博客为水墨纸质风格 + 搭建后台管理系统
- 重新设计全站 UI:parchment/ink/terracotta 水墨纸质色系,宋式 serif 排版 - 新增页面:文章列表、文章详情、分类、标签、关于 - GSAP ScrollTrigger 滚动动画 + 逐字揭示效果 - 后台管理系统 /admin:文章/分类/标签 CRUD,JSON 文件存储 - 登录认证(cookie session) - 设计系统文档 UI.md
This commit is contained in:
+141
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Seed script — run once to populate initial data from mock posts.
|
||||
* Usage: npx tsx src/lib/seed.ts
|
||||
*/
|
||||
import { createPost, createCategory, createTag, getPosts, getCategories, getTags } from "./store";
|
||||
|
||||
const seedPosts = [
|
||||
{
|
||||
slug: "on-writing-and-silence",
|
||||
title: "论写作与沉默",
|
||||
excerpt: "有些话适合写在纸上,有些话适合留在风里。写作不是填满空白的过程,而是从空白中提炼意义的旅程。",
|
||||
content: "<p>有些话适合写在纸上,有些话适合留在风里。</p><p>我常常觉得,沉默是一种被低估的能力。在这个信息过载的时代,我们急于表达、急于分享,却很少给自己留出沉默的空间。写作不是填满空白的过程,而是从空白中提炼意义的旅程。</p><p>每一次落笔,都是一次与自己的对话。那些在深夜里涌现的念头,像潮水一样涌来,又像退潮后的贝壳,最终留下的才是最珍贵的。</p><p>我开始学会在写作之前先沉默。让想法在脑海中沉淀,让语言在时间里发酵。好的文字从来不是急出来的。</p>",
|
||||
date: "2026-06-15",
|
||||
category: "随笔",
|
||||
tags: ["写作", "思考", "生活哲学"],
|
||||
readingTime: 4,
|
||||
featured: true,
|
||||
status: "published" as const,
|
||||
},
|
||||
{
|
||||
slug: "a-walk-in-the-mountains",
|
||||
title: "山中漫步",
|
||||
excerpt: "大别山的清晨,雾气从谷底升起,像是大地在缓缓呼吸。脚下的石板路被露水打湿,每一步都需要格外小心。",
|
||||
content: "<p>大别山的清晨,雾气从谷底升起,像是大地在缓缓呼吸。</p><p>脚下的石板路被露水打湿,每一步都需要格外小心。路旁的野花在薄雾中若隐若现,紫色和白色交替出现,像是大自然精心编排的欢迎仪式。</p><p>山里的时间过得格外慢。没有手机的信号,没有城市的喧嚣,只有鸟鸣和溪流的声音。这种安静让我想起小时候在外婆家的日子。</p>",
|
||||
date: "2026-06-10",
|
||||
category: "旅行",
|
||||
tags: ["旅行", "自然", "六安"],
|
||||
readingTime: 6,
|
||||
featured: true,
|
||||
status: "published" as const,
|
||||
},
|
||||
{
|
||||
slug: "notes-on-digital-twin",
|
||||
title: "数字孪生笔记:从3D建模到Web可视化",
|
||||
excerpt: "从 Three.js 到 React Three Fiber,Web 3D 的门槛比想象中低很多,但要做好,需要理解的东西远不止代码。",
|
||||
content: "<p>从 Three.js 到 React Three Fiber,Web 3D 的门槛比想象中低很多。</p><p>但要做好,需要理解的东西远不止代码。光照、材质、相机、性能优化,每一个都是深坑。这篇文章记录我在数字孪生项目中的一些实践和思考。</p>",
|
||||
date: "2026-06-05",
|
||||
category: "技术",
|
||||
tags: ["Web3D", "React", "Three.js", "前端"],
|
||||
readingTime: 8,
|
||||
featured: false,
|
||||
status: "published" as const,
|
||||
},
|
||||
{
|
||||
slug: "reading-list-spring",
|
||||
title: "春日书单:五本改变我看世界方式的书",
|
||||
excerpt: "春天适合读一些柔软的书。不是那种让人醍醐灌顶的大部头,而是像春雨一样润物细无声的文字。",
|
||||
content: "<p>春天适合读一些柔软的书。</p><p>不是那种让人醍醐灌顶的大部头,而是像春雨一样润物细无声的文字。以下五本书,在这个春天给了我很多安静的力量。</p>",
|
||||
date: "2026-05-28",
|
||||
category: "阅读",
|
||||
tags: ["阅读", "书单", "生活"],
|
||||
readingTime: 5,
|
||||
featured: true,
|
||||
status: "published" as const,
|
||||
},
|
||||
{
|
||||
slug: "stable-diffusion-local-setup",
|
||||
title: "本地部署 Stable Diffusion 踩坑记",
|
||||
excerpt: "M3 16GB 的统一内存是优势也是限制。记录在 Apple Silicon 上跑 SD 1.5 和 SDXL 的完整过程。",
|
||||
content: "<p>M3 16GB 的统一内存是优势也是限制。</p><p>这篇文章记录在 Apple Silicon 上跑 SD 1.5 和 SDXL 的完整过程,包括环境搭建、模型选择、LoRA 训练的一些尝试。</p>",
|
||||
date: "2026-05-20",
|
||||
category: "技术",
|
||||
tags: ["AI", "Stable Diffusion", "Apple Silicon"],
|
||||
readingTime: 10,
|
||||
featured: false,
|
||||
status: "published" as const,
|
||||
},
|
||||
{
|
||||
slug: "lightbox-dream",
|
||||
title: "灯箱:一个小城青年的创业梦",
|
||||
excerpt: "标识灯箱这个行业,外行人觉得简单,内行人知道水深。从3D预览到商业模式,记录 sui_lightbox 的诞生过程。",
|
||||
content: "<p>标识灯箱这个行业,外行人觉得简单,内行人知道水深。</p><p>从最初的一个想法,到3D预览原型的实现,再到商业模式的探索。这篇文章记录 sui_lightbox 项目从0到1的过程。</p>",
|
||||
date: "2026-05-12",
|
||||
category: "创业",
|
||||
tags: ["创业", "灯箱", "产品", "sui_lightbox"],
|
||||
readingTime: 7,
|
||||
featured: true,
|
||||
status: "published" as const,
|
||||
},
|
||||
{
|
||||
slug: "rainy-day-thoughts",
|
||||
title: "雨天杂记",
|
||||
excerpt: "六月的梅雨季,窗外的梧桐叶被雨打得东倒西歪。泡一壶六安瓜片,坐在窗前看雨,什么都不想。",
|
||||
content: "<p>六月的梅雨季,窗外的梧桐叶被雨打得东倒西歪。</p><p>泡一壶六安瓜片,坐在窗前看雨,什么都不想。这种无所事事的下午,反而是一周中最有创造力的时刻。</p>",
|
||||
date: "2026-05-05",
|
||||
category: "随笔",
|
||||
tags: ["随笔", "生活", "六安"],
|
||||
readingTime: 3,
|
||||
featured: false,
|
||||
status: "published" as const,
|
||||
},
|
||||
{
|
||||
slug: "next-js-blog-from-scratch",
|
||||
title: "从零搭建一个博客系统",
|
||||
excerpt: "为什么选择 Next.js + Halo CMS?为什么不用 WordPress?这篇文章聊聊技术选型的思考过程。",
|
||||
content: "<p>为什么选择 Next.js + Halo CMS?为什么不用 WordPress?</p><p>作为一个前端开发者,我希望能完全掌控博客的UI和交互体验。Halo CMS 提供了足够好的内容管理API,而 Next.js 则让我可以自由设计前端展示。</p>",
|
||||
date: "2026-04-28",
|
||||
category: "技术",
|
||||
tags: ["Next.js", "博客", "Halo CMS", "前端"],
|
||||
readingTime: 6,
|
||||
featured: false,
|
||||
status: "published" as const,
|
||||
},
|
||||
];
|
||||
|
||||
const seedCategories = [
|
||||
{ name: "技术", description: "代码、架构与技术探索" },
|
||||
{ name: "随笔", description: "生活感悟与碎片思考" },
|
||||
{ name: "旅行", description: "在路上看到的风景与人" },
|
||||
{ name: "阅读", description: "书中世界与阅读心得" },
|
||||
{ name: "创业", description: "产品思考与创业记录" },
|
||||
];
|
||||
|
||||
const seedTags = [
|
||||
"写作", "思考", "生活哲学", "旅行", "自然", "六安",
|
||||
"Web3D", "React", "Three.js", "前端", "阅读", "书单",
|
||||
"生活", "AI", "Stable Diffusion", "Apple Silicon",
|
||||
"创业", "灯箱", "产品", "sui_lightbox", "随笔",
|
||||
"Next.js", "博客", "Halo CMS",
|
||||
];
|
||||
|
||||
// Only seed if empty
|
||||
if (getPosts().length === 0) {
|
||||
console.log("Seeding posts...");
|
||||
seedPosts.forEach((p) => createPost(p));
|
||||
console.log(` Created ${seedPosts.length} posts`);
|
||||
}
|
||||
|
||||
if (getCategories().length === 0) {
|
||||
console.log("Seeding categories...");
|
||||
seedCategories.forEach((c) => createCategory(c));
|
||||
console.log(` Created ${seedCategories.length} categories`);
|
||||
}
|
||||
|
||||
if (getTags().length === 0) {
|
||||
console.log("Seeding tags...");
|
||||
seedTags.forEach((t) => createTag({ name: t }));
|
||||
console.log(` Created ${seedTags.length} tags`);
|
||||
}
|
||||
|
||||
console.log("Seed complete.");
|
||||
@@ -0,0 +1,153 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), "src/data/storage");
|
||||
|
||||
// Ensure storage directory exists
|
||||
if (!existsSync(DATA_DIR)) {
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
content: string;
|
||||
date: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
readingTime: number;
|
||||
featured: boolean;
|
||||
status: "draft" | "published";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function readJSON<T>(filename: string, fallback: T): T {
|
||||
const filepath = path.join(DATA_DIR, filename);
|
||||
if (!existsSync(filepath)) return fallback;
|
||||
try {
|
||||
return JSON.parse(readFileSync(filepath, "utf-8"));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJSON(filename: string, data: unknown) {
|
||||
const filepath = path.join(DATA_DIR, filename);
|
||||
writeFileSync(filepath, JSON.stringify(data, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
||||
}
|
||||
|
||||
// ── Posts ──
|
||||
|
||||
export function getPosts(): Post[] {
|
||||
return readJSON<Post[]>("posts.json", []);
|
||||
}
|
||||
|
||||
export function getPost(id: string): Post | undefined {
|
||||
return getPosts().find((p) => p.id === id);
|
||||
}
|
||||
|
||||
export function getPostBySlug(slug: string): Post | undefined {
|
||||
return getPosts().find((p) => p.slug === slug);
|
||||
}
|
||||
|
||||
export function createPost(data: Omit<Post, "id" | "createdAt" | "updatedAt">): Post {
|
||||
const posts = getPosts();
|
||||
const now = new Date().toISOString();
|
||||
const post: Post = {
|
||||
...data,
|
||||
id: generateId(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
posts.unshift(post);
|
||||
writeJSON("posts.json", posts);
|
||||
return post;
|
||||
}
|
||||
|
||||
export function updatePost(id: string, data: Partial<Post>): Post | null {
|
||||
const posts = getPosts();
|
||||
const index = posts.findIndex((p) => p.id === id);
|
||||
if (index === -1) return null;
|
||||
posts[index] = { ...posts[index], ...data, updatedAt: new Date().toISOString() };
|
||||
writeJSON("posts.json", posts);
|
||||
return posts[index];
|
||||
}
|
||||
|
||||
export function deletePost(id: string): boolean {
|
||||
const posts = getPosts();
|
||||
const filtered = posts.filter((p) => p.id !== id);
|
||||
if (filtered.length === posts.length) return false;
|
||||
writeJSON("posts.json", filtered);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Categories ──
|
||||
|
||||
export function getCategories(): Category[] {
|
||||
return readJSON<Category[]>("categories.json", []);
|
||||
}
|
||||
|
||||
export function createCategory(data: Omit<Category, "id">): Category {
|
||||
const categories = getCategories();
|
||||
const cat: Category = { ...data, id: generateId() };
|
||||
categories.push(cat);
|
||||
writeJSON("categories.json", categories);
|
||||
return cat;
|
||||
}
|
||||
|
||||
export function updateCategory(id: string, data: Partial<Category>): Category | null {
|
||||
const categories = getCategories();
|
||||
const index = categories.findIndex((c) => c.id === id);
|
||||
if (index === -1) return null;
|
||||
categories[index] = { ...categories[index], ...data };
|
||||
writeJSON("categories.json", categories);
|
||||
return categories[index];
|
||||
}
|
||||
|
||||
export function deleteCategory(id: string): boolean {
|
||||
const categories = getCategories();
|
||||
const filtered = categories.filter((c) => c.id !== id);
|
||||
if (filtered.length === categories.length) return false;
|
||||
writeJSON("categories.json", filtered);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Tags ──
|
||||
|
||||
export function getTags(): Tag[] {
|
||||
return readJSON<Tag[]>("tags.json", []);
|
||||
}
|
||||
|
||||
export function createTag(data: Omit<Tag, "id">): Tag {
|
||||
const tags = getTags();
|
||||
const tag: Tag = { ...data, id: generateId() };
|
||||
tags.push(tag);
|
||||
writeJSON("tags.json", tags);
|
||||
return tag;
|
||||
}
|
||||
|
||||
export function deleteTag(id: string): boolean {
|
||||
const tags = getTags();
|
||||
const filtered = tags.filter((t) => t.id !== id);
|
||||
if (filtered.length === tags.length) return false;
|
||||
writeJSON("tags.json", filtered);
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user