dev-tmp1
tangjie 2026-08-19 13:47:08 +08:00
parent fcf1a193f2
commit 5e7c49095b
6 changed files with 402 additions and 15 deletions

View File

@ -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: {

View File

@ -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",
);

View File

@ -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 } : {}),
})) || []
);
}}

View File

@ -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 ? <Badge status={item.status} text={item.label} /> : "-";
},
},
{
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) => (
<TableAction>
<Button
type="link"
size="small"
onClick={() => setViewRecord(record)}
>
查看
</Button>
<Button
type="link"
size="small"
onClick={() => message.info("课程启用/禁用接口待接入")}
>
{record.status === 1 ? "禁用" : "启用"}
</Button>
<Button
type="link"
size="small"
onClick={() => message.info("课程编辑接口待接入")}
>
编辑
</Button>
<Button
danger
type="link"
size="small"
onClick={() => message.info("课程删除接口待接入")}
>
删除
</Button>
</TableAction>
),
},
];
const relatedCourseware = viewRecord?.courseRelCoursewareVOList || [];
return (
<PageLayout title="培训课程管理">
<SearchForm
style={{ marginBottom: 24 }}
form={searchForm}
loading={loading}
formLine={[
<Form.Item key="courseName" name="courseName">
<ControlWrapper.Input
label="课程名称"
placeholder="请输入名称"
allowClear
maxLength={50}
/>
</Form.Item>,
<Form.Item key="trainingType" name="trainingType">
<ControlWrapper.Select
label="培训类型"
placeholder="请选择"
allowClear
style={{ width: "100%" }}
options={COURSEWARE_TRAINING_TYPE_OPTIONS}
/>
</Form.Item>,
<Form.Item key="status" name="status">
<ControlWrapper.Select
label="课程状态"
placeholder="请选择课程状态"
allowClear
style={{ width: "100%" }}
options={COURSEWARE_STATUS_OPTIONS}
/>
</Form.Item>,
]}
onReset={(value) => {
router.query = { ...value, current: 1, size: 10 };
handleSearch();
}}
onFinish={(value) => {
router.query = { ...value, current: 1, size: 10 };
handleSearch();
}}
/>
<Table
rowKey="id"
columns={columns}
dataSource={Array.isArray(dataSource) ? dataSource : []}
scroll={{ x: 1500, y: props.scrollY }}
loading={loading}
pagination={{
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${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();
},
}}
/>
<Modal
title="课程详情"
open={!!viewRecord}
width={760}
footer={null}
onCancel={() => setViewRecord(null)}
>
<Descriptions column={2} bordered size="small">
<Descriptions.Item label="课程名称" span={2}>
{viewRecord?.courseName || "-"}
</Descriptions.Item>
<Descriptions.Item label="培训类型">
{COURSEWARE_TRAINING_TYPE_MAP[viewRecord?.trainingType]
?? (viewRecord?.trainingType || "-")}
</Descriptions.Item>
<Descriptions.Item label="课程状态">
{COURSEWARE_STATUS_MAP[viewRecord?.status]?.label || "-"}
</Descriptions.Item>
<Descriptions.Item label="总课时">
{viewRecord?.classHourDuration ?? "-"}
</Descriptions.Item>
<Descriptions.Item label="是否已使用">
{viewRecord?.used === 1 ? "已使用" : "未使用"}
</Descriptions.Item>
<Descriptions.Item label="上传单位" span={2}>
{viewRecord?.uploadUnit || "-"}
</Descriptions.Item>
<Descriptions.Item label="课程描述" span={2}>
{viewRecord?.courseDescription || "-"}
</Descriptions.Item>
</Descriptions>
<div style={{ marginTop: 16, marginBottom: 8, fontWeight: 600 }}>
关联课件
</div>
<Table
rowKey={(row) => 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) => (
<PreviewUrlButton key={f.uid || i} url={f.url}>
{f.name || f.url}
</PreviewUrlButton>
))
) : (
"-"
);
},
},
]}
dataSource={relatedCourseware}
pagination={false}
/>
</Modal>
</PageLayout>
);
}
export default Connect(
[NS_COURSEWARE],
true,
)(AntdTableFuncControl(CourseManagePage));

View File

@ -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) {
<Button
type="link"
size="small"
onClick={() => setViewRecord(record)}
onClick={() => onView(record)}
>
查看
</Button>
@ -254,10 +291,11 @@ function CoursewareManagePage(props) {
<Modal
title="课件详情"
open={!!viewRecord}
open={viewOpen}
width={720}
footer={null}
onCancel={() => setViewRecord(null)}
loading={courseware?.coursewareDetailLoading}
onCancel={() => setViewOpen(false)}
>
<Descriptions column={2} bordered size="small">
<Descriptions.Item label="课件名称" span={2}>
@ -293,6 +331,22 @@ function CoursewareManagePage(props) {
<Descriptions.Item label="状态">
{COURSEWARE_STATUS_MAP[viewRecord?.status]?.label || "-"}
</Descriptions.Item>
<Descriptions.Item label="课件文档" span={2}>
{(() => {
const files = parseCoursewareDocument(
viewRecord?.coursewareDocument,
);
return files.length ? (
files.map((f, i) => (
<PreviewUrlButton key={f.uid || i} url={f.url}>
{f.name || f.url}
</PreviewUrlButton>
))
) : (
"-"
);
})()}
</Descriptions.Item>
<Descriptions.Item label="课件描述" span={2}>
{viewRecord?.coursewareDescription || "-"}
</Descriptions.Item>
@ -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}
>
<Form form={form} layout="vertical" scrollToFirstError>
<Form.Item noStyle name='classHourDuration' />
<Form.Item
name="coursewareName"
label="课件名称"
@ -399,6 +466,7 @@ function AddCoursewareModal({
rules={[{ required: true, message: "请上传课件文档" }]}
extra="最多上传1个文件"
maxCount={1}
keepOriginFile
/>
<Form.Item name="coursewareDescription" label="课件描述">
<Input.TextArea

View File

@ -294,6 +294,11 @@ const menuItems = [
label: "培训课件管理",
icon: <FileTextOutlined />,
},
{
key: "/safetyEval/container/EduTraining/CourseManage",
label: "培训课程管理",
icon: <FileTextOutlined />,
},
],
},
{