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 8143d58..6fb5c12 100644 --- a/src/compenents/index.ts +++ b/src/compenents/index.ts @@ -11,3 +11,5 @@ export * from "./duration-text"; export * from "./upload-video-sub"; export * from "./select-resource"; export * from "./upload-courseware-button"; +export * from "./upload-courseware-sub"; +export * from "./select-attachment"; diff --git a/src/compenents/select-attachment/index.tsx b/src/compenents/select-attachment/index.tsx new file mode 100644 index 0000000..2129965 --- /dev/null +++ b/src/compenents/select-attachment/index.tsx @@ -0,0 +1,77 @@ +import { useEffect, useState } from "react"; +import { Button, Row, Modal, message, Tabs } from "antd"; +import styles from "./index.module.less"; +import { UploadCoursewareSub } from "../../compenents"; +import type { TabsProps } from "antd"; + +interface VideoItem { + id: number; + category_id: number; + name: string; + duration: number; +} + +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([]); + + 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/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..23d7985 --- /dev/null +++ b/src/compenents/upload-courseware-sub/index.tsx @@ -0,0 +1,242 @@ +import { useEffect, useState } from "react"; +import { Checkbox, Row, Col, Empty, message, Pagination } from "antd"; +import { resource } from "../../api"; +import styles from "./index.module.less"; +import { TreeCategory, UploadCoursewareButton } 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; +} + +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 [plainOptions, setPlainOptions] = useState([]); + const [checkedList, setCheckedList] = useState([]); + const [indeterminate, setIndeterminate] = useState(false); + const [checkAll, setCheckAll] = useState(false); + + // 获取列表 + const getvideoList = (defaultKeys: any[]) => { + 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); + let data = res.data.result.data; + const arr = []; + for (let i = 0; i < data.length; i++) { + arr.push({ + label: ( +
+ +
{data[i].name}
+
{data[i].type}
+
+ ), + 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); + setVideoList([]); + 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, + disabled: plainOptions[index].disabled, + }); + } + }); + } + props.onSelected(localKeys, arrVideos); + }; + + 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, + disabled: plainOptions[index].disabled, + }); + } + }); + } + if (e.target.checked) { + props.onSelected(localKeys, arrVideos); + } else { + props.onSelected([], []); + } + }; + + return ( + <> + + + setCategoryIds(keys)} + /> + + + + + { + resetVideoList(); + }} + > + + +
+ {videoList.length === 0 && ( + + + + )} + {videoList.length > 0 && ( +
+ + 全选 + + +
+ )} +
+ + {videoList.length > 0 && total > 10 && ( + + { + setPage(currentPage); + setSize(currentSize); + }} + defaultCurrent={page} + total={total} + /> + + )} + + +
+ + ); +}; diff --git a/src/index.less b/src/index.less index 2931939..5858831 100644 --- a/src/index.less +++ b/src/index.less @@ -596,6 +596,7 @@ textarea.ant-input { .ant-checkbox-wrapper { margin-inline-start: 0px; height: 38px; + align-items: center !important; } .video-title { @@ -660,3 +661,26 @@ 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/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 1d82990..79989cd 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; @@ -58,6 +63,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 +89,7 @@ export const CourseCreate: React.FC = ({ setChapterHours([]); setHours([]); setTreeData([]); + setAttachmentData([]); }, [form, open]); const getParams = () => { @@ -225,7 +235,7 @@ export const CourseCreate: React.FC = ({ values.category_ids, chapters, treeData, - [] + attachmentData ) .then((res: any) => { message.success("保存成功!"); @@ -269,6 +279,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) { @@ -327,6 +351,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]; @@ -446,6 +500,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 ( @@ -738,6 +794,54 @@ export const CourseCreate: React.FC = ({
)} + +
setShowDrop(!showDrop)} + > + + (课程简介、课件) +
+
+
+ + + + + + +
+ {attachmentData.length === 0 && ( + + 请点击上方按钮添加课件 + + )} + {attachmentData.length > 0 && ( + { + delAttachments(id); + }} + onUpdate={(arr: any[]) => { + transAttachments(arr); + }} + /> + )} +
+