feat(knowledge): restore complete human rendering layer

This commit is contained in:
冰朔 2026-09-03 16:25:43 +08:00
commit 3466b507c6
15 changed files with 665 additions and 23 deletions

View file

@ -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)}>