知识是备忘录,备忘录不能只有字:手册、日志、导出表格都得能挂上去。
附件本来就落在 item 上(`ALLOWED_KINDS` 早含 knowledge),缺的是"新建时
还没有条目 id"这一段。
- 懒建草稿:`KnowledgeComposer` 第一次传附件(或往正文贴图)时先 `POST /knowledge`
存一条草稿拿到 id,`draftingRef` 去重防止贴图与上传同时打出两条草稿;
之后的保存走 `PATCH /knowledge/{id}`。没传过附件就取消,库里不留空条目。
- `RichTextEditor` 增 `ensureItemId`:没有 itemId 时先把草稿存下来再传图,
于是"新建时不能贴图"这条限制也没了;工具栏上传按钮按是否有落点显隐。
- 新建面板加了附件区:有草稿 id 就是正常的 `AttachmentPanel`(上传/下载/删),
没有就先给一个「添加附件」按钮,点一下建草稿。
- 安卓知识详情加附件区:列表(文件名 + 大小)、`OpenDocument` 上传任意文件、
点「打开」用浏览器看签名直链。编辑器原本只能插图,现在手册/日志也能传。
- 文档:API.md 说明附件三类事项共用同一套接口,以及懒建草稿的先后顺序;
ROADMAP 加 3.13。
验证:后端 pytest 94 passed、ruff 全绿;Web `tsc --noEmit` 通过、`vite build` 通过;
`knowledge-notebook.mjs` 15 项全绿(新增「新建面板里能上传附件」「详情抽屉里能看到
刚传的附件」「附件挂在知识上,详情回读得到」)。安卓端仍是 JDK 25 + Gradle 8.7
不兼容,本次未编译验证。
This commit is contained in:
@@ -3,7 +3,7 @@ import { useParams } from "react-router-dom";
|
||||
import { AppShell } from "../components/AppShell";
|
||||
import { AttachmentPanel } from "../components/AttachmentPanel";
|
||||
import { RichTextEditor } from "../components/RichTextEditor";
|
||||
import { Link2, Plus, Search, Sparkle } from "../components/icons";
|
||||
import { Link2, Paperclip, Plus, Search, Sparkle } from "../components/icons";
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
@@ -566,6 +566,16 @@ function KnowledgeComposer({
|
||||
const [newCat, setNewCat] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
/**
|
||||
* 附件与图片都要落在具体条目上,可新建时条目还不存在。
|
||||
* 解法是懒建草稿:第一次传附件/贴图时才 POST 一条草稿,之后一律 PATCH。
|
||||
* 没传过附件的人,取消后什么都不会留在库里。
|
||||
*/
|
||||
const [draftId, setDraftId] = useState<string | null>(null);
|
||||
const [draftVersion, setDraftVersion] = useState(0);
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const draftingRef = useRef<Promise<string | null> | null>(null);
|
||||
|
||||
const parentDepth = categories.find((c) => c.id === categoryId)?.depth ?? 0;
|
||||
const canAddCategory = parentDepth < 3 && Boolean(newCat.trim());
|
||||
@@ -581,17 +591,50 @@ function KnowledgeComposer({
|
||||
}
|
||||
}
|
||||
|
||||
/** 拿条目 id:已有就直接返回,没有就现存一条草稿;并发调用共用同一个请求。 */
|
||||
async function ensureDraft(): Promise<string | null> {
|
||||
if (draftId) return draftId;
|
||||
if (draftingRef.current) return draftingRef.current;
|
||||
const pending = (async () => {
|
||||
try {
|
||||
const doc = await api.post<Knowledge>(`/workspaces/${wsId}/knowledge`, {
|
||||
title: title.trim() || "未命名知识",
|
||||
body_md: body,
|
||||
doc_type: docType,
|
||||
tag_names: tags.split(/[\s,,]+/).filter(Boolean),
|
||||
category_id: categoryId || null,
|
||||
});
|
||||
setDraftId(doc.id);
|
||||
setDraftVersion(doc.version);
|
||||
setAttachments(doc.attachments ?? []);
|
||||
setTitle(doc.title);
|
||||
setNotice("已存成草稿,现在可以继续写、贴图、传附件");
|
||||
return doc.id;
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "保存草稿失败");
|
||||
return null;
|
||||
} finally {
|
||||
draftingRef.current = null;
|
||||
}
|
||||
})();
|
||||
draftingRef.current = pending;
|
||||
return pending;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!canSave) return;
|
||||
const itemId = await ensureDraft();
|
||||
if (!itemId) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const doc = await api.post<Knowledge>(`/workspaces/${wsId}/knowledge`, {
|
||||
const doc = await api.patch<Knowledge>(`/workspaces/${wsId}/knowledge/${itemId}`, {
|
||||
version: draftVersion,
|
||||
title: title.trim(),
|
||||
body_md: body,
|
||||
doc_type: docType,
|
||||
tag_names: tags.split(/[\s,,]+/).filter(Boolean),
|
||||
category_id: categoryId || null,
|
||||
...(categoryId ? { category_id: categoryId } : { clear_category: true }),
|
||||
});
|
||||
onCreated(doc);
|
||||
} catch (err) {
|
||||
@@ -607,7 +650,7 @@ function KnowledgeComposer({
|
||||
onClose={onClose}
|
||||
size="xl"
|
||||
title="记一条知识"
|
||||
subtitle="标题、正文、标签、分类一次填完;保存后可以继续贴图、传附件。"
|
||||
subtitle="标题、正文、标签、分类一次填完;附件和贴图会先把这条存成草稿。"
|
||||
footer={
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant="primary" disabled={!canSave} onClick={() => void save()}>
|
||||
@@ -617,7 +660,9 @@ function KnowledgeComposer({
|
||||
取消
|
||||
</Button>
|
||||
<span className="ml-auto text-2xs text-ink-400">
|
||||
先存成草稿,核对发布后才进团队检索
|
||||
{draftId
|
||||
? "已经是草稿了:直接关掉会留在「待核对」里"
|
||||
: "先存成草稿,核对发布后才进团队检索"}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@@ -717,12 +762,39 @@ function KnowledgeComposer({
|
||||
value={body}
|
||||
onChange={setBody}
|
||||
minHeight={320}
|
||||
ensureItemId={ensureDraft}
|
||||
onError={setError}
|
||||
placeholder={"写下要点、步骤、结论…\n\n标题、类型、分类、标签都在上面,正文就是这张备忘的主体。"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-line-200 bg-paper-50 px-3 py-2">
|
||||
{draftId ? (
|
||||
<AttachmentPanel
|
||||
wsId={wsId}
|
||||
itemId={draftId}
|
||||
attachments={attachments}
|
||||
onChange={setAttachments}
|
||||
onError={setError}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="inline-flex items-center gap-1.5 text-2xs font-medium text-ink-500">
|
||||
<Paperclip size={13} />
|
||||
附件
|
||||
</span>
|
||||
<Button variant="ghost" disabled={busy} onClick={() => void ensureDraft()}>
|
||||
添加附件
|
||||
</Button>
|
||||
<span className="text-2xs text-ink-400">
|
||||
附件要挂在条目上:点一下先把这条存成草稿,然后就能上传、下载
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{notice && <p className="text-2xs text-primary-700">{notice}</p>}
|
||||
{error && <InlineAlert>{error}</InlineAlert>}
|
||||
</div>
|
||||
</Sheet>
|
||||
|
||||
Reference in New Issue
Block a user