Compare commits

...

2 Commits

Author SHA1 Message Date
bc8dc4cff6 format && textbookApi all ,resource ui, change chapter-editor ui 2025-11-28 18:25:08 +08:00
29054c6188 format 2025-11-26 10:37:19 +08:00
35 changed files with 1839 additions and 2015 deletions

View File

@ -38,7 +38,7 @@ export const knowledgeKeyApi = {
},
validDataSet: () => {
return client.get('/knowledge/dataset/validDataSet', {});
}
},
// // 获取知识库密钥
// getKnowledgeKey: () => {

View File

@ -78,5 +78,5 @@ export function videoUpdate(id: number, params: any) {
}
export function getResourceUrl(id: number) {
return client.get(`/backend/v1/resource/getResourceUrl?id=`+id, {});
return client.get(`/backend/v1/resource/getResourceUrl?id=` + id, {});
}

View File

@ -1,16 +1,15 @@
import client from './internal/httpClient';
// textBook
export function textbookList(page: number, size: number, title: string) {
export function GetTextbookListApi(page: number, size: number, title: string) {
return client.get('/backend/v1/jc/textbook/index', {
// return client.get('/backend/v1/course/index', {
page: page,
size: size,
title: title,
});
}
export function createTextbook(
export function CreateTextbookApi(
title: string,
thumb: string,
shortDesc: string,
@ -21,8 +20,10 @@ export function createTextbook(
userIds: number[],
publishTime: string,
publishUnit: string,
createTime: string
createTime: string,
isbn: string
) {
console.log(thumb, thumb, '>>>>');
return client.post('/backend/v1/jc/textbook', {
title,
thumb,
@ -35,14 +36,15 @@ export function createTextbook(
publishTime,
publishUnit,
createTime,
isbn,
});
}
export function textbookDetail(id: number) {
export function GetTextbookDetailApi(id: number) {
return client.get(`/backend/v1/jc/textbook/${id}`, {});
}
export function updateTextbook(
export function UpdateTextbookApi(
id: number,
title: string,
thumb: string,
@ -54,7 +56,8 @@ export function updateTextbook(
userIds: number[],
publishTime: string,
publishUnit: string,
createTime: string
createTime: string,
isbn: string
) {
return client.put(`/backend/v1/jc/textbook`, {
id,
@ -69,62 +72,163 @@ export function updateTextbook(
publishTime,
publishUnit,
createTime,
isbn,
});
}
export function destroyTextbook(id: number) {
export function DestroyTextbookApi(id: number) {
return client.destroy(`/backend/v1/jc/textbook/${id}`);
}
export function courseUser(
courseId: number,
page: number,
size: number,
sortField: string,
sortAlgo: string,
/**
*
**/
export function GetResourceListApi(
page: number = 1,
size: number = 10,
type: any = 0,
sortOrder: any = '',
sortField: any = '',
searchData: any = ''
) {
return client.get('/backend/v1/jc/resource/index', {
page,
size,
type,
sortOrder,
sortField,
searchData,
});
}
//删除+ 批量删除
export function DelResourceItemApi(idList: string) {
return client.destroy(`/backend/v1/jc/softwareInfo?idList=${idList}`);
}
//根据id查详情
export function GetDetailApi(id: number) {
return client.get(`/backend/v1/jc/resource/${id}`, {});
}
export function UpdateDetailApi(
id: number,
softwareName: string,
version: string,
company: string,
desc: string,
softNo: string,
type: string,
softPath: string,
maxConnect: string,
minSpeed: string,
shareSchoolId: string[],
videoResourceInfos?: SoftwareResourceInfo[],
docResourceInfos?: SoftwareResourceInfo[],
softwareItemInfos?: SoftwareItemInfo[]
) {
return client.post(`/backend/v1/virtual/update`, {
id,
softwareName,
type,
softNo,
softPath,
version,
company,
desc,
maxConnect,
minSpeed,
shareSchoolId,
softwareItemInfos,
videoResourceInfos,
docResourceInfos,
});
}
// 获取分类列表
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相关信息
* */
export function GetChapterListApi(params: any) {
return client.get('/backend/v1/jc/chapter/index', params);
}
// 包含嵌套的
export function DropDiffClassApi(id: number, parent_id: number, ids: number[], book_id: number) {
return client.put(`/backend/v1/jc/chapter/update/parent`, {
id: id,
parent_id: parent_id,
ids: ids,
book_id: book_id,
});
}
export function DropSameClassApi(ids: number[], book_id: number) {
return client.put(`/backend/v1/jc/chapter/update/sort`, {
ids: ids,
book_id: book_id,
});
}
export function checkDestroy(id: number) {
return client.get(`/backend/v1/department/${id}/destroy`, {});
}
export function DestroyChapterApi(id: number) {
return client.destroy(`/backend/v1/department/${id}`);
}
export function CreateChapterApi(name: string, parentId: number, sort: number, bookId: number) {
return client.post('/backend/v1/jc/chapter/create', {
name,
parentId,
sort,
bookId,
});
}
export function EditChapterApi(
id: number,
name: string,
state: number | null,
depId: number | null
parentId: number,
sort: number,
bookId: number
) {
return client.get(`/backend/v1/offline/course/${courseId}/user/index`, {
page: page,
size: size,
sort_field: sortField,
sort_algo: sortAlgo,
name: name,
state,
dep_id: depId,
return client.post(`/backend/v1/jc/chapter/${id}`, {
name,
parentId,
sort,
bookId,
});
}
export function updateUserStateMulti(courseId: number, ids: number[], state: number) {
return client.post(`/backend/v1/offline/course/${courseId}/user/state-multi`, {
user_ids: ids,
state,
});
}
export function updateUser(
courseId: number,
userId: number,
state: number,
grade: string,
remark: string
) {
return client.post(`/backend/v1/offline/course/${courseId}/user/${userId}`, {
state,
grade,
remark,
});
}
export function storeBatch(courseId: number, startLine: number, records: string[][]) {
return client.post(`/backend/v1/offline/course/${courseId}/user/store-batch`, {
start_line: startLine,
records,
});
}
export function getDynamic(code: string) {
return client.get(`/backend/v1/offline/course/dynamic`, { code: code });
}
/*
* resource List
* */

View File

@ -40,7 +40,7 @@ export const LeftMenu: React.FC = () => {
'^/group': ['user'],
'^/course': ['courses'],
'^/offline-course': ['courses'],
'^/textbook': ['textbook'],
'^/textbook': ['courses'],
'^/task': ['task'],
'^/system': ['system'],
'^/cert': ['resource'],
@ -504,6 +504,12 @@ export const LeftMenu: React.FC = () => {
} else if (location.pathname.indexOf('/offline-course') !== -1) {
setSelectedKeys(['/offline-course']);
setOpenKeys(openKeyMerge('/offline-course'));
} else if (location.pathname.indexOf('/textbook') !== -1) {
setSelectedKeys(['/textbook']);
setOpenKeys(openKeyMerge('/textbook'));
} else if (location.pathname.indexOf('/teacher') !== -1) {
setSelectedKeys(['/teacher']);
setOpenKeys(openKeyMerge('/textbook'));
} else if (location.pathname.indexOf('/virtualSimulation/software') !== -1) {
// 处理虚拟仿真软件详情页
setSelectedKeys(['/virtualSimulation/software']);

View File

@ -30,6 +30,7 @@
"levelCate": "作为一级分类",
"edit": "编辑",
"del": "删除",
"moreDel": "批量删除",
"more": "更多",
"drawerOk": "确 认",
"drawerCancel": "取 消",
@ -2013,11 +2014,13 @@
"name": "教材名称",
"namePlaceholder": "请填写教材名称",
"desc": "教材简介",
"descPlaceholder": "请填写教材简介(最多200字",
"descPlaceholder": "请填写教材简介(最多300字",
"thumb": "教材封面",
"thumbPlaceholder": "请选择教材封面",
"subject": "学科专业",
"subjectPlaceholder": "请填写学科专业",
"isbn": "ISBN",
"isbnPlaceholder": "请填写ISBN",
"author": "作者",
"authorPlaceholder": "请填写作者",
"publisher": "出版社",
@ -2028,18 +2031,55 @@
"createTimePlaceholder": "请填写创建时间",
"publishTime":"发布时间",
"publishTimePlaceholder":"请填写发布时间",
"thumbTip": "(推荐尺寸:400x300px"
"thumbTip": "(推荐尺寸:217x290px"
},
"chapter":{
"management": "章节管理",
"title": "章节标题",
"titleTip": "请输入章节标题",
"add": "添加章节",
"edit": "编辑章节",
"delete": "删除章节",
"drag": "拖拽章节",
"saveDrag": "保存目录",
"parent": "章节层级",
"parent": "所属章节",
"parentTip": "请选择所属章节",
"tips": "注意:章节目录不可超过三级!",
"tips3": "注意:章节目录超过三级,请调整!"
},
"resource": {
"pageTitle": "资源管理",
"upload":"上传资源",
"searchPlaceholder": "搜索资源名称",
"title1": "资源名称",
"title1Placeholder": "请输入资源名称",
"title2": "资源类型",
"title2PlaceHolder": "请选择资源类型",
"title3": "大小",
"title4": "创建时间",
"title5": "操作",
"moreDel": "批量删除",
"editItem": "编辑资源",
"type": "资源类型",
"typePlaceholder": "请选择资源类型",
"desc": "资源描述",
"descPlaceholder": "请输入资源描述",
"chapter": "所属章节",
"chapterPlaceholder": "请选择所属章节",
"uploadTips": "点击或拖拽文件到此处上传",
"uploadTips2": "支持视频、图片、文档、音频等常见格式",
"btnSave": "保存内容",
"btnPreview": "预览",
"knowledge": "知识图谱",
"typeList": {
"all": "全部",
"video": "视频",
"img": "图片",
"doc": "文档",
"audio": "音频",
"other": "其他"
}
}
}
}

View File

@ -1951,7 +1951,7 @@
"createTimePlaceholder": "請填入建立時間",
"publishTime":"發佈時間",
"publishTimePlaceholder":"請填入發佈時間",
"thumbTip": "(建議尺寸:400x300px"
"thumbTip": "(建議尺寸:210x297px"
}
}
}

View File

@ -185,4 +185,4 @@ const ExamAdministrationDetails = () => {
);
};
export default ExamAdministrationDetails
export default ExamAdministrationDetails;

View File

@ -329,7 +329,8 @@ const ExamAdministrationPage = () => {
}*/
// 根据不同操作类型,调用对应的后端批量接口
switch (currentOperation) {
case 'resetExam': { // 批量重置接口:传递 examIds
case 'resetExam': {
// 批量重置接口:传递 examIds
const resResetExam = (await updateList(ids ?? [], '3', '')) as UpdateListResponse;
if (resResetExam?.code === 0) {
message.success(`成功重置 ${data.records.length} 名学生的考试状态`);

View File

@ -369,7 +369,12 @@ const DictionaryDetailPage = () => {
<Button onClick={enterFullscreen}></Button>
</div>
<div className={styles.iframeContainer}>
<iframe ref={iframeRef} src={accessUrl} className={styles.iframe} title="数字孪生程序" />
<iframe
ref={iframeRef}
src={accessUrl}
className={styles.iframe}
title="数字孪生程序"
/>
</div>
</Card>
@ -472,7 +477,7 @@ const DictionaryDetailPage = () => {
right: 4,
bottom: 4,
borderRadius: 8,
overflow: 'hidden'
overflow: 'hidden',
}}
onError={(error) => handleVideoError(error, item)}
onReady={() => handleVideoReady(item)}
@ -491,7 +496,7 @@ const DictionaryDetailPage = () => {
justifyContent: 'center',
backgroundColor: '#f0f0f0',
color: '#666',
borderRadius: 8
borderRadius: 8,
}}
>
@ -511,7 +516,7 @@ const DictionaryDetailPage = () => {
justifyContent: 'center',
backgroundColor: '#f0f0f0',
color: '#666',
borderRadius: 8
borderRadius: 8,
}}
>

View File

@ -38,7 +38,7 @@ import defaultThumb2 from '../../assets/thumb/thumb2.png';
import defaultThumb3 from '../../assets/thumb/thumb3.png';
import { FileUploader } from '../../compenents/uploadFile';
import { TreeAttachments } from '../offline-course/compenents/attachments';
import {getResourceUrl} from "../../api/resource";
import { getResourceUrl } from '../../api/resource';
//搜索框
type SearchProps = GetProps<typeof Input.Search>;
@ -214,7 +214,7 @@ const Experiment = () => {
interface UploadRes {
data?: string; // 根据后端实际返回结构调整,比如可能是路径字符串
}
const getImageUrl=(id:any)=>{
const getImageUrl = (id: any) => {
getResourceUrl(id).then((res: any) => {
setThumb(res.data.resource_url);
});
@ -356,7 +356,7 @@ const Experiment = () => {
}
// 修复拼写错误并添加空值判断
if (res.data.labCourseImage) {
getImageUrl(res.data.labCourseImage)
getImageUrl(res.data.labCourseImage);
}
setSelectedId(res.data.id);
// 打开弹窗

View File

@ -11,7 +11,6 @@ const HomePage = () => {
return (
<>
<div className={styles['layout-wrap']}>
<div className={styles['left-menu']}>
<LeftMenu />
</div>

View File

@ -81,8 +81,7 @@ const formatDate = (value?: string | number | null): string => {
return '';
}
const date =
value.toString().length === 10 ? new Date(value * 1000) : new Date(value);
const date = value.toString().length === 10 ? new Date(value * 1000) : new Date(value);
if (Number.isNaN(date.getTime())) {
return '';
}
@ -106,8 +105,7 @@ const normalizeListResponse = (payload: any) => {
root?.items,
Array.isArray(root) ? root : null,
];
const list =
listCandidates.find((candidate) => Array.isArray(candidate)) ?? [];
const list = listCandidates.find((candidate) => Array.isArray(candidate)) ?? [];
const total =
root?.total ??
@ -155,9 +153,7 @@ const ResourceLibraryReviewPage = () => {
});
const [growthTrend, setGrowthTrend] = useState<GrowthRecord[]>([]);
const [distributionData, setDistributionData] = useState<
{ value: number; name: string }[]
>([]);
const [distributionData, setDistributionData] = useState<{ value: number; name: string }[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [selectedQuestion, setSelectedQuestion] = useState<QuestionRecord | null>(null);
@ -320,9 +316,7 @@ const ResourceLibraryReviewPage = () => {
});
const filtered =
kmType !== undefined
? normalizedList.filter(
(item) => Number(item?.kmType) === Number(kmType)
)
? normalizedList.filter((item) => Number(item?.kmType) === Number(kmType))
: normalizedList;
setQuestionList(filtered);
if (kmType !== undefined && filtered.length !== list.length) {
@ -486,15 +480,10 @@ const ResourceLibraryReviewPage = () => {
<div
className={styles['htr-stat-trend']}
style={{
color:
statistics.kmTypeReviewGrowthRate >= 0 ? '#52c41a' : '#ff4d4f',
color: statistics.kmTypeReviewGrowthRate >= 0 ? '#52c41a' : '#ff4d4f',
}}
>
{statistics.kmTypeReviewGrowthRate >= 0 ? (
<RiseOutlined />
) : (
<FallOutlined />
)}
{statistics.kmTypeReviewGrowthRate >= 0 ? <RiseOutlined /> : <FallOutlined />}
<span>
{statistics.kmTypeReviewGrowthRate >= 0 ? '较昨日增长' : '较昨日下降'}{' '}
{Math.abs(statistics.kmTypeReviewGrowthRate)}%
@ -568,8 +557,7 @@ const ResourceLibraryReviewPage = () => {
<div
className={styles['htr-stat-trend']}
style={{
color:
statistics.kmQueryDuplicateRateGrowth >= 0 ? '#52c41a' : '#ff4d4f',
color: statistics.kmQueryDuplicateRateGrowth >= 0 ? '#52c41a' : '#ff4d4f',
}}
>
{statistics.kmQueryDuplicateRateGrowth >= 0 ? (
@ -636,7 +624,9 @@ const ResourceLibraryReviewPage = () => {
<Button
type="primary"
icon={<SearchOutlined />}
onClick={() => fetchQuestionList(1, searchKeyword, statusFilter, experimentTypeFilter)}
onClick={() =>
fetchQuestionList(1, searchKeyword, statusFilter, experimentTypeFilter)
}
>
</Button>
@ -662,15 +652,15 @@ const ResourceLibraryReviewPage = () => {
showTotal: (count, range) => `显示 ${range[0]}-${range[1]} 条,共 ${count}`,
onChange: (current) => setPage(current),
}}
rowKey={(record) =>
record.id ||
record.kmConversationId ||
record.km_conversation_id ||
record.kmId ||
record.km_id ||
record.kmQuery ||
Math.random()
}
rowKey={(record) =>
record.id ||
record.kmConversationId ||
record.km_conversation_id ||
record.kmId ||
record.km_id ||
record.kmQuery ||
Math.random()
}
/>
</Card>
@ -819,12 +809,7 @@ const ResourceLibraryReviewPage = () => {
>
</Button>,
<Button
key="submit"
type="primary"
onClick={handleModalOk}
loading={modalSubmitting}
>
<Button key="submit" type="primary" onClick={handleModalOk} loading={modalSubmitting}>
</Button>,
]}
@ -845,7 +830,9 @@ const ResourceLibraryReviewPage = () => {
<div className={styles['htr-modal-info-item']}>
<Text strong></Text>
<Space>
<Avatar>{selectedQuestion.kmUser ? selectedQuestion.kmUser.charAt(0) : ''}</Avatar>
<Avatar>
{selectedQuestion.kmUser ? selectedQuestion.kmUser.charAt(0) : ''}
</Avatar>
<div>{selectedQuestion.kmUser || '匿名用户'}</div>
</Space>
</div>
@ -915,4 +902,3 @@ const ResourceLibraryReviewPage = () => {
};
export default ResourceLibraryReviewPage;

View File

@ -9,23 +9,23 @@
flex-direction: row;
overflow: hidden;
.left-box {
width: 350px;
width: 300px;
float: left;
height: auto;
min-height: calc(100vh - 172px);
border-right: 1px solid #f6f6f6;
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-sizing: border-box;
padding: 24px 16px;
background-color: white;
}
.right-box {
width: calc(100% - 351px);
width: calc(100% - 354px);
float: left;
height: auto;
min-height: calc(100vh - 172px);
background: #f9fafb;
box-sizing: border-box;
padding: 24px;
background-color: white;
}
}

View File

@ -7,6 +7,7 @@ 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';
export interface ChapterItemModel {
created_at: string;
@ -40,57 +41,17 @@ const ChapterManagementPage = () => {
const [loading, setLoading] = useState<boolean>(false);
const [treeData, setTreeData] = useState<ChaptersBoxModel>([]);
const [selectedChapterId, setSelectedChapterId] = useState<string>();
const [did, setDid] = useState<number>(0);
// 获取数据
const getData = () => {
department.departmentList({ from_scene: 0 }).then((res: any) => {
const resData: ChaptersBoxModel = res.data.departments;
setTreeData(resData);
setLoading(false);
});
};
const getChapterData = () => {
department.departmentList({ from_scene: 0 }).then((res: any) => {
const resData: ChaptersBoxModel = res.data.departments;
setTreeData(resData);
setLoading(false);
});
};
useEffect(() => {
getData();
getChapterData();
}, []);
console.log(bookId, 'bookid');
// 处理章节更新
const handleChapterUpdate = (keys: any, title: any) => {
console.log('选中的章节:', keys, title);
};
}, [bookId]);
// 处理添加章节
const handleAddChapter = (parentId: string | number | null, level: number) => {
console.log('添加章节, 父级ID:', parentId, '级别:', level);
};
// 处理编辑章节
const handleEditChapter = (chapter: any) => {
console.log('编辑章节:', chapter);
};
// 处理删除章节
const handleDeleteChapter = (chapter: any) => {
console.log('删除章节:', chapter);
};
// 处理选中章节
const handleSelectChapter = (chapter: any) => {
console.log('选中章节详情:', chapter);
setSelectedChapter(chapter);
};
// 处理节点拖拽
const handleChangeOrder = (chapters: any[]) => {
console.log(chapters, '<>><><');
const getChapterData = () => {
GetChapterListApi({ bookId: bookId }).then((res: any) => {
const resData: ChaptersBoxModel = res.data.chapters;
setTreeData(resData);
setLoading(false);
});
};
const onSave = () => {
@ -102,45 +63,30 @@ const ChapterManagementPage = () => {
return (
<div className="playedu-main-body">
<BackBartment title={title || '课程名称'} />
<BackBartment title={t('textbook.chapter.management')} />
<div className={styles['chapter-main-body']}>
<div className={styles['left-box']}>
<ChapterTree
selectedId={selectedChapterId}
bookId={Number(bookId)}
isLoading={loading}
title={title}
chapterTreeData={treeData}
onUpdate={handleChapterUpdate}
onAdd={handleAddChapter}
onEdit={handleEditChapter}
onDelete={handleDeleteChapter}
onSelect={handleSelectChapter}
onOrderChange={handleChangeOrder}
refreshTreeData={getChapterData}
/>
</div>
<div className={styles['right-box']}>
{selectedChapter ? (
<div className="chapter-detail">
<h3></h3>
<p>
<strong>:</strong> {selectedChapter.name}
</p>
<p>
<strong>ID:</strong> {selectedChapter.id}
</p>
</div>
) : (
<div className={styles['chapter-detail']}>
<h3></h3>
<p></p>
</div>
)}
{/*{selectedChapter ? (*/}
<EnhancedTextbookEditor
chapterId={'1'}
chapterTitle={'xuande'}
chapterId={selectedChapter?.id || 22}
chapterTitle={selectedChapter?.name || '测试数据'}
initialContent="请编写内容"
onSave={onSave}
onContentChange={onContentChange}
></EnhancedTextbookEditor>
{/* : (
<div></div>
)}*/}
</div>
</div>
</div>

View File

@ -0,0 +1,194 @@
import React, { useState, useEffect } from 'react';
import { Modal, Form, Input, message, Space, Spin, type FormProps, Select } from 'antd';
import { useTranslation } from 'react-i18next';
import { InboxOutlined } from '@ant-design/icons';
import Dragger from 'antd/es/upload/Dragger';
import TextArea from 'antd/es/input/TextArea';
interface ModalPropsType {
isOpen: boolean;
bookId: any;
onCancel: () => void;
isEdit: boolean;
resourceId: number;
typeOptions: any;
}
const CreateResourceModal = (props: ModalPropsType) => {
const { t } = useTranslation();
const { isOpen, onCancel, bookId, resourceId, isEdit, typeOptions } = props;
const [form] = Form.useForm();
const [spinInit, setSpinInit] = useState(false);
useEffect(() => {
setSpinInit(true);
if (isEdit && resourceId) {
getDetail();
} else {
setSpinInit(false);
}
}, [form, isEdit]);
const getDetail = () => {
/* if () {
form.setFieldsValue({
total_progress: hour.duration,
});
}*/
setSpinInit(false);
};
const onFinish = (values: any) => {
console.log('表单提交:', values);
const { name = '', desc = '', chapterId = '', type = '' } = values;
try {
if (isEdit) {
/*UpdateTextbookApi(
editId,
values.title,
thumb,
values.short_desc,
values.author,
values.major,
dep_ids,
group_ids,
user_ids,
values.publish_time,
values.publish_unit,
values.create_time,
values.isbn
)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
})
.catch((e) => {
setLoading(false);
});*/
} else {
/* UpdateTextbookApi(
editId,
values.title,
thumb,
values.short_desc,
values.author,
values.major,
dep_ids,
group_ids,
user_ids,
values.publish_time,
values.publish_unit,
values.create_time,
values.isbn
)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
})
.catch((e) => {
setLoading(false);
});*/
}
onCancel();
} catch (error) {
message.error(isEdit ? '更新失败' : '新增失败');
}
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
//弹窗确认
const handleOk = async () => {
try {
await form.validateFields(); // 手动触发表单验证
await onFinish(form.getFieldsValue()); // 调用 onFinish 并传递当前表单的值
} catch (error) {
console.error('表单验证失败:', error);
} finally {
onCancel();
}
};
return (
<>
<Modal
title={isEdit ? t('textbook.resource.edit') : t('textbook.resource.upload')}
centered
forceRender
open={isOpen}
width={580}
onOk={() => form.submit()}
onCancel={() => onCancel()}
maskClosable={false}
>
{spinInit && (
<div className="float-left text-center mt-30">
<Spin></Spin>
</div>
)}
<div
className="float-left m-24"
style={{ display: spinInit ? 'none' : 'block', marginBottom: 20 }}
>
<Form
form={form}
name="courseware-config"
labelCol={{ span: 4 }}
wrapperCol={{ span: 18 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
name="name"
label={t('textbook.resource.title1')}
rules={[{ required: true, message: t('textbook.resource.title1Placeholder') }]}
>
<Input allowClear placeholder={t('textbook.resource.title1Placeholder')} />
</Form.Item>
<Form.Item
name="desc"
label={t('textbook.resource.desc')}
rules={[{ required: true, message: t('textbook.resource.descPlaceholder') }]}
>
<TextArea allowClear placeholder={t('textbook.resource.descPlaceholder')} />
</Form.Item>
<Form.Item
name="chapterId"
label={t('textbook.resource.chapter')}
rules={[{ required: true, message: t('textbook.resource.chapterPlaceholder') }]}
>
<Input allowClear placeholder={t('textbook.resource.chapterPlaceholder')} />
</Form.Item>
<Form.Item
name="type"
label={t('textbook.resource.type')}
rules={[{ required: true, message: t('textbook.resource.typePlaceholder') }]}
>
<Select
allowClear
placeholder={t('textbook.resource.typePlaceholder')}
options={typeOptions}
/>
</Form.Item>
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">{t('textbook.resource.uploadTips')}</p>
<p className="ant-upload-hint">{t('textbook.resource.uploadTips2')}</p>
</Dragger>
</Form>
</div>
</Modal>
</>
);
};
export default CreateResourceModal;

View File

@ -2,8 +2,6 @@
import React from 'react';
import { Editor, useEditorState } from '@tiptap/react';
import { EditorState } from '../../../../types/editor';
import Highlight from '@tiptap/extension-highlight';
import TextAlign from '@tiptap/extension-text-align';
import {
AlignCenterOutlined,
AlignLeftOutlined,
@ -86,25 +84,25 @@ const EnhancedToolbar: React.FC<EnhancedToolbarProps> = ({ editor, onFileUpload
disabled: !editorState.canStrike,
active: editorState.isStrike,
},
{
/*{
icon: '</>',
title: '行内代码',
action: () => editor.chain().focus().toggleCode().run(),
disabled: !editorState.canCode,
active: editorState.isCode,
},
},*/
],
},
{
name: 'heading',
buttons: [
{
/*{
icon: '正文',
title: '正文',
action: () => editor.chain().focus().setParagraph().run(),
disabled: false,
active: editorState.isParagraph,
},
},*/
{
icon: 'H1',
title: '标题1',
@ -202,7 +200,7 @@ const EnhancedToolbar: React.FC<EnhancedToolbarProps> = ({ editor, onFileUpload
title: '分割线',
action: () => editor.chain().focus().setHorizontalRule().run(),
disabled: false,
active: true,
active: false,
},
],
},
@ -214,14 +212,14 @@ const EnhancedToolbar: React.FC<EnhancedToolbarProps> = ({ editor, onFileUpload
title: '撤销',
action: () => editor.chain().focus().undo().run(),
disabled: !editorState.canUndo,
active: !editorState.canUndo,
active: false,
},
{
icon: '↷',
title: '重做',
action: () => editor.chain().focus().redo().run(),
disabled: !editorState.canRedo,
active: !editorState.canRedo,
active: false,
},
],
},

View File

@ -3,8 +3,7 @@
display: flex;
flex-direction: column;
height: 100%;
background: white;
border: 1px solid #e1e5e9;
background: #f9fafb;
border-radius: 8px;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@ -38,26 +37,26 @@
color: #6c757d;
}
/* 编辑器头部 */
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #f8f9fa;
background: #fff;
border-bottom: 1px solid #e1e5e9;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.1);
}
.header-left .chapter-title {
margin: 0;
font-size: 18px;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.header-left .chapter-id {
font-size: 12px;
color: #6c757d;
font-size: 14px;
color: #9daabe;
margin-top: 4px;
}
@ -116,11 +115,30 @@
border-color: #1e7e34;
}
/*编辑区域*/
.editor-main-box{
background: #8F8F8F;
position: relative;
margin: 20px;
height: 640px;
overflow-y: scroll;
overflow-x: 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;
}
/* 工具栏 */
.toolbar-container {
background: #f8f9fa;
border-bottom: 1px solid #e1e5e9;
padding: 12px 16px;
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
.enhanced-toolbar {
@ -190,7 +208,7 @@
.editor-content {
outline: none;
padding: 20px;
min-height: 500px;
min-height: 510px;
font-size: 14px;
line-height: 1.6;
color: #333;

View File

@ -9,6 +9,15 @@ 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,
@ -37,8 +46,11 @@ const EnhancedTextbookEditor: React.FC<EditorProps> = ({
onContentChange,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [wordCount, setWordCount] = useState(0);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const { t } = useTranslation();
// 保留
/* const [wordCount, setWordCount] = useState(0);
const [lastSaved, setLastSaved] = useState<Date | null>(null);*/
// 初始化编辑器
const editor = useEditor({
@ -55,7 +67,7 @@ const EnhancedTextbookEditor: React.FC<EditorProps> = ({
const text = editor.getText();
// 更新统计
setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length);
// setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length);
// 通知父组件
onContentChange(chapterId, content);
@ -66,7 +78,7 @@ const EnhancedTextbookEditor: React.FC<EditorProps> = ({
if (isEditing) {
const content = editor.getHTML();
onSave(chapterId, content);
setLastSaved(new Date());
// setLastSaved(new Date());
setIsEditing(false);
}
},
@ -92,7 +104,7 @@ const EnhancedTextbookEditor: React.FC<EditorProps> = ({
// 更新统计
const text = editor.getText();
setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length);
// setWordCount(text.split(/\s+/).filter((word) => word.length > 0).length);
}
}, [chapterId, initialContent, chapterTitle, editor]);
@ -100,8 +112,9 @@ const EnhancedTextbookEditor: React.FC<EditorProps> = ({
const handleManualSave = useCallback(() => {
if (editor && isEditing) {
const content = editor.getHTML();
onSave(chapterId, content);
setLastSaved(new Date());
// setLastSaved(new Date());
setIsEditing(false);
}
}, [editor, chapterId, isEditing, onSave]);
@ -115,11 +128,16 @@ const EnhancedTextbookEditor: React.FC<EditorProps> = ({
});
};
// 预览
const handlePreview = () => {
alert('preview');
};
if (!chapterId) {
return (
<div className="enhanced-textbook-editor empty-state">
<div className="empty-content">
<div className="empty-icon">📚</div>
<div className="empty-icon"></div>
<h3></h3>
<p></p>
</div>
@ -137,10 +155,10 @@ const EnhancedTextbookEditor: React.FC<EditorProps> = ({
<div className="editor-header">
<div className="header-left">
<h2 className="chapter-title">{chapterTitle}</h2>
<span className="chapter-id">ID: {chapterId}</span>
<span className="chapter-id">: {chapterId}</span>
</div>
<div className="header-right">
<div className="editor-stats">
{/*<div className="editor-stats">
<span className="word-count">: {wordCount}</span>
{isEditing && <span className="editing-indicator"> </span>}
<span className="last-saved">: {formatTime(lastSaved)}</span>
@ -151,24 +169,44 @@ const EnhancedTextbookEditor: React.FC<EditorProps> = ({
disabled={!isEditing}
>
{isEditing ? '保存更改' : '已保存'}
</button>
</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="toolbar-container">
<EnhancedToolbar editor={editor} />
</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>TipTap v3 </span>
{/* 编辑器内容 */}
<div className="editor-container">
<EditorContent editor={editor} />
</div>
{/* 状态栏 */}
<div className="editor-status">
<div className="status-info">
<span> </span>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,231 @@
import { useEffect, useState } from 'react';
import { Upload, message, Image } from 'antd';
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import type { UploadChangeParam } from 'antd/es/upload';
import type { RcFile, UploadFile, UploadProps } from 'antd/es/upload/interface';
import { useTranslation } from 'react-i18next';
import config from '../../../../js/config';
import { getTenant, getToken } from '../../../../utils';
import defaultThumb1 from '../../../../assets/thumb/thumb1.png';
interface IProps {
categoryIds: number[];
allowedFileTypes?: string[];
type: string;
poster: string;
label: string;
note: string;
value: string;
onChange?: (value: string) => void;
ImageWidth?: number;
ImageHeight?: number;
ImageBorderRadius?: number;
allUrl: string;
isUploading: boolean;
setIsUploading: (value: boolean) => void;
}
export const ThumbUpload = (props: IProps) => {
const { t } = useTranslation();
const {
ImageWidth = 210,
ImageHeight = 297,
ImageBorderRadius = 5,
isUploading,
setIsUploading,
} = props;
const [loading, setLoading] = useState(false);
const [imageUrl, setImageUrl] = useState<string>(props.value || '');
useEffect(() => {
setImageUrl(props.value);
}, [props.value]);
const beforeUpload = (file: RcFile) => {
// 检查文件类型
const isValidType = props.allowedFileTypes
? props.allowedFileTypes.includes(file.type)
: ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'].includes(file.type);
if (!isValidType) {
message.error('只能上传图片文件!');
return false;
}
// 检查文件大小 (2MB)
const isLt12M = file.size / 1024 / 1024 < 12;
if (!isLt12M) {
message.error('图片大小不能超过 12MB!');
return false;
}
return true;
};
const handleChange: UploadProps['onChange'] = async (info: UploadChangeParam<UploadFile>) => {
if (info.file.status === 'uploading') {
setLoading(true);
setIsUploading(true);
return;
}
if (info.file.status === 'done') {
const url = info.file.response?.data?.url || defaultThumb1; //返回的图片地址
const path = info.file.response?.data?.path; //返回的图片地址
console.log(path, '>>>');
console.log(url, 'info.file.response', info.file.response);
if (url) {
setImageUrl(url);
setLoading(false);
setIsUploading(false);
props.onChange?.(path);
message.success('上传成功');
}
}
if (info.file.status === 'error') {
setLoading(false);
setIsUploading(false);
message.error('上传失败');
}
};
const customRequest = async (options: any) => {
const { file, onProgress, onSuccess, onError } = options;
try {
// 获取上传参数
let appUrl = config.app_url.replace(/\/+$/, '');
if (!appUrl.startsWith('http')) {
appUrl =
window.location.protocol +
'//' +
window.location.host +
(appUrl.startsWith('/') ? appUrl : '/' + appUrl);
}
// 小文件直接上传
// if (file.size <= 2 * 1024 * 1024) {
const formData = new FormData();
formData.append('file', file);
formData.append('category_ids', props.categoryIds.join(','));
formData.append('module', props.type);
formData.append('duration', '0');
const xhr = new XMLHttpRequest();
// 进度监听
if (onProgress) {
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
onProgress({ percent });
}
};
}
xhr.onload = () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
onSuccess(response, xhr);
} else {
onError(new Error('Upload failed'));
}
};
xhr.onerror = () => onError(new Error('Upload failed'));
xhr.open('POST', `${appUrl}/backend/v1/localUpload/upload`); //新改接口
xhr.setRequestHeader('Authorization', 'Bearer ' + getToken());
xhr.setRequestHeader('tenant-id', getTenant());
xhr.send(formData);
// }
} catch (error) {
onError(error);
}
};
const uploadButton = (
<div>
{loading ? <LoadingOutlined /> : <PlusOutlined />}
<div style={{ marginTop: 8 }}>{props.label}</div>
</div>
);
const handleRemove = (e: React.MouseEvent) => {
e.stopPropagation(); // 阻止事件冒泡
setImageUrl('');
props.onChange?.(''); // 清空图片
};
return (
<Upload
name="avatar"
listType="picture-card"
className={'avatar-uploader'}
showUploadList={false}
beforeUpload={beforeUpload}
onChange={handleChange}
style={{
width: ImageWidth,
height: ImageHeight,
borderRadius: ImageBorderRadius,
}}
customRequest={customRequest}
accept={props.allowedFileTypes?.join(',') || 'image/jpeg,image/jpg,image/png,image/gif'}
disabled={!!imageUrl}
>
{imageUrl ? (
<div
style={{
position: 'relative',
width: ImageWidth,
height: ImageHeight,
objectFit: 'cover',
borderRadius: ImageBorderRadius,
}}
>
<Image
draggable={false}
src={props?.allUrl || imageUrl}
alt="avatar"
preview={false}
style={{
width: ImageWidth,
height: ImageHeight,
objectFit: 'cover',
borderRadius: ImageBorderRadius,
}}
fallback={defaultThumb1}
/>
{/* 删除按钮 */}
<button
style={{
position: 'absolute',
top: 4,
right: 4,
background: 'rgba(0,0,0,0.5)',
color: 'white',
border: 'none',
borderRadius: '50%',
width: 20,
height: 20,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
}}
onClick={handleRemove}
type="button"
>
×
</button>
</div>
) : (
uploadButton
)}
</Upload>
);
};
// 使用示例:

View File

@ -1,16 +1,24 @@
import { Modal, Form, Input, Select } from 'antd';
import { useEffect } from 'react';
import { Modal, Form, Input, Select, 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';
const { Option } = Select;
interface Option {
value: string | number;
label: string;
children?: Option[];
}
interface ChapterItem {
id: string | number;
name: string;
title?: string;
level: number;
parent_chain: string;
parent_id: string;
parentId: string;
sort: number;
children?: ChapterItem[];
}
@ -21,114 +29,208 @@ interface ChapterItemModel {
name: string;
from_scene: number;
parent_chain: string;
parent_id: number;
parentId: number;
sort: number;
updated_at: string;
}
interface ChapterModalProps {
visible: boolean;
mode: 'add' | 'edit';
initialData?: {
title?: string;
level?: number;
};
parentChapter?: ChapterItemModel | null;
onOk: (values: { title: string; level: number }) => void;
bookId: number;
isEdit: boolean;
editData?: any;
onCancel: () => void;
confirmLoading?: boolean;
onSuccess: () => void;
}
export const ChapterModal: React.FC<ChapterModalProps> = ({
visible,
mode,
initialData,
parentChapter,
onOk,
bookId,
isEdit,
editData,
onCancel,
confirmLoading = false,
onSuccess,
}) => {
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
useEffect(() => {
if (visible) {
if (mode === 'edit' && initialData) {
form.setFieldsValue(initialData);
} else {
form.resetFields();
let defaultLevel = 1;
if (parentChapter) {
defaultLevel = parentChapter.parent_chain.split(',').length + 1;
}
form.setFieldsValue({
level: defaultLevel,
...initialData,
});
}
setInit(true);
if (isEdit && editData) {
form.setFieldsValue(editData);
} else {
form.resetFields();
let defaultLevel = 1;
form.setFieldsValue({
level: defaultLevel,
...editData,
});
}
}, [visible, mode, initialData, parentChapter, form]);
setInit(false);
getParams();
}, [visible, isEdit, editData, form]);
const handleOk = async () => {
try {
const values = await form.validateFields();
onOk(values);
onFinish(values);
console.log(values, 'add values');
} catch (error) {
console.error('表单验证失败:', error);
}
};
const onFinish = (values: any) => {
if (loading) {
return;
}
setLoading(true);
if (isEdit && editingId) {
// 编辑模式:调用更新接口,保持自己的sort
EditChapterApi(editingId, values.name, parentId || 0, editData.sort, bookId)
.then((res: any) => {
setLoading(false);
message.success('更新成功');
onCancel();
if (onSuccess) {
onSuccess();
}
})
.catch(() => {
setLoading(false);
});
} else {
// 新建模式:调用新增接口
CreateChapterApi(values.name, parentId || 0, 0, bookId)
.then((res: any) => {
setLoading(false);
message.success('创建成功');
onCancel();
if (onSuccess) {
onSuccess();
}
})
.catch(() => {
setLoading(false);
});
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
const handleChange = (value: any) => {
if (value !== undefined) {
let it = value[value.length - 1];
setParentId(it);
} else {
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];
};
return (
<Modal
title={mode == 'edit' ? '编辑章节' : '添加章节'}
title={isEdit ? t('textbook.chapter.edit') : t('textbook.chapter.add')}
open={visible}
onOk={handleOk}
onCancel={handleCancel}
okText="确认"
cancelText="取消"
onOk={() => form.submit()}
onCancel={() => onCancel()}
okText={t('commen.okText')}
cancelText={t('commen.cancelText')}
confirmLoading={confirmLoading}
destroyOnHidden={true}
>
<Form form={form} layout="vertical" preserve={false}>
<Form.Item
name="parent_id"
label={t('textbook.chapter.parent')}
rules={[{ required: true, message: '请选择章节层级' }]}
{init && (
<div className="float-left text-center mt-30">
<Spin></Spin>
</div>
)}
<div className="float-left mt-24" style={{ display: init ? 'none' : 'block' }}>
<Form
form={form}
name="basic"
labelCol={{ span: 8 }}
initialValues={{ remember: true }}
onFinish={handleOk}
autoComplete="off"
layout="vertical"
preserve={false}
>
<Select placeholder="请选择章节层级" disabled={mode === 'edit'}>
<Option
value={1}
disabled={parentChapter && parentChapter.parent_chain.split(',').length >= 1}
>
</Option>
<Option
value={2}
disabled={!parentChapter || parentChapter.parent_chain.split(',').length === 2}
>
</Option>
<Option
value={3}
disabled={!parentChapter || parentChapter.parent_chain.split(',').length >= 3}
>
</Option>
</Select>
</Form.Item>
<Form.Item
name="title"
label="章节标题"
rules={[{ required: true, message: '请输入章节标题' }]}
>
<Input placeholder="请输入章节标题" />
</Form.Item>
</Form>
<Form.Item
name="parentId"
label={t('textbook.chapter.parent')}
rules={[{ required: true, message: t('textbook.chapter.parentTip') }]}
>
<Cascader
allowClear
placeholder={t('textbook.chapter.parentTip')}
onChange={handleChange}
options={chapters}
changeOnSelect
expand-trigger="hover"
displayRender={displayRender}
disabled={isEdit && editingId === 0} // 禁止编辑根分类的父级
/>
</Form.Item>
<Form.Item
name="name"
label={t('textbook.chapter.title')}
rules={[{ required: true, message: t('textbook.chapter.titleTip') }]}
>
<Input allowClear placeholder={t('textbook.chapter.titleTip')} />
</Form.Item>
</Form>
</div>
</Modal>
);
};

View File

@ -1,7 +1,7 @@
// chapterTree.module.less
.chapterTree {
display: block;
width: 330px;
width: 280px;
overflow: hidden;
background: white;
}
@ -10,12 +10,12 @@
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
width: 98%;
font-weight: bold;
border-bottom: 1px solid #cccccc;
padding: 16px 2px;
padding: 10px 2px;
box-sizing: border-box;
font-size:16px
font-size:14px
}
.bottom-tree-box {

View File

@ -24,6 +24,7 @@ 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;
@ -60,28 +61,15 @@ interface Option {
interface PropInterface {
chapterTreeData: ChaptersBoxModel;
isLoading: boolean;
bookId: number;
title: any;
selected?: any;
selectedId?: any;
onUpdate?: (keys: any, title: any) => void;
onAdd?: (parentId: string | number | null, level: number) => void;
onEdit?: (item: ChapterItem) => void;
onDelete?: (item: ChapterItem) => void;
onSelect?: (item: ChapterItem) => void;
onOrderChange?: (chapters: any[]) => void; // 新增:拖拽顺序改变回调
refreshTreeData: () => void;
}
export const ChapterTree = (props: PropInterface) => {
const {
chapterTreeData,
isLoading,
selected,
onUpdate,
onAdd,
onEdit,
onDelete,
onSelect,
onOrderChange,
} = props;
const { chapterTreeData, isLoading, selected, bookId, title, refreshTreeData } = props;
const permissions = useSelector((state: any) => state.loginUser.value.permissions);
const through = (p: string) => {
@ -93,10 +81,11 @@ export const ChapterTree = (props: PropInterface) => {
const navigate = useNavigate();
const { t } = useTranslation();
const [treeData, setTreeData] = useState<Option[]>([]);
const [loading, setLoading] = useState<boolean>(true);
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();
@ -126,11 +115,6 @@ export const ChapterTree = (props: PropInterface) => {
return styles.chapterLevel3;
}
};
const onDragEnter: TreeProps['onDragEnter'] = (info) => {
console.log(info);
// expandedKeys 需要受控时设置
// setExpandedKeys(info.expandedKeys)
};
const onDrop: TreeProps['onDrop'] = (info) => {
const dropKey = info.node.key; //目标节点key
@ -229,7 +213,7 @@ export const ChapterTree = (props: PropInterface) => {
};
const submitChildDrop = (key: any, pid: any, ids: any) => {
department.dropDiffClass(key, pid, ids.ids).then((res: any) => {
DropDiffClassApi(key, pid, ids.ids, bookId).then((res: any) => {
console.log('ok');
});
};
@ -258,7 +242,7 @@ export const ChapterTree = (props: PropInterface) => {
const result = checkDropArr(data, key);
if (result) {
if (isTop) {
department.dropSameClass(result.ids).then((res: any) => {
DropSameClassApi(result.ids, bookId).then((res: any) => {
console.log('ok');
});
} else {
@ -336,7 +320,10 @@ export const ChapterTree = (props: PropInterface) => {
const name = (
<div className={styles['tree-node-content']}>
<div className={styles['tree-title-content']}>
<div className={`${styles['tree-title-elli']} ${getLevelClassName(level)}`}>
<div
title={item.name}
className={`${styles['tree-title-elli']} ${getLevelClassName(level)}`}
>
{item.name}
</div>
</div>
@ -506,6 +493,9 @@ export const ChapterTree = (props: PropInterface) => {
// 打开编辑章节弹窗
const handleEdit = (chapter: ChapterItemModel) => {
console.log('编辑章节:', chapter);
/* const result = getDetailByClassId(treeListData, editSelectId);
setEditSelectItem(result);*/
setEditingChapter(chapter);
setParentChapter(chapter);
setModalVisible(true);
@ -532,10 +522,6 @@ export const ChapterTree = (props: PropInterface) => {
return node.title || '';
};
const onExpand = (selectedKeys: any, info: any) => {
// 处理展开逻辑
};
// 关闭弹窗
const handleModalCancel = () => {
setModalVisible(false);
@ -543,26 +529,18 @@ export const ChapterTree = (props: PropInterface) => {
setParentChapter(null);
};
const handleSubmit = (values: any) => {
if (loading) {
return;
}
setLoading(true);
department
.storeDepartment(values.name, values.parent_id || 0, 0)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
handleModalCancel();
})
.catch((e) => {
setLoading(false);
});
const handleSubmit = () => {
/* setEditSelectItem(null);
setEditSelectId(null);
setIsEditClass(false);*/
setModalVisible(false);
refreshTreeData();
};
return (
<div className={styles.chapterTree} id={'chapter-tree-container'}>
<div className={styles.chapterTitle}>
<div>{t('textbook.chapter.management')}</div>
<div>{title}</div>
<div>
<Button
type="link"
@ -650,7 +628,6 @@ export const ChapterTree = (props: PropInterface) => {
onSelect={onSelectTree}
blockNode
treeData={treeData}
onExpand={onExpand}
switcherIcon={
<CaretDownFilled style={{ color: '#a89f9e', fontSize: 20, paddingTop: 10 }} />
}
@ -658,24 +635,17 @@ export const ChapterTree = (props: PropInterface) => {
</div>
)}
</div>
<ChapterModal
visible={!dragEnabled && modalVisible}
mode={editingChapter ? 'edit' : 'add'}
initialData={
editingChapter
? {
title: editingChapter.name,
level: editingChapter?.parent_chain?.split(',').length
? editingChapter?.parent_chain?.split(',').length + 1
: 1,
}
: undefined
}
parentChapter={parentChapter}
onOk={handleSubmit}
onCancel={handleModalCancel}
confirmLoading={submitLoading}
/>
{modalVisible && !dragEnabled && (
<ChapterModal
visible={!dragEnabled && modalVisible}
isEdit={isEdit}
editData={editingChapter}
bookId={bookId}
onCancel={handleModalCancel}
confirmLoading={submitLoading}
onSuccess={refreshTreeData}
/>
)}
</div>
);
};

View File

@ -1,85 +1,27 @@
import React, { useState, useEffect } from 'react';
import {
Space,
Radio,
Button,
Drawer,
Form,
Input,
Modal,
message,
Image,
Tag,
DatePicker,
} from 'antd';
import styles from './createTextbook.module.less';
import { SelectRange, UploadImageButton } from '../../../compenents';
import defaultThumb1 from '../../../assets/thumb/thumb1.png';
import defaultThumb2 from '../../../assets/thumb/thumb2.png';
import defaultThumb3 from '../../../assets/thumb/thumb3.png';
import { Space, Button, Drawer, Form, Input, message, Tag, DatePicker, Spin } from 'antd';
import { SelectRange } from '../../../compenents';
import { useTranslation } from 'react-i18next';
import moment from 'moment/moment';
import { createTextbook } from '../../../api/textbook';
import { CreateTextbookApi, GetTextbookDetailApi, UpdateTextbookApi } from '../../../api/textbook';
import { ThumbUpload } from './Upload/UploadThumb';
import dayjs from 'dayjs';
const { TextArea } = Input;
const { confirm } = Modal;
// 类型定义
interface KnowledgePoint {
id: number;
name: string;
content?: string;
sortOrder: number;
}
interface Chapter {
id: number;
name: string;
level: 1 | 2 | 3;
parentId: number | null;
sortOrder: number;
children?: Chapter[];
knowledgePoints?: KnowledgePoint[];
hours: CourseHourModel[];
}
interface CourseCreateProps {
cateIds?: any;
isEdit: boolean;
open: boolean;
onCancel: () => void;
}
interface Option {
value: string | number;
title: string;
children?: Option[];
}
interface TeacherModel {
label: string;
value: number;
}
interface AttachmentDataModel {
rid: number;
name: string;
type: string;
size: number;
}
interface CourseHourModel {
rid: number;
name: string;
type: string;
duration: number;
extra?: any;
editId?: any;
}
// @ts-ignore
export const CreateTextbook: React.FC<CourseCreateProps> = ({ open, onCancel }) => {
export const CreateTextbook: React.FC<CourseCreateProps> = ({ editId, isEdit, open, onCancel }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(false);
const [thumb, setThumb] = useState<string>(defaultThumb1); // 封面
const [thumb, setThumb] = useState<string>(''); // 封面
const [allUrl, setAllUrl] = useState<string>(''); // 封面全部路径
// 范围指派
const [depIds, setDepIds] = useState<number[]>([]);
const [groupIds, setGroupIds] = useState<number[]>([]);
@ -89,28 +31,84 @@ export const CreateTextbook: React.FC<CourseCreateProps> = ({ open, onCancel })
const [users, setUsers] = useState<any[]>([]);
const [idsVisible, setIdsVisible] = useState<boolean>(false);
const [spinInit, setSpinInit] = useState<boolean>(false);
const [isUploading, setIsUploading] = useState<boolean>(false);
const InitForm = () => {
form.setFieldsValue({
title: '',
thumb: undefined,
isbn: '',
dep_ids: undefined,
short_desc: '',
publish_time: undefined,
major: undefined,
author: undefined,
publish_unit: undefined,
create_time: undefined,
});
setThumb('');
setAllUrl('');
setDepIds([]);
setDeps([]);
setGroupIds([]);
setGroups([]);
setUserIds([]);
setUsers([]);
};
useEffect(() => {
if (open) {
form.setFieldsValue({
creditType: 0,
title: '',
thumb: -1,
ids: undefined,
isRequired: 1,
short_desc: '',
hasChapter: 0,
drag: 0,
hangUp: 0,
});
setThumb(defaultThumb1);
setDepIds([]);
setDeps([]);
setGroupIds([]);
setGroups([]);
setUserIds([]);
setUsers([]);
if (editId && isEdit) {
setSpinInit(true);
getDetail();
} else if (!isEdit && open) {
InitForm();
}
}, [form, open]);
}, [form, open, isEdit, editId]);
useEffect(() => {
return () => {
InitForm();
};
}, []);
// Edit信息回显
const getDetail = () => {
GetTextbookDetailApi(editId).then((res: any) => {
form.setFieldsValue({
title: res.data.textbook.title,
thumb: res.data.textbook.thumb,
short_desc: res.data.textbook.shortDesc,
author: res.data.textbook.author,
major: res.data.textbook.major,
publish_time: res.data.textbook.publishTime ? dayjs(res.data.textbook.publishTime) : '',
publish_unit: res.data.textbook.publishUnit,
create_time: res.data.textbook.createTime ? dayjs(res.data.textbook.createTime) : '',
isbn: res.data.textbook.isbn,
});
const deps = res.data.deps;
if (deps && JSON.stringify(deps) !== '{}') {
getDepsDetail(deps);
}
const groups = res.data.groups;
if (groups && JSON.stringify(groups) !== '{}') {
getGroupsDetail(groups);
}
const users = res.data.users;
if (users && JSON.stringify(users) !== '{}') {
getUsersDetail(users);
}
if (
(deps && JSON.stringify(deps) !== '{}') ||
(groups && JSON.stringify(groups) !== '{}') ||
(users && JSON.stringify(users) !== '{}')
) {
form.setFieldsValue({ ids: [1, 2] });
}
setThumb(res.data.textbook.thumb);
setAllUrl(res.data.textbook.allUrl);
setSpinInit(false);
});
};
const onFinish = (values: any) => {
if (loading) return;
@ -119,27 +117,58 @@ export const CreateTextbook: React.FC<CourseCreateProps> = ({ open, onCancel })
const user_ids: any[] = userIds;
// 接口位置
setLoading(true);
createTextbook(
values.title,
values.thumb,
values.short_desc,
values.author,
values.major,
dep_ids,
group_ids,
user_ids,
values.publish_time,
values.publish_unit,
values.create_time
)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
})
.catch((e) => {
setLoading(false);
});
if (isEdit) {
console.log(thumb, 'thumb');
console.log('thumb 类型:', typeof thumb);
UpdateTextbookApi(
editId,
values.title,
thumb,
values.short_desc,
values.author,
values.major,
dep_ids,
group_ids,
user_ids,
values.publish_time,
values.publish_unit,
values.create_time,
values.isbn
)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
})
.catch((e) => {
setLoading(false);
});
} else {
console.log(thumb, 'thumb');
console.log('thumb 类型:', typeof thumb);
CreateTextbookApi(
values.title,
thumb,
values.short_desc,
values.author,
values.major,
dep_ids,
group_ids,
user_ids,
values.publish_time,
values.publish_unit,
values.create_time,
values.isbn
)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
})
.catch((e) => {
setLoading(false);
});
}
};
const onFinishFailed = (errorInfo: any) => {
@ -147,51 +176,89 @@ export const CreateTextbook: React.FC<CourseCreateProps> = ({ open, onCancel })
};
const disabledDate = (current: any) => {
return current && current >= moment().add(0, 'days'); // 选择时间要大于等于当前天。若今天不能被选择,去掉等号即可。
return current && current >= moment().add(0, 'days');
};
const getDepsDetail = (deps: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(deps).map((v, i) => {
arr.push(Number(v));
arr2.push({
key: Number(v),
title: {
props: {
children: deps[v],
},
},
});
});
setDepIds(arr);
setDeps(arr2);
};
const getGroupsDetail = (groups: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(groups).map((v, i) => {
arr.push(Number(v));
arr2.push({
key: Number(v),
title: {
props: {
children: groups[v],
},
},
});
});
setGroupIds(arr);
setGroups(arr2);
};
const getUsersDetail = (users: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(users).map((v, i) => {
arr.push(Number(v));
arr2.push({
id: Number(v),
name: users[v],
});
});
setUserIds(arr);
setUsers(arr2);
};
return (
<>
{open ? (
<Drawer
title={t('course.createTextbook')}
onClose={onCancel}
maskClosable={false}
open={true}
footer={
<Space className="j-r-flex">
<Button onClick={() => onCancel()}>{t('commen.drawerCancel')}</Button>
<Button loading={loading} onClick={() => form.submit()} type="primary">
{t('commen.drawerOk')}
</Button>
</Space>
}
width={700}
>
<div className="float-left mt-24">
<SelectRange
defaultDepIds={depIds}
defaultGroupIds={groupIds}
defaultUserIds={userIds}
defaultDeps={deps}
defaultGroups={groups}
defaultUsers={users}
open={idsVisible}
onCancel={() => setIdsVisible(false)}
onSelected={(selDepIds, selDeps, selGroupIds, selGroups, selUserIds, selUsers) => {
setDepIds(selDepIds);
setDeps(selDeps);
setGroupIds(selGroupIds);
setGroups(selGroups);
setUserIds(selUserIds);
setUsers(selUsers);
form.setFieldsValue({
ids: selDepIds.concat(selGroupIds).concat(selUserIds),
});
setIdsVisible(false);
}}
/>
{/* 表单 */}
<Drawer
title={isEdit ? t('course.updateTextbook') : t('course.createTextbook')}
onClose={onCancel}
maskClosable={false}
open={open}
footer={
<Space className="j-r-flex">
<Button onClick={() => onCancel()} disabled={isUploading}>
{t('commen.drawerCancel')}
</Button>
<Button
loading={loading}
disabled={isUploading}
onClick={() => form.submit()}
type="primary"
>
{t('commen.drawerOk')}
</Button>
</Space>
}
width={700}
>
<div className="float-left mt-24">
{spinInit ? (
<div className="text-center mt-30">
<Spin></Spin>
</div>
) : (
<Form
form={form}
name="create-basic"
@ -221,91 +288,23 @@ export const CreateTextbook: React.FC<CourseCreateProps> = ({ open, onCancel })
rules={[{ required: true, message: t('textbook.create.thumbPlaceholder') }]}
>
<div className="d-flex">
<Image
src={thumb}
width={160}
height={120}
style={{ borderRadius: 6 }}
preview={false}
<ThumbUpload
categoryIds={[]}
allowedFileTypes={['image/jpeg', 'image/png']}
type="IMAGE"
poster=""
label="上传图片"
note="支持 JPG、PNG 格式,大小不超过 2MB"
value={thumb}
onChange={setThumb}
ImageBorderRadius={5}
ImageHeight={290}
ImageWidth={217}
allUrl={allUrl}
isUploading={isUploading}
setIsUploading={setIsUploading}
/>
<div className="c-flex ml-8 flex-1">
<div className="d-flex mb-28">
<div
className={
thumb === defaultThumb1
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb1);
form.setFieldsValue({
thumb: -1,
});
}}
>
<Image
src={defaultThumb1}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb2
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb2);
form.setFieldsValue({
thumb: -2,
});
}}
>
<Image
src={defaultThumb2}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb3
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb3);
form.setFieldsValue({
thumb: -3,
});
}}
>
<Image
src={defaultThumb3}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
</div>
<div className="d-flex">
<UploadImageButton
text={t('course.edit.thumbText')}
isDefault
onSelected={(url, id) => {
setThumb(url);
form.setFieldsValue({ thumb: id });
}}
></UploadImageButton>
<span className="helper-text ml-16">{t('textbook.create.thumbTip')}</span>
</div>
</div>
<span className="helper-text ml-16">{t('textbook.create.thumbTip')}</span>
</div>
</Form.Item>
{/*范围指派*/}
@ -437,6 +436,17 @@ export const CreateTextbook: React.FC<CourseCreateProps> = ({ open, onCancel })
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.isbn')}
name="isbn"
rules={[{ required: true, message: t('textbook.create.isbnPlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('textbook.create.isbnPlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.author')}
name="author"
@ -470,6 +480,7 @@ export const CreateTextbook: React.FC<CourseCreateProps> = ({ open, onCancel })
allowClear
format="YYYY-MM-DD"
disabledDate={disabledDate}
showNow={false}
/>
</Form.Item>
<Form.Item
@ -482,14 +493,37 @@ export const CreateTextbook: React.FC<CourseCreateProps> = ({ open, onCancel })
rows={6}
placeholder={t('textbook.create.descPlaceholder')}
allowClear
maxLength={200}
maxLength={300}
autoSize={{ minRows: 6, maxRows: 6 }}
/>
</Form.Item>
</Form>
</div>
</Drawer>
) : null}
)}
<SelectRange
defaultDepIds={depIds}
defaultGroupIds={groupIds}
defaultUserIds={userIds}
defaultDeps={deps}
defaultGroups={groups}
defaultUsers={users}
open={idsVisible}
onCancel={() => setIdsVisible(false)}
onSelected={(selDepIds, selDeps, selGroupIds, selGroups, selUserIds, selUsers) => {
setDepIds(selDepIds);
setDeps(selDeps);
setGroupIds(selGroupIds);
setGroups(selGroups);
setUserIds(selUserIds);
setUsers(selUsers);
form.setFieldsValue({
ids: selDepIds.concat(selGroupIds).concat(selUserIds),
});
setIdsVisible(false);
}}
/>
{/* 表单 */}
</div>
</Drawer>
</>
);
};

View File

@ -1,22 +0,0 @@
.thumb-item {
width: 80px;
height: 60px;
cursor: pointer;
margin-right: 8px;
border-radius: 6px;
&:last-child {
margin-right: 0;
}
}
.thumb-item-avtive {
width: 80px;
height: 60px;
border: 2px solid #ff4d4f;
cursor: pointer;
margin-right: 8px;
border-radius: 8px;
&:last-child {
margin-right: 0;
}
}

View File

@ -1,746 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Space,
Radio,
Button,
Drawer,
Form,
TreeSelect,
Input,
message,
Image,
Spin,
Select,
DatePicker,
Tag,
Switch,
InputNumber,
} from 'antd';
import styles from './update.module.less';
import { course, teacher } from '../../../api/index';
import { UploadImageButton, SelectRange } from '../../../compenents';
import defaultThumb1 from '../../../assets/thumb/thumb1.png';
import defaultThumb2 from '../../../assets/thumb/thumb2.png';
import defaultThumb3 from '../../../assets/thumb/thumb3.png';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import moment from 'moment';
interface PropInterface {
id: number;
open: boolean;
onCancel: () => void;
}
interface Option {
value: string | number;
title: string;
children?: Option[];
}
type selTeacherModel = {
label: string;
value: number;
};
export const CourseUpdate: React.FC<PropInterface> = ({ id, open, onCancel }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [init, setInit] = useState(true);
const [loading, setLoading] = useState(false);
const [categories, setCategories] = useState<Option[]>([]);
const [thumb, setThumb] = useState('');
const [teachers, setTeachers] = useState<selTeacherModel[]>([]);
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const [depIds, setDepIds] = useState<number[]>([]);
const [groupIds, setGroupIds] = useState<number[]>([]);
const [userIds, setUserIds] = useState<number[]>([]);
const [deps, setDeps] = useState<any[]>([]);
const [groups, setGroups] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [idsVisible, setIdsVisible] = useState(false);
const [drag, setDrag] = useState(0);
const [credit, setCredit] = useState(0);
useEffect(() => {
setInit(true);
if (id === 0) {
return;
}
if (open) {
form.setFieldsValue({
ids: undefined,
});
setDepIds([]);
setDeps([]);
setGroupIds([]);
setGroups([]);
setUserIds([]);
setUsers([]);
getTeachers();
getCategory();
getDetail();
}
}, [form, id, open]);
const getCategory = () => {
course.createCourse().then((res: any) => {
const categories = res.data.categories;
if (JSON.stringify(categories) !== '{}') {
const new_arr: any = checkArr(categories, 0, null);
setCategories(new_arr);
}
});
};
const getTeachers = () => {
teacher.list(1, 100000, '', '', '').then((res: any) => {
const arr = [];
const roles: any = res.data.result.data;
for (let i = 0; i < roles.length; i++) {
arr.push({
label: roles[i].name,
value: roles[i].id,
});
}
setTeachers(arr);
});
};
const getDetail = () => {
course.course(id).then((res: any) => {
let teacherIds = [];
if (res.data.teachers && res.data.teachers.length > 0) {
teacherIds = res.data.teachers[0].id;
}
const chapterType = res.data.chapters.length > 0 ? 1 : 0;
form.setFieldsValue({
title: res.data.course.title,
thumb: res.data.course.thumb,
category_ids: res.data.category_ids,
isRequired: res.data.course.is_required,
short_desc: res.data.course.short_desc,
hasChapter: chapterType,
teacherIds: teacherIds,
sort_at: res.data.course.sort_at ? dayjs(res.data.course.sort_at) : '',
});
const deps = res.data.deps;
if (deps && JSON.stringify(deps) !== '{}') {
getDepsDetail(deps);
}
const groups = res.data.groups;
if (groups && JSON.stringify(groups) !== '{}') {
getGroupsDetail(groups);
}
const users = res.data.users;
if (users && JSON.stringify(users) !== '{}') {
getUsersDetail(users);
}
if (
(deps && JSON.stringify(deps) !== '{}') ||
(groups && JSON.stringify(groups) !== '{}') ||
(users && JSON.stringify(users) !== '{}')
) {
form.setFieldsValue({ ids: [1, 2] });
}
setResourceUrl(res.data.resource_url);
setThumb(
res.data.course.thumb === -1
? defaultThumb1
: res.data.course.thumb === -2
? defaultThumb2
: res.data.course.thumb === -3
? defaultThumb3
: res.data.resource_url[res.data.course.thumb]
);
if (res.data.course.extra) {
const obj = JSON.parse(res.data.course.extra).rules;
form.setFieldsValue({
drag: Number(obj.drag),
hangUp: Number(obj.hang_up),
});
setDrag(Number(obj.drag));
const key = obj.credit;
if (key && key > 0) {
form.setFieldsValue({
creditType: 1,
credit: key,
});
setCredit(1);
} else {
form.setFieldsValue({
creditType: 0,
});
setCredit(0);
}
} else {
form.setFieldsValue({
creditType: 0,
});
setCredit(0);
}
setInit(false);
});
};
const getDepsDetail = (deps: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(deps).map((v, i) => {
arr.push(Number(v));
arr2.push({
key: Number(v),
title: {
props: {
children: deps[v],
},
},
});
});
setDepIds(arr);
setDeps(arr2);
};
const getGroupsDetail = (groups: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(groups).map((v, i) => {
arr.push(Number(v));
arr2.push({
key: Number(v),
title: {
props: {
children: groups[v],
},
},
});
});
setGroupIds(arr);
setGroups(arr2);
};
const getUsersDetail = (users: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(users).map((v, i) => {
arr.push(Number(v));
arr2.push({
id: Number(v),
name: users[v],
});
});
setUserIds(arr);
setUsers(arr2);
};
const getNewTitle = (title: any, id: number, counts: any) => {
if (counts) {
const value = counts[id] || 0;
return title + '(' + value + ')';
} else {
return title;
}
};
const checkArr = (departments: any[], id: number, counts: any) => {
const arr = [];
for (let i = 0; i < departments[id].length; i++) {
if (!departments[departments[id][i].id]) {
arr.push({
title: getNewTitle(departments[id][i].name, departments[id][i].id, counts),
value: departments[id][i].id,
});
} else {
const new_arr: any = checkArr(departments, departments[id][i].id, counts);
arr.push({
title: getNewTitle(departments[id][i].name, departments[id][i].id, counts),
value: departments[id][i].id,
children: new_arr,
});
}
}
return arr;
};
const onFinish = (values: any) => {
if (loading) {
return;
}
const dep_ids: any[] = depIds;
const user_ids: any[] = userIds;
const group_ids: any[] = groupIds;
const teacherIds: any[] = [];
if (values.teacherIds > 0) {
teacherIds.push(values.teacherIds);
}
values.sort_at = moment(new Date(values.sort_at)).format('YYYY-MM-DD HH:mm:ss');
const extra = {
version: 'v1',
rules: {
drag: values.drag,
hang_up: values.hangUp,
scope: credit === 0 ? 'GLOBAL' : 'COURSE',
credit: credit === 0 ? 0 : values.credit,
},
};
setLoading(true);
course
.updateCourse(
id,
values.title,
values.thumb,
values.short_desc,
1,
values.isRequired,
dep_ids,
group_ids,
user_ids,
values.category_ids,
[],
[],
teacherIds,
values.sort_at,
JSON.stringify(extra)
)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
})
.catch((e) => {
setLoading(false);
});
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const disabledDate = (current: any) => {
return current && current >= moment().add(0, 'days'); // 选择时间要大于等于当前天。若今天不能被选择,去掉等号即可。
};
const onDragChange = (checked: boolean) => {
if (checked) {
form.setFieldsValue({ drag: 1 });
setDrag(1);
} else {
form.setFieldsValue({ drag: 0, hangUp: 0 });
setDrag(0);
}
};
const onHangUpChange = (checked: boolean) => {
if (checked) {
form.setFieldsValue({ hangUp: 1 });
} else {
form.setFieldsValue({ hangUp: 0 });
}
};
return (
<>
{open ? (
<Drawer
title={t('course.update')}
onClose={onCancel}
maskClosable={false}
open={true}
footer={
<Space className="j-r-flex">
<Button onClick={() => onCancel()}>{t('commen.drawerCancel')}</Button>
<Button loading={loading} onClick={() => form.submit()} type="primary">
{t('commen.drawerOk')}
</Button>
</Space>
}
width={634}
>
{init && (
<div className="float-left text-center mt-30">
<Spin></Spin>
</div>
)}
<div className="float-left mt-24" style={{ display: init ? 'none' : 'block' }}>
<SelectRange
defaultDepIds={depIds}
defaultGroupIds={groupIds}
defaultUserIds={userIds}
defaultDeps={deps}
defaultGroups={groups}
defaultUsers={users}
open={idsVisible}
onCancel={() => setIdsVisible(false)}
onSelected={(selDepIds, selDeps, selGroupIds, selGroups, selUserIds, selUsers) => {
setDepIds(selDepIds);
setDeps(selDeps);
setGroupIds(selGroupIds);
setGroups(selGroups);
setUserIds(selUserIds);
setUsers(selUsers);
form.setFieldsValue({
ids: selDepIds.concat(selGroupIds).concat(selUserIds),
});
setIdsVisible(false);
}}
/>
<Form
form={form}
name="update-basic"
labelCol={{ span: 5 }}
wrapperCol={{ span: 19 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label={t('course.edit.category')}
name="category_ids"
rules={[
{
required: true,
message: t('course.edit.categoryPlaceholder'),
},
]}
>
<TreeSelect
showCheckedStrategy={TreeSelect.SHOW_ALL}
allowClear
multiple
style={{ width: 424 }}
treeData={categories}
placeholder={t('course.edit.categoryPlaceholder')}
treeDefaultExpandAll
/>
</Form.Item>
<Form.Item
label={t('course.edit.name')}
name="title"
rules={[{ required: true, message: t('course.edit.namePlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('course.edit.namePlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('course.edit.isRequired')}
name="isRequired"
rules={[
{
required: true,
message: t('course.edit.isRequiredPlaceholder'),
},
]}
>
<Radio.Group>
<Radio value={1}>{t('course.columns.text1')}</Radio>
<Radio value={0} style={{ marginLeft: 22 }}>
{t('course.columns.text2')}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label={t('course.edit.ids')}
name="ids"
rules={[{ required: true, message: t('course.edit.idsPlaceholder') }]}
>
<div
className="d-flex"
style={{ width: '100%', flexWrap: 'wrap', marginBottom: -8 }}
>
<Button
type="default"
style={{ marginBottom: 14 }}
onClick={() => setIdsVisible(true)}
>
{t('course.edit.idsText')}
</Button>
<div
className="d-flex"
style={{
width: '100%',
flexWrap: 'wrap',
marginBottom: -16,
}}
>
{deps.length > 0 &&
deps.map((item: any, i: number) => (
<Tag
key={i}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...deps];
const arr2 = [...depIds];
arr.splice(i, 1);
arr2.splice(i, 1);
setDeps(arr);
setDepIds(arr2);
form.setFieldsValue({
ids: arr2.concat(groupIds).concat(userIds),
});
}}
>
{item.title.props.children}
</Tag>
))}
{groups.length > 0 &&
groups.map((item: any, i: number) => (
<Tag
key={i}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...groups];
const arr2 = [...groupIds];
arr.splice(i, 1);
arr2.splice(i, 1);
setGroups(arr);
setGroupIds(arr2);
form.setFieldsValue({
ids: depIds.concat(arr2).concat(userIds),
});
}}
>
{item.title.props.children}
</Tag>
))}
{users.length > 0 &&
users.map((item: any, j: number) => (
<Tag
key={j}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...users];
const arr2 = [...userIds];
arr.splice(j, 1);
arr2.splice(j, 1);
setUsers(arr);
setUserIds(arr2);
form.setFieldsValue({
ids: depIds.concat(groupIds).concat(arr2),
});
}}
>
{item.name}
</Tag>
))}
</div>
</div>
</Form.Item>
<Form.Item
label={t('course.edit.thumb')}
name="thumb"
rules={[
{
required: true,
message: t('course.edit.thumbPlaceholder'),
},
]}
>
<div className="d-flex">
<Image
src={thumb}
width={160}
height={120}
style={{ borderRadius: 6 }}
preview={false}
/>
<div className="c-flex ml-8 flex-1">
<div className="d-flex mb-28">
<div
className={
thumb === defaultThumb1
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb1);
form.setFieldsValue({
thumb: -1,
});
}}
>
<Image
src={defaultThumb1}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb2
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb2);
form.setFieldsValue({
thumb: -2,
});
}}
>
<Image
src={defaultThumb2}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb3
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb3);
form.setFieldsValue({
thumb: -3,
});
}}
>
<Image
src={defaultThumb3}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
</div>
<div className="d-flex">
<UploadImageButton
text={t('course.edit.thumbText')}
isDefault
onSelected={(url, id) => {
setThumb(url);
form.setFieldsValue({ thumb: id });
}}
></UploadImageButton>
<span className="helper-text ml-16">{t('course.edit.thumbTip')}</span>
</div>
</div>
</div>
</Form.Item>
<Form.Item label={t('course.edit.drag')} name="drag">
<Space align="baseline" style={{ height: 32 }}>
<Form.Item name="drag" valuePropName="checked">
<Switch onChange={onDragChange} />
</Form.Item>
<div className="helper-text">{t('course.edit.dragPlaceholder')}</div>
</Space>
</Form.Item>
{drag === 1 && (
<Form.Item label={t('course.edit.hangUp')} name="hangUp">
<Space align="baseline" style={{ height: 32 }}>
<Form.Item name="hangUp" valuePropName="checked">
<Switch onChange={onHangUpChange} />
</Form.Item>
<div className="helper-text">{t('course.edit.hangUpPlaceholder')}</div>
</Space>
</Form.Item>
)}
<Form.Item label={t('course.edit.credit')}>
<Space align="baseline" style={{ height: 32 }}>
<Form.Item name="creditType">
<Radio.Group
onChange={(e) => {
setCredit(Number(e.target.value));
if (Number(e.target.value) > 0) {
form.setFieldsValue({ credit: 0 });
}
}}
>
<Radio value={0}>{t('course.edit.radio5')}</Radio>
<Radio value={1}>{t('course.edit.radio6')}</Radio>
</Radio.Group>
</Form.Item>
{credit > 0 && (
<>
<div className="d-flex">{t('course.edit.text3')}</div>
<Form.Item name="credit">
<InputNumber
min={0}
max={9999}
style={{ width: 56 }}
precision={0}
controls={false}
/>
</Form.Item>
<div className="d-flex">{t('credit.rules.text')}</div>
</>
)}
</Space>
</Form.Item>
<Form.Item label={t('course.edit.teacher')} name="teacherIds">
<Select
style={{ width: 424 }}
allowClear
placeholder={t('course.edit.teacherPlaceholder')}
options={teachers}
/>
</Form.Item>
<Form.Item label={t('course.edit.desc')} name="short_desc">
<Input.TextArea
style={{ width: 424, minHeight: 80 }}
allowClear
placeholder={t('course.edit.descPlaceholder')}
maxLength={200}
/>
</Form.Item>
<Form.Item label={t('course.edit.time')}>
<Space align="baseline" style={{ height: 32 }}>
<Form.Item name="sort_at">
<DatePicker
disabledDate={disabledDate}
format="YYYY-MM-DD HH:mm:ss"
style={{ width: 240 }}
showTime
placeholder={t('course.edit.timePlaceholder')}
/>
</Form.Item>
<div className="helper-text">{t('course.edit.timeTip')}</div>
</Space>
</Form.Item>
</Form>
</div>
</Drawer>
) : null}
</>
);
};

View File

@ -1,22 +0,0 @@
.thumb-item {
width: 80px;
height: 60px;
cursor: pointer;
margin-right: 8px;
border-radius: 6px;
&:last-child {
margin-right: 0;
}
}
.thumb-item-avtive {
width: 80px;
height: 60px;
border: 2px solid #ff4d4f;
cursor: pointer;
margin-right: 8px;
border-radius: 8px;
&:last-child {
margin-right: 0;
}
}

View File

@ -1,548 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Space,
Radio,
Button,
Drawer,
Form,
TreeSelect,
Input,
message,
Image,
Spin,
Select,
DatePicker,
Tag,
Switch,
InputNumber,
} from 'antd';
import styles from './update.module.less';
import { course, teacher } from '../../../api/index';
import { UploadImageButton, SelectRange } from '../../../compenents';
import defaultThumb1 from '../../../assets/thumb/thumb1.png';
import defaultThumb2 from '../../../assets/thumb/thumb2.png';
import defaultThumb3 from '../../../assets/thumb/thumb3.png';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import moment from 'moment';
import { textbook } from '../../../api';
const { TextArea } = Input;
interface PropInterface {
id: number;
open: boolean;
onCancel: () => void;
}
export const TextbookUpdate: React.FC<PropInterface> = ({ id, open, onCancel }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [init, setInit] = useState(true);
const [loading, setLoading] = useState(false);
const [thumb, setThumb] = useState('');
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const [depIds, setDepIds] = useState<number[]>([]);
const [groupIds, setGroupIds] = useState<number[]>([]);
const [userIds, setUserIds] = useState<number[]>([]);
const [deps, setDeps] = useState<any[]>([]);
const [groups, setGroups] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [idsVisible, setIdsVisible] = useState(false);
useEffect(() => {
setInit(true);
if (id === 0) {
return;
}
if (open) {
setDepIds([]);
setDeps([]);
setGroupIds([]);
setGroups([]);
setUserIds([]);
setUsers([]);
getDetail();
}
}, [form, id, open]);
// 信息回显
const getDetail = () => {
textbook.textbookDetail(id).then((res: any) => {
form.setFieldsValue({
title: res.data.textbook.title,
thumb: res.data.textbook.thumb,
short_desc: res.data.textbook.shortDesc,
author: res.data.textbook.author,
major: res.data.textbook.major,
publish_time: res.data.textbook.publishTime ? dayjs(res.data.textbook.publishTime) : '',
publish_unit: res.data.textbook.publishUnit,
create_time: res.data.textbook.createTime ? dayjs(res.data.textbook.createTime) : '',
});
const deps = res.data.deps;
if (deps && JSON.stringify(deps) !== '{}') {
getDepsDetail(deps);
}
const groups = res.data.groups;
if (groups && JSON.stringify(groups) !== '{}') {
getGroupsDetail(groups);
}
const users = res.data.users;
if (users && JSON.stringify(users) !== '{}') {
getUsersDetail(users);
}
if (
(deps && JSON.stringify(deps) !== '{}') ||
(groups && JSON.stringify(groups) !== '{}') ||
(users && JSON.stringify(users) !== '{}')
) {
form.setFieldsValue({ ids: [1, 2] });
}
setResourceUrl(res.data.resource_url);
setThumb(
res.data.textbook.thumb === -1
? defaultThumb1
: res.data.textbook.thumb === -2
? defaultThumb2
: res.data.textbook.thumb === -3
? defaultThumb3
: res.data.resource_url[res.data.textbook.thumb]
);
setInit(false);
});
};
const getDepsDetail = (deps: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(deps).map((v, i) => {
arr.push(Number(v));
arr2.push({
key: Number(v),
title: {
props: {
children: deps[v],
},
},
});
});
setDepIds(arr);
setDeps(arr2);
};
const getGroupsDetail = (groups: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(groups).map((v, i) => {
arr.push(Number(v));
arr2.push({
key: Number(v),
title: {
props: {
children: groups[v],
},
},
});
});
setGroupIds(arr);
setGroups(arr2);
};
const getUsersDetail = (users: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(users).map((v, i) => {
arr.push(Number(v));
arr2.push({
id: Number(v),
name: users[v],
});
});
setUserIds(arr);
setUsers(arr2);
};
const onFinish = (values: any) => {
if (loading) {
return;
}
const dep_ids: any[] = depIds;
const user_ids: any[] = userIds;
const group_ids: any[] = groupIds;
values.sort_at = moment(new Date(values.sort_at)).format('YYYY-MM-DD');
setLoading(true);
textbook
.updateTextbook(
id,
values.title,
values.thumb,
values.short_desc,
values.author,
values.major,
dep_ids,
group_ids,
user_ids,
values.publish_time,
values.publish_unit,
values.create_time
)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
})
.catch((e) => {
setLoading(false);
});
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const disabledDate = (current: any) => {
return current && current >= moment().add(0, 'days');
};
return (
<>
{open ? (
<Drawer
title={t('course.updateTextbook')}
onClose={onCancel}
maskClosable={false}
open={true}
footer={
<Space className="j-r-flex">
<Button onClick={() => onCancel()}>{t('commen.drawerCancel')}</Button>
<Button loading={loading} onClick={() => form.submit()} type="primary">
{t('commen.drawerOk')}
</Button>
</Space>
}
width={700}
>
<div className="float-left mt-24">
<SelectRange
defaultDepIds={depIds}
defaultGroupIds={groupIds}
defaultUserIds={userIds}
defaultDeps={deps}
defaultGroups={groups}
defaultUsers={users}
open={idsVisible}
onCancel={() => setIdsVisible(false)}
onSelected={(selDepIds, selDeps, selGroupIds, selGroups, selUserIds, selUsers) => {
setDepIds(selDepIds);
setDeps(selDeps);
setGroupIds(selGroupIds);
setGroups(selGroups);
setUserIds(selUserIds);
setUsers(selUsers);
form.setFieldsValue({
ids: selDepIds.concat(selGroupIds).concat(selUserIds),
});
setIdsVisible(false);
}}
/>
{/* 表单 */}
<Form
form={form}
name="create-basic"
labelCol={{ span: 5 }}
wrapperCol={{ span: 19 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
{/* 表单字段 */}
<Form.Item
label={t('textbook.create.name')}
name="title"
rules={[{ required: true, message: t('textbook.create.namePlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('textbook.create.namePlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.thumb')}
name="thumb"
rules={[{ required: true, message: t('textbook.create.thumbPlaceholder') }]}
>
<div className="d-flex">
<Image
src={thumb}
width={160}
height={120}
style={{ borderRadius: 6 }}
preview={false}
/>
<div className="c-flex ml-8 flex-1">
<div className="d-flex mb-28">
<div
className={
thumb === defaultThumb1
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb1);
form.setFieldsValue({
thumb: -1,
});
}}
>
<Image
src={defaultThumb1}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb2
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb2);
form.setFieldsValue({
thumb: -2,
});
}}
>
<Image
src={defaultThumb2}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb3
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb3);
form.setFieldsValue({
thumb: -3,
});
}}
>
<Image
src={defaultThumb3}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
</div>
<div className="d-flex">
<UploadImageButton
text={t('course.edit.thumbText')}
isDefault
onSelected={(url, id) => {
setThumb(url);
form.setFieldsValue({ thumb: id });
}}
></UploadImageButton>
<span className="helper-text ml-16">{t('textbook.create.thumbTip')}</span>
</div>
</div>
</div>
</Form.Item>
{/*范围指派*/}
<Form.Item
label={t('textbook.create.assign')}
name="ids"
rules={[{ required: true, message: t('textbook.create.assignPlaceholder') }]}
>
<div
className="d-flex"
style={{ width: '100%', flexWrap: 'wrap', marginBottom: -8 }}
>
<Button
type="default"
style={{ marginBottom: 14 }}
onClick={() => setIdsVisible(true)}
>
{t('course.edit.idsText')}
</Button>
<div
className="d-flex"
style={{
width: '100%',
flexWrap: 'wrap',
marginBottom: -16,
}}
>
{deps.length > 0 &&
deps.map((item: any, i: number) => (
<Tag
key={i}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...deps];
const arr2 = [...depIds];
arr.splice(i, 1);
arr2.splice(i, 1);
setDeps(arr);
setDepIds(arr2);
form.setFieldsValue({
ids: arr2.concat(groupIds).concat(userIds),
});
}}
>
{item.title.props.children}
</Tag>
))}
{groups.length > 0 &&
groups.map((item: any, i: number) => (
<Tag
key={i}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...groups];
const arr2 = [...groupIds];
arr.splice(i, 1);
arr2.splice(i, 1);
setGroups(arr);
setGroupIds(arr2);
form.setFieldsValue({
ids: depIds.concat(arr2).concat(userIds),
});
}}
>
{item.title.props.children}
</Tag>
))}
{users.length > 0 &&
users.map((item: any, j: number) => (
<Tag
key={j}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...users];
const arr2 = [...userIds];
arr.splice(j, 1);
arr2.splice(j, 1);
setUsers(arr);
setUserIds(arr2);
form.setFieldsValue({
dep_ids: depIds.concat(groupIds).concat(arr2),
});
}}
>
{item.name}
</Tag>
))}
</div>
</div>
</Form.Item>
<Form.Item
label={t('textbook.create.subject')}
name="major"
rules={[{ required: true, message: t('textbook.create.subjectPlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('textbook.create.subjectPlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.author')}
name="author"
rules={[{ required: true, message: t('textbook.create.authorPlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('textbook.create.authorPlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.publisher')}
name="publish_unit"
rules={[{ required: true, message: t('textbook.create.publisherPlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('textbook.create.publisherPlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.publishTime')}
name="publish_time"
rules={[{ required: true, message: t('textbook.create.publishTimePlaceholder') }]}
>
<DatePicker
style={{ width: 424 }}
placeholder={t('textbook.create.publishTimePlaceholder')}
allowClear
format="YYYY-MM-DD"
disabledDate={disabledDate}
/>
</Form.Item>
<Form.Item
label={t('textbook.create.desc')}
name="short_desc"
rules={[{ required: true, message: t('textbook.create.descPlaceholder') }]}
>
<TextArea
style={{ width: 424 }}
rows={6}
placeholder={t('textbook.create.descPlaceholder')}
allowClear
maxLength={200}
autoSize={{ minRows: 6, maxRows: 6 }}
/>
</Form.Item>
</Form>
</div>
</Drawer>
) : null}
</>
);
};

View File

@ -7,7 +7,6 @@ import { dateFormatNoTime } from '../../utils';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { PerButton } from '../../compenents';
import { CreateTextbook } from './compenents/createTextbook';
import { TextbookUpdate } from './compenents/updateTextbook';
import defaultThumb1 from '../../assets/thumb/thumb1.png';
import defaultThumb2 from '../../assets/thumb/thumb2.png';
import defaultThumb3 from '../../assets/thumb/thumb3.png';
@ -18,27 +17,20 @@ const { confirm } = Modal;
interface DataType {
id: React.Key;
charge: number;
class_hour: number;
created_at: string;
is_required: number;
is_show: number;
short_desc: string;
thumb: string;
title: string;
sort_at?: string;
isbn: string;
dep_ids: undefined;
publish_time: undefined;
major: undefined;
author: undefined;
publish_unit: undefined;
create_time: undefined;
}
type TeacherModel = {
about: string;
avatar: string;
created_at: string;
deleted: number;
id: number;
name: string;
updated_at: string;
};
interface LocalSearchParamsInterface {
page?: number;
size?: number;
@ -66,12 +58,12 @@ const CoursePage = () => {
const [refresh, setRefresh] = useState(false);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const [createVisible, setCreateVisible] = useState(false);
const [updateVisible, setUpdateVisible] = useState(false);
const [cid, setCid] = useState(0);
const [isDrawerVisible, setIsDrawerVisible] = useState<boolean>(false);
const [isEdit, setIsEdit] = useState<boolean>(false);
const [editId, setEditId] = useState<number>(0);
const [errorData, setErrorData] = useState<any>({});
const [showDetail, setShowDetail] = useState(false);
const [textbookId, setTextbookId] = useState<number>(0);
const resetLocalSearchParams = (params: LocalSearchParamsInterface) => {
setSearchParams(
@ -115,19 +107,12 @@ const CoursePage = () => {
<Image
preview={false}
width={80}
height={60}
height={107}
style={{ borderRadius: 6 }}
src={
record.thumb === -1
? defaultThumb1
: record.thumb === -2
? defaultThumb2
: record.thumb === -3
? defaultThumb3
: resourceUrl[record.thumb]
}
src={record.allUrl}
loading="lazy"
alt="thumb"
fallback={defaultThumb1}
></Image>
</div>
<span className="ml-8">{record.title}</span>
@ -206,7 +191,8 @@ const CoursePage = () => {
size="small"
className="b-n-link c-red"
onClick={() => {
alert('查看关联资源');
setTextbookId(Number(record.id));
navigate('/textbook/resource/' + Number(record.id) + '?title=' + record.title);
}}
>
{t('textbook.bookColumns.option2')}
@ -221,8 +207,9 @@ const CoursePage = () => {
size="small"
className="b-n-link c-red"
onClick={() => {
setCid(Number(record.id));
setUpdateVisible(true);
setEditId(Number(record.id));
setIsDrawerVisible(true);
setIsEdit(true);
}}
>
{t('textbook.bookColumns.option3')}
@ -253,7 +240,7 @@ const CoursePage = () => {
icon={null}
p="course" // TODO此处需要权限限定 后期修改为textbook
onClick={() => {
setCid(Number(record.id));
setTextbookId(Number(record.id));
navigate('/textbook/chapter/' + Number(record.id) + '?title=' + record.title);
}}
disabled={null}
@ -287,7 +274,7 @@ const CoursePage = () => {
okText: t('commen.okText'),
cancelText: t('commen.cancelText'),
onOk() {
textbook.destroyTextbook(id).then((res: any) => {
textbook.DestroyTextbookApi(id).then((res: any) => {
if (res.data) {
setErrorData(res.data);
setShowDetail(true);
@ -307,17 +294,31 @@ const CoursePage = () => {
const getList = () => {
setLoading(true);
textbook
.textbookList(page, size, title)
.GetTextbookListApi(page, size, title)
.then((res: any) => {
setTotal(res.data.total);
setList(res.data.data);
setResourceUrl(res.data.resource_url);
setLoading(false);
})
.catch((err: any) => {
console.log('error', err);
});
};
const paginationProps = {
current: page, //当前页码
pageSize: size,
total: total, // 总条数
onChange: (page: number, pageSize: number) => handlePageChange(page, pageSize),
showSizeChanger: true,
};
const handlePageChange = (page: number, pageSize: number) => {
resetLocalSearchParams({
page: page,
size: pageSize,
});
};
// 重置列表
const resetList = () => {
resetLocalSearchParams({
@ -330,21 +331,6 @@ const CoursePage = () => {
setRefresh(!refresh);
};
const paginationProps = {
current: page, //当前页码
pageSize: size,
total: total, // 总条数
onChange: (page: number, pageSize: number) => handlePageChange(page, pageSize), //改变页码的函数
showSizeChanger: true,
};
const handlePageChange = (page: number, pageSize: number) => {
resetLocalSearchParams({
page: page,
size: pageSize,
});
};
return (
<div className="playedu-main-body">
<div className="playedu-main-title float-left mb-24">{t('course.label4')}</div>
@ -357,7 +343,10 @@ const CoursePage = () => {
class="mr-16"
icon={<PlusOutlined />}
p="course" // TODO此处需要权限限定 后期修改为textbook
onClick={() => setCreateVisible(true)}
onClick={() => {
setIsDrawerVisible(true);
setIsEdit(false);
}}
disabled={null}
/>
</div>
@ -401,21 +390,19 @@ const CoursePage = () => {
pagination={paginationProps}
rowKey={(record) => record.id}
/>
<CreateTextbook
open={createVisible}
onCancel={() => {
setCreateVisible(false);
setRefresh(!refresh);
}}
/>
<TextbookUpdate
id={cid}
open={updateVisible}
onCancel={() => {
setUpdateVisible(false);
setRefresh(!refresh);
}}
/>
{isDrawerVisible && (
<CreateTextbook
open={isDrawerVisible}
onCancel={() => {
setIsDrawerVisible(false);
setRefresh(!refresh);
setIsEdit(false);
setEditId(0);
}}
isEdit={isEdit}
editId={editId}
/>
)}
{/*Error 展示*/}
{showDetail ? (
<Modal
@ -454,7 +441,7 @@ const CoursePage = () => {
</div>
{errorData[v].tasks &&
errorData[v].tasks.length > 0 &&
errorData[v].tasks.map((it: any, i: number) => (
errorData[v].tasks.map((it: any) => (
<div key={it.id} className="j-b-flex mt-8">
<div
style={{

View File

@ -0,0 +1,49 @@
.container {
width: 100%;
background-color: white;
box-sizing: border-box;
padding: 24px;
border-radius: 12px;
}
.title{
margin: 20px 0 ;
font-size: 14px;
}
.mainBox{
width: 100%;
background-color: white;
box-sizing: border-box;
padding: 24px;
border-radius: 12px;
.search{
width: 100%;
display: flex;
align-content: center;
justify-content: space-between;
margin-bottom: 16px;
.btns{
width: 70%;
display: flex;
flex-direction: row;
justify-content: end;
align-items: baseline;
}
.types{
width: 30%;
display: flex;
flex-direction: row;
justify-content: start;
align-items: baseline;
.typeTitle {
font-size: 14px;
line-height: 40px;
margin-right: 8px;
}
}
}
}

View File

@ -0,0 +1,450 @@
import { Button, Input, message, Modal, Radio, Space, Table, TableProps } from 'antd';
import React, { useEffect, useState } from 'react';
import { GetResourceListApi, DelResourceItemApi, GetDetailApi } from '../../api/textbook';
import styles from './resource.module.less';
import { TableRowSelection } from 'antd/es/table/interface';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import LoadingPage from '../loading';
import { BackBartment } from '../../compenents';
import { useTranslation } from 'react-i18next';
import { DeleteOutlined, UploadOutlined } from '@ant-design/icons';
import CreateResourceModal from './compenents/Resource/CreateResourceModal';
interface ResourceBase {
id: number | null | string | undefined; // 资源id
typeId: number; // 分类Id
bookId: 0;
chapterId: 0;
name: string;
type: string;
ext: string;
size: 0;
duration: 0;
url: string;
cover: string;
status: 0;
creator: string;
updater: string;
createTime: string;
updateTime: string;
tenantId: string;
}
// 列表展示
interface ResourceDataType extends ResourceBase {
index?: number;
}
// 分页查询
interface ResourceResData {
data: {
records: ResourceDataType[];
total: number;
};
}
const ResourcePage = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const [searchParams] = useSearchParams();
const title = searchParams.get('title');
const { bookId } = useParams();
const [isEdit, setIsEdit] = useState(false); // 是否编辑模式
const [editId, setEditId] = useState<number>(0);
const [isAddModalOpen, setIsAddModalOpen] = useState<boolean>(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const [selectedId, setSelectedId] = useState<number | string | null>(null);
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
const [resource, setResource] = useState<ResourceBase[]>([]);
const [resourceTotal, setResourceTotal] = useState<number>(0);
const [type, setType] = useState<number>(0);
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [sortOrder, setSortOrder] = useState<string>('');
const [sortField, setSortField] = useState<string>('');
const [searchData, setSearchData] = useState<string>('');
const [refresh, setRefresh] = useState(false);
const [pageLoading, setPageLoading] = useState<boolean>(false);
const TypeOptions = [
{ label: t('textbook.resource.typeList.all'), value: 0, color: '#f3f4f6' },
{ label: t('textbook.resource.typeList.video'), value: 1, color: '#dbeafe' },
{ label: t('textbook.resource.typeList.img'), value: 2, color: '#dcfce7' },
{ label: t('textbook.resource.typeList.doc'), value: 3, color: '#fef9c3' },
{ label: t('textbook.resource.typeList.audio'), value: 4, color: '#f3e8ff' },
{ label: t('textbook.resource.typeList.other'), value: 5, color: '#f3f4f6' },
];
// 排序字段/**/
/* name
size createTime这三个*/
// 列表数据
const columns: TableProps<ResourceBase>['columns'] = [
{
title: t('textbook.resource.title1'),
dataIndex: 'softNo',
align: 'left',
width: 300,
sorter: true,
},
{
title: t('textbook.resource.title2'),
dataIndex: 'softwareName',
align: 'left',
},
{
title: t('textbook.resource.title3'),
dataIndex: 'company',
ellipsis: true,
sorter: true,
},
{
title: t('textbook.resource.title4'),
dataIndex: 'version',
align: 'left',
ellipsis: true,
width: 140,
sorter: true,
},
{
title: t('textbook.resource.title5'),
// align: 'center',
render: (_, record) => (
<Space
size="middle"
style={{ display: 'flex', justifyContent: 'space-around', alignItems: 'center' }}
>
<a
key="pre"
onClick={() => {
console.log('预览');
}}
>
{/*<EyeOutlined />*/}
{t('commen.preview')}
</a>
<a
key={'edit'}
//@ts-ignore
onClick={() => {
getDetail(Number(record.id));
}}
>
{t('commen.edit')}
</a>
<a
key={'del'}
className="b-link c-red"
onClick={() => showDeleteConfirm(Number(record.id))}
>
{/*<DeleteOutlined />*/}
{t('commen.del')}
</a>
</Space>
),
},
];
// @ts-ignore
const getResourceList = async () => {
try {
const res = (await GetResourceListApi(
page,
pageSize,
type,
searchData,
sortOrder,
sortField
)) as ResourceResData;
if (res && 'data' in res && 'records' in res.data) {
setResource(res.data.records);
setResourceTotal(res.data.total);
} else {
console.warn('接口返回数据结构异常:', res);
setResource([]); // 设置为空数组防止崩溃
}
} catch (error) {
console.error('获取列表失败:', error);
}
};
const resetVirtualList = async () => {
try {
const res = (await GetResourceListApi(1, 10, null, null, null, null)) as ResourceResData;
if (res && 'data' in res && 'records' in res.data) {
setResource(res.data.records || []);
setResourceTotal(res.data.total);
} else {
console.warn('接口返回数据结构异常:', res);
setResource([]); // 设置为空数组防止崩溃
}
} catch (error) {
console.error('获取列表失败:', error);
}
};
const getDetail = async (id: number) => {
try {
const res = (await GetDetailApi(id)) as VirtualResDetail | undefined;
if (!res || !res.data) {
message.error('获取详情失败');
return;
}
setIsEdit(true);
//@ts-ignore
setItemDetailData(res.data);
setSelectedId(res.data.id);
// 打开弹窗
setIsAddModalOpen(true);
} catch (error) {
message.error('获取详情失败');
setPageLoading(false);
console.error('获取详情失败:', error);
}
};
const showDeleteConfirm = (id: number) => {
setSelectedId(id);
setModalVisible(true);
};
const handleDeleteItem = async () => {
if (selectedId === null) return;
setConfirmLoading(true);
try {
await DelResourceItemApi(selectedId.toString());
message.success('删除成功');
// @ts-ignore
await getVirtualList();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
} finally {
setConfirmLoading(false);
setModalVisible(false);
setSelectedId(null);
}
};
//弹窗取消
const showAddSoftModal = () => {
setIsEdit(false); // 设置为新增模式
setSelectedId(null); // 清除选中 ID
setIsAddModalOpen(true);
};
const handleCancelDeleteItem = () => {
setModalVisible(false);
};
const handleCancelDeleteItems = () => {
setIsConfirmModalOpen(false);
};
const handleReset = () => {
setPage(1);
setPageSize(10);
setType(0);
setSelectedIdList([]);
setSearchData('');
resetVirtualList();
};
// 批量删除
const handleDeleteItems = () => {
toDeleteSoftwareList();
};
const toDeleteSoftwareList = async () => {
try {
const selectedIdListString = selectedIdList.join(',');
const res = await DelResourceItemApi(selectedIdListString);
message.success('删除成功');
// @ts-ignore
await getVirtualList();
} catch (error) {
message.error('删除失败');
console.error('删除失败:', error);
} finally {
setConfirmLoading(false);
setIsConfirmModalOpen(false);
setSelectedId(null);
setSelectedIdList([]);
}
};
const handleAddCancel = () => {
setIsAddModalOpen(false);
setIsEdit(false);
setSelectedId(null);
setType(0);
};
useEffect(() => {
setResource([]);
getResourceList();
}, [refresh, page, pageSize, type, searchData]);
const onSelectChange = (newSelectedRowKeys: any[]) => {
setSelectedIdList(newSelectedRowKeys);
};
const canDelete = selectedIdList.length > 0;
const rowSelection: TableRowSelection<ResourceBase> = {
selectedRowKeys: selectedIdList,
onChange: onSelectChange,
};
useEffect(() => {
getResourceList();
}, [page, pageSize, type, sortOrder, sortField, searchData]);
const handleTableChange = (
pagination: { current: number; pageSize: number },
filters: any,
sorter: { field: string; order: string }
) => {
// 统一处理分页
// if (pagination.current !== page || pagination.pageSize !== pageSize) {
setPage(pagination.current);
setPageSize(pagination.pageSize);
// }
// 处理排序
if (sorter && sorter.field) {
const sortField = sorter.field as string;
const sortOrder =
sorter.order === 'ascend' ? 'asc' : sorter.order === 'descend' ? 'desc' : '';
console.log('排序字段:', sortField, '排序方向:', sortOrder);
}
};
return (
<>
<div className={styles.container}>
{pageLoading && <LoadingPage />}
<BackBartment title={`${t('textbook.resource.pageTitle')}`} />
<div className={styles.mainBox}>
<div className={styles.title}>: {title}</div>
<div className={styles.search}>
<div className={styles.types}>
<div className={styles.typeTitle}>{t('textbook.resource.type')}:</div>
<Radio.Group
optionType="button"
buttonStyle={'solid'}
defaultValue={0}
value={type}
onChange={(e) => {
console.log(e.target.value, 'data');
setType(e.target.value);
}}
style={{
display: 'flex',
height: 40,
borderRadius: 16,
gap: 8,
}}
>
{TypeOptions.map((option) => {
return (
<Radio.Button
key={option.value}
value={option.value}
style={{
borderRadius: '16px',
border: '1px solid #d9d9d9',
}}
>
{option.label}
</Radio.Button>
);
})}
</Radio.Group>
</div>
<div className={styles.btns}>
<Input
placeholder={t('textbook.resource.searchPlaceholder')}
style={{ marginRight: 15, width: 360 }}
value={searchData}
allowClear
onChange={(e) => setSearchData(e.target.value)}
/>
<Button type="primary" onClick={getResourceList}>
{t('commen.search')}
</Button>
<Button
variant="outlined"
style={{ marginRight: 15, marginLeft: 15 }}
onClick={handleReset}
>
{t('commen.reset')}
</Button>
<Button type="primary" style={{ marginRight: 15 }} onClick={showAddSoftModal}>
<UploadOutlined />
{t('textbook.resource.upload')}
</Button>
<Button
variant="outlined"
style={{ marginRight: 15 }}
disabled={!canDelete}
onClick={() => setIsConfirmModalOpen(true)}
>
<DeleteOutlined /> {t('commen.moreDel')}
</Button>
</div>
</div>
<Table<ResourceBase>
rowSelection={rowSelection}
columns={columns}
dataSource={resource}
// @ts-ignore
rowKey={(record) => record.id}
pagination={{
pageSize: pageSize,
current: page,
total: resourceTotal,
showSizeChanger: true,
align: 'start',
showTotal: (total) => `${resourceTotal} 条记录`,
}}
// @ts-ignore
onChange={handleTableChange}
/>
</div>
</div>
{isAddModalOpen && (
<CreateResourceModal
isEdit={isEdit}
isOpen={isAddModalOpen}
onCancel={handleAddCancel}
resourceId={editId}
bookId={bookId}
typeOptions={TypeOptions}
></CreateResourceModal>
)}
{/*删除确认*/}
<Modal
title="确认删除"
open={modalVisible}
onOk={handleDeleteItem}
onCancel={handleCancelDeleteItem}
confirmLoading={confirmLoading}
>
<p></p>
</Modal>
{/*多个删除确认*/}
<Modal
title="确认删除"
open={isConfirmModalOpen}
onOk={handleDeleteItems}
onCancel={handleCancelDeleteItems}
confirmLoading={confirmLoading}
>
<p></p>
</Modal>
</>
);
};
export default ResourcePage;

View File

@ -37,8 +37,6 @@ const TreeSelect = (props: TreeSelectProps) => {
const [searchInputShow, setSearchInputShow] = useState<string>(searchClassKey);
const [editSelectId, setEditSelectId] = useState<any>();
console.log(editSelectId, 'editSelectId', classId, 'classID,');
const [isEditClass, setIsEditClass] = useState<boolean>(false);
const [editSelectItem, setEditSelectItem] = useState<any>();

View File

@ -184,7 +184,7 @@ const VideoModal: React.FC<VideoModalProps> = ({
justifyContent: 'center',
}}
centered
destroyOnClose
destroyOnHidden={true}
>
<div
className="video-container"

View File

@ -60,6 +60,7 @@ const OfflineCourseQrcodePage = lazy(() => import('../pages/offline-course/qrcod
// 教材管理
const TextbookPage = lazy(() => import('../pages/textbook/index'));
const ChapterManagementPage = lazy(() => import('../pages/textbook/chapter'));
const ResourceManagementPage = lazy(() => import('../pages/textbook/resource'));
//试卷课时人工阅卷
const CoursePaperMarkPage = lazy(() => import('../pages/course/mark'));
//学员相关
@ -270,6 +271,11 @@ const routes: RouteObject[] = [
path: '/textbook/chapter/:bookId',
element: <PrivateRoute Component={<ChapterManagementPage />} />,
},
// 资源管理
{
path: '/textbook/resource/:bookId',
element: <PrivateRoute Component={<ResourceManagementPage />} />,
},
{
path: '/exam/questions',
element: <PrivateRoute Component={<ExamQuestionsPage />} />,