add、del、edit、drag chaptersItem,add content

This commit is contained in:
penpenwang 2025-11-30 15:48:57 +08:00
parent 6b6761f7bb
commit 1d1fa94983
10 changed files with 571 additions and 433 deletions

View File

@ -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
* */

View File

@ -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": {

View File

@ -68,7 +68,7 @@ export const DepartmentCreate: React.FC<PropInterface> = ({ 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,

View File

@ -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: (

View File

@ -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<any>(null);
const [contentChapterName, setContentChapterName] = useState<any>(null);
const [searchParams] = useSearchParams();
const title = searchParams.get('title');
const { bookId } = params;
const [loading, setLoading] = useState<boolean>(false);
const [treeData, setTreeData] = useState<ChaptersBoxModel>([]);
const [isContentLoading, setIsContentLoading] = useState<boolean>(false);
const [chapterListData, setChapterListData] = useState<ChaptersBoxModel>([]);
const [selectedChapterId, setSelectedChapterId] = useState<string>();
const [parentTitle, setParentTitle] = useState<string>();
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 (
<div className={styles['chapter-box']}>
@ -68,25 +137,34 @@ const ChapterManagementPage = () => {
<div className={styles['left-box']}>
<ChapterTree
selectedId={selectedChapterId}
onSelecet={handleSelectItem}
bookId={Number(bookId)}
isLoading={loading}
title={title}
chapterTreeData={treeData}
refreshTreeData={getChapterData}
chapterData={chapterListData}
refreshTreeData={refreshData}
/>
</div>
<div className={styles['right-box']}>
{/*{selectedChapter ? (*/}
{selectedChapter ? (
<TextbookEditor
chapterId={selectedChapter?.id || 22}
chapterTitle={selectedChapter?.name || '测试数据'}
initialContent="请编写内容"
isLoading={isContentLoading}
chapterId={selectedChapter?.id}
parentTitle={parentTitle || '根目录'}
chapterTitle={contentChapterName || selectedChapter?.name}
onSave={onSave}
bookId={Number(bookId)}
onContentChange={onContentChange}
></TextbookEditor>
{/* : (
<div></div>
)}*/}
) : (
<div className="enhanced-textbook-editor empty-state">
<div className="empty-content">
<div className="empty-icon"></div>
<h3></h3>
<p></p>
</div>
</div>
)}
</div>
</div>
</div>

View File

@ -82,7 +82,7 @@
/*编辑区域*/
.editor-main-box{
background: #8F8F8F;
background: #ffffff;
position: relative;
margin: 20px;
height: 590px;

View File

@ -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<EditorProps> = ({
chapterId,
parentTitle,
chapterTitle,
initialContent = '',
onSave,
bookId,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [isEditing, setIsEditing] = useState<boolean>(false);
const { t } = useTranslation();
// editor 实例
const [editor, setEditor] = useState<IDomEditor | null>(null); // TS 语法
const [editor, setEditor] = useState<IDomEditor | null>(null);
// 编辑器内容
const [html, setHtml] = useState('<p>hello</p>');
const [html, setHtml] = useState<string>();
const [loading, setLoading] = useState<boolean>(true);
const [isPreviewModalShow, setIsPreviewModalShow] = useState<boolean>(false);
const toolbarConfig: Partial<IToolbarConfig> = {};
toolbarConfig.excludeKeys = ['fullScreen']; //移除不想要的fullScreen
useEffect(() => {
if (editor && initialContent !== editor.getHtml()) {
editor.insertText(
// 添加内容
initialContent ||
`
<h2>${chapterTitle}</h2>
<p>...</p>
`
);
setIsEditing(false);
setLoading(true);
const timer = setTimeout(() => {
getContent();
}, 2000);
return () => clearTimeout(timer);
}, [chapterTitle, chapterId]);
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);
}
}, [initialContent, editor, chapterTitle]);
// 工具栏配置
const toolbarConfig: Partial<IToolbarConfig> = {}; // TS 语法
// }
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
@ -56,24 +61,22 @@ const TextbookEditor: React.FC<EditorProps> = ({
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,31 +89,34 @@ const TextbookEditor: React.FC<EditorProps> = ({
// 预览
const handlePreview = () => {
alert('preview');
setIsPreviewModalShow(true);
};
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>
);
}
// 在编辑器创建完成后关闭 loading
const handleEditorCreated = (editorInstance: any) => {
setEditor(editorInstance);
};
return (
<>
<div className="enhanced-textbook-editor">
{/* 编辑器头部 */}
{loading ? (
<div style={{ width: '100%', height: 600, textAlign: 'center', lineHeight: '600px' }}>
<Spin size="large" tip="Loading..." style={{ color: '#1890ff' }} />
</div>
) : (
<>
<div className="editor-header">
<div className="header-left">
<h2 className="chapter-title">{chapterTitle}</h2>
<span className="chapter-id">: {chapterId}</span>
<span className="chapter-id">: {parentTitle}</span>
</div>
<div className="header-right">
<Button style={{ marginRight: 10 }} onClick={handlePreview}>
<Button
style={{ marginRight: 10 }}
onClick={handlePreview}
disabled={!html || html == '<p><br></p>' || html == '<p></p>'}
>
<EyeFilled />
{t('textbook.resource.btnPreview')}
</Button>
@ -125,30 +131,125 @@ const TextbookEditor: React.FC<EditorProps> = ({
<ForkOutlined />
{t('textbook.resource.knowledge')}
</Button>
{isEditing ? (
<Button type="primary" style={{ marginRight: 5 }} onClick={handleManualSave}>
<SaveFilled />
{t('textbook.resource.btnSave')}
</Button>
) : (
<Button
type="primary"
style={{ marginRight: 5 }}
onClick={() => {
setIsEditing(true);
}}
>
<EditFilled />
{t('textbook.resource.btnEdit')}
</Button>
)}
</div>
</div>
<div className="editor-main-box">
{isEditing ? (
<>
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="default"
style={{ borderBottom: '1px solid #ccc' }}
/>
<Editor
defaultConfig={editorConfig}
value={html}
onCreated={setEditor}
onChange={(editor) => setHtml(editor.getHtml())}
onCreated={(editor: any) => handleEditorCreated(editor)}
onChange={(editor) => {
const currentHtml = editor.getHtml();
setHtml(currentHtml);
window.localStorage.setItem('html', currentHtml);
}}
mode="default"
style={{ height: '550px', overflowY: 'hidden' }}
style={{
height: '550px',
paddingBottom: 15,
boxSizing: 'border-box',
overflowY: 'hidden',
}}
/>
</>
) : (
<div
style={{
height: '590px',
borderRadius: '6px',
padding: '20px',
boxSizing: 'border-box',
backgroundColor: '#fafafa',
overflowY: 'auto',
width: '100%',
overflowX: 'hidden',
}}
>
{html && html !== '<p><br></p>' && html !== '<p></p>' ? (
// @ts-ignore
<div style={{ width: '100%' }} dangerouslySetInnerHTML={{ __html: html }} />
) : (
<div
style={{
height: '550px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div
style={{
textAlign: 'center',
color: '#999',
fontSize: '16px',
}}
>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>
<Empty description={false} />
</div>
<div style={{ marginBottom: '8px', fontWeight: 500 }}></div>
<div style={{ fontSize: '14px', color: '#bfbfbf' }}>
</div>
</div>
</div>
)}
</div>
)}
</div>
</>
)}
</div>
<Drawer
key={'previewModal'}
title={t('textbook.resource.btnPreview')}
onClose={() => setIsPreviewModalShow(false)}
width={1000}
open={isPreviewModalShow}
footer={null}
>
<div
style={{
height: 'auto',
borderRadius: '6px',
padding: '8px',
margin: '10px',
width: 'auto',
backgroundColor: '#fafafa',
overflowY: 'auto',
}}
// @ts-ignore
dangerouslySetInnerHTML={{ __html: html }}
/>
</Drawer>
</>
);
};

View File

@ -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<ChapterModalProps> = ({
bookId,
isEdit,
editData,
parentData,
onCancel,
confirmLoading = false,
onSuccess,
@ -56,32 +33,169 @@ export const ChapterModal: React.FC<ChapterModalProps> = ({
const [form] = Form.useForm();
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [init, setInit] = useState(true);
const [chapters, setChapters] = useState<any>([]);
const [parentId, setParentId] = useState<number>(0);
const [editingId, setEditingId] = useState<number>(0); // 存储正在编辑的ID
// 根据 parentData 生成固定的章节目录选项
const generateFixedChapterOptions = (parentData: any): Option[] => {
if (!parentData) {
return [
{
label: t('commen.levelCate'),
value: 0,
},
];
}
const fixedOptions: Option[] = [
{
label: parentData.name,
value: parentData.id,
},
];
fixedOptions.unshift({
label: t('commen.levelCate'),
value: 0,
});
return fixedOptions;
};
useEffect(() => {
setInit(true);
if (isEdit && editData) {
form.setFieldsValue(editData);
} else {
setLoading(true);
form.resetFields();
let defaultLevel = 1;
form.setFieldsValue({
level: defaultLevel,
...editData,
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,
});
}
setInit(false);
getParams();
}, [visible, isEdit, editData, form]);
}
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<ChapterModalProps> = ({
}
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<ChapterModalProps> = ({
});
} 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<ChapterModalProps> = ({
};
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<ChapterModalProps> = ({
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<ChapterModalProps> = ({
confirmLoading={confirmLoading}
destroyOnHidden={true}
>
{init && (
{loading && (
<div className="float-left text-center mt-30">
<Spin></Spin>
</div>
)}
<div className="float-left mt-24" style={{ display: init ? 'none' : 'block' }}>
<div className="float-left mt-24" style={{ display: loading ? 'none' : 'block' }}>
<Form
form={form}
name="basic"
@ -212,14 +295,14 @@ export const ChapterModal: React.FC<ChapterModalProps> = ({
rules={[{ required: true, message: t('textbook.chapter.parentTip') }]}
>
<Cascader
allowClear
allowClear={!parentData}
placeholder={t('textbook.chapter.parentTip')}
onChange={handleChange}
options={chapters}
changeOnSelect
expand-trigger="hover"
expandTrigger="hover"
displayRender={displayRender}
disabled={isEdit && editingId === 0} // 禁止编辑根分类的父级
disabled={!!parentData || (isEdit && editingId === 0)} // 有 parentData 时禁用选择
/>
</Form.Item>
<Form.Item

View File

@ -1,4 +1,4 @@
import { Button, Image, Tree, Modal, Form, message, Spin, Tooltip } from 'antd';
import { Button, Image, Tree, Modal, Form, message, Spin, Tooltip, Empty } from 'antd';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './chapterTree.module.less';
@ -24,77 +24,74 @@ import type { DataNode, TreeProps } from 'antd/es/tree';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import textbook from '../index';
import { CreateChapterApi, DropDiffClassApi, DropSameClassApi } from '../../../api/textbook';
interface ChapterItem {
id: string | number;
name: string;
title?: string;
level?: number;
parent_chain: string;
parent_id: string;
sort: number;
children?: ChapterItem[];
}
interface ChapterItemModel {
created_at: string;
id: number;
name: string;
from_scene: number;
parent_chain: string;
parent_id: number;
sort: number;
updated_at: string;
}
interface ChaptersBoxModel {
[key: number]: ChapterItemModel[];
}
import {
CreateChapterApi,
DestroyChapterApi,
DropDiffClassApi,
DropSameClassApi,
GetPreDestroyChapterApi,
} from '../../../api/textbook';
import { ChapterItemModel, ChaptersBoxModel, ChapterTreeItem } from '../chapter';
interface Option {
key: string | number;
title: any;
children?: Option[];
level?: number;
sort?: number;
rawData?: any;
}
interface PropInterface {
chapterTreeData: ChaptersBoxModel;
chapterData: ChaptersBoxModel;
isLoading: boolean;
bookId: number;
title: any;
selected?: any;
selectedId?: any;
onSelecet?: any;
refreshTreeData: () => 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<Option[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [selectKey, setSelectKey] = useState<number[]>([]);
const [selectedNodeId, setSelectedNodeId] = useState<string | number | null>(null);
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [isEdit, setIsEdit] = useState<boolean>(false);
const [editingChapter, setEditingChapter] = useState<ChapterItemModel | null>(null);
const [parentChapter, setParentChapter] = useState<ChapterItemModel | null>(null);
const [form] = Form.useForm();
const [dragEnabled, setDragEnabled] = useState<boolean>(false); // 拖拽状态
const [did, setDid] = useState<number>(0);
const [modal, contextHolder] = Modal.useModal();
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
const [refresh, setRefresh] = useState(false);
const [isMoreThanThree, setIsMoreThanThree] = useState<boolean>(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) => {
</div>
</div>
);
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: (
<p>
{t('department.unbindText1', {
// depName: depNameOBJ[systemLanguage][depDefaultName],
})}
{t('textbook.chapter.unbindText1')}
<span className="c-red">
{res.data.children.length}
{t('department.unbindText2', {
// depName: depNameOBJ[systemLanguage][depDefaultName],
})}
{t('textbook.chapter.unbindText2')}
</span>
{t('department.unbindText3')}
</p>
),
});
} else {
modal.warning({
title: t('commen.confirmError'),
centered: true,
okText: t('commen.okText2'),
content: (
<p>
{t('department.unbindText4', {
// depName: depNameOBJ[systemLanguage][depDefaultName],
})}
{res.data.courses && res.data.courses.length > 0 && (
<Button
style={{ paddingLeft: 4, paddingRight: 4 }}
type="link"
danger
onClick={() => navigate('/course?did=' + id + '&label=' + label)}
>
{res.data.courses.length}
{t('department.unbindText5')}
</Button>
)}
{res.data.users && res.data.users.length > 0 && (
<Button
type="link"
style={{ paddingLeft: 4, paddingRight: 4 }}
danger
onClick={() => navigate('/member/index?did=' + id + '&label=' + label)}
>
{res.data.users.length}
{t('department.unbindText6')}
</Button>
)}
{t('department.unbindText3')}
{t('textbook.chapter.unbindText3')}
</p>
),
});
@ -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: <ExclamationCircleFilled />,
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 (
<div className={styles.chapterTree} id={'chapter-tree-container'}>
{contextHolder}
<div className={styles.chapterTitle}>
<div>{title}</div>
<div>
@ -551,21 +425,12 @@ export const ChapterTree = (props: PropInterface) => {
>
<PlusOutlined style={{ fontSize: '20px' }} />
</Button>
<Button
type="link"
className="b-link c-red mr-8"
title={t('textbook.chapter.edit')}
disabled={dragEnabled}
onClick={(e) => e.preventDefault()}
>
<FormOutlined style={{ fontSize: '20px' }} />
</Button>
<Button
type="link"
className="b-link c-red mr-8"
title={t('textbook.chapter.delete')}
disabled={dragEnabled}
onClick={(e) => e.preventDefault()}
disabled={dragEnabled || selectKey.length === 0}
onClick={handlePreDeleteItem}
>
<DeleteOutlined style={{ fontSize: '20px' }} />
</Button>
@ -600,7 +465,7 @@ export const ChapterTree = (props: PropInterface) => {
)}
{dragEnabled && <div className={'primary'}> {t('textbook.chapter.tips')}</div>}
{isMoreThanThree && <div className={'primary'}> {t('textbook.chapter.tips3')}</div>}
{treeData.length > 0 && (
{treeData.length > 0 ? (
<div className={`${styles[`bottom-tree-box`]}`}>
<Tree
draggable={
@ -633,6 +498,8 @@ export const ChapterTree = (props: PropInterface) => {
}
/>
</div>
) : (
<Empty style={{ marginTop: 40 }}></Empty>
)}
</div>
{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}

View File

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