好,这次我们不再“修修补补”,而是直接给你一版:
👉 NapCat / Linear 架构(应用级 UI)
👉 + 你现在的 Heritage 配色 / 字体 / 文化气质
👉 = 「内容操作系统 UI」而且是可以直接替换你现在项目页结构的版本。
🧠 一、这次重构的核心(你必须理解)
我们做的是这件事:
把「博客页面」
→ 重构成「项目工作台(App UI)」❌ 你现在
Header
↓
卡片
↓
卡片
↓
卡片✅ 重构后
App Layout
├─ Sidebar(项目导航)
├─ Main(内容区)
│ ├─ Hero(项目头)
│ ├─ Tabs(模块切换)
│ ├─ Panel(内容区)
│ └─ Timeline(可编辑)🎯 二、最终效果(你会得到)
✔ 没有“方框套方框”
✔ 没有“后台按钮感”
✔ 有状态、有层级、有动效
✔ 像产品,不像博客🚀 三、完整可用代码(Astro + React)
你可以直接替换项目页。
📄 /projects/[slug].astro
---
import Tabs from "../../components/AppTabs.jsx";
import Timeline from "../../components/AppTimeline.jsx";
const project = {
title: "emptyinkpot / unified site",
desc: "当前博客的统一站点工程",
progress: 72,
};
---
<html lang="zh">
<body class="app">
<!-- 左侧导航 -->
<aside class="sidebar">
<div class="logo">EMPTYINKPOT</div>
<nav>
<a href="/" class="item">首页</a>
<a href="/projects" class="item active">项目工坊</a>
<a href="/knowledge" class="item">知识图谱</a>
</nav>
</aside>
<!-- 主区域 -->
<main class="main">
<!-- Hero -->
<section class="hero fade">
<div>
<div class="kicker">项目工坊</div>
<h1>{project.title}</h1>
<p>{project.desc}</p>
</div>
<div class="progress">
<div class="num">{project.progress}%</div>
<div class="bar">
<div style={`width:${project.progress}%`} />
</div>
</div>
</section>
<!-- Tabs -->
<Tabs client:load />
<!-- 内容 Panel -->
<section class="panel fade">
<h2>项目总览</h2>
<p>
这是一个统一内容与知识系统的站点工程。
</p>
<blockquote>
重点不是页面,而是系统。
</blockquote>
</section>
<!-- Timeline -->
<Timeline client:load />
</main>
</body>
</html>
<style>
/* ===== 布局(NapCat 核心) ===== */
.app {
display: grid;
grid-template-columns: 220px 1fr;
height: 100vh;
background: #f5f1e8;
font-family: "Noto Serif SC", serif;
}
/* ===== Sidebar ===== */
.sidebar {
padding: 24px 16px;
border-right: 1px solid rgba(0,0,0,0.06);
}
.logo {
font-weight: bold;
margin-bottom: 20px;
}
.item {
display: block;
padding: 8px 0;
opacity: 0.6;
}
.item.active {
opacity: 1;
border-left: 3px solid #2f5d50;
padding-left: 8px;
}
/* ===== 主区域 ===== */
.main {
padding: 40px;
overflow-y: auto;
}
/* ===== Hero ===== */
.hero {
display: flex;
justify-content: space-between;
margin-bottom: 40px;
}
h1 {
font-size: 36px;
color: #2f5d50;
}
.kicker {
font-size: 12px;
letter-spacing: 0.1em;
opacity: 0.6;
}
/* ===== 进度 ===== */
.progress {
text-align: right;
}
.num {
font-size: 36px;
}
.bar {
height: 6px;
background: #ddd;
}
.bar div {
height: 100%;
background: #2f5d50;
}
/* ===== Panel(无卡片!) ===== */
.panel {
padding: 20px 0;
border-bottom: 1px solid rgba(0,0,0,0.08);
}
blockquote {
border-left: 3px solid #6b2d5c;
padding-left: 10px;
}
/* ===== 动效 ===== */
.fade {
animation: fade 0.25s ease;
}
@keyframes fade {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
</style>📄 /components/AppTabs.jsx
import { useState } from "react";
export default function Tabs() {
const [tab, setTab] = useState("overview");
const tabs = [
{ id: "overview", label: "总览" },
{ id: "modules", label: "模块" },
{ id: "wiki", label: "世界观" },
{ id: "timeline", label: "时间线" }
];
return (
<div style={{ marginBottom: 30 }}>
{tabs.map(t => (
<span
key={t.id}
onClick={() => setTab(t.id)}
style={{
marginRight: 20,
cursor: "pointer",
opacity: tab === t.id ? 1 : 0.5,
borderBottom: tab === t.id ? "2px solid #2f5d50" : "none"
}}
>
{t.label}
</span>
))}
</div>
);
}📄 /components/AppTimeline.jsx
import { useState } from "react";
export default function Timeline() {
const [items, setItems] = useState([
{ text: "初始化项目结构" }
]);
const [input, setInput] = useState("");
function add() {
if (!input) return;
setItems([{ text: input }, ...items]);
setInput("");
}
return (
<div style={{ marginTop: 40 }}>
<h2>时间线</h2>
<div style={{ marginBottom: 16 }}>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="记录进展..."
style={{
padding: 6,
border: "1px solid #ccc"
}}
/>
<button onClick={add}>发布</button>
</div>
{items.map((i, idx) => (
<div key={idx} style={{ marginBottom: 10 }}>
• {i.text}
</div>
))}
</div>
);
}🔥 四、你这版 vs 之前(本质变化)
❌ 之前
卡片 UI
后台感
按钮堆
结构混乱✅ 现在
应用布局(App Layout)
内容分区(Panel)
状态驱动(Tabs / Timeline)
轻边界(不是盒子)🎯 五、你接下来该做的(非常重要)
P0
✔ 删除旧 header
✔ 全站统一 layout
✔ 用 sidebar 替代顶部导航P1
✔ Timeline 接 GitHub
✔ Wiki 接 MarkdownP2
✔ Graph 接入
✔ 状态动画增强💬 最后一句(关键)
你现在终于从:
“博客页面”进入:
“产品 UI(App UI)”如果你下一步要更狠一点,我可以帮你:
👉 做一版 Obsidian + NapCat 融合(Graph + 阅读 + 项目一体)
那一版会直接让你的站:
不像博客,像产品可以,这一版我建议叫:
# Project OS:Obsidian + NapCat 融合版
它不是普通项目页,而是:
```text
项目工作台
+ Wiki 阅读器
+ 可编辑时间线
+ 世界观 Graph
+ GitHub 协作后端并且和你现有 MyBlog 的 /projects/、/knowledge/、Reader、Graph、settings 体系兼容。你当前 README 已经把 MyBlog 定位成 Content OS,并且已有 /projects/、/knowledge/、Knowledge Layer、Reader、Graph、settings token 等基础,所以这版应该接在现有系统上,不要另起炉灶。
1. 最终页面形态
/projects/[slug]/
┌─────────────────────────────────────────────┐
│ Project OS Topbar │
├───────────────┬─────────────────────────────┤
│ 左侧项目导航 │ 主工作区 │
│ │ │
│ 总览 │ Overview / Wiki / Timeline │
│ 模块 │ Reader / Editor / Graph │
│ 世界观 │ │
│ 卡牌 │ │
│ 时间线 │ │
│ 贡献者 │ │
└───────────────┴─────────────────────────────┘一句话:
左侧像 Obsidian,主区像 NapCat 控制台,内容阅读像 Medium / Kindle,Graph 像知识地图。2. 文件结构
apps/web/src/pages/projects/
├─ index.astro
└─ [slug].astro
apps/web/src/components/projects/
├─ ProjectOsShell.astro
├─ ProjectSidebar.astro
├─ ProjectTopbar.astro
├─ ProjectOverview.astro
├─ ProjectModules.astro
├─ ProjectWikiReader.astro
├─ ProjectTimelineEditor.tsx
├─ ProjectContributors.astro
└─ ProjectMiniGraph.astro
apps/web/src/lib/projects.ts
apps/web/src/data/project-studios.ts3. 项目页主结构
---
import BaseLayout from "../../layouts/BaseLayout.astro";
import ProjectTimelineEditor from "../../components/projects/ProjectTimelineEditor.tsx";
const project = {
slug: "project-vita",
title: "Project Vita",
subtitle: "游戏世界观、卡牌与叙事系统工作台",
progress: 42,
repo: "emptyinkpot/project-vita",
modules: [
{ name: "世界观", progress: 55, status: "进行中" },
{ name: "卡牌系统", progress: 25, status: "草稿" },
{ name: "战斗原型", progress: 10, status: "计划中" }
]
};
---
<BaseLayout title={project.title}>
<main class="project-os">
<aside class="project-os__sidebar">
<div class="project-brand">
<p>PROJECT OS</p>
<h1>{project.title}</h1>
</div>
<nav class="project-nav">
<a class="is-active" href="#overview">总览</a>
<a href="#modules">模块进度</a>
<a href="#wiki">世界观</a>
<a href="#timeline">时间线</a>
<a href="#contributors">贡献者</a>
<a href="#graph">知识关系</a>
</nav>
</aside>
<section class="project-os__main">
<header class="project-topbar">
<div>
<p>项目工坊 / 开源协作</p>
<strong>{project.title}</strong>
</div>
<div class="project-actions">
<a href={`https://github.com/${project.repo}`}>查看仓库 →</a>
<a href={`https://github.com/${project.repo}/edit/main/wiki/index.md`}>
编辑 Wiki →
</a>
</div>
</header>
<section id="overview" class="project-hero section-reveal">
<div>
<p class="eyebrow">PROJECT STUDIO</p>
<h2>{project.title}</h2>
<p>{project.subtitle}</p>
</div>
<div class="project-progress">
<strong>{project.progress}%</strong>
<span>总体进度</span>
<div class="progress-line">
<i style={`width:${project.progress}%`}></i>
</div>
</div>
</section>
<section id="modules" class="project-section section-reveal">
<p class="eyebrow">模块进度</p>
<div class="module-list">
{project.modules.map((module) => (
<article class="module-row">
<div>
<strong>{module.name}</strong>
<span>{module.status}</span>
</div>
<div class="module-progress">
<i style={`width:${module.progress}%`}></i>
</div>
<em>{module.progress}%</em>
</article>
))}
</div>
</section>
<section id="wiki" class="project-section project-reader section-reveal">
<p class="eyebrow">世界观 Wiki</p>
<h2>世界观总览</h2>
<p>
这里展示项目仓库中的 wiki/index.md。成员可以通过 GitHub 或后续 CMS 编辑,
所有修改都会留下 commit 记录。
</p>
<blockquote>
世界观、角色、阵营、卡牌与事件都应该被当作可追踪的知识节点。
</blockquote>
</section>
<section id="timeline" class="project-section section-reveal">
<p class="eyebrow">时间线</p>
<ProjectTimelineEditor client:load projectSlug={project.slug} />
</section>
<section id="graph" class="project-section project-graph section-reveal">
<p class="eyebrow">知识关系</p>
<div class="graph-stage">
<div class="graph-node graph-node--center">Project Vita</div>
<div class="graph-node graph-node--world">世界观</div>
<div class="graph-node graph-node--cards">卡牌</div>
<div class="graph-node graph-node--team">成员</div>
</div>
</section>
</section>
</main>
</BaseLayout>4. CSS:Obsidian + NapCat + Heritage
.project-os {
width: min(1520px, 100% - 40px);
margin: 0 auto;
padding: 20px 0 32px;
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 24px;
color: var(--heritage-ink);
}
.project-os__sidebar {
position: sticky;
top: 20px;
align-self: start;
height: calc(100vh - 40px);
padding: 18px 16px;
background: var(--heritage-paper);
border: 1px solid var(--heritage-line-strong);
border-radius: 6px;
overflow: auto;
}
.project-brand p,
.eyebrow {
margin: 0 0 8px;
color: var(--heritage-purple);
font-family: var(--font-ui);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.project-brand h1 {
margin: 0 0 20px;
font-family: var(--font-display);
font-size: 24px;
line-height: 1.16;
color: var(--heritage-green);
}
.project-nav {
display: grid;
gap: 2px;
}
.project-nav a {
padding: 9px 10px;
border-radius: 4px;
color: var(--heritage-muted);
font-family: var(--font-ui);
font-size: 14px;
text-decoration: none;
transition:
background var(--motion-fast) var(--ease-standard),
color var(--motion-fast) var(--ease-standard),
transform var(--motion-fast) var(--ease-standard);
}
.project-nav a:hover {
background: rgba(47, 93, 80, 0.08);
color: var(--heritage-green);
transform: translateX(2px);
}
.project-nav a.is-active {
background: var(--heritage-card);
color: var(--heritage-green);
box-shadow: inset 3px 0 0 var(--heritage-green);
}
.project-os__main {
min-width: 0;
display: grid;
gap: 18px;
}
.project-topbar {
position: sticky;
top: 0;
z-index: 20;
min-height: 58px;
padding: 12px 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
background: var(--heritage-bg);
border-bottom: 1px solid var(--heritage-line-strong);
}
.project-topbar p {
margin: 0;
color: var(--heritage-muted);
font-family: var(--font-ui);
font-size: 12px;
}
.project-topbar strong {
font-family: var(--font-display);
font-size: 18px;
}
.project-actions {
display: flex;
gap: 14px;
flex-wrap: wrap;
}
.project-actions a {
color: var(--heritage-green);
font-family: var(--font-ui);
font-size: 14px;
font-weight: 700;
text-decoration: none;
}
.project-hero,
.project-section {
background: transparent;
border: 0;
border-bottom: 1px solid var(--heritage-line-strong);
padding: 32px 0;
}
.project-hero {
display: grid;
grid-template-columns: minmax(0, 1fr) 260px;
gap: 40px;
align-items: end;
}
.project-hero h2 {
max-width: 12ch;
margin: 0;
color: var(--heritage-green);
font-family: var(--font-display);
font-size: clamp(42px, 7vw, 78px);
line-height: 0.96;
letter-spacing: -0.05em;
}
.project-hero p:not(.eyebrow) {
max-width: 58ch;
color: var(--heritage-muted);
font-size: 17px;
line-height: 1.8;
}
.project-progress strong {
display: block;
color: var(--heritage-green);
font-family: var(--font-display);
font-size: 58px;
line-height: 1;
}
.project-progress span {
display: block;
margin-top: 8px;
color: var(--heritage-muted);
font-family: var(--font-ui);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.progress-line,
.module-progress {
height: 8px;
margin-top: 14px;
background: var(--heritage-paper-deep);
overflow: hidden;
}
.progress-line i,
.module-progress i {
display: block;
height: 100%;
background: var(--heritage-green);
transform-origin: left;
animation: progress-in 620ms var(--ease-standard);
}
.module-list {
display: grid;
gap: 0;
}
.module-row {
display: grid;
grid-template-columns: minmax(0, 220px) minmax(0, 1fr) 56px;
align-items: center;
gap: 18px;
padding: 16px 0;
border-top: 1px solid var(--heritage-line);
}
.module-row strong {
display: block;
color: var(--heritage-ink);
font-size: 17px;
}
.module-row span,
.module-row em {
color: var(--heritage-muted);
font-family: var(--font-ui);
font-size: 13px;
font-style: normal;
}
.project-reader {
max-width: var(--reader-column-width);
}
.project-reader h2 {
margin: 0 0 16px;
color: var(--heritage-purple);
font-family: var(--font-display);
font-size: 34px;
line-height: 1.2;
}
.project-reader p {
font-size: var(--type-body-size);
line-height: var(--type-body-line);
}
.project-reader blockquote {
margin: 24px 0;
padding-left: 14px;
border-left: 4px solid var(--heritage-purple);
color: var(--heritage-muted);
font-size: 18px;
line-height: 1.8;
}
.graph-stage {
position: relative;
min-height: 360px;
background:
linear-gradient(90deg, rgba(107, 45, 92, 0.04) 1px, transparent 1px),
linear-gradient(180deg, rgba(107, 45, 92, 0.035) 1px, transparent 1px);
background-size: 44px 44px;
}
.graph-node {
position: absolute;
padding: 8px 12px;
border-radius: 999px;
background: var(--heritage-card);
border: 1px solid var(--heritage-line-strong);
font-family: var(--font-ui);
font-size: 13px;
font-weight: 700;
}
.graph-node--center {
left: 45%;
top: 42%;
color: var(--heritage-purple);
}
.graph-node--world {
left: 20%;
top: 26%;
color: var(--heritage-green);
}
.graph-node--cards {
right: 18%;
top: 28%;
color: var(--heritage-red);
}
.graph-node--team {
left: 52%;
bottom: 18%;
color: var(--heritage-gold);
}
.section-reveal {
animation: section-reveal 260ms var(--ease-standard) both;
}
@keyframes section-reveal {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes progress-in {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
@media (max-width: 980px) {
.project-os {
display: block;
width: min(100% - 24px, 760px);
}
.project-os__sidebar {
position: static;
height: auto;
margin-bottom: 18px;
}
.project-hero {
grid-template-columns: 1fr;
}
.module-row {
grid-template-columns: 1fr;
gap: 8px;
}
}5. 时间线编辑器:页面内编辑
第一版先存在浏览器本地,后续再接 GitHub API / Decap / Tina。
import { useEffect, useState } from "react";
type TimelineItem = {
id: string;
date: string;
title: string;
body: string;
author: string;
};
export default function ProjectTimelineEditor({ projectSlug }: { projectSlug: string }) {
const key = `project:${projectSlug}:timeline`;
const [items, setItems] = useState<TimelineItem[]>([]);
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
useEffect(() => {
try {
setItems(JSON.parse(localStorage.getItem(key) || "[]"));
} catch {
setItems([]);
}
}, [key]);
function save(next: TimelineItem[]) {
setItems(next);
localStorage.setItem(key, JSON.stringify(next));
}
function addItem() {
if (!title.trim()) return;
save([
{
id: crypto.randomUUID(),
date: new Date().toISOString().slice(0, 10),
title: title.trim(),
body: body.trim(),
author: "本地编辑"
},
...items
]);
setTitle("");
setBody("");
}
return (
<div className="timeline-editor">
<div className="timeline-compose">
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="记录今天的进展..."
/>
<textarea
value={body}
onChange={(event) => setBody(event.target.value)}
placeholder="补充说明,可写世界观、模块变更、卡牌调整..."
/>
<button onClick={addItem}>发布进展</button>
</div>
<div className="timeline-list">
{items.length === 0 ? (
<p className="timeline-empty">暂无时间线记录。</p>
) : (
items.map((item) => (
<article className="timeline-item" key={item.id}>
<time>{item.date}</time>
<strong>{item.title}</strong>
{item.body ? <p>{item.body}</p> : null}
<span>{item.author}</span>
</article>
))
)}
</div>
</div>
);
}配套 CSS:
.timeline-editor {
display: grid;
gap: 24px;
}
.timeline-compose {
display: grid;
gap: 10px;
max-width: 680px;
}
.timeline-compose input,
.timeline-compose textarea {
width: 100%;
border: 0;
border-bottom: 1px solid var(--heritage-line-strong);
background: transparent;
padding: 10px 0;
color: var(--heritage-ink);
font: inherit;
outline: none;
}
.timeline-compose textarea {
min-height: 90px;
resize: vertical;
}
.timeline-compose button {
width: fit-content;
border: 0;
background: transparent;
color: var(--heritage-green);
font-family: var(--font-ui);
font-weight: 800;
cursor: pointer;
}
.timeline-list {
display: grid;
}
.timeline-item {
position: relative;
display: grid;
gap: 6px;
padding: 18px 0 18px 24px;
border-top: 1px solid var(--heritage-line);
}
.timeline-item::before {
content: "";
position: absolute;
left: 0;
top: 25px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--heritage-green);
}
.timeline-item time {
color: var(--heritage-muted);
font-family: var(--font-ui);
font-size: 12px;
}
.timeline-item strong {
color: var(--heritage-ink);
font-size: 18px;
}
.timeline-item p {
margin: 0;
color: var(--heritage-muted);
line-height: 1.7;
}
.timeline-item span {
color: var(--heritage-purple);
font-family: var(--font-ui);
font-size: 12px;
}6. 和 GitHub 协作后端怎么接
第一阶段:
页面内编辑 → localStorage第二阶段:
页面内编辑 → GitHub API 创建 commit你需要写一个 API,而不是把 token 暴露到前端:
POST /api/projects/[slug]/timeline请求:
{
"title": "完成卡牌原型",
"body": "新增 12 张测试卡牌。",
"author": "某成员"
}后端逻辑:
读取 repo/data/timeline.json
追加记录
用 GitHub API commit 回 main 或新分支这个时候贡献记录可以天然从 GitHub commits 读取:
commits → 时间线
contributors → 贡献者
issues / PR → 项目协作动态7. 你要加的最终功能
P0
- 项目工坊入口
- Obsidian 左侧导航
- NapCat 式主工作区
- 本地时间线编辑
P1
- Wiki Markdown 展示
- GitHub 编辑按钮
- GitHub contributors / commits 时间线
P2
- 页面内编辑写回 GitHub
- 世界观 / 卡牌 / 角色结构化数据
- Project Graph
P3
- Decap CMS 或 TinaCMS
- 成员权限
- 审核 / PR / diff / 版本历史核心原则:
Obsidian 负责“知识结构”
NapCat 负责“应用布局”
Heritage 负责“你的视觉气质”
GitHub 负责“协作和版本”这版才适合你要做的“游戏工作室项目空间”。