KNOWLEDGE

站点项目工作台UI重构-NapCat-Linear风格

2026/05/04 40 min read KNOWLEDGE 站点项目工作台UI重构 NAPCAT LINEAR风格

好,这次我们不再“修修补补”,而是直接给你一版:

👉 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 接 Markdown

P2

✔ 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.ts

3. 项目页主结构

---
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 负责“协作和版本”

这版才适合你要做的“游戏工作室项目空间”。