feat(knowledge): restore complete human rendering layer
This commit is contained in:
parent
893c1a0a12
commit
3466b507c6
15 changed files with 665 additions and 23 deletions
|
|
@ -4,6 +4,12 @@ import { relaunch } from "@tauri-apps/plugin-process";
|
|||
import { Icon } from "./icons";
|
||||
import * as api from "./runtime";
|
||||
import type { KnowledgeDocument, SourceKind, SystemSnapshot } from "./types";
|
||||
import {
|
||||
MarkdownDocument,
|
||||
documentOutline,
|
||||
jumpToHeading,
|
||||
splitFrontmatter,
|
||||
} from "./modules/knowledge-render";
|
||||
|
||||
type View =
|
||||
| "channel"
|
||||
|
|
@ -248,9 +254,10 @@ function Channel({
|
|||
<article>
|
||||
<h2>公共更新</h2>
|
||||
<strong>
|
||||
{data.publicDistributionState === "SIGNED_UPDATE_FEED_READY_NO_RELEASE"
|
||||
? "更新入口在线 · 暂无发行"
|
||||
: data.publicDistributionState}
|
||||
{data.publicDistributionState ===
|
||||
"SIGNED_UPDATE_FEED_READY_NO_RELEASE"
|
||||
? "更新入口在线 · 暂无发行"
|
||||
: data.publicDistributionState}
|
||||
</strong>
|
||||
<p>只接收签名发行,不自动执行普通 Git 拉取。</p>
|
||||
</article>
|
||||
|
|
@ -310,12 +317,15 @@ function Knowledge({
|
|||
[doc, setDoc] = useState<KnowledgeDocument | null>(null),
|
||||
[body, setBody] = useState(""),
|
||||
[query, setQuery] = useState(""),
|
||||
[newTitle, setNewTitle] = useState<string | null>(null);
|
||||
[newTitle, setNewTitle] = useState<string | null>(null),
|
||||
[editing, setEditing] = useState(false),
|
||||
[inspectorOpen, setInspectorOpen] = useState(true);
|
||||
useEffect(() => {
|
||||
if (selected)
|
||||
api.readDocument(selected).then((d) => {
|
||||
setDoc(d);
|
||||
setBody(d.body);
|
||||
setEditing(false);
|
||||
});
|
||||
}, [selected]);
|
||||
const list = useMemo(
|
||||
|
|
@ -337,6 +347,8 @@ function Knowledge({
|
|||
const save = async () => {
|
||||
if (doc) {
|
||||
await api.saveDocument(doc.path, body);
|
||||
setDoc({ ...doc, body });
|
||||
setEditing(false);
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
|
@ -348,6 +360,20 @@ function Knowledge({
|
|||
refresh();
|
||||
}
|
||||
};
|
||||
const parsed = useMemo(() => splitFrontmatter(doc?.body ?? ""), [doc?.body]);
|
||||
const outline = useMemo(() => documentOutline(doc?.body ?? ""), [doc?.body]);
|
||||
const stats = useMemo(() => {
|
||||
const chars = parsed.content.replace(/\s/g, "").length;
|
||||
return { chars, minutes: Math.max(1, Math.ceil(chars / 450)) };
|
||||
}, [parsed.content]);
|
||||
const openWiki = (target: string) => {
|
||||
const normalized = target.replace(/[\s·•・\-_|*`]/g, "").toLowerCase();
|
||||
const hit = data.documents.find(
|
||||
(item) =>
|
||||
item.title.replace(/[\s·•・\-_|*`]/g, "").toLowerCase() === normalized,
|
||||
);
|
||||
if (hit) setSelected(hit.path);
|
||||
};
|
||||
return (
|
||||
<div className="page knowledge-page">
|
||||
<header>
|
||||
|
|
@ -368,8 +394,14 @@ function Knowledge({
|
|||
<button onClick={() => setNewTitle("")}>新建知识页</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="knowledge-workspace">
|
||||
<aside>
|
||||
<div
|
||||
className={`knowledge-workspace complete ${inspectorOpen ? "" : "inspector-closed"}`}
|
||||
>
|
||||
<aside className="knowledge-browser">
|
||||
<div className="browser-title">
|
||||
<strong>知识目录</strong>
|
||||
<span>{data.documents.length} 篇</span>
|
||||
</div>
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
|
@ -392,25 +424,69 @@ function Knowledge({
|
|||
))
|
||||
)}
|
||||
</aside>
|
||||
<section>
|
||||
<section className="document-workspace">
|
||||
{doc ? (
|
||||
<>
|
||||
<div className="editor-tools">
|
||||
<strong>{doc.title}</strong>
|
||||
<span />
|
||||
<button onClick={() => setInspectorOpen((value) => !value)}>
|
||||
{inspectorOpen ? "收起大纲" : "展开大纲"}
|
||||
</button>
|
||||
<button onClick={() => api.exportDocument(doc.path)}>
|
||||
下载
|
||||
</button>
|
||||
<button className="danger" onClick={del}>
|
||||
删除
|
||||
</button>
|
||||
<button onClick={save}>保存</button>
|
||||
{editing ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setBody(doc.body);
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button onClick={save}>保存</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button className="danger" onClick={del}>
|
||||
删除
|
||||
</button>
|
||||
<button onClick={() => setEditing(true)}>编辑</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
className="editor"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
{editing ? (
|
||||
<textarea
|
||||
className="editor"
|
||||
aria-label="Markdown 编辑器"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<div className="document-scroll">
|
||||
<header className="reader-heading">
|
||||
<h1>{doc.title}</h1>
|
||||
<p>
|
||||
{stats.chars.toLocaleString()} 字 · 约 {stats.minutes}{" "}
|
||||
分钟读完
|
||||
</p>
|
||||
{parsed.tags.length > 0 && (
|
||||
<div className="meta-tags">
|
||||
{parsed.tags.map((tag) => (
|
||||
<span key={tag}>{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<MarkdownDocument
|
||||
body={doc.body}
|
||||
knownTitles={data.documents.map((item) => item.title)}
|
||||
onWiki={openWiki}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="welcome">
|
||||
|
|
@ -421,6 +497,53 @@ function Knowledge({
|
|||
</div>
|
||||
)}
|
||||
</section>
|
||||
{inspectorOpen && (
|
||||
<aside className="document-inspector">
|
||||
<header>
|
||||
<strong>页面大纲</strong>
|
||||
<span>来源与证据</span>
|
||||
</header>
|
||||
{doc ? (
|
||||
<>
|
||||
<nav className="outline-list">
|
||||
{outline.length ? (
|
||||
outline.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
style={{ paddingLeft: 12 + (item.level - 1) * 10 }}
|
||||
onClick={() => jumpToHeading(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<p>当前页面没有标题层级。</p>
|
||||
)}
|
||||
</nav>
|
||||
<dl className="document-evidence">
|
||||
<div>
|
||||
<dt>知识根</dt>
|
||||
<dd>本机私人 Git</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>路径</dt>
|
||||
<dd>{doc.path}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>内容指纹</dt>
|
||||
<dd>{doc.sha256.slice(0, 16)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>状态</dt>
|
||||
<dd>可编辑 · 可回收</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</>
|
||||
) : (
|
||||
<p className="empty">选择页面后显示大纲与证据。</p>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
{newTitle !== null && (
|
||||
<div className="modal-backdrop" onMouseDown={() => setNewTitle(null)}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue