Merge pull request #7 from PlayEdu/dev

Dev
This commit is contained in:
Teng 2023-06-13 14:10:48 +08:00 committed by GitHub
commit 7dc58e2fde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 792 additions and 158 deletions

View File

@ -8,9 +8,15 @@
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
<title>管理后台</title>
<script src="/js/DPlayer.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script
crossorigin="anonymous"
integrity="sha512-oHrfR/z2wkuRuaHrdZ9NhoT/o/1kteub+QvmQgVzOKK7NTvIKQMvnY9+/RR0+eW311o4lAE/YzzLXXmP2XUvig=="
src="https://lib.baomitu.com/hls.js/1.1.4/hls.min.js"
></script>
</body>
</html>

1
public/js/DPlayer.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
public/js/xg/hls.min.js vendored Normal file

File diff suppressed because one or more lines are too long

21
public/js/xg/index.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -61,3 +61,11 @@ export function destroyResourceMulti(ids: number[]) {
ids: ids,
});
}
export function videoDetail(id: number) {
return client.get(`/backend/v1/resource/${id}`, {});
}
export function videoUpdate(id: number, params: any) {
return client.put(`/backend/v1/resource/${id}`, params);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

View File

@ -1,16 +1,25 @@
import React, { useEffect, useState } from "react";
import { Menu } from "antd";
import { useSelector } from "react-redux";
import { useNavigate, useLocation } from "react-router-dom";
import styles from "./index.module.less";
import logo from "../../assets/logo.png";
function getItem(label: any, key: any, icon: any, children: any, type: any) {
function getItem(
label: any,
key: any,
icon: any,
children: any,
type: any,
permission: any
) {
return {
key,
icon,
children,
label,
type,
permission,
};
}
const items = [
@ -19,6 +28,7 @@ const items = [
"/",
<i className={`iconfont icon-icon-home`} />,
null,
null,
null
),
getItem(
@ -26,6 +36,7 @@ const items = [
"/resource-category",
<i className="iconfont icon-icon-category" />,
null,
null,
null
),
getItem(
@ -33,16 +44,18 @@ const items = [
"resource",
<i className="iconfont icon-icon-file" />,
[
getItem("视频", "/videos", null, null, null),
getItem("图片", "/images", null, null, null),
getItem("视频", "/videos", null, null, null, null),
getItem("图片", "/images", null, null, null, null),
],
null,
null
),
getItem(
"课程中心",
"courses",
<i className="iconfont icon-icon-study" />,
[getItem("线上课", "/course", null, null, null)],
[getItem("线上课", "/course", null, null, null, "course")],
null,
null
),
getItem(
@ -50,9 +63,10 @@ const items = [
"user",
<i className="iconfont icon-icon-user" />,
[
getItem("学员", "/member/index", null, null, null),
getItem("部门", "/department", null, null, null),
getItem("学员", "/member/index", null, null, null, "user-index"),
getItem("部门", "/department", null, null, null, "department-cud"),
],
null,
null
),
getItem(
@ -60,26 +74,40 @@ const items = [
"system",
<i className="iconfont icon-icon-setting" />,
[
getItem("系统配置", "/system/config/index", null, null, null),
getItem("管理人员", "/system/administrator", null, null, null),
// getItem("角色配置", "/system/adminroles", null, null, null),
getItem(
"系统配置",
"/system/config/index",
null,
null,
null,
"system-config"
),
getItem(
"管理人员",
"/system/administrator",
null,
null,
null,
"admin-user-index"
),
// getItem("角色配置", "/system/adminroles", null, null, null, null),
],
null,
null
),
];
const children2Parent: any = {
"^/video": ["resource"],
"^/image": ["resource"],
"^/member": ["user"],
"^/department": ["user"],
"^/course": ["courses"],
"^/system": ["system"],
};
export const LeftMenu: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const children2Parent: any = {
"^/video": ["resource"],
"^/image": ["resource"],
"^/member": ["user"],
"^/department": ["user"],
"^/course": ["courses"],
"^/system": ["system"],
};
const hit = (pathname: string): string[] => {
for (let p in children2Parent) {
@ -104,7 +132,6 @@ export const LeftMenu: React.FC = () => {
}
newOpenKeys.push(openKeys[i]);
}
return newOpenKeys;
};
@ -114,11 +141,54 @@ export const LeftMenu: React.FC = () => {
]);
// 展开菜单
const [openKeys, setOpenKeys] = useState<string[]>(hit(location.pathname));
const permissions = useSelector(
(state: any) => state.loginUser.value.permissions
);
const [activeMenus, setActiveMenus] = useState<any>([]);
const onClick = (e: any) => {
navigate(e.key);
};
useEffect(() => {
checkMenuPermissions(items, permissions);
}, [items, permissions]);
const checkMenuPermissions = (items: any, permissions: any) => {
let menus: any = [];
if (permissions.length === 0) {
setActiveMenus(menus);
return;
}
for (let i in items) {
let menuItem = items[i];
if (!menuItem.children) {
// 一级菜单不做权限控制
menus.push(menuItem);
continue;
}
let children = [];
for (let j in menuItem.children) {
let childrenItem = menuItem.children[j];
if (
typeof permissions[childrenItem.permission] !== "undefined" ||
!childrenItem.permission
) {
// 存在权限
children.push(childrenItem);
}
}
if (children.length > 0) {
menus.push(Object.assign({}, menuItem, { children: children }));
}
}
setActiveMenus(menus);
};
useEffect(() => {
if (location.pathname.indexOf("/course/user") !== -1) {
setSelectedKeys(["/course"]);
@ -159,7 +229,7 @@ export const LeftMenu: React.FC = () => {
selectedKeys={selectedKeys}
openKeys={openKeys}
mode="inline"
items={items}
items={activeMenus}
onSelect={(data: any) => {
setSelectedKeys(data.selectedKeys);
}}

View File

@ -13,13 +13,14 @@ interface PropInterface {
roleDelSuccess: boolean;
type: string;
text: string;
onUpdate: (keys: any, title: any) => void;
onUpdate: (keys: any, title: any, isSuper: boolean) => void;
}
export const TreeAdminroles = (props: PropInterface) => {
const [treeData, setTreeData] = useState<any>([]);
const [loading, setLoading] = useState<boolean>(true);
const [selectKey, setSelectKey] = useState<any>([]);
const [superId, setSuperId] = useState(0);
useEffect(() => {
onSelect([], "");
@ -28,6 +29,7 @@ export const TreeAdminroles = (props: PropInterface) => {
useEffect(() => {
adminRole.adminRoleList().then((res: any) => {
let adminrole = res.data;
let superId = 0;
if (adminrole.length > 0) {
const new_arr: Option[] = [];
for (let i = 0; i < adminrole.length; i++) {
@ -36,9 +38,13 @@ export const TreeAdminroles = (props: PropInterface) => {
key: adminrole[i].id,
children: [],
});
if (adminrole[i].slug === "super-role") {
superId = adminrole[i].id;
}
}
setTreeData(new_arr);
}
setSuperId(superId);
});
}, [props.refresh]);
@ -47,7 +53,11 @@ export const TreeAdminroles = (props: PropInterface) => {
if (info) {
label = info.node.title;
}
props.onUpdate(selectedKeys, label);
let isSuper = false;
if (selectedKeys[0] === superId && superId !== 0) {
isSuper = true;
}
props.onUpdate(selectedKeys, label, isSuper);
setSelectKey(selectedKeys);
};

View File

@ -5,13 +5,13 @@ import { resourceCategory } from "../../api/index";
interface Option {
key: string | number;
title: any;
children?: Option[];
}
interface PropInterface {
type: string;
text: string;
selected: any;
onUpdate: (keys: any, title: any) => void;
}
@ -20,6 +20,12 @@ export const TreeCategory = (props: PropInterface) => {
const [loading, setLoading] = useState<boolean>(true);
const [selectKey, setSelectKey] = useState<any>([]);
useEffect(() => {
if (props.selected && props.selected.length > 0) {
setSelectKey(props.selected);
}
}, [props.selected]);
useEffect(() => {
resourceCategory.resourceCategoryList().then((res: any) => {
const categories = res.data.categories;

View File

@ -13,6 +13,7 @@ interface PropInterface {
text: string;
refresh: boolean;
showNum: boolean;
selected: any;
onUpdate: (keys: any, title: any) => void;
}
@ -22,6 +23,12 @@ export const TreeDepartment = (props: PropInterface) => {
const [selectKey, setSelectKey] = useState<any>([]);
const [userTotal, setUserTotal] = useState(0);
useEffect(() => {
if (props.selected && props.selected.length > 0) {
setSelectKey(props.selected);
}
}, [props.selected]);
useEffect(() => {
setLoading(true);
department.departmentList().then((res: any) => {

View File

@ -110,6 +110,7 @@ export const UploadImageButton = (props: PropsInterface) => {
<Row style={{ width: 752, minHeight: 520, marginTop: 24 }}>
<Col span={7}>
<TreeCategory
selected={category_ids}
type="no-cate"
text={"图片"}
onUpdate={(keys: any) => {

View File

@ -2,7 +2,7 @@ import { Button, message, Modal } from "antd";
import Dragger from "antd/es/upload/Dragger";
import { useState } from "react";
import config from "../../../js/config";
import { getToken } from "../../../utils";
import { getToken, checkUrl } from "../../../utils";
import { InboxOutlined } from "@ant-design/icons";
interface PropsInterface {
@ -17,8 +17,8 @@ export const UploadImageSub = (props: PropsInterface) => {
name: "file",
multiple: true,
action:
config.app_url +
"/backend/v1/upload/minio?category_ids=" +
checkUrl(config.app_url) +
"backend/v1/upload/minio?category_ids=" +
props.categoryIds.join(","),
headers: {
authorization: "Bearer " + getToken(),

View File

@ -173,6 +173,7 @@ export const UploadVideoSub = (props: PropsInterface) => {
<Row style={{ width: 752, minHeight: 520 }}>
<Col span={7}>
<TreeCategory
selected={[]}
type="no-cate"
text={props.label}
onUpdate={(keys: any) => setCategoryIds(keys)}

View File

@ -519,16 +519,27 @@ textarea.ant-input {
position: relative;
}
.ant-modal-confirm-btns > .ant-btn-default:hover {
color: #ff4d4f !important;
border-color: #ff4d4f;
.ant-modal-confirm-btns > .ant-btn-default {
outline: none;
box-shadow: none !important;
&:hover {
box-shadow: none !important;
color: #ff4d4f !important;
border-color: #ff4d4f;
outline: none;
}
}
.ant-modal-confirm-btns > .ant-btn-primary {
border: none;
box-shadow: none !important;
background-color: #ff4d4f !important;
color: #fff;
outline: none;
&:hover {
box-shadow: none !important;
opacity: 0.8;
outline: none;
}
}
.ant-tree-treenode {

View File

@ -482,9 +482,9 @@ export const CourseCreate: React.FC<PropInterface> = ({
/>
</Form.Item>
<Form.Item
label="必修选修"
label="课程属性"
name="isRequired"
rules={[{ required: true, message: "请选择必修选修!" }]}
rules={[{ required: true, message: "请选择课程属性!" }]}
>
<Radio.Group>
<Radio value={1}></Radio>

View File

@ -230,9 +230,9 @@ export const CourseUpdate: React.FC<PropInterface> = ({
/>
</Form.Item>
<Form.Item
label="必修选修"
label="课程属性"
name="isRequired"
rules={[{ required: true, message: "请选择必修选修!" }]}
rules={[{ required: true, message: "请选择课程属性!" }]}
>
<Radio.Group>
<Radio value={1}></Radio>

View File

@ -21,7 +21,7 @@ import {
import type { MenuProps } from "antd";
import type { ColumnsType } from "antd/es/table";
import { dateFormat } from "../../utils/index";
import { useNavigate } from "react-router-dom";
import { useNavigate, useLocation } from "react-router-dom";
import { TreeDepartment, TreeCategory, PerButton } from "../../compenents";
import type { TabsProps } from "antd";
import { CourseCreate } from "./compenents/create";
@ -40,6 +40,7 @@ interface DataType {
}
const CoursePage = () => {
const result = new URLSearchParams(useLocation().search);
const navigate = useNavigate();
const [list, setList] = useState<any>([]);
const [refresh, setRefresh] = useState(false);
@ -50,18 +51,43 @@ const CoursePage = () => {
const [category_ids, setCategoryIds] = useState<any>([]);
const [title, setTitle] = useState<string>("");
const [dep_ids, setDepIds] = useState<any>([]);
const [selLabel, setLabel] = useState<string>("全部分类");
const [selDepLabel, setDepLabel] = useState<string>("全部部门");
const [selLabel, setLabel] = useState<string>(
result.get("label") ? String(result.get("label")) : "全部分类"
);
const [selDepLabel, setDepLabel] = useState<string>(
result.get("label") ? String(result.get("label")) : "全部部门"
);
const [course_category_ids, setCourseCategoryIds] = useState<any>({});
const [course_dep_ids, setCourseDepIds] = useState<any>({});
const [categories, setCategories] = useState<any>({});
const [departments, setDepartments] = useState<any>({});
const [tabKey, setTabKey] = useState(1);
const [tabKey, setTabKey] = useState(result.get("did") ? "2" : "1");
const [createVisible, setCreateVisible] = useState<boolean>(false);
const [updateVisible, setUpdateVisible] = useState<boolean>(false);
const [updateHourVisible, setHourUpdateVisible] = useState<boolean>(false);
const [cid, setCid] = useState<number>(0);
const [cateId, setCateId] = useState(Number(result.get("cid")));
const [did, setDid] = useState(Number(result.get("did")));
useEffect(() => {
getList();
}, [category_ids, dep_ids, refresh, page, size, tabKey]);
useEffect(() => {
setCateId(Number(result.get("cid")));
if (Number(result.get("cid"))) {
let arr = [];
arr.push(Number(result.get("cid")));
setCategoryIds(arr);
}
setDid(Number(result.get("did")));
if (Number(result.get("did"))) {
let arr = [];
arr.push(Number(result.get("did")));
setDepIds(arr);
}
}, [result.get("cid"), result.get("did")]);
const items: TabsProps["items"] = [
{
@ -70,9 +96,11 @@ const CoursePage = () => {
children: (
<div className="float-left">
<TreeCategory
selected={category_ids}
type=""
text={"分类"}
onUpdate={(keys: any, title: any) => {
setPage(1);
setCategoryIds(keys);
if (typeof title === "string") {
setLabel(title);
@ -90,11 +118,13 @@ const CoursePage = () => {
children: (
<div className="float-left">
<TreeDepartment
selected={dep_ids}
refresh={refresh}
showNum={false}
type="no-course"
text={"部门"}
onUpdate={(keys: any, title: any) => {
setPage(1);
setDepIds(keys);
setDepLabel(title);
}}
@ -288,7 +318,7 @@ const CoursePage = () => {
setLoading(true);
let categoryIds = "";
let depIds = "";
if (tabKey === 1) {
if (tabKey === "1") {
categoryIds = category_ids.join(",");
} else {
depIds = dep_ids.join(",");
@ -317,11 +347,6 @@ const CoursePage = () => {
setRefresh(!refresh);
};
// 加载列表
useEffect(() => {
getList();
}, [category_ids, dep_ids, refresh, page, size, tabKey]);
const paginationProps = {
current: page, //当前页码
pageSize: size,
@ -337,7 +362,7 @@ const CoursePage = () => {
};
const onChange = (key: string) => {
setTabKey(Number(key));
setTabKey(key);
};
return (
@ -345,7 +370,7 @@ const CoursePage = () => {
<div className="tree-main-body">
<div className="left-box">
<Tabs
defaultActiveKey="1"
defaultActiveKey={tabKey}
centered
tabBarGutter={55}
items={items}
@ -354,7 +379,7 @@ const CoursePage = () => {
</div>
<div className="right-box">
<div className="playedu-main-title float-left mb-24">
线 | {tabKey === 1 ? selLabel : selDepLabel}
线 | {tabKey === "1" ? selLabel : selDepLabel}
</div>
<div className="float-left j-b-flex mb-24">
<div className="d-flex">
@ -406,8 +431,8 @@ const CoursePage = () => {
rowKey={(record) => record.id}
/>
<CourseCreate
cateIds={tabKey === 1 ? category_ids : []}
depIds={tabKey === 2 ? dep_ids : []}
cateIds={tabKey === "1" ? category_ids : []}
depIds={tabKey === "2" ? dep_ids : []}
open={createVisible}
onCancel={() => {
setCreateVisible(false);

View File

@ -89,7 +89,12 @@ const DepartmentPage = () => {
<i
className="iconfont icon-icon-delete"
style={{ fontSize: 24 }}
onClick={() => removeItem(departments[id][i].id)}
onClick={() =>
removeItem(
departments[id][i].id,
departments[id][i].name
)
}
/>
</>
)}
@ -124,7 +129,12 @@ const DepartmentPage = () => {
<i
className="iconfont icon-icon-delete"
style={{ fontSize: 24 }}
onClick={() => removeItem(departments[id][i].id)}
onClick={() =>
removeItem(
departments[id][i].id,
departments[id][i].name
)
}
/>
</>
)}
@ -144,19 +154,22 @@ const DepartmentPage = () => {
setRefresh(!refresh);
};
const removeItem = (id: number) => {
const removeItem = (id: number, label: string) => {
if (id === 0) {
return;
}
department.checkDestroy(id).then((res: any) => {
if (
res.data.children &&
res.data.children.length === 0 &&
res.data.courses &&
res.data.courses.length === 0 &&
res.data.users &&
res.data.users.length === 0
) {
delUser(id);
} else {
if (res.data.children.length > 0) {
if (res.data.children && res.data.children.length > 0) {
modal.warning({
title: "操作确认",
centered: true,
@ -179,22 +192,26 @@ const DepartmentPage = () => {
content: (
<p>
{res.data.courses.length > 0 && (
{res.data.courses && res.data.courses.length > 0 && (
<Button
style={{ paddingLeft: 4, paddingRight: 4 }}
type="link"
danger
onClick={() => navigate("/course")}
onClick={() =>
navigate("/course?did=" + id + "&label=" + label)
}
>
{res.data.courses.length}线
</Button>
)}
{res.data.users.length > 0 && (
{res.data.users && res.data.users.length > 0 && (
<Button
type="link"
style={{ paddingLeft: 4, paddingRight: 4 }}
danger
onClick={() => navigate("/member/index")}
onClick={() =>
navigate("/member/index?did=" + id + "&label=" + label)
}
>
{res.data.users.length}
</Button>

View File

@ -20,7 +20,7 @@ import {
} from "@ant-design/icons";
import { user } from "../../api/index";
import { dateFormat } from "../../utils/index";
import { Link, Navigate } from "react-router-dom";
import { Link, Navigate, useLocation } from "react-router-dom";
import { TreeDepartment, PerButton } from "../../compenents";
import { MemberCreate } from "./compenents/create";
import { MemberUpdate } from "./compenents/update";
@ -37,6 +37,7 @@ interface DataType {
}
const MemberPage = () => {
const result = new URLSearchParams(useLocation().search);
const [loading, setLoading] = useState<boolean>(true);
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
@ -47,12 +48,24 @@ const MemberPage = () => {
const [nickname, setNickname] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [dep_ids, setDepIds] = useState<any>([]);
const [selLabel, setLabel] = useState<string>("全部部门");
const [selLabel, setLabel] = useState<string>(
result.get("label") ? String(result.get("label")) : "全部部门"
);
const [createVisible, setCreateVisible] = useState<boolean>(false);
const [updateVisible, setUpdateVisible] = useState<boolean>(false);
const [mid, setMid] = useState<number>(0);
const [user_dep_ids, setUserDepIds] = useState<any>({});
const [departments, setDepartments] = useState<any>({});
const [did, setDid] = useState(Number(result.get("did")));
useEffect(() => {
setDid(Number(result.get("did")));
if (Number(result.get("did"))) {
let arr = [];
arr.push(Number(result.get("did")));
setDepIds(arr);
}
}, [result.get("did")]);
const columns: ColumnsType<DataType> = [
{
@ -247,11 +260,13 @@ const MemberPage = () => {
<div className="tree-main-body">
<div className="left-box">
<TreeDepartment
selected={dep_ids}
refresh={refresh}
showNum={true}
type=""
text={"部门"}
onUpdate={(keys: any, title: any) => {
setPage(1);
setDepIds(keys);
var index = title.indexOf("(");
if (index !== -1) {

View File

@ -11,6 +11,7 @@ import {
Pagination,
} from "antd";
import { resource } from "../../../api";
import { useLocation } from "react-router-dom";
import styles from "./index.module.less";
import { UploadImageSub } from "../../../compenents/upload-image-button/upload-image-sub";
import { TreeCategory, PerButton } from "../../../compenents";
@ -32,6 +33,7 @@ interface ImageItem {
}
const ResourceImagesPage = () => {
const result = new URLSearchParams(useLocation().search);
const [imageList, setImageList] = useState<ImageItem[]>([]);
const [refresh, setRefresh] = useState(false);
const [page, setPage] = useState(1);
@ -41,8 +43,25 @@ const ResourceImagesPage = () => {
const [selectKey, setSelectKey] = useState<any>([]);
const [visibleArr, setVisibleArr] = useState<any>([]);
const [hoverArr, setHoverArr] = useState<any>([]);
const [selLabel, setLabel] = useState<string>("全部图片");
const [selLabel, setLabel] = useState<string>(
result.get("label") ? String(result.get("label")) : "全部图片"
);
const [loading, setLoading] = useState<boolean>(false);
const [cateId, setCateId] = useState(Number(result.get("cid")));
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(() => {
getImageList();
}, [category_ids, refresh, page, size]);
// 删除图片
const removeResource = () => {
@ -70,9 +89,6 @@ const ResourceImagesPage = () => {
// 获取图片列表
const getImageList = () => {
if (loading) {
return;
}
setLoading(true);
let categoryIds = category_ids.join(",");
resource
@ -102,11 +118,6 @@ const ResourceImagesPage = () => {
setRefresh(!refresh);
};
// 加载图片列表
useEffect(() => {
getImageList();
}, [category_ids, refresh, page, size]);
const onChange = (e: any, id: number) => {
e.preventDefault();
e.stopPropagation();
@ -151,9 +162,11 @@ const ResourceImagesPage = () => {
<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);
@ -187,15 +200,13 @@ const ResourceImagesPage = () => {
<Button className="mr-16" onClick={() => selectAll()}>
</Button>
<PerButton
<Button
disabled={selectKey.length === 0}
type="primary"
text="删除"
class=""
icon={null}
p="resource-destroy"
onClick={() => removeResource()}
/>
>
</Button>
</>
)}
</div>

View File

@ -84,13 +84,13 @@ const ResourceCategoryPage = () => {
}}
/>
)}
{through("resource-destroy") && (
<i
className="iconfont icon-icon-delete"
style={{ fontSize: 24 }}
onClick={() => removeItem(categories[id][i].id)}
/>
)}
<i
className="iconfont icon-icon-delete"
style={{ fontSize: 24 }}
onClick={() =>
removeItem(categories[id][i].id, categories[id][i].name)
}
/>
</div>
</>
),
@ -119,13 +119,13 @@ const ResourceCategoryPage = () => {
}}
/>
)}
{through("resource-destroy") && (
<i
className="iconfont icon-icon-delete"
style={{ fontSize: 24 }}
onClick={() => removeItem(categories[id][i].id)}
/>
)}
<i
className="iconfont icon-icon-delete"
style={{ fontSize: 24 }}
onClick={() =>
removeItem(categories[id][i].id, categories[id][i].name)
}
/>
</div>
</>
),
@ -142,20 +142,24 @@ const ResourceCategoryPage = () => {
setRefresh(!refresh);
};
const removeItem = (id: number) => {
const removeItem = (id: number, label: string) => {
if (id === 0) {
return;
}
resourceCategory.checkDestroy(id).then((res: any) => {
if (
res.data.children &&
res.data.children.length === 0 &&
res.data.courses &&
res.data.courses.length === 0 &&
res.data.images &&
res.data.images.length === 0 &&
res.data.videos &&
res.data.videos.length === 0
) {
delUser(id);
} else {
if (res.data.children.length > 0) {
if (res.data.children && res.data.children.length > 0) {
modal.warning({
title: "操作确认",
centered: true,
@ -178,32 +182,38 @@ const ResourceCategoryPage = () => {
content: (
<p>
{res.data.courses.length > 0 && (
{res.data.courses && res.data.courses.length > 0 && (
<Button
style={{ paddingLeft: 4, paddingRight: 4 }}
type="link"
danger
onClick={() => navigate("/course")}
onClick={() =>
navigate("/course?cid=" + id + "&label=" + label)
}
>
{res.data.courses.length}线
</Button>
)}
{res.data.videos.length > 0 && (
{res.data.videos && res.data.videos.length > 0 && (
<Button
type="link"
style={{ paddingLeft: 4, paddingRight: 4 }}
danger
onClick={() => navigate("/videos")}
onClick={() =>
navigate("/videos?cid=" + id + "&label=" + label)
}
>
{res.data.videos.length}
</Button>
)}
{res.data.images.length > 0 && (
{res.data.images && res.data.images.length > 0 && (
<Button
type="link"
style={{ paddingLeft: 4, paddingRight: 4 }}
danger
onClick={() => navigate("/images")}
onClick={() =>
navigate("/images?cid=" + id + "&label=" + label)
}
>
{res.data.images.length}
</Button>

View File

@ -0,0 +1,143 @@
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 VideosUpdateDialog: 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) => {
if (Array.isArray(values.category_id)) {
values.category_id = values.category_id[0];
}
resource.videoUpdate(id, values).then((res: any) => {
message.success("保存成功!");
onSuccess();
});
};
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,40 @@
.play-mask {
width: 100%;
height: 100%;
top: 0;
bottom: 0;
left: 0;
right: 0;
position: fixed;
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
.play-dialog {
width: 800px;
height: 450px;
border-radius: 8px;
overflow: hidden;
position: relative;
.close-button {
width: 30px;
height: 30px;
position: absolute;
top: 24px;
right: 24px;
cursor: pointer;
z-index: 1000;
&:hover {
opacity: 0.8;
}
img {
width: 30px;
height: 30px;
}
}
.play-box {
width: 800px;
height: 450px;
}
}
}

View File

@ -0,0 +1,64 @@
import React, { useState, useEffect } from "react";
import styles from "./index.module.less";
import closeIcon from "../../../../../assets/images/commen/close.png";
interface PropInterface {
id: number;
url: string;
open: boolean;
onCancel: () => void;
}
declare const window: any;
export const VideoPlayDialog: React.FC<PropInterface> = ({
id,
url,
open,
onCancel,
}) => {
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (open && url) {
initDPlayer(url);
}
}, [id, open, url]);
const initDPlayer = (playUrl: string) => {
window.player = new window.DPlayer({
container: document.getElementById("meedu-player-container"),
autoplay: false,
video: {
url: playUrl,
},
});
window.player.on("ended", () => {
window.player && window.player.destroy();
});
};
return (
<>
{open && (
<div className={styles["play-mask"]}>
<div className={styles["play-dialog"]}>
<div
className={styles["close-button"]}
onClick={() => {
window.player && window.player.destroy();
onCancel();
}}
>
<img src={closeIcon} />
</div>
<div
className={styles["play-box"]}
id="meedu-player-container"
></div>
</div>
</div>
)}
</>
);
};

View File

@ -1,12 +1,16 @@
import { useEffect, useState } from "react";
import { Modal, Table, message, Space } from "antd";
import { Modal, Table, message, Space, Dropdown, Button } from "antd";
import type { MenuProps } from "antd";
import { resource } from "../../../api";
// import styles from "./index.module.less";
import { ExclamationCircleFilled } from "@ant-design/icons";
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 { UploadVideoButton } from "../../../compenents/upload-video-button";
import { VideoPlayDialog } from "./compenents/video-play-dialog";
import { VideosUpdateDialog } from "./compenents/update-dialog";
const { confirm } = Modal;
@ -18,6 +22,7 @@ interface DataType {
}
const ResourceVideosPage = () => {
const result = new URLSearchParams(useLocation().search);
const [videoList, setVideoList] = useState<any>([]);
const [videosExtra, setVideoExtra] = useState<any>([]);
const [adminUsers, setAdminUsers] = useState<any>({});
@ -27,21 +32,27 @@ const ResourceVideosPage = () => {
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState<boolean>(true);
const [category_ids, setCategoryIds] = useState<any>([]);
const [selLabel, setLabel] = useState<string>("全部视频");
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
const [selLabel, setLabel] = useState<string>(
result.get("label") ? String(result.get("label")) : "全部视频"
);
const [cateId, setCateId] = useState(Number(result.get("cid")));
const [updateVisible, setUpdateVisible] = useState<boolean>(false);
const [playVisible, setPlayeVisible] = useState<boolean>(false);
const [multiConfig, setMultiConfig] = useState<boolean>(false);
const [updateId, setUpdateId] = useState(0);
const [playUrl, setPlayUrl] = useState<string>("");
useEffect(() => {
setCateId(Number(result.get("cid")));
if (Number(result.get("cid"))) {
let arr = [];
arr.push(Number(result.get("cid")));
setCategoryIds(arr);
}
}, [result.get("cid")]);
const columns: ColumnsType<DataType> = [
// {
// title: "封面",
// dataIndex: "id",
// render: (id: string) => (
// <Image
// preview={false}
// width={120}
// height={80}
// src={videosExtra[id].poster}
// ></Image>
// ),
// },
{
title: "视频名称",
dataIndex: "name",
@ -80,20 +91,68 @@ const ResourceVideosPage = () => {
title: "操作",
key: "action",
fixed: "right",
width: 100,
render: (_, record: any) => (
<Space size="small">
<PerButton
type="link"
text="删除"
class="b-link c-red"
icon={null}
p="resource-destroy"
onClick={() => removeResource(record.id)}
disabled={null}
/>
</Space>
),
width: 160,
render: (_, record: any) => {
const items: MenuProps["items"] = [
{
key: "1",
label: (
<Button
type="link"
className="b-link c-red"
onClick={() => {
setUpdateId(record.id);
setUpdateVisible(true);
}}
>
</Button>
),
},
{
key: "2",
label: (
<Button
type="link"
className="b-link c-red"
onClick={() => removeResource(record.id)}
>
</Button>
),
},
];
return (
<Space size="small">
<Button
type="link"
size="small"
className="b-n-link c-red"
onClick={() => {
setUpdateId(record.id);
setPlayUrl(record.url);
setPlayeVisible(true);
}}
>
</Button>
<div className="form-column"></div>
<Dropdown menu={{ items }}>
<Button
type="link"
className="b-link c-red"
onClick={(e) => e.preventDefault()}
>
<Space size="small" align="center">
<DownOutlined />
</Space>
</Button>
</Dropdown>
</Space>
);
},
},
];
@ -105,7 +164,7 @@ const ResourceVideosPage = () => {
confirm({
title: "操作确认",
icon: <ExclamationCircleFilled />,
content: "确认删除此视频",
content: "删除前请检查选中视频文件无关联课程,确认删除?",
centered: true,
okText: "确认",
cancelText: "取消",
@ -121,6 +180,29 @@ const ResourceVideosPage = () => {
});
};
const removeResourceMulti = () => {
if (selectedRowKeys.length === 0) {
return;
}
confirm({
title: "操作确认",
icon: <ExclamationCircleFilled />,
content: "删除前请检查选中视频文件无关联课程,确认删除?",
centered: true,
okText: "确认",
cancelText: "取消",
onOk() {
resource.destroyResourceMulti(selectedRowKeys).then(() => {
message.success("删除成功");
resetVideoList();
});
},
onCancel() {
console.log("Cancel");
},
});
};
// 获取视频列表
const getVideoList = () => {
setLoading(true);
@ -138,11 +220,13 @@ const ResourceVideosPage = () => {
console.log("错误,", err);
});
};
// 重置列表
const resetVideoList = () => {
setPage(1);
setSize(10);
setVideoList([]);
setSelectedRowKeys([]);
setRefresh(!refresh);
};
@ -165,14 +249,22 @@ const ResourceVideosPage = () => {
setSize(pageSize);
};
const rowSelection = {
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
setSelectedRowKeys(selectedRowKeys);
},
};
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);
@ -186,24 +278,74 @@ const ResourceVideosPage = () => {
<div className="d-flex playedu-main-title float-left mb-24">
| {selLabel}
</div>
<div className="float-left mb-24">
<UploadVideoButton
categoryIds={category_ids}
onUpdate={() => {
resetVideoList();
}}
></UploadVideoButton>
<div className="float-left j-b-flex mb-24">
<div>
<UploadVideoButton
categoryIds={category_ids}
onUpdate={() => {
resetVideoList();
}}
></UploadVideoButton>
</div>
<div className="d-flex">
<Button
type="default"
className="mr-16"
onClick={() => {
setSelectedRowKeys([]);
setMultiConfig(!multiConfig);
}}
>
{multiConfig ? "取消操作" : "批量操作"}
</Button>
<Button
type="default"
onClick={() => removeResourceMulti()}
disabled={selectedRowKeys.length === 0}
>
</Button>
</div>
</div>
<div className="float-left">
<Table
columns={columns}
dataSource={videoList}
loading={loading}
pagination={paginationProps}
rowKey={(record) => record.id}
/>
{multiConfig ? (
<Table
rowSelection={{
type: "checkbox",
...rowSelection,
}}
columns={columns}
dataSource={videoList}
loading={loading}
pagination={paginationProps}
rowKey={(record) => record.id}
/>
) : (
<Table
columns={columns}
dataSource={videoList}
loading={loading}
pagination={paginationProps}
rowKey={(record) => record.id}
/>
)}
</div>
</div>
<VideoPlayDialog
id={Number(updateId)}
open={playVisible}
url={playUrl}
onCancel={() => setPlayeVisible(false)}
></VideoPlayDialog>
<VideosUpdateDialog
id={Number(updateId)}
open={updateVisible}
onCancel={() => setUpdateVisible(false)}
onSuccess={() => {
setUpdateVisible(false);
setRefresh(!refresh);
}}
></VideosUpdateDialog>
</div>
</>
);

View File

@ -40,6 +40,7 @@ const SystemAdministratorPage = () => {
const [role_ids, setRoleIds] = useState<any>([]);
const [selLabel, setLabel] = useState<string>("全部管理员");
const [roleDelSuccess, setRoleDelSuccess] = useState(false);
const [isSuper, setIsSuper] = useState(false);
const [name, setName] = useState<string>("");
@ -216,9 +217,10 @@ const SystemAdministratorPage = () => {
refresh={refresh}
type=""
text={"管理员"}
onUpdate={(keys: any, title: any) => {
onUpdate={(keys: any, title: any, isSuper: boolean) => {
setRoleIds(keys);
setLabel(title);
setIsSuper(isSuper);
}}
/>
</div>
@ -248,7 +250,7 @@ const SystemAdministratorPage = () => {
disabled={null}
/>
)}
{role_ids.length > 0 && (
{!isSuper && role_ids.length > 0 && (
<>
<PerButton
text="角色权限"

View File

@ -72,11 +72,6 @@ export const SystemAdminrolesCreate: React.FC<PropInterface> = ({
value: "线上课-n",
children: [],
},
{
title: "资源",
value: "资源-n",
children: [],
},
{
title: "资源分类",
value: "资源分类-n",
@ -87,6 +82,11 @@ export const SystemAdminrolesCreate: React.FC<PropInterface> = ({
value: "部门-n",
children: [],
},
{
title: "系统配置",
value: "系统配置-n",
children: [],
},
{
title: "其它",
value: "其它-n",

View File

@ -75,11 +75,6 @@ export const SystemAdminrolesUpdate: React.FC<PropInterface> = ({
value: "线上课-n",
children: [],
},
{
title: "资源",
value: "资源-n",
children: [],
},
{
title: "资源分类",
value: "资源分类-n",
@ -90,6 +85,11 @@ export const SystemAdminrolesUpdate: React.FC<PropInterface> = ({
value: "部门-n",
children: [],
},
{
title: "系统配置",
value: "系统配置-n",
children: [],
},
{
title: "其它",
value: "其它-n",

View File

@ -52,6 +52,10 @@ const SystemConfigPage = () => {
form.setFieldsValue({
"system.api_url": configData[i].key_value,
});
} else if (configData[i].key_name === "system.api_url") {
form.setFieldsValue({
"system.api_url": configData[i].key_value,
});
} else if (configData[i].key_name === "system.pc_url") {
form.setFieldsValue({
"system.pc_url": configData[i].key_value,
@ -275,6 +279,13 @@ const SystemConfigPage = () => {
</div>
</Form.Item>
)}
<Form.Item
style={{ marginBottom: 30 }}
label="API访问地址"
name="system.api_url"
>
<Input style={{ width: 274 }} placeholder="请填写API访问地址" />
</Form.Item>
<Form.Item
style={{ marginBottom: 30 }}
label="网站标题"
@ -297,13 +308,7 @@ const SystemConfigPage = () => {
placeholder="请填写网站页脚"
/>
</Form.Item>
{/* <Form.Item
style={{ marginBottom: 30 }}
label="API访问地址"
name="system.api_url"
>
<Input style={{ width: 274 }} placeholder="请填写API访问地址" />
</Form.Item>
{/*
<Form.Item
style={{ marginBottom: 30 }}
label="PC端访问地址"
@ -373,6 +378,14 @@ const SystemConfigPage = () => {
style={{ width: 274 }}
allowClear
placeholder="自定义跑马灯内容"
onChange={(e) => {
const { value } = e.target;
if (!value && e.type !== "change") {
setNameChecked(false);
setEmailChecked(false);
setIdCardChecked(false);
}
}}
/>
</Form.Item>
<Checkbox

View File

@ -124,3 +124,12 @@ export function ValidataCredentials(value: any) {
}
}
}
export function checkUrl(value: any) {
let url = value;
let str = url.substr(url.length - 1, 1);
if (str !== "/") {
url = url + "/";
}
return url;
}