mirror of
https://github.com/PlayEdu/backend
synced 2025-07-22 10:49:35 +08:00
aerge branch 'dev'
This commit is contained in:
commit
518dea2b57
21
src/api/admin-log.ts
Normal file
21
src/api/admin-log.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function adminLogList(
|
||||
page: number,
|
||||
size: number,
|
||||
admin_name: string,
|
||||
title: string,
|
||||
opt: string,
|
||||
start_time: string,
|
||||
end_time: string
|
||||
) {
|
||||
return client.get("/backend/v1/admin/log/index", {
|
||||
page: page,
|
||||
size: size,
|
||||
admin_name: admin_name,
|
||||
title: title,
|
||||
opt: opt,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
});
|
||||
}
|
20
src/api/course-attachment.ts
Normal file
20
src/api/course-attachment.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import client from "./internal/httpClient";
|
||||
|
||||
export function storeCourseAttachmentMulti(
|
||||
courseId: number,
|
||||
attachments: number[]
|
||||
) {
|
||||
return client.post(`/backend/v1/course/${courseId}/attachment/create-batch`, {
|
||||
attachments: attachments,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyAttachment(courseId: number, id: number) {
|
||||
return client.destroy(`/backend/v1/course/${courseId}/attachment/${id}`);
|
||||
}
|
||||
|
||||
export function transCourseAttachment(courseId: number, ids: number[]) {
|
||||
return client.put(`/backend/v1/course/${courseId}/attachment/update/sort`, {
|
||||
ids: ids,
|
||||
});
|
||||
}
|
@ -35,7 +35,8 @@ export function storeCourse(
|
||||
depIds: number[],
|
||||
categoryIds: number[],
|
||||
chapters: number[],
|
||||
hours: number[]
|
||||
hours: number[],
|
||||
attachments: any[]
|
||||
) {
|
||||
return client.post("/backend/v1/course/create", {
|
||||
title: title,
|
||||
@ -47,6 +48,7 @@ export function storeCourse(
|
||||
category_ids: categoryIds,
|
||||
chapters: chapters,
|
||||
hours: hours,
|
||||
attachments: attachments,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ export * as courseCategory from "./course-category";
|
||||
export * as courseChapter from "./course-chapter";
|
||||
export * as course from "./course";
|
||||
export * as courseHour from "./course-hour";
|
||||
export * as courseAttachment from "./course-attachment";
|
||||
export * as department from "./department";
|
||||
export * as resourceCategory from "./resource-category";
|
||||
export * as resource from "./resource";
|
||||
@ -13,3 +14,4 @@ export * as upload from "./upload";
|
||||
export * as user from "./user";
|
||||
export * as appConfig from "./app-config";
|
||||
export * as dashboard from "./dashboard";
|
||||
export * as adminLog from "./admin-log";
|
||||
|
@ -7,6 +7,10 @@ const GoLogin = () => {
|
||||
window.location.href = "/login";
|
||||
};
|
||||
|
||||
const GoError = (code: number) => {
|
||||
// window.location.href = "/error?code=" + code;
|
||||
};
|
||||
|
||||
export class HttpClient {
|
||||
axios: Axios;
|
||||
|
||||
@ -39,7 +43,24 @@ export class HttpClient {
|
||||
|
||||
if (code === 0) {
|
||||
return Promise.resolve(response);
|
||||
} else if (code === 404) {
|
||||
message.error(msg);
|
||||
// 跳转到404页面
|
||||
GoError(404);
|
||||
} else if (code === 403) {
|
||||
message.error(msg);
|
||||
// 跳转到无权限页面
|
||||
GoError(403);
|
||||
} else if (code === 429) {
|
||||
message.error(msg);
|
||||
// 跳转到429页面
|
||||
GoError(429);
|
||||
} else if (code === 500) {
|
||||
message.error(msg);
|
||||
// 跳转到500异常页面
|
||||
GoError(500);
|
||||
} else {
|
||||
GoError(code);
|
||||
message.error(msg);
|
||||
}
|
||||
return Promise.reject(response);
|
||||
@ -52,13 +73,18 @@ export class HttpClient {
|
||||
GoLogin();
|
||||
} else if (status === 404) {
|
||||
// 跳转到404页面
|
||||
GoLogin();
|
||||
GoError(404);
|
||||
} else if (status === 403) {
|
||||
// 跳转到无权限页面
|
||||
GoLogin();
|
||||
GoError(403);
|
||||
} else if (status === 429) {
|
||||
// 跳转到429页面
|
||||
GoError(429);
|
||||
} else if (status === 500) {
|
||||
// 跳转到500异常页面
|
||||
GoLogin();
|
||||
GoError(500);
|
||||
} else {
|
||||
GoError(status);
|
||||
}
|
||||
return Promise.reject(error.response);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export function minioMergeVideo(
|
||||
duration: number,
|
||||
poster: string
|
||||
) {
|
||||
return client.post("/backend/v1/upload/minio/merge-video", {
|
||||
return client.post("/backend/v1/upload/minio/merge-file", {
|
||||
filename,
|
||||
upload_id: uploadId,
|
||||
original_filename: originalFilename,
|
||||
|
@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3943555 */
|
||||
src: url('iconfont.woff2?t=1679383201256') format('woff2'),
|
||||
url('iconfont.woff?t=1679383201256') format('woff'),
|
||||
url('iconfont.ttf?t=1679383201256') format('truetype');
|
||||
src: url('iconfont.woff2?t=1690600882833') format('woff2'),
|
||||
url('iconfont.woff?t=1690600882833') format('woff'),
|
||||
url('iconfont.ttf?t=1690600882833') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@ -13,6 +13,54 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-playedu:before {
|
||||
content: "\e756";
|
||||
}
|
||||
|
||||
.icon-icon-xuexi:before {
|
||||
content: "\e753";
|
||||
}
|
||||
|
||||
.icon-icon-wode:before {
|
||||
content: "\e754";
|
||||
}
|
||||
|
||||
.icon-icon-shouye:before {
|
||||
content: "\e755";
|
||||
}
|
||||
|
||||
.icon-icon-xiala:before {
|
||||
content: "\e752";
|
||||
}
|
||||
|
||||
.icon-close:before {
|
||||
content: "\e751";
|
||||
}
|
||||
|
||||
.icon-fullscreen:before {
|
||||
content: "\e74b";
|
||||
}
|
||||
|
||||
.icon-speed:before {
|
||||
content: "\e74c";
|
||||
}
|
||||
|
||||
.icon-mute:before {
|
||||
content: "\e74d";
|
||||
}
|
||||
|
||||
.icon-play:before {
|
||||
content: "\e74e";
|
||||
}
|
||||
|
||||
.icon-pause:before {
|
||||
content: "\e74f";
|
||||
}
|
||||
|
||||
.icon-unmute:before {
|
||||
content: "\e750";
|
||||
}
|
||||
|
||||
.icon-icon-tips:before {
|
||||
content: "\e74a";
|
||||
}
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -10,3 +10,6 @@ export * from ".//tree-adminroles";
|
||||
export * from "./duration-text";
|
||||
export * from "./upload-video-sub";
|
||||
export * from "./select-resource";
|
||||
export * from "./upload-courseware-button";
|
||||
export * from "./upload-courseware-sub";
|
||||
export * from "./select-attachment";
|
||||
|
@ -46,6 +46,7 @@ const items = [
|
||||
[
|
||||
getItem("视频", "/videos", null, null, null, null),
|
||||
getItem("图片", "/images", null, null, null, null),
|
||||
getItem("课件", "/courseware", null, null, null, null),
|
||||
],
|
||||
null,
|
||||
null
|
||||
@ -90,6 +91,7 @@ const items = [
|
||||
null,
|
||||
"admin-user-index"
|
||||
),
|
||||
getItem("管理日志", "/system/adminlog", null, null, null, "admin-log"),
|
||||
// getItem("角色配置", "/system/adminroles", null, null, null, null),
|
||||
],
|
||||
null,
|
||||
@ -103,6 +105,7 @@ export const LeftMenu: React.FC = () => {
|
||||
const children2Parent: any = {
|
||||
"^/video": ["resource"],
|
||||
"^/image": ["resource"],
|
||||
"^/courseware": ["resource"],
|
||||
"^/member": ["user"],
|
||||
"^/department": ["user"],
|
||||
"^/course": ["courses"],
|
||||
|
74
src/compenents/select-attachment/index.tsx
Normal file
74
src/compenents/select-attachment/index.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Row, Modal, Tabs } from "antd";
|
||||
import styles from "./index.module.less";
|
||||
import { UploadCoursewareSub } from "../../compenents";
|
||||
import type { TabsProps } from "antd";
|
||||
|
||||
interface PropsInterface {
|
||||
defaultKeys: any[];
|
||||
open: boolean;
|
||||
onSelected: (arr: any[], videos: any[]) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const SelectAttachment = (props: PropsInterface) => {
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
const [tabKey, setTabKey] = useState(1);
|
||||
const [selectKeys, setSelectKeys] = useState<any>([]);
|
||||
const [selectVideos, setSelectVideos] = useState<any>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setRefresh(!refresh);
|
||||
}, [props.open]);
|
||||
|
||||
const items: TabsProps["items"] = [
|
||||
{
|
||||
key: "1",
|
||||
label: `课件`,
|
||||
children: (
|
||||
<div className="float-left">
|
||||
<UploadCoursewareSub
|
||||
label="课件"
|
||||
defaultCheckedList={props.defaultKeys}
|
||||
open={refresh}
|
||||
onSelected={(arr: any[], videos: any[]) => {
|
||||
setSelectKeys(arr);
|
||||
setSelectVideos(videos);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = (key: string) => {
|
||||
setTabKey(Number(key));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="资源素材库"
|
||||
centered
|
||||
closable={false}
|
||||
onCancel={() => {
|
||||
setSelectKeys([]);
|
||||
setSelectVideos([]);
|
||||
props.onCancel();
|
||||
}}
|
||||
open={props.open}
|
||||
width={800}
|
||||
maskClosable={false}
|
||||
onOk={() => {
|
||||
props.onSelected(selectKeys, selectVideos);
|
||||
setSelectKeys([]);
|
||||
setSelectVideos([]);
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
<Tabs defaultActiveKey="1" items={items} onChange={onChange} />
|
||||
</Row>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,16 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Row, Modal, message, Tabs } from "antd";
|
||||
import { Row, Modal, Tabs } from "antd";
|
||||
import styles from "./index.module.less";
|
||||
import { UploadVideoSub } from "../../compenents";
|
||||
import type { TabsProps } from "antd";
|
||||
|
||||
interface VideoItem {
|
||||
id: number;
|
||||
category_id: number;
|
||||
name: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface PropsInterface {
|
||||
defaultKeys: any[];
|
||||
open: boolean;
|
||||
@ -24,6 +17,10 @@ export const SelectResource = (props: PropsInterface) => {
|
||||
const [selectKeys, setSelectKeys] = useState<any>([]);
|
||||
const [selectVideos, setSelectVideos] = useState<any>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setRefresh(!refresh);
|
||||
}, [props.open]);
|
||||
|
||||
const items: TabsProps["items"] = [
|
||||
{
|
||||
key: "1",
|
||||
|
262
src/compenents/upload-courseware-button/index.tsx
Normal file
262
src/compenents/upload-courseware-button/index.tsx
Normal file
@ -0,0 +1,262 @@
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
message,
|
||||
Modal,
|
||||
Progress,
|
||||
Row,
|
||||
Table,
|
||||
Tag,
|
||||
Upload,
|
||||
} from "antd";
|
||||
import Dragger from "antd/es/upload/Dragger";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { generateUUID, parseVideo } from "../../utils";
|
||||
import { minioMergeVideo, minioUploadId } from "../../api/upload";
|
||||
import { UploadChunk } from "../../js/minio-upload-chunk";
|
||||
|
||||
interface PropsInterface {
|
||||
categoryIds: number[];
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export const UploadCoursewareButton = (props: PropsInterface) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const localFileList = useRef<FileItem[]>([]);
|
||||
const intervalId = useRef<number>();
|
||||
const [fileList, setFileList] = useState<FileItem[]>([]);
|
||||
|
||||
const getMinioUploadId = async (extension: string) => {
|
||||
let resp: any = await minioUploadId(extension);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
intervalId.current = setInterval(() => {
|
||||
if (localFileList.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < localFileList.current.length; i++) {
|
||||
if (localFileList.current[i].upload.status === 0) {
|
||||
localFileList.current[i].upload.handler.start();
|
||||
break;
|
||||
}
|
||||
if (localFileList.current[i].upload.status === 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
console.log("定时器已创建", intervalId.current);
|
||||
} else {
|
||||
window.clearInterval(intervalId.current);
|
||||
console.log("定时器已销毁");
|
||||
}
|
||||
}, [showModal]);
|
||||
|
||||
const uploadProps = {
|
||||
multiple: true,
|
||||
beforeUpload: async (file: File) => {
|
||||
let extension: any = file.name.split(".");
|
||||
extension = extension[extension.length - 1];
|
||||
if (
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
|
||||
file.type === "application/msword" ||
|
||||
file.type === "application/vnd.ms-word.document.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-word.template.macroEnabled.12" ||
|
||||
file.type === "text/plain" ||
|
||||
file.type === "application/pdf" ||
|
||||
file.type === "application/x-zip-compressed" ||
|
||||
file.type === "application/octet-stream" ||
|
||||
file.type === "application/zip" ||
|
||||
file.type === "application/x-rar" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
|
||||
file.type === "application/vnd.ms-excel" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template" ||
|
||||
file.type === "application/vnd.ms-excel.sheet.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-excel.template.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-excel.addin.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-excel.sheet.binary.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-powerpoint" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.template" ||
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow" ||
|
||||
file.type === "application/vnd.ms-powerpoint.addin.macroEnabled.12" ||
|
||||
file.type ===
|
||||
"application/vnd.ms-powerpoint.presentation.macroEnabled.12" ||
|
||||
file.type === "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"
|
||||
) {
|
||||
// 添加到本地待上传
|
||||
let data = await getMinioUploadId(extension);
|
||||
let run = new UploadChunk(file, data["upload_id"], data["filename"]);
|
||||
let item: FileItem = {
|
||||
id: generateUUID(),
|
||||
file: file,
|
||||
upload: {
|
||||
handler: run,
|
||||
progress: 0,
|
||||
status: 0,
|
||||
remark: "",
|
||||
},
|
||||
};
|
||||
item.upload.handler.on("success", () => {
|
||||
minioMergeVideo(
|
||||
data["filename"],
|
||||
data["upload_id"],
|
||||
props.categoryIds.join(","),
|
||||
item.file.name,
|
||||
extension,
|
||||
item.file.size,
|
||||
0,
|
||||
""
|
||||
).then(() => {
|
||||
item.upload.progress = 100;
|
||||
item.upload.status = item.upload.handler.getUploadStatus();
|
||||
setFileList([...localFileList.current]);
|
||||
});
|
||||
});
|
||||
item.upload.handler.on("progress", (p: number) => {
|
||||
item.upload.status = item.upload.handler.getUploadStatus();
|
||||
item.upload.progress = p >= 100 ? 99 : p;
|
||||
setFileList([...localFileList.current]);
|
||||
});
|
||||
item.upload.handler.on("error", (msg: string) => {
|
||||
item.upload.status = item.upload.handler.getUploadStatus();
|
||||
item.upload.remark = msg;
|
||||
setFileList([...localFileList.current]);
|
||||
});
|
||||
// 先插入到ref
|
||||
localFileList.current.push(item);
|
||||
// 再更新list
|
||||
setFileList([...localFileList.current]);
|
||||
} else {
|
||||
message.error(`${file.name} 并不是可上传文件`);
|
||||
}
|
||||
return Upload.LIST_IGNORE;
|
||||
},
|
||||
};
|
||||
|
||||
const closeWin = () => {
|
||||
if (fileList.length > 0) {
|
||||
fileList.forEach((item) => {
|
||||
if (item.upload.status !== 5 && item.upload.status !== 7) {
|
||||
item.upload.handler.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
props.onUpdate();
|
||||
localFileList.current = [];
|
||||
setFileList([]);
|
||||
setShowModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
上传课件
|
||||
</Button>
|
||||
|
||||
{showModal ? (
|
||||
<Modal
|
||||
width={800}
|
||||
title="上传课件"
|
||||
open={true}
|
||||
onCancel={() => {
|
||||
closeWin();
|
||||
}}
|
||||
maskClosable={false}
|
||||
closable={false}
|
||||
onOk={() => {
|
||||
closeWin();
|
||||
}}
|
||||
okText="完成"
|
||||
>
|
||||
<Row gutter={[0, 10]}>
|
||||
<Col span={24}>
|
||||
<Dragger {...uploadProps}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">请将文件拖拽到此处上传</p>
|
||||
<p className="ant-upload-hint">
|
||||
支持一次上传多个 /
|
||||
支持word、excel、ppt、pdf、zip、rar、txt格式文件
|
||||
</p>
|
||||
</Dragger>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Table
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
columns={[
|
||||
{
|
||||
title: "课件",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (_, record) => <span>{record.file.name}</span>,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
key: "size",
|
||||
render: (_, record) => (
|
||||
<span>
|
||||
{(record.file.size / 1024 / 1024).toFixed(2)}M
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "进度",
|
||||
dataIndex: "progress",
|
||||
key: "progress",
|
||||
render: (_, record: FileItem) => (
|
||||
<>
|
||||
{record.upload.status === 0 ? (
|
||||
"等待上传"
|
||||
) : (
|
||||
<Progress
|
||||
size="small"
|
||||
steps={20}
|
||||
percent={record.upload.progress}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_, record) => (
|
||||
<>
|
||||
{record.upload.status === 5 ? (
|
||||
<Tag color="red">{record.upload.remark}</Tag>
|
||||
) : null}
|
||||
|
||||
{record.upload.status === 7 ? (
|
||||
<Tag color="success">上传成功</Tag>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
dataSource={fileList}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
199
src/compenents/upload-courseware-sub/index.tsx
Normal file
199
src/compenents/upload-courseware-sub/index.tsx
Normal file
@ -0,0 +1,199 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Row, Col, Empty, Table } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { resource } from "../../api";
|
||||
import styles from "./index.module.less";
|
||||
import { TreeCategory, UploadCoursewareButton } from "../../compenents";
|
||||
|
||||
interface VideoItem {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: string;
|
||||
type: string;
|
||||
url: string;
|
||||
path: string;
|
||||
size: number;
|
||||
extension: string;
|
||||
admin_id: number;
|
||||
}
|
||||
|
||||
interface DataType {
|
||||
id: React.Key;
|
||||
name: string;
|
||||
created_at: string;
|
||||
type: string;
|
||||
url: string;
|
||||
path: string;
|
||||
size: number;
|
||||
extension: string;
|
||||
admin_id: number;
|
||||
}
|
||||
|
||||
interface PropsInterface {
|
||||
defaultCheckedList: any[];
|
||||
label: string;
|
||||
open: boolean;
|
||||
onSelected: (arr: any[], videos: []) => void;
|
||||
}
|
||||
|
||||
export const UploadCoursewareSub = (props: PropsInterface) => {
|
||||
const [category_ids, setCategoryIds] = useState<any>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [videoList, setVideoList] = useState<VideoItem[]>([]);
|
||||
const [existingTypes, setExistingTypes] = useState<any>([]);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
|
||||
|
||||
// 加载列表
|
||||
useEffect(() => {
|
||||
getvideoList();
|
||||
}, [props.open, category_ids, refresh, page, size]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaultCheckedList.length > 0) {
|
||||
setSelectedRowKeys(props.defaultCheckedList);
|
||||
}
|
||||
}, [props.defaultCheckedList]);
|
||||
|
||||
// 获取列表
|
||||
const getvideoList = () => {
|
||||
let categoryIds = category_ids.join(",");
|
||||
resource
|
||||
.resourceList(
|
||||
page,
|
||||
size,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"WORD,EXCEL,PPT,PDF,TXT,RAR,ZIP",
|
||||
categoryIds
|
||||
)
|
||||
.then((res: any) => {
|
||||
setTotal(res.data.result.total);
|
||||
setExistingTypes(res.data.existing_types);
|
||||
setVideoList(res.data.result.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("错误,", err);
|
||||
});
|
||||
};
|
||||
|
||||
// 重置列表
|
||||
const resetVideoList = () => {
|
||||
setPage(1);
|
||||
setVideoList([]);
|
||||
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) => {
|
||||
setPage(page);
|
||||
setSize(pageSize);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: "课件",
|
||||
render: (_, record: any) => (
|
||||
<div className="d-flex">
|
||||
<i
|
||||
className="iconfont icon-icon-file"
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "rgba(0,0,0,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div className="video-title ml-8">{record.name}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
render: (_, record: any) => <span>{record.type}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedRowKeys,
|
||||
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
|
||||
let row: any = selectedRows;
|
||||
let arrVideos: any = [];
|
||||
if (row) {
|
||||
for (var i = 0; i < row.length; i++) {
|
||||
if (props.defaultCheckedList.indexOf(row[i].id) === -1) {
|
||||
arrVideos.push({
|
||||
name: row[i].name,
|
||||
type: row[i].type,
|
||||
rid: row[i].id,
|
||||
});
|
||||
}
|
||||
}
|
||||
props.onSelected(selectedRowKeys, arrVideos);
|
||||
}
|
||||
setSelectedRowKeys(selectedRowKeys);
|
||||
},
|
||||
getCheckboxProps: (record: any) => ({
|
||||
disabled: props.defaultCheckedList.indexOf(record.id) !== -1, //禁用的条件
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row style={{ width: 752, minHeight: 520 }}>
|
||||
<Col span={7}>
|
||||
<TreeCategory
|
||||
selected={[]}
|
||||
type="no-cate"
|
||||
text={props.label}
|
||||
onUpdate={(keys: any) => setCategoryIds(keys)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={17}>
|
||||
<Row style={{ marginBottom: 24, paddingLeft: 10 }}>
|
||||
<Col span={24}>
|
||||
<UploadCoursewareButton
|
||||
categoryIds={category_ids}
|
||||
onUpdate={() => {
|
||||
resetVideoList();
|
||||
}}
|
||||
></UploadCoursewareButton>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className={styles["video-list"]}>
|
||||
{videoList.length === 0 && (
|
||||
<Col span={24} style={{ marginTop: 150 }}>
|
||||
<Empty description="暂无课件" />
|
||||
</Col>
|
||||
)}
|
||||
{videoList.length > 0 && (
|
||||
<div className="list-select-column-box c-flex">
|
||||
<Table
|
||||
rowSelection={{
|
||||
type: "checkbox",
|
||||
...rowSelection,
|
||||
}}
|
||||
columns={columns}
|
||||
dataSource={videoList}
|
||||
loading={loading}
|
||||
pagination={paginationProps}
|
||||
rowKey={(record) => record.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,19 +1,33 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Checkbox, Row, Col, Empty, message, Pagination } from "antd";
|
||||
import { Row, Col, Empty, Table } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { resource } from "../../api";
|
||||
import styles from "./index.module.less";
|
||||
import { UploadVideoButton } from "../upload-video-button";
|
||||
import { DurationText, TreeCategory } from "../../compenents";
|
||||
import type { CheckboxChangeEvent } from "antd/es/checkbox";
|
||||
import type { CheckboxValueType } from "antd/es/checkbox/Group";
|
||||
|
||||
const CheckboxGroup = Checkbox.Group;
|
||||
|
||||
interface VideoItem {
|
||||
id: number;
|
||||
category_id: number;
|
||||
name: string;
|
||||
duration: number;
|
||||
created_at: string;
|
||||
type: string;
|
||||
url: string;
|
||||
path: string;
|
||||
size: number;
|
||||
extension: string;
|
||||
admin_id: number;
|
||||
}
|
||||
|
||||
interface DataType {
|
||||
id: React.Key;
|
||||
name: string;
|
||||
created_at: string;
|
||||
type: string;
|
||||
url: string;
|
||||
path: string;
|
||||
size: number;
|
||||
extension: string;
|
||||
admin_id: number;
|
||||
}
|
||||
|
||||
interface PropsInterface {
|
||||
@ -32,14 +46,21 @@ export const UploadVideoSub = (props: PropsInterface) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
|
||||
|
||||
const [plainOptions, setPlainOptions] = useState<any>([]);
|
||||
const [checkedList, setCheckedList] = useState<CheckboxValueType[]>([]);
|
||||
const [indeterminate, setIndeterminate] = useState(false);
|
||||
const [checkAll, setCheckAll] = useState(false);
|
||||
// 加载列表
|
||||
useEffect(() => {
|
||||
getvideoList();
|
||||
}, [props.open, category_ids, refresh, page, size]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaultCheckedList.length > 0) {
|
||||
setSelectedRowKeys(props.defaultCheckedList);
|
||||
}
|
||||
}, [props.defaultCheckedList]);
|
||||
|
||||
// 获取列表
|
||||
const getvideoList = (defaultKeys: any[]) => {
|
||||
const getvideoList = () => {
|
||||
let categoryIds = category_ids.join(",");
|
||||
resource
|
||||
.resourceList(page, size, "", "", "", "VIDEO", categoryIds)
|
||||
@ -47,46 +68,12 @@ export const UploadVideoSub = (props: PropsInterface) => {
|
||||
setTotal(res.data.result.total);
|
||||
setVideoExtra(res.data.videos_extra);
|
||||
setVideoList(res.data.result.data);
|
||||
let data = res.data.result.data;
|
||||
const arr = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
arr.push({
|
||||
label: (
|
||||
<div className="d-flex">
|
||||
<i
|
||||
className="iconfont icon-icon-video"
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: "rgba(0,0,0,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div className="video-title ml-8">{data[i].name}</div>
|
||||
<div className="video-time">
|
||||
<DurationText
|
||||
duration={res.data.videos_extra[data[i].id].duration}
|
||||
></DurationText>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
value: data[i].id,
|
||||
disabled: false,
|
||||
});
|
||||
}
|
||||
if (defaultKeys.length > 0 && arr.length > 0) {
|
||||
for (let i = 0; i < defaultKeys.length; i++) {
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
if (arr[j].value === defaultKeys[i]) {
|
||||
arr[j].disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setPlainOptions(arr);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("错误,", err);
|
||||
});
|
||||
};
|
||||
|
||||
// 重置列表
|
||||
const resetVideoList = () => {
|
||||
setPage(1);
|
||||
@ -94,78 +81,71 @@ export const UploadVideoSub = (props: PropsInterface) => {
|
||||
setRefresh(!refresh);
|
||||
};
|
||||
|
||||
// 加载列表
|
||||
useEffect(() => {
|
||||
const arr = [...props.defaultCheckedList];
|
||||
setCheckedList(arr);
|
||||
if (arr.length === 0) {
|
||||
setIndeterminate(false);
|
||||
setCheckAll(false);
|
||||
}
|
||||
getvideoList(arr);
|
||||
}, [props.open, props.defaultCheckedList, category_ids, refresh, page, size]);
|
||||
|
||||
const onChange = (list: CheckboxValueType[]) => {
|
||||
setCheckedList(list);
|
||||
setIndeterminate(!!list.length && list.length < plainOptions.length);
|
||||
setCheckAll(list.length === plainOptions.length);
|
||||
const defalut = [...props.defaultCheckedList];
|
||||
let localKeys: any = [];
|
||||
list.map((item: any) => {
|
||||
if (defalut.indexOf(item) === -1) {
|
||||
localKeys.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
let arrVideos: any = [];
|
||||
|
||||
for (let i = 0; i < localKeys.length; i++) {
|
||||
videoList.map((item: any, index: number) => {
|
||||
if (item.id === localKeys[i]) {
|
||||
arrVideos.push({
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
rid: item.id,
|
||||
duration: videosExtra[item.id].duration,
|
||||
disabled: plainOptions[index].disabled,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
props.onSelected(localKeys, arrVideos);
|
||||
const paginationProps = {
|
||||
current: page, //当前页码
|
||||
pageSize: size,
|
||||
total: total, // 总条数
|
||||
onChange: (page: number, pageSize: number) =>
|
||||
handlePageChange(page, pageSize), //改变页码的函数
|
||||
showSizeChanger: true,
|
||||
};
|
||||
|
||||
const onCheckAllChange = (e: CheckboxChangeEvent) => {
|
||||
const arr = plainOptions.map((item: any) => item.value);
|
||||
setCheckedList(e.target.checked ? arr : []);
|
||||
setIndeterminate(false);
|
||||
setCheckAll(e.target.checked);
|
||||
const defalut = [...props.defaultCheckedList];
|
||||
let localKeys: any = [];
|
||||
arr.map((item: any) => {
|
||||
if (defalut.indexOf(item) === -1) {
|
||||
localKeys.push(item);
|
||||
}
|
||||
});
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setSize(pageSize);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: "视频",
|
||||
render: (_, record: any) => (
|
||||
<div className="d-flex">
|
||||
<i
|
||||
className="iconfont icon-icon-video"
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: "rgba(0,0,0,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div className="video-title ml-8">{record.name}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "时长",
|
||||
render: (_, record: any) => (
|
||||
<div>
|
||||
<DurationText
|
||||
duration={videosExtra[record.id].duration}
|
||||
></DurationText>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedRowKeys,
|
||||
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
|
||||
let row: any = selectedRows;
|
||||
let arrVideos: any = [];
|
||||
for (let i = 0; i < localKeys.length; i++) {
|
||||
videoList.map((item: any, index: number) => {
|
||||
if (item.id === localKeys[i]) {
|
||||
if (row) {
|
||||
for (var i = 0; i < row.length; i++) {
|
||||
if (props.defaultCheckedList.indexOf(row[i].id) === -1) {
|
||||
arrVideos.push({
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
rid: item.id,
|
||||
duration: videosExtra[item.id].duration,
|
||||
disabled: plainOptions[index].disabled,
|
||||
name: row[i].name,
|
||||
type: row[i].type,
|
||||
rid: row[i].id,
|
||||
duration: videosExtra[row[i].id].duration,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (e.target.checked) {
|
||||
props.onSelected(localKeys, arrVideos);
|
||||
} else {
|
||||
props.onSelected([], []);
|
||||
props.onSelected(selectedRowKeys, arrVideos);
|
||||
}
|
||||
setSelectedRowKeys(selectedRowKeys);
|
||||
},
|
||||
getCheckboxProps: (record: any) => ({
|
||||
disabled: props.defaultCheckedList.indexOf(record.id) !== -1, //禁用的条件
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
@ -198,43 +178,20 @@ export const UploadVideoSub = (props: PropsInterface) => {
|
||||
)}
|
||||
{videoList.length > 0 && (
|
||||
<div className="list-select-column-box c-flex">
|
||||
<Checkbox
|
||||
indeterminate={indeterminate}
|
||||
onChange={onCheckAllChange}
|
||||
checked={checkAll}
|
||||
>
|
||||
全选
|
||||
</Checkbox>
|
||||
<CheckboxGroup
|
||||
className="c-flex"
|
||||
options={plainOptions}
|
||||
value={checkedList}
|
||||
onChange={onChange}
|
||||
<Table
|
||||
rowSelection={{
|
||||
type: "checkbox",
|
||||
...rowSelection,
|
||||
}}
|
||||
columns={columns}
|
||||
dataSource={videoList}
|
||||
loading={loading}
|
||||
pagination={paginationProps}
|
||||
rowKey={(record) => record.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Row
|
||||
style={{
|
||||
paddingLeft: 10,
|
||||
}}
|
||||
>
|
||||
{videoList.length > 0 && total > 10 && (
|
||||
<Col
|
||||
span={24}
|
||||
style={{ display: "flex", flexDirection: "row-reverse" }}
|
||||
>
|
||||
<Pagination
|
||||
onChange={(currentPage, currentSize) => {
|
||||
setPage(currentPage);
|
||||
setSize(currentSize);
|
||||
}}
|
||||
defaultCurrent={page}
|
||||
total={total}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
|
@ -525,7 +525,7 @@ textarea.ant-input {
|
||||
&:hover {
|
||||
box-shadow: none !important;
|
||||
color: #ff4d4f !important;
|
||||
border-color: #ff4d4f;
|
||||
border-color: #ff4d4f !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@ -593,22 +593,18 @@ textarea.ant-input {
|
||||
}
|
||||
|
||||
.list-select-column-box {
|
||||
.ant-checkbox-wrapper {
|
||||
margin-inline-start: 0px;
|
||||
height: 38px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
float: left;
|
||||
.ant-table-cell {
|
||||
padding: 0px 0px;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
width: 390px;
|
||||
width: 360px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.video-time {
|
||||
width: 80px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.image-list-box {
|
||||
@ -660,3 +656,25 @@ textarea.ant-input {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.drop-item {
|
||||
width: 140px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
i {
|
||||
margin-right: 12px;
|
||||
}
|
||||
span {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { Provider } from "react-redux";
|
||||
import store from "./store";
|
||||
import { ConfigProvider } from "antd";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import AutoScorllTop from "./AutoTop";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
|
43
src/pages/course/compenents/attachment-update.module.scss
Normal file
43
src/pages/course/compenents/attachment-update.module.scss
Normal file
@ -0,0 +1,43 @@
|
||||
.hous-box {
|
||||
width: 500.53px;
|
||||
min-height: 56px;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
/* Firefox */
|
||||
-webkit-box-sizing: border-box;
|
||||
/* Safari */
|
||||
padding: 8px 8px 0px 8px;
|
||||
margin-left: 42px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.no-hours {
|
||||
height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
line-height: 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
.top-content {
|
||||
width: 502px;
|
||||
height: auto;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 16px;
|
||||
margin: 0 auto;
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #ff4d4f;
|
||||
line-height: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
218
src/pages/course/compenents/attachment-update.tsx
Normal file
218
src/pages/course/compenents/attachment-update.tsx
Normal file
@ -0,0 +1,218 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Drawer, Form, Modal, message } from "antd";
|
||||
import styles from "./hour-update.module.less";
|
||||
import { course, courseAttachment } from "../../../api/index";
|
||||
import { SelectAttachment } from "../../../compenents";
|
||||
import { ExclamationCircleFilled } from "@ant-design/icons";
|
||||
import { TreeAttachments } from "./attachments";
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface PropInterface {
|
||||
id: number;
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const CourseAttachmentUpdate: React.FC<PropInterface> = ({
|
||||
id,
|
||||
open,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [attachmentVisible, setAttachmentVisible] = useState<boolean>(false);
|
||||
const [attachmentData, setAttachmentData] = useState<any>([]);
|
||||
const [attachments, setAttachments] = useState<any>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id === 0) {
|
||||
return;
|
||||
}
|
||||
getDetail();
|
||||
}, [id, open]);
|
||||
|
||||
const getDetail = () => {
|
||||
course.course(id).then((res: any) => {
|
||||
let treeData = res.data.attachments;
|
||||
if (treeData.length > 0) {
|
||||
const arr: any = resetAttachments(treeData).arr;
|
||||
const keys: any = resetAttachments(treeData).keys;
|
||||
setAttachmentData(arr);
|
||||
setAttachments(keys);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetAttachments = (data: any) => {
|
||||
const arr: any = [];
|
||||
const keys: any = [];
|
||||
if (data) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
arr.push({
|
||||
type: data[i].type,
|
||||
name: data[i].title,
|
||||
rid: data[i].rid,
|
||||
id: data[i].id,
|
||||
});
|
||||
keys.push(data[i].rid);
|
||||
}
|
||||
}
|
||||
return { arr, keys };
|
||||
};
|
||||
|
||||
const onFinish = (values: any) => {};
|
||||
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log("Failed:", errorInfo);
|
||||
};
|
||||
|
||||
const selectAttachmentData = (arr: any, videos: any) => {
|
||||
const hours: any = [];
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
hours.push({
|
||||
sort: attachmentData.length + i,
|
||||
title: videos[i].name,
|
||||
type: videos[i].type,
|
||||
rid: videos[i].rid,
|
||||
});
|
||||
}
|
||||
if (hours.length === 0) {
|
||||
message.error("请选择课件");
|
||||
return;
|
||||
}
|
||||
|
||||
courseAttachment
|
||||
.storeCourseAttachmentMulti(id, hours)
|
||||
.then((res: any) => {
|
||||
console.log("ok");
|
||||
setAttachmentVisible(false);
|
||||
getDetail();
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
const delAttachments = (hid: number) => {
|
||||
const data = [...attachmentData];
|
||||
confirm({
|
||||
title: "操作确认",
|
||||
icon: <ExclamationCircleFilled />,
|
||||
content: "确认删除此课件?",
|
||||
centered: true,
|
||||
okText: "确认",
|
||||
cancelText: "取消",
|
||||
onOk() {
|
||||
const index = data.findIndex((i: any) => i.rid === hid);
|
||||
let delId = data[index].id;
|
||||
if (index >= 0) {
|
||||
data.splice(index, 1);
|
||||
}
|
||||
if (data.length > 0) {
|
||||
setAttachmentData(data);
|
||||
const keys = data.map((item: any) => item.rid);
|
||||
setAttachments(keys);
|
||||
} else {
|
||||
setAttachmentData([]);
|
||||
setAttachments([]);
|
||||
}
|
||||
if (delId) {
|
||||
courseAttachment.destroyAttachment(id, delId).then((res: any) => {
|
||||
console.log("ok");
|
||||
});
|
||||
}
|
||||
},
|
||||
onCancel() {
|
||||
console.log("Cancel");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const transAttachments = (arr: any) => {
|
||||
setAttachments(arr);
|
||||
const data = [...attachmentData];
|
||||
const newArr: any = [];
|
||||
const hourIds: any = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
data.map((item: any) => {
|
||||
if (item.rid === arr[i]) {
|
||||
newArr.push(item);
|
||||
hourIds.push(item.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
setAttachmentData(newArr);
|
||||
courseAttachment.transCourseAttachment(id, hourIds).then((res: any) => {
|
||||
console.log("ok");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
title="课件管理"
|
||||
onClose={onCancel}
|
||||
maskClosable={false}
|
||||
open={open}
|
||||
width={634}
|
||||
>
|
||||
<div className={styles["top-content"]}>
|
||||
<p>1.线上课课件调整及时生效,操作不可逆,请谨慎操作。</p>
|
||||
</div>
|
||||
<div className="float-left mt-24">
|
||||
<SelectAttachment
|
||||
defaultKeys={attachments}
|
||||
open={attachmentVisible}
|
||||
onCancel={() => {
|
||||
setAttachmentVisible(false);
|
||||
}}
|
||||
onSelected={(arr: any, videos: any) => {
|
||||
selectAttachmentData(arr, videos);
|
||||
}}
|
||||
></SelectAttachment>
|
||||
<Form
|
||||
form={form}
|
||||
name="attachment-update-basic"
|
||||
labelCol={{ span: 5 }}
|
||||
wrapperCol={{ span: 19 }}
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="c-flex">
|
||||
<Form.Item>
|
||||
<div className="ml-42">
|
||||
<Button
|
||||
onClick={() => setAttachmentVisible(true)}
|
||||
type="primary"
|
||||
>
|
||||
添加课件
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<div className={styles["hous-box"]}>
|
||||
{attachmentData.length === 0 && (
|
||||
<span className={styles["no-hours"]}>
|
||||
请点击上方按钮添加课件
|
||||
</span>
|
||||
)}
|
||||
{attachmentData.length > 0 && (
|
||||
<TreeAttachments
|
||||
data={attachmentData}
|
||||
onRemoveItem={(id: number) => {
|
||||
delAttachments(id);
|
||||
}}
|
||||
onUpdate={(arr: any[]) => {
|
||||
transAttachments(arr);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
157
src/pages/course/compenents/attachments.tsx
Normal file
157
src/pages/course/compenents/attachments.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import { message, Tree, Tooltip } from "antd";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { DataNode, TreeProps } from "antd/es/tree";
|
||||
|
||||
interface Option {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface PropInterface {
|
||||
data: Option[];
|
||||
onRemoveItem: (id: number) => void;
|
||||
onUpdate: (arr: any[]) => void;
|
||||
}
|
||||
|
||||
export const TreeAttachments = (props: PropInterface) => {
|
||||
const [treeData, setTreeData] = useState<any>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
useEffect(() => {
|
||||
const hours = props.data;
|
||||
if (hours.length === 0) {
|
||||
return;
|
||||
}
|
||||
checkTree(hours);
|
||||
}, [props.data]);
|
||||
|
||||
const checkTree = (hours: any) => {
|
||||
const arr = [];
|
||||
for (let i = 0; i < hours.length; i++) {
|
||||
arr.push({
|
||||
title: (
|
||||
<div className="d-flex">
|
||||
<div className="d-flex">
|
||||
<i
|
||||
className="iconfont icon-icon-file"
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: "rgba(0,0,0,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div className="tree-video-title mr-24">{hours[i].name}</div>
|
||||
</div>
|
||||
<Tooltip placement="top" title="可拖拽排序">
|
||||
<i
|
||||
className="iconfont icon-icon-drag mr-16"
|
||||
style={{ fontSize: 24 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<i
|
||||
className="iconfont icon-icon-delete"
|
||||
style={{ fontSize: 24 }}
|
||||
onClick={() => removeItem(hours[i].rid)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
key: hours[i].rid,
|
||||
});
|
||||
}
|
||||
setTreeData(arr);
|
||||
};
|
||||
|
||||
const removeItem = (id: number) => {
|
||||
if (id === 0) {
|
||||
return;
|
||||
}
|
||||
props.onRemoveItem(id);
|
||||
};
|
||||
const onDrop: TreeProps["onDrop"] = (info) => {
|
||||
const dropKey = info.node.key;
|
||||
const dragKey = info.dragNode.key;
|
||||
const dropPos = info.node.pos.split("-");
|
||||
const dropPosition =
|
||||
info.dropPosition - Number(dropPos[dropPos.length - 1]);
|
||||
const loop = (
|
||||
data: DataNode[],
|
||||
key: React.Key,
|
||||
callback: (node: DataNode, i: number, data: DataNode[]) => void
|
||||
) => {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].key === key) {
|
||||
return callback(data[i], i, data);
|
||||
}
|
||||
if (data[i].children) {
|
||||
loop(data[i].children!, key, callback);
|
||||
}
|
||||
}
|
||||
};
|
||||
const data = [...treeData];
|
||||
let isTop = false;
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].key === dragKey) {
|
||||
isTop = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Find dragObject
|
||||
let dragObj: DataNode;
|
||||
loop(data, dragKey, (item, index, arr) => {
|
||||
arr.splice(index, 1);
|
||||
dragObj = item;
|
||||
});
|
||||
|
||||
if (!info.dropToGap) {
|
||||
// Drop on the content
|
||||
loop(data, dropKey, (item) => {
|
||||
item.children = item.children || [];
|
||||
// where to insert 示例添加到头部,可以是随意位置
|
||||
item.children.unshift(dragObj);
|
||||
});
|
||||
} else if (
|
||||
((info.node as any).props.children || []).length > 0 && // Has children
|
||||
(info.node as any).props.expanded && // Is expanded
|
||||
dropPosition === 1 // On the bottom gap
|
||||
) {
|
||||
loop(data, dropKey, (item) => {
|
||||
item.children = item.children || [];
|
||||
// where to insert 示例添加到头部,可以是随意位置
|
||||
item.children.unshift(dragObj);
|
||||
// in previous version, we use item.children.push(dragObj) to insert the
|
||||
// item to the tail of the children
|
||||
});
|
||||
} else {
|
||||
let ar: DataNode[] = [];
|
||||
let i: number;
|
||||
loop(data, dropKey, (_item, index, arr) => {
|
||||
ar = arr;
|
||||
i = index;
|
||||
});
|
||||
if (dropPosition === -1) {
|
||||
ar.splice(i!, 0, dragObj!);
|
||||
} else {
|
||||
ar.splice(i! + 1, 0, dragObj!);
|
||||
}
|
||||
}
|
||||
setTreeData(data);
|
||||
const keys = data.map((item: any) => item.key);
|
||||
props.onUpdate(keys);
|
||||
};
|
||||
|
||||
const onDragEnter: TreeProps["onDragEnter"] = (info) => {
|
||||
console.log(info);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tree
|
||||
draggable
|
||||
blockNode
|
||||
onDragEnter={onDragEnter}
|
||||
onDrop={onDrop}
|
||||
selectable={false}
|
||||
treeData={treeData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -14,9 +14,14 @@ import {
|
||||
import styles from "./create.module.less";
|
||||
import { useSelector } from "react-redux";
|
||||
import { course, department } from "../../../api/index";
|
||||
import { UploadImageButton, SelectResource } from "../../../compenents";
|
||||
import {
|
||||
UploadImageButton,
|
||||
SelectResource,
|
||||
SelectAttachment,
|
||||
} from "../../../compenents";
|
||||
import { ExclamationCircleFilled } from "@ant-design/icons";
|
||||
import { TreeHours } from "./hours";
|
||||
import { TreeAttachments } from "./attachments";
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
@ -27,12 +32,6 @@ interface PropInterface {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
value: string | number;
|
||||
title: string;
|
||||
children?: Option[];
|
||||
}
|
||||
|
||||
export const CourseCreate: React.FC<PropInterface> = ({
|
||||
cateIds,
|
||||
depIds,
|
||||
@ -58,6 +57,10 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
const [videoVisible, setVideoVisible] = useState<boolean>(false);
|
||||
const [treeData, setTreeData] = useState<any>([]);
|
||||
const [addvideoCurrent, setAddvideoCurrent] = useState(0);
|
||||
const [showDrop, setShowDrop] = useState<boolean>(false);
|
||||
const [attachmentVisible, setAttachmentVisible] = useState<boolean>(false);
|
||||
const [attachmentData, setAttachmentData] = useState<any>([]);
|
||||
const [attachments, setAttachments] = useState<any>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@ -80,6 +83,9 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
setChapterHours([]);
|
||||
setHours([]);
|
||||
setTreeData([]);
|
||||
setAttachmentData([]);
|
||||
setAttachments([]);
|
||||
setShowDrop(false);
|
||||
}, [form, open]);
|
||||
|
||||
const getParams = () => {
|
||||
@ -224,7 +230,8 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
dep_ids,
|
||||
values.category_ids,
|
||||
chapters,
|
||||
treeData
|
||||
treeData,
|
||||
attachmentData
|
||||
)
|
||||
.then((res: any) => {
|
||||
message.success("保存成功!");
|
||||
@ -268,6 +275,20 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
setVideoVisible(false);
|
||||
};
|
||||
|
||||
const selectAttachmentData = (arr: any, videos: any) => {
|
||||
if (arr.length === 0) {
|
||||
message.error("请选择课件");
|
||||
return;
|
||||
}
|
||||
let keys = [...attachments];
|
||||
let data = [...attachmentData];
|
||||
keys = keys.concat(arr);
|
||||
data = data.concat(videos);
|
||||
setAttachments(keys);
|
||||
setAttachmentData(data);
|
||||
setAttachmentVisible(false);
|
||||
};
|
||||
|
||||
const getChapterType = (e: any) => {
|
||||
const arr = [...chapters];
|
||||
if (arr.length > 0) {
|
||||
@ -326,6 +347,36 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
setTreeData(newArr);
|
||||
};
|
||||
|
||||
const delAttachments = (id: number) => {
|
||||
const data = [...attachmentData];
|
||||
const index = data.findIndex((i: any) => i.rid === id);
|
||||
if (index >= 0) {
|
||||
data.splice(index, 1);
|
||||
}
|
||||
if (data.length > 0) {
|
||||
setAttachmentData(data);
|
||||
const keys = data.map((item: any) => item.rid);
|
||||
setAttachments(keys);
|
||||
} else {
|
||||
setAttachmentData([]);
|
||||
setAttachments([]);
|
||||
}
|
||||
};
|
||||
|
||||
const transAttachments = (arr: any) => {
|
||||
setAttachments(arr);
|
||||
const data = [...attachmentData];
|
||||
const newArr: any = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
data.map((item: any) => {
|
||||
if (item.rid === arr[i]) {
|
||||
newArr.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
setAttachmentData(newArr);
|
||||
};
|
||||
|
||||
const addNewChapter = () => {
|
||||
const arr = [...chapters];
|
||||
const keys = [...chapterHours];
|
||||
@ -445,6 +496,16 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SelectAttachment
|
||||
defaultKeys={attachments}
|
||||
open={attachmentVisible}
|
||||
onCancel={() => {
|
||||
setAttachmentVisible(false);
|
||||
}}
|
||||
onSelected={(arr: any, videos: any) => {
|
||||
selectAttachmentData(arr, videos);
|
||||
}}
|
||||
></SelectAttachment>
|
||||
<Form
|
||||
form={form}
|
||||
name="create-basic"
|
||||
@ -621,14 +682,6 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="课程简介" name="short_desc">
|
||||
<Input.TextArea
|
||||
style={{ width: 424, minHeight: 80 }}
|
||||
allowClear
|
||||
placeholder="请输入课程简介(最多200字)"
|
||||
maxLength={200}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="课时列表"
|
||||
name="hasChapter"
|
||||
@ -642,7 +695,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{chapterType === 0 && (
|
||||
<div className="c-flex">
|
||||
<div className="c-flex mb-24">
|
||||
<Form.Item>
|
||||
<div className="ml-120">
|
||||
<Button
|
||||
@ -674,7 +727,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
</div>
|
||||
)}
|
||||
{chapterType === 1 && (
|
||||
<div className="c-flex">
|
||||
<div className="c-flex mb-24">
|
||||
{chapters.length > 0 &&
|
||||
chapters.map((item: any, index: number) => {
|
||||
return (
|
||||
@ -737,6 +790,57 @@ export const CourseCreate: React.FC<PropInterface> = ({
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
<Form.Item label="更多选项">
|
||||
<div
|
||||
className={showDrop ? "drop-item active" : "drop-item"}
|
||||
onClick={() => setShowDrop(!showDrop)}
|
||||
>
|
||||
<i
|
||||
style={{ fontSize: 14 }}
|
||||
className="iconfont icon-icon-xiala c-red"
|
||||
/>
|
||||
<span>(课程简介、课件)</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="c-flex"
|
||||
style={{ display: showDrop ? "block" : "none" }}
|
||||
>
|
||||
<Form.Item label="课程简介" name="short_desc">
|
||||
<Input.TextArea
|
||||
style={{ width: 424, minHeight: 80 }}
|
||||
allowClear
|
||||
placeholder="请输入课程简介(最多200字)"
|
||||
maxLength={200}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="课程附件">
|
||||
<Button
|
||||
onClick={() => setAttachmentVisible(true)}
|
||||
type="primary"
|
||||
>
|
||||
添加课件
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<div className={styles["hous-box"]}>
|
||||
{attachmentData.length === 0 && (
|
||||
<span className={styles["no-hours"]}>
|
||||
请点击上方按钮添加课件
|
||||
</span>
|
||||
)}
|
||||
{attachmentData.length > 0 && (
|
||||
<TreeAttachments
|
||||
data={attachmentData}
|
||||
onRemoveItem={(id: number) => {
|
||||
delAttachments(id);
|
||||
}}
|
||||
onUpdate={(arr: any[]) => {
|
||||
transAttachments(arr);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Space, Button, Drawer, Form, Input, Modal, message } from "antd";
|
||||
import { Button, Drawer, Form, Input, Modal, message } from "antd";
|
||||
import styles from "./hour-update.module.less";
|
||||
import { course, courseHour, courseChapter } from "../../../api/index";
|
||||
import { SelectResource } from "../../../compenents";
|
||||
@ -14,12 +14,6 @@ interface PropInterface {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
value: string | number;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
}
|
||||
|
||||
export const CourseHourUpdate: React.FC<PropInterface> = ({
|
||||
id,
|
||||
open,
|
||||
@ -106,7 +100,6 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
|
||||
const selectData = (arr: any, videos: any) => {
|
||||
const hours: any = [];
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
if (videos[i].disabled === false) {
|
||||
hours.push({
|
||||
chapter_id: 0,
|
||||
sort: treeData.length + i,
|
||||
@ -116,7 +109,6 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
|
||||
rid: videos[i].rid,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (hours.length === 0) {
|
||||
message.error("请选择视频");
|
||||
return;
|
||||
@ -141,7 +133,6 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
|
||||
}
|
||||
const hours: any = [];
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
if (videos[i].disabled === false) {
|
||||
hours.push({
|
||||
chapter_id: data[addvideoCurrent].id,
|
||||
sort: data[addvideoCurrent].hours.length + i,
|
||||
@ -151,7 +142,6 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
|
||||
rid: videos[i].rid,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (hours.length === 0) {
|
||||
message.error("请选择视频");
|
||||
return;
|
||||
|
@ -16,20 +16,12 @@ import { useSelector } from "react-redux";
|
||||
import { course, department } from "../../../api/index";
|
||||
import { UploadImageButton } from "../../../compenents";
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface PropInterface {
|
||||
id: number;
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
value: string | number;
|
||||
title: string;
|
||||
children?: Option[];
|
||||
}
|
||||
|
||||
export const CourseUpdate: React.FC<PropInterface> = ({
|
||||
id,
|
||||
open,
|
||||
|
@ -27,6 +27,7 @@ import type { TabsProps } from "antd";
|
||||
import { CourseCreate } from "./compenents/create";
|
||||
import { CourseUpdate } from "./compenents/update";
|
||||
import { CourseHourUpdate } from "./compenents/hour-update";
|
||||
import { CourseAttachmentUpdate } from "./compenents/attachment-update";
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
@ -66,6 +67,8 @@ const CoursePage = () => {
|
||||
const [createVisible, setCreateVisible] = useState<boolean>(false);
|
||||
const [updateVisible, setUpdateVisible] = useState<boolean>(false);
|
||||
const [updateHourVisible, setHourUpdateVisible] = useState<boolean>(false);
|
||||
const [updateAttachmentVisible, setUpdateAttachmentVisible] =
|
||||
useState<boolean>(false);
|
||||
const [cid, setCid] = useState<number>(0);
|
||||
const [cateId, setCateId] = useState(Number(result.get("cid")));
|
||||
const [did, setDid] = useState(Number(result.get("did")));
|
||||
@ -241,6 +244,23 @@ const CoursePage = () => {
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
label: (
|
||||
<Button
|
||||
style={{ verticalAlign: "middle" }}
|
||||
type="link"
|
||||
size="small"
|
||||
className="b-n-link c-red"
|
||||
onClick={() => {
|
||||
setCid(Number(record.id));
|
||||
setUpdateAttachmentVisible(true);
|
||||
}}
|
||||
>
|
||||
课件
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "4",
|
||||
label: (
|
||||
<Button
|
||||
type="link"
|
||||
@ -455,6 +475,14 @@ const CoursePage = () => {
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
<CourseAttachmentUpdate
|
||||
id={cid}
|
||||
open={updateAttachmentVisible}
|
||||
onCancel={() => {
|
||||
setUpdateAttachmentVisible(false);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,15 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Result } from "antd";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useParams, useNavigate, useLocation } from "react-router-dom";
|
||||
import styles from "./index.module.less";
|
||||
|
||||
const ErrorPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const result = new URLSearchParams(useLocation().search);
|
||||
const [code, setCode] = useState(Number(result.get("code")));
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setCode(Number(result.get("code")));
|
||||
}, [result.get("code")]);
|
||||
|
||||
useEffect(() => {
|
||||
if (code === 403) {
|
||||
setError("无权限操作");
|
||||
} else if (code === 404) {
|
||||
setError("URL或资源不存在");
|
||||
} else if (code === 429) {
|
||||
setError("请求次数过多,请稍后再试");
|
||||
} else {
|
||||
setError("系统错误");
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
return (
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="您访问的页面不存在"
|
||||
title={code}
|
||||
subTitle={error}
|
||||
className={styles["main"]}
|
||||
extra={
|
||||
<Button
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import styles from "./index.module.less";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Header, LeftMenu } from "../../compenents";
|
||||
import { Header, LeftMenu } from "../../../compenents";
|
||||
import { Suspense } from "react";
|
||||
import LoadingPage from "../../loading";
|
||||
|
||||
const HomePage = () => {
|
||||
useEffect(() => {}, []);
|
||||
@ -17,8 +19,10 @@ const HomePage = () => {
|
||||
<Header></Header>
|
||||
</div>
|
||||
<div className={styles["right-main"]}>
|
||||
<Suspense fallback={<LoadingPage height="100vh" />}>
|
||||
{/* 二级路由出口 */}
|
||||
<Outlet />
|
||||
<Outlet />{" "}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,8 @@
|
||||
.layout-wrap {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
}
|
16
src/pages/layouts/without-header-without-footer/index.tsx
Normal file
16
src/pages/layouts/without-header-without-footer/index.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { Suspense } from "react";
|
||||
import styles from "./index.module.less";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import LoadingPage from "../../loading";
|
||||
|
||||
const WithoutHeaderWithoutFooter = () => {
|
||||
return (
|
||||
<div className={styles["layout-wrap"]}>
|
||||
<Suspense fallback={<LoadingPage height="100vh" />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithoutHeaderWithoutFooter;
|
@ -1,6 +1,6 @@
|
||||
.loading-parent-box {
|
||||
width: 100vd;
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
.loading-box {
|
||||
width: 100vw;
|
||||
line-height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,20 @@
|
||||
import { Spin } from "antd";
|
||||
import styles from "./index.module.less";
|
||||
|
||||
const LoadingPage = () => {
|
||||
interface PropsInterface {
|
||||
height?: string;
|
||||
}
|
||||
|
||||
const LoadingPage = (props: PropsInterface) => {
|
||||
return (
|
||||
<div className={styles["loading-parent-box"]}>
|
||||
<>
|
||||
<div
|
||||
className={styles["loading-box"]}
|
||||
style={{ height: props.height || "100vh" }}
|
||||
>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
150
src/pages/resource/courseware/compenents/update-dialog/index.tsx
Normal file
150
src/pages/resource/courseware/compenents/update-dialog/index.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Input, message, TreeSelect } from "antd";
|
||||
import { resource, resourceCategory } from "../../../../../api/index";
|
||||
|
||||
interface PropInterface {
|
||||
id: number;
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export const CoursewareUpdateDialog: React.FC<PropInterface> = ({
|
||||
id,
|
||||
open,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [categories, setCategories] = useState<any>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id === 0) {
|
||||
return;
|
||||
}
|
||||
if (open) {
|
||||
getCategory();
|
||||
getDetail();
|
||||
}
|
||||
}, [id, open]);
|
||||
|
||||
const getCategory = () => {
|
||||
resourceCategory.resourceCategoryList().then((res: any) => {
|
||||
const categories = res.data.categories;
|
||||
if (JSON.stringify(categories) !== "{}") {
|
||||
const new_arr: any = checkArr(categories, 0, null);
|
||||
setCategories(new_arr);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getDetail = () => {
|
||||
resource.videoDetail(id).then((res: any) => {
|
||||
let data = res.data.resources;
|
||||
form.setFieldsValue({
|
||||
name: data.name,
|
||||
category_id: res.data.category_ids,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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: departments[id][i].name,
|
||||
value: departments[id][i].id,
|
||||
});
|
||||
} else {
|
||||
const new_arr: any = checkArr(
|
||||
departments,
|
||||
departments[id][i].id,
|
||||
counts
|
||||
);
|
||||
arr.push({
|
||||
title: departments[id][i].name,
|
||||
value: departments[id][i].id,
|
||||
children: new_arr,
|
||||
});
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
setLoading(true);
|
||||
if (Array.isArray(values.category_id)) {
|
||||
values.category_id = values.category_id[0];
|
||||
}
|
||||
resource
|
||||
.videoUpdate(id, values)
|
||||
.then((res: any) => {
|
||||
setLoading(false);
|
||||
message.success("保存成功!");
|
||||
onSuccess();
|
||||
})
|
||||
.catch((e) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log("Failed:", errorInfo);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="编辑课件"
|
||||
centered
|
||||
forceRender
|
||||
open={open}
|
||||
width={416}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={() => onCancel()}
|
||||
maskClosable={false}
|
||||
>
|
||||
<div className="float-left mt-24">
|
||||
<Form
|
||||
form={form}
|
||||
name="videos-update"
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
label="课件分类"
|
||||
name="category_id"
|
||||
rules={[{ required: true, message: "请选择课件分类!" }]}
|
||||
>
|
||||
<TreeSelect
|
||||
showCheckedStrategy={TreeSelect.SHOW_ALL}
|
||||
allowClear
|
||||
style={{ width: 200 }}
|
||||
treeData={categories}
|
||||
placeholder="课件分类"
|
||||
treeDefaultExpandAll
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="课件名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入课件名称!" }]}
|
||||
>
|
||||
<Input
|
||||
allowClear
|
||||
style={{ width: 200 }}
|
||||
placeholder="请输入课件名称"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
0
src/pages/resource/courseware/index.module.less
Normal file
0
src/pages/resource/courseware/index.module.less
Normal file
392
src/pages/resource/courseware/index.tsx
Normal file
392
src/pages/resource/courseware/index.tsx
Normal file
@ -0,0 +1,392 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Table,
|
||||
message,
|
||||
Space,
|
||||
Typography,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
} from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import { resource } from "../../../api";
|
||||
// import styles from "./index.module.less";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { DownOutlined, ExclamationCircleFilled } from "@ant-design/icons";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { dateFormat } from "../../../utils/index";
|
||||
import { TreeCategory, UploadCoursewareButton } from "../../../compenents";
|
||||
import { CoursewareUpdateDialog } from "./compenents/update-dialog";
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface DataType {
|
||||
id: React.Key;
|
||||
name: string;
|
||||
created_at: string;
|
||||
type: string;
|
||||
number: number;
|
||||
}
|
||||
|
||||
const ResourceCoursewarePage = () => {
|
||||
const result = new URLSearchParams(useLocation().search);
|
||||
const [list, setList] = useState<any>([]);
|
||||
const [adminUsers, setAdminUsers] = useState<any>({});
|
||||
const [existingTypes, setExistingTypes] = useState<any>([]);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [category_ids, setCategoryIds] = useState<any>([]);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
|
||||
const [type, setType] = useState("WORD,EXCEL,PPT,PDF,TXT,RAR,ZIP");
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [multiConfig, setMultiConfig] = useState<boolean>(false);
|
||||
const [selLabel, setLabel] = useState<string>(
|
||||
result.get("label") ? String(result.get("label")) : "全部课件"
|
||||
);
|
||||
const [cateId, setCateId] = useState(Number(result.get("cid")));
|
||||
const [updateId, setUpdateId] = useState(0);
|
||||
const [updateVisible, setUpdateVisible] = useState<boolean>(false);
|
||||
const types = [
|
||||
{ label: "全部", value: "WORD,EXCEL,PPT,PDF,TXT,RAR,ZIP" },
|
||||
{ label: "WORD", value: "WORD" },
|
||||
{ label: "EXCEL", value: "EXCEL" },
|
||||
{ label: "PPT", value: "PPT" },
|
||||
{ label: "PDF", value: "PDF" },
|
||||
{ label: "TXT", value: "TXT" },
|
||||
{ label: "RAR", value: "RAR" },
|
||||
{ label: "ZIP", value: "ZIP" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
setCateId(Number(result.get("cid")));
|
||||
if (Number(result.get("cid"))) {
|
||||
let arr = [];
|
||||
arr.push(Number(result.get("cid")));
|
||||
setCategoryIds(arr);
|
||||
}
|
||||
}, [result.get("cid")]);
|
||||
|
||||
// 加载课件列表
|
||||
useEffect(() => {
|
||||
getList();
|
||||
}, [category_ids, refresh, page, size]);
|
||||
|
||||
const getList = () => {
|
||||
setLoading(true);
|
||||
let categoryIds = category_ids.join(",");
|
||||
resource
|
||||
.resourceList(page, size, "", "", title, type, categoryIds)
|
||||
.then((res: any) => {
|
||||
setTotal(res.data.result.total);
|
||||
setList(res.data.result.data);
|
||||
setExistingTypes(res.data.existing_types);
|
||||
setAdminUsers(res.data.admin_users);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log("错误,", err);
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: "课件名称",
|
||||
render: (_, record: any) => (
|
||||
<div className="d-flex">
|
||||
<i
|
||||
className="iconfont icon-icon-file"
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: "rgba(0,0,0,0.3)",
|
||||
}}
|
||||
/>
|
||||
<span className="ml-8">
|
||||
{record.name}.{record.extension}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "课件格式",
|
||||
dataIndex: "type",
|
||||
width: 204,
|
||||
render: (type: string) => <span>{type}</span>,
|
||||
},
|
||||
{
|
||||
title: "课件大小",
|
||||
dataIndex: "size",
|
||||
width: 204,
|
||||
render: (size: number) => <span>{(size / 1024 / 1024).toFixed(2)}M</span>,
|
||||
},
|
||||
{
|
||||
title: "创建人",
|
||||
dataIndex: "admin_id",
|
||||
width: 204,
|
||||
render: (text: number) =>
|
||||
JSON.stringify(adminUsers) !== "{}" && <span>{adminUsers[text]}</span>,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "created_at",
|
||||
width: 204,
|
||||
render: (text: string) => <span>{dateFormat(text)}</span>,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right",
|
||||
width: 180,
|
||||
render: (_, record: any) => {
|
||||
return (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
className="b-n-link c-red"
|
||||
onClick={() => {
|
||||
downLoadFile(record.url);
|
||||
}}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
<div className="form-column"></div>
|
||||
<Button
|
||||
type="link"
|
||||
className="b-link c-red"
|
||||
onClick={() => {
|
||||
setUpdateId(record.id);
|
||||
setUpdateVisible(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<div className="form-column"></div>
|
||||
<Button
|
||||
type="link"
|
||||
className="b-link c-red"
|
||||
onClick={() => removeResource(record.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
current: page, //当前页码
|
||||
pageSize: size,
|
||||
total: total, // 总条数
|
||||
onChange: (page: number, pageSize: number) =>
|
||||
handlePageChange(page, pageSize), //改变页码的函数
|
||||
showSizeChanger: true,
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setSize(pageSize);
|
||||
};
|
||||
|
||||
const rowSelection = {
|
||||
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
|
||||
setSelectedRowKeys(selectedRowKeys);
|
||||
},
|
||||
};
|
||||
|
||||
// 重置列表
|
||||
const resetList = () => {
|
||||
setPage(1);
|
||||
setSize(10);
|
||||
setList([]);
|
||||
setTitle("");
|
||||
setSelectedRowKeys([]);
|
||||
setType("WORD,EXCEL,PPT,PDF,TXT,RAR,ZIP");
|
||||
setRefresh(!refresh);
|
||||
};
|
||||
|
||||
// 删除课件
|
||||
const removeResource = (id: number) => {
|
||||
if (id === 0) {
|
||||
return;
|
||||
}
|
||||
confirm({
|
||||
title: "操作确认",
|
||||
icon: <ExclamationCircleFilled />,
|
||||
content: "删除前请检查选中课件文件无关联课程,确认删除?",
|
||||
centered: true,
|
||||
okText: "确认",
|
||||
cancelText: "取消",
|
||||
onOk() {
|
||||
resource.destroyResource(id).then(() => {
|
||||
message.success("删除成功");
|
||||
resetList();
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log("Cancel");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除课件
|
||||
const removeResourceMulti = () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
confirm({
|
||||
title: "操作确认",
|
||||
icon: <ExclamationCircleFilled />,
|
||||
content: "删除前请检查选中课件文件无关联课程,确认删除?",
|
||||
centered: true,
|
||||
okText: "确认",
|
||||
cancelText: "取消",
|
||||
onOk() {
|
||||
resource.destroyResourceMulti(selectedRowKeys).then(() => {
|
||||
message.success("删除成功");
|
||||
resetList();
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log("Cancel");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const downLoadFile = (url: string) => {
|
||||
console.log(url);
|
||||
window.open(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tree-main-body">
|
||||
<div className="left-box">
|
||||
<TreeCategory
|
||||
selected={category_ids}
|
||||
type="no-cate"
|
||||
text={"课件"}
|
||||
onUpdate={(keys: any, title: any) => {
|
||||
setPage(1);
|
||||
setCategoryIds(keys);
|
||||
if (typeof title === "string") {
|
||||
setLabel(title);
|
||||
} else {
|
||||
setLabel(title.props.children[0]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="right-box">
|
||||
<div className="d-flex playedu-main-title float-left mb-24">
|
||||
课件 | {selLabel}
|
||||
</div>
|
||||
<div className="float-left j-b-flex mb-24">
|
||||
<div>
|
||||
<UploadCoursewareButton
|
||||
categoryIds={category_ids}
|
||||
onUpdate={() => {
|
||||
resetList();
|
||||
}}
|
||||
></UploadCoursewareButton>
|
||||
<Button
|
||||
type="default"
|
||||
className="ml-16"
|
||||
onClick={() => {
|
||||
setSelectedRowKeys([]);
|
||||
setMultiConfig(!multiConfig);
|
||||
}}
|
||||
>
|
||||
{multiConfig ? "取消操作" : "批量操作"}
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
className="ml-16"
|
||||
onClick={() => removeResourceMulti()}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
<div className="d-flex">
|
||||
<div className="d-flex mr-24">
|
||||
<Typography.Text>名称:</Typography.Text>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
}}
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
placeholder="请输入名称关键字"
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex mr-24">
|
||||
<Typography.Text>格式:</Typography.Text>
|
||||
<Select
|
||||
style={{ width: 160 }}
|
||||
placeholder="请选择格式"
|
||||
value={type}
|
||||
onChange={(value: string) => setType(value)}
|
||||
options={types}
|
||||
/>
|
||||
</div>
|
||||
<Button className="mr-16" onClick={resetList}>
|
||||
重 置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
>
|
||||
查 询
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="float-left">
|
||||
{multiConfig ? (
|
||||
<Table
|
||||
rowSelection={{
|
||||
type: "checkbox",
|
||||
...rowSelection,
|
||||
}}
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
loading={loading}
|
||||
pagination={paginationProps}
|
||||
rowKey={(record) => record.id}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
loading={loading}
|
||||
pagination={paginationProps}
|
||||
rowKey={(record) => record.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CoursewareUpdateDialog
|
||||
id={Number(updateId)}
|
||||
open={updateVisible}
|
||||
onCancel={() => setUpdateVisible(false)}
|
||||
onSuccess={() => {
|
||||
setUpdateVisible(false);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
></CoursewareUpdateDialog>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceCoursewarePage;
|
@ -183,33 +183,35 @@ const ResourceImagesPage = () => {
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={24}>
|
||||
<div className="j-b-flex">
|
||||
<div className="d-flex">
|
||||
<UploadImageSub
|
||||
categoryIds={category_ids}
|
||||
onUpdate={() => {
|
||||
resetImageList();
|
||||
}}
|
||||
></UploadImageSub>
|
||||
<div className="d-flex">
|
||||
{selectKey.length > 0 && (
|
||||
<Button className="mr-16" onClick={() => cancelAll()}>
|
||||
取消选择
|
||||
<Button className="ml-16" onClick={() => cancelAll()}>
|
||||
取消操作
|
||||
</Button>
|
||||
)}
|
||||
{selectKey.length === 0 && (
|
||||
<Button className="ml-16" onClick={() => selectAll()}>
|
||||
批量操作
|
||||
</Button>
|
||||
)}
|
||||
{imageList.length !== 0 && (
|
||||
<>
|
||||
<Button className="mr-16" onClick={() => selectAll()}>
|
||||
全选
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-16"
|
||||
disabled={selectKey.length === 0}
|
||||
type="primary"
|
||||
onClick={() => removeResource()}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="d-flex"></div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -158,6 +158,15 @@ const ResourceCategoryPage = () => {
|
||||
res.data.videos.length === 0
|
||||
) {
|
||||
delUser(id);
|
||||
} else if (
|
||||
res.data.children &&
|
||||
res.data.children.length === 0 &&
|
||||
res.data.courses &&
|
||||
res.data.courses.length === 0 &&
|
||||
!res.data.images &&
|
||||
!res.data.videos
|
||||
) {
|
||||
delUser(id);
|
||||
} else {
|
||||
if (res.data.children && res.data.children.length > 0) {
|
||||
modal.warning({
|
||||
|
@ -7,7 +7,7 @@ import { useLocation } from "react-router-dom";
|
||||
import { DownOutlined, ExclamationCircleFilled } from "@ant-design/icons";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { dateFormat } from "../../../utils/index";
|
||||
import { TreeCategory, DurationText, PerButton } from "../../../compenents";
|
||||
import { TreeCategory, DurationText } from "../../../compenents";
|
||||
import { UploadVideoButton } from "../../../compenents/upload-video-button";
|
||||
import { VideoPlayDialog } from "./compenents/video-play-dialog";
|
||||
import { VideosUpdateDialog } from "./compenents/update-dialog";
|
||||
@ -83,7 +83,7 @@ const ResourceVideosPage = () => {
|
||||
JSON.stringify(adminUsers) !== "{}" && <span>{adminUsers[text]}</span>,
|
||||
},
|
||||
{
|
||||
title: "视频时长",
|
||||
title: "创建时间",
|
||||
dataIndex: "created_at",
|
||||
render: (text: string) => <span>{dateFormat(text)}</span>,
|
||||
},
|
||||
@ -286,11 +286,9 @@ const ResourceVideosPage = () => {
|
||||
resetVideoList();
|
||||
}}
|
||||
></UploadVideoButton>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
<Button
|
||||
type="default"
|
||||
className="mr-16"
|
||||
className="ml-16"
|
||||
onClick={() => {
|
||||
setSelectedRowKeys([]);
|
||||
setMultiConfig(!multiConfig);
|
||||
@ -299,6 +297,7 @@ const ResourceVideosPage = () => {
|
||||
{multiConfig ? "取消操作" : "批量操作"}
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-16"
|
||||
type="default"
|
||||
onClick={() => removeResourceMulti()}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
@ -306,6 +305,7 @@ const ResourceVideosPage = () => {
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
<div className="d-flex"></div>
|
||||
</div>
|
||||
<div className="float-left">
|
||||
{multiConfig ? (
|
||||
|
57
src/pages/system/adminlog/compenents/detail-dialog.tsx
Normal file
57
src/pages/system/adminlog/compenents/detail-dialog.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form } from "antd";
|
||||
|
||||
interface PropInterface {
|
||||
param: string;
|
||||
result: string;
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const AdminLogDetailDialog: React.FC<PropInterface> = ({
|
||||
param,
|
||||
open,
|
||||
onCancel,
|
||||
result,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const onFinish = (values: any) => {};
|
||||
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log("Failed:", errorInfo);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="日志详情"
|
||||
centered
|
||||
forceRender
|
||||
open={open}
|
||||
width={416}
|
||||
onOk={() => onCancel()}
|
||||
onCancel={() => onCancel()}
|
||||
footer={null}
|
||||
maskClosable={false}
|
||||
>
|
||||
<div className="mt-24">
|
||||
<Form
|
||||
form={form}
|
||||
name="adminlog-detail"
|
||||
labelCol={{ span: 5 }}
|
||||
wrapperCol={{ span: 19 }}
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item label="Param">{param}</Form.Item>
|
||||
<Form.Item label="Result">{result}</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
0
src/pages/system/adminlog/index.module.less
Normal file
0
src/pages/system/adminlog/index.module.less
Normal file
224
src/pages/system/adminlog/index.tsx
Normal file
224
src/pages/system/adminlog/index.tsx
Normal file
@ -0,0 +1,224 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Table, Typography, Input, Button, DatePicker } from "antd";
|
||||
import { adminLog } from "../../../api";
|
||||
// import styles from "./index.module.less";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { dateWholeFormat, transUtcTime } from "../../../utils/index";
|
||||
import { AdminLogDetailDialog } from "./compenents/detail-dialog";
|
||||
const { RangePicker } = DatePicker;
|
||||
import moment from "moment";
|
||||
|
||||
interface DataType {
|
||||
id: React.Key;
|
||||
admin_id: number;
|
||||
ip: string;
|
||||
opt: string;
|
||||
admin_name: string;
|
||||
module: string;
|
||||
created_at: string;
|
||||
title: string;
|
||||
ip_area: string;
|
||||
param: string;
|
||||
result: string;
|
||||
}
|
||||
|
||||
const SystemLogPage = () => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(10);
|
||||
const [list, setList] = useState<any>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [title, setTitle] = useState("");
|
||||
const [adminId, setAdminId] = useState("");
|
||||
const [adminName, setAdminName] = useState("");
|
||||
const [created_at, setCreatedAt] = useState<any>([]);
|
||||
const [createdAts, setCreatedAts] = useState<any>([]);
|
||||
const [param, setParam] = useState("");
|
||||
const [result, setResult] = useState("");
|
||||
const [visiable, setVisiable] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getData();
|
||||
}, [refresh, page, size]);
|
||||
|
||||
const getData = () => {
|
||||
setLoading(true);
|
||||
adminLog
|
||||
.adminLogList(
|
||||
page,
|
||||
size,
|
||||
adminName,
|
||||
title,
|
||||
"",
|
||||
created_at[0],
|
||||
created_at[1]
|
||||
)
|
||||
.then((res: any) => {
|
||||
setList(res.data.data);
|
||||
setTotal(res.data.total);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const resetData = () => {
|
||||
setTitle("");
|
||||
setAdminId("");
|
||||
setAdminName("");
|
||||
setPage(1);
|
||||
setSize(10);
|
||||
setList([]);
|
||||
setCreatedAts([]);
|
||||
setCreatedAt([]);
|
||||
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) => {
|
||||
setPage(page);
|
||||
setSize(pageSize);
|
||||
};
|
||||
|
||||
const disabledDate = (current: any) => {
|
||||
return current && current >= moment().add(0, "days"); // 选择时间要大于等于当前天。若今天不能被选择,去掉等号即可。
|
||||
};
|
||||
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: "管理员名称",
|
||||
width: 150,
|
||||
render: (_, record: any) => <span>{record.admin_name}</span>,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
render: (_, record: any) => <span>{record.title}</span>,
|
||||
},
|
||||
{
|
||||
title: "IP地区",
|
||||
width: 250,
|
||||
dataIndex: "ip_area",
|
||||
render: (ip_area: string) => <span>{ip_area}</span>,
|
||||
},
|
||||
{
|
||||
title: "时间",
|
||||
width: 200,
|
||||
dataIndex: "created_at",
|
||||
render: (created_at: string) => (
|
||||
<span>{dateWholeFormat(created_at)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right",
|
||||
width: 160,
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
className="b-link c-red"
|
||||
onClick={() => {
|
||||
setParam(record.param);
|
||||
setResult(record.result);
|
||||
setVisiable(true);
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="playedu-main-body">
|
||||
<div className="float-left j-b-flex mb-24">
|
||||
<div className="d-flex"></div>
|
||||
<div className="d-flex">
|
||||
<div className="d-flex mr-24">
|
||||
<Typography.Text>管理员名称:</Typography.Text>
|
||||
<Input
|
||||
value={adminName}
|
||||
onChange={(e) => {
|
||||
setAdminName(e.target.value);
|
||||
}}
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
placeholder="请输入管理员名称"
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex mr-24">
|
||||
<Typography.Text>操作:</Typography.Text>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
}}
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
placeholder="请输入操作"
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex mr-24">
|
||||
<Typography.Text>时间:</Typography.Text>
|
||||
<RangePicker
|
||||
disabledDate={disabledDate}
|
||||
format={"YYYY-MM-DD"}
|
||||
value={createdAts}
|
||||
style={{ marginLeft: 10 }}
|
||||
onChange={(date, dateString) => {
|
||||
let date1 = dateString[0] + " 00:00:00";
|
||||
let date2 = dateString[1] + " 23:59:59";
|
||||
dateString[0] = transUtcTime(date1);
|
||||
dateString[1] = transUtcTime(date2);
|
||||
setCreatedAt(dateString);
|
||||
setCreatedAts(date);
|
||||
}}
|
||||
placeholder={["时间-开始", "时间-结束"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
<Button className="mr-16" onClick={resetData}>
|
||||
重 置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
>
|
||||
查 询
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="float-left">
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
rowKey={(record) => record.id}
|
||||
pagination={paginationProps}
|
||||
/>
|
||||
</div>
|
||||
<AdminLogDetailDialog
|
||||
param={param}
|
||||
result={result}
|
||||
open={visiable}
|
||||
onCancel={() => setVisiable(false)}
|
||||
></AdminLogDetailDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemLogPage;
|
@ -62,6 +62,11 @@ export const SystemAdminrolesCreate: React.FC<PropInterface> = ({
|
||||
value: "管理员-n",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "管理员日志",
|
||||
value: "管理员日志-n",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "管理员角色",
|
||||
value: "管理员角色-n",
|
||||
|
@ -65,6 +65,11 @@ export const SystemAdminrolesUpdate: React.FC<PropInterface> = ({
|
||||
value: "管理员-n",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "管理员日志",
|
||||
value: "管理员日志-n",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "管理员角色",
|
||||
value: "管理员角色-n",
|
||||
|
@ -7,7 +7,9 @@ import KeepAlive from "../compenents/keep-alive";
|
||||
// 页面加载
|
||||
import InitPage from "../pages/init";
|
||||
import LoginPage from "../pages/login";
|
||||
import HomePage from "../pages/home";
|
||||
import WithHeaderWithoutFooter from "../pages/layouts/with-header-without-footer";
|
||||
import WithoutHeaderWithoutFooter from "../pages/layouts/without-header-without-footer";
|
||||
|
||||
//首页
|
||||
const DashboardPage = lazy(() => import("../pages/dashboard"));
|
||||
//修改密码页面
|
||||
@ -18,6 +20,9 @@ const ResourceCategoryPage = lazy(
|
||||
);
|
||||
const ResourceImagesPage = lazy(() => import("../pages/resource/images"));
|
||||
const ResourceVideosPage = lazy(() => import("../pages/resource/videos"));
|
||||
const ResourceCoursewarePage = lazy(
|
||||
() => import("../pages/resource/courseware")
|
||||
);
|
||||
//课程相关
|
||||
const CoursePage = lazy(() => import("../pages/course/index"));
|
||||
const CourseUserPage = lazy(() => import("../pages/course/user"));
|
||||
@ -34,6 +39,7 @@ const SystemAdministratorPage = lazy(
|
||||
() => import("../pages/system/administrator")
|
||||
);
|
||||
const SystemAdminrolesPage = lazy(() => import("../pages/system/adminroles"));
|
||||
const SystemLogPage = lazy(() => import("../pages/system/adminlog"));
|
||||
//部门页面
|
||||
const DepartmentPage = lazy(() => import("../pages/department"));
|
||||
//测试
|
||||
@ -77,7 +83,7 @@ const routes: RouteObject[] = [
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <PrivateRoute Component={<HomePage />} />,
|
||||
element: <PrivateRoute Component={<WithHeaderWithoutFooter />} />,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
@ -99,6 +105,10 @@ const routes: RouteObject[] = [
|
||||
path: "/videos",
|
||||
element: <PrivateRoute Component={<ResourceVideosPage />} />,
|
||||
},
|
||||
{
|
||||
path: "/courseware",
|
||||
element: <PrivateRoute Component={<ResourceCoursewarePage />} />,
|
||||
},
|
||||
{
|
||||
path: "/course",
|
||||
element: <PrivateRoute Component={<CoursePage />} />,
|
||||
@ -143,12 +153,20 @@ const routes: RouteObject[] = [
|
||||
path: "/system/adminroles",
|
||||
element: <PrivateRoute Component={<SystemAdminrolesPage />} />,
|
||||
},
|
||||
{
|
||||
path: "/system/adminlog",
|
||||
element: <PrivateRoute Component={<SystemLogPage />} />,
|
||||
},
|
||||
{
|
||||
path: "/department",
|
||||
element: <PrivateRoute Component={<DepartmentPage />} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
element: <WithoutHeaderWithoutFooter />,
|
||||
children: [
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginPage />,
|
||||
@ -157,12 +175,18 @@ const routes: RouteObject[] = [
|
||||
path: "/test",
|
||||
element: <TestPage />,
|
||||
},
|
||||
{
|
||||
path: "/error",
|
||||
element: <ErrorPage />,
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
element: <ErrorPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
@ -133,3 +133,20 @@ export function checkUrl(value: any) {
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function dateWholeFormat(dateStr: string) {
|
||||
if (!dateStr) {
|
||||
return "";
|
||||
}
|
||||
return moment(dateStr).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
export function transUtcTime(value: string) {
|
||||
const specifiedTime = value;
|
||||
// 创建一个新的Date对象,传入指定时间
|
||||
const specifiedDate = new Date(specifiedTime);
|
||||
//将指定时间转换为UTC+0时间
|
||||
const utcTime = specifiedDate.toISOString();
|
||||
|
||||
return utcTime;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user