85 Commits

Author SHA1 Message Date
Teng
5d834ab2d6 Merge pull request #4 from PlayEdu/dev
v1.0-beta.4
2023-05-04 15:24:06 +08:00
禺狨
6c3b29e919 系统配置跑马灯颜色配置优化 2023-04-25 17:29:01 +08:00
禺狨
e44b129f82 系统配置优化 2023-04-25 11:42:48 +08:00
禺狨
096466633f 技术部学习进度部门title刷新优化 2023-04-25 11:30:29 +08:00
禺狨
a656f21cbc 技术部学习进度筛选优化 2023-04-25 11:28:01 +08:00
禺狨
d85ba55b9c 技术部学习进度表格样式优化 2023-04-25 11:26:46 +08:00
禺狨
5f26441b8c 登录错误toast 2023-04-25 11:17:41 +08:00
禺狨
1900e55d03 技术部学习进度表格样式优化 2023-04-25 10:58:43 +08:00
禺狨
e133c724f3 新建学员携带部门id 2023-04-25 10:45:16 +08:00
禺狨
6bbe2cbeec 线上课新建课程时部门,分类独立化 2023-04-25 10:37:00 +08:00
禺狨
e0e1192fb7 首页概览查看产品文档跳转链接替换 2023-04-25 10:32:42 +08:00
禺狨
0eae2f7673 footer组件样式优化 2023-04-25 10:28:22 +08:00
禺狨
86ab6b7751 学员学习进度明细中文案优化 2023-04-25 10:20:10 +08:00
禺狨
09cbffe3ae 线上课学习清楚学习记录按钮相关文案优化 2023-04-25 10:17:54 +08:00
禺狨
33e302503c 线上课学习清楚学习记录按钮文案优化 2023-04-25 10:16:59 +08:00
禺狨
86a16c9722 input 都增加清空按钮 2023-04-25 10:15:07 +08:00
禺狨
391af1a488 学员批量导入后刷新错误信息 2023-04-24 11:15:45 +08:00
禺狨
18f6056dc9 线上课tab切换 2023-04-24 11:11:18 +08:00
禺狨
b897dfffac 学员批量导入模板替换 2023-04-24 10:51:45 +08:00
禺狨
0dfbaaf5a6 系统配置切换tab刷新数据 2023-04-24 10:49:25 +08:00
禺狨
ea934eae07 部门学院学习明细 2023-04-24 10:45:37 +08:00
禺狨
516c718c78 选择图片组件选中后再确定 2023-04-24 10:45:09 +08:00
禺狨
1b7f11489b 选择图片组件选中后再确定 2023-04-24 09:54:26 +08:00
禺狨
b34cd955c4 线上课列表切换tab重置分类、部门组件 2023-04-24 09:19:58 +08:00
禺狨
27ec0e6c4a 部门学员学习进度列表导出表格 2023-04-23 15:43:21 +08:00
禺狨
c7fa24021b 学员线上课学习进度 2023-04-23 14:54:23 +08:00
禺狨
1aa209640d 学员各页面缓存情况下刷新数据 2023-04-23 09:17:45 +08:00
禺狨
409fa080fc 学员各页面缓存 2023-04-21 10:27:54 +08:00
Teng
7383f08ab8 Merge pull request #3 from PlayEdu/dev
Dev
2023-04-20 17:45:01 +08:00
禺狨
492270bfc3 部门学习进度 2023-04-20 17:04:00 +08:00
禺狨
685c9001b8 部门学习进度 2023-04-20 16:57:07 +08:00
禺狨
3982380c44 部门学习进度 2023-04-20 16:55:52 +08:00
禺狨
b5e2d351e5 学员列表优化 2023-04-20 12:03:11 +08:00
禺狨
e32ecd205e 线上课学员列表优化 2023-04-20 12:02:28 +08:00
禺狨
0c5a7f2f60 学员列表优化 2023-04-20 11:58:09 +08:00
禺狨
746a48d4d6 学员列表变化关联部门组件刷新 2023-04-20 11:55:48 +08:00
禺狨
b8bb5234ca 学员列表标题优化 2023-04-20 11:52:35 +08:00
禺狨
1782f6acef 线上课学员携带课程标题 2023-04-20 11:46:19 +08:00
禺狨
b197da34c8 学员学习折线图样式 2023-04-20 11:37:22 +08:00
禺狨
d4df2fd07f 学员学习折线图样式 2023-04-20 11:36:41 +08:00
禺狨
a18d731913 学员学习线上课进度列表文案优化 2023-04-20 11:18:22 +08:00
禺狨
922708647e 学员学习折线图样式 2023-04-20 11:09:59 +08:00
禺狨
41ef729b3f 学员学习折线图样式 2023-04-20 11:03:16 +08:00
禺狨
43abf5c1cb 部门学员进度优化 2023-04-20 10:48:51 +08:00
禺狨
795609f5be 部门学员进度优化 2023-04-20 10:44:05 +08:00
禺狨
2afada157d 时间渲染为空时显示优化 2023-04-20 10:32:08 +08:00
禺狨
e7a63c350f 学员列表多余的操作按钮收缩到更多里 2023-04-20 10:28:37 +08:00
禺狨
c9253af78f 网站设置样式优化 2023-04-20 10:16:31 +08:00
禺狨
914b530004 学员设置样式优化 2023-04-20 10:15:27 +08:00
禺狨
13af594ed0 线上课、视频、图片、学员列表标题文案优化 2023-04-20 10:08:11 +08:00
禺狨
c7788f4d2b 线上课tabs样式优化 2023-04-20 10:03:39 +08:00
禺狨
361da70ed5 线上课tabs样式优化 2023-04-20 10:00:06 +08:00
禺狨
53132ffd3c 线上课tabs样式优化 2023-04-20 09:57:13 +08:00
禺狨
947e48fe0d 线上课tabs样式优化 2023-04-20 09:44:39 +08:00
禺狨
a71be154d8 登录跳转更新组件缓存优化 2023-04-20 09:36:05 +08:00
禺狨
db39805031 部门学员进度列表筛选优化 2023-04-19 16:50:43 +08:00
禺狨
6c923e1756 部门列表学员全部人数渲染 2023-04-19 16:12:37 +08:00
禺狨
b616335d18 部门学员进度 2023-04-19 13:43:05 +08:00
禺狨
30785497d3 部门学员进度 2023-04-19 13:40:12 +08:00
禺狨
2a2a091144 部门学员进度 2023-04-19 12:08:11 +08:00
禺狨
f5ad3a4eb2 学员学习 2023-04-19 11:17:19 +08:00
禺狨
46e352fac3 Merge branch 'dev' of https://gitee.com/playeduxyz/backend into dev 2023-04-17 17:26:06 +08:00
禺狨
c01de5b51b 退出登录逻辑优化 2023-04-17 17:25:57 +08:00
none
c0e9e5bd5c docker 2023-04-17 16:05:23 +08:00
none
016f5b4a70 优化docker编译 2023-04-17 15:55:51 +08:00
禺狨
e57ef9ce46 自动滚到到顶部 2023-04-17 14:27:45 +08:00
禺狨
36a24ae87f 自动滚到到顶部 2023-04-17 11:19:15 +08:00
禺狨
23d16b52dd 后台学员列表部门显示数量 2023-04-17 11:12:45 +08:00
禺狨
9dadb9818d 后台增加学员默认头像配置 2023-04-17 11:03:11 +08:00
禺狨
a475e693dc 后台学员列表部门显示数量 2023-04-17 10:11:55 +08:00
Teng
0fe75d745e Merge pull request #2 from PlayEdu/dev
Dev
2023-04-13 18:01:36 +08:00
禺狨
facbaad8f5 线上课选择部门显示数量 2023-04-13 14:20:28 +08:00
禺狨
9b4e53176c 引入默认头像、封面 2023-04-13 10:12:13 +08:00
禺狨
060d686cee 上传图片按钮文案替换 2023-04-11 16:40:54 +08:00
禺狨
d2ca3f535f 首页概览显示优化 2023-04-11 16:32:50 +08:00
禺狨
d90afcc08b 学员页面重复请求api和分类默认展开 2023-04-11 16:28:49 +08:00
禺狨
dfc33aa754 跳转链接优化 2023-04-11 16:17:49 +08:00
禺狨
e2a0aaf695 部门默认展开 2023-04-11 16:14:28 +08:00
禺狨
f728ded148 Merge branch 'dev' of https://gitee.com/playeduxyz/backend into dev 2023-04-11 16:11:36 +08:00
禺狨
72a91d642a 部门默认展开 2023-04-11 16:11:22 +08:00
none
f05a696941 完成系统配置的加载 2023-04-11 11:08:56 +08:00
none
86964737d3 登录成功请求系统配置 2023-04-11 11:01:15 +08:00
禺狨
7bc01276b5 后台登录验证码字数验证 2023-04-10 09:11:41 +08:00
禺狨
980d559fb9 Merge branch 'dev' of https://gitee.com/playeduxyz/backend into dev 2023-04-10 09:09:13 +08:00
禺狨
757e9ace1a 后台标题文案优化 2023-04-10 09:09:05 +08:00
55 changed files with 1829 additions and 256 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
/node_modules
/build
/dist

View File

@@ -1,5 +1,13 @@
FROM node:lts-slim as builder
WORKDIR /app
COPY . /app
RUN yarn config set registry https://registry.npm.taobao.org && yarn && yarn build
FROM nginx:1.23.4-alpine-slim
COPY dist /usr/share/nginx/html
COPY --from=builder /app/dist /usr/share/nginx/html
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/docker/nginx.conf /etc/nginx/nginx.conf

View File

@@ -7,7 +7,7 @@
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
<title>PlayEdu</title>
<title>管理后台</title>
</head>
<body>
<div id="root"></div>

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@reduxjs/toolkit": "^1.9.3",
"ahooks": "^3.7.6",
"antd": "^5.3.2",
"axios": "^1.3.4",
"echarts": "^5.4.2",

13
src/AutoTop.ts Normal file
View File

@@ -0,0 +1,13 @@
import React from "react";
import { useLayoutEffect } from "react";
import { useLocation } from "react-router-dom";
const AutoScorllTop: React.FC<{ children: any }> = ({ children }) => {
const location = useLocation();
useLayoutEffect(() => {
document.documentElement.scrollTo(0, 0);
}, [location.pathname]);
return children;
};
export default AutoScorllTop;

View File

@@ -3,3 +3,7 @@ import client from "./internal/httpClient";
export function getImageCaptcha() {
return client.get("/backend/v1/system/image-captcha", {});
}
export function getSystemConfig() {
return client.get("/backend/v1/system/config", {});
}

View File

@@ -78,3 +78,72 @@ export function storeBatch(startLine: number, users: string[][]) {
users: users,
});
}
export function learnStats(id: number) {
return client.get(`/backend/v1/user/${id}/learn-stats`, {});
}
export function learnHours(
id: number,
page: number,
size: number,
params: object
) {
return client.get(`/backend/v1/user/${id}/learn-hours`, {
page,
size,
...params,
});
}
export function learnCourses(
id: number,
page: number,
size: number,
params: object
) {
return client.get(`/backend/v1/user/${id}/learn-courses`, {
page,
size,
...params,
});
}
export function learnAllCourses(id: number) {
return client.get(`/backend/v1/user/${id}/all-courses`, {});
}
export function departmentProgress(
id: number,
page: number,
size: number,
params: object
) {
return client.get(`/backend/v1/department/${id}/users`, {
page,
size,
...params,
});
}
export function learnCoursesProgress(
id: number,
courseId: number,
params: any
) {
return client.get(`/backend/v1/user/${id}/learn-course/${courseId} `, params);
}
export function destroyAllUserLearned(id: number, courseId: number) {
return client.destroy(`/backend/v1/user/${id}/learn-course/${courseId}`);
}
export function destroyUserLearned(
id: number,
courseId: number,
hourId: number
) {
return client.destroy(
`/backend/v1/user/${id}/learn-course/${courseId}/hour/${hourId}`
);
}

View File

@@ -54,6 +54,7 @@ export const CreateResourceCategory = (props: PropInterface) => {
onChange={(e) => {
setName(e.target.value);
}}
allowClear
/>
</Modal>
</>

View File

@@ -8,12 +8,12 @@ export const Footer: React.FC = () => {
style={{
width: "100%",
backgroundColor: "#F6F6F6",
height: 232,
height: 166,
paddingTop: 80,
textAlign: "center",
}}
>
<Link to="https://playedu.xyz/">
<Link to="https://playedu.xyz/" target="blank">
<i
style={{ fontSize: 30, color: "#cccccc" }}
className="iconfont icon-waterprint footer-icon"

View File

@@ -5,6 +5,7 @@ import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import avatar from "../../assets/images/commen/avatar.png";
import { logoutAction } from "../../store/user/loginUserSlice";
import { clearToken } from "../../utils/index";
export const Header: React.FC = () => {
const dispatch = useDispatch();
@@ -13,6 +14,7 @@ export const Header: React.FC = () => {
const onClick: MenuProps["onClick"] = ({ key }) => {
if (key === "login_out") {
clearToken();
dispatch(logoutAction());
navigate("/login");
} else if (key === "change_password") {

View File

@@ -0,0 +1,29 @@
import { useUpdate } from "ahooks";
import { useEffect, useRef } from "react";
import { useLocation, useOutlet } from "react-router-dom";
function KeepAlive() {
const componentList = useRef(new Map());
const outLet = useOutlet();
const { pathname } = useLocation();
const forceUpdate = useUpdate();
useEffect(() => {
if (!componentList.current.has(pathname)) {
componentList.current.set(pathname, outLet);
}
forceUpdate();
}, [pathname]);
return (
<div>
{Array.from(componentList.current).map(([key, component]) => (
<div key={key} style={{ display: pathname === key ? "block" : "none" }}>
{component}
</div>
))}
</div>
);
}
export default KeepAlive;

View File

@@ -50,7 +50,7 @@ const items = [
"user",
<i className="iconfont icon-icon-user" />,
[
getItem("学员", "/member", null, null, null),
getItem("学员", "/member/index", null, null, null),
getItem("部门", "/department", null, null, null),
],
null

View File

@@ -5,6 +5,7 @@ import { resourceCategory } from "../../api/index";
interface Option {
key: string | number;
title: any;
children?: Option[];
}
@@ -89,13 +90,16 @@ export const TreeCategory = (props: PropInterface) => {
<span>{props.text}</span>
</div>
</div>
<Tree
onSelect={onSelect}
selectedKeys={selectKey}
onExpand={onExpand}
treeData={treeData}
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
/>
{treeData.length > 0 && (
<Tree
onSelect={onSelect}
selectedKeys={selectKey}
onExpand={onExpand}
treeData={treeData}
defaultExpandAll={true}
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
/>
)}
</div>
);
};

View File

@@ -11,6 +11,8 @@ interface Option {
interface PropInterface {
type: string;
text: string;
refresh: boolean;
showNum: boolean;
onUpdate: (keys: any, title: any) => void;
}
@@ -18,14 +20,22 @@ export const TreeDepartment = (props: PropInterface) => {
const [treeData, setTreeData] = useState<any>([]);
const [loading, setLoading] = useState<boolean>(true);
const [selectKey, setSelectKey] = useState<any>([]);
const [userTotal, setUserTotal] = useState(0);
useEffect(() => {
setLoading(true);
department.departmentList().then((res: any) => {
const departments = res.data.departments;
const departCount = res.data.dep_user_count;
setUserTotal(res.data.user_total);
if (JSON.stringify(departments) !== "{}") {
const new_arr: Option[] = checkArr(departments, 0);
setTreeData(new_arr);
if (props.showNum) {
const new_arr: any = checkNewArr(departments, 0, departCount);
setTreeData(new_arr);
} else {
const new_arr: Option[] = checkArr(departments, 0);
setTreeData(new_arr);
}
} else {
const new_arr: Option[] = [
{
@@ -38,7 +48,39 @@ export const TreeDepartment = (props: PropInterface) => {
}
setLoading(false);
});
}, []);
}, [props.refresh]);
const checkNewArr = (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: getNewTitle(
departments[id][i].name,
departments[id][i].id,
counts
),
key: departments[id][i].id,
});
} else {
const new_arr: any = checkNewArr(
departments,
departments[id][i].id,
counts
);
arr.push({
title: getNewTitle(
departments[id][i].name,
departments[id][i].id,
counts
),
key: departments[id][i].id,
children: new_arr,
});
}
}
return arr;
};
const checkArr = (departments: any[], id: number) => {
const arr = [];
@@ -60,6 +102,15 @@ export const TreeDepartment = (props: PropInterface) => {
return arr;
};
const getNewTitle = (title: any, id: number, counts: any) => {
if (counts) {
let value = counts[id] || 0;
return title + "(" + value + ")";
} else {
return title;
}
};
const onSelect = (selectedKeys: any, info: any) => {
let label = "全部" + props.text;
if (info) {
@@ -89,14 +140,18 @@ export const TreeDepartment = (props: PropInterface) => {
onClick={() => onSelect([], "")}
>
{props.text}
{props.showNum && userTotal ? "(" + userTotal + ")" : ""}
</div>
<Tree
selectedKeys={selectKey}
onSelect={onSelect}
onExpand={onExpand}
treeData={treeData}
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
/>
{treeData.length > 0 && (
<Tree
selectedKeys={selectKey}
onSelect={onSelect}
onExpand={onExpand}
treeData={treeData}
defaultExpandAll={true}
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
/>
)}
</div>
);
};

View File

@@ -18,3 +18,20 @@
line-height: 30px;
display: flex;
}
.checked {
width: 16px;
height: 16px;
background: #ff4d4f;
border-radius: 3px;
border: 2px solid #ff4d4f;
position: absolute;
left: 5px;
top: 5px;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
cursor: pointer;
}

View File

@@ -12,7 +12,7 @@ import {
import { resource, resourceCategory } from "../../api";
import styles from "./index.module.less";
import { CreateResourceCategory } from "../create-rs-category";
import { CloseOutlined } from "@ant-design/icons";
import { CloseOutlined, CheckOutlined } from "@ant-design/icons";
import { UploadImageSub } from "./upload-image-sub";
import { TreeCategory } from "../../compenents";
@@ -36,6 +36,7 @@ interface ImageItem {
}
interface PropsInterface {
text: any;
onSelected: (url: string) => void;
}
@@ -48,6 +49,7 @@ export const UploadImageButton = (props: PropsInterface) => {
const [page, setPage] = useState(1);
const [size, setSize] = useState(15);
const [total, setTotal] = useState(0);
const [selected, setSelected] = useState<string>("");
// 获取图片列表
const getImageList = () => {
@@ -71,8 +73,10 @@ export const UploadImageButton = (props: PropsInterface) => {
// 加载图片列表
useEffect(() => {
getImageList();
}, [category_ids, refresh, page, size]);
if (showModal) {
getImageList();
}
}, [category_ids, refresh, page, size, showModal]);
return (
<>
@@ -81,7 +85,7 @@ export const UploadImageButton = (props: PropsInterface) => {
setShowModal(true);
}}
>
{props.text ? props.text : "上传图片"}
</Button>
{showModal && (
@@ -94,13 +98,24 @@ export const UploadImageButton = (props: PropsInterface) => {
open={true}
width={820}
maskClosable={false}
onOk={() => {
if (!selected) {
message.error("请选择图片后确定");
return;
}
props.onSelected(selected);
setShowModal(false);
}}
>
<Row style={{ width: 752, minHeight: 520, marginTop: 24 }}>
<Col span={7}>
<TreeCategory
type="no-cate"
text={"图片"}
onUpdate={(keys: any) => setCategoryIds(keys)}
onUpdate={(keys: any) => {
setSelected("");
setCategoryIds(keys);
}}
/>
</Col>
<Col span={17}>
@@ -126,10 +141,21 @@ export const UploadImageButton = (props: PropsInterface) => {
className="image-item"
style={{ backgroundImage: `url(${item.url})` }}
onClick={() => {
props.onSelected(item.url);
setShowModal(false);
setSelected(item.url);
}}
></div>
>
{selected.indexOf(item.url) !== -1 && (
<i
className={styles.checked}
onClick={(e) => {
e.stopPropagation();
setSelected("");
}}
>
<CheckOutlined />
</i>
)}
</div>
))}
</div>
{imageList.length > 0 && (

View File

@@ -257,6 +257,17 @@ code {
position: relative;
}
.playedu-main-sp-top {
width: 100%;
height: auto;
float: left;
background-color: white;
box-sizing: border-box;
padding: 24px 0px;
border-radius: 12px;
position: relative;
}
.playedu-main-body {
width: 100%;
height: auto;
@@ -603,6 +614,8 @@ textarea.ant-input {
background-size: contain;
background-position: center center;
background-color: #f6f6f6;
position: relative;
cursor: pointer;
}
}

View File

@@ -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 AutoScorllTop from "./AutoTop";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<Provider store={store}>
@@ -15,7 +16,9 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
theme={{ token: { colorPrimary: "#ff4d4f" } }}
>
<BrowserRouter>
<App />
<AutoScorllTop>
<App />
</AutoScorllTop>
</BrowserRouter>
</ConfigProvider>
</Provider>

View File

@@ -45,14 +45,14 @@ const ChangePasswordPage = () => {
name="old_password"
rules={[{ required: true, message: "请输入原密码!" }]}
>
<Input.Password placeholder="请输入原密码" />
<Input.Password placeholder="请输入原密码" allowClear />
</Form.Item>
<Form.Item
label="新密码"
name="new_password"
rules={[{ required: true, message: "请输入新密码!" }]}
>
<Input.Password placeholder="请输入新密码" />
<Input.Password placeholder="请输入新密码" allowClear />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">

View File

@@ -12,10 +12,10 @@ import {
TreeSelect,
} from "antd";
import styles from "./create.module.less";
import { useSelector } from "react-redux";
import { course, department } from "../../../api/index";
import { UploadImageButton, SelectResource } from "../../../compenents";
import { ExclamationCircleFilled } from "@ant-design/icons";
import { getHost } from "../../../utils/index";
import { TreeHours } from "./hours";
const { confirm } = Modal;
@@ -40,9 +40,12 @@ export const CourseCreate: React.FC<PropInterface> = ({
onCancel,
}) => {
const [form] = Form.useForm();
const defaultThumb1 = getHost() + "thumb/thumb1.png";
const defaultThumb2 = getHost() + "thumb/thumb2.png";
const defaultThumb3 = getHost() + "thumb/thumb3.png";
const courseDefaultThumbs = useSelector(
(state: any) => state.systemConfig.value.courseDefaultThumbs
);
const defaultThumb1 = courseDefaultThumbs[0];
const defaultThumb2 = courseDefaultThumbs[1];
const defaultThumb3 = courseDefaultThumbs[2];
const [loading, setLoading] = useState<boolean>(true);
const [departments, setDepartments] = useState<any>([]);
const [categories, setCategories] = useState<any>([]);
@@ -82,8 +85,9 @@ export const CourseCreate: React.FC<PropInterface> = ({
const getParams = () => {
department.departmentList().then((res: any) => {
const departments = res.data.departments;
const departCount = res.data.dep_user_count;
if (JSON.stringify(departments) !== "{}") {
const new_arr: Option[] = checkArr(departments, 0);
const new_arr: any = checkArr(departments, 0, departCount);
setDepartments(new_arr);
}
let type = "open";
@@ -131,7 +135,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
course.createCourse().then((res: any) => {
const categories = res.data.categories;
if (JSON.stringify(categories) !== "{}") {
const new_arr: Option[] = checkArr(categories, 0);
const new_arr: any = checkArr(categories, 0, null);
setCategories(new_arr);
}
@@ -160,18 +164,39 @@ export const CourseCreate: React.FC<PropInterface> = ({
});
};
const checkArr = (departments: any[], id: number) => {
const getNewTitle = (title: any, id: number, counts: any) => {
if (counts) {
let value = counts[id] || 0;
return title + "(" + value + ")";
} else {
return title;
}
};
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,
title: getNewTitle(
departments[id][i].name,
departments[id][i].id,
counts
),
value: departments[id][i].id,
});
} else {
const new_arr: Option[] = checkArr(departments, departments[id][i].id);
const new_arr: any = checkArr(
departments,
departments[id][i].id,
counts
);
arr.push({
title: departments[id][i].name,
title: getNewTitle(
departments[id][i].name,
departments[id][i].id,
counts
),
value: departments[id][i].id,
children: new_arr,
});
@@ -434,6 +459,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
style={{ width: 424 }}
treeData={categories}
placeholder="请选择课程分类"
treeDefaultExpandAll
/>
</Form.Item>
<Form.Item
@@ -444,6 +470,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
<Input
style={{ width: 424 }}
placeholder="请在此处输入课程名称"
allowClear
/>
</Form.Item>
<Form.Item
@@ -486,6 +513,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
treeData={departments}
multiple
allowClear
treeDefaultExpandAll
placeholder="请选择部门"
/>
</Form.Item>
@@ -572,6 +600,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
</div>
<div className="d-flex">
<UploadImageButton
text="更换封面"
onSelected={(url) => {
setThumb(url);
form.setFieldsValue({ thumb: url });
@@ -655,6 +684,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
onChange={(e) => {
setChapterName(index, e.target.value);
}}
allowClear
placeholder="请在此处输入章节名称"
/>
<Button

View File

@@ -451,6 +451,7 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
saveChapterName(index, e.target.value);
}}
placeholder="请在此处输入章节名称"
allowClear
/>
<Button
className="mr-16"

View File

@@ -12,9 +12,9 @@ import {
Image,
} from "antd";
import styles from "./update.module.less";
import { useSelector } from "react-redux";
import { course, department } from "../../../api/index";
import { UploadImageButton } from "../../../compenents";
import { getHost } from "../../../utils/index";
const { confirm } = Modal;
@@ -36,9 +36,12 @@ export const CourseUpdate: React.FC<PropInterface> = ({
onCancel,
}) => {
const [form] = Form.useForm();
const defaultThumb1 = getHost() + "thumb/thumb1.png";
const defaultThumb2 = getHost() + "thumb/thumb2.png";
const defaultThumb3 = getHost() + "thumb/thumb3.png";
const courseDefaultThumbs = useSelector(
(state: any) => state.systemConfig.value.courseDefaultThumbs
);
const defaultThumb1 = courseDefaultThumbs[0];
const defaultThumb2 = courseDefaultThumbs[1];
const defaultThumb3 = courseDefaultThumbs[2];
const [loading, setLoading] = useState<boolean>(true);
const [departments, setDepartments] = useState<any>([]);
const [categories, setCategories] = useState<any>([]);
@@ -63,7 +66,7 @@ export const CourseUpdate: React.FC<PropInterface> = ({
course.createCourse().then((res: any) => {
const categories = res.data.categories;
if (JSON.stringify(categories) !== "{}") {
const new_arr: Option[] = checkArr(categories, 0);
const new_arr: any = checkArr(categories, 0, null);
setCategories(new_arr);
}
});
@@ -71,8 +74,9 @@ export const CourseUpdate: React.FC<PropInterface> = ({
const getParams = () => {
department.departmentList().then((res: any) => {
const departments = res.data.departments;
const departCount = res.data.dep_user_count;
if (JSON.stringify(departments) !== "{}") {
const new_arr: Option[] = checkArr(departments, 0);
const new_arr: any = checkArr(departments, 0, departCount);
setDepartments(new_arr);
}
});
@@ -98,18 +102,39 @@ export const CourseUpdate: React.FC<PropInterface> = ({
});
};
const checkArr = (departments: any[], id: number) => {
const getNewTitle = (title: any, id: number, counts: any) => {
if (counts) {
let value = counts[id] || 0;
return title + "(" + value + ")";
} else {
return title;
}
};
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,
title: getNewTitle(
departments[id][i].name,
departments[id][i].id,
counts
),
value: departments[id][i].id,
});
} else {
const new_arr: Option[] = checkArr(departments, departments[id][i].id);
const new_arr: any = checkArr(
departments,
departments[id][i].id,
counts
);
arr.push({
title: departments[id][i].name,
title: getNewTitle(
departments[id][i].name,
departments[id][i].id,
counts
),
value: departments[id][i].id,
children: new_arr,
});
@@ -190,6 +215,7 @@ export const CourseUpdate: React.FC<PropInterface> = ({
style={{ width: 424 }}
treeData={categories}
placeholder="请选择课程分类"
treeDefaultExpandAll
/>
</Form.Item>
<Form.Item
@@ -198,6 +224,7 @@ export const CourseUpdate: React.FC<PropInterface> = ({
rules={[{ required: true, message: "请在此处输入课程名称!" }]}
>
<Input
allowClear
style={{ width: 424 }}
placeholder="请在此处输入课程名称"
/>
@@ -242,6 +269,7 @@ export const CourseUpdate: React.FC<PropInterface> = ({
treeData={departments}
multiple
allowClear
treeDefaultExpandAll
placeholder="请选择部门"
/>
</Form.Item>
@@ -328,6 +356,7 @@ export const CourseUpdate: React.FC<PropInterface> = ({
</div>
<div className="d-flex">
<UploadImageButton
text="更换封面"
onSelected={(url) => {
setThumb(url);
form.setFieldsValue({ thumb: url });

View File

@@ -51,6 +51,7 @@ const CoursePage = () => {
const [title, setTitle] = useState<string>("");
const [dep_ids, setDepIds] = useState<any>([]);
const [selLabel, setLabel] = useState<string>("全部分类");
const [selDepLabel, setDepLabel] = useState<string>("全部部门");
const [course_category_ids, setCourseCategoryIds] = useState<any>({});
const [course_dep_ids, setCourseDepIds] = useState<any>({});
const [categories, setCategories] = useState<any>({});
@@ -89,11 +90,13 @@ const CoursePage = () => {
children: (
<div className="float-left">
<TreeDepartment
refresh={refresh}
showNum={false}
type="no-course"
text={"部门"}
onUpdate={(keys: any, title: any) => {
setDepIds(keys);
setLabel(title);
setDepLabel(title);
}}
/>
</div>
@@ -231,7 +234,9 @@ const CoursePage = () => {
p="course"
onClick={() => {
setCid(Number(record.id));
navigate("/course/user/" + Number(record.id));
navigate(
"/course/user/" + Number(record.id) + "?title=" + record.title
);
}}
disabled={null}
/>
@@ -278,11 +283,16 @@ const CoursePage = () => {
});
};
// 获取视频列表
// 获取列表
const getList = () => {
setLoading(true);
let categoryIds = category_ids.join(",");
let depIds = dep_ids.join(",");
let categoryIds = "";
let depIds = "";
if (tabKey === 1) {
categoryIds = category_ids.join(",");
} else {
depIds = dep_ids.join(",");
}
course
.courseList(page, size, "", "", title, depIds, categoryIds)
.then((res: any) => {
@@ -310,7 +320,7 @@ const CoursePage = () => {
// 加载列表
useEffect(() => {
getList();
}, [category_ids, dep_ids, refresh, page, size]);
}, [category_ids, dep_ids, refresh, page, size, tabKey]);
const paginationProps = {
current: page, //当前页码
@@ -337,13 +347,14 @@ const CoursePage = () => {
<Tabs
defaultActiveKey="1"
centered
tabBarGutter={55}
items={items}
onChange={onChange}
/>
</div>
<div className="right-box">
<div className="playedu-main-title float-left mb-24">
/{selLabel}
线 | {tabKey === 1 ? selLabel : selDepLabel}
</div>
<div className="float-left j-b-flex mb-24">
<div className="d-flex">
@@ -365,6 +376,7 @@ const CoursePage = () => {
onChange={(e) => {
setTitle(e.target.value);
}}
allowClear
style={{ width: 160 }}
placeholder="请输入名称关键字"
/>
@@ -394,8 +406,8 @@ const CoursePage = () => {
rowKey={(record) => record.id}
/>
<CourseCreate
cateIds={category_ids}
depIds={dep_ids}
cateIds={tabKey === 1 ? category_ids : []}
depIds={tabKey === 2 ? dep_ids : []}
open={createVisible}
onCancel={() => {
setCreateVisible(false);

View File

@@ -11,7 +11,7 @@ import {
Image,
} from "antd";
import { course } from "../../api";
import { useParams } from "react-router-dom";
import { useParams, useLocation } from "react-router-dom";
import type { ColumnsType } from "antd/es/table";
import { BackBartment } from "../../compenents";
import { ExclamationCircleFilled } from "@ant-design/icons";
@@ -31,6 +31,7 @@ interface DataType {
const CourseUserPage = () => {
const params = useParams();
const result = new URLSearchParams(useLocation().search);
const [list, setList] = useState<any>([]);
const [users, setUsers] = useState<any>([]);
const [refresh, setRefresh] = useState(false);
@@ -42,10 +43,11 @@ const CourseUserPage = () => {
const [email, setEmail] = useState<string>("");
const [idCard, setIdCard] = useState<string>("");
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
const [title, setTitle] = useState<string>(String(result.get("title")));
const columns: ColumnsType<DataType> = [
{
title: "学员名称",
title: "学员",
render: (_, record: any) => (
<div className="d-flex">
<Image
@@ -70,11 +72,6 @@ const CourseUserPage = () => {
</span>
),
},
{
title: "学习进度",
dataIndex: "progress",
render: (progress: number) => <span>{progress / 100}%</span>,
},
{
title: "第一次学习时间",
dataIndex: "created_at",
@@ -85,6 +82,15 @@ const CourseUserPage = () => {
dataIndex: "finished_at",
render: (text: string) => <span>{dateFormat(text)}</span>,
},
{
title: "学习进度",
dataIndex: "progress",
render: (progress: number) => (
<span className={progress >= 10000 ? "c-green" : "c-red"}>
{progress / 100}%
</span>
),
},
];
useEffect(() => {
@@ -143,13 +149,13 @@ const CourseUserPage = () => {
// 删除学员
const delItem = () => {
if (selectedRowKeys.length === 0) {
message.error("请选择学员后再清除");
message.error("请选择学员后再重置");
return;
}
confirm({
title: "操作确认",
icon: <ExclamationCircleFilled />,
content: "确认清除选中学员学习记录?",
content: "确认重置选中学员学习记录?",
centered: true,
okText: "确认",
cancelText: "取消",
@@ -178,13 +184,13 @@ const CourseUserPage = () => {
<Row className="playedu-main-body">
<Col span={24}>
<div className="float-left mb-24">
<BackBartment title="线上课学员" />
<BackBartment title={title || "线上课学员"} />
</div>
<div className="float-left j-b-flex mb-24">
<div className="d-flex">
<PerButton
type="primary"
text="清除学习记录"
text="重置学习记录"
class="mr-16"
icon={null}
p="course"
@@ -200,6 +206,7 @@ const CourseUserPage = () => {
onChange={(e) => {
setName(e.target.value);
}}
allowClear
style={{ width: 160 }}
placeholder="请输入姓名关键字"
/>
@@ -211,11 +218,12 @@ const CourseUserPage = () => {
onChange={(e) => {
setEmail(e.target.value);
}}
allowClear
style={{ width: 160 }}
placeholder="请输入学员邮箱"
/>
</div>
<div className="d-flex mr-24">
{/* <div className="d-flex mr-24">
<Typography.Text>身份证号:</Typography.Text>
<Input
value={idCard}
@@ -225,7 +233,7 @@ const CourseUserPage = () => {
style={{ width: 160 }}
placeholder="请输入身份证号"
/>
</div>
</div> */}
<div className="d-flex">
<Button className="mr-16" onClick={resetList}>

View File

@@ -181,7 +181,7 @@ const DashboardPage = () => {
<div className={styles["num"]}>{basicData.user_total}</div>
<div className={styles["compare"]}>
<span className="mr-5"></span>
{compareNum(basicData.user_today, basicData.user_yesterday)}
{compareNum(basicData.user_today, 0)}
</div>
</div>
</div>
@@ -199,7 +199,7 @@ const DashboardPage = () => {
<div
className={styles["link-mode"]}
onClick={() => {
navigate("/member");
navigate("/member/index");
}}
>
<i
@@ -515,7 +515,11 @@ const DashboardPage = () => {
<div className={styles["large-title"]}></div>
<div className={styles["usage-guide"]}>
<img className={styles["banner"]} src={banner} alt="" />
<Link to="https://www.playedu.xyz/" className={styles["link"]}>
<Link
to="https://www.playedu.xyz/docs/docs/guide/"
target="blank"
className={styles["link"]}
>
Playedu
<img className={styles["icon"]} src={icon} alt="" />
</Link>

View File

@@ -147,7 +147,11 @@ export const DepartmentCreate: React.FC<PropInterface> = ({
name="name"
rules={[{ required: true, message: "请输入部门名称!" }]}
>
<Input style={{ width: 200 }} placeholder="请输入部门名称" />
<Input
style={{ width: 200 }}
allowClear
placeholder="请输入部门名称"
/>
</Form.Item>
</Form>
</div>

View File

@@ -194,7 +194,7 @@ const DepartmentPage = () => {
type="link"
style={{ paddingLeft: 4, paddingRight: 4 }}
danger
onClick={() => navigate("/member")}
onClick={() => navigate("/member/index")}
>
{res.data.users.length}
</Button>
@@ -364,15 +364,18 @@ const DepartmentPage = () => {
</div>
<div className="playedu-main-body">
<div style={{ width: 366 }}>
<Tree
onSelect={onSelect}
treeData={treeData}
draggable
blockNode
onDragEnter={onDragEnter}
onDrop={onDrop}
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
/>
{treeData.length > 0 && (
<Tree
onSelect={onSelect}
treeData={treeData}
draggable
blockNode
onDragEnter={onDragEnter}
onDrop={onDrop}
defaultExpandAll={true}
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
/>
)}
</div>
<DepartmentCreate
open={createVisible}

View File

@@ -15,7 +15,7 @@ const ErrorPage = () => {
<Button
type="primary"
onClick={() => {
navigate("/");
navigate("/", { replace: true });
}}
>

View File

@@ -1,9 +1,14 @@
import { useDispatch } from "react-redux";
import { Outlet } from "react-router-dom";
import { loginAction } from "../../store/user/loginUserSlice";
import {
SystemConfigStoreInterface,
saveConfigAction,
} from "../../store/system/systemConfigSlice";
interface Props {
loginData: any | null;
loginData?: any;
configData?: any;
}
const InitPage = (props: Props) => {
@@ -12,6 +17,19 @@ const InitPage = (props: Props) => {
dispatch(loginAction(props.loginData));
}
if (props.configData) {
let config: SystemConfigStoreInterface = {
systemName: props.configData["system.name"],
systemLogo: props.configData["system.logo"],
systemApiUrl: props.configData["system.api_url"],
systemPcUrl: props.configData["system.pc_url"],
systemH5Url: props.configData["system.h5_url"],
memberDefaultAvatar: props.configData["member.default_avatar"],
courseDefaultThumbs: props.configData["default.course_thumbs"],
};
dispatch(saveConfigAction(config));
}
return (
<>
<Outlet />

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import styles from "./index.module.less";
import { Spin, Input, Button, message } from "antd";
import { login, system } from "../../api/index";
import { login as loginApi, system } from "../../api/index";
import { setToken } from "../../utils/index";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
@@ -9,6 +9,10 @@ import banner from "../../assets/images/login/banner.png";
import icon from "../../assets/images/login/icon.png";
import "./login.less";
import { loginAction } from "../../store/user/loginUserSlice";
import {
SystemConfigStoreInterface,
saveConfigAction,
} from "../../store/system/systemConfigSlice";
const LoginPage = () => {
const dispatch = useDispatch();
@@ -22,6 +26,7 @@ const LoginPage = () => {
const [captchaLoading, setCaptchaLoading] = useState(true);
const fetchImageCaptcha = () => {
setCaptchaVal("");
setCaptchaLoading(true);
system.getImageCaptcha().then((res: any) => {
setImage(res.data.image);
@@ -30,7 +35,7 @@ const LoginPage = () => {
});
};
const loginSubmit = (e: any) => {
const loginSubmit = async () => {
if (!email) {
message.error("请输入管理员邮箱账号");
return;
@@ -43,43 +48,59 @@ const LoginPage = () => {
message.error("请输入图形验证码");
return;
}
if (loading) {
if (captchaVal.length !== 4) {
message.error("图形验证码错误");
return;
}
handleSubmit();
await handleSubmit();
};
const handleSubmit = () => {
const handleSubmit = async () => {
if (loading) {
return;
}
setLoading(true);
login
.login(email, password, captchaKey, captchaVal)
.then((res: any) => {
const token = res.data.token;
setToken(token);
getUser();
})
.catch((e) => {
setLoading(false);
setCaptchaVal("");
fetchImageCaptcha();
});
try {
let res: any = await loginApi.login(
email,
password,
captchaKey,
captchaVal
);
setToken(res.data.token); //将token写入本地
await getSystemConfig(); //获取系统配置并写入store
await getUser(); //获取登录用户的信息并写入store
navigate("/", { replace: true });
} catch (e) {
console.error("错误信息", e);
setLoading(false);
fetchImageCaptcha(); //刷新图形验证码
}
};
const getUser = () => {
login.getUser().then((res: any) => {
const data = res.data;
dispatch(loginAction(data));
setLoading(false);
navigate("/");
});
const getUser = async () => {
let res: any = await loginApi.getUser();
dispatch(loginAction(res.data));
};
const getSystemConfig = async () => {
let res: any = await system.getSystemConfig();
let data: SystemConfigStoreInterface = {
systemName: res.data["system.name"],
systemLogo: res.data["system.logo"],
systemApiUrl: res.data["system.api_url"],
systemPcUrl: res.data["system.pc_url"],
systemH5Url: res.data["system.h5_url"],
memberDefaultAvatar: res.data["member.default_avatar"],
courseDefaultThumbs: res.data["default.course_thumbs"],
};
dispatch(saveConfigAction(data));
};
const keyUp = (e: any) => {
if (e.keyCode === 13) {
loginSubmit(e);
loginSubmit();
}
};
@@ -107,6 +128,7 @@ const LoginPage = () => {
style={{ width: 400, height: 54 }}
placeholder="请输入管理员邮箱账号"
onKeyUp={(e) => keyUp(e)}
allowClear
/>
</div>
<div className="login-box d-flex mt-50">
@@ -115,6 +137,7 @@ const LoginPage = () => {
onChange={(e) => {
setPassword(e.target.value);
}}
allowClear
style={{ width: 400, height: 54 }}
placeholder="请输入密码"
/>
@@ -127,6 +150,7 @@ const LoginPage = () => {
onChange={(e) => {
setCaptchaVal(e.target.value);
}}
allowClear
onKeyUp={(e) => keyUp(e)}
/>
<div className={styles["captcha-box"]}>

View File

@@ -1,12 +1,14 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, TreeSelect, Input, message } from "antd";
import styles from "./create.module.less";
import { useSelector } from "react-redux";
import { user, department } from "../../../api/index";
import { UploadImageButton } from "../../../compenents";
import { ValidataCredentials, getHost } from "../../../utils/index";
import { ValidataCredentials } from "../../../utils/index";
interface PropInterface {
open: boolean;
depIds: any;
onCancel: () => void;
}
@@ -16,11 +18,18 @@ interface Option {
children?: Option[];
}
export const MemberCreate: React.FC<PropInterface> = ({ open, onCancel }) => {
export const MemberCreate: React.FC<PropInterface> = ({
open,
depIds,
onCancel,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(true);
const [departments, setDepartments] = useState<any>([]);
const [avatar, setAvatar] = useState<string>(getHost() + "avatar/avatar.png");
const memberDefaultAvatar = useSelector(
(state: any) => state.systemConfig.value.memberDefaultAvatar
);
const [avatar, setAvatar] = useState<string>(memberDefaultAvatar);
useEffect(() => {
if (open) {
@@ -33,12 +42,12 @@ export const MemberCreate: React.FC<PropInterface> = ({ open, onCancel }) => {
email: "",
name: "",
password: "",
avatar: getHost() + "avatar/avatar.png",
avatar: memberDefaultAvatar,
idCard: "",
dep_ids: [],
dep_ids: depIds,
});
setAvatar(getHost() + "avatar/avatar.png");
}, [form, open]);
setAvatar(memberDefaultAvatar);
}, [form, open, depIds]);
const getParams = () => {
department.departmentList().then((res: any) => {
@@ -129,6 +138,7 @@ export const MemberCreate: React.FC<PropInterface> = ({ open, onCancel }) => {
)}
<div className="d-flex">
<UploadImageButton
text="更换头像"
onSelected={(url) => {
setAvatar(url);
form.setFieldsValue({ avatar: url });
@@ -149,7 +159,11 @@ export const MemberCreate: React.FC<PropInterface> = ({ open, onCancel }) => {
name="email"
rules={[{ required: true, message: "请输入登录邮箱!" }]}
>
<Input style={{ width: 274 }} placeholder="请输入学员登录邮箱" />
<Input
allowClear
style={{ width: 274 }}
placeholder="请输入学员登录邮箱"
/>
</Form.Item>
<Form.Item
label="登录密码"
@@ -157,6 +171,7 @@ export const MemberCreate: React.FC<PropInterface> = ({ open, onCancel }) => {
rules={[{ required: true, message: "请输入登录密码!" }]}
>
<Input.Password
allowClear
style={{ width: 274 }}
placeholder="请输入登录密码"
/>
@@ -172,11 +187,16 @@ export const MemberCreate: React.FC<PropInterface> = ({ open, onCancel }) => {
treeData={departments}
multiple
allowClear
treeDefaultExpandAll
placeholder="请选择学员所属部门"
/>
</Form.Item>
<Form.Item label="身份证号" name="idCard">
<Input style={{ width: 274 }} placeholder="请填写学员身份证号" />
<Input
style={{ width: 274 }}
allowClear
placeholder="请填写学员身份证号"
/>
</Form.Item>
</Form>
</div>

View File

@@ -0,0 +1,252 @@
import { useState, useEffect } from "react";
import styles from "./progrss.module.less";
import { Table, Modal, message } from "antd";
import { PerButton, DurationText } from "../../../compenents";
import { user as member } from "../../../api/index";
import type { ColumnsType } from "antd/es/table";
import { dateFormat } from "../../../utils/index";
import { ExclamationCircleFilled } from "@ant-design/icons";
const { confirm } = Modal;
interface DataType {
id: React.Key;
title: string;
type: string;
created_at: string;
duration: number;
finished_duration: number;
is_finished: boolean;
finished_at: boolean;
}
interface PropInterface {
open: boolean;
uid: number;
id: number;
onCancel: () => void;
}
export const MemberLearnProgressDialog: React.FC<PropInterface> = ({
open,
uid,
id,
onCancel,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const [list, setList] = useState<any>([]);
const [records, setRecords] = useState<any>({});
const [refresh, setRefresh] = useState(false);
useEffect(() => {
if (open) {
getData();
}
}, [uid, id, open, refresh]);
const getData = () => {
if (loading) {
return;
}
setLoading(true);
member.learnCoursesProgress(uid, id, {}).then((res: any) => {
setList(res.data.hours);
setRecords(res.data.learn_records);
setLoading(false);
});
};
const column: ColumnsType<DataType> = [
{
title: "课时标题",
dataIndex: "title",
render: (title: string) => (
<>
<span>{title}</span>
</>
),
},
{
title: "总时长",
width: 120,
dataIndex: "duration",
render: (duration: number) => (
<>
<DurationText duration={duration}></DurationText>
</>
),
},
{
title: "已学习时长",
width: 120,
dataIndex: "finished_duration",
render: (_, record: any) => (
<>
{records && records[record.id] ? (
<span>
<DurationText
duration={records[record.id].finished_duration || 0}
></DurationText>
</span>
) : (
<span>-</span>
)}
</>
),
},
{
title: "是否学完",
width: 100,
dataIndex: "is_finished",
render: (_, record: any) => (
<>
{records &&
records[record.id] &&
records[record.id].is_finished === 1 ? (
<span className="c-green"></span>
) : (
<span className="c-red"></span>
)}
</>
),
},
{
title: "开始时间",
width: 150,
dataIndex: "created_at",
render: (_, record: any) => (
<>
{records && records[record.id] ? (
<span>{dateFormat(records[record.id].created_at)}</span>
) : (
<span>-</span>
)}
</>
),
},
{
title: "学完时间",
width: 150,
dataIndex: "finished_at",
render: (_, record: any) => (
<>
{records && records[record.id] ? (
<span>{dateFormat(records[record.id].finished_at)}</span>
) : (
<span>-</span>
)}
</>
),
},
{
title: "操作",
key: "action",
fixed: "right",
width: 70,
render: (_, record: any) => (
<>
{records && records[record.id] ? (
<PerButton
type="link"
text="重置"
class="b-link c-red"
icon={null}
p="user-learn-destroy"
onClick={() => {
clearSingleProgress(records[record.id].hour_id);
}}
disabled={null}
/>
) : (
<span>-</span>
)}
</>
),
},
];
const clearProgress = () => {
confirm({
title: "操作确认",
icon: <ExclamationCircleFilled />,
content: "确认重置此课程下所有课时的学习记录?",
centered: true,
okText: "确认",
cancelText: "取消",
onOk() {
member.destroyAllUserLearned(uid, id).then((res: any) => {
message.success("操作成功");
setRefresh(!refresh);
});
},
onCancel() {
console.log("Cancel");
},
});
};
const clearSingleProgress = (hour_id: number) => {
if (hour_id === 0) {
return;
}
confirm({
title: "操作确认",
icon: <ExclamationCircleFilled />,
content: "确认重置此课时的学习记录?",
centered: true,
okText: "确认",
cancelText: "取消",
onOk() {
member.destroyUserLearned(uid, id, hour_id).then((res: any) => {
message.success("操作成功");
setRefresh(!refresh);
});
},
onCancel() {
console.log("Cancel");
},
});
};
return (
<>
<Modal
title="课时学习进度"
centered
forceRender
open={open}
width={1000}
onOk={() => onCancel()}
onCancel={() => onCancel()}
maskClosable={false}
footer={null}
>
<div className="d-flex mt-24">
<PerButton
type="primary"
text="重置学习记录"
class="c-white"
icon={null}
p="user-learn-destroy"
onClick={() => {
clearProgress();
}}
disabled={null}
/>
</div>
<div
className="d-flex mt-24"
style={{ maxHeight: 800, overflowY: "auto" }}
>
<Table
columns={column}
dataSource={list}
loading={loading}
rowKey={(record) => record.id}
pagination={false}
/>
</div>
</Modal>
</>
);
};

View File

@@ -1,9 +1,10 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, TreeSelect, Input, message } from "antd";
import styles from "./create.module.less";
import styles from "./update.module.less";
import { useSelector } from "react-redux";
import { user, department } from "../../../api/index";
import { UploadImageButton } from "../../../compenents";
import { ValidataCredentials, getHost } from "../../../utils/index";
import { ValidataCredentials } from "../../../utils/index";
interface PropInterface {
id: number;
@@ -25,7 +26,10 @@ export const MemberUpdate: React.FC<PropInterface> = ({
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(true);
const [departments, setDepartments] = useState<any>([]);
const [avatar, setAvatar] = useState<string>(getHost() + "avatar/avatar.png");
const memberDefaultAvatar = useSelector(
(state: any) => state.systemConfig.value.memberDefaultAvatar
);
const [avatar, setAvatar] = useState<string>(memberDefaultAvatar);
useEffect(() => {
if (id == 0) {
@@ -163,6 +167,7 @@ export const MemberUpdate: React.FC<PropInterface> = ({
)}
<div className="d-flex">
<UploadImageButton
text="更换头像"
onSelected={(url) => {
setAvatar(url);
form.setFieldsValue({ avatar: url });
@@ -176,18 +181,27 @@ export const MemberUpdate: React.FC<PropInterface> = ({
name="name"
rules={[{ required: true, message: "请输入学员姓名!" }]}
>
<Input style={{ width: 274 }} placeholder="请填写学员姓名" />
<Input
allowClear
style={{ width: 274 }}
placeholder="请填写学员姓名"
/>
</Form.Item>
<Form.Item
label="登录邮箱"
name="email"
rules={[{ required: true, message: "请输入登录邮箱!" }]}
>
<Input style={{ width: 274 }} placeholder="请输入学员登录邮箱" />
<Input
style={{ width: 274 }}
allowClear
placeholder="请输入学员登录邮箱"
/>
</Form.Item>
<Form.Item label="登录密码" name="password">
<Input.Password
style={{ width: 274 }}
allowClear
placeholder="请输入登录密码"
/>
</Form.Item>
@@ -202,11 +216,16 @@ export const MemberUpdate: React.FC<PropInterface> = ({
treeData={departments}
multiple
allowClear
treeDefaultExpandAll
placeholder="请选择学员所属部门"
/>
</Form.Item>
<Form.Item label="身份证号" name="idCard">
<Input style={{ width: 274 }} placeholder="请填写学员身份证号" />
<Input
allowClear
style={{ width: 274 }}
placeholder="请填写学员身份证号"
/>
</Form.Item>
</Form>
</div>

View File

@@ -0,0 +1,346 @@
import { useState, useEffect } from "react";
import styles from "./departmentUser.module.less";
import {
Typography,
Input,
Modal,
Image,
Button,
Space,
message,
Table,
Select,
} from "antd";
import { useNavigate, useLocation } from "react-router-dom";
import { BackBartment, DurationText } from "../../compenents";
import { dateFormat } from "../../utils/index";
import { user as member } from "../../api/index";
const { Column, ColumnGroup } = Table;
import * as XLSX from "xlsx";
interface DataType {
id: React.Key;
title: string;
type: string;
created_at: string;
total_duration: number;
finished_duration: number;
is_finished: boolean;
}
const MemberDepartmentProgressPage = () => {
const result = new URLSearchParams(useLocation().search);
const [loading, setLoading] = useState<boolean>(false);
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
const [list, setList] = useState<any>([]);
const [total, setTotal] = useState(0);
const [refresh, setRefresh] = useState(false);
const [courses, setCourses] = useState<any>([]);
const [records, setRecords] = useState<any>({});
const [totalHour, setTotalHour] = useState(0);
const [name, setName] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [id_card, setIdCard] = useState<string>("");
const [showMode, setShowMode] = useState<string>("all");
const [did, setDid] = useState(Number(result.get("id")));
const [title, setTitle] = useState(String(result.get("title")));
const [exportLoading, setExportLoading] = useState(false);
const modes = [
{ label: "全部", value: "all" },
{ label: "不显示公开课", value: "only_dep" },
];
useEffect(() => {
setDid(Number(result.get("id")));
setTitle(String(result.get("title")));
resetData();
}, [result.get("id"), result.get("title")]);
useEffect(() => {
getData();
}, [refresh, page, size]);
const getData = () => {
if (loading) {
return;
}
setLoading(true);
member
.departmentProgress(did, page, size, {
sort_field: "",
sort_algo: "",
name: name,
email: email,
id_card: id_card,
show_mode: showMode,
})
.then((res: any) => {
setList(res.data.data);
setTotal(res.data.total);
let data = res.data.courses;
let arr = [];
let value = 0;
for (let key in data) {
arr.push(data[key]);
value += data[key].class_hour;
}
setCourses(arr);
setTotalHour(value);
setRecords(res.data.user_course_records);
setLoading(false);
});
};
const resetData = () => {
setName("");
setEmail("");
setIdCard("");
setShowMode("all");
setPage(1);
setSize(10);
setList([]);
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 getTotalHours = (params: any) => {
if (params) {
let value = 0;
for (let key in params) {
value += params[key].hour_count;
}
return value;
} else {
return 0;
}
};
const getFinishedHours = (params: any) => {
if (params) {
let value = 0;
for (let key in params) {
value += params[key].finished_count;
}
return value;
} else {
return 0;
}
};
const exportExcel = () => {
if (exportLoading) {
return;
}
setExportLoading(true);
let filter = {
sort_field: "",
sort_algo: "",
name: name,
email: email,
id_card: id_card,
show_mode: showMode,
};
member.departmentProgress(did, page, total, filter).then((res: any) => {
if (res.data.total === 0) {
message.error("数据为空");
setExportLoading(false);
return;
}
let filename = title + "学习进度.xlsx";
let sheetName = "sheet1";
let data = [];
let arr = ["学员"];
courses.map((item: any) => {
arr.push(item.title);
});
arr.push("总计课时");
data.push(arr);
res.data.data.forEach((item: any) => {
let arr = [item.name];
courses.map((it: any) => {
if (records && records[item.id] && records[item.id][it.id]) {
if (records && records[item.id][it.id].is_finished === 1) {
arr.push("已学完");
} else {
arr.push(
records &&
records[item.id][it.id].finished_count + " / " + it.class_hour
);
}
} else {
arr.push(0 + " / " + it.class_hour);
}
});
arr.push(getFinishedHours(records[item.id]) + " / " + totalHour);
data.push(arr);
});
const jsonWorkSheet = XLSX.utils.json_to_sheet(data);
const workBook: XLSX.WorkBook = {
SheetNames: [sheetName],
Sheets: {
[sheetName]: jsonWorkSheet,
},
};
XLSX.writeFile(workBook, filename);
setExportLoading(false);
});
};
return (
<div className="playedu-main-body">
<div className="float-left mb-24">
<BackBartment title={title + "学习进度"} />
</div>
<div className="float-left j-b-flex mb-24">
<div className="d-flex">
<Button type="default" onClick={() => exportExcel()}>
</Button>
<div className="helper-text ml-24">
/
</div>
</div>
<div className="d-flex">
<div className="d-flex mr-24 ">
<Typography.Text></Typography.Text>
<Input
value={name}
onChange={(e) => {
setName(e.target.value);
}}
allowClear
style={{ width: 160 }}
placeholder="请输入姓名关键字"
/>
</div>
{/* <div className="d-flex mr-24">
<Typography.Text>邮箱:</Typography.Text>
<Input
value={email}
onChange={(e) => {
setEmail(e.target.value);
}}
allowClear
style={{ width: 160 }}
placeholder="请输入邮箱"
/>
</div>
<div className="d-flex mr-24">
<Typography.Text>模式:</Typography.Text>
<Select
style={{ width: 160 }}
allowClear
placeholder="请选择"
value={showMode}
onChange={(value: string) => setShowMode(value)}
options={modes}
/>
</div> */}
<div className="d-flex">
<Button className="mr-16" onClick={resetData}>
</Button>
<Button
type="primary"
onClick={() => {
setPage(1);
setRefresh(!refresh);
}}
>
</Button>
</div>
</div>
</div>
<div className="float-left">
<Table
bordered
dataSource={list}
loading={loading}
pagination={paginationProps}
rowKey={(record) => record.id}
scroll={{ x: 1200 }}
>
<Column
fixed="left"
title="学员"
dataIndex="name"
key="name"
width={150}
render={(_, record: any) => (
<>
<Image
style={{ borderRadius: "50%" }}
src={record.avatar}
preview={false}
width={40}
height={40}
/>
<span className="ml-8">{record.name}</span>
</>
)}
/>
{courses.map((item: any) => (
<Column
title={item.title}
ellipsis={true}
dataIndex="id"
key={item.id}
width={168}
render={(_, record: any) => (
<>
{records[record.id] && records[record.id][item.id] ? (
records[record.id][item.id].is_finished === 1 ? (
<span></span>
) : (
<>
<span>
{records[record.id][item.id].finished_count}
</span>{" "}
/ <span>{item.class_hour}</span>
</>
)
) : (
<>
<span>0</span> / <span>{item.class_hour}</span>
</>
)}
</>
)}
/>
))}
<Column
fixed="right"
title="总计课时"
dataIndex="id"
key="id"
width={150}
render={(_, record: any) => (
<>
<span>{getFinishedHours(records[record.id])}</span> /{" "}
<span>{totalHour}</span>
</>
)}
/>
</Table>
</div>
</div>
);
};
export default MemberDepartmentProgressPage;

View File

@@ -58,6 +58,7 @@ const MemberImportPage = () => {
user
.storeBatch(2, data)
.then(() => {
setErrorData([]);
message.success("导入成功!");
navigate(-1);
})
@@ -92,9 +93,9 @@ const MemberImportPage = () => {
{errorData &&
errorData.map((item: any, index: number) => {
return (
<span key={index} className="c-red mb-10">
<div key={index} className="c-red mb-10">
{item}
</span>
</div>
);
})}
</div>

View File

@@ -8,13 +8,19 @@ import {
Table,
message,
Image,
Dropdown,
} from "antd";
import type { MenuProps } from "antd";
import type { ColumnsType } from "antd/es/table";
// import styles from "./index.module.less";
import { PlusOutlined, ExclamationCircleFilled } from "@ant-design/icons";
import {
PlusOutlined,
DownOutlined,
ExclamationCircleFilled,
} from "@ant-design/icons";
import { user } from "../../api/index";
import { dateFormat } from "../../utils/index";
import { Link } from "react-router-dom";
import { Link, Navigate } from "react-router-dom";
import { TreeDepartment, PerButton } from "../../compenents";
import { MemberCreate } from "./compenents/create";
import { MemberUpdate } from "./compenents/update";
@@ -50,7 +56,7 @@ const MemberPage = () => {
const columns: ColumnsType<DataType> = [
{
title: "学员姓名",
title: "学员",
dataIndex: "name",
render: (_, record: any) => (
<>
@@ -97,32 +103,73 @@ const MemberPage = () => {
key: "action",
fixed: "right",
width: 160,
render: (_, record: any) => (
<Space size="small">
<PerButton
type="link"
text="编辑"
class="b-link c-red"
icon={null}
p="user-update"
onClick={() => {
setMid(Number(record.id));
setUpdateVisible(true);
}}
disabled={null}
/>
<div className="form-column"></div>
<PerButton
type="link"
text="删除"
class="b-link c-red"
icon={null}
p="user-destroy"
onClick={() => delUser(record.id)}
disabled={null}
/>
</Space>
),
render: (_, record: any) => {
const items: MenuProps["items"] = [
{
key: "1",
label: (
<PerButton
type="link"
text="编辑"
class="b-link c-red"
icon={null}
p="user-update"
onClick={() => {
setMid(Number(record.id));
setUpdateVisible(true);
}}
disabled={null}
/>
),
},
{
key: "2",
label: (
<PerButton
type="link"
text="删除"
class="b-link c-red"
icon={null}
p="user-destroy"
onClick={() => delUser(record.id)}
disabled={null}
/>
),
},
];
return (
<Space size="small">
<Link
style={{ textDecoration: "none" }}
to={`/member/learn?id=${record.id}&name=${record.name}`}
>
<PerButton
type="link"
text="学习"
class="b-link c-red"
icon={null}
p="user-learn"
onClick={() => null}
disabled={null}
/>
</Link>
<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>
);
},
},
];
@@ -200,17 +247,25 @@ const MemberPage = () => {
<div className="tree-main-body">
<div className="left-box">
<TreeDepartment
refresh={refresh}
showNum={true}
type=""
text={"部门"}
onUpdate={(keys: any, title: any) => {
setDepIds(keys);
setLabel(title);
var index = title.indexOf("(");
if (index !== -1) {
var resolve = title.substring(0, index);
setLabel(resolve);
} else {
setLabel(title);
}
}}
/>
</div>
<div className="right-box">
<div className="playedu-main-title float-left mb-24">
/{selLabel}
| {selLabel}
</div>
<div className="float-left j-b-flex mb-24">
<div className="d-flex">
@@ -223,17 +278,37 @@ const MemberPage = () => {
onClick={() => setCreateVisible(true)}
disabled={null}
/>
<Link style={{ textDecoration: "none" }} to={`/member/import`}>
<PerButton
type="default"
text="批量导入学员"
class="mr-16"
icon={null}
p="user-store"
onClick={() => null}
disabled={null}
/>
</Link>
{dep_ids.length === 0 && (
<Link style={{ textDecoration: "none" }} to={`/member/import`}>
<PerButton
type="default"
text="批量导入学员"
class="mr-16"
icon={null}
p="user-store"
onClick={() => null}
disabled={null}
/>
</Link>
)}
{dep_ids.length > 0 && (
<Link
style={{ textDecoration: "none" }}
to={`/member/departmentUser?id=${dep_ids.join(
","
)}&title=${selLabel}`}
>
<PerButton
type="default"
text="部门学员进度"
class="mr-16"
icon={null}
p="department-user-learn"
onClick={() => null}
disabled={null}
/>
</Link>
)}
</div>
<div className="d-flex">
<div className="d-flex mr-24">
@@ -245,6 +320,7 @@ const MemberPage = () => {
}}
style={{ width: 160 }}
placeholder="请输入姓名关键字"
allowClear
/>
</div>
<div className="d-flex mr-24">
@@ -256,6 +332,7 @@ const MemberPage = () => {
}}
style={{ width: 160 }}
placeholder="请输入邮箱账号"
allowClear
/>
</div>
<div className="d-flex">
@@ -284,6 +361,7 @@ const MemberPage = () => {
/>
<MemberCreate
open={createVisible}
depIds={dep_ids}
onCancel={() => {
setCreateVisible(false);
setRefresh(!refresh);

View File

@@ -0,0 +1,14 @@
.large-title {
width: 100%;
height: 28px;
font-size: 18px;
font-weight: 600;
color: rgba(0, 0, 0, 0.88);
line-height: 28px;
}
.charts {
width: 100%;
height: 320px;
box-sizing: border-box;
}

332
src/pages/member/learn.tsx Normal file
View File

@@ -0,0 +1,332 @@
import { useState, useEffect, useRef } from "react";
import styles from "./learn.module.less";
import { Row, Image, Table, Button, Select } from "antd";
import { useLocation, useNavigate } from "react-router-dom";
import { BackBartment, DurationText } from "../../compenents";
import { dateFormat } from "../../utils/index";
import { user as member } from "../../api/index";
import * as echarts from "echarts";
import type { ColumnsType } from "antd/es/table";
import { MemberLearnProgressDialog } from "./compenents/progress";
interface DataType {
id: React.Key;
title: string;
type: string;
created_at: string;
total_duration: number;
finished_duration: number;
is_finished: boolean;
}
const MemberLearnPage = () => {
let chartRef = useRef(null);
const navigate = useNavigate();
const result = new URLSearchParams(useLocation().search);
const [loading2, setLoading2] = useState<boolean>(false);
const [list2, setList2] = useState<any>([]);
const [courses, setCourses] = useState<any>({});
const [deps, setDeps] = useState<any>([]);
const [depValue, setDepValue] = useState<number>(0);
const [currentCourses, setCurrentCourses] = useState<any>([]);
const [openCourses, setOpenCourses] = useState<any>([]);
const [records, setRecords] = useState<any>({});
const [total2, setTotal2] = useState(0);
const [refresh2, setRefresh2] = useState(false);
const [uid, setUid] = useState(Number(result.get("id")));
const [userName, setUserName] = useState<string>(String(result.get("name")));
const [visiable, setVisiable] = useState(false);
const [courseId, setcourseId] = useState<number>(0);
useEffect(() => {
setUid(Number(result.get("id")));
setUserName(String(result.get("name")));
setLoading2(false);
setRefresh2(!refresh2);
}, [result.get("id"), result.get("name")]);
useEffect(() => {
getZxtData();
return () => {
window.onresize = null;
};
}, [uid]);
useEffect(() => {
getLearnCourses();
}, [refresh2, uid]);
useEffect(() => {
if (depValue === 0) {
return;
}
let arr = [...courses[depValue]];
let arr2 = [...openCourses];
if (arr2.length > 0) {
var data = arr.concat(arr2);
setCurrentCourses(data);
} else {
setCurrentCourses(arr);
}
}, [depValue]);
const getZxtData = () => {
member.learnStats(uid).then((res: any) => {
renderView(res.data);
});
};
const minuteFormat = (duration: number) => {
if (duration === 0) {
return "0小时0分0秒";
}
let h = Math.trunc(duration / 3600);
let m = Math.trunc((duration % 3600) / 60);
let s = Math.trunc((duration % 3600) % 60);
return h + "小时" + m + "分" + s + "秒";
};
const renderView = (params: any) => {
const timeData: any = [];
const valueData: any = [];
params.map((item: any) => {
timeData.push(item.key);
valueData.push(item.value / 1000);
});
let dom: any = chartRef.current;
let myChart = echarts.init(dom);
myChart.setOption({
tooltip: {
trigger: "axis",
formatter: function (params: any) {
// 只粘贴formatter了
let relVal = params[0].axisValueLabel;
for (let i = 0; i < params.length; i++) {
relVal +=
"<br/>" +
params[i].marker +
params[i].seriesName +
": " +
minuteFormat(params[i].value);
}
return relVal;
},
},
legend: {
data: ["每日学习时长"],
x: "right",
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
data: timeData,
},
yAxis: {
type: "value",
},
series: [
{
name: "每日学习时长",
type: "line",
data: valueData,
color: "#ff4d4f",
},
],
});
window.onresize = () => {
myChart.resize();
};
};
const getLearnCourses = () => {
if (loading2) {
return;
}
setLoading2(true);
member.learnAllCourses(uid).then((res: any) => {
setList2(res.data.departments);
setCourses(res.data.dep_courses);
setOpenCourses(res.data.open_courses);
setRecords(res.data.user_course_records);
if (res.data.departments.length > 0) {
let box: any = [];
res.data.departments.map((item: any) => {
box.push({
label: item.name,
value: String(item.id),
});
});
setDepValue(Number(box[0].value));
setDeps(box);
} else {
setDepValue(0);
setDeps([]);
}
setLoading2(false);
});
};
const column2: ColumnsType<DataType> = [
{
title: "课程名称",
dataIndex: "title",
render: (_, record: any) => (
<div className="d-flex">
<Image
src={record.thumb}
preview={false}
width={80}
height={60}
style={{ borderRadius: 6 }}
/>
<span className="ml-8">{record.title}</span>
</div>
),
},
{
title: "课程进度",
dataIndex: "total_duration",
render: (_, record: any) => (
<>
<span>
{(records[record.id] && records[record.id].finished_count) ||
0} / {record.class_hour}
</span>
</>
),
},
{
title: "第一次学习时间",
dataIndex: "created_at",
render: (_, record: any) => (
<>
{records[record.id] ? (
<span>{dateFormat(records[record.id].created_at)}</span>
) : (
<span>-</span>
)}
</>
),
},
{
title: "学习完成时间",
dataIndex: "finished_at",
render: (_, record: any) => (
<>
{records[record.id] ? (
<span>{dateFormat(records[record.id].finished_at)}</span>
) : (
<span>-</span>
)}
</>
),
},
{
title: "学习进度",
dataIndex: "is_finished",
render: (_, record: any) => (
<>
{records[record.id] ? (
<span
className={
Math.floor(
(records[record.id].finished_count /
records[record.id].hour_count) *
100
) >= 100
? "c-green"
: "c-red"
}
>
{Math.floor(
(records[record.id].finished_count /
records[record.id].hour_count) *
100
)}
%
</span>
) : (
<span className="c-red">0%</span>
)}
</>
),
},
{
title: "操作",
key: "action",
fixed: "right",
width: 100,
render: (_, record: any) => (
<Button
type="link"
className="b-link c-red"
onClick={() => {
setcourseId(record.id);
setVisiable(true);
}}
>
</Button>
),
},
];
return (
<>
<Row className="playedu-main-top mb-24">
<MemberLearnProgressDialog
open={visiable}
uid={uid}
id={courseId}
onCancel={() => {
setVisiable(false);
setRefresh2(!refresh2);
}}
></MemberLearnProgressDialog>
<div className="float-left mb-24">
<BackBartment title={userName + "的学习明细"} />
</div>
<div className={styles["charts"]}>
<div
ref={chartRef}
style={{
width: "100% !important",
height: 300,
position: "relative",
}}
></div>
</div>
<div className="float-left mt-24">
{list2.length > 1 && (
<div className="d-flex mb-24">
<span></span>
<Select
style={{ width: 160 }}
allowClear
placeholder="请选择部门"
value={String(depValue)}
onChange={(value: string) => setDepValue(Number(value))}
options={deps}
/>
</div>
)}
<Table
columns={column2}
dataSource={currentCourses}
loading={loading2}
pagination={false}
rowKey={(record) => record.id}
/>
</div>
</Row>
</>
);
};
export default MemberLearnPage;

View File

@@ -164,7 +164,7 @@ const ResourceImagesPage = () => {
</div>
<div className="right-box">
<div className="d-flex playedu-main-title float-left mb-24">
/ {selLabel}
| {selLabel}
</div>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={24}>

View File

@@ -147,7 +147,11 @@ export const ResourceCategoryCreate: React.FC<PropInterface> = ({
name="name"
rules={[{ required: true, message: "请输入分类名称!" }]}
>
<Input style={{ width: 200 }} placeholder="请输入分类名称" />
<Input
style={{ width: 200 }}
allowClear
placeholder="请输入分类名称"
/>
</Form.Item>
</Form>
</div>

View File

@@ -169,7 +169,11 @@ export const ResourceCategoryUpdate: React.FC<PropInterface> = ({
name="name"
rules={[{ required: true, message: "请输入分类名称!" }]}
>
<Input style={{ width: 200 }} placeholder="请输入分类名称" />
<Input
style={{ width: 200 }}
allowClear
placeholder="请输入分类名称"
/>
</Form.Item>
</Form>
</div>

View File

@@ -372,15 +372,18 @@ const ResourceCategoryPage = () => {
</div>
<div className="playedu-main-body">
<div style={{ width: 366 }}>
<Tree
onSelect={onSelect}
treeData={treeData}
draggable
blockNode
onDragEnter={onDragEnter}
onDrop={onDrop}
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
/>
{treeData.length > 0 && (
<Tree
onSelect={onSelect}
treeData={treeData}
draggable
blockNode
onDragEnter={onDragEnter}
onDrop={onDrop}
defaultExpandAll={true}
switcherIcon={<i className="iconfont icon-icon-fold c-gray" />}
/>
)}
</div>
<ResourceCategoryCreate
open={createVisible}

View File

@@ -184,7 +184,7 @@ const ResourceVideosPage = () => {
</div>
<div className="right-box">
<div className="d-flex playedu-main-title float-left mb-24">
/ {selLabel}
| {selLabel}
</div>
<div className="float-left mb-24">
<UploadVideoButton

View File

@@ -125,14 +125,22 @@ export const SystemAdministratorCreate: React.FC<PropInterface> = ({
name="name"
rules={[{ required: true, message: "请输入管理员姓名!" }]}
>
<Input style={{ width: 200 }} placeholder="请输入管理员姓名" />
<Input
allowClear
style={{ width: 200 }}
placeholder="请输入管理员姓名"
/>
</Form.Item>
<Form.Item
label="邮箱"
name="email"
rules={[{ required: true, message: "请输入学员邮箱!" }]}
>
<Input style={{ width: 200 }} placeholder="请输入学员邮箱" />
<Input
allowClear
style={{ width: 200 }}
placeholder="请输入学员邮箱"
/>
</Form.Item>
<Form.Item
label="密码"
@@ -140,6 +148,7 @@ export const SystemAdministratorCreate: React.FC<PropInterface> = ({
rules={[{ required: true, message: "请输入登录密码!" }]}
>
<Input.Password
allowClear
style={{ width: 200 }}
placeholder="请输入登录密码"
/>

View File

@@ -30,7 +30,9 @@ export const SystemAdministratorUpdate: React.FC<PropInterface> = ({
if (id === 0) {
return;
}
getDetail();
if (open) {
getDetail();
}
}, [id, open]);
const getParams = () => {
@@ -131,18 +133,27 @@ export const SystemAdministratorUpdate: React.FC<PropInterface> = ({
name="name"
rules={[{ required: true, message: "请输入管理员姓名!" }]}
>
<Input style={{ width: 200 }} placeholder="请输入管理员姓名" />
<Input
allowClear
style={{ width: 200 }}
placeholder="请输入管理员姓名"
/>
</Form.Item>
<Form.Item
label="邮箱"
name="email"
rules={[{ required: true, message: "请输入学员邮箱!" }]}
>
<Input style={{ width: 200 }} placeholder="请输入学员邮箱" />
<Input
allowClear
style={{ width: 200 }}
placeholder="请输入学员邮箱"
/>
</Form.Item>
<Form.Item label="密码" name="password">
<Input.Password
style={{ width: 200 }}
allowClear
placeholder="请输入登录密码"
/>
</Form.Item>

View File

@@ -283,6 +283,7 @@ const SystemAdministratorPage = () => {
onChange={(e) => {
setName(e.target.value);
}}
allowClear
style={{ width: 160 }}
placeholder="请输入管理员姓名"
/>

View File

@@ -180,6 +180,7 @@ export const SystemAdminrolesCreate: React.FC<PropInterface> = ({
<Input
style={{ width: 424 }}
placeholder="请在此处输入角色名称"
allowClear
/>
</Form.Item>
<Form.Item label="操作权限" name="action_ids">

View File

@@ -35,7 +35,9 @@ export const SystemAdminrolesUpdate: React.FC<PropInterface> = ({
if (id === undefined) {
return;
}
getDetail();
if (open) {
getDetail();
}
}, [id, open]);
const getParams = () => {
@@ -189,7 +191,11 @@ export const SystemAdminrolesUpdate: React.FC<PropInterface> = ({
name="name"
rules={[{ required: true, message: "请输入角色名!" }]}
>
<Input style={{ width: 424 }} placeholder="请输入角色名" />
<Input
style={{ width: 424 }}
allowClear
placeholder="请输入角色名"
/>
</Form.Item>
<Form.Item label="操作权限" name="action_ids">
<TreeSelect

View File

@@ -23,6 +23,7 @@ const SystemConfigPage = () => {
const [loading, setLoading] = useState<boolean>(false);
const [logo, setLogo] = useState<string>("");
const [thumb, setThumb] = useState<string>("");
const [avatar, setAvatar] = useState<string>("");
const [tabKey, setTabKey] = useState(1);
const [nameChecked, setNameChecked] = useState(false);
const [emailChecked, setEmailChecked] = useState(false);
@@ -30,7 +31,7 @@ const SystemConfigPage = () => {
useEffect(() => {
getDetail();
}, []);
}, [tabKey]);
const getDetail = () => {
appConfig.appConfig().then((res: any) => {
@@ -103,6 +104,11 @@ const SystemConfigPage = () => {
form.setFieldsValue({
"system.pc_index_footer_msg": configData[i].key_value,
});
} else if (configData[i].key_name === "member.default_avatar") {
setAvatar(configData[i].key_value);
form.setFieldsValue({
"member.default_avatar": configData[i].key_value,
});
}
}
});
@@ -192,12 +198,13 @@ const SystemConfigPage = () => {
style={{ marginBottom: 30 }}
label="网站Logo"
name="system.logo"
labelCol={{ style: { marginTop: 8, marginLeft: 54 } }}
labelCol={{ style: { marginTop: 4, marginLeft: 54 } }}
>
<div className="d-flex">
<Image preview={false} height={40} src={logo} />
<div className="d-flex ml-24">
<UploadImageButton
text="更换Logo"
onSelected={(url) => {
setLogo(url);
form.setFieldsValue({ "system.logo": url });
@@ -219,6 +226,7 @@ const SystemConfigPage = () => {
<div className="d-flex">
<div className="d-flex ml-24">
<UploadImageButton
text="更换Logo"
onSelected={(url) => {
setLogo(url);
form.setFieldsValue({ "system.logo": url });
@@ -236,14 +244,22 @@ const SystemConfigPage = () => {
label="网站标题"
name="system.name"
>
<Input style={{ width: 274 }} placeholder="请填写网站标题" />
<Input
style={{ width: 274 }}
allowClear
placeholder="请填写网站标题"
/>
</Form.Item>
<Form.Item
style={{ marginBottom: 30 }}
label="网站页脚"
name="system.pc_index_footer_msg"
>
<Input style={{ width: 274 }} placeholder="请填写网站页脚" />
<Input
style={{ width: 274 }}
allowClear
placeholder="请填写网站页脚"
/>
</Form.Item>
{/* <Form.Item
style={{ marginBottom: 30 }}
@@ -307,7 +323,11 @@ const SystemConfigPage = () => {
<Form.Item style={{ marginBottom: 30 }} label="跑马灯内容">
<Space align="baseline" style={{ height: 32 }}>
<Form.Item name="player.bullet_secret_text">
<Input style={{ width: 274 }} placeholder="自定义跑马灯内容" />
<Input
style={{ width: 274 }}
allowClear
placeholder="自定义跑马灯内容"
/>
</Form.Item>
<Checkbox
checked={nameChecked}
@@ -363,6 +383,7 @@ const SystemConfigPage = () => {
/>
<div className="d-flex ml-24">
<UploadImageButton
text="更换封面"
onSelected={(url) => {
setThumb(url);
form.setFieldsValue({ "player.poster": url });
@@ -384,6 +405,7 @@ const SystemConfigPage = () => {
<div className="d-flex">
<div className="d-flex">
<UploadImageButton
text="更换封面"
onSelected={(url) => {
setThumb(url);
form.setFieldsValue({ "player.poster": url });
@@ -407,6 +429,79 @@ const SystemConfigPage = () => {
</Form>
),
},
{
key: "3",
label: `学员设置`,
children: (
<Form
form={form}
name="m-basic"
labelCol={{ span: 3 }}
wrapperCol={{ span: 21 }}
style={{ width: 1000, paddingTop: 30 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
{avatar && (
<Form.Item
style={{ marginBottom: 30 }}
label="学员默认头像"
name="member.default_avatar"
labelCol={{ style: { marginTop: 14, marginLeft: 28 } }}
>
<div className="d-flex">
<Image
preview={false}
width={60}
height={60}
src={avatar}
style={{ borderRadius: "50%" }}
/>
<div className="d-flex ml-24">
<UploadImageButton
text="更换头像"
onSelected={(url) => {
setAvatar(url);
form.setFieldsValue({ "member.default_avatar": url });
}}
></UploadImageButton>
<div className="helper-text ml-24"></div>
</div>
</div>
</Form.Item>
)}
{!avatar && (
<Form.Item
style={{ marginBottom: 30 }}
label="学员默认头像"
name="member.default_avatar"
>
<div className="d-flex">
<div className="d-flex">
<UploadImageButton
text="更换头像"
onSelected={(url) => {
setAvatar(url);
form.setFieldsValue({ "member.default_avatar": url });
}}
></UploadImageButton>
<div className="helper-text ml-24"></div>
</div>
</div>
</Form.Item>
)}
<Form.Item
style={{ marginBottom: 30 }}
wrapperCol={{ offset: 3, span: 21 }}
>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Form.Item>
</Form>
),
},
];
const onChange = (key: string) => {

View File

@@ -1,9 +1,10 @@
import { lazy } from "react";
import { RouteObject } from "react-router-dom";
import { login } from "../api";
import { login, system } from "../api";
import InitPage from "../pages/init";
import { getToken } from "../utils";
import KeepAlive from "../compenents/keep-alive";
import LoginPage from "../pages/login";
import HomePage from "../pages/home";
@@ -16,6 +17,8 @@ import CoursePage from "../pages/course/index";
import CourseUserPage from "../pages/course/user";
import MemberPage from "../pages/member";
import MemberImportPage from "../pages/member/import";
import MemberLearnPage from "../pages/member/learn";
import MemberDepartmentProgressPage from "../pages/member/departmentUser";
import SystemConfigPage from "../pages/system/config";
import SystemAdministratorPage from "../pages/system/administrator";
import SystemAdminrolesPage from "../pages/system/adminroles";
@@ -23,52 +26,34 @@ import DepartmentPage from "../pages/department";
import TestPage from "../pages/test";
import ErrorPage from "../pages/error";
// 异步加载页面
// const LoginPage = lazy(() => import("../pages/login"));
// const HomePage = lazy(() => import("../pages/home"));
// const DashboardPage = lazy(() => import("../pages/dashboard"));
// const ErrorPage = lazy(() => import("../pages/error"));
// const CoursePage = lazy(() => import("../pages/course"));
// const TestPage = lazy(() => import("../pages/test"));
// const MemberPage = lazy(() => import("../pages/member"));
// const MemberImportPage = lazy(() => import("../pages/member/import"));
// const SystemAdministratorPage = lazy(
// () => import("../pages/system/administrator")
// );
// const SystemAdminrolesPage = lazy(() => import("../pages/system/adminroles"));
// const DepartmentPage = lazy(() => import("../pages/department"));
// const ChangePasswordPage = lazy(() => import("../pages/change-password"));
// const ResourceImagesPage = lazy(() => import("../pages/resource/images"));
// const ResourceCategoryPage = lazy(
// () => import("../pages/resource/resource-category")
// );
// const ResourceVideosPage = lazy(() => import("../pages/resource/videos"));
// const SystemConfigPage = lazy(() => import("../pages/system/config"));
let RootPage: any = null;
if (getToken()) {
RootPage = lazy(async () => {
return new Promise<any>((resolve) => {
let userLoginToken = getToken();
if (!userLoginToken) {
return new Promise<any>(async (resolve) => {
try {
let configRes: any = await system.getSystemConfig();
let userRes: any = await login.getUser();
resolve({
default: InitPage,
default: (
<InitPage configData={configRes.data} loginData={userRes.data} />
),
});
} catch (e) {
console.error("系统初始化失败", e);
resolve({
default: <ErrorPage />,
});
return;
}
login.getUser().then((res: any) => {
resolve({
default: <InitPage loginData={res.data} />,
});
});
// todo token过期处理
});
});
} else {
if (window.location.pathname !== "/login") {
window.location.href = "/login";
}
RootPage = <InitPage loginData={null} />;
RootPage = <InitPage />;
}
const routes: RouteObject[] = [
@@ -110,11 +95,22 @@ const routes: RouteObject[] = [
},
{
path: "/member",
element: <MemberPage />,
},
{
path: "/member/import",
element: <MemberImportPage />,
element: <KeepAlive />,
children: [
{ path: "/member/index", element: <MemberPage /> },
{
path: "/member/import",
element: <MemberImportPage />,
},
{
path: "/member/learn",
element: <MemberLearnPage />,
},
{
path: "/member/departmentUser",
element: <MemberDepartmentProgressPage />,
},
],
},
{
path: "/system/config/index",

View File

@@ -1,25 +1,19 @@
import { createSlice } from "@reduxjs/toolkit";
type SystemConfigStoreInterface = {
systemApiUrl: string;
systemPcUrl: string;
systemH5Url: string;
systemLogo: string;
systemName: string;
};
let defaultValue: SystemConfigStoreInterface = {
systemApiUrl: "",
systemPcUrl: "",
systemH5Url: "",
systemLogo: "",
systemName: "",
systemApiUrl?: string;
systemPcUrl?: string;
systemH5Url?: string;
systemLogo?: string;
systemName?: string;
memberDefaultAvatar?: string;
courseDefaultThumbs?: string[];
};
const systemConfigSlice = createSlice({
name: "systemConfig",
initialState: {
value: defaultValue,
value: {},
},
reducers: {
saveConfigAction(stage, e) {

View File

@@ -13,10 +13,16 @@ export function clearToken() {
}
export function dateFormat(dateStr: string) {
if (!dateStr) {
return "-";
}
return moment(dateStr).format("YYYY-MM-DD HH:mm");
}
export function timeFormat(dateStr: number) {
if (!dateStr) {
return "-";
}
var d = moment.duration(dateStr, "seconds");
let value =
Math.floor(d.asDays()) +
@@ -117,4 +123,4 @@ export function ValidataCredentials(value: any) {
return true;
}
}
}
}