diff --git a/app/backend/package.json b/app/backend/package.json index 73d07ba..d63cce6 100644 --- a/app/backend/package.json +++ b/app/backend/package.json @@ -14,12 +14,6 @@ "dependencies": { "@ant-design/icons": "5.x", "@reduxjs/toolkit": "^1.9.3", - "@tiptap/extension-color": "^3.11.0", - "@tiptap/extension-text-align": "^3.11.0", - "@tiptap/extension-text-style": "^3.11.0", - "@tiptap/extension-underline": "^3.11.0", - "@tiptap/react": "^3.11.0", - "@tiptap/starter-kit": "^3.11.0", "@uppy/aws-s3": "4.1.0", "@uppy/core": "4.2.0", "@uppy/dashboard": "4.1.0", @@ -29,6 +23,8 @@ "@uppy/progress-bar": "4.0.0", "@uppy/react": "4.0.2", "@uppy/status-bar": "4.0.3", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-react": "^1.0.6", "ahooks": "^3.7.6", "antd": "^5.12.2", "axios": "^1.3.4", diff --git a/app/backend/src/pages/textbook/chapter.module.less b/app/backend/src/pages/textbook/chapter.module.less index 074dacf..c5b5c49 100644 --- a/app/backend/src/pages/textbook/chapter.module.less +++ b/app/backend/src/pages/textbook/chapter.module.less @@ -1,3 +1,12 @@ +.chapter-box { + width: 100%; + height: auto; + min-height: calc(100vh - 172px); + float: left; + background-color: white; + box-sizing: border-box; + border-radius: 12px; +} .chapter-main-body { width: 100%; height: auto; @@ -7,6 +16,7 @@ border-radius: 12px; display: flex; flex-direction: row; + justify-content: space-between; overflow: hidden; .left-box { width: 300px; @@ -14,13 +24,13 @@ height: auto; min-height: calc(100vh - 172px); border-right: 2px solid #f6f6f6; - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25), 0 6px 20px rgba(0, 0, 0, 0.2); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.1), 0 6px 20px rgba(0, 0, 0, 0.2); box-sizing: border-box; padding: 24px 16px; background-color: white; } .right-box { - width: calc(100% - 354px); + width: calc(100% - 301px); float: left; height: auto; min-height: calc(100vh - 172px); diff --git a/app/backend/src/pages/textbook/chapter.tsx b/app/backend/src/pages/textbook/chapter.tsx index 0291e28..8732306 100644 --- a/app/backend/src/pages/textbook/chapter.tsx +++ b/app/backend/src/pages/textbook/chapter.tsx @@ -4,10 +4,8 @@ import React, { useEffect, useState } from 'react'; import { ChapterTree } from './compenents/chapterTree'; import { BackBartment } from '../../compenents'; import styles from './chapter.module.less'; -import { department } from '../../api/index'; -import EditorTextbookContent from './compenents/TextEditor/EditorToolbar'; -import EnhancedTextbookEditor from './compenents/TextEditor/EnhancedTextbookEditor'; import { GetChapterListApi } from '../../api/textbook'; +import TextbookEditor from './compenents/TextEditor/TextbookEditor'; export interface ChapterItemModel { created_at: string; @@ -62,8 +60,10 @@ const ChapterManagementPage = () => { }; return ( -
- +
+
+ +
{
{/*{selectedChapter ? (*/} - + > {/* : (
点击左侧目录选择
)}*/} diff --git a/app/backend/src/pages/textbook/compenents/TextEditor/EnhancedTextbookEditor.tsx b/app/backend/src/pages/textbook/compenents/TextEditor/EnhancedTextbookEditor.tsx deleted file mode 100644 index 214077c..0000000 --- a/app/backend/src/pages/textbook/compenents/TextEditor/EnhancedTextbookEditor.tsx +++ /dev/null @@ -1,216 +0,0 @@ -// components/EnhancedTextbookEditor.tsx -import React, { useEffect, useState, useCallback } from 'react'; -import { EditorContent, useEditor } from '@tiptap/react'; -import { TextStyleKit } from '@tiptap/extension-text-style'; -import StarterKit from '@tiptap/starter-kit'; -import EnhancedToolbar from './EditorToolbar'; -import { EditorProps } from '../../../../types/editor'; -import './EnhancedTextbookEditor.less'; -import TextAlign from '@tiptap/extension-text-align'; -import Highlight from '@tiptap/extension-highlight'; -import Color from '@tiptap/extension-color'; -import { Button } from 'antd'; -import { - EyeFilled, - EyeOutlined, - ForkOutlined, - SaveFilled, - UploadOutlined, -} from '@ant-design/icons'; -import { useTranslation } from 'react-i18next'; - -const extensions = [ - TextStyleKit, - StarterKit.configure({ - heading: { - levels: [1, 2, 3], - }, - }), - TextAlign.configure({ - types: ['heading', 'paragraph'], - alignments: ['left', 'center', 'right', 'justify'], - }), - Highlight.configure({ - multicolor: true, - }), - Color.configure({ - types: ['textStyle'], - }), -]; - -const EnhancedTextbookEditor: React.FC = ({ - chapterId, - chapterTitle, - initialContent = '', - onSave, - onContentChange, -}) => { - const [isEditing, setIsEditing] = useState(false); - const { t } = useTranslation(); - - // 保留 - /* const [wordCount, setWordCount] = useState(0); - const [lastSaved, setLastSaved] = useState(null);*/ - - // 初始化编辑器 - const editor = useEditor({ - extensions, - content: - initialContent || - ` -

${chapterTitle}

-

开始编写本章节的内容...

- `, - immediatelyRender: false, - onUpdate: ({ editor }) => { - const content = editor.getHTML(); - const text = editor.getText(); - - // 更新统计 - // setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length); - - // 通知父组件 - onContentChange(chapterId, content); - setIsEditing(true); - }, - onBlur: ({ editor }) => { - // 失去焦点时自动保存 - if (isEditing) { - const content = editor.getHTML(); - onSave(chapterId, content); - // setLastSaved(new Date()); - setIsEditing(false); - } - }, - editorProps: { - attributes: { - class: 'editor-content', - 'data-chapter-id': chapterId, - }, - }, - }); - - // 章节切换时更新内容 - useEffect(() => { - if (editor && initialContent !== editor.getHTML()) { - editor.commands.setContent( - initialContent || - ` -

${chapterTitle}

-

开始编写本章节的内容...

- ` - ); - setIsEditing(false); - - // 更新统计 - const text = editor.getText(); - // setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length); - } - }, [chapterId, initialContent, chapterTitle, editor]); - - // 手动保存 - const handleManualSave = useCallback(() => { - if (editor && isEditing) { - const content = editor.getHTML(); - - onSave(chapterId, content); - // setLastSaved(new Date()); - setIsEditing(false); - } - }, [editor, chapterId, isEditing, onSave]); - - // 格式化时间 - const formatTime = (date: Date | null): string => { - if (!date) return '未保存'; - return date.toLocaleTimeString('zh-CN', { - hour: '2-digit', - minute: '2-digit', - }); - }; - - // 预览 - const handlePreview = () => { - alert('preview'); - }; - - if (!chapterId) { - return ( -
-
-
-

请选择章节

-

从左侧目录中选择一个章节开始编辑

-
-
- ); - } - - if (!editor) { - return
编辑器加载中...
; - } - - return ( -
- {/* 编辑器头部 */} -
-
-

{chapterTitle}

- 所属章节: {chapterId} -
-
- {/*
- 字数: {wordCount} - {isEditing && ● 编辑中} - 最后保存: {formatTime(lastSaved)} -
- */} - - - -
-
- -
- {/* 工具栏 */} -
- -
- - {/* 编辑器内容 */} -
- -
- {/* 状态栏 */} -
-
- 章节编辑器 -
-
-
-
- ); -}; - -export default EnhancedTextbookEditor; diff --git a/app/backend/src/pages/textbook/compenents/TextEditor/EnhancedTextbookEditor.less b/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.less similarity index 57% rename from app/backend/src/pages/textbook/compenents/TextEditor/EnhancedTextbookEditor.less rename to app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.less index 93fd936..da0c07e 100644 --- a/app/backend/src/pages/textbook/compenents/TextEditor/EnhancedTextbookEditor.less +++ b/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.less @@ -78,42 +78,6 @@ font-weight: 500; } -.editing-indicator { - color: #28a745; - font-weight: 500; -} - -.save-button { - padding: 8px 16px; - border: 1px solid #007bff; - background: #007bff; - color: white; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - transition: all 0.2s; -} - -.save-button:hover:not(:disabled) { - background: #0056b3; - border-color: #0056b3; -} - -.save-button:disabled { - background: #6c757d; - border-color: #6c757d; - cursor: not-allowed; -} - -.save-button.has-changes { - background: #28a745; - border-color: #28a745; -} - -.save-button.has-changes:hover { - background: #218838; - border-color: #1e7e34; -} /*编辑区域*/ @@ -121,9 +85,8 @@ background: #8F8F8F; position: relative; margin: 20px; - height: 640px; - overflow-y: scroll; - overflow-x: hidden; + height: 590px; + overflow: hidden; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.1); border-radius: 4px; } @@ -134,11 +97,6 @@ background: #f8f9fa; border-bottom: 1px solid #e1e5e9; padding: 12px 16px; - position: sticky; - top: 0; - left: 0; - right: 0; - z-index: 100; } .enhanced-toolbar { @@ -204,92 +162,6 @@ background: white; } -/* 编辑器内容样式 */ -.editor-content { - outline: none; - padding: 20px; - min-height: 510px; - font-size: 14px; - line-height: 1.6; - color: #333; -} - -.editor-content h1, -.editor-content h2, -.editor-content h3, -.editor-content h4, -.editor-content h5, -.editor-content h6 { - margin: 1.5em 0 0.5em 0; - font-weight: 600; - line-height: 1.25; -} - -.editor-content h1 { - font-size: 2em; - border-bottom: 2px solid #007bff; - padding-bottom: 0.3em; -} - -.editor-content h2 { - font-size: 1.5em; -} - -.editor-content h3 { - font-size: 1.25em; -} - -.editor-content p { - margin: 1em 0; -} - -.editor-content ul, -.editor-content ol { - margin: 1em 0; - padding-left: 2em; -} - -.editor-content li { - margin: 0.5em 0; -} - -.editor-content blockquote { - border-left: 4px solid #007bff; - margin: 1.5em 0; - padding: 0.5em 1em; - background: #f8f9fa; - font-style: italic; -} - -.editor-content code { - background: #f1f3f4; - padding: 0.2em 0.4em; - border-radius: 3px; - font-family: 'Monaco', 'Menlo', monospace; - font-size: 0.9em; -} - -.editor-content pre { - background: #1a1a1a; - color: #f8f9fa; - padding: 1em; - border-radius: 6px; - overflow-x: auto; - margin: 1.5em 0; -} - -.editor-content pre code { - background: none; - padding: 0; - color: inherit; -} - -.editor-content hr { - border: none; - border-top: 2px solid #dee2e6; - margin: 2em 0; -} - /* 状态栏 */ .editor-status { padding: 8px 16px; diff --git a/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.tsx b/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.tsx new file mode 100644 index 0000000..ed45423 --- /dev/null +++ b/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.tsx @@ -0,0 +1,156 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { EditorProps } from '../../../../types/editor'; +import './TextbookEditor.less'; +import { Button } from 'antd'; +import { EyeFilled, ForkOutlined, SaveFilled } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; +import '@wangeditor/editor/dist/css/style.css'; +import { Editor, Toolbar } from '@wangeditor/editor-for-react'; +import { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'; +import { DomEditor } from '@wangeditor/editor'; + +const TextbookEditor: React.FC = ({ + chapterId, + chapterTitle, + initialContent = '', + onSave, +}) => { + const [isEditing, setIsEditing] = useState(false); + const { t } = useTranslation(); + + // editor 实例 + const [editor, setEditor] = useState(null); // TS 语法 + + // 编辑器内容 + const [html, setHtml] = useState('

hello

'); + + useEffect(() => { + if (editor && initialContent !== editor.getHtml()) { + editor.insertText( + // 添加内容 + initialContent || + ` +

${chapterTitle}

+

开始编写本章节的内容...

+ ` + ); + setIsEditing(false); + + // 更新统计 + const text = editor.getText(); + // setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length); + } + }, []); + + // 工具栏配置 + const toolbarConfig: Partial = {}; // TS 语法 + + // 编辑器配置 + const editorConfig: Partial = { + placeholder: '请输入内容...', + }; + + /*仅用于本地开发 查看按钮*/ + if (editor) { + const toolbar = DomEditor.getToolbar(editor); + const curToolbarConfig = toolbar?.getConfig(); + console.log(curToolbarConfig?.toolbarKeys); + } + + toolbarConfig.excludeKeys = ['fullScreen']; //移除不想要的fullScreen + + useEffect(() => { + return () => { + if (editor == null) return; + editor.destroy(); + setEditor(null); + }; + }, [chapterId, initialContent, chapterTitle, editor]); + + // 手动保存 + const handleManualSave = useCallback(() => { + if (editor && isEditing) { + onSave(chapterId, html); + // setLastSaved(new Date()); + setIsEditing(false); + } + }, [editor, chapterId, isEditing, onSave]); + + // 格式化时间 + const formatTime = (date: Date | null): string => { + if (!date) return '未保存'; + return date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + }); + }; + + // 预览 + const handlePreview = () => { + alert('preview'); + }; + + if (!chapterId) { + return ( +
+
+
+

请选择章节

+

从左侧目录中选择一个章节开始编辑

+
+
+ ); + } + + return ( +
+ {/* 编辑器头部 */} +
+
+

{chapterTitle}

+ 所属章节: {chapterId} +
+
+ + + +
+
+ +
+ + setHtml(editor.getHtml())} + mode="default" + style={{ height: '550px', overflowY: 'hidden' }} + /> +
+
+ ); +}; + +export default TextbookEditor; diff --git a/app/backend/src/pages/textbook/compenents/createTextbook.tsx b/app/backend/src/pages/textbook/compenents/createTextbook.tsx index 8c35b98..e53049c 100644 --- a/app/backend/src/pages/textbook/compenents/createTextbook.tsx +++ b/app/backend/src/pages/textbook/compenents/createTextbook.tsx @@ -118,8 +118,6 @@ export const CreateTextbook: React.FC = ({ editId, isEdit, op // 接口位置 setLoading(true); if (isEdit) { - console.log(thumb, 'thumb'); - console.log('thumb 类型:', typeof thumb); UpdateTextbookApi( editId, values.title, @@ -144,8 +142,6 @@ export const CreateTextbook: React.FC = ({ editId, isEdit, op setLoading(false); }); } else { - console.log(thumb, 'thumb'); - console.log('thumb 类型:', typeof thumb); CreateTextbookApi( values.title, thumb,