diff --git a/src/api/admin-log.ts b/src/api/admin-log.ts new file mode 100644 index 0000000..806c607 --- /dev/null +++ b/src/api/admin-log.ts @@ -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, + }); +} diff --git a/src/api/course-attachment.ts b/src/api/course-attachment.ts new file mode 100644 index 0000000..8629805 --- /dev/null +++ b/src/api/course-attachment.ts @@ -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, + }); +} diff --git a/src/api/course.ts b/src/api/course.ts index bb93ea1..87dedc4 100644 --- a/src/api/course.ts +++ b/src/api/course.ts @@ -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, }); } diff --git a/src/api/index.ts b/src/api/index.ts index ad717e8..927f052 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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"; diff --git a/src/api/internal/httpClient.ts b/src/api/internal/httpClient.ts index 0b29793..b3856ef 100644 --- a/src/api/internal/httpClient.ts +++ b/src/api/internal/httpClient.ts @@ -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); } diff --git a/src/api/upload.ts b/src/api/upload.ts index 8011867..53f20b2 100644 --- a/src/api/upload.ts +++ b/src/api/upload.ts @@ -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, diff --git a/src/assets/iconfont/iconfont.css b/src/assets/iconfont/iconfont.css index bfbcd32..a6751c1 100644 --- a/src/assets/iconfont/iconfont.css +++ b/src/assets/iconfont/iconfont.css @@ -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"; } diff --git a/src/assets/iconfont/iconfont.ttf b/src/assets/iconfont/iconfont.ttf index ab333cf..80df28f 100644 Binary files a/src/assets/iconfont/iconfont.ttf and b/src/assets/iconfont/iconfont.ttf differ diff --git a/src/assets/iconfont/iconfont.woff b/src/assets/iconfont/iconfont.woff index 2830f65..ca17632 100644 Binary files a/src/assets/iconfont/iconfont.woff and b/src/assets/iconfont/iconfont.woff differ diff --git a/src/assets/iconfont/iconfont.woff2 b/src/assets/iconfont/iconfont.woff2 index 5dd256c..53acad6 100644 Binary files a/src/assets/iconfont/iconfont.woff2 and b/src/assets/iconfont/iconfont.woff2 differ diff --git a/src/compenents/index.ts b/src/compenents/index.ts index fd57bbe..6fb5c12 100644 --- a/src/compenents/index.ts +++ b/src/compenents/index.ts @@ -9,4 +9,7 @@ export * from "./tree-category"; export * from ".//tree-adminroles"; export * from "./duration-text"; export * from "./upload-video-sub"; -export * from "./select-resource"; \ No newline at end of file +export * from "./select-resource"; +export * from "./upload-courseware-button"; +export * from "./upload-courseware-sub"; +export * from "./select-attachment"; diff --git a/src/compenents/left-menu/index.tsx b/src/compenents/left-menu/index.tsx index 4b3a579..85f7715 100644 --- a/src/compenents/left-menu/index.tsx +++ b/src/compenents/left-menu/index.tsx @@ -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"], diff --git a/src/compenents/select-attachment/index.tsx b/src/compenents/select-attachment/index.tsx new file mode 100644 index 0000000..8739670 --- /dev/null +++ b/src/compenents/select-attachment/index.tsx @@ -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([]); + const [selectVideos, setSelectVideos] = useState([]); + + useEffect(() => { + setRefresh(!refresh); + }, [props.open]); + + const items: TabsProps["items"] = [ + { + key: "1", + label: `课件`, + children: ( +
+ { + setSelectKeys(arr); + setSelectVideos(videos); + }} + /> +
+ ), + }, + ]; + + const onChange = (key: string) => { + setTabKey(Number(key)); + }; + + return ( + <> + { + setSelectKeys([]); + setSelectVideos([]); + props.onCancel(); + }} + open={props.open} + width={800} + maskClosable={false} + onOk={() => { + props.onSelected(selectKeys, selectVideos); + setSelectKeys([]); + setSelectVideos([]); + }} + > + + + + + + ); +}; diff --git a/src/compenents/select-resource/index.tsx b/src/compenents/select-resource/index.tsx index e7f9525..017cf30 100644 --- a/src/compenents/select-resource/index.tsx +++ b/src/compenents/select-resource/index.tsx @@ -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([]); const [selectVideos, setSelectVideos] = useState([]); + useEffect(() => { + setRefresh(!refresh); + }, [props.open]); + const items: TabsProps["items"] = [ { key: "1", diff --git a/src/compenents/upload-courseware-button/index.tsx b/src/compenents/upload-courseware-button/index.tsx new file mode 100644 index 0000000..741b1ae --- /dev/null +++ b/src/compenents/upload-courseware-button/index.tsx @@ -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([]); + const intervalId = useRef(); + const [fileList, setFileList] = useState([]); + + 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 ( + <> + + + {showModal ? ( + { + closeWin(); + }} + maskClosable={false} + closable={false} + onOk={() => { + closeWin(); + }} + okText="完成" + > + + + +

+ +

+

请将文件拖拽到此处上传

+

+ 支持一次上传多个 / + 支持word、excel、ppt、pdf、zip、rar、txt格式文件 +

+
+ + + {record.file.name}, + }, + { + title: "大小", + dataIndex: "size", + key: "size", + render: (_, record) => ( + + {(record.file.size / 1024 / 1024).toFixed(2)}M + + ), + }, + { + title: "进度", + dataIndex: "progress", + key: "progress", + render: (_, record: FileItem) => ( + <> + {record.upload.status === 0 ? ( + "等待上传" + ) : ( + + )} + + ), + }, + { + title: "操作", + key: "action", + render: (_, record) => ( + <> + {record.upload.status === 5 ? ( + {record.upload.remark} + ) : null} + + {record.upload.status === 7 ? ( + 上传成功 + ) : null} + + ), + }, + ]} + dataSource={fileList} + /> + + + + ) : null} + + ); +}; diff --git a/src/compenents/upload-courseware-sub/index.module.less b/src/compenents/upload-courseware-sub/index.module.less new file mode 100644 index 0000000..e69de29 diff --git a/src/compenents/upload-courseware-sub/index.tsx b/src/compenents/upload-courseware-sub/index.tsx new file mode 100644 index 0000000..d2d4689 --- /dev/null +++ b/src/compenents/upload-courseware-sub/index.tsx @@ -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([]); + const [loading, setLoading] = useState(false); + const [videoList, setVideoList] = useState([]); + const [existingTypes, setExistingTypes] = useState([]); + const [refresh, setRefresh] = useState(false); + const [page, setPage] = useState(1); + const [size, setSize] = useState(10); + const [total, setTotal] = useState(0); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + // 加载列表 + 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 = [ + { + title: "课件", + render: (_, record: any) => ( +
+ +
{record.name}
+
+ ), + }, + { + title: "类型", + render: (_, record: any) => {record.type}, + }, + ]; + + 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 ( + <> + +
+ setCategoryIds(keys)} + /> + + + + + { + resetVideoList(); + }} + > + + +
+ {videoList.length === 0 && ( +
+ + + )} + {videoList.length > 0 && ( +
+
record.id} + /> + + )} + + + + + ); +}; diff --git a/src/compenents/upload-video-sub/index.tsx b/src/compenents/upload-video-sub/index.tsx index 530e63c..b9fe6db 100644 --- a/src/compenents/upload-video-sub/index.tsx +++ b/src/compenents/upload-video-sub/index.tsx @@ -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([]); - const [plainOptions, setPlainOptions] = useState([]); - const [checkedList, setCheckedList] = useState([]); - 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: ( -
- -
{data[i].name}
-
- -
-
- ), - 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); - } - }); - 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, - }); + const handlePageChange = (page: number, pageSize: number) => { + setPage(page); + setSize(pageSize); + }; + + const columns: ColumnsType = [ + { + title: "视频", + render: (_, record: any) => ( +
+ +
{record.name}
+
+ ), + }, + { + title: "时长", + render: (_, record: any) => ( +
+ +
+ ), + }, + ]; + + 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, + 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 && (
- - 全选 - - record.id} />
)} - - {videoList.length > 0 && total > 10 && ( -
- { - setPage(currentPage); - setSize(currentSize); - }} - defaultCurrent={page} - total={total} - /> - - )} - diff --git a/src/index.less b/src/index.less index 283a914..5251d39 100644 --- a/src/index.less +++ b/src/index.less @@ -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; + } +} diff --git a/src/main.tsx b/src/main.tsx index 98bf043..b20c456 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( diff --git a/src/pages/course/compenents/attachment-update.module.scss b/src/pages/course/compenents/attachment-update.module.scss new file mode 100644 index 0000000..cc25058 --- /dev/null +++ b/src/pages/course/compenents/attachment-update.module.scss @@ -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; + } +} diff --git a/src/pages/course/compenents/attachment-update.tsx b/src/pages/course/compenents/attachment-update.tsx new file mode 100644 index 0000000..233d5fe --- /dev/null +++ b/src/pages/course/compenents/attachment-update.tsx @@ -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 = ({ + id, + open, + onCancel, +}) => { + const [form] = Form.useForm(); + const [attachmentVisible, setAttachmentVisible] = useState(false); + const [attachmentData, setAttachmentData] = useState([]); + const [attachments, setAttachments] = useState([]); + + 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: , + 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 ( + <> + +
+

1.线上课课件调整及时生效,操作不可逆,请谨慎操作。

+
+
+ { + setAttachmentVisible(false); + }} + onSelected={(arr: any, videos: any) => { + selectAttachmentData(arr, videos); + }} + > +
+
+ +
+ +
+
+
+ {attachmentData.length === 0 && ( + + 请点击上方按钮添加课件 + + )} + {attachmentData.length > 0 && ( + { + delAttachments(id); + }} + onUpdate={(arr: any[]) => { + transAttachments(arr); + }} + /> + )} +
+
+ +
+
+ + ); +}; diff --git a/src/pages/course/compenents/attachments.tsx b/src/pages/course/compenents/attachments.tsx new file mode 100644 index 0000000..5a0aa58 --- /dev/null +++ b/src/pages/course/compenents/attachments.tsx @@ -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([]); + const [loading, setLoading] = useState(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: ( +
+
+ +
{hours[i].name}
+
+ + + + removeItem(hours[i].rid)} + /> +
+ ), + 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 ( +
+ +
+ ); +}; diff --git a/src/pages/course/compenents/create.tsx b/src/pages/course/compenents/create.tsx index 23cd599..cba360e 100644 --- a/src/pages/course/compenents/create.tsx +++ b/src/pages/course/compenents/create.tsx @@ -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 = ({ cateIds, depIds, @@ -58,6 +57,10 @@ export const CourseCreate: React.FC = ({ const [videoVisible, setVideoVisible] = useState(false); const [treeData, setTreeData] = useState([]); const [addvideoCurrent, setAddvideoCurrent] = useState(0); + const [showDrop, setShowDrop] = useState(false); + const [attachmentVisible, setAttachmentVisible] = useState(false); + const [attachmentData, setAttachmentData] = useState([]); + const [attachments, setAttachments] = useState([]); useEffect(() => { if (open) { @@ -80,6 +83,9 @@ export const CourseCreate: React.FC = ({ setChapterHours([]); setHours([]); setTreeData([]); + setAttachmentData([]); + setAttachments([]); + setShowDrop(false); }, [form, open]); const getParams = () => { @@ -224,7 +230,8 @@ export const CourseCreate: React.FC = ({ dep_ids, values.category_ids, chapters, - treeData + treeData, + attachmentData ) .then((res: any) => { message.success("保存成功!"); @@ -268,6 +275,20 @@ export const CourseCreate: React.FC = ({ 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 = ({ 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 = ({ } }} /> + { + setAttachmentVisible(false); + }} + onSelected={(arr: any, videos: any) => { + selectAttachmentData(arr, videos); + }} + >
= ({ - - - = ({ {chapterType === 0 && ( -
+
)} {chapterType === 1 && ( -
+
{chapters.length > 0 && chapters.map((item: any, index: number) => { return ( @@ -737,6 +790,57 @@ export const CourseCreate: React.FC = ({
)} + +
setShowDrop(!showDrop)} + > + + (课程简介、课件) +
+
+
+ + + + + + +
+ {attachmentData.length === 0 && ( + + 请点击上方按钮添加课件 + + )} + {attachmentData.length > 0 && ( + { + delAttachments(id); + }} + onUpdate={(arr: any[]) => { + transAttachments(arr); + }} + /> + )} +
+
diff --git a/src/pages/course/compenents/hour-update.tsx b/src/pages/course/compenents/hour-update.tsx index 14d5f33..d4b8b9f 100644 --- a/src/pages/course/compenents/hour-update.tsx +++ b/src/pages/course/compenents/hour-update.tsx @@ -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 = ({ id, open, @@ -106,16 +100,14 @@ export const CourseHourUpdate: React.FC = ({ 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, - title: videos[i].name, - type: videos[i].type, - duration: videos[i].duration, - rid: videos[i].rid, - }); - } + hours.push({ + chapter_id: 0, + sort: treeData.length + i, + title: videos[i].name, + type: videos[i].type, + duration: videos[i].duration, + rid: videos[i].rid, + }); } if (hours.length === 0) { message.error("请选择视频"); @@ -141,16 +133,14 @@ export const CourseHourUpdate: React.FC = ({ } 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, - title: videos[i].name, - type: videos[i].type, - duration: videos[i].duration, - rid: videos[i].rid, - }); - } + hours.push({ + chapter_id: data[addvideoCurrent].id, + sort: data[addvideoCurrent].hours.length + i, + title: videos[i].name, + type: videos[i].type, + duration: videos[i].duration, + rid: videos[i].rid, + }); } if (hours.length === 0) { message.error("请选择视频"); diff --git a/src/pages/course/compenents/update.tsx b/src/pages/course/compenents/update.tsx index 41d6cb4..c36b3ae 100644 --- a/src/pages/course/compenents/update.tsx +++ b/src/pages/course/compenents/update.tsx @@ -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 = ({ id, open, diff --git a/src/pages/course/index.tsx b/src/pages/course/index.tsx index a4643c1..5374f6b 100644 --- a/src/pages/course/index.tsx +++ b/src/pages/course/index.tsx @@ -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(false); const [updateVisible, setUpdateVisible] = useState(false); const [updateHourVisible, setHourUpdateVisible] = useState(false); + const [updateAttachmentVisible, setUpdateAttachmentVisible] = + useState(false); const [cid, setCid] = useState(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: ( + + ), + }, + { + key: "4", label: (
diff --git a/src/pages/error/index.tsx b/src/pages/error/index.tsx index 9a1a37d..e71298f 100644 --- a/src/pages/error/index.tsx +++ b/src/pages/error/index.tsx @@ -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 ( { useEffect(() => {}, []); @@ -17,8 +19,10 @@ const HomePage = () => {
- {/* 二级路由出口 */} - + }> + {/* 二级路由出口 */} + {" "} +
diff --git a/src/pages/layouts/without-header-without-footer/index.module.less b/src/pages/layouts/without-header-without-footer/index.module.less new file mode 100644 index 0000000..d41ce28 --- /dev/null +++ b/src/pages/layouts/without-header-without-footer/index.module.less @@ -0,0 +1,8 @@ +.layout-wrap { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; +} diff --git a/src/pages/layouts/without-header-without-footer/index.tsx b/src/pages/layouts/without-header-without-footer/index.tsx new file mode 100644 index 0000000..31cd2a7 --- /dev/null +++ b/src/pages/layouts/without-header-without-footer/index.tsx @@ -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 ( +
+ }> + + +
+ ); +}; + +export default WithoutHeaderWithoutFooter; diff --git a/src/pages/loading/index.module.less b/src/pages/loading/index.module.less index 0ada21a..ca9d22e 100644 --- a/src/pages/loading/index.module.less +++ b/src/pages/loading/index.module.less @@ -1,6 +1,6 @@ -.loading-parent-box { - width: 100vd; - height: 100vh; - text-align: center; +.loading-box { + width: 100vw; line-height: 100vh; + text-align: center; } + diff --git a/src/pages/loading/index.tsx b/src/pages/loading/index.tsx index 58a90d9..2e51159 100644 --- a/src/pages/loading/index.tsx +++ b/src/pages/loading/index.tsx @@ -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 ( -
- -
+ <> +
+ +
+ ); }; diff --git a/src/pages/resource/courseware/compenents/update-dialog/index.tsx b/src/pages/resource/courseware/compenents/update-dialog/index.tsx new file mode 100644 index 0000000..32f88da --- /dev/null +++ b/src/pages/resource/courseware/compenents/update-dialog/index.tsx @@ -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 = ({ + id, + open, + onCancel, + onSuccess, +}) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(true); + const [categories, setCategories] = useState([]); + + 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 ( + <> + form.submit()} + onCancel={() => onCancel()} + maskClosable={false} + > +
+
+ + + + + + + +
+
+ + ); +}; diff --git a/src/pages/resource/courseware/index.module.less b/src/pages/resource/courseware/index.module.less new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/resource/courseware/index.tsx b/src/pages/resource/courseware/index.tsx new file mode 100644 index 0000000..fe49a49 --- /dev/null +++ b/src/pages/resource/courseware/index.tsx @@ -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([]); + const [adminUsers, setAdminUsers] = useState({}); + const [existingTypes, setExistingTypes] = useState([]); + const [refresh, setRefresh] = useState(false); + const [page, setPage] = useState(1); + const [size, setSize] = useState(10); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(true); + const [category_ids, setCategoryIds] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [type, setType] = useState("WORD,EXCEL,PPT,PDF,TXT,RAR,ZIP"); + const [title, setTitle] = useState(""); + const [multiConfig, setMultiConfig] = useState(false); + const [selLabel, setLabel] = useState( + result.get("label") ? String(result.get("label")) : "全部课件" + ); + const [cateId, setCateId] = useState(Number(result.get("cid"))); + const [updateId, setUpdateId] = useState(0); + const [updateVisible, setUpdateVisible] = useState(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 = [ + { + title: "课件名称", + render: (_, record: any) => ( +
+ + + {record.name}.{record.extension} + +
+ ), + }, + { + title: "课件格式", + dataIndex: "type", + width: 204, + render: (type: string) => {type}, + }, + { + title: "课件大小", + dataIndex: "size", + width: 204, + render: (size: number) => {(size / 1024 / 1024).toFixed(2)}M, + }, + { + title: "创建人", + dataIndex: "admin_id", + width: 204, + render: (text: number) => + JSON.stringify(adminUsers) !== "{}" && {adminUsers[text]}, + }, + { + title: "创建时间", + dataIndex: "created_at", + width: 204, + render: (text: string) => {dateFormat(text)}, + }, + { + title: "操作", + key: "action", + fixed: "right", + width: 180, + render: (_, record: any) => { + return ( + + +
+ +
+ +
+ ); + }, + }, + ]; + + 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: , + 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: , + 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 ( + <> +
+
+ { + setPage(1); + setCategoryIds(keys); + if (typeof title === "string") { + setLabel(title); + } else { + setLabel(title.props.children[0]); + } + }} + /> +
+
+
+ 课件 | {selLabel} +
+
+
+ { + resetList(); + }} + > + + +
+
+
+
+ 名称: + { + setTitle(e.target.value); + }} + allowClear + style={{ width: 160 }} + placeholder="请输入名称关键字" + /> +
+
+ 格式: +
record.id} + /> + ) : ( +
record.id} + /> + )} + + + setUpdateVisible(false)} + onSuccess={() => { + setUpdateVisible(false); + setRefresh(!refresh); + }} + > + + + ); +}; + +export default ResourceCoursewarePage; diff --git a/src/pages/resource/images/index.tsx b/src/pages/resource/images/index.tsx index f6c9d8f..bc4f09f 100644 --- a/src/pages/resource/images/index.tsx +++ b/src/pages/resource/images/index.tsx @@ -183,33 +183,35 @@ const ResourceImagesPage = () => {
- { - resetImageList(); - }} - >
+ { + resetImageList(); + }} + > {selectKey.length > 0 && ( - + )} + {selectKey.length === 0 && ( + )} {imageList.length !== 0 && ( - <> - - - + )}
+
diff --git a/src/pages/resource/resource-category/index.tsx b/src/pages/resource/resource-category/index.tsx index 79e0a53..1566605 100644 --- a/src/pages/resource/resource-category/index.tsx +++ b/src/pages/resource/resource-category/index.tsx @@ -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({ diff --git a/src/pages/resource/videos/index.tsx b/src/pages/resource/videos/index.tsx index 15790a9..4376509 100644 --- a/src/pages/resource/videos/index.tsx +++ b/src/pages/resource/videos/index.tsx @@ -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) !== "{}" && {adminUsers[text]}, }, { - title: "视频时长", + title: "创建时间", dataIndex: "created_at", render: (text: string) => {dateFormat(text)}, }, @@ -286,11 +286,9 @@ const ResourceVideosPage = () => { resetVideoList(); }} > - -
+
{multiConfig ? ( diff --git a/src/pages/system/adminlog/compenents/detail-dialog.tsx b/src/pages/system/adminlog/compenents/detail-dialog.tsx new file mode 100644 index 0000000..1c62d52 --- /dev/null +++ b/src/pages/system/adminlog/compenents/detail-dialog.tsx @@ -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 = ({ + param, + open, + onCancel, + result, +}) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(true); + + const onFinish = (values: any) => {}; + + const onFinishFailed = (errorInfo: any) => { + console.log("Failed:", errorInfo); + }; + + return ( + <> + onCancel()} + onCancel={() => onCancel()} + footer={null} + maskClosable={false} + > +
+
+ {param} + {result} + +
+
+ + ); +}; diff --git a/src/pages/system/adminlog/index.module.less b/src/pages/system/adminlog/index.module.less new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/system/adminlog/index.tsx b/src/pages/system/adminlog/index.tsx new file mode 100644 index 0000000..754d2d1 --- /dev/null +++ b/src/pages/system/adminlog/index.tsx @@ -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(true); + const [page, setPage] = useState(1); + const [size, setSize] = useState(10); + const [list, setList] = useState([]); + 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([]); + const [createdAts, setCreatedAts] = useState([]); + 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 = [ + { + title: "管理员名称", + width: 150, + render: (_, record: any) => {record.admin_name}, + }, + { + title: "操作", + render: (_, record: any) => {record.title}, + }, + { + title: "IP地区", + width: 250, + dataIndex: "ip_area", + render: (ip_area: string) => {ip_area}, + }, + { + title: "时间", + width: 200, + dataIndex: "created_at", + render: (created_at: string) => ( + {dateWholeFormat(created_at)} + ), + }, + { + title: "操作", + key: "action", + fixed: "right", + width: 160, + render: (_, record) => ( + + ), + }, + ]; + + return ( +
+
+
+
+
+ 管理员名称: + { + setAdminName(e.target.value); + }} + allowClear + style={{ width: 160 }} + placeholder="请输入管理员名称" + /> +
+
+ 操作: + { + setTitle(e.target.value); + }} + allowClear + style={{ width: 160 }} + placeholder="请输入操作" + /> +
+
+ 时间: + { + 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={["时间-开始", "时间-结束"]} + /> +
+
+ + +
+
+
+
+
record.id} + pagination={paginationProps} + /> + + setVisiable(false)} + > + + ); +}; + +export default SystemLogPage; diff --git a/src/pages/system/adminroles/compenents/create.tsx b/src/pages/system/adminroles/compenents/create.tsx index f6ca78a..cf5d0ba 100644 --- a/src/pages/system/adminroles/compenents/create.tsx +++ b/src/pages/system/adminroles/compenents/create.tsx @@ -62,6 +62,11 @@ export const SystemAdminrolesCreate: React.FC = ({ value: "管理员-n", children: [], }, + { + title: "管理员日志", + value: "管理员日志-n", + children: [], + }, { title: "管理员角色", value: "管理员角色-n", diff --git a/src/pages/system/adminroles/compenents/update.tsx b/src/pages/system/adminroles/compenents/update.tsx index 0638e2e..d2b2639 100644 --- a/src/pages/system/adminroles/compenents/update.tsx +++ b/src/pages/system/adminroles/compenents/update.tsx @@ -65,6 +65,11 @@ export const SystemAdminrolesUpdate: React.FC = ({ value: "管理员-n", children: [], }, + { + title: "管理员日志", + value: "管理员日志-n", + children: [], + }, { title: "管理员角色", value: "管理员角色-n", diff --git a/src/routes/index.tsx b/src/routes/index.tsx index f44e6f8..b137f33 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -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: } />, + element: } />, children: [ { path: "/", @@ -99,6 +105,10 @@ const routes: RouteObject[] = [ path: "/videos", element: } />, }, + { + path: "/courseware", + element: } />, + }, { path: "/course", element: } />, @@ -143,6 +153,10 @@ const routes: RouteObject[] = [ path: "/system/adminroles", element: } />, }, + { + path: "/system/adminlog", + element: } />, + }, { path: "/department", element: } />, @@ -150,16 +164,26 @@ const routes: RouteObject[] = [ ], }, { - path: "/login", - element: , - }, - { - path: "/test", - element: , - }, - { - path: "*", - element: , + path: "/", + element: , + children: [ + { + path: "/login", + element: , + }, + { + path: "/test", + element: , + }, + { + path: "/error", + element: , + }, + { + path: "*", + element: , + }, + ], }, ], }, diff --git a/src/utils/index.ts b/src/utils/index.ts index 05c0a0c..b37888b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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; +}