201 lines
5.7 KiB
TypeScript
201 lines
5.7 KiB
TypeScript
import { memo, useCallback, useEffect, useRef } from 'react';
|
||
import { useChatHistory } from '~/.client/persistence';
|
||
import { isValidContent } from '~/.client/utils/html-parse';
|
||
import { throttleWithTrailing } from '~/.client/utils/throttle';
|
||
import type { Section } from '~/types/actions';
|
||
import type { DocumentProperties, Editor } from '~/types/editor';
|
||
import { logger } from '~/utils/logger';
|
||
import { EditorComponent } from './EditorComponent';
|
||
|
||
export interface ScrollPosition {
|
||
top: number;
|
||
left: number;
|
||
}
|
||
|
||
export interface EditorUpdate {
|
||
content: string;
|
||
}
|
||
|
||
export type OnChangeCallback = (editor: Editor, pageName: string, html: string) => void;
|
||
export type OnSaveCallback = () => void;
|
||
export type OnLoadCallback = (editor: Editor) => void;
|
||
export type OnReadyCallback = (editor: Editor) => void;
|
||
|
||
interface Props {
|
||
documents?: Record<string, DocumentProperties>;
|
||
currentPage?: string;
|
||
currentSection?: Section;
|
||
editable?: boolean;
|
||
debounceChange?: number;
|
||
debounceScroll?: number;
|
||
onChange?: OnChangeCallback;
|
||
onReset?: () => void;
|
||
onSave?: OnSaveCallback;
|
||
onLoad?: OnLoadCallback;
|
||
onReady?: OnReadyCallback;
|
||
className?: string;
|
||
settings?: any;
|
||
}
|
||
|
||
export const EditorStudio = memo(
|
||
({ documents, currentPage, currentSection, onChange, onSave, onLoad, onReady }: Props) => {
|
||
const editorRef = useRef<Editor | null>(null);
|
||
|
||
const pendingSectionRef = useRef<Section | null>(null);
|
||
const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||
const { getLoadProject } = useChatHistory();
|
||
|
||
const updateComponents = useCallback((editor: Editor, section: Section) => {
|
||
if (!editor) {
|
||
logger.warn('编辑器实例不存在,无法更新组件');
|
||
return;
|
||
}
|
||
|
||
if (!section.domId) {
|
||
logger.warn('节点ID不存在,无法更新组件');
|
||
return;
|
||
}
|
||
|
||
const { domId, action, content, sort, rootDomId } = section;
|
||
// 验证 content 是否有效
|
||
if (action !== 'remove' && !isValidContent(content)) {
|
||
logger.warn('内容无效,无法更新组件', JSON.stringify({ action, domId }));
|
||
return;
|
||
}
|
||
if (rootDomId) {
|
||
editor.scrollToElement(`#${rootDomId}`);
|
||
}
|
||
const id = `#${domId}`;
|
||
try {
|
||
switch (action) {
|
||
case 'add':
|
||
editor.appendContent(id, content, sort);
|
||
break;
|
||
case 'update': {
|
||
editor.updateContent(id, content, sort);
|
||
break;
|
||
}
|
||
case 'remove': {
|
||
editor.deleteContent(id);
|
||
break;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
logger.error('执行组件操作时出错', JSON.stringify({ error, action, domId }));
|
||
}
|
||
}, []);
|
||
|
||
const throttledSetComponents = useCallback(updateComponents, []);
|
||
|
||
const lastSectionRef = useRef<Section | undefined>(undefined);
|
||
const throttledSetComponentsRef = useRef(throttleWithTrailing(throttledSetComponents, 150));
|
||
|
||
function flushPendingUpdate(editor: Editor) {
|
||
const lastSection = lastSectionRef.current;
|
||
if (lastSection && lastSection.content) {
|
||
updateComponents(editor, lastSection);
|
||
lastSectionRef.current = undefined;
|
||
}
|
||
}
|
||
|
||
function setEditorDocument(editor: Editor, section?: Section) {
|
||
if (!section) {
|
||
return;
|
||
}
|
||
/*
|
||
* 使用节流函数来更新组件内容
|
||
* 这样可以避免频繁的更新导致编辑器卡顿
|
||
*/
|
||
if (section) {
|
||
lastSectionRef.current = section;
|
||
throttledSetComponentsRef.current(editor, section);
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
const editor = editorRef.current;
|
||
|
||
if (!editor) {
|
||
return;
|
||
}
|
||
|
||
if (!currentSection) {
|
||
return;
|
||
}
|
||
|
||
if (!currentSection.pageName) {
|
||
logger.warn('page should not be empty');
|
||
}
|
||
|
||
// section变更时,先执行上一个section的待处理更新
|
||
flushPendingUpdate(editor);
|
||
|
||
// 保存最新的页面属性,确保在节流期间如果有新的更新进来,会使用最新的数据
|
||
pendingSectionRef.current = currentSection;
|
||
setEditorDocument(editor, currentSection);
|
||
}, [currentSection]);
|
||
|
||
// 确保在组件卸载前应用最后一次更新
|
||
useEffect(() => {
|
||
return () => {
|
||
const editor = editorRef.current;
|
||
const pendingSection = pendingSectionRef.current;
|
||
|
||
// 清除保存定时器
|
||
if (saveTimerRef.current) {
|
||
clearTimeout(saveTimerRef.current);
|
||
saveTimerRef.current = null;
|
||
}
|
||
|
||
if (editor && pendingSection && pendingSection) {
|
||
// 直接应用最后的更新,不通过节流
|
||
updateComponents(editor, pendingSection);
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
const handleEditorReady = useCallback(
|
||
async (editor: Editor) => {
|
||
editorRef.current = editor ?? null;
|
||
if (onReady) {
|
||
onReady(editor);
|
||
}
|
||
},
|
||
[onSave],
|
||
);
|
||
|
||
const handleAutoSave = useCallback(async () => {
|
||
const editor = editorRef.current;
|
||
if (!editor) {
|
||
return;
|
||
}
|
||
if (onSave) {
|
||
onSave();
|
||
}
|
||
}, []);
|
||
|
||
const handleContentChange = useCallback((pageName: string, html: string) => {
|
||
if (editorRef.current && onChange) {
|
||
onChange(editorRef.current, pageName, html);
|
||
}
|
||
}, []);
|
||
|
||
const handleLoad = useCallback(async () => {
|
||
if (editorRef.current && onLoad) {
|
||
onLoad(editorRef.current);
|
||
}
|
||
}, [getLoadProject]);
|
||
|
||
return (
|
||
<EditorComponent
|
||
currentPage={currentPage}
|
||
documents={documents}
|
||
onLoad={handleLoad}
|
||
onReady={handleEditorReady}
|
||
onSave={handleAutoSave}
|
||
onContentChange={handleContentChange}
|
||
/>
|
||
);
|
||
},
|
||
);
|