diff --git a/package.json b/package.json index c89999d..83edae1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/template/学员批量导入模板.xlsx b/public/template/学员批量导入模板.xlsx index 99483ad..c64a6a5 100644 Binary files a/public/template/学员批量导入模板.xlsx and b/public/template/学员批量导入模板.xlsx differ diff --git a/src/api/user.ts b/src/api/user.ts index c1165e1..66d4fcd 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -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}` + ); +} diff --git a/src/compenents/create-rs-category/index.tsx b/src/compenents/create-rs-category/index.tsx index 4899a15..e1c8449 100644 --- a/src/compenents/create-rs-category/index.tsx +++ b/src/compenents/create-rs-category/index.tsx @@ -54,6 +54,7 @@ export const CreateResourceCategory = (props: PropInterface) => { onChange={(e) => { setName(e.target.value); }} + allowClear /> diff --git a/src/compenents/footer/index.tsx b/src/compenents/footer/index.tsx index 4dbc669..aa52911 100644 --- a/src/compenents/footer/index.tsx +++ b/src/compenents/footer/index.tsx @@ -8,7 +8,7 @@ export const Footer: React.FC = () => { style={{ width: "100%", backgroundColor: "#F6F6F6", - height: 232, + height: 166, paddingTop: 80, textAlign: "center", }} diff --git a/src/compenents/keep-alive/index.tsx b/src/compenents/keep-alive/index.tsx new file mode 100644 index 0000000..94a10f4 --- /dev/null +++ b/src/compenents/keep-alive/index.tsx @@ -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 ( +
+ {Array.from(componentList.current).map(([key, component]) => ( +
+ {component} +
+ ))} +
+ ); +} + +export default KeepAlive; diff --git a/src/compenents/left-menu/index.tsx b/src/compenents/left-menu/index.tsx index c3ce869..07d4b24 100644 --- a/src/compenents/left-menu/index.tsx +++ b/src/compenents/left-menu/index.tsx @@ -50,7 +50,7 @@ const items = [ "user", , [ - getItem("学员", "/member", null, null, null), + getItem("学员", "/member/index", null, null, null), getItem("部门", "/department", null, null, null), ], null diff --git a/src/compenents/tree-category/index.tsx b/src/compenents/tree-category/index.tsx index 7e4cb67..aedc7ea 100644 --- a/src/compenents/tree-category/index.tsx +++ b/src/compenents/tree-category/index.tsx @@ -5,6 +5,7 @@ import { resourceCategory } from "../../api/index"; interface Option { key: string | number; title: any; + children?: Option[]; } diff --git a/src/compenents/upload-image-button/index.module.less b/src/compenents/upload-image-button/index.module.less index 8874736..4084585 100644 --- a/src/compenents/upload-image-button/index.module.less +++ b/src/compenents/upload-image-button/index.module.less @@ -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; +} diff --git a/src/compenents/upload-image-button/index.tsx b/src/compenents/upload-image-button/index.tsx index 8c6cdb8..89fc6bb 100644 --- a/src/compenents/upload-image-button/index.tsx +++ b/src/compenents/upload-image-button/index.tsx @@ -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(""); // 获取图片列表 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); + }} > setCategoryIds(keys)} + onUpdate={(keys: any) => { + setSelected(""); + setCategoryIds(keys); + }} /> @@ -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); }} - > + > + {selected.indexOf(item.url) !== -1 && ( + { + e.stopPropagation(); + setSelected(""); + }} + > + + + )} + ))} {imageList.length > 0 && ( diff --git a/src/index.less b/src/index.less index 019888e..5b5c90e 100644 --- a/src/index.less +++ b/src/index.less @@ -614,6 +614,8 @@ textarea.ant-input { background-size: contain; background-position: center center; background-color: #f6f6f6; + position: relative; + cursor: pointer; } } diff --git a/src/pages/change-password/index.tsx b/src/pages/change-password/index.tsx index 11aced1..b23e718 100644 --- a/src/pages/change-password/index.tsx +++ b/src/pages/change-password/index.tsx @@ -45,14 +45,14 @@ const ChangePasswordPage = () => { name="old_password" rules={[{ required: true, message: "请输入原密码!" }]} > - + - + diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index a614298..f3fe4d7 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -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 />
@@ -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)} />
diff --git a/src/pages/member/compenents/create.tsx b/src/pages/member/compenents/create.tsx index 2dc214d..6eff2bf 100644 --- a/src/pages/member/compenents/create.tsx +++ b/src/pages/member/compenents/create.tsx @@ -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 = ({ open, onCancel }) => { +export const MemberCreate: React.FC = ({ + open, + depIds, + onCancel, +}) => { const [form] = Form.useForm(); const [loading, setLoading] = useState(true); const [departments, setDepartments] = useState([]); @@ -39,10 +44,10 @@ export const MemberCreate: React.FC = ({ 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 = ({ open, onCancel }) => { name="email" rules={[{ required: true, message: "请输入登录邮箱!" }]} > - + = ({ open, onCancel }) => { rules={[{ required: true, message: "请输入登录密码!" }]} > @@ -182,7 +192,11 @@ export const MemberCreate: React.FC = ({ open, onCancel }) => { /> - +
diff --git a/src/pages/member/compenents/progress.module.scss b/src/pages/member/compenents/progress.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/member/compenents/progress.tsx b/src/pages/member/compenents/progress.tsx new file mode 100644 index 0000000..82fb20a --- /dev/null +++ b/src/pages/member/compenents/progress.tsx @@ -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 = ({ + open, + uid, + id, + onCancel, +}) => { + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [records, setRecords] = useState({}); + 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 = [ + { + title: "课时标题", + dataIndex: "title", + + render: (title: string) => ( + <> + {title} + + ), + }, + { + title: "总时长", + width: 120, + dataIndex: "duration", + render: (duration: number) => ( + <> + + + ), + }, + { + title: "已学习时长", + width: 120, + dataIndex: "finished_duration", + render: (_, record: any) => ( + <> + {records && records[record.id] ? ( + + + + ) : ( + - + )} + + ), + }, + { + title: "是否学完", + width: 100, + dataIndex: "is_finished", + render: (_, record: any) => ( + <> + {records && + records[record.id] && + records[record.id].is_finished === 1 ? ( + 已学完 + ) : ( + 未学完 + )} + + ), + }, + { + title: "开始时间", + width: 150, + dataIndex: "created_at", + render: (_, record: any) => ( + <> + {records && records[record.id] ? ( + {dateFormat(records[record.id].created_at)} + ) : ( + - + )} + + ), + }, + { + title: "学完时间", + width: 150, + dataIndex: "finished_at", + render: (_, record: any) => ( + <> + {records && records[record.id] ? ( + {dateFormat(records[record.id].finished_at)} + ) : ( + - + )} + + ), + }, + { + title: "操作", + key: "action", + fixed: "right", + width: 70, + render: (_, record: any) => ( + <> + {records && records[record.id] ? ( + { + clearSingleProgress(records[record.id].hour_id); + }} + disabled={null} + /> + ) : ( + - + )} + + ), + }, + ]; + + const clearProgress = () => { + confirm({ + title: "操作确认", + icon: , + 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: , + content: "确认重置此课时的学习记录?", + centered: true, + okText: "确认", + cancelText: "取消", + onOk() { + member.destroyUserLearned(uid, id, hour_id).then((res: any) => { + message.success("操作成功"); + setRefresh(!refresh); + }); + }, + onCancel() { + console.log("Cancel"); + }, + }); + }; + + return ( + <> + onCancel()} + onCancel={() => onCancel()} + maskClosable={false} + footer={null} + > +
+ { + clearProgress(); + }} + disabled={null} + /> +
+
+ record.id} + pagination={false} + /> + + + + ); +}; diff --git a/src/pages/member/compenents/update.tsx b/src/pages/member/compenents/update.tsx index 477f77f..4ee2bf2 100644 --- a/src/pages/member/compenents/update.tsx +++ b/src/pages/member/compenents/update.tsx @@ -181,18 +181,27 @@ export const MemberUpdate: React.FC = ({ name="name" rules={[{ required: true, message: "请输入学员姓名!" }]} > - + - + @@ -212,7 +221,11 @@ export const MemberUpdate: React.FC = ({ /> - + diff --git a/src/pages/member/departmentUser.tsx b/src/pages/member/departmentUser.tsx index de96f52..577b574 100644 --- a/src/pages/member/departmentUser.tsx +++ b/src/pages/member/departmentUser.tsx @@ -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(""); const [email, setEmail] = useState(""); const [id_card, setIdCard] = useState(""); + const [showMode, setShowMode] = useState("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 (
-
- (以下表格内数字对应的是表头课程的“已学完课时数/总课时数”) +
+ +
+ (以下表格内数字对应的是表头课程的“已学完课时数/总课时数”) +
@@ -142,30 +224,32 @@ const MemberDepartmentProgressPage = () => { onChange={(e) => { setName(e.target.value); }} + allowClear style={{ width: 160 }} placeholder="请输入姓名关键字" />
-
+ {/*
邮箱: { setEmail(e.target.value); }} + allowClear style={{ width: 160 }} placeholder="请输入邮箱" />
- {/*
- 身份证号: - { - setIdCard(e.target.value); - }} +
+ 模式: + setDepValue(Number(value))} + options={deps} + /> +
+ )}
record.id} /> - {/*
-
课时学习记录
-
-
record.id} - /> - - */} ); }; diff --git a/src/pages/resource/resource-category/compenents/create.tsx b/src/pages/resource/resource-category/compenents/create.tsx index d58d80f..09d4979 100644 --- a/src/pages/resource/resource-category/compenents/create.tsx +++ b/src/pages/resource/resource-category/compenents/create.tsx @@ -147,7 +147,11 @@ export const ResourceCategoryCreate: React.FC = ({ name="name" rules={[{ required: true, message: "请输入分类名称!" }]} > - + diff --git a/src/pages/resource/resource-category/compenents/update.tsx b/src/pages/resource/resource-category/compenents/update.tsx index 28bac87..aa5d7e7 100644 --- a/src/pages/resource/resource-category/compenents/update.tsx +++ b/src/pages/resource/resource-category/compenents/update.tsx @@ -169,7 +169,11 @@ export const ResourceCategoryUpdate: React.FC = ({ name="name" rules={[{ required: true, message: "请输入分类名称!" }]} > - + diff --git a/src/pages/system/administrator/compenents/create.tsx b/src/pages/system/administrator/compenents/create.tsx index e447677..641cfa6 100644 --- a/src/pages/system/administrator/compenents/create.tsx +++ b/src/pages/system/administrator/compenents/create.tsx @@ -125,14 +125,22 @@ export const SystemAdministratorCreate: React.FC = ({ name="name" rules={[{ required: true, message: "请输入管理员姓名!" }]} > - + - + = ({ rules={[{ required: true, message: "请输入登录密码!" }]} > diff --git a/src/pages/system/administrator/compenents/update.tsx b/src/pages/system/administrator/compenents/update.tsx index c87be6c..f069316 100644 --- a/src/pages/system/administrator/compenents/update.tsx +++ b/src/pages/system/administrator/compenents/update.tsx @@ -133,18 +133,27 @@ export const SystemAdministratorUpdate: React.FC = ({ name="name" rules={[{ required: true, message: "请输入管理员姓名!" }]} > - + - + diff --git a/src/pages/system/administrator/index.tsx b/src/pages/system/administrator/index.tsx index ab0c9c9..6161916 100644 --- a/src/pages/system/administrator/index.tsx +++ b/src/pages/system/administrator/index.tsx @@ -283,6 +283,7 @@ const SystemAdministratorPage = () => { onChange={(e) => { setName(e.target.value); }} + allowClear style={{ width: 160 }} placeholder="请输入管理员姓名" /> diff --git a/src/pages/system/adminroles/compenents/create.tsx b/src/pages/system/adminroles/compenents/create.tsx index 8525b2a..8bb1d60 100644 --- a/src/pages/system/adminroles/compenents/create.tsx +++ b/src/pages/system/adminroles/compenents/create.tsx @@ -180,6 +180,7 @@ export const SystemAdminrolesCreate: React.FC = ({ diff --git a/src/pages/system/adminroles/compenents/update.tsx b/src/pages/system/adminroles/compenents/update.tsx index 555cae1..7b2d750 100644 --- a/src/pages/system/adminroles/compenents/update.tsx +++ b/src/pages/system/adminroles/compenents/update.tsx @@ -191,7 +191,11 @@ export const SystemAdminrolesUpdate: React.FC = ({ name="name" rules={[{ required: true, message: "请输入角色名!" }]} > - + { useEffect(() => { getDetail(); - }, []); + }, [tabKey]); const getDetail = () => { appConfig.appConfig().then((res: any) => { @@ -244,14 +244,22 @@ const SystemConfigPage = () => { label="网站标题" name="system.name" > - + - + {/* { - + , - }, - { - path: "/member/import", - element: , - }, - { - path: "/member/learn", - element: , - }, - { - path: "/member/departmentUser", - element: , + element: , + children: [ + { path: "/member/index", element: }, + { + path: "/member/import", + element: , + }, + { + path: "/member/learn", + element: , + }, + { + path: "/member/departmentUser", + element: , + }, + ], }, { path: "/system/config/index",