主编辑区改造准备

This commit is contained in:
lix-2026
2026-04-21 06:26:35 +08:00
parent 1e686bfa3c
commit 5d1c94eb9e
49 changed files with 6730 additions and 2565 deletions
@@ -27,6 +27,8 @@ import {
moveDocumentCommand,
renameDocumentCommand,
} from "@/lib/documents/tree-command-client";
import { EditorHost } from "@/components/editor/editor-host";
import type { EditorHostKind } from "@/components/editor/editor-host-config";
const BlockNoteEditor = dynamic(
() => import("@/components/editor/blocknote-editor").then((mod) => mod.BlockNoteEditor),
@@ -38,8 +40,6 @@ const BlockNoteEditor = dynamic(
},
);
// 说明:这些组件都不是“进入文档页首屏”所必需。
// 先拆成独立 chunk,避免 /documents/[id] 首次编译时把评论、历史、弹层、检查器等整串依赖一并拉进来。
const PageOptionsSidebar = dynamic(
() => import("@/components/editor/page-options-sidebar").then((mod) => mod.PageOptionsSidebar),
{ ssr: false },
@@ -89,6 +89,7 @@ export interface DocumentContentProps {
initialOptions: PageOptionsState;
initialStats: DocumentStats | null;
openTableId?: string | null;
editorHostKind?: EditorHostKind;
readOnly?: boolean;
disableDownload?: boolean;
disableCopy?: boolean;
@@ -124,6 +125,7 @@ export function DocumentContent({
initialOptions,
initialStats,
openTableId,
editorHostKind = "blocknote",
readOnly = false,
disableDownload = false,
disableCopy = false,
@@ -154,8 +156,12 @@ export function DocumentContent({
const [contentError, setContentError] = useState<string | null>(null);
const [contentReloadKey, setContentReloadKey] = useState(0);
const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);
const [isEditing, setIsEditing] = useState(() => Boolean((openTableId ?? "").trim()) && !readOnly);
const [keepEditorMounted, setKeepEditorMounted] = useState(() => Boolean((openTableId ?? "").trim()) && !readOnly);
const shouldUseExperimentalHost =
editorHostKind === "leptos_tiptap_inline" ||
editorHostKind === "leptos_tiptap_iframe_debug";
const shouldStartEditing = canEditDocument;
const [isEditing, setIsEditing] = useState(() => shouldStartEditing);
const [keepEditorMounted, setKeepEditorMounted] = useState(() => shouldStartEditing);
const contentLoadingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const editorUnmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pendingOpenTableRef = useRef<string | null>(null);
@@ -246,20 +252,17 @@ export function DocumentContent({
editorBridge.openTableFullScreen(tableId);
// 清理 URL 参数,避免刷新/回退时重复触发
if (typeof window !== "undefined") {
try {
const url = new URL(window.location.href);
url.searchParams.delete("openTableId");
window.history.replaceState({}, "", url.toString());
} catch {
// fallback:不影响主流程
router.replace(`/documents/${documentId}`);
}
}
}, [documentId, editorBridge, openTableId, router]);
useEffect(() => {
setPageTitle(title ?? "无标题");
}, [title]);
@@ -347,7 +350,6 @@ export function DocumentContent({
clearTimeout(contentLoadingTimerRef.current);
contentLoadingTimerRef.current = null;
}
// 避免"秒闪"的加载提示:只有当加载超过短阈值时才显示提示
contentLoadingTimerRef.current = setTimeout(() => {
if (!canceled) {
setShowContentLoadingIndicator(true);
@@ -358,9 +360,9 @@ export function DocumentContent({
const response = await fetch(
`/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,
{
method: "GET",
credentials: "include",
signal: controller.signal,
method: "GET",
credentials: "include",
signal: controller.signal,
},
);
if (!response.ok) {
@@ -586,7 +588,6 @@ export function DocumentContent({
window.alert(successMessage);
return;
} catch {
// ignore and fallback
}
}
window.prompt("复制失败,请手动复制内容", text);
@@ -890,12 +891,10 @@ export function DocumentContent({
<p className="mt-1 text-sm text-gray-500"></p>
) : options.protectEditing && isEditing ? (
<p className="mt-1 text-sm text-[#b91c1c]"></p>
) : !isEditing && canEditDocument ? (
<p className="mt-1 text-sm text-gray-500"></p>
) : null}
<p className="text-sm text-wolai-text-secondary">{formattedUpdatedAt}</p>
</div>
{canEditDocument && (
{canEditDocument && false && (
<div className="flex shrink-0 items-center gap-2">
{isEditing ? (
<Button type="button" variant="outline" size="sm" onClick={handleExitEditMode}>
@@ -938,22 +937,44 @@ export function DocumentContent({
<div className="relative">
{keepEditorMounted && (
<div className={cn(!isEditing && "pointer-events-none absolute inset-0 opacity-0")} aria-hidden={!isEditing}>
<BlockNoteEditor
documentId={documentId}
workspaceId={workspaceId}
initialContent={content}
initialRevision={contentRevision}
initialConflictDetectionKey={conflictDetectionKey}
pageOptions={options}
readOnly={readOnly}
onStatsChange={handleStatsChange}
onSnapshot={handleSnapshot}
onCloseToc={closeToc}
onPersistedMetaChange={({ revision, conflictDetectionKey: nextConflictDetectionKey }) => {
setContentRevision(revision);
setConflictDetectionKey(nextConflictDetectionKey);
}}
/>
{shouldUseExperimentalHost ? (
<EditorHost
documentId={documentId}
workspaceId={workspaceId}
initialContent={content}
title={title}
hostKind={editorHostKind}
initialRevision={contentRevision}
initialConflictDetectionKey={conflictDetectionKey}
pageOptions={options}
readOnly={readOnly}
onStatsChange={handleStatsChange}
onSnapshot={handleSnapshot}
onCloseToc={closeToc}
onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {
setContentRevision(meta.revision);
setConflictDetectionKey(meta.conflictDetectionKey);
}}
/>
) : (
<BlockNoteEditor
documentId={documentId}
workspaceId={workspaceId}
initialContent={content}
title={title}
initialRevision={contentRevision}
initialConflictDetectionKey={conflictDetectionKey}
pageOptions={options}
readOnly={readOnly}
onStatsChange={handleStatsChange}
onSnapshot={handleSnapshot}
onCloseToc={closeToc}
onPersistedMetaChange={(meta: { revision: number | null; conflictDetectionKey: string | null }) => {
setContentRevision(meta.revision);
setConflictDetectionKey(meta.conflictDetectionKey);
}}
/>
)}
</div>
)}
{!isEditing && (