铸渊 2026-08-08 开发,冰朔架构决策:
- Git 是底层引擎,不依赖任何数据库(PostgreSQL/Redis/ORM)
- Agent 直达底层,中间不隔第三方服务
- 拆解 Outline v0.80.2 为参考样本,光湖自己实现全部能力
技术栈:
- 后端:Express v5 + simple-git + gray-matter(Git 操作层)
- 前端:Vite + React 19 + TypeScript + marked
- 存储:Markdown 文件 + Git 仓库(commit=版本历史,diff=对比)
功能清单(全部可用):
- 文档 CRUD(创建/读取/更新/删除/移动)
- 文档树导航(文件夹层级)
- Markdown 编辑器(编辑/预览双栏)
- 全文搜索(防抖 + 下拉结果)
- 版本历史(git log)
- 版本 diff 对比(选择两个 commit 对比)
API 端点(Agent 和 UI 共用):
- GET/POST/PUT/DELETE /api/docs/{*path}
- GET /api/tree
- GET /api/history/{*path}
- GET /api/version/:hash/{*path}
- GET /api/diff/{*path}?from=&to=
- GET /api/search?q=
- POST /api/move
- GET /api/health
105 lines
2.8 KiB
TypeScript
105 lines
2.8 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
|
import { DocContent } from '../api';
|
|
import { marked } from 'marked';
|
|
|
|
interface Props {
|
|
doc: DocContent;
|
|
onSave: (title: string, body: string) => void;
|
|
}
|
|
|
|
export function Editor({ doc, onSave }: Props) {
|
|
const [editing, setEditing] = useState(false);
|
|
const [title, setTitle] = useState(doc.meta.title);
|
|
const [body, setBody] = useState(doc.body);
|
|
|
|
// 切换文档时重置
|
|
useEffect(() => {
|
|
setTitle(doc.meta.title);
|
|
setBody(doc.body);
|
|
setEditing(false);
|
|
}, [doc.meta.id]);
|
|
|
|
const rendered = useMemo(() => {
|
|
try {
|
|
return marked(body, { async: false }) as string;
|
|
} catch {
|
|
return '<p>渲染失败</p>';
|
|
}
|
|
}, [body]);
|
|
|
|
const handleSave = () => {
|
|
onSave(title, body);
|
|
setEditing(false);
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setTitle(doc.meta.title);
|
|
setBody(doc.body);
|
|
setEditing(false);
|
|
};
|
|
|
|
return (
|
|
<div className="kb-editor">
|
|
{/* 元信息栏 */}
|
|
<div className="kb-editor-meta">
|
|
<span className="kb-editor-path">{doc.meta.id}</span>
|
|
<span className="kb-editor-date">
|
|
更新于 {new Date(doc.meta.updatedAt).toLocaleString('zh-CN')}
|
|
</span>
|
|
<span className="kb-editor-author">by {doc.meta.author}</span>
|
|
</div>
|
|
|
|
{/* 标题 */}
|
|
{editing ? (
|
|
<input
|
|
className="kb-editor-title-input"
|
|
value={title}
|
|
onChange={e => setTitle(e.target.value)}
|
|
placeholder="文档标题"
|
|
/>
|
|
) : (
|
|
<h1 className="kb-editor-title" onClick={() => setEditing(true)}>
|
|
{doc.meta.title}
|
|
</h1>
|
|
)}
|
|
|
|
{/* 操作栏 */}
|
|
<div className="kb-editor-toolbar">
|
|
{editing ? (
|
|
<>
|
|
<button className="kb-btn-primary" onClick={handleSave}>保存</button>
|
|
<button className="kb-btn-secondary" onClick={handleCancel}>取消</button>
|
|
</>
|
|
) : (
|
|
<button className="kb-btn-primary" onClick={() => setEditing(true)}>编辑</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 内容区 */}
|
|
{editing ? (
|
|
<div className="kb-editor-edit-pane">
|
|
<textarea
|
|
className="kb-editor-textarea"
|
|
value={body}
|
|
onChange={e => setBody(e.target.value)}
|
|
placeholder="用 Markdown 写作..."
|
|
spellCheck={false}
|
|
/>
|
|
<div className="kb-editor-preview-pane">
|
|
<div
|
|
className="kb-markdown-render"
|
|
dangerouslySetInnerHTML={{ __html: rendered }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="kb-editor-read-pane">
|
|
<div
|
|
className="kb-markdown-render"
|
|
dangerouslySetInnerHTML={{ __html: rendered }}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|