Compare commits

...

2 Commits

Author SHA1 Message Date
6b6761f7bb change Editor information 2025-11-28 20:05:32 +08:00
52c33a9940 change Editor 2025-11-28 20:04:15 +08:00
7 changed files with 178 additions and 365 deletions

View File

@ -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",

View File

@ -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);

View File

@ -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 (
<div className="playedu-main-body">
<div className={styles['chapter-box']}>
<div style={{ margin: 24 }}>
<BackBartment title={t('textbook.chapter.management')} />
</div>
<div className={styles['chapter-main-body']}>
<div className={styles['left-box']}>
<ChapterTree
@ -77,13 +77,13 @@ const ChapterManagementPage = () => {
</div>
<div className={styles['right-box']}>
{/*{selectedChapter ? (*/}
<EnhancedTextbookEditor
<TextbookEditor
chapterId={selectedChapter?.id || 22}
chapterTitle={selectedChapter?.name || '测试数据'}
initialContent="请编写内容"
onSave={onSave}
onContentChange={onContentChange}
></EnhancedTextbookEditor>
></TextbookEditor>
{/* : (
<div></div>
)}*/}

View File

@ -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<EditorProps> = ({
chapterId,
chapterTitle,
initialContent = '',
onSave,
onContentChange,
}) => {
const [isEditing, setIsEditing] = useState(false);
const { t } = useTranslation();
// 保留
/* const [wordCount, setWordCount] = useState(0);
const [lastSaved, setLastSaved] = useState<Date | null>(null);*/
// 初始化编辑器
const editor = useEditor({
extensions,
content:
initialContent ||
`
<h2>${chapterTitle}</h2>
<p>...</p>
`,
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 ||
`
<h2>${chapterTitle}</h2>
<p>...</p>
`
);
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 (
<div className="enhanced-textbook-editor empty-state">
<div className="empty-content">
<div className="empty-icon"></div>
<h3></h3>
<p></p>
</div>
</div>
);
}
if (!editor) {
return <div>...</div>;
}
return (
<div className="enhanced-textbook-editor">
{/* 编辑器头部 */}
<div className="editor-header">
<div className="header-left">
<h2 className="chapter-title">{chapterTitle}</h2>
<span className="chapter-id">: {chapterId}</span>
</div>
<div className="header-right">
{/*<div className="editor-stats">
<span className="word-count">: {wordCount}</span>
{isEditing && <span className="editing-indicator"> </span>}
<span className="last-saved">: {formatTime(lastSaved)}</span>
</div>
<button
className={`save-button ${isEditing ? 'has-changes' : ''}`}
onClick={handleManualSave}
disabled={!isEditing}
>
{isEditing ? '保存更改' : '已保存'}
</button>*/}
<Button style={{ marginRight: 15 }} onClick={handlePreview}>
<EyeFilled />
{t('textbook.resource.btnPreview')}
</Button>
<Button
type="primary"
style={{ marginRight: 15 }}
onClick={() => {
alert('知识图谱');
}}
disabled={true}
>
<ForkOutlined />
{t('textbook.resource.knowledge')}
</Button>
<Button type="primary" style={{ marginRight: 15 }} onClick={handleManualSave}>
<SaveFilled />
{t('textbook.resource.btnSave')}
</Button>
</div>
</div>
<div className="editor-main-box">
{/* 工具栏 */}
<div className="toolbar-container">
<EnhancedToolbar editor={editor} />
</div>
{/* 编辑器内容 */}
<div className="editor-container">
<EditorContent editor={editor} />
</div>
{/* 状态栏 */}
<div className="editor-status">
<div className="status-info">
<span> </span>
</div>
</div>
</div>
</div>
);
};
export default EnhancedTextbookEditor;

View File

@ -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;

View File

@ -0,0 +1,155 @@
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<EditorProps> = ({
chapterId,
chapterTitle,
initialContent = '',
onSave,
}) => {
const [isEditing, setIsEditing] = useState(false);
const { t } = useTranslation();
// editor 实例
const [editor, setEditor] = useState<IDomEditor | null>(null); // TS 语法
// 编辑器内容
const [html, setHtml] = useState('<p>hello</p>');
useEffect(() => {
if (editor && initialContent !== editor.getHtml()) {
editor.insertText(
// 添加内容
initialContent ||
`
<h2>${chapterTitle}</h2>
<p>...</p>
`
);
setIsEditing(false);
// 更新统计
// const text = editor.getText();
// setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length);
}
}, [initialContent, editor, chapterTitle]);
// 工具栏配置
const toolbarConfig: Partial<IToolbarConfig> = {}; // TS 语法
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
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 (
<div className="enhanced-textbook-editor empty-state">
<div className="empty-content">
<div className="empty-icon"></div>
<h3></h3>
<p></p>
</div>
</div>
);
}
return (
<div className="enhanced-textbook-editor">
{/* 编辑器头部 */}
<div className="editor-header">
<div className="header-left">
<h2 className="chapter-title">{chapterTitle}</h2>
<span className="chapter-id">: {chapterId}</span>
</div>
<div className="header-right">
<Button style={{ marginRight: 10 }} onClick={handlePreview}>
<EyeFilled />
{t('textbook.resource.btnPreview')}
</Button>
<Button
type="primary"
style={{ marginRight: 10 }}
onClick={() => {
alert('知识图谱');
}}
disabled={true}
>
<ForkOutlined />
{t('textbook.resource.knowledge')}
</Button>
<Button type="primary" style={{ marginRight: 5 }} onClick={handleManualSave}>
<SaveFilled />
{t('textbook.resource.btnSave')}
</Button>
</div>
</div>
<div className="editor-main-box">
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="default"
style={{ borderBottom: '1px solid #ccc' }}
/>
<Editor
defaultConfig={editorConfig}
value={html}
onCreated={setEditor}
onChange={(editor) => setHtml(editor.getHtml())}
mode="default"
style={{ height: '550px', overflowY: 'hidden' }}
/>
</div>
</div>
);
};
export default TextbookEditor;

View File

@ -118,8 +118,6 @@ export const CreateTextbook: React.FC<CourseCreateProps> = ({ 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<CourseCreateProps> = ({ editId, isEdit, op
setLoading(false);
});
} else {
console.log(thumb, 'thumb');
console.log('thumb 类型:', typeof thumb);
CreateTextbookApi(
values.title,
thumb,