0.1.11 ai修复与全屏
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useCallback, useEffect, useMemo, useState, type ChangeEvent, type KeyboardEvent } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
|
||||
import type { DocumentStats, PageOptionsState } from "@/types/page-options";
|
||||
import { PageOptionsSidebar } from "@/components/editor/page-options-sidebar";
|
||||
import { PageBacklinksPanel } from "@/components/editor/page-backlinks-panel";
|
||||
@@ -12,6 +12,7 @@ import { DocumentHistoryDrawer } from "@/components/editor/document-history-draw
|
||||
import type { DocumentSnapshot } from "@/types/document";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import { ImagePickerProvider } from "@/components/media/image-picker-context";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const BlockNoteEditor = dynamic(
|
||||
() => import("@/components/editor/blocknote-editor").then((mod) => mod.BlockNoteEditor),
|
||||
@@ -31,6 +32,7 @@ export interface DocumentContentProps {
|
||||
initialContent: unknown;
|
||||
initialOptions: PageOptionsState;
|
||||
initialStats: DocumentStats | null;
|
||||
openTableId?: string | null;
|
||||
}
|
||||
|
||||
const defaultOptions: PageOptionsState = {
|
||||
@@ -52,14 +54,45 @@ export function DocumentContent({
|
||||
initialContent,
|
||||
initialOptions,
|
||||
initialStats,
|
||||
openTableId,
|
||||
}: DocumentContentProps) {
|
||||
const [options, setOptions] = useState<PageOptionsState>(initialOptions ?? defaultOptions);
|
||||
const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);
|
||||
const [history, setHistory] = useState<DocumentSnapshot[]>([]);
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const editorBridge = useEditorBridgeStore((state) => state.bridge);
|
||||
const router = useRouter();
|
||||
const showInspector = usePageLayoutStore((state) => state.showInspector);
|
||||
const [pageTitle, setPageTitle] = useState(title ?? "无标题");
|
||||
const [content, setContent] = useState<unknown>(initialContent);
|
||||
const [contentLoading, setContentLoading] = useState(() => initialContent == null);
|
||||
const [contentError, setContentError] = useState<string | null>(null);
|
||||
const [contentReloadKey, setContentReloadKey] = useState(0);
|
||||
const [showContentLoadingIndicator, setShowContentLoadingIndicator] = useState(false);
|
||||
const contentLoadingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pendingOpenTableRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const tableId = (openTableId ?? "").trim();
|
||||
if (!tableId) return;
|
||||
if (!editorBridge?.openTableFullScreen) return;
|
||||
if (pendingOpenTableRef.current === tableId) return;
|
||||
pendingOpenTableRef.current = tableId;
|
||||
|
||||
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]);
|
||||
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
useEffect(() => {
|
||||
@@ -75,6 +108,72 @@ export function DocumentContent({
|
||||
}, [initialStats]);
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
const controller = new AbortController();
|
||||
|
||||
const load = async () => {
|
||||
setContentError(null);
|
||||
setContentLoading(initialContent == null);
|
||||
setContent(initialContent);
|
||||
setShowContentLoadingIndicator(false);
|
||||
|
||||
if (initialContent != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentLoadingTimerRef.current) {
|
||||
clearTimeout(contentLoadingTimerRef.current);
|
||||
contentLoadingTimerRef.current = null;
|
||||
}
|
||||
// 避免“秒闪”的加载提示:只有当加载超过短阈值时才显示提示
|
||||
contentLoadingTimerRef.current = setTimeout(() => {
|
||||
if (!canceled) {
|
||||
setShowContentLoadingIndicator(true);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/documents/content?documentId=${encodeURIComponent(documentId)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
throw new Error(payload?.error ?? "加载页面内容失败");
|
||||
}
|
||||
const payload = (await response.json()) as { content?: unknown };
|
||||
if (canceled) return;
|
||||
setContent(payload.content ?? null);
|
||||
} catch (error) {
|
||||
if (canceled) return;
|
||||
if ((error as { name?: string })?.name === "AbortError") return;
|
||||
setContentError(error instanceof Error ? error.message : "加载页面内容失败");
|
||||
} finally {
|
||||
if (!canceled) {
|
||||
setContentLoading(false);
|
||||
setShowContentLoadingIndicator(false);
|
||||
}
|
||||
if (contentLoadingTimerRef.current) {
|
||||
clearTimeout(contentLoadingTimerRef.current);
|
||||
contentLoadingTimerRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void load();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
controller.abort();
|
||||
if (contentLoadingTimerRef.current) {
|
||||
clearTimeout(contentLoadingTimerRef.current);
|
||||
contentLoadingTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [documentId, initialContent, contentReloadKey]);
|
||||
|
||||
const persistTitle = useCallback(
|
||||
async (nextTitle: string) => {
|
||||
const payload = nextTitle.trim() || "无标题";
|
||||
@@ -226,14 +325,39 @@ export function DocumentContent({
|
||||
<p className="text-sm text-gray-400">最近更新:{formattedUpdatedAt}</p>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-12 py-6">
|
||||
<BlockNoteEditor
|
||||
documentId={documentId}
|
||||
workspaceId={workspaceId}
|
||||
initialContent={initialContent}
|
||||
pageOptions={options}
|
||||
onStatsChange={handleStatsChange}
|
||||
onSnapshot={handleSnapshot}
|
||||
/>
|
||||
{contentLoading ? (
|
||||
showContentLoadingIndicator ? (
|
||||
<div className="flex h-64 items-center justify-center text-sm text-gray-400">
|
||||
页面内容加载中...
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-64" />
|
||||
)
|
||||
) : contentError ? (
|
||||
<div className="flex h-64 flex-col items-center justify-center gap-2 text-sm text-red-600">
|
||||
<div>{contentError}</div>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-red-200 bg-red-50 px-3 py-1 text-sm text-red-700 hover:bg-red-100"
|
||||
onClick={() => {
|
||||
setContentError(null);
|
||||
setContentLoading(true);
|
||||
setContentReloadKey((prev) => prev + 1);
|
||||
}}
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<BlockNoteEditor
|
||||
documentId={documentId}
|
||||
workspaceId={workspaceId}
|
||||
initialContent={content}
|
||||
pageOptions={options}
|
||||
onStatsChange={handleStatsChange}
|
||||
onSnapshot={handleSnapshot}
|
||||
/>
|
||||
)}
|
||||
<PageBacklinksPanel className="mt-10" workspaceId={workspaceId} documentId={documentId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user