Compare commits
2 Commits
dc0d6d5b48
...
bc8dc4cff6
| Author | SHA1 | Date | |
|---|---|---|---|
| bc8dc4cff6 | |||
| 29054c6188 |
@ -38,7 +38,7 @@ export const knowledgeKeyApi = {
|
||||
},
|
||||
validDataSet: () => {
|
||||
return client.get('/knowledge/dataset/validDataSet', {});
|
||||
}
|
||||
},
|
||||
|
||||
// // 获取知识库密钥
|
||||
// getKnowledgeKey: () => {
|
||||
|
||||
@ -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, {});
|
||||
}
|
||||
|
||||
@ -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
|
||||
* */
|
||||
|
||||
@ -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']);
|
||||
|
||||
@ -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": "其他"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1951,7 +1951,7 @@
|
||||
"createTimePlaceholder": "請填入建立時間",
|
||||
"publishTime":"發佈時間",
|
||||
"publishTimePlaceholder":"請填入發佈時間",
|
||||
"thumbTip": "(建議尺寸:400x300px)"
|
||||
"thumbTip": "(建議尺寸:210x297px)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,4 +185,4 @@ const ExamAdministrationDetails = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ExamAdministrationDetails
|
||||
export default ExamAdministrationDetails;
|
||||
|
||||
@ -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} 名学生的考试状态`);
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
>
|
||||
无视频源
|
||||
|
||||
@ -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);
|
||||
// 打开弹窗
|
||||
|
||||
@ -11,7 +11,6 @@ const HomePage = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles['layout-wrap']}>
|
||||
|
||||
<div className={styles['left-menu']}>
|
||||
<LeftMenu />
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
231
app/backend/src/pages/textbook/compenents/Upload/UploadThumb.tsx
Normal file
231
app/backend/src/pages/textbook/compenents/Upload/UploadThumb.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
||||
// 使用示例:
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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={{
|
||||
|
||||
49
app/backend/src/pages/textbook/resource.module.less
Normal file
49
app/backend/src/pages/textbook/resource.module.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
450
app/backend/src/pages/textbook/resource.tsx
Normal file
450
app/backend/src/pages/textbook/resource.tsx
Normal 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;
|
||||
@ -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>();
|
||||
|
||||
|
||||
@ -184,7 +184,7 @@ const VideoModal: React.FC<VideoModalProps> = ({
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
centered
|
||||
destroyOnClose
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
<div
|
||||
className="video-container"
|
||||
|
||||
@ -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 />} />,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user