aerge branch 'dev'

This commit is contained in:
none 2023-07-31 14:40:17 +08:00
commit 518dea2b57
47 changed files with 2389 additions and 282 deletions

21
src/api/admin-log.ts Normal file
View 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,
});
}

View 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,
});
}

View File

@ -35,7 +35,8 @@ export function storeCourse(
depIds: number[], depIds: number[],
categoryIds: number[], categoryIds: number[],
chapters: number[], chapters: number[],
hours: number[] hours: number[],
attachments: any[]
) { ) {
return client.post("/backend/v1/course/create", { return client.post("/backend/v1/course/create", {
title: title, title: title,
@ -47,6 +48,7 @@ export function storeCourse(
category_ids: categoryIds, category_ids: categoryIds,
chapters: chapters, chapters: chapters,
hours: hours, hours: hours,
attachments: attachments,
}); });
} }

View File

@ -6,6 +6,7 @@ export * as courseCategory from "./course-category";
export * as courseChapter from "./course-chapter"; export * as courseChapter from "./course-chapter";
export * as course from "./course"; export * as course from "./course";
export * as courseHour from "./course-hour"; export * as courseHour from "./course-hour";
export * as courseAttachment from "./course-attachment";
export * as department from "./department"; export * as department from "./department";
export * as resourceCategory from "./resource-category"; export * as resourceCategory from "./resource-category";
export * as resource from "./resource"; export * as resource from "./resource";
@ -13,3 +14,4 @@ export * as upload from "./upload";
export * as user from "./user"; export * as user from "./user";
export * as appConfig from "./app-config"; export * as appConfig from "./app-config";
export * as dashboard from "./dashboard"; export * as dashboard from "./dashboard";
export * as adminLog from "./admin-log";

View File

@ -7,6 +7,10 @@ const GoLogin = () => {
window.location.href = "/login"; window.location.href = "/login";
}; };
const GoError = (code: number) => {
// window.location.href = "/error?code=" + code;
};
export class HttpClient { export class HttpClient {
axios: Axios; axios: Axios;
@ -39,7 +43,24 @@ export class HttpClient {
if (code === 0) { if (code === 0) {
return Promise.resolve(response); 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 { } else {
GoError(code);
message.error(msg); message.error(msg);
} }
return Promise.reject(response); return Promise.reject(response);
@ -52,13 +73,18 @@ export class HttpClient {
GoLogin(); GoLogin();
} else if (status === 404) { } else if (status === 404) {
// 跳转到404页面 // 跳转到404页面
GoLogin(); GoError(404);
} else if (status === 403) { } else if (status === 403) {
// 跳转到无权限页面 // 跳转到无权限页面
GoLogin(); GoError(403);
} else if (status === 429) {
// 跳转到429页面
GoError(429);
} else if (status === 500) { } else if (status === 500) {
// 跳转到500异常页面 // 跳转到500异常页面
GoLogin(); GoError(500);
} else {
GoError(status);
} }
return Promise.reject(error.response); return Promise.reject(error.response);
} }

View File

@ -27,7 +27,7 @@ export function minioMergeVideo(
duration: number, duration: number,
poster: string poster: string
) { ) {
return client.post("/backend/v1/upload/minio/merge-video", { return client.post("/backend/v1/upload/minio/merge-file", {
filename, filename,
upload_id: uploadId, upload_id: uploadId,
original_filename: originalFilename, original_filename: originalFilename,

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3943555 */ font-family: "iconfont"; /* Project id 3943555 */
src: url('iconfont.woff2?t=1679383201256') format('woff2'), src: url('iconfont.woff2?t=1690600882833') format('woff2'),
url('iconfont.woff?t=1679383201256') format('woff'), url('iconfont.woff?t=1690600882833') format('woff'),
url('iconfont.ttf?t=1679383201256') format('truetype'); url('iconfont.ttf?t=1690600882833') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,54 @@
-moz-osx-font-smoothing: grayscale; -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 { .icon-icon-tips:before {
content: "\e74a"; content: "\e74a";
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -10,3 +10,6 @@ export * from ".//tree-adminroles";
export * from "./duration-text"; export * from "./duration-text";
export * from "./upload-video-sub"; export * from "./upload-video-sub";
export * from "./select-resource"; export * from "./select-resource";
export * from "./upload-courseware-button";
export * from "./upload-courseware-sub";
export * from "./select-attachment";

View File

@ -46,6 +46,7 @@ const items = [
[ [
getItem("视频", "/videos", null, null, null, null), getItem("视频", "/videos", null, null, null, null),
getItem("图片", "/images", null, null, null, null), getItem("图片", "/images", null, null, null, null),
getItem("课件", "/courseware", null, null, null, null),
], ],
null, null,
null null
@ -90,6 +91,7 @@ const items = [
null, null,
"admin-user-index" "admin-user-index"
), ),
getItem("管理日志", "/system/adminlog", null, null, null, "admin-log"),
// getItem("角色配置", "/system/adminroles", null, null, null, null), // getItem("角色配置", "/system/adminroles", null, null, null, null),
], ],
null, null,
@ -103,6 +105,7 @@ export const LeftMenu: React.FC = () => {
const children2Parent: any = { const children2Parent: any = {
"^/video": ["resource"], "^/video": ["resource"],
"^/image": ["resource"], "^/image": ["resource"],
"^/courseware": ["resource"],
"^/member": ["user"], "^/member": ["user"],
"^/department": ["user"], "^/department": ["user"],
"^/course": ["courses"], "^/course": ["courses"],

View 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>
</>
);
};

View File

@ -1,16 +1,9 @@
import { useEffect, useState } from "react"; 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 styles from "./index.module.less";
import { UploadVideoSub } from "../../compenents"; import { UploadVideoSub } from "../../compenents";
import type { TabsProps } from "antd"; import type { TabsProps } from "antd";
interface VideoItem {
id: number;
category_id: number;
name: string;
duration: number;
}
interface PropsInterface { interface PropsInterface {
defaultKeys: any[]; defaultKeys: any[];
open: boolean; open: boolean;
@ -24,6 +17,10 @@ export const SelectResource = (props: PropsInterface) => {
const [selectKeys, setSelectKeys] = useState<any>([]); const [selectKeys, setSelectKeys] = useState<any>([]);
const [selectVideos, setSelectVideos] = useState<any>([]); const [selectVideos, setSelectVideos] = useState<any>([]);
useEffect(() => {
setRefresh(!refresh);
}, [props.open]);
const items: TabsProps["items"] = [ const items: TabsProps["items"] = [
{ {
key: "1", key: "1",

View 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">
/
wordexcelpptpdfziprartxt格式文件
</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}
</>
);
};

View 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>
</>
);
};

View File

@ -1,19 +1,33 @@
import { useEffect, useState } from "react"; 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 { resource } from "../../api";
import styles from "./index.module.less"; import styles from "./index.module.less";
import { UploadVideoButton } from "../upload-video-button"; import { UploadVideoButton } from "../upload-video-button";
import { DurationText, TreeCategory } from "../../compenents"; 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 { interface VideoItem {
id: number; id: number;
category_id: number;
name: string; 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 { interface PropsInterface {
@ -32,14 +46,21 @@ export const UploadVideoSub = (props: PropsInterface) => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [size, setSize] = useState(10); const [size, setSize] = useState(10);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
const [plainOptions, setPlainOptions] = useState<any>([]); // 加载列表
const [checkedList, setCheckedList] = useState<CheckboxValueType[]>([]); useEffect(() => {
const [indeterminate, setIndeterminate] = useState(false); getvideoList();
const [checkAll, setCheckAll] = useState(false); }, [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(","); let categoryIds = category_ids.join(",");
resource resource
.resourceList(page, size, "", "", "", "VIDEO", categoryIds) .resourceList(page, size, "", "", "", "VIDEO", categoryIds)
@ -47,46 +68,12 @@ export const UploadVideoSub = (props: PropsInterface) => {
setTotal(res.data.result.total); setTotal(res.data.result.total);
setVideoExtra(res.data.videos_extra); setVideoExtra(res.data.videos_extra);
setVideoList(res.data.result.data); 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) => { .catch((err) => {
console.log("错误,", err); console.log("错误,", err);
}); });
}; };
// 重置列表 // 重置列表
const resetVideoList = () => { const resetVideoList = () => {
setPage(1); setPage(1);
@ -94,78 +81,71 @@ export const UploadVideoSub = (props: PropsInterface) => {
setRefresh(!refresh); setRefresh(!refresh);
}; };
// 加载列表 const paginationProps = {
useEffect(() => { current: page, //当前页码
const arr = [...props.defaultCheckedList]; pageSize: size,
setCheckedList(arr); total: total, // 总条数
if (arr.length === 0) { onChange: (page: number, pageSize: number) =>
setIndeterminate(false); handlePageChange(page, pageSize), //改变页码的函数
setCheckAll(false); showSizeChanger: true,
}
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 onCheckAllChange = (e: CheckboxChangeEvent) => { const handlePageChange = (page: number, pageSize: number) => {
const arr = plainOptions.map((item: any) => item.value); setPage(page);
setCheckedList(e.target.checked ? arr : []); setSize(pageSize);
setIndeterminate(false); };
setCheckAll(e.target.checked);
const defalut = [...props.defaultCheckedList]; const columns: ColumnsType<DataType> = [
let localKeys: any = []; {
arr.map((item: any) => { title: "视频",
if (defalut.indexOf(item) === -1) { render: (_, record: any) => (
localKeys.push(item); <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 = []; let arrVideos: any = [];
for (let i = 0; i < localKeys.length; i++) { if (row) {
videoList.map((item: any, index: number) => { for (var i = 0; i < row.length; i++) {
if (item.id === localKeys[i]) { if (props.defaultCheckedList.indexOf(row[i].id) === -1) {
arrVideos.push({ arrVideos.push({
name: item.name, name: row[i].name,
type: item.type, type: row[i].type,
rid: item.id, rid: row[i].id,
duration: videosExtra[item.id].duration, duration: videosExtra[row[i].id].duration,
disabled: plainOptions[index].disabled,
}); });
} }
});
} }
if (e.target.checked) { props.onSelected(selectedRowKeys, arrVideos);
props.onSelected(localKeys, arrVideos);
} else {
props.onSelected([], []);
} }
setSelectedRowKeys(selectedRowKeys);
},
getCheckboxProps: (record: any) => ({
disabled: props.defaultCheckedList.indexOf(record.id) !== -1, //禁用的条件
}),
}; };
return ( return (
@ -198,43 +178,20 @@ export const UploadVideoSub = (props: PropsInterface) => {
)} )}
{videoList.length > 0 && ( {videoList.length > 0 && (
<div className="list-select-column-box c-flex"> <div className="list-select-column-box c-flex">
<Checkbox <Table
indeterminate={indeterminate} rowSelection={{
onChange={onCheckAllChange} type: "checkbox",
checked={checkAll} ...rowSelection,
> }}
columns={columns}
</Checkbox> dataSource={videoList}
<CheckboxGroup loading={loading}
className="c-flex" pagination={paginationProps}
options={plainOptions} rowKey={(record) => record.id}
value={checkedList}
onChange={onChange}
/> />
</div> </div>
)} )}
</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> </Col>
</Row> </Row>
</> </>

View File

@ -525,7 +525,7 @@ textarea.ant-input {
&:hover { &:hover {
box-shadow: none !important; box-shadow: none !important;
color: #ff4d4f !important; color: #ff4d4f !important;
border-color: #ff4d4f; border-color: #ff4d4f !important;
outline: none; outline: none;
} }
} }
@ -593,22 +593,18 @@ textarea.ant-input {
} }
.list-select-column-box { .list-select-column-box {
.ant-checkbox-wrapper { width: 100%;
margin-inline-start: 0px; height: auto;
height: 38px; float: left;
.ant-table-cell {
padding: 0px 0px;
} }
.video-title { .video-title {
width: 390px; width: 360px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.video-time {
width: 80px;
display: flex;
flex-direction: row-reverse;
}
} }
.image-list-box { .image-list-box {
@ -660,3 +656,25 @@ textarea.ant-input {
transform: rotate(90deg); 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;
}
}

View File

@ -7,6 +7,7 @@ import { Provider } from "react-redux";
import store from "./store"; import store from "./store";
import { ConfigProvider } from "antd"; import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN"; import zhCN from "antd/locale/zh_CN";
import "dayjs/locale/zh-cn";
import AutoScorllTop from "./AutoTop"; import AutoScorllTop from "./AutoTop";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(

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

View 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>
</>
);
};

View 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>
);
};

View File

@ -14,9 +14,14 @@ import {
import styles from "./create.module.less"; import styles from "./create.module.less";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { course, department } from "../../../api/index"; import { course, department } from "../../../api/index";
import { UploadImageButton, SelectResource } from "../../../compenents"; import {
UploadImageButton,
SelectResource,
SelectAttachment,
} from "../../../compenents";
import { ExclamationCircleFilled } from "@ant-design/icons"; import { ExclamationCircleFilled } from "@ant-design/icons";
import { TreeHours } from "./hours"; import { TreeHours } from "./hours";
import { TreeAttachments } from "./attachments";
const { confirm } = Modal; const { confirm } = Modal;
@ -27,12 +32,6 @@ interface PropInterface {
onCancel: () => void; onCancel: () => void;
} }
interface Option {
value: string | number;
title: string;
children?: Option[];
}
export const CourseCreate: React.FC<PropInterface> = ({ export const CourseCreate: React.FC<PropInterface> = ({
cateIds, cateIds,
depIds, depIds,
@ -58,6 +57,10 @@ export const CourseCreate: React.FC<PropInterface> = ({
const [videoVisible, setVideoVisible] = useState<boolean>(false); const [videoVisible, setVideoVisible] = useState<boolean>(false);
const [treeData, setTreeData] = useState<any>([]); const [treeData, setTreeData] = useState<any>([]);
const [addvideoCurrent, setAddvideoCurrent] = useState(0); 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(() => { useEffect(() => {
if (open) { if (open) {
@ -80,6 +83,9 @@ export const CourseCreate: React.FC<PropInterface> = ({
setChapterHours([]); setChapterHours([]);
setHours([]); setHours([]);
setTreeData([]); setTreeData([]);
setAttachmentData([]);
setAttachments([]);
setShowDrop(false);
}, [form, open]); }, [form, open]);
const getParams = () => { const getParams = () => {
@ -224,7 +230,8 @@ export const CourseCreate: React.FC<PropInterface> = ({
dep_ids, dep_ids,
values.category_ids, values.category_ids,
chapters, chapters,
treeData treeData,
attachmentData
) )
.then((res: any) => { .then((res: any) => {
message.success("保存成功!"); message.success("保存成功!");
@ -268,6 +275,20 @@ export const CourseCreate: React.FC<PropInterface> = ({
setVideoVisible(false); 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 getChapterType = (e: any) => {
const arr = [...chapters]; const arr = [...chapters];
if (arr.length > 0) { if (arr.length > 0) {
@ -326,6 +347,36 @@ export const CourseCreate: React.FC<PropInterface> = ({
setTreeData(newArr); 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 addNewChapter = () => {
const arr = [...chapters]; const arr = [...chapters];
const keys = [...chapterHours]; 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={form} form={form}
name="create-basic" name="create-basic"
@ -621,14 +682,6 @@ export const CourseCreate: React.FC<PropInterface> = ({
</div> </div>
</div> </div>
</Form.Item> </Form.Item>
<Form.Item label="课程简介" name="short_desc">
<Input.TextArea
style={{ width: 424, minHeight: 80 }}
allowClear
placeholder="请输入课程简介最多200字"
maxLength={200}
/>
</Form.Item>
<Form.Item <Form.Item
label="课时列表" label="课时列表"
name="hasChapter" name="hasChapter"
@ -642,7 +695,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
{chapterType === 0 && ( {chapterType === 0 && (
<div className="c-flex"> <div className="c-flex mb-24">
<Form.Item> <Form.Item>
<div className="ml-120"> <div className="ml-120">
<Button <Button
@ -674,7 +727,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
</div> </div>
)} )}
{chapterType === 1 && ( {chapterType === 1 && (
<div className="c-flex"> <div className="c-flex mb-24">
{chapters.length > 0 && {chapters.length > 0 &&
chapters.map((item: any, index: number) => { chapters.map((item: any, index: number) => {
return ( return (
@ -737,6 +790,57 @@ export const CourseCreate: React.FC<PropInterface> = ({
</Form.Item> </Form.Item>
</div> </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> </Form>
</div> </div>
</Drawer> </Drawer>

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; 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 styles from "./hour-update.module.less";
import { course, courseHour, courseChapter } from "../../../api/index"; import { course, courseHour, courseChapter } from "../../../api/index";
import { SelectResource } from "../../../compenents"; import { SelectResource } from "../../../compenents";
@ -14,12 +14,6 @@ interface PropInterface {
onCancel: () => void; onCancel: () => void;
} }
interface Option {
value: string | number;
label: string;
children?: Option[];
}
export const CourseHourUpdate: React.FC<PropInterface> = ({ export const CourseHourUpdate: React.FC<PropInterface> = ({
id, id,
open, open,
@ -106,7 +100,6 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
const selectData = (arr: any, videos: any) => { const selectData = (arr: any, videos: any) => {
const hours: any = []; const hours: any = [];
for (let i = 0; i < videos.length; i++) { for (let i = 0; i < videos.length; i++) {
if (videos[i].disabled === false) {
hours.push({ hours.push({
chapter_id: 0, chapter_id: 0,
sort: treeData.length + i, sort: treeData.length + i,
@ -116,7 +109,6 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
rid: videos[i].rid, rid: videos[i].rid,
}); });
} }
}
if (hours.length === 0) { if (hours.length === 0) {
message.error("请选择视频"); message.error("请选择视频");
return; return;
@ -141,7 +133,6 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
} }
const hours: any = []; const hours: any = [];
for (let i = 0; i < videos.length; i++) { for (let i = 0; i < videos.length; i++) {
if (videos[i].disabled === false) {
hours.push({ hours.push({
chapter_id: data[addvideoCurrent].id, chapter_id: data[addvideoCurrent].id,
sort: data[addvideoCurrent].hours.length + i, sort: data[addvideoCurrent].hours.length + i,
@ -151,7 +142,6 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
rid: videos[i].rid, rid: videos[i].rid,
}); });
} }
}
if (hours.length === 0) { if (hours.length === 0) {
message.error("请选择视频"); message.error("请选择视频");
return; return;

View File

@ -16,20 +16,12 @@ import { useSelector } from "react-redux";
import { course, department } from "../../../api/index"; import { course, department } from "../../../api/index";
import { UploadImageButton } from "../../../compenents"; import { UploadImageButton } from "../../../compenents";
const { confirm } = Modal;
interface PropInterface { interface PropInterface {
id: number; id: number;
open: boolean; open: boolean;
onCancel: () => void; onCancel: () => void;
} }
interface Option {
value: string | number;
title: string;
children?: Option[];
}
export const CourseUpdate: React.FC<PropInterface> = ({ export const CourseUpdate: React.FC<PropInterface> = ({
id, id,
open, open,

View File

@ -27,6 +27,7 @@ import type { TabsProps } from "antd";
import { CourseCreate } from "./compenents/create"; import { CourseCreate } from "./compenents/create";
import { CourseUpdate } from "./compenents/update"; import { CourseUpdate } from "./compenents/update";
import { CourseHourUpdate } from "./compenents/hour-update"; import { CourseHourUpdate } from "./compenents/hour-update";
import { CourseAttachmentUpdate } from "./compenents/attachment-update";
const { confirm } = Modal; const { confirm } = Modal;
@ -66,6 +67,8 @@ const CoursePage = () => {
const [createVisible, setCreateVisible] = useState<boolean>(false); const [createVisible, setCreateVisible] = useState<boolean>(false);
const [updateVisible, setUpdateVisible] = useState<boolean>(false); const [updateVisible, setUpdateVisible] = useState<boolean>(false);
const [updateHourVisible, setHourUpdateVisible] = useState<boolean>(false); const [updateHourVisible, setHourUpdateVisible] = useState<boolean>(false);
const [updateAttachmentVisible, setUpdateAttachmentVisible] =
useState<boolean>(false);
const [cid, setCid] = useState<number>(0); const [cid, setCid] = useState<number>(0);
const [cateId, setCateId] = useState(Number(result.get("cid"))); const [cateId, setCateId] = useState(Number(result.get("cid")));
const [did, setDid] = useState(Number(result.get("did"))); const [did, setDid] = useState(Number(result.get("did")));
@ -241,6 +244,23 @@ const CoursePage = () => {
}, },
{ {
key: "3", 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: ( label: (
<Button <Button
type="link" type="link"
@ -455,6 +475,14 @@ const CoursePage = () => {
setRefresh(!refresh); setRefresh(!refresh);
}} }}
/> />
<CourseAttachmentUpdate
id={cid}
open={updateAttachmentVisible}
onCancel={() => {
setUpdateAttachmentVisible(false);
setRefresh(!refresh);
}}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,15 +1,35 @@
import { useEffect, useState } from "react";
import { Button, Result } from "antd"; 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"; import styles from "./index.module.less";
const ErrorPage = () => { const ErrorPage = () => {
const navigate = useNavigate(); 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 ( return (
<Result <Result
status="404" status="404"
title="404" title={code}
subTitle="您访问的页面不存在" subTitle={error}
className={styles["main"]} className={styles["main"]}
extra={ extra={
<Button <Button

View File

@ -1,7 +1,9 @@
import React, { useEffect } from "react"; import { useEffect } from "react";
import styles from "./index.module.less"; import styles from "./index.module.less";
import { Outlet } from "react-router-dom"; 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 = () => { const HomePage = () => {
useEffect(() => {}, []); useEffect(() => {}, []);
@ -17,8 +19,10 @@ const HomePage = () => {
<Header></Header> <Header></Header>
</div> </div>
<div className={styles["right-main"]}> <div className={styles["right-main"]}>
<Suspense fallback={<LoadingPage height="100vh" />}>
{/* 二级路由出口 */} {/* 二级路由出口 */}
<Outlet /> <Outlet />{" "}
</Suspense>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,8 @@
.layout-wrap {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
}

View 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;

View File

@ -1,6 +1,6 @@
.loading-parent-box { .loading-box {
width: 100vd; width: 100vw;
height: 100vh;
text-align: center;
line-height: 100vh; line-height: 100vh;
text-align: center;
} }

View File

@ -1,11 +1,20 @@
import { Spin } from "antd"; import { Spin } from "antd";
import styles from "./index.module.less"; import styles from "./index.module.less";
const LoadingPage = () => { interface PropsInterface {
height?: string;
}
const LoadingPage = (props: PropsInterface) => {
return ( return (
<div className={styles["loading-parent-box"]}> <>
<div
className={styles["loading-box"]}
style={{ height: props.height || "100vh" }}
>
<Spin size="large" /> <Spin size="large" />
</div> </div>
</>
); );
}; };

View 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>
</>
);
};

View 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;

View File

@ -183,33 +183,35 @@ const ResourceImagesPage = () => {
<Row gutter={16} style={{ marginBottom: 24 }}> <Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={24}> <Col span={24}>
<div className="j-b-flex"> <div className="j-b-flex">
<div className="d-flex">
<UploadImageSub <UploadImageSub
categoryIds={category_ids} categoryIds={category_ids}
onUpdate={() => { onUpdate={() => {
resetImageList(); resetImageList();
}} }}
></UploadImageSub> ></UploadImageSub>
<div className="d-flex">
{selectKey.length > 0 && ( {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> </Button>
)} )}
{imageList.length !== 0 && ( {imageList.length !== 0 && (
<>
<Button className="mr-16" onClick={() => selectAll()}>
</Button>
<Button <Button
className="ml-16"
disabled={selectKey.length === 0} disabled={selectKey.length === 0}
type="primary" type="primary"
onClick={() => removeResource()} onClick={() => removeResource()}
> >
</Button> </Button>
</>
)} )}
</div> </div>
<div className="d-flex"></div>
</div> </div>
</Col> </Col>
</Row> </Row>

View File

@ -158,6 +158,15 @@ const ResourceCategoryPage = () => {
res.data.videos.length === 0 res.data.videos.length === 0
) { ) {
delUser(id); 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 { } else {
if (res.data.children && res.data.children.length > 0) { if (res.data.children && res.data.children.length > 0) {
modal.warning({ modal.warning({

View File

@ -7,7 +7,7 @@ import { useLocation } from "react-router-dom";
import { DownOutlined, ExclamationCircleFilled } from "@ant-design/icons"; import { DownOutlined, ExclamationCircleFilled } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table"; import type { ColumnsType } from "antd/es/table";
import { dateFormat } from "../../../utils/index"; import { dateFormat } from "../../../utils/index";
import { TreeCategory, DurationText, PerButton } from "../../../compenents"; import { TreeCategory, DurationText } from "../../../compenents";
import { UploadVideoButton } from "../../../compenents/upload-video-button"; import { UploadVideoButton } from "../../../compenents/upload-video-button";
import { VideoPlayDialog } from "./compenents/video-play-dialog"; import { VideoPlayDialog } from "./compenents/video-play-dialog";
import { VideosUpdateDialog } from "./compenents/update-dialog"; import { VideosUpdateDialog } from "./compenents/update-dialog";
@ -83,7 +83,7 @@ const ResourceVideosPage = () => {
JSON.stringify(adminUsers) !== "{}" && <span>{adminUsers[text]}</span>, JSON.stringify(adminUsers) !== "{}" && <span>{adminUsers[text]}</span>,
}, },
{ {
title: "视频时长", title: "创建时间",
dataIndex: "created_at", dataIndex: "created_at",
render: (text: string) => <span>{dateFormat(text)}</span>, render: (text: string) => <span>{dateFormat(text)}</span>,
}, },
@ -286,11 +286,9 @@ const ResourceVideosPage = () => {
resetVideoList(); resetVideoList();
}} }}
></UploadVideoButton> ></UploadVideoButton>
</div>
<div className="d-flex">
<Button <Button
type="default" type="default"
className="mr-16" className="ml-16"
onClick={() => { onClick={() => {
setSelectedRowKeys([]); setSelectedRowKeys([]);
setMultiConfig(!multiConfig); setMultiConfig(!multiConfig);
@ -299,6 +297,7 @@ const ResourceVideosPage = () => {
{multiConfig ? "取消操作" : "批量操作"} {multiConfig ? "取消操作" : "批量操作"}
</Button> </Button>
<Button <Button
className="ml-16"
type="default" type="default"
onClick={() => removeResourceMulti()} onClick={() => removeResourceMulti()}
disabled={selectedRowKeys.length === 0} disabled={selectedRowKeys.length === 0}
@ -306,6 +305,7 @@ const ResourceVideosPage = () => {
</Button> </Button>
</div> </div>
<div className="d-flex"></div>
</div> </div>
<div className="float-left"> <div className="float-left">
{multiConfig ? ( {multiConfig ? (

View 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>
</>
);
};

View 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;

View File

@ -62,6 +62,11 @@ export const SystemAdminrolesCreate: React.FC<PropInterface> = ({
value: "管理员-n", value: "管理员-n",
children: [], children: [],
}, },
{
title: "管理员日志",
value: "管理员日志-n",
children: [],
},
{ {
title: "管理员角色", title: "管理员角色",
value: "管理员角色-n", value: "管理员角色-n",

View File

@ -65,6 +65,11 @@ export const SystemAdminrolesUpdate: React.FC<PropInterface> = ({
value: "管理员-n", value: "管理员-n",
children: [], children: [],
}, },
{
title: "管理员日志",
value: "管理员日志-n",
children: [],
},
{ {
title: "管理员角色", title: "管理员角色",
value: "管理员角色-n", value: "管理员角色-n",

View File

@ -7,7 +7,9 @@ import KeepAlive from "../compenents/keep-alive";
// 页面加载 // 页面加载
import InitPage from "../pages/init"; import InitPage from "../pages/init";
import LoginPage from "../pages/login"; 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")); const DashboardPage = lazy(() => import("../pages/dashboard"));
//修改密码页面 //修改密码页面
@ -18,6 +20,9 @@ const ResourceCategoryPage = lazy(
); );
const ResourceImagesPage = lazy(() => import("../pages/resource/images")); const ResourceImagesPage = lazy(() => import("../pages/resource/images"));
const ResourceVideosPage = lazy(() => import("../pages/resource/videos")); const ResourceVideosPage = lazy(() => import("../pages/resource/videos"));
const ResourceCoursewarePage = lazy(
() => import("../pages/resource/courseware")
);
//课程相关 //课程相关
const CoursePage = lazy(() => import("../pages/course/index")); const CoursePage = lazy(() => import("../pages/course/index"));
const CourseUserPage = lazy(() => import("../pages/course/user")); const CourseUserPage = lazy(() => import("../pages/course/user"));
@ -34,6 +39,7 @@ const SystemAdministratorPage = lazy(
() => import("../pages/system/administrator") () => import("../pages/system/administrator")
); );
const SystemAdminrolesPage = lazy(() => import("../pages/system/adminroles")); const SystemAdminrolesPage = lazy(() => import("../pages/system/adminroles"));
const SystemLogPage = lazy(() => import("../pages/system/adminlog"));
//部门页面 //部门页面
const DepartmentPage = lazy(() => import("../pages/department")); const DepartmentPage = lazy(() => import("../pages/department"));
//测试 //测试
@ -77,7 +83,7 @@ const routes: RouteObject[] = [
children: [ children: [
{ {
path: "/", path: "/",
element: <PrivateRoute Component={<HomePage />} />, element: <PrivateRoute Component={<WithHeaderWithoutFooter />} />,
children: [ children: [
{ {
path: "/", path: "/",
@ -99,6 +105,10 @@ const routes: RouteObject[] = [
path: "/videos", path: "/videos",
element: <PrivateRoute Component={<ResourceVideosPage />} />, element: <PrivateRoute Component={<ResourceVideosPage />} />,
}, },
{
path: "/courseware",
element: <PrivateRoute Component={<ResourceCoursewarePage />} />,
},
{ {
path: "/course", path: "/course",
element: <PrivateRoute Component={<CoursePage />} />, element: <PrivateRoute Component={<CoursePage />} />,
@ -143,12 +153,20 @@ const routes: RouteObject[] = [
path: "/system/adminroles", path: "/system/adminroles",
element: <PrivateRoute Component={<SystemAdminrolesPage />} />, element: <PrivateRoute Component={<SystemAdminrolesPage />} />,
}, },
{
path: "/system/adminlog",
element: <PrivateRoute Component={<SystemLogPage />} />,
},
{ {
path: "/department", path: "/department",
element: <PrivateRoute Component={<DepartmentPage />} />, element: <PrivateRoute Component={<DepartmentPage />} />,
}, },
], ],
}, },
{
path: "/",
element: <WithoutHeaderWithoutFooter />,
children: [
{ {
path: "/login", path: "/login",
element: <LoginPage />, element: <LoginPage />,
@ -157,12 +175,18 @@ const routes: RouteObject[] = [
path: "/test", path: "/test",
element: <TestPage />, element: <TestPage />,
}, },
{
path: "/error",
element: <ErrorPage />,
},
{ {
path: "*", path: "*",
element: <ErrorPage />, element: <ErrorPage />,
}, },
], ],
}, },
],
},
]; ];
export default routes; export default routes;

View File

@ -133,3 +133,20 @@ export function checkUrl(value: any) {
} }
return url; 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;
}