mirror of
https://github.com/PlayEdu/backend
synced 2025-06-28 13:04:40 +08:00
commit
7383f08ab8
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
|
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
|
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;
|
@ -78,3 +78,46 @@ export function storeBatch(startLine: number, users: string[][]) {
|
|||||||
users: users,
|
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 departmentProgress(
|
||||||
|
id: number,
|
||||||
|
page: number,
|
||||||
|
size: number,
|
||||||
|
params: object
|
||||||
|
) {
|
||||||
|
return client.get(`/backend/v1/department/${id}/users`, {
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { useDispatch, useSelector } from "react-redux";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import avatar from "../../assets/images/commen/avatar.png";
|
import avatar from "../../assets/images/commen/avatar.png";
|
||||||
import { logoutAction } from "../../store/user/loginUserSlice";
|
import { logoutAction } from "../../store/user/loginUserSlice";
|
||||||
|
import { clearToken } from "../../utils/index";
|
||||||
|
|
||||||
export const Header: React.FC = () => {
|
export const Header: React.FC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -13,6 +14,7 @@ export const Header: React.FC = () => {
|
|||||||
|
|
||||||
const onClick: MenuProps["onClick"] = ({ key }) => {
|
const onClick: MenuProps["onClick"] = ({ key }) => {
|
||||||
if (key === "login_out") {
|
if (key === "login_out") {
|
||||||
|
clearToken();
|
||||||
dispatch(logoutAction());
|
dispatch(logoutAction());
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
} else if (key === "change_password") {
|
} else if (key === "change_password") {
|
||||||
|
@ -11,6 +11,8 @@ interface Option {
|
|||||||
interface PropInterface {
|
interface PropInterface {
|
||||||
type: string;
|
type: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
refresh: boolean;
|
||||||
|
showNum: boolean;
|
||||||
onUpdate: (keys: any, title: any) => void;
|
onUpdate: (keys: any, title: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,14 +20,22 @@ export const TreeDepartment = (props: PropInterface) => {
|
|||||||
const [treeData, setTreeData] = useState<any>([]);
|
const [treeData, setTreeData] = useState<any>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [selectKey, setSelectKey] = useState<any>([]);
|
const [selectKey, setSelectKey] = useState<any>([]);
|
||||||
|
const [userTotal, setUserTotal] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
department.departmentList().then((res: any) => {
|
department.departmentList().then((res: any) => {
|
||||||
const departments = res.data.departments;
|
const departments = res.data.departments;
|
||||||
|
const departCount = res.data.dep_user_count;
|
||||||
|
setUserTotal(res.data.user_total);
|
||||||
if (JSON.stringify(departments) !== "{}") {
|
if (JSON.stringify(departments) !== "{}") {
|
||||||
const new_arr: Option[] = checkArr(departments, 0);
|
if (props.showNum) {
|
||||||
setTreeData(new_arr);
|
const new_arr: any = checkNewArr(departments, 0, departCount);
|
||||||
|
setTreeData(new_arr);
|
||||||
|
} else {
|
||||||
|
const new_arr: Option[] = checkArr(departments, 0);
|
||||||
|
setTreeData(new_arr);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const new_arr: Option[] = [
|
const new_arr: Option[] = [
|
||||||
{
|
{
|
||||||
@ -38,7 +48,39 @@ export const TreeDepartment = (props: PropInterface) => {
|
|||||||
}
|
}
|
||||||
setLoading(false);
|
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 checkArr = (departments: any[], id: number) => {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
@ -60,6 +102,15 @@ export const TreeDepartment = (props: PropInterface) => {
|
|||||||
return arr;
|
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) => {
|
const onSelect = (selectedKeys: any, info: any) => {
|
||||||
let label = "全部" + props.text;
|
let label = "全部" + props.text;
|
||||||
if (info) {
|
if (info) {
|
||||||
@ -89,6 +140,7 @@ export const TreeDepartment = (props: PropInterface) => {
|
|||||||
onClick={() => onSelect([], "")}
|
onClick={() => onSelect([], "")}
|
||||||
>
|
>
|
||||||
全部{props.text}
|
全部{props.text}
|
||||||
|
{props.showNum && userTotal ? "(" + userTotal + ")" : ""}
|
||||||
</div>
|
</div>
|
||||||
{treeData.length > 0 && (
|
{treeData.length > 0 && (
|
||||||
<Tree
|
<Tree
|
||||||
|
@ -257,6 +257,17 @@ code {
|
|||||||
position: relative;
|
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 {
|
.playedu-main-body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -7,6 +7,7 @@ import { Provider } from "react-redux";
|
|||||||
import store from "./store";
|
import store from "./store";
|
||||||
import { ConfigProvider } from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import zhCN from "antd/locale/zh_CN";
|
import zhCN from "antd/locale/zh_CN";
|
||||||
|
import AutoScorllTop from "./AutoTop";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
@ -15,7 +16,9 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|||||||
theme={{ token: { colorPrimary: "#ff4d4f" } }}
|
theme={{ token: { colorPrimary: "#ff4d4f" } }}
|
||||||
>
|
>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<AutoScorllTop>
|
||||||
|
<App />
|
||||||
|
</AutoScorllTop>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
@ -89,6 +89,8 @@ const CoursePage = () => {
|
|||||||
children: (
|
children: (
|
||||||
<div className="float-left">
|
<div className="float-left">
|
||||||
<TreeDepartment
|
<TreeDepartment
|
||||||
|
refresh={refresh}
|
||||||
|
showNum={false}
|
||||||
type="no-course"
|
type="no-course"
|
||||||
text={"部门"}
|
text={"部门"}
|
||||||
onUpdate={(keys: any, title: any) => {
|
onUpdate={(keys: any, title: any) => {
|
||||||
@ -231,7 +233,9 @@ const CoursePage = () => {
|
|||||||
p="course"
|
p="course"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCid(Number(record.id));
|
setCid(Number(record.id));
|
||||||
navigate("/course/user/" + Number(record.id));
|
navigate(
|
||||||
|
"/course/user/" + Number(record.id) + "?title=" + record.title
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
disabled={null}
|
disabled={null}
|
||||||
/>
|
/>
|
||||||
@ -337,13 +341,14 @@ const CoursePage = () => {
|
|||||||
<Tabs
|
<Tabs
|
||||||
defaultActiveKey="1"
|
defaultActiveKey="1"
|
||||||
centered
|
centered
|
||||||
|
tabBarGutter={55}
|
||||||
items={items}
|
items={items}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="right-box">
|
<div className="right-box">
|
||||||
<div className="playedu-main-title float-left mb-24">
|
<div className="playedu-main-title float-left mb-24">
|
||||||
课程/{selLabel}
|
线上课 | {selLabel}
|
||||||
</div>
|
</div>
|
||||||
<div className="float-left j-b-flex mb-24">
|
<div className="float-left j-b-flex mb-24">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { course } from "../../api";
|
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 type { ColumnsType } from "antd/es/table";
|
||||||
import { BackBartment } from "../../compenents";
|
import { BackBartment } from "../../compenents";
|
||||||
import { ExclamationCircleFilled } from "@ant-design/icons";
|
import { ExclamationCircleFilled } from "@ant-design/icons";
|
||||||
@ -31,6 +31,7 @@ interface DataType {
|
|||||||
|
|
||||||
const CourseUserPage = () => {
|
const CourseUserPage = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const result = new URLSearchParams(useLocation().search);
|
||||||
const [list, setList] = useState<any>([]);
|
const [list, setList] = useState<any>([]);
|
||||||
const [users, setUsers] = useState<any>([]);
|
const [users, setUsers] = useState<any>([]);
|
||||||
const [refresh, setRefresh] = useState(false);
|
const [refresh, setRefresh] = useState(false);
|
||||||
@ -42,10 +43,11 @@ const CourseUserPage = () => {
|
|||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
const [idCard, setIdCard] = useState<string>("");
|
const [idCard, setIdCard] = useState<string>("");
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
|
||||||
|
const [title, setTitle] = useState<string>(String(result.get("title")));
|
||||||
|
|
||||||
const columns: ColumnsType<DataType> = [
|
const columns: ColumnsType<DataType> = [
|
||||||
{
|
{
|
||||||
title: "学员名称",
|
title: "学员",
|
||||||
render: (_, record: any) => (
|
render: (_, record: any) => (
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<Image
|
<Image
|
||||||
@ -70,11 +72,6 @@ const CourseUserPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "学习进度",
|
|
||||||
dataIndex: "progress",
|
|
||||||
render: (progress: number) => <span>{progress / 100}%</span>,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "第一次学习时间",
|
title: "第一次学习时间",
|
||||||
dataIndex: "created_at",
|
dataIndex: "created_at",
|
||||||
@ -85,6 +82,15 @@ const CourseUserPage = () => {
|
|||||||
dataIndex: "finished_at",
|
dataIndex: "finished_at",
|
||||||
render: (text: string) => <span>{dateFormat(text)}</span>,
|
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(() => {
|
useEffect(() => {
|
||||||
@ -178,7 +184,7 @@ const CourseUserPage = () => {
|
|||||||
<Row className="playedu-main-body">
|
<Row className="playedu-main-body">
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<div className="float-left mb-24">
|
<div className="float-left mb-24">
|
||||||
<BackBartment title="线上课学员" />
|
<BackBartment title={title || "线上课学员"} />
|
||||||
</div>
|
</div>
|
||||||
<div className="float-left j-b-flex mb-24">
|
<div className="float-left j-b-flex mb-24">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
@ -215,7 +221,7 @@ const CourseUserPage = () => {
|
|||||||
placeholder="请输入学员邮箱"
|
placeholder="请输入学员邮箱"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex mr-24">
|
{/* <div className="d-flex mr-24">
|
||||||
<Typography.Text>身份证号:</Typography.Text>
|
<Typography.Text>身份证号:</Typography.Text>
|
||||||
<Input
|
<Input
|
||||||
value={idCard}
|
value={idCard}
|
||||||
@ -225,7 +231,7 @@ const CourseUserPage = () => {
|
|||||||
style={{ width: 160 }}
|
style={{ width: 160 }}
|
||||||
placeholder="请输入身份证号"
|
placeholder="请输入身份证号"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<Button className="mr-16" onClick={resetList}>
|
<Button className="mr-16" onClick={resetList}>
|
||||||
重 置
|
重 置
|
||||||
|
@ -15,7 +15,7 @@ const ErrorPage = () => {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/");
|
navigate("/", { replace: true });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
返回首页
|
返回首页
|
||||||
|
@ -71,7 +71,7 @@ const LoginPage = () => {
|
|||||||
await getSystemConfig(); //获取系统配置并写入store
|
await getSystemConfig(); //获取系统配置并写入store
|
||||||
await getUser(); //获取登录用户的信息并写入store
|
await getUser(); //获取登录用户的信息并写入store
|
||||||
|
|
||||||
navigate("/");
|
navigate("/", { replace: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error("登录出现错误");
|
message.error("登录出现错误");
|
||||||
console.error("错误信息", e);
|
console.error("错误信息", e);
|
||||||
|
0
src/pages/member/departmentUser.module.less
Normal file
0
src/pages/member/departmentUser.module.less
Normal file
262
src/pages/member/departmentUser.tsx
Normal file
262
src/pages/member/departmentUser.tsx
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import styles from "./departmentUser.module.less";
|
||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Image,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
message,
|
||||||
|
Table,
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
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 [did, setDid] = useState(Number(result.get("id")));
|
||||||
|
const [title, setTitle] = useState(String(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,
|
||||||
|
})
|
||||||
|
.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("");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 helper-text ">
|
||||||
|
(以下表格内数字对应的是表头课程的“已学完课时数/总课时数”)
|
||||||
|
</div>
|
||||||
|
<div className="d-flex">
|
||||||
|
<div className="d-flex mr-24 ">
|
||||||
|
<Typography.Text>姓名:</Typography.Text>
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => {
|
||||||
|
setName(e.target.value);
|
||||||
|
}}
|
||||||
|
style={{ width: 160 }}
|
||||||
|
placeholder="请输入姓名关键字"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex mr-24">
|
||||||
|
<Typography.Text>邮箱:</Typography.Text>
|
||||||
|
<Input
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEmail(e.target.value);
|
||||||
|
}}
|
||||||
|
style={{ width: 160 }}
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* <div className="d-flex mr-24">
|
||||||
|
<Typography.Text>身份证号:</Typography.Text>
|
||||||
|
<Input
|
||||||
|
value={id_card}
|
||||||
|
onChange={(e) => {
|
||||||
|
setIdCard(e.target.value);
|
||||||
|
}}
|
||||||
|
style={{ width: 160 }}
|
||||||
|
placeholder="请输入身份证号"
|
||||||
|
/>
|
||||||
|
</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={100}
|
||||||
|
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={100}
|
||||||
|
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={100}
|
||||||
|
render={(_, record: any) => (
|
||||||
|
<>
|
||||||
|
<span>{getFinishedHours(records[record.id])}</span> /{" "}
|
||||||
|
<span>{totalHour}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MemberDepartmentProgressPage;
|
@ -8,13 +8,19 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
message,
|
message,
|
||||||
Image,
|
Image,
|
||||||
|
Dropdown,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
|
import type { MenuProps } from "antd";
|
||||||
import type { ColumnsType } from "antd/es/table";
|
import type { ColumnsType } from "antd/es/table";
|
||||||
// import styles from "./index.module.less";
|
// 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 { user } from "../../api/index";
|
||||||
import { dateFormat } from "../../utils/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 { TreeDepartment, PerButton } from "../../compenents";
|
||||||
import { MemberCreate } from "./compenents/create";
|
import { MemberCreate } from "./compenents/create";
|
||||||
import { MemberUpdate } from "./compenents/update";
|
import { MemberUpdate } from "./compenents/update";
|
||||||
@ -50,7 +56,7 @@ const MemberPage = () => {
|
|||||||
|
|
||||||
const columns: ColumnsType<DataType> = [
|
const columns: ColumnsType<DataType> = [
|
||||||
{
|
{
|
||||||
title: "学员姓名",
|
title: "学员",
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
render: (_, record: any) => (
|
render: (_, record: any) => (
|
||||||
<>
|
<>
|
||||||
@ -97,32 +103,73 @@ const MemberPage = () => {
|
|||||||
key: "action",
|
key: "action",
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
width: 160,
|
width: 160,
|
||||||
render: (_, record: any) => (
|
render: (_, record: any) => {
|
||||||
<Space size="small">
|
const items: MenuProps["items"] = [
|
||||||
<PerButton
|
{
|
||||||
type="link"
|
key: "1",
|
||||||
text="编辑"
|
label: (
|
||||||
class="b-link c-red"
|
<PerButton
|
||||||
icon={null}
|
type="link"
|
||||||
p="user-update"
|
text="编辑"
|
||||||
onClick={() => {
|
class="b-link c-red"
|
||||||
setMid(Number(record.id));
|
icon={null}
|
||||||
setUpdateVisible(true);
|
p="user-update"
|
||||||
}}
|
onClick={() => {
|
||||||
disabled={null}
|
setMid(Number(record.id));
|
||||||
/>
|
setUpdateVisible(true);
|
||||||
<div className="form-column"></div>
|
}}
|
||||||
<PerButton
|
disabled={null}
|
||||||
type="link"
|
/>
|
||||||
text="删除"
|
),
|
||||||
class="b-link c-red"
|
},
|
||||||
icon={null}
|
{
|
||||||
p="user-destroy"
|
key: "2",
|
||||||
onClick={() => delUser(record.id)}
|
label: (
|
||||||
disabled={null}
|
<PerButton
|
||||||
/>
|
type="link"
|
||||||
</Space>
|
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}`}
|
||||||
|
>
|
||||||
|
<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="tree-main-body">
|
||||||
<div className="left-box">
|
<div className="left-box">
|
||||||
<TreeDepartment
|
<TreeDepartment
|
||||||
|
refresh={refresh}
|
||||||
|
showNum={true}
|
||||||
type=""
|
type=""
|
||||||
text={"部门"}
|
text={"部门"}
|
||||||
onUpdate={(keys: any, title: any) => {
|
onUpdate={(keys: any, title: any) => {
|
||||||
setDepIds(keys);
|
setDepIds(keys);
|
||||||
setLabel(title);
|
var index = title.indexOf("(");
|
||||||
|
if (index !== -1) {
|
||||||
|
var resolve = title.substring(0, index);
|
||||||
|
setLabel(resolve);
|
||||||
|
} else {
|
||||||
|
setLabel(title);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="right-box">
|
<div className="right-box">
|
||||||
<div className="playedu-main-title float-left mb-24">
|
<div className="playedu-main-title float-left mb-24">
|
||||||
学员/{selLabel}
|
学员 | {selLabel}
|
||||||
</div>
|
</div>
|
||||||
<div className="float-left j-b-flex mb-24">
|
<div className="float-left j-b-flex mb-24">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
@ -223,17 +278,37 @@ const MemberPage = () => {
|
|||||||
onClick={() => setCreateVisible(true)}
|
onClick={() => setCreateVisible(true)}
|
||||||
disabled={null}
|
disabled={null}
|
||||||
/>
|
/>
|
||||||
<Link style={{ textDecoration: "none" }} to={`/member/import`}>
|
{dep_ids.length === 0 && (
|
||||||
<PerButton
|
<Link style={{ textDecoration: "none" }} to={`/member/import`}>
|
||||||
type="default"
|
<PerButton
|
||||||
text="批量导入学员"
|
type="default"
|
||||||
class="mr-16"
|
text="批量导入学员"
|
||||||
icon={null}
|
class="mr-16"
|
||||||
p="user-store"
|
icon={null}
|
||||||
onClick={() => null}
|
p="user-store"
|
||||||
disabled={null}
|
onClick={() => null}
|
||||||
/>
|
disabled={null}
|
||||||
</Link>
|
/>
|
||||||
|
</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>
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<div className="d-flex mr-24">
|
<div className="d-flex mr-24">
|
||||||
|
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;
|
||||||
|
}
|
348
src/pages/member/learn.tsx
Normal file
348
src/pages/member/learn.tsx
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import styles from "./learn.module.less";
|
||||||
|
import { Row, Image, Table } from "antd";
|
||||||
|
import { useLocation } 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 { duration } from "moment";
|
||||||
|
|
||||||
|
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 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 [hours, setHours] = useState<any>({});
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [refresh, setRefresh] = useState(false);
|
||||||
|
const [loading2, setLoading2] = useState<boolean>(false);
|
||||||
|
const [page2, setPage2] = useState(1);
|
||||||
|
const [size2, setSize2] = useState(10);
|
||||||
|
const [list2, setList2] = useState<any>([]);
|
||||||
|
const [courses, setCourses] = useState<any>({});
|
||||||
|
const [total2, setTotal2] = useState(0);
|
||||||
|
const [refresh2, setRefresh2] = useState(false);
|
||||||
|
const [uid, setUid] = useState(Number(result.get("id")));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getZxtData();
|
||||||
|
return () => {
|
||||||
|
window.onresize = null;
|
||||||
|
};
|
||||||
|
}, [uid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLearnHours();
|
||||||
|
}, [refresh, page, size]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLearnCourses();
|
||||||
|
}, [refresh2, page2, size2]);
|
||||||
|
|
||||||
|
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 getLearnHours = () => {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
member
|
||||||
|
.learnHours(uid, page, size, {
|
||||||
|
sort_field: "",
|
||||||
|
sort_algo: "",
|
||||||
|
is_finished: "",
|
||||||
|
})
|
||||||
|
.then((res: any) => {
|
||||||
|
setList(res.data.data);
|
||||||
|
setHours(res.data.hours);
|
||||||
|
setTotal(res.data.total);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLearnCourses = () => {
|
||||||
|
if (loading2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading2(true);
|
||||||
|
member
|
||||||
|
.learnCourses(uid, page2, size2, {
|
||||||
|
sort_field: "",
|
||||||
|
sort_algo: "",
|
||||||
|
is_finished: "",
|
||||||
|
})
|
||||||
|
.then((res: any) => {
|
||||||
|
setList2(res.data.data);
|
||||||
|
setCourses(res.data.courses);
|
||||||
|
setTotal2(res.data.total);
|
||||||
|
setLoading2(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 paginationProps2 = {
|
||||||
|
current: page2, //当前页码
|
||||||
|
pageSize: size2,
|
||||||
|
total: total2, // 总条数
|
||||||
|
onChange: (page: number, pageSize: number) =>
|
||||||
|
handlePageChange2(page, pageSize), //改变页码的函数
|
||||||
|
showSizeChanger: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange2 = (page: number, pageSize: number) => {
|
||||||
|
setPage2(page);
|
||||||
|
setSize2(pageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<DataType> = [
|
||||||
|
{
|
||||||
|
title: "课时标题",
|
||||||
|
dataIndex: "title",
|
||||||
|
render: (_, record: any) => (
|
||||||
|
<>
|
||||||
|
<span>{hours[record.hour_id].title}</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "课时类型",
|
||||||
|
dataIndex: "type",
|
||||||
|
render: (_, record: any) => (
|
||||||
|
<>
|
||||||
|
<span>{hours[record.hour_id].type}</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "总时长",
|
||||||
|
dataIndex: "total_duration",
|
||||||
|
render: (_, record: any) => (
|
||||||
|
<>
|
||||||
|
<DurationText duration={record.total_duration}></DurationText>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "已学习时长",
|
||||||
|
dataIndex: "finished_duration",
|
||||||
|
render: (_, record: any) => (
|
||||||
|
<>
|
||||||
|
<DurationText duration={record.finished_duration || 0}></DurationText>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
dataIndex: "is_finished",
|
||||||
|
render: (_, record: any) => (
|
||||||
|
<>
|
||||||
|
{record.is_finished === 1 ? <span>已学完</span> : <span>未学完</span>}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "时间",
|
||||||
|
dataIndex: "created_at",
|
||||||
|
render: (text: string) => <span>{dateFormat(text)}</span>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const column2: ColumnsType<DataType> = [
|
||||||
|
{
|
||||||
|
title: "课程名称",
|
||||||
|
dataIndex: "title",
|
||||||
|
render: (_, record: any) => (
|
||||||
|
<div className="d-flex">
|
||||||
|
<Image
|
||||||
|
src={courses[record.course_id].thumb}
|
||||||
|
preview={false}
|
||||||
|
width={80}
|
||||||
|
height={60}
|
||||||
|
style={{ borderRadius: 6 }}
|
||||||
|
/>
|
||||||
|
<span className="ml-8">{courses[record.course_id].title}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "课程进度",
|
||||||
|
dataIndex: "total_duration",
|
||||||
|
render: (_, record: any) => (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
已完成课时:{record.finished_count} / {record.hour_count}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "第一次学习时间",
|
||||||
|
dataIndex: "created_at",
|
||||||
|
render: (text: string) => <span>{dateFormat(text)}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "学习完成时间",
|
||||||
|
dataIndex: "finished_at",
|
||||||
|
render: (text: string) => <span>{dateFormat(text)}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "学习进度",
|
||||||
|
dataIndex: "is_finished",
|
||||||
|
render: (_, record: any) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
Math.floor((record.finished_count / record.hour_count) * 100) >=
|
||||||
|
100
|
||||||
|
? "c-green"
|
||||||
|
: "c-red"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Math.floor((record.finished_count / record.hour_count) * 100)}%
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row className="playedu-main-top mb-24">
|
||||||
|
<div className="float-left mb-24">
|
||||||
|
<BackBartment title="学员学习" />
|
||||||
|
</div>
|
||||||
|
<div className={styles["charts"]}>
|
||||||
|
<div
|
||||||
|
ref={chartRef}
|
||||||
|
style={{
|
||||||
|
width: "100% !important",
|
||||||
|
height: 300,
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="float-left mt-24">
|
||||||
|
<Table
|
||||||
|
columns={column2}
|
||||||
|
dataSource={list2}
|
||||||
|
loading={loading2}
|
||||||
|
pagination={paginationProps2}
|
||||||
|
rowKey={(record) => record.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
{/* <div className="playedu-main-top mb-24">
|
||||||
|
<div className={styles["large-title"]}>课时学习记录</div>
|
||||||
|
<div className="float-left mt-24">
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={list}
|
||||||
|
loading={loading}
|
||||||
|
pagination={paginationProps}
|
||||||
|
rowKey={(record) => record.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MemberLearnPage;
|
@ -164,7 +164,7 @@ const ResourceImagesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="right-box">
|
<div className="right-box">
|
||||||
<div className="d-flex playedu-main-title float-left mb-24">
|
<div className="d-flex playedu-main-title float-left mb-24">
|
||||||
图片 / {selLabel}
|
图片 | {selLabel}
|
||||||
</div>
|
</div>
|
||||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
|
@ -184,7 +184,7 @@ const ResourceVideosPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="right-box">
|
<div className="right-box">
|
||||||
<div className="d-flex playedu-main-title float-left mb-24">
|
<div className="d-flex playedu-main-title float-left mb-24">
|
||||||
视频 / {selLabel}
|
视频 | {selLabel}
|
||||||
</div>
|
</div>
|
||||||
<div className="float-left mb-24">
|
<div className="float-left mb-24">
|
||||||
<UploadVideoButton
|
<UploadVideoButton
|
||||||
|
@ -23,6 +23,7 @@ const SystemConfigPage = () => {
|
|||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [logo, setLogo] = useState<string>("");
|
const [logo, setLogo] = useState<string>("");
|
||||||
const [thumb, setThumb] = useState<string>("");
|
const [thumb, setThumb] = useState<string>("");
|
||||||
|
const [avatar, setAvatar] = useState<string>("");
|
||||||
const [tabKey, setTabKey] = useState(1);
|
const [tabKey, setTabKey] = useState(1);
|
||||||
const [nameChecked, setNameChecked] = useState(false);
|
const [nameChecked, setNameChecked] = useState(false);
|
||||||
const [emailChecked, setEmailChecked] = useState(false);
|
const [emailChecked, setEmailChecked] = useState(false);
|
||||||
@ -103,6 +104,11 @@ const SystemConfigPage = () => {
|
|||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
"system.pc_index_footer_msg": configData[i].key_value,
|
"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,7 +198,7 @@ const SystemConfigPage = () => {
|
|||||||
style={{ marginBottom: 30 }}
|
style={{ marginBottom: 30 }}
|
||||||
label="网站Logo"
|
label="网站Logo"
|
||||||
name="system.logo"
|
name="system.logo"
|
||||||
labelCol={{ style: { marginTop: 8, marginLeft: 54 } }}
|
labelCol={{ style: { marginTop: 4, marginLeft: 54 } }}
|
||||||
>
|
>
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<Image preview={false} height={40} src={logo} />
|
<Image preview={false} height={40} src={logo} />
|
||||||
@ -411,6 +417,79 @@ const SystemConfigPage = () => {
|
|||||||
</Form>
|
</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) => {
|
const onChange = (key: string) => {
|
||||||
|
@ -16,6 +16,8 @@ import CoursePage from "../pages/course/index";
|
|||||||
import CourseUserPage from "../pages/course/user";
|
import CourseUserPage from "../pages/course/user";
|
||||||
import MemberPage from "../pages/member";
|
import MemberPage from "../pages/member";
|
||||||
import MemberImportPage from "../pages/member/import";
|
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 SystemConfigPage from "../pages/system/config";
|
||||||
import SystemAdministratorPage from "../pages/system/administrator";
|
import SystemAdministratorPage from "../pages/system/administrator";
|
||||||
import SystemAdminrolesPage from "../pages/system/adminroles";
|
import SystemAdminrolesPage from "../pages/system/adminroles";
|
||||||
@ -98,6 +100,14 @@ const routes: RouteObject[] = [
|
|||||||
path: "/member/import",
|
path: "/member/import",
|
||||||
element: <MemberImportPage />,
|
element: <MemberImportPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/member/learn",
|
||||||
|
element: <MemberLearnPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/member/departmentUser",
|
||||||
|
element: <MemberDepartmentProgressPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/system/config/index",
|
path: "/system/config/index",
|
||||||
element: <SystemConfigPage />,
|
element: <SystemConfigPage />,
|
||||||
|
@ -13,10 +13,16 @@ export function clearToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function dateFormat(dateStr: string) {
|
export function dateFormat(dateStr: string) {
|
||||||
|
if (!dateStr) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
return moment(dateStr).format("YYYY-MM-DD HH:mm");
|
return moment(dateStr).format("YYYY-MM-DD HH:mm");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeFormat(dateStr: number) {
|
export function timeFormat(dateStr: number) {
|
||||||
|
if (!dateStr) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
var d = moment.duration(dateStr, "seconds");
|
var d = moment.duration(dateStr, "seconds");
|
||||||
let value =
|
let value =
|
||||||
Math.floor(d.asDays()) +
|
Math.floor(d.asDays()) +
|
||||||
|
Loading…
x
Reference in New Issue
Block a user