Merge pull request #3 from PlayEdu/dev

Dev
This commit is contained in:
Teng 2023-04-20 17:45:01 +08:00 committed by GitHub
commit 7383f08ab8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1008 additions and 68 deletions

3
.dockerignore Normal file
View File

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

View File

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

13
src/AutoTop.ts Normal file
View File

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

View File

@ -78,3 +78,46 @@ 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 departmentProgress(
id: number,
page: number,
size: number,
params: object
) {
return client.get(`/backend/v1/department/${id}/users`, {
page,
size,
...params,
});
}

View File

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

View File

@ -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,6 +140,7 @@ export const TreeDepartment = (props: PropInterface) => {
onClick={() => onSelect([], "")}
>
{props.text}
{props.showNum && userTotal ? "(" + userTotal + ")" : ""}
</div>
{treeData.length > 0 && (
<Tree

View File

@ -257,6 +257,17 @@ code {
position: relative;
}
.playedu-main-sp-top {
width: 100%;
height: auto;
float: left;
background-color: white;
box-sizing: border-box;
padding: 24px 0px;
border-radius: 12px;
position: relative;
}
.playedu-main-body {
width: 100%;
height: auto;

View File

@ -7,6 +7,7 @@ import { Provider } from "react-redux";
import store from "./store";
import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
import AutoScorllTop from "./AutoTop";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<Provider store={store}>
@ -15,7 +16,9 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
theme={{ token: { colorPrimary: "#ff4d4f" } }}
>
<BrowserRouter>
<App />
<AutoScorllTop>
<App />
</AutoScorllTop>
</BrowserRouter>
</ConfigProvider>
</Provider>

View File

@ -89,6 +89,8 @@ const CoursePage = () => {
children: (
<div className="float-left">
<TreeDepartment
refresh={refresh}
showNum={false}
type="no-course"
text={"部门"}
onUpdate={(keys: any, title: any) => {
@ -231,7 +233,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}
/>
@ -337,13 +341,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}
线 | {selLabel}
</div>
<div className="float-left j-b-flex mb-24">
<div className="d-flex">

View File

@ -11,7 +11,7 @@ import {
Image,
} from "antd";
import { course } from "../../api";
import { useParams } from "react-router-dom";
import { useParams, useLocation } from "react-router-dom";
import type { ColumnsType } from "antd/es/table";
import { BackBartment } from "../../compenents";
import { ExclamationCircleFilled } from "@ant-design/icons";
@ -31,6 +31,7 @@ interface DataType {
const CourseUserPage = () => {
const params = useParams();
const result = new URLSearchParams(useLocation().search);
const [list, setList] = useState<any>([]);
const [users, setUsers] = useState<any>([]);
const [refresh, setRefresh] = useState(false);
@ -42,10 +43,11 @@ const CourseUserPage = () => {
const [email, setEmail] = useState<string>("");
const [idCard, setIdCard] = useState<string>("");
const [selectedRowKeys, setSelectedRowKeys] = useState<any>([]);
const [title, setTitle] = useState<string>(String(result.get("title")));
const columns: ColumnsType<DataType> = [
{
title: "学员名称",
title: "学员",
render: (_, record: any) => (
<div className="d-flex">
<Image
@ -70,11 +72,6 @@ const CourseUserPage = () => {
</span>
),
},
{
title: "学习进度",
dataIndex: "progress",
render: (progress: number) => <span>{progress / 100}%</span>,
},
{
title: "第一次学习时间",
dataIndex: "created_at",
@ -85,6 +82,15 @@ const CourseUserPage = () => {
dataIndex: "finished_at",
render: (text: string) => <span>{dateFormat(text)}</span>,
},
{
title: "学习进度",
dataIndex: "progress",
render: (progress: number) => (
<span className={progress >= 10000 ? "c-green" : "c-red"}>
{progress / 100}%
</span>
),
},
];
useEffect(() => {
@ -178,7 +184,7 @@ 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">
@ -215,7 +221,7 @@ const CourseUserPage = () => {
placeholder="请输入学员邮箱"
/>
</div>
<div className="d-flex mr-24">
{/* <div className="d-flex mr-24">
<Typography.Text></Typography.Text>
<Input
value={idCard}
@ -225,7 +231,7 @@ const CourseUserPage = () => {
style={{ width: 160 }}
placeholder="请输入身份证号"
/>
</div>
</div> */}
<div className="d-flex">
<Button className="mr-16" onClick={resetList}>

View File

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

View File

@ -71,7 +71,7 @@ const LoginPage = () => {
await getSystemConfig(); //获取系统配置并写入store
await getUser(); //获取登录用户的信息并写入store
navigate("/");
navigate("/", { replace: true });
} catch (e) {
message.error("登录出现错误");
console.error("错误信息", e);

View 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;

View File

@ -8,13 +8,19 @@ import {
Table,
message,
Image,
Dropdown,
} from "antd";
import type { MenuProps } from "antd";
import type { ColumnsType } from "antd/es/table";
// import styles from "./index.module.less";
import { PlusOutlined, ExclamationCircleFilled } from "@ant-design/icons";
import {
PlusOutlined,
DownOutlined,
ExclamationCircleFilled,
} from "@ant-design/icons";
import { user } from "../../api/index";
import { dateFormat } from "../../utils/index";
import { Link } from "react-router-dom";
import { Link, Navigate } from "react-router-dom";
import { TreeDepartment, PerButton } from "../../compenents";
import { MemberCreate } from "./compenents/create";
import { MemberUpdate } from "./compenents/update";
@ -50,7 +56,7 @@ const MemberPage = () => {
const columns: ColumnsType<DataType> = [
{
title: "学员姓名",
title: "学员",
dataIndex: "name",
render: (_, record: any) => (
<>
@ -97,32 +103,73 @@ const MemberPage = () => {
key: "action",
fixed: "right",
width: 160,
render: (_, record: any) => (
<Space size="small">
<PerButton
type="link"
text="编辑"
class="b-link c-red"
icon={null}
p="user-update"
onClick={() => {
setMid(Number(record.id));
setUpdateVisible(true);
}}
disabled={null}
/>
<div className="form-column"></div>
<PerButton
type="link"
text="删除"
class="b-link c-red"
icon={null}
p="user-destroy"
onClick={() => delUser(record.id)}
disabled={null}
/>
</Space>
),
render: (_, record: any) => {
const items: MenuProps["items"] = [
{
key: "1",
label: (
<PerButton
type="link"
text="编辑"
class="b-link c-red"
icon={null}
p="user-update"
onClick={() => {
setMid(Number(record.id));
setUpdateVisible(true);
}}
disabled={null}
/>
),
},
{
key: "2",
label: (
<PerButton
type="link"
text="删除"
class="b-link c-red"
icon={null}
p="user-destroy"
onClick={() => delUser(record.id)}
disabled={null}
/>
),
},
];
return (
<Space size="small">
<Link
style={{ textDecoration: "none" }}
to={`/member/learn?id=${record.id}`}
>
<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">

View File

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

348
src/pages/member/learn.tsx Normal file
View 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;

View File

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

View File

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

View File

@ -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);
@ -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,7 +198,7 @@ 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} />
@ -411,6 +417,79 @@ const SystemConfigPage = () => {
</Form>
),
},
{
key: "3",
label: `学员设置`,
children: (
<Form
form={form}
name="m-basic"
labelCol={{ span: 3 }}
wrapperCol={{ span: 21 }}
style={{ width: 1000, paddingTop: 30 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
{avatar && (
<Form.Item
style={{ marginBottom: 30 }}
label="学员默认头像"
name="member.default_avatar"
labelCol={{ style: { marginTop: 14, marginLeft: 28 } }}
>
<div className="d-flex">
<Image
preview={false}
width={60}
height={60}
src={avatar}
style={{ borderRadius: "50%" }}
/>
<div className="d-flex ml-24">
<UploadImageButton
text="更换头像"
onSelected={(url) => {
setAvatar(url);
form.setFieldsValue({ "member.default_avatar": url });
}}
></UploadImageButton>
<div className="helper-text ml-24"></div>
</div>
</div>
</Form.Item>
)}
{!avatar && (
<Form.Item
style={{ marginBottom: 30 }}
label="学员默认头像"
name="member.default_avatar"
>
<div className="d-flex">
<div className="d-flex">
<UploadImageButton
text="更换头像"
onSelected={(url) => {
setAvatar(url);
form.setFieldsValue({ "member.default_avatar": url });
}}
></UploadImageButton>
<div className="helper-text ml-24"></div>
</div>
</div>
</Form.Item>
)}
<Form.Item
style={{ marginBottom: 30 }}
wrapperCol={{ offset: 3, span: 21 }}
>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Form.Item>
</Form>
),
},
];
const onChange = (key: string) => {

View File

@ -16,6 +16,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";
@ -32,7 +34,7 @@ if (getToken()) {
try {
let configRes: any = await system.getSystemConfig();
let userRes: any = await login.getUser();
resolve({
default: (
<InitPage configData={configRes.data} loginData={userRes.data} />
@ -98,6 +100,14 @@ const routes: RouteObject[] = [
path: "/member/import",
element: <MemberImportPage />,
},
{
path: "/member/learn",
element: <MemberLearnPage />,
},
{
path: "/member/departmentUser",
element: <MemberDepartmentProgressPage />,
},
{
path: "/system/config/index",
element: <SystemConfigPage />,

View File

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