From 5e7c49095b22892d384957c7d56f00d166874510 Mon Sep 17 00:00:00 2001 From: tangjie <122778500@qq.com> Date: Wed, 19 Aug 2026 13:47:08 +0800 Subject: [PATCH] feat --- jjb.config.js | 4 +- src/api/courseware/index.js | 19 +- src/components/AttachmentUpload/index.js | 4 +- .../EduTraining/CourseManage/index.js | 299 ++++++++++++++++++ .../EduTraining/CoursewareManage/index.js | 86 ++++- src/pages/Container/Layout/menuConfig.js | 5 + 6 files changed, 402 insertions(+), 15 deletions(-) create mode 100644 src/pages/Container/EduTraining/CourseManage/index.js diff --git a/jjb.config.js b/jjb.config.js index 17ef125..5157f2e 100644 --- a/jjb.config.js +++ b/jjb.config.js @@ -12,9 +12,9 @@ module.exports = { // 可通过环境变量覆盖: SAFETY_EVAL_API_HOST=http://192.168.x.x:8095 //API_HOST: "http://localhost:80", - // API_HOST: "http://192.168.0.134", + API_HOST: "http://192.168.0.134", // API_HOST: "http://192.168.0.150", //太浅 - API_HOST: "https://gbs-gateway.qhdsafety.com", + // API_HOST: "https://gbs-gateway.qhdsafety.com", // API_HOST: "http://192.168.0.103", //huwei }, production: { diff --git a/src/api/courseware/index.js b/src/api/courseware/index.js index 29a565a..11ecbee 100644 --- a/src/api/courseware/index.js +++ b/src/api/courseware/index.js @@ -1,6 +1,6 @@ import { declareRequest } from "@cqsjjb/jjb-dva-runtime"; -/** 教育培训 — 培训课件管理 */ +/** 教育培训 — 培训课件管理 / 培训课程管理 */ /** 分页查询课件 */ export const coursewarePage = declareRequest( @@ -9,16 +9,22 @@ export const coursewarePage = declareRequest( "coursewareList: [] | res.data || [] & coursewareTotal: 0 | res.total || 0", ); +/** 课件详情 */ +export const coursewareDetail = declareRequest( + "coursewareDetailLoading", + "Get > /safetyEval/coursewareManagement/get", +); + /** 新增课件 */ export const coursewareAdd = declareRequest( "coursewareConfirmLoading", "Post > @/safetyEval/coursewareManagement/save", ); -/** 修改课件(启用/禁用等) */ +/** 修改课件状态(启用/禁用) */ export const coursewareModify = declareRequest( "coursewareLoading", - "Post > @/safetyEval/coursewareManagement/modify", + "Post > @/safetyEval/coursewareManagement/modifyStatus", ); /** 删除课件 */ @@ -26,3 +32,10 @@ export const coursewareRemove = declareRequest( "coursewareLoading", "Post > @/safetyEval/coursewareManagement/delete", ); + +/** 分页查询课程 */ +export const coursePage = declareRequest( + "courseLoading", + "Get > /safetyEval/courseManagement/page", + "courseList: [] | res.data || [] & courseTotal: 0 | res.total || 0", +); diff --git a/src/components/AttachmentUpload/index.js b/src/components/AttachmentUpload/index.js index 61cadd2..1af5035 100644 --- a/src/components/AttachmentUpload/index.js +++ b/src/components/AttachmentUpload/index.js @@ -4,7 +4,7 @@ import { PlusOutlined } from "@ant-design/icons"; const isImage = (url) => /\.(png|jpe?g|gif|bmp|webp|svg)(\?.*)?$/i.test(url || ""); -export default function AttachmentUpload({ name, label, disabled, maxCount, accept, extra ,rules, style }) { +export default function AttachmentUpload({ name, label, disabled, maxCount, accept, extra ,rules, style, keepOriginFile }) { const [previewImage, setPreviewImage] = useState(""); return ( @@ -46,6 +46,8 @@ export default function AttachmentUpload({ name, label, disabled, maxCount, acce status: file.status, name: file.name, percent: file.percent, + // keepOriginFile 时保留原始文件对象,供业务侧读取本地文件信息(如视频时长解析) + ...(keepOriginFile ? { originFileObj: file.originFileObj } : {}), })) || [] ); }} diff --git a/src/pages/Container/EduTraining/CourseManage/index.js b/src/pages/Container/EduTraining/CourseManage/index.js new file mode 100644 index 0000000..acd0200 --- /dev/null +++ b/src/pages/Container/EduTraining/CourseManage/index.js @@ -0,0 +1,299 @@ +import { useEffect, useState } from "react"; +import { + Badge, + Button, + Descriptions, + Form, + Modal, + Table, + message, +} from "antd"; +import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; +import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction"; +import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper"; +import SearchForm from "~/components/SearchForm"; +import PreviewUrlButton from "~/components/PreviewUrlButton"; +import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd"; +import { Connect } from "@cqsjjb/jjb-dva-runtime"; +import { tools } from "@cqsjjb/jjb-common-lib"; +import { NS_COURSEWARE } from "~/enumerate/namespace"; +import { + COURSEWARE_STATUS_MAP, + COURSEWARE_STATUS_OPTIONS, + COURSEWARE_TRAINING_TYPE_MAP, + COURSEWARE_TRAINING_TYPE_OPTIONS, +} from "~/enumerate/constant"; + +const { router } = tools; + +/** 解析课件文档字段:JSON 数组字符串,容错非法格式 */ +const parseCoursewareDocument = (raw) => { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +}; + +/** 培训课程管理 */ +function CourseManagePage(props) { + const [searchForm] = Form.useForm(); + const [viewRecord, setViewRecord] = useState(null); + const { courseware } = props; + const { courseList: dataSource, courseTotal: total, courseLoading: loading } = + courseware || {}; + + const handleSearch = () => { + props.coursePage(router.query); + }; + + useEffect(() => { + searchForm.setFieldsValue(router.query); + handleSearch(); + }, []); + + const columns = [ + { + title: "序号", + width: 70, + fixed: "left", + render: (_, __, index) => { + const current = Number(router.query.current) || 1; + const size = Number(router.query.size) || 10; + return (current - 1) * size + index + 1; + }, + }, + { + title: "课程名称", + dataIndex: "courseName", + width: 260, + ellipsis: true, + }, + { + title: "课程描述", + dataIndex: "courseDescription", + width: 320, + ellipsis: true, + }, + { + title: "课程状态", + dataIndex: "status", + width: 90, + render: (v) => { + const item = COURSEWARE_STATUS_MAP[v]; + return item ? : "-"; + }, + }, + { + title: "总课时", + dataIndex: "classHourDuration", + width: 90, + render: (v) => (v == null ? "-" : v), + }, + { + title: "上传单位", + dataIndex: "uploadUnit", + width: 200, + ellipsis: true, + }, + { + title: "是否已使用", + dataIndex: "used", + width: 100, + render: (v) => (v === 1 ? "已使用" : "未使用"), + }, + { + title: "操作", + width: 200, + fixed: "right", + render: (_, record) => ( + + + + + + + ), + }, + ]; + + const relatedCourseware = viewRecord?.courseRelCoursewareVOList || []; + + return ( + + + + , + + + , + + + , + ]} + onReset={(value) => { + router.query = { ...value, current: 1, size: 10 }; + handleSearch(); + }} + onFinish={(value) => { + router.query = { ...value, current: 1, size: 10 }; + handleSearch(); + }} + /> + + `共 ${t} 条`, + current: Number(router.query.current) || 1, + pageSize: Number(router.query.size) || 10, + onChange: (page, pageSize) => { + router.query = { ...router.query, current: page, size: pageSize }; + handleSearch(); + }, + }} + /> + + setViewRecord(null)} + > + + + {viewRecord?.courseName || "-"} + + + {COURSEWARE_TRAINING_TYPE_MAP[viewRecord?.trainingType] + ?? (viewRecord?.trainingType || "-")} + + + {COURSEWARE_STATUS_MAP[viewRecord?.status]?.label || "-"} + + + {viewRecord?.classHourDuration ?? "-"} + + + {viewRecord?.used === 1 ? "已使用" : "未使用"} + + + {viewRecord?.uploadUnit || "-"} + + + {viewRecord?.courseDescription || "-"} + + + +
+ 关联课件 +
+
row.coursewareId} + size="small" + columns={[ + { title: "序号", width: 60, render: (_, __, i) => i + 1 }, + { + title: "课件名称", + dataIndex: "coursewareName", + ellipsis: true, + }, + { + title: "课时时长(小时)", + dataIndex: "classHourDuration", + width: 130, + render: (v) => (v == null ? "-" : v), + }, + { + title: "学时", + dataIndex: "creditHours", + width: 90, + render: (v) => (v == null ? "-" : v), + }, + { + title: "课件文档", + dataIndex: "coursewareDocument", + width: 160, + render: (raw) => { + const files = parseCoursewareDocument(raw); + return files.length ? ( + files.map((f, i) => ( + + {f.name || f.url} + + )) + ) : ( + "-" + ); + }, + }, + ]} + dataSource={relatedCourseware} + pagination={false} + /> + + + ); +} + +export default Connect( + [NS_COURSEWARE], + true, +)(AntdTableFuncControl(CourseManagePage)); diff --git a/src/pages/Container/EduTraining/CoursewareManage/index.js b/src/pages/Container/EduTraining/CoursewareManage/index.js index f4cb485..c718ad0 100644 --- a/src/pages/Container/EduTraining/CoursewareManage/index.js +++ b/src/pages/Container/EduTraining/CoursewareManage/index.js @@ -17,6 +17,7 @@ import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction"; import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper"; import SearchForm from "~/components/SearchForm"; import AttachmentUpload from "~/components/AttachmentUpload"; +import PreviewUrlButton from "~/components/PreviewUrlButton"; import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd"; import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { tools } from "@cqsjjb/jjb-common-lib"; @@ -32,9 +33,34 @@ import { const { router } = tools; +/** 解析本地视频时长(秒),依赖浏览器解码能力,个别编码可能失败 */ +const getVideoDuration = (file) => + new Promise((resolve, reject) => { + const video = document.createElement("video"); + video.preload = "metadata"; + video.onloadedmetadata = () => { + URL.revokeObjectURL(video.src); + resolve(video.duration); + }; + video.onerror = () => reject(new Error("无法解析视频时长")); + video.src = URL.createObjectURL(file); + }); + +/** 解析课件文档字段:JSON 数组字符串,容错非法格式 */ +const parseCoursewareDocument = (raw) => { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +}; + /** 培训课件管理 */ function CoursewareManagePage(props) { const [searchForm] = Form.useForm(); + const [viewOpen, setViewOpen] = useState(false); const [viewRecord, setViewRecord] = useState(null); const [addOpen, setAddOpen] = useState(false); const { courseware } = props; @@ -53,6 +79,16 @@ function CoursewareManagePage(props) { handleSearch(); }, []); + /** 查看详情:列表数据不全,调详情接口获取 */ + const onView = async (record) => { + setViewRecord(null); + setViewOpen(true); + const res = await props.coursewareDetail({ id: record.id }); + if (res?.success !== false) { + setViewRecord(res?.data || {}); + } + }; + /** 启用/禁用 */ const onToggleStatus = (record) => { const enabled = record.status === 1; @@ -63,7 +99,7 @@ function CoursewareManagePage(props) { cancelText: "否", onOk: async () => { const res = await props.coursewareModify({ - ...record, + id: record.id, status: enabled ? 0 : 1, }); if (res?.success !== false) { @@ -94,6 +130,7 @@ function CoursewareManagePage(props) { { title: "序号", width: 70, + fixed: "left", render: (_, __, index) => { const current = Number(router.query.current) || 1; const size = Number(router.query.size) || 10; @@ -153,7 +190,7 @@ function CoursewareManagePage(props) { @@ -254,10 +291,11 @@ function CoursewareManagePage(props) { setViewRecord(null)} + loading={courseware?.coursewareDetailLoading} + onCancel={() => setViewOpen(false)} > @@ -293,6 +331,22 @@ function CoursewareManagePage(props) { {COURSEWARE_STATUS_MAP[viewRecord?.status]?.label || "-"} + + {(() => { + const files = parseCoursewareDocument( + viewRecord?.coursewareDocument, + ); + return files.length ? ( + files.map((f, i) => ( + + {f.name || f.url} + + )) + ) : ( + "-" + ); + })()} + {viewRecord?.coursewareDescription || "-"} @@ -337,11 +391,23 @@ function AddCoursewareModal({ message.warning("请等待文件上传完成"); return; } - values.coursewareDocument = - values.coursewareDocument - ?.map?.((f) => f.url) - .filter(Boolean) - .join(",") || undefined; + // 视频课件:先取原始文件,尝试解析实际时长(秒转小时)随表单提交,解析失败不阻断提交 + const originFile = values.coursewareDocument?.[0]?.originFileObj; + if (originFile?.type?.startsWith("video")) { + try { + const seconds = await getVideoDuration(originFile); + if (seconds > 0) { + values.classHourDuration = Math.round((seconds / 3600) * 100) / 100; + } + } catch { + // 解析失败时保留用户填写的时长 + } + } + values.coursewareDocument = values.coursewareDocument?.length + ? JSON.stringify( + values.coursewareDocument.map(({ originFileObj, ...rest }) => rest), + ) + : undefined; const res = await requestAdd(values); if (res?.success !== false) { message.success("新增成功"); @@ -364,6 +430,7 @@ function AddCoursewareModal({ onOk={handleSubmit} >
+ , }, + { + key: "/safetyEval/container/EduTraining/CourseManage", + label: "培训课程管理", + icon: , + }, ], }, {