Merge pull request #4 from PlayEdu/dev

v1.0-beta.4
This commit is contained in:
Teng 2023-05-04 15:24:06 +08:00 committed by GitHub
commit 5d834ab2d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 752 additions and 234 deletions

View File

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

View File

@ -109,6 +109,10 @@ export function learnCourses(
});
}
export function learnAllCourses(id: number) {
return client.get(`/backend/v1/user/${id}/all-courses`, {});
}
export function departmentProgress(
id: number,
page: number,
@ -121,3 +125,25 @@ export function departmentProgress(
...params,
});
}
export function learnCoursesProgress(
id: number,
courseId: number,
params: any
) {
return client.get(`/backend/v1/user/${id}/learn-course/${courseId} `, params);
}
export function destroyAllUserLearned(id: number, courseId: number) {
return client.destroy(`/backend/v1/user/${id}/learn-course/${courseId}`);
}
export function destroyUserLearned(
id: number,
courseId: number,
hourId: number
) {
return client.destroy(
`/backend/v1/user/${id}/learn-course/${courseId}/hour/${hourId}`
);
}

View File

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

View File

@ -8,7 +8,7 @@ export const Footer: React.FC = () => {
style={{
width: "100%",
backgroundColor: "#F6F6F6",
height: 232,
height: 166,
paddingTop: 80,
textAlign: "center",
}}

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import { resourceCategory } from "../../api/index";
interface Option {
key: string | number;
title: any;
children?: Option[];
}

View File

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

View File

@ -12,7 +12,7 @@ import {
import { resource, resourceCategory } from "../../api";
import styles from "./index.module.less";
import { CreateResourceCategory } from "../create-rs-category";
import { CloseOutlined } from "@ant-design/icons";
import { CloseOutlined, CheckOutlined } from "@ant-design/icons";
import { UploadImageSub } from "./upload-image-sub";
import { TreeCategory } from "../../compenents";
@ -49,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 = () => {
@ -97,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}>
@ -129,10 +141,21 @@ export const UploadImageButton = (props: PropsInterface) => {
className="image-item"
style={{ backgroundImage: `url(${item.url})` }}
onClick={() => {
props.onSelected(item.url);
setShowModal(false);
setSelected(item.url);
}}
></div>
>
{selected.indexOf(item.url) !== -1 && (
<i
className={styles.checked}
onClick={(e) => {
e.stopPropagation();
setSelected("");
}}
>
<CheckOutlined />
</i>
)}
</div>
))}
</div>
{imageList.length > 0 && (

View File

@ -614,6 +614,8 @@ textarea.ant-input {
background-size: contain;
background-position: center center;
background-color: #f6f6f6;
position: relative;
cursor: pointer;
}
}

View File

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

View File

@ -470,6 +470,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
<Input
style={{ width: 424 }}
placeholder="请在此处输入课程名称"
allowClear
/>
</Form.Item>
<Form.Item
@ -683,6 +684,7 @@ export const CourseCreate: React.FC<PropInterface> = ({
onChange={(e) => {
setChapterName(index, e.target.value);
}}
allowClear
placeholder="请在此处输入章节名称"
/>
<Button

View File

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

View File

@ -224,6 +224,7 @@ export const CourseUpdate: React.FC<PropInterface> = ({
rules={[{ required: true, message: "请在此处输入课程名称!" }]}
>
<Input
allowClear
style={{ width: 424 }}
placeholder="请在此处输入课程名称"
/>

View File

@ -51,6 +51,7 @@ const CoursePage = () => {
const [title, setTitle] = useState<string>("");
const [dep_ids, setDepIds] = useState<any>([]);
const [selLabel, setLabel] = useState<string>("全部分类");
const [selDepLabel, setDepLabel] = useState<string>("全部部门");
const [course_category_ids, setCourseCategoryIds] = useState<any>({});
const [course_dep_ids, setCourseDepIds] = useState<any>({});
const [categories, setCategories] = useState<any>({});
@ -95,7 +96,7 @@ const CoursePage = () => {
text={"部门"}
onUpdate={(keys: any, title: any) => {
setDepIds(keys);
setLabel(title);
setDepLabel(title);
}}
/>
</div>
@ -282,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) => {
@ -314,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, //当前页码
@ -348,7 +354,7 @@ const CoursePage = () => {
</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">
@ -370,6 +376,7 @@ const CoursePage = () => {
onChange={(e) => {
setTitle(e.target.value);
}}
allowClear
style={{ width: 160 }}
placeholder="请输入名称关键字"
/>
@ -399,8 +406,8 @@ const CoursePage = () => {
rowKey={(record) => record.id}
/>
<CourseCreate
cateIds={category_ids}
depIds={dep_ids}
cateIds={tabKey === 1 ? category_ids : []}
depIds={tabKey === 2 ? dep_ids : []}
open={createVisible}
onCancel={() => {
setCreateVisible(false);

View File

@ -149,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: "取消",
@ -190,7 +190,7 @@ const CourseUserPage = () => {
<div className="d-flex">
<PerButton
type="primary"
text="清除学习记录"
text="重置学习记录"
class="mr-16"
icon={null}
p="course"
@ -206,6 +206,7 @@ const CourseUserPage = () => {
onChange={(e) => {
setName(e.target.value);
}}
allowClear
style={{ width: 160 }}
placeholder="请输入姓名关键字"
/>
@ -217,6 +218,7 @@ const CourseUserPage = () => {
onChange={(e) => {
setEmail(e.target.value);
}}
allowClear
style={{ width: 160 }}
placeholder="请输入学员邮箱"
/>

View File

@ -199,7 +199,7 @@ const DashboardPage = () => {
<div
className={styles["link-mode"]}
onClick={() => {
navigate("/member");
navigate("/member/index");
}}
>
<i
@ -516,7 +516,7 @@ const DashboardPage = () => {
<div className={styles["usage-guide"]}>
<img className={styles["banner"]} src={banner} alt="" />
<Link
to="https://www.playedu.xyz/docs/docs/intro/"
to="https://www.playedu.xyz/docs/docs/guide/"
target="blank"
className={styles["link"]}
>

View File

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

View File

@ -194,7 +194,7 @@ const DepartmentPage = () => {
type="link"
style={{ paddingLeft: 4, paddingRight: 4 }}
danger
onClick={() => navigate("/member")}
onClick={() => navigate("/member/index")}
>
{res.data.users.length}
</Button>

View File

@ -73,7 +73,6 @@ const LoginPage = () => {
navigate("/", { replace: true });
} catch (e) {
message.error("登录出现错误");
console.error("错误信息", e);
setLoading(false);
fetchImageCaptcha(); //刷新图形验证码
@ -129,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">
@ -137,6 +137,7 @@ const LoginPage = () => {
onChange={(e) => {
setPassword(e.target.value);
}}
allowClear
style={{ width: 400, height: 54 }}
placeholder="请输入密码"
/>
@ -149,6 +150,7 @@ const LoginPage = () => {
onChange={(e) => {
setCaptchaVal(e.target.value);
}}
allowClear
onKeyUp={(e) => keyUp(e)}
/>
<div className={styles["captcha-box"]}>

View File

@ -8,6 +8,7 @@ import { ValidataCredentials } from "../../../utils/index";
interface PropInterface {
open: boolean;
depIds: any;
onCancel: () => void;
}
@ -17,7 +18,11 @@ 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>([]);
@ -39,10 +44,10 @@ export const MemberCreate: React.FC<PropInterface> = ({ open, onCancel }) => {
password: "",
avatar: memberDefaultAvatar,
idCard: "",
dep_ids: [],
dep_ids: depIds,
});
setAvatar(memberDefaultAvatar);
}, [form, open]);
}, [form, open, depIds]);
const getParams = () => {
department.departmentList().then((res: any) => {
@ -154,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="登录密码"
@ -162,6 +171,7 @@ export const MemberCreate: React.FC<PropInterface> = ({ open, onCancel }) => {
rules={[{ required: true, message: "请输入登录密码!" }]}
>
<Input.Password
allowClear
style={{ width: 274 }}
placeholder="请输入登录密码"
/>
@ -182,7 +192,11 @@ export const MemberCreate: React.FC<PropInterface> = ({ open, onCancel }) => {
/>
</Form.Item>
<Form.Item label="身份证号" name="idCard">
<Input style={{ width: 274 }} placeholder="请填写学员身份证号" />
<Input
style={{ width: 274 }}
allowClear
placeholder="请填写学员身份证号"
/>
</Form.Item>
</Form>
</div>

View File

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

View File

@ -181,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>
@ -212,7 +221,11 @@ export const MemberUpdate: React.FC<PropInterface> = ({
/>
</Form.Item>
<Form.Item label="身份证号" name="idCard">
<Input style={{ width: 274 }} placeholder="请填写学员身份证号" />
<Input
allowClear
style={{ width: 274 }}
placeholder="请填写学员身份证号"
/>
</Form.Item>
</Form>
</div>

View File

@ -9,12 +9,14 @@ import {
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;
@ -40,8 +42,20 @@ const MemberDepartmentProgressPage = () => {
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();
@ -59,6 +73,7 @@ const MemberDepartmentProgressPage = () => {
name: name,
email: email,
id_card: id_card,
show_mode: showMode,
})
.then((res: any) => {
setList(res.data.data);
@ -81,6 +96,7 @@ const MemberDepartmentProgressPage = () => {
setName("");
setEmail("");
setIdCard("");
setShowMode("all");
setPage(1);
setSize(10);
setList([]);
@ -125,14 +141,80 @@ const MemberDepartmentProgressPage = () => {
}
};
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 helper-text ">
/
<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 ">
@ -142,30 +224,32 @@ const MemberDepartmentProgressPage = () => {
onChange={(e) => {
setName(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={email}
onChange={(e) => {
setEmail(e.target.value);
}}
allowClear
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);
}}
<div className="d-flex mr-24">
<Typography.Text></Typography.Text>
<Select
style={{ width: 160 }}
placeholder="请输入身份证号"
allowClear
placeholder="请选择"
value={showMode}
onChange={(value: string) => setShowMode(value)}
options={modes}
/>
</div> */}
<div className="d-flex">
@ -198,7 +282,7 @@ const MemberDepartmentProgressPage = () => {
title="学员"
dataIndex="name"
key="name"
width={100}
width={150}
render={(_, record: any) => (
<>
<Image
@ -218,12 +302,12 @@ const MemberDepartmentProgressPage = () => {
ellipsis={true}
dataIndex="id"
key={item.id}
width={100}
width={168}
render={(_, record: any) => (
<>
{records[record.id] && records[record.id][item.id] ? (
records[record.id][item.id].is_finished === 1 ? (
<span></span>
<span></span>
) : (
<>
<span>
@ -243,10 +327,10 @@ const MemberDepartmentProgressPage = () => {
))}
<Column
fixed="right"
title="所有课程总课时"
title="课时"
dataIndex="id"
key="id"
width={100}
width={150}
render={(_, record: any) => (
<>
<span>{getFinishedHours(records[record.id])}</span> /{" "}

View File

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

View File

@ -142,7 +142,7 @@ const MemberPage = () => {
<Space size="small">
<Link
style={{ textDecoration: "none" }}
to={`/member/learn?id=${record.id}`}
to={`/member/learn?id=${record.id}&name=${record.name}`}
>
<PerButton
type="link"
@ -320,6 +320,7 @@ const MemberPage = () => {
}}
style={{ width: 160 }}
placeholder="请输入姓名关键字"
allowClear
/>
</div>
<div className="d-flex mr-24">
@ -331,6 +332,7 @@ const MemberPage = () => {
}}
style={{ width: 160 }}
placeholder="请输入邮箱账号"
allowClear
/>
</div>
<div className="d-flex">
@ -359,6 +361,7 @@ const MemberPage = () => {
/>
<MemberCreate
open={createVisible}
depIds={dep_ids}
onCancel={() => {
setCreateVisible(false);
setRefresh(!refresh);

View File

@ -1,13 +1,13 @@
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 { 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 { duration } from "moment";
import { MemberLearnProgressDialog } from "./compenents/progress";
interface DataType {
id: React.Key;
@ -21,22 +21,29 @@ interface DataType {
const MemberLearnPage = () => {
let chartRef = useRef(null);
const navigate = useNavigate();
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 [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();
@ -46,12 +53,22 @@ const MemberLearnPage = () => {
}, [uid]);
useEffect(() => {
getLearnHours();
}, [refresh, page, size]);
getLearnCourses();
}, [refresh2, uid]);
useEffect(() => {
getLearnCourses();
}, [refresh2, page2, size2]);
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) => {
@ -127,125 +144,34 @@ const MemberLearnPage = () => {
};
};
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);
});
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 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: "课程名称",
@ -253,13 +179,13 @@ const MemberLearnPage = () => {
render: (_, record: any) => (
<div className="d-flex">
<Image
src={courses[record.course_id].thumb}
src={record.thumb}
preview={false}
width={80}
height={60}
style={{ borderRadius: 6 }}
/>
<span className="ml-8">{courses[record.course_id].title}</span>
<span className="ml-8">{record.title}</span>
</div>
),
},
@ -269,7 +195,9 @@ const MemberLearnPage = () => {
render: (_, record: any) => (
<>
<span>
{record.finished_count} / {record.hour_count}
{(records[record.id] && records[record.id].finished_count) ||
0} / {record.class_hour}
</span>
</>
),
@ -277,38 +205,93 @@ const MemberLearnPage = () => {
{
title: "第一次学习时间",
dataIndex: "created_at",
render: (text: string) => <span>{dateFormat(text)}</span>,
render: (_, record: any) => (
<>
{records[record.id] ? (
<span>{dateFormat(records[record.id].created_at)}</span>
) : (
<span>-</span>
)}
</>
),
},
{
title: "学习完成时间",
dataIndex: "finished_at",
render: (text: string) => <span>{dateFormat(text)}</span>,
render: (_, record: any) => (
<>
{records[record.id] ? (
<span>{dateFormat(records[record.id].finished_at)}</span>
) : (
<span>-</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>
{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="学员学习" />
<BackBartment title={userName + "的学习明细"} />
</div>
<div className={styles["charts"]}>
<div
@ -321,27 +304,28 @@ const MemberLearnPage = () => {
></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={list2}
dataSource={currentCourses}
loading={loading2}
pagination={paginationProps2}
pagination={false}
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> */}
</>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ const SystemConfigPage = () => {
useEffect(() => {
getDetail();
}, []);
}, [tabKey]);
const getDetail = () => {
appConfig.appConfig().then((res: any) => {
@ -244,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 }}
@ -315,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}

View File

@ -4,6 +4,7 @@ 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";
@ -94,19 +95,22 @@ const routes: RouteObject[] = [
},
{
path: "/member",
element: <MemberPage />,
},
{
path: "/member/import",
element: <MemberImportPage />,
},
{
path: "/member/learn",
element: <MemberLearnPage />,
},
{
path: "/member/departmentUser",
element: <MemberDepartmentProgressPage />,
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",