mirror of
https://github.com/PlayEdu/backend
synced 2025-10-27 06:42:59 +08:00
Compare commits
85 Commits
v1.0-beta.
...
v1.0-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d834ab2d6 | ||
|
|
6c3b29e919 | ||
|
|
e44b129f82 | ||
|
|
096466633f | ||
|
|
a656f21cbc | ||
|
|
d85ba55b9c | ||
|
|
5f26441b8c | ||
|
|
1900e55d03 | ||
|
|
e133c724f3 | ||
|
|
6bbe2cbeec | ||
|
|
e0e1192fb7 | ||
|
|
0eae2f7673 | ||
|
|
86ab6b7751 | ||
|
|
09cbffe3ae | ||
|
|
33e302503c | ||
|
|
86a16c9722 | ||
|
|
391af1a488 | ||
|
|
18f6056dc9 | ||
|
|
b897dfffac | ||
|
|
0dfbaaf5a6 | ||
|
|
ea934eae07 | ||
|
|
516c718c78 | ||
|
|
1b7f11489b | ||
|
|
b34cd955c4 | ||
|
|
27ec0e6c4a | ||
|
|
c7fa24021b | ||
|
|
1aa209640d | ||
|
|
409fa080fc | ||
|
|
7383f08ab8 | ||
|
|
492270bfc3 | ||
|
|
685c9001b8 | ||
|
|
3982380c44 | ||
|
|
b5e2d351e5 | ||
|
|
e32ecd205e | ||
|
|
0c5a7f2f60 | ||
|
|
746a48d4d6 | ||
|
|
b8bb5234ca | ||
|
|
1782f6acef | ||
|
|
b197da34c8 | ||
|
|
d4df2fd07f | ||
|
|
a18d731913 | ||
|
|
922708647e | ||
|
|
41ef729b3f | ||
|
|
43abf5c1cb | ||
|
|
795609f5be | ||
|
|
2afada157d | ||
|
|
e7a63c350f | ||
|
|
c9253af78f | ||
|
|
914b530004 | ||
|
|
13af594ed0 | ||
|
|
c7788f4d2b | ||
|
|
361da70ed5 | ||
|
|
53132ffd3c | ||
|
|
947e48fe0d | ||
|
|
a71be154d8 | ||
|
|
db39805031 | ||
|
|
6c923e1756 | ||
|
|
b616335d18 | ||
|
|
30785497d3 | ||
|
|
2a2a091144 | ||
|
|
f5ad3a4eb2 | ||
|
|
46e352fac3 | ||
|
|
c01de5b51b | ||
|
|
c0e9e5bd5c | ||
|
|
016f5b4a70 | ||
|
|
e57ef9ce46 | ||
|
|
36a24ae87f | ||
|
|
23d16b52dd | ||
|
|
9dadb9818d | ||
|
|
a475e693dc | ||
|
|
0fe75d745e | ||
|
|
facbaad8f5 | ||
|
|
9b4e53176c | ||
|
|
060d686cee | ||
|
|
d2ca3f535f | ||
|
|
d90afcc08b | ||
|
|
dfc33aa754 | ||
|
|
e2a0aaf695 | ||
|
|
f728ded148 | ||
|
|
72a91d642a | ||
|
|
f05a696941 | ||
|
|
86964737d3 | ||
|
|
7bc01276b5 | ||
|
|
980d559fb9 | ||
|
|
757e9ace1a |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
/node_modules
|
||||
/build
|
||||
/dist
|
||||
12
Dockerfile
12
Dockerfile
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Binary file not shown.
13
src/AutoTop.ts
Normal file
13
src/AutoTop.ts
Normal 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;
|
||||
@@ -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", {});
|
||||
}
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export const CreateResourceCategory = (props: PropInterface) => {
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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") {
|
||||
|
||||
29
src/compenents/keep-alive/index.tsx
Normal file
29
src/compenents/keep-alive/index.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -451,6 +451,7 @@ export const CourseHourUpdate: React.FC<PropInterface> = ({
|
||||
saveChapterName(index, e.target.value);
|
||||
}}
|
||||
placeholder="请在此处输入章节名称"
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
className="mr-16"
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}>
|
||||
重 置
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -15,7 +15,7 @@ const ErrorPage = () => {
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
navigate("/", { replace: true });
|
||||
}}
|
||||
>
|
||||
返回首页
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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"]}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
0
src/pages/member/compenents/progress.module.scss
Normal file
0
src/pages/member/compenents/progress.module.scss
Normal file
252
src/pages/member/compenents/progress.tsx
Normal file
252
src/pages/member/compenents/progress.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
0
src/pages/member/departmentUser.module.less
Normal file
0
src/pages/member/departmentUser.module.less
Normal file
346
src/pages/member/departmentUser.tsx
Normal file
346
src/pages/member/departmentUser.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
14
src/pages/member/learn.module.less
Normal file
14
src/pages/member/learn.module.less
Normal 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
332
src/pages/member/learn.tsx
Normal 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;
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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="请输入登录密码"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -283,6 +283,7 @@ const SystemAdministratorPage = () => {
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
}}
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
placeholder="请输入管理员姓名"
|
||||
/>
|
||||
|
||||
@@ -180,6 +180,7 @@ export const SystemAdminrolesCreate: React.FC<PropInterface> = ({
|
||||
<Input
|
||||
style={{ width: 424 }}
|
||||
placeholder="请在此处输入角色名称"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="操作权限" name="action_ids">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user