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: ,
+ },
],
},
{