From 1d1fa949837a5cff3c9d8bf17f3b72a040b7e325 Mon Sep 17 00:00:00 2001 From: penpenwang Date: Sun, 30 Nov 2025 15:48:57 +0800 Subject: [PATCH] =?UTF-8?q?add=E3=80=81del=E3=80=81edit=E3=80=81drag=20cha?= =?UTF-8?q?ptersItem,add=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/backend/src/api/textbook.ts | 65 ++-- app/backend/src/i18n/cn.json | 8 +- .../pages/department/compenents/create.tsx | 2 +- app/backend/src/pages/department/index.tsx | 2 +- app/backend/src/pages/textbook/chapter.tsx | 126 ++++++-- .../compenents/TextEditor/TextbookEditor.less | 2 +- .../compenents/TextEditor/TextbookEditor.tsx | 277 ++++++++++++------ .../textbook/compenents/chapterModal.tsx | 261 +++++++++++------ .../pages/textbook/compenents/chapterTree.tsx | 256 ++++------------ app/backend/src/types/editor.ts | 5 +- 10 files changed, 571 insertions(+), 433 deletions(-) diff --git a/app/backend/src/api/textbook.ts b/app/backend/src/api/textbook.ts index d2cd50e..6aa9105 100644 --- a/app/backend/src/api/textbook.ts +++ b/app/backend/src/api/textbook.ts @@ -146,34 +146,6 @@ export function UpdateDetailApi( }); } -// 获取分类列表 -export function getSoftwareClassApi() { - return client.get(`/backend/v1/softwareClass`, {}); -} -// 删除分类 -export function deleteSoftwareClassApi(idList: any[]) { - return client.destroy(`/backend/v1/softwareClass?idList=${idList}`); -} - -// 编辑分类 -export function updateSortClassApi(id: number, className: any, parentId: any, sort: any) { - return client.put(`/backend/v1/softwareClass`, { - id, - className, - parentId, - sort, - }); -} - -// 获取绑定的学校信息 -export function getAssignedSchoolApi(softId: number | string) { - return client.get(`/backend/v1/softwareInfo/getAllocateInfo`, { softId }); -} -// 进行学校分配 -export function assignedSchoolApi(softId: number, tenantIds: string) { - return client.post(`/backend/v1/softwareInfo/allocateSoft`, { softId, tenantIds }); -} - /* * Chapter相关信息 * */ @@ -181,6 +153,13 @@ export function assignedSchoolApi(softId: number, tenantIds: string) { export function GetChapterListApi(params: any) { return client.get('/backend/v1/jc/chapter/index', params); } +export function GetChapterTreeApi(params: any) { + return client.get(`/backend/v1/jc/chapter/tree`, params); +} +export function GetChapterDetailApi(id: any, bookId: number) { + return client.get(`/backend/v1/jc/chapter/${id}`, { bookId }); +} + // 包含嵌套的 export function DropDiffClassApi(id: number, parent_id: number, ids: number[], book_id: number) { return client.put(`/backend/v1/jc/chapter/update/parent`, { @@ -198,12 +177,12 @@ export function DropSameClassApi(ids: number[], book_id: number) { }); } -export function checkDestroy(id: number) { - return client.get(`/backend/v1/department/${id}/destroy`, {}); +export function GetPreDestroyChapterApi(id: number, bookId: number) { + return client.get(`/backend/v1/jc/chapter/${id}/destroy`, { bookId }); } -export function DestroyChapterApi(id: number) { - return client.destroy(`/backend/v1/department/${id}`); +export function DestroyChapterApi(id: number, bookId: number) { + return client.destroy(`/backend/v1/jc/chapter/${id}?bookId=${bookId}`); } export function CreateChapterApi(name: string, parentId: number, sort: number, bookId: number) { @@ -221,7 +200,7 @@ export function EditChapterApi( sort: number, bookId: number ) { - return client.post(`/backend/v1/jc/chapter/${id}`, { + return client.put(`/backend/v1/jc/chapter/${id}`, { name, parentId, sort, @@ -229,6 +208,26 @@ export function EditChapterApi( }); } +/*chapterContent*/ +export function SaveChapterContentApi( + chapterId: number, + content: string, + resourceIds: any[], + knowledgeIds: any[], + highlight: string +) { + return client.post(`/backend/v1/jc/chapter-content/${chapterId}`, { + content, + resourceIds, + knowledgeIds, + highlight, + }); +} + +export function GetChapterContentApi(chapterId: number, bookId: number) { + return client.get(`/backend/v1/jc/chapter-content/${chapterId}`, { bookId }); +} + /* * resource List * */ diff --git a/app/backend/src/i18n/cn.json b/app/backend/src/i18n/cn.json index e57877e..823521a 100644 --- a/app/backend/src/i18n/cn.json +++ b/app/backend/src/i18n/cn.json @@ -2045,7 +2045,12 @@ "parent": "所属章节", "parentTip": "请选择所属章节", "tips": "注意:章节目录不可超过三级!", - "tips3": "注意:章节目录超过三级,请调整!" + "tips3": "注意:章节目录超过三级,请调整!", + "delText": "即将删除此章节目录及内容,且无法恢复,确认删除?", + "unbindText1": "此章节下包含", + "unbindText2": "个子章节", + "unbindText3": "请先删除子章节后再删除此项。" + }, "resource": { "pageTitle": "资源管理", @@ -2069,6 +2074,7 @@ "uploadTips": "点击或拖拽文件到此处上传", "uploadTips2": "支持视频、图片、文档、音频等常见格式", "btnSave": "保存内容", + "btnEdit": "编辑内容", "btnPreview": "预览", "knowledge": "知识图谱", "typeList": { diff --git a/app/backend/src/pages/department/compenents/create.tsx b/app/backend/src/pages/department/compenents/create.tsx index 4f4557a..4c23935 100644 --- a/app/backend/src/pages/department/compenents/create.tsx +++ b/app/backend/src/pages/department/compenents/create.tsx @@ -68,7 +68,7 @@ export const DepartmentCreate: React.FC = ({ open, onCancel }) => const checkArr = (departments: any[], id: number) => { const arr = []; - for (let i = 0; i < departments[id].length; i++) { + for (let i = 0; i < departments[id]?.length; i++) { if (!departments[departments[id][i].id]) { arr.push({ label: departments[id][i].name, diff --git a/app/backend/src/pages/department/index.tsx b/app/backend/src/pages/department/index.tsx index d7d2b9a..1a161da 100644 --- a/app/backend/src/pages/department/index.tsx +++ b/app/backend/src/pages/department/index.tsx @@ -88,7 +88,7 @@ const DepartmentPage = () => { const checkArr = (departments: DepartmentsBoxModel, id: number) => { const arr = []; - for (let i = 0; i < departments[id].length; i++) { + for (let i = 0; i < departments[id]?.length; i++) { if (!departments[departments[id][i].id]) { arr.push({ title: ( diff --git a/app/backend/src/pages/textbook/chapter.tsx b/app/backend/src/pages/textbook/chapter.tsx index 8732306..2e926f4 100644 --- a/app/backend/src/pages/textbook/chapter.tsx +++ b/app/backend/src/pages/textbook/chapter.tsx @@ -4,18 +4,38 @@ import React, { useEffect, useState } from 'react'; import { ChapterTree } from './compenents/chapterTree'; import { BackBartment } from '../../compenents'; import styles from './chapter.module.less'; -import { GetChapterListApi } from '../../api/textbook'; +import { + GetChapterContentApi, + GetChapterDetailApi, + GetChapterListApi, + GetChapterTreeApi, + SaveChapterContentApi, +} from '../../api/textbook'; import TextbookEditor from './compenents/TextEditor/TextbookEditor'; +import { Empty } from 'antd'; export interface ChapterItemModel { - created_at: string; - id: number; + bookId: number; + chapterCode: string; + createTime: string; + creator: string; + level: null | number; name: string; - from_scene: number; - parent_chain: string; - parent_id: number; sort: number; - updated_at: string; + id: number; + tenantId: number; + updateTime: string; + updater: string; + parentId: number; +} +export interface ChapterTreeItem { + bookId: number; + chapterCode: string; + name: string; + sort: number; + id: number; + parentId: number; + children: ChapterTreeItem[]; } export interface ChaptersBoxModel { @@ -29,35 +49,84 @@ export interface Option { children?: Option[]; } +export interface ChapterContent { + title?: any; + level?: number; +} + const ChapterManagementPage = () => { const { t } = useTranslation(); const params = useParams(); const [selectedChapter, setSelectedChapter] = useState(null); + const [contentChapterName, setContentChapterName] = useState(null); const [searchParams] = useSearchParams(); const title = searchParams.get('title'); const { bookId } = params; const [loading, setLoading] = useState(false); - const [treeData, setTreeData] = useState([]); + const [isContentLoading, setIsContentLoading] = useState(false); + const [chapterListData, setChapterListData] = useState([]); const [selectedChapterId, setSelectedChapterId] = useState(); + const [parentTitle, setParentTitle] = useState(); useEffect(() => { getChapterData(); }, [bookId]); + useEffect(() => { + getChapterDetail(); + getChapterParentDetail(); + }, [selectedChapter, bookId]); + const getChapterData = () => { + setLoading(true); GetChapterListApi({ bookId: bookId }).then((res: any) => { const resData: ChaptersBoxModel = res.data.chapters; - setTreeData(resData); + setChapterListData(resData); setLoading(false); }); }; - const onSave = () => { - console.log(selectedChapterId); + const refreshData = () => { + getChapterData(); + getChapterDetail(); + getChapterParentDetail(); + }; + + const getChapterParentDetail = () => { + if (selectedChapter) { + if (selectedChapter?.parentId === 0) { + setParentTitle('根目录'); + } else { + GetChapterDetailApi(selectedChapter?.parentId, Number(bookId)).then((res: any) => { + const resData: ChapterItemModel = res.data; + setParentTitle(resData.name); + }); + } + } + }; + + const getChapterDetail = () => { + if (selectedChapter) { + GetChapterDetailApi(selectedChapter?.id, Number(bookId)).then((res: any) => { + const resData: ChapterItemModel = res.data; + setContentChapterName(resData.name); + }); + } + }; + + const onSave = (data: string) => { + console.log(data, 'string'); + SaveChapterContentApi(selectedChapter?.id, data, [], [], '').then((res: any) => { + console.log(res, 'res'); + }); }; const onContentChange = () => { console.log(selectedChapterId); }; + const handleSelectItem = (data: any) => { + setSelectedChapter(data); + setSelectedChapterId(data?.id); + }; return (
@@ -68,25 +137,34 @@ const ChapterManagementPage = () => {
- {/*{selectedChapter ? (*/} - - {/* : ( -
点击左侧目录选择
- )}*/} + {selectedChapter ? ( + + ) : ( +
+
+
+

请选择章节

+

点击左侧目录,选择一个章节进行查看

+
+
+ )}
diff --git a/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.less b/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.less index da0c07e..380b432 100644 --- a/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.less +++ b/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.less @@ -82,7 +82,7 @@ /*编辑区域*/ .editor-main-box{ - background: #8F8F8F; + background: #ffffff; position: relative; margin: 20px; height: 590px; diff --git a/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.tsx b/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.tsx index c3aa046..3f5cd56 100644 --- a/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.tsx +++ b/app/backend/src/pages/textbook/compenents/TextEditor/TextbookEditor.tsx @@ -1,48 +1,53 @@ 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 { Button, Drawer, Empty, Modal, Spin } from 'antd'; +import { EditFilled, 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'; +import { GetChapterContentApi } from '../../../../api/textbook'; const TextbookEditor: React.FC = ({ chapterId, + parentTitle, chapterTitle, - initialContent = '', onSave, + bookId, }) => { - const [isEditing, setIsEditing] = useState(false); + const [isEditing, setIsEditing] = useState(false); const { t } = useTranslation(); // editor 实例 - const [editor, setEditor] = useState(null); // TS 语法 - + const [editor, setEditor] = useState(null); // 编辑器内容 - const [html, setHtml] = useState('

hello

'); + const [html, setHtml] = useState(); + const [loading, setLoading] = useState(true); + const [isPreviewModalShow, setIsPreviewModalShow] = useState(false); + const toolbarConfig: Partial = {}; + + toolbarConfig.excludeKeys = ['fullScreen']; //移除不想要的fullScreen useEffect(() => { - if (editor && initialContent !== editor.getHtml()) { - editor.insertText( - // 添加内容 - initialContent || - ` -

${chapterTitle}

-

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

- ` - ); - setIsEditing(false); + setLoading(true); + const timer = setTimeout(() => { + getContent(); + }, 2000); - // 更新统计 - // const text = editor.getText(); - // setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length); - } - }, [initialContent, editor, chapterTitle]); + return () => clearTimeout(timer); + }, [chapterTitle, chapterId]); - // 工具栏配置 - const toolbarConfig: Partial = {}; // TS 语法 + const getContent = () => { + GetChapterContentApi(Number(chapterId), bookId).then((res: any) => { + setHtml(res?.data?.content || null); + setLoading(false); + }); + }; + // 更新统计 + // const text = editor.getText(); + // setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length); + // } // 编辑器配置 const editorConfig: Partial = { @@ -56,24 +61,22 @@ const TextbookEditor: React.FC = ({ console.log(curToolbarConfig?.toolbarKeys); } - toolbarConfig.excludeKeys = ['fullScreen']; //移除不想要的fullScreen - useEffect(() => { return () => { if (editor == null) return; editor.destroy(); setEditor(null); + window.localStorage.removeItem('html'); }; - }, [chapterId, initialContent, chapterTitle, editor]); + }, [chapterId, chapterTitle, editor]); // 手动保存 const handleManualSave = useCallback(() => { - if (editor && isEditing) { - onSave(chapterId, html); - // setLastSaved(new Date()); + if (editor) { + onSave(html); setIsEditing(false); } - }, [editor, chapterId, isEditing, onSave]); + }, [editor, chapterId, isEditing, onSave, html]); // 格式化时间 const formatTime = (date: Date | null): string => { @@ -86,69 +89,167 @@ const TextbookEditor: React.FC = ({ // 预览 const handlePreview = () => { - alert('preview'); + setIsPreviewModalShow(true); }; - if (!chapterId) { - return ( -
-
-
-

请选择章节

-

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

-
-
- ); - } + // 在编辑器创建完成后关闭 loading + const handleEditorCreated = (editorInstance: any) => { + setEditor(editorInstance); + }; return ( -
- {/* 编辑器头部 */} -
-
-

{chapterTitle}

- 所属章节: {chapterId} -
-
- - - -
-
+ <> +
+ {loading ? ( +
+ +
+ ) : ( + <> +
+
+

{chapterTitle}

+ 所属章节: {parentTitle} +
+
+ + -
- - setHtml(editor.getHtml())} - mode="default" - style={{ height: '550px', overflowY: 'hidden' }} - /> + {isEditing ? ( + + ) : ( + + )} +
+
+
+ {isEditing ? ( + <> + + + handleEditorCreated(editor)} + onChange={(editor) => { + const currentHtml = editor.getHtml(); + setHtml(currentHtml); + window.localStorage.setItem('html', currentHtml); + }} + mode="default" + style={{ + height: '550px', + paddingBottom: 15, + boxSizing: 'border-box', + overflowY: 'hidden', + }} + /> + + ) : ( +
+ {html && html !== '


' && html !== '

' ? ( + // @ts-ignore +
+ ) : ( +
+
+
+ +
+
暂无内容
+
+ 请在编辑器中添加内容 +
+
+
+ )} +
+ )} +
+ + )}
-
+ setIsPreviewModalShow(false)} + width={1000} + open={isPreviewModalShow} + footer={null} + > +
+ + ); }; diff --git a/app/backend/src/pages/textbook/compenents/chapterModal.tsx b/app/backend/src/pages/textbook/compenents/chapterModal.tsx index bceeee6..1137a09 100644 --- a/app/backend/src/pages/textbook/compenents/chapterModal.tsx +++ b/app/backend/src/pages/textbook/compenents/chapterModal.tsx @@ -1,44 +1,20 @@ -import { Modal, Form, Input, Select, message, Spin, Cascader } from 'antd'; +import { Modal, Form, Input, message, Spin, Cascader } from 'antd'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - addSortClassApi, - getSoftwareClassApi, - updateSortClassApi, -} from '../../virtual/api/virtual'; -import { CreateChapterApi, EditChapterApi } from '../../../api/textbook'; +import { CreateChapterApi, EditChapterApi, GetChapterListApi } from '../../../api/textbook'; interface Option { value: string | number; label: string; children?: Option[]; } -interface ChapterItem { - id: string | number; - name: string; - level: number; - parent_chain: string; - parentId: string; - sort: number; - children?: ChapterItem[]; -} - -interface ChapterItemModel { - created_at: string; - id: number; - name: string; - from_scene: number; - parent_chain: string; - parentId: number; - sort: number; - updated_at: string; -} interface ChapterModalProps { visible: boolean; bookId: number; isEdit: boolean; editData?: any; + parentData?: any; onCancel: () => void; confirmLoading?: boolean; onSuccess: () => void; @@ -49,6 +25,7 @@ export const ChapterModal: React.FC = ({ bookId, isEdit, editData, + parentData, onCancel, confirmLoading = false, onSuccess, @@ -56,32 +33,169 @@ export const ChapterModal: React.FC = ({ const [form] = Form.useForm(); const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [init, setInit] = useState(true); const [chapters, setChapters] = useState([]); const [parentId, setParentId] = useState(0); const [editingId, setEditingId] = useState(0); // 存储正在编辑的ID - useEffect(() => { - setInit(true); - if (isEdit && editData) { - form.setFieldsValue(editData); - } else { - form.resetFields(); - let defaultLevel = 1; - form.setFieldsValue({ - level: defaultLevel, - ...editData, - }); + + // 根据 parentData 生成固定的章节目录选项 + const generateFixedChapterOptions = (parentData: any): Option[] => { + if (!parentData) { + return [ + { + label: t('commen.levelCate'), + value: 0, + }, + ]; } - setInit(false); - getParams(); - }, [visible, isEdit, editData, form]); + const fixedOptions: Option[] = [ + { + label: parentData.name, + value: parentData.id, + }, + ]; + + fixedOptions.unshift({ + label: t('commen.levelCate'), + value: 0, + }); + return fixedOptions; + }; + + useEffect(() => { + setLoading(true); + form.resetFields(); + if (parentData && parentData.id) { + setParentId(parentData.id); + } + // 如果是编辑模式,设置编辑ID + if (isEdit && editData) { + setEditingId(editData.id); + } + getChapterParams(); + }, [parentData, isEdit, editData]); + + const checkArr = ( + categories: any[], + id: number, + currentLevel: number = 1, + maxLevel: number = 2, + editingId?: number // 添加编辑ID参数 + ) => { + const arr: { label: any; value: any; children?: Option[] }[] = []; + + if (currentLevel > maxLevel) { + return arr; + } + + for (let i = 0; i < categories[id].length; i++) { + const currentItem = categories[id][i]; + const currentItemId = currentItem.id; + + // 排除编辑数据本身 + if (editingId && currentItemId === editingId) { + continue; + } + // 检查当前项是否包含编辑数据(避免选择子集) + const isEditingDataChild = editingId && hasRealChild(categories, currentItemId, editingId); + + if (!categories[currentItemId] || currentLevel === maxLevel || isEditingDataChild) { + arr.push({ + label: currentItem.name, + value: currentItemId, + }); + } else { + const new_arr: Option[] = checkArr( + categories, + currentItemId, + currentLevel + 1, + maxLevel, + editingId + ); + arr.push({ + label: currentItem.name, + value: currentItemId, + children: new_arr, + }); + } + } + return arr; + }; + + const hasRealChild = (categories: any[], parentId: number, targetId: number): boolean => { + if (!categories[parentId]) return false; + + for (let i = 0; i < categories[parentId].length; i++) { + const childId = categories[parentId][i].id; + + // 跳过直接匹配(编辑数据本身) + if (childId === targetId) { + continue; + } + + // 检查子节点是否匹配 + if (childId === targetId) { + return true; + } + // 递归检查孙节点 + if (hasRealChild(categories, childId, targetId)) { + return true; + } + } + return false; + }; + const getChapterParams = () => { + // 使用固定的章节目录 + if (parentData) { + const fixedOptions = generateFixedChapterOptions(parentData); + setChapters(fixedOptions); + form.setFieldsValue({ + parentId: parentData.id, + }); + setLoading(false); + return; + } + GetChapterListApi({ bookId }).then((res: any) => { + const chapters = res.data.chapters; + if (JSON.stringify(chapters) !== '{}') { + const editingId = isEdit && editData ? editData.id : undefined; + const new_arr: Option[] = checkArr(chapters, 0, 1, 2, editingId); + new_arr.unshift({ + label: t('commen.levelCate'), + value: 0, + }); + setChapters(new_arr); + + if (isEdit && editData) { + const arr = editData.chapterCode.split(','); + const p_arr: any[] = []; + arr.map((num: any) => { + p_arr.push(Number(num)); + }); + form.setFieldsValue({ + name: editData.name, + parentId: p_arr, + }); + setParentId(editData.parentId); + setLoading(false); + } + } else { + const new_arr: Option[] = [ + { + label: t('commen.levelCate'), + value: 0, + }, + ]; + setChapters(new_arr); + } + setLoading(false); + }); + }; const handleOk = async () => { try { const values = await form.validateFields(); onFinish(values); - console.log(values, 'add values'); } catch (error) { console.error('表单验证失败:', error); } @@ -93,9 +207,11 @@ export const ChapterModal: React.FC = ({ } setLoading(true); + // 使用固定的 parentId(来自 parentData)或表单中的值 + const finalParentId = parentData ? parentData.id : parentId || 0; + if (isEdit && editingId) { - // 编辑模式:调用更新接口,保持自己的sort - EditChapterApi(editingId, values.name, parentId || 0, editData.sort, bookId) + EditChapterApi(editingId, values.name, finalParentId, editData.sort, bookId) .then((res: any) => { setLoading(false); message.success('更新成功'); @@ -109,7 +225,7 @@ export const ChapterModal: React.FC = ({ }); } else { // 新建模式:调用新增接口 - CreateChapterApi(values.name, parentId || 0, 0, bookId) + CreateChapterApi(values.name, finalParentId, 0, bookId) .then((res: any) => { setLoading(false); message.success('创建成功'); @@ -130,6 +246,10 @@ export const ChapterModal: React.FC = ({ }; const handleChange = (value: any) => { + // 如果有 parentData,不允许更改 + if (parentData) { + return; + } if (value !== undefined) { let it = value[value.length - 1]; setParentId(it); @@ -137,44 +257,7 @@ export const ChapterModal: React.FC = ({ setParentId(0); } }; - const checkArr = (categories: any[], id: number) => { - const arr = []; - for (let i = 0; i < categories[id].length; i++) { - if (!categories[categories[id][i].id]) { - arr.push({ - label: categories[id][i].className, - value: categories[id][i].id, - }); - } else { - arr.push({ - label: categories[id][i].className, - value: categories[id][i].id, - }); - } - } - return arr; - }; - const getParams = () => { - getSoftwareClassApi().then((res: any) => { - const chapters = res.data; - if (JSON.stringify(chapters) !== '{}') { - const new_arr: Option[] = checkArr(chapters, 0); - new_arr.unshift({ - label: t('commen.levelCate'), - value: 0, - }); - setChapters(new_arr); - } else { - const new_arr: Option[] = []; - new_arr.unshift({ - label: t('commen.levelCate'), - value: 0, - }); - setChapters(new_arr); - } - setInit(false); - }); - }; + const displayRender = (label: any, selectedOptions: any) => { return label[label.length - 1]; }; @@ -190,12 +273,12 @@ export const ChapterModal: React.FC = ({ confirmLoading={confirmLoading} destroyOnHidden={true} > - {init && ( + {loading && (
)} -
+
= ({ rules={[{ required: true, message: t('textbook.chapter.parentTip') }]} > void; } export const ChapterTree = (props: PropInterface) => { - const { chapterTreeData, isLoading, selected, bookId, title, refreshTreeData } = props; + const { chapterData, isLoading, selected, bookId, title, onSelecet, refreshTreeData } = props; const permissions = useSelector((state: any) => state.loginUser.value.permissions); + // 权限 const through = (p: string) => { if (!permissions) { return false; } return typeof permissions[p] !== 'undefined'; }; - const navigate = useNavigate(); const { t } = useTranslation(); const [treeData, setTreeData] = useState([]); - const [loading, setLoading] = useState(false); const [selectKey, setSelectKey] = useState([]); - const [selectedNodeId, setSelectedNodeId] = useState(null); const [modalVisible, setModalVisible] = useState(false); const [isEdit, setIsEdit] = useState(false); const [editingChapter, setEditingChapter] = useState(null); const [parentChapter, setParentChapter] = useState(null); - const [form] = Form.useForm(); const [dragEnabled, setDragEnabled] = useState(false); // 拖拽状态 - const [did, setDid] = useState(0); const [modal, contextHolder] = Modal.useModal(); const [submitLoading, setSubmitLoading] = useState(false); - const [refresh, setRefresh] = useState(false); const [isMoreThanThree, setIsMoreThanThree] = useState(false); + + useEffect(() => { + if (selected && selected.length > 0) { + setSelectKey(selected); + } + }, [selected]); + + useEffect(() => { + setTimeout(() => { + if (JSON.stringify(chapterData) !== '{}') { + const new_arr: Option[] = checkArr(chapterData, 0); + setTreeData(new_arr); + console.log(new_arr, 'new-arr'); + } + }, 500); + }, [chapterData]); + // 切换拖拽模式 const toggleDragMode = () => { const newState = !dragEnabled; @@ -251,70 +248,13 @@ export const ChapterTree = (props: PropInterface) => { } }; - // 生成带序号的名称 - 需要更新以处理级别变化 - const generateChapterName = (chapter: ChapterItem, parent?: ChapterItem | null): string => { - // 如果节点成为一级目录,重新生成名称 - if (chapter.level === 1) { - return `第${chapter.sort}章 ${chapter.title}`; - } else if (chapter.level === 2) { - const parentSort = parent?.sort ?? '?'; - return `${parentSort}.${chapter.sort} ${chapter.title}`; - } else { - const parentSort = parent?.sort ?? '?'; - return `${parentSort}.${chapter.sort} ${chapter.title}`; - } - }; - - // 更新 regenerateChapterNames 以处理级别变化 - const regenerateChapterNames = (data: { [key: number]: ChapterItem[] }) => { - const regenerate = (chapters: ChapterItem[], parent?: ChapterItem): ChapterItem[] => { - return chapters.map((chapter, index) => { - // 更新排序 - const updatedChapter = { - ...chapter, - sort: index + 1, - }; - - // 重新生成名称 - updatedChapter.name = generateChapterName(updatedChapter, parent); - - // 递归处理子节点 - if (chapter.children && chapter.children.length > 0) { - return { - ...updatedChapter, - children: regenerate(chapter.children, updatedChapter), - }; - } - - return updatedChapter; - }); - }; - - return { 0: regenerate(data[0]) }; - }; - - useEffect(() => { - if (selected && selected.length > 0) { - setSelectKey(selected); - } - }, [selected]); - - useEffect(() => { - setTimeout(() => { - if (JSON.stringify(chapterTreeData) !== '{}') { - const new_arr: Option[] = checkArr(chapterTreeData, 0); - setTreeData(new_arr); - } - }, 500); - }, [chapterTreeData]); - const checkArr = (departments: ChaptersBoxModel, id: number) => { const arr: any = []; if (!departments[id]) return arr; for (let i = 0; i < departments[id].length; i++) { const item: ChapterItemModel = departments[id][i]; - const level = item.parent_chain ? item.parent_chain.split(',').length + 1 : 1; + const level = item.chapterCode ? item.chapterCode.split(',').length + 1 : 1; const hasChildren = departments[item.id]; const name = ( @@ -333,6 +273,7 @@ export const ChapterTree = (props: PropInterface) => { className="b-link c-red" onClick={(e) => { e.stopPropagation(); + handleEdit(item); }} style={{ padding: '6px', marginRight: 5, minWidth: 'auto' }} > @@ -361,7 +302,6 @@ export const ChapterTree = (props: PropInterface) => {
); - if (hasChildren) { const new_arr: Option[] = checkArr(departments, item.id); arr.push({ @@ -369,86 +309,42 @@ export const ChapterTree = (props: PropInterface) => { key: item.id, children: new_arr, level: level, + rowData: item, }); } else { arr.push({ title: name, key: item.id, level: level, + rowData: item, }); } } return arr; }; - const removeItem = (id: number, label: string) => { - if (id === 0) { + + const handlePreDeleteItem = () => { + if (selectKey.length === 0) { return; } - department.checkDestroy(id).then((res: any) => { - if ( - res.data.children && - res.data.children.length === 0 && - res.data.courses && - res.data.courses.length === 0 && - res.data.users && - res.data.users.length === 0 - ) { - delUser(id); + GetPreDestroyChapterApi(selectKey[0], bookId).then((res: any) => { + if (res.data.children && res.data.children.length === 0) { + handleDeleteItem(); } else { if (res.data.children && res.data.children.length > 0) { + console.log(123); modal.warning({ title: t('commen.confirmError'), centered: true, okText: t('commen.okText2'), content: (

- {t('department.unbindText1', { - // depName: depNameOBJ[systemLanguage][depDefaultName], - })} + {t('textbook.chapter.unbindText1')} ({res.data.children.length} - {t('department.unbindText2', { - // depName: depNameOBJ[systemLanguage][depDefaultName], - })} - ) + {t('textbook.chapter.unbindText2')}) - ,{t('department.unbindText3')} -

- ), - }); - } else { - modal.warning({ - title: t('commen.confirmError'), - centered: true, - okText: t('commen.okText2'), - content: ( -

- {t('department.unbindText4', { - // depName: depNameOBJ[systemLanguage][depDefaultName], - })} - {res.data.courses && res.data.courses.length > 0 && ( - - )} - {res.data.users && res.data.users.length > 0 && ( - - )} - {t('department.unbindText3')} + ,{t('textbook.chapter.unbindText3')}

), }); @@ -456,26 +352,19 @@ export const ChapterTree = (props: PropInterface) => { } }); }; - - const resetData = () => { - setTreeData([]); - setRefresh(!refresh); - }; - - const delUser = (id: any) => { + const handleDeleteItem = () => { confirm({ title: t('commen.confirmError'), icon: , - content: t('department.delText', { - // depName: depNameOBJ[systemLanguage][depDefaultName], - }), + content: t('textbook.chapter.delText'), centered: true, okText: t('commen.okText'), cancelText: t('commen.cancelText'), onOk() { - department.destroyDepartment(id).then((res: any) => { + DestroyChapterApi(selectKey[0], bookId).then(() => { message.success(t('commen.success')); - resetData(); + setTreeData([]); + refreshTreeData(); }); }, onCancel() { @@ -487,6 +376,7 @@ export const ChapterTree = (props: PropInterface) => { // 打开添加章节弹窗 const handleAdd = (parent?: ChapterItemModel) => { setParentChapter(parent || null); + setIsEdit(false); setEditingChapter(null); setModalVisible(true); }; @@ -494,32 +384,15 @@ export const ChapterTree = (props: PropInterface) => { // 打开编辑章节弹窗 const handleEdit = (chapter: ChapterItemModel) => { console.log('编辑章节:', chapter); - /* const result = getDetailByClassId(treeListData, editSelectId); - setEditSelectItem(result);*/ + setIsEdit(true); setEditingChapter(chapter); - setParentChapter(chapter); setModalVisible(true); }; - // 操作完成后清除选中ID - const handleOperationComplete = () => { - setSelectedNodeId(null); - }; - const onSelectTree = (selectedKeys: any, info: any) => { setSelectKey(selectedKeys); - }; - - const getNodeTitle = (node: any): string => { - if (node.title && node.title.props && node.title.props.children) { - const titleContent = node.title.props.children; - if (typeof titleContent === 'string') { - return titleContent; - } else if (titleContent.props && titleContent.props.children) { - return titleContent.props.children; - } - } - return node.title || ''; + onSelecet(info.node.rowData); + console.log(info); }; // 关闭弹窗 @@ -539,6 +412,7 @@ export const ChapterTree = (props: PropInterface) => { return (
+ {contextHolder}
{title}
@@ -551,21 +425,12 @@ export const ChapterTree = (props: PropInterface) => { > - @@ -600,7 +465,7 @@ export const ChapterTree = (props: PropInterface) => { )} {dragEnabled &&
{t('textbook.chapter.tips')}
} {isMoreThanThree &&
{t('textbook.chapter.tips3')}
} - {treeData.length > 0 && ( + {treeData.length > 0 ? (
{ } />
+ ) : ( + )}
{modalVisible && !dragEnabled && ( @@ -641,6 +508,7 @@ export const ChapterTree = (props: PropInterface) => { isEdit={isEdit} editData={editingChapter} bookId={bookId} + parentData={parentChapter} onCancel={handleModalCancel} confirmLoading={submitLoading} onSuccess={refreshTreeData} diff --git a/app/backend/src/types/editor.ts b/app/backend/src/types/editor.ts index edc08fe..2d9938b 100644 --- a/app/backend/src/types/editor.ts +++ b/app/backend/src/types/editor.ts @@ -9,10 +9,13 @@ export interface Chapter { } export interface EditorProps { + parentTitle: string; + bookId: number; + isLoading: boolean; chapterId: string; chapterTitle: string; initialContent?: string; - onSave: (chapterId: string, content: string) => void; + onSave: (data: any) => void; onContentChange: (chapterId: string, content: string) => void; }