feat(product-source): 光湖知识库模块 v0.1.0 — Git 驱动的完整知识库系统
铸渊 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
This commit is contained in:
parent
920b6bf690
commit
ecfeca40e2
16 changed files with 5970 additions and 0 deletions
105
product-source/guanghu-knowledge-base/src/components/Editor.tsx
Normal file
105
product-source/guanghu-knowledge-base/src/components/Editor.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue