dev-tmp1
tangjie 2026-08-25 14:47:27 +08:00
parent d12fd88685
commit 45beeabf85
15 changed files with 281 additions and 368 deletions

View File

@ -1,109 +0,0 @@
{
"openapi": "3.0.1",
"info": {
"title": "分页查询学员考试记录",
"description": "同步自 Apifox学员考试记录int64 主键按前端精度规范落为 string",
"version": "1.0.0"
},
"paths": {
"/safetyEval/classStudentExamRecord/page": {
"get": {
"summary": "分页查询学员考试记录",
"tags": ["学员考试记录"],
"parameters": [
{
"name": "current",
"in": "query",
"description": "当前页默认1",
"required": false,
"schema": { "type": "integer", "default": 1 }
},
{
"name": "size",
"in": "query",
"description": "每页条数默认10",
"required": false,
"schema": { "type": "integer", "default": 10 }
},
{
"name": "classId",
"in": "query",
"description": "所属班级ID后端 Long前端按字符串保证精度",
"required": false,
"schema": { "type": "string" }
},
{
"name": "studentId",
"in": "query",
"description": "学员ID后端 Long前端按字符串保证精度",
"required": false,
"schema": { "type": "string" }
},
{
"name": "examId",
"in": "query",
"description": "试卷id后端 Long前端按字符串保证精度",
"required": false,
"schema": { "type": "string" }
},
{
"name": "passed",
"in": "query",
"description": "是否通过0-未通过1-通过)",
"required": false,
"schema": { "type": "integer" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PageResponseClassStudentExamRecordVO" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"PageResponseClassStudentExamRecordVO": {
"type": "object",
"properties": {
"success": { "type": "boolean", "description": "是否成功" },
"code": { "type": "string", "description": "响应码" },
"message": { "type": "string", "description": "响应消息" },
"errMessage": { "type": "string", "description": "错误响应消息" },
"data": {
"type": "array",
"description": "响应数据",
"items": { "$ref": "#/components/schemas/ClassStudentExamRecordVO" }
},
"total": { "type": "integer", "description": "总条数" }
}
},
"ClassStudentExamRecordVO": {
"type": "object",
"description": "学员考试记录",
"properties": {
"id": { "type": "string", "description": "主键" },
"studentId": { "type": "string", "description": "学员ID后端 Long前端按字符串保证精度" },
"studentName": { "type": "string", "description": "学员名称" },
"classId": { "type": "string", "description": "所属班级ID后端 Long前端按字符串保证精度" },
"paperName": { "type": "string", "description": "试卷名称" },
"paperId": { "type": "string", "description": "试卷id后端 Long前端按字符串保证精度" },
"score": { "type": "number", "description": "得分" },
"passed": { "type": "integer", "description": "是否通过0-未通过1-通过)" },
"wrongCount": { "type": "integer", "description": "错误题数" },
"accuracy": { "type": "number", "description": "正确率(百分比)" },
"startTime": { "type": "string", "description": "考试开始时间" },
"endTime": { "type": "string", "description": "考试结束时间" },
"className": { "type": "string", "description": "app: 班级名称" },
"examDurationMinutes": { "type": "integer", "description": "app: 考试时长分钟" }
}
}
}
}
}

View File

@ -1,62 +0,0 @@
/**
* GET /safetyEval/classStudentExamRecord/page
* spec.json
*/
/** 分页查询入参 */
export interface ClassStudentExamRecordPageQuery {
/** 当前页默认1 */
current?: number;
/** 每页条数默认10 */
size?: number;
/** 所属班级ID后端 Long前端按字符串保证精度 */
classId?: string;
/** 学员ID后端 Long前端按字符串保证精度 */
studentId?: string;
/** 试卷id后端 Long前端按字符串保证精度 */
examId?: string;
/** 是否通过0-未通过1-通过) */
passed?: number;
}
/** 学员考试记录 */
export interface ClassStudentExamRecordVO {
/** 主键 */
id?: string;
/** 学员ID后端 Long前端按字符串保证精度 */
studentId?: string;
/** 学员名称 */
studentName?: string;
/** 所属班级ID后端 Long前端按字符串保证精度 */
classId?: string;
/** 试卷名称 */
paperName?: string;
/** 试卷id后端 Long前端按字符串保证精度 */
paperId?: string;
/** 得分 */
score?: number;
/** 是否通过0-未通过1-通过) */
passed?: number;
/** 错误题数 */
wrongCount?: number;
/** 正确率(百分比) */
accuracy?: number;
/** 考试开始时间 */
startTime?: string;
/** 考试结束时间 */
endTime?: string;
/** app: 班级名称 */
className?: string;
/** app: 考试时长分钟 */
examDurationMinutes?: number;
}
/** 响应体 */
export interface PageResponseClassStudentExamRecordVO {
success?: boolean;
code?: string;
message?: string;
errMessage?: string;
data?: ClassStudentExamRecordVO[];
total?: number;
}

View File

@ -1,122 +0,0 @@
{
"openapi": "3.0.1",
"info": {
"title": "分页查询学员学习记录",
"description": "同步自 Apifox学员学习记录int64 主键按前端精度规范落为 string",
"version": "1.0.0"
},
"paths": {
"/safetyEval/classStudentLearningRecord/page": {
"get": {
"summary": "分页查询学员学习记录",
"tags": ["学员学习记录"],
"parameters": [
{
"name": "current",
"in": "query",
"description": "当前页默认1",
"required": false,
"schema": { "type": "integer", "default": 1 }
},
{
"name": "size",
"in": "query",
"description": "每页条数默认10",
"required": false,
"schema": { "type": "integer", "default": 10 }
},
{
"name": "classId",
"in": "query",
"description": "所属班级ID后端 Long前端按字符串保证精度",
"required": false,
"schema": { "type": "string" }
},
{
"name": "studentId",
"in": "query",
"description": "学员ID后端 Long前端按字符串保证精度",
"required": false,
"schema": { "type": "string" }
},
{
"name": "courseId",
"in": "query",
"description": "课程ID后端 Long前端按字符串保证精度",
"required": false,
"schema": { "type": "string" }
},
{
"name": "studyStatus",
"in": "query",
"description": "学习状态0-未学习1-学习中2-已完成",
"required": false,
"schema": { "type": "integer" }
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PageResponseClassStudentLearningRecordVO"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"PageResponseClassStudentLearningRecordVO": {
"type": "object",
"properties": {
"success": { "type": "boolean", "description": "是否成功" },
"code": { "type": "string", "description": "响应码" },
"message": { "type": "string", "description": "响应消息" },
"errMessage": { "type": "string", "description": "错误响应消息" },
"data": {
"type": "array",
"description": "响应数据",
"items": {
"$ref": "#/components/schemas/ClassStudentLearningRecordVO"
}
},
"total": { "type": "integer", "description": "总条数" }
}
},
"ClassStudentLearningRecordVO": {
"type": "object",
"description": "学员学习记录(节点级)",
"properties": {
"id": { "type": "string", "description": "主键" },
"studentId": {
"type": "string",
"description": "学员ID后端 Long前端按字符串保证精度"
},
"classId": {
"type": "string",
"description": "所属班级ID后端 Long前端按字符串保证精度"
},
"courseId": {
"type": "string",
"description": "课程ID后端 Long前端按字符串保证精度"
},
"courseName": { "type": "string", "description": "课程名称" },
"coursewareName": { "type": "string", "description": "小节点名称" },
"studyStatus": {
"type": "integer",
"description": "学习状态0-未学习1-学习中2-已完成"
},
"learningProgress": { "type": "string", "description": "学习情况" },
"startTime": { "type": "string", "description": "开始学习时间" },
"endTime": { "type": "string", "description": "完成时间" },
"lastWatchTime": { "type": "string", "description": "最近观看时间" }
}
}
}
}
}

View File

@ -1,56 +0,0 @@
/**
* GET /safetyEval/classStudentLearningRecord/page
* spec.json
*/
/** 分页查询入参 */
export interface ClassStudentLearningRecordPageQuery {
/** 当前页默认1 */
current?: number;
/** 每页条数默认10 */
size?: number;
/** 所属班级ID后端 Long前端按字符串保证精度 */
classId?: string;
/** 学员ID后端 Long前端按字符串保证精度 */
studentId?: string;
/** 课程ID后端 Long前端按字符串保证精度 */
courseId?: string;
/** 学习状态0-未学习1-学习中2-已完成 */
studyStatus?: number;
}
/** 学员学习记录(节点级) */
export interface ClassStudentLearningRecordVO {
/** 主键 */
id?: string;
/** 学员ID后端 Long前端按字符串保证精度 */
studentId?: string;
/** 所属班级ID后端 Long前端按字符串保证精度 */
classId?: string;
/** 课程ID后端 Long前端按字符串保证精度 */
courseId?: string;
/** 课程名称 */
courseName?: string;
/** 小节点名称 */
coursewareName?: string;
/** 学习状态0-未学习1-学习中2-已完成 */
studyStatus?: number;
/** 学习情况 */
learningProgress?: string;
/** 开始学习时间 */
startTime?: string;
/** 完成时间 */
endTime?: string;
/** 最近观看时间 */
lastWatchTime?: string;
}
/** 分页响应体 */
export interface PageResponseClassStudentLearningRecordVO {
success?: boolean;
code?: string;
message?: string;
errMessage?: string;
data?: ClassStudentLearningRecordVO[];
total?: number;
}

View File

@ -210,3 +210,9 @@ export const classStudentExamRecordPage = declareRequest(
"Get > /safetyEval/classStudentExamRecord/page",
"examRecordList: [] | res.data || [] & examRecordTotal: 0 | res.total || 0",
);
/** 考试试卷信息查询按考试记录id含试题列表及参考答案 */
export const studentExamPaperInfo = declareRequest(
"studentExamPaperLoading",
"Get > /safetyEval/paperExam/exam/info",
);

View File

@ -710,8 +710,8 @@ export const CLASS_STUDENT_FACE_AUTH_MAP = {
export const CLASS_STUDENT_STUDY_STATUS_OPTIONS = [
{ label: "未学习", value: 0 },
{ label: "学习中", value: 1 },
{ label: "已学完", value: 2 },
{ label: "已完成", value: 3 },
{ label: "已完成", value: 2 },
];
export const CLASS_STUDENT_STUDY_STATUS_MAP = CLASS_STUDENT_STUDY_STATUS_OPTIONS.reduce(
@ -743,7 +743,7 @@ export const CLASS_STUDENT_EXAM_STATUS_OPTIONS = [
{ label: "待考试", value: 1 },
{ label: "考试未通过", value: 2 },
{ label: "考试通过", value: 3 },
{ label: "未参加", value: 4 },
];
export const CLASS_STUDENT_EXAM_STATUS_MAP = CLASS_STUDENT_EXAM_STATUS_OPTIONS.reduce(

View File

@ -5,6 +5,7 @@ import SearchForm from "~/components/SearchForm";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_COURSEWARE } from "~/enumerate/namespace";
import StaffViewModal from "~/components/StaffViewModal";
import {
CLASS_STUDENT_EXAM_STATUS_OPTIONS,
@ -32,7 +33,7 @@ const buildQuery = (value) => {
/** 班级详情 — 学员 Tab班级学员查询 */
function ClassStudentTab(props) {
const { classId } = props;
const { classId, form } = props;
const [searchForm] = Form.useForm();
const [query, setQuery] = useState({ current: 1, size: 10 });
const [selectOpen, setSelectOpen] = useState(false);
@ -228,7 +229,7 @@ function ClassStudentTab(props) {
rowKey="id"
columns={columns}
dataSource={tableSource}
scroll={{ x: 1400 }}
scroll={{ x: 1400, y: 400 }}
loading={loading}
pagination={{
total,
@ -265,6 +266,7 @@ function ClassStudentTab(props) {
studentName: userName,
department: deptName,
phone: account,
examStatus: form.getFieldValue("openExam"),
studentId: studentId || id,
fileNumber: fileNumber|| id,
unitName: unitName,
@ -395,4 +397,4 @@ function StudentSelectModal({
);
}
export default Connect([NS_COURSEWARE], true)(ClassStudentTab);
export default Connect([NS_COURSEWARE], true)((ClassStudentTab) );

View File

@ -297,7 +297,7 @@ function ClassAddPage(props) {
label: "学员",
disabled: !unlocked,
children: unlocked ? (
<StudentTab classId={savedClassId} history={props.history} />
<StudentTab classId={savedClassId} history={props.history} form={form} />
) : null,
},
{

View File

@ -0,0 +1,149 @@
import React, { useEffect, useState } from "react";
import { Button, Empty, Modal, Tag } from "antd";
import { NS_COURSEWARE } from "~/enumerate/namespace";
import {
EXAM_RECORD_PASSED_MAP,
QUESTION_TYPE_MAP,
} from "~/enumerate/constant";
/** 区块标题 */
const SectionTitle = ({ children }) => (
<div className="srd-detail__section-title">{children}</div>
);
/** label: value 字段 */
const Field = ({ label, value }) => (
<div className="srd-detail__field">
<span className="srd-detail__label">{label}</span>
<span className="srd-detail__value">{value ?? "-"}</span>
</div>
);
/** 考试详情弹窗:学员信息 + 试卷基本信息 + 试卷内容(试题列表) */
function ExamDetailModal({
open,
record,
loading,
fetchPaperInfo,
resetModelState,
onClose,
}) {
const [info, setInfo] = useState(null);
useEffect(() => {
if (!open) {
resetModelState?.(NS_COURSEWARE, { studentExamPaperLoading: false });
return;
}
if (!record?.id) return;
setInfo(null);
fetchPaperInfo({ paperExamId: record.id }).then((res) => {
if (res?.success !== false) setInfo(res?.data || null);
});
}, [open, record?.id]);
const basic = info?.paperBasicInfoVO || {};
const questionList = Array.isArray(info?.questionVOList)
? info.questionVOList
: [];
return (
<Modal
open={open}
title={`考试详情${record?.studentName ? ` - ${record.studentName}` : ""}`}
width={980}
footer={
<Button onClick={onClose}>关闭</Button>
}
destroyOnHidden
loading={loading}
onCancel={onClose}
className="srd-detail"
>
<SectionTitle>考试学员信息</SectionTitle>
<div className="srd-detail__grid">
<Field label="考试学员" value={record?.studentName} />
<Field
label="状态"
value={
record?.passed != null ? (
<Tag color={record.passed === 1 ? "green" : "red"}>
{EXAM_RECORD_PASSED_MAP[record.passed]}
</Tag>
) : (
"-"
)
}
/>
<Field label="学员得分" value={record?.score} />
<Field label="考试时间" value={record?.startTime} />
<Field
label="答题时长"
value={
record?.examDurationMinutes != null
? `${record.examDurationMinutes}分钟`
: undefined
}
/>
</div>
<SectionTitle>试卷基本信息</SectionTitle>
<div className="srd-detail__grid">
<Field label="试卷名称" value={basic.paperName} />
<Field
label="考试时长"
value={basic.examTime != null ? `${basic.examTime}分钟` : undefined}
/>
<Field label="总分" value={basic.paperTotalScore} />
<Field label="合格分数" value={basic.paperPassScore} />
</div>
<SectionTitle>试卷内容信息</SectionTitle>
<div className="srd-detail__count">
试题数量<strong>{questionList.length}</strong>
</div>
{questionList.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无试题"
/>
) : (
<div className="srd-detail__questions">
{questionList.map((q, index) => (
<div key={q.id} className="srd-detail__question">
<div className="srd-detail__question-title">
{index + 1}. {q.title || "-"}
</div>
<div className="srd-detail__question-meta">
题型{QUESTION_TYPE_MAP[q.questionType] || q.questionType || "-"}
<span className="srd-detail__question-score">
分值{q.questionScore ?? q.score ?? "-"}
</span>
</div>
{(q.questionOptionVOList || []).length > 0 && (
<div className="srd-detail__options">
{q.questionOptionVOList.map((o) => (
<div key={o.id} className="srd-detail__option">
{o.optionKey}. {o.optionText}
</div>
))}
</div>
)}
<div className="srd-detail__question-info">
参考答案{q.answer || "-"}
</div>
<div className="srd-detail__question-info">
答案解析{q.analysis || "-"}
</div>
<div className="srd-detail__question-info">
关联课程{q.coursewareManagementName || "-"}
</div>
</div>
))}
</div>
)}
</Modal>
);
}
export default ExamDetailModal;

View File

@ -1,7 +1,8 @@
import React, { useEffect, useState } from "react";
import { Form, Table, Tabs } from "antd";
import { Button, Form, Table, Tabs } from "antd";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import SearchForm from "~/components/SearchForm";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
@ -12,6 +13,8 @@ import {
LEARNING_RECORD_STUDY_STATUS_MAP,
LEARNING_RECORD_STUDY_STATUS_OPTIONS,
} from "~/enumerate/constant";
import ExamDetailModal from "./ExamDetailModal";
import "./index.less";
const { router } = tools;
@ -21,6 +24,7 @@ function ClassStudyRecordPage(props) {
const [examSearchForm] = Form.useForm();
const [activeTab, setActiveTab] = useState("study");
const [examQuery, setExamQuery] = useState({ current: 1, size: 10 });
const [examDetailRecord, setExamDetailRecord] = useState(null);
const studentName = router.query?.studentName;
const {
learningRecordList: dataSource,
@ -29,6 +33,7 @@ function ClassStudyRecordPage(props) {
examRecordList: examDataSource,
examRecordTotal: examTotal,
examRecordLoading: examLoading,
studentExamPaperLoading: examDetailLoading,
} = props.courseware || {};
const handleSearch = () => {
@ -169,6 +174,23 @@ function ClassStudyRecordPage(props) {
width: 170,
render: (v) => v || "-",
},
{
title: "操作",
key: "action",
width: 110,
fixed: "right",
render: (_, record) => (
<TableAction>
<Button
type="link"
size="small"
onClick={() => setExamDetailRecord(record)}
>
考试详情
</Button>
</TableAction>
),
},
];
/** 考试记录搜索/重置:试卷名称接口暂未支持,先行透传 */
@ -302,6 +324,15 @@ function ClassStudyRecordPage(props) {
},
]}
/>
<ExamDetailModal
open={!!examDetailRecord}
record={examDetailRecord}
loading={examDetailLoading}
fetchPaperInfo={props.studentExamPaperInfo}
resetModelState={props.resetModelState}
onClose={() => setExamDetailRecord(null)}
/>
</PageLayout>
);
}

View File

@ -0,0 +1,74 @@
/* StudyRecord — 考试详情弹窗样式(前缀 srd- */
.srd-detail {
&__section-title {
margin: 16px 0 12px;
padding-left: 8px;
font-size: 15px;
font-weight: 600;
line-height: 1.2;
border-left: 3px solid #fa8c16;
}
&__grid {
display: grid;
grid-template-columns: 1fr 1fr;
row-gap: 12px;
}
&__label {
color: rgba(0, 0, 0, 45%);
}
&__value {
color: rgba(0, 0, 0, 88%);
}
&__count {
margin-bottom: 12px;
color: rgba(0, 0, 0, 65%);
}
&__questions {
max-height: 420px;
overflow-y: auto;
}
&__question {
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&-title {
margin-bottom: 8px;
font-weight: 600;
}
&-meta {
margin-bottom: 8px;
color: rgba(0, 0, 0, 65%);
}
&-score {
margin-left: 24px;
}
&-info {
margin-top: 6px;
color: rgba(0, 0, 0, 65%);
}
}
&__options {
display: grid;
grid-template-columns: 1fr 1fr;
row-gap: 6px;
margin-bottom: 8px;
}
&__option {
color: rgba(0, 0, 0, 85%);
}
}

View File

@ -213,18 +213,18 @@ const menuItems = [
label: "项目踏勘监管",
icon: <EnvironmentOutlined />,
},
{
key: "/safetyEval/container/Supervision/EvalPortrait",
label: "安全评价画像",
icon: <PieChartOutlined />,
},
{
key: "/safetyEval/container/Supervision/EvalReportDatabase",
label: "安评报告数据库",
label: "历史报告数据库",
icon: <FileProtectOutlined />,
},
],
},
{
key: "/safetyEval/container/Supervision/EvalPortrait",
label: "安评机构画像",
icon: <PieChartOutlined />,
},
{
key: "/safetyEval/container/MonitorManage/RiskCenter",
label: "风险预警中心",

View File

@ -219,7 +219,7 @@ const EvalPortrait = (props) => {
return (
<ConfigProvider theme={PROTO_THEME}>
<PageLayout
title="安全评价画像"
title="安评机构画像"
extra={
<Button onClick={openRules}>指标说明</Button>
}
@ -284,7 +284,7 @@ const EvalPortrait = (props) => {
{/* 查看画像 — 对齐原型 institutionProfile */}
<Modal
title={detail ? `全评价画像 - ${detail.orgName}` : "安全评价画像"}
title={detail ? `评机构画像 - ${detail.orgName}` : "安评机构画像"}
open={detailOpen}
onCancel={() => setDetailOpen(false)}
width={980}

View File

@ -714,7 +714,7 @@ const EvalReportDatabase = (props) => {
await downloadBinaryPost(
"/safetyEval/regulator/eval-report/export-catalog",
body,
`安评报告数据库目录_${dayjs().format("YYYYMMDDHHmmss")}.csv`,
`历史报告数据库目录_${dayjs().format("YYYYMMDDHHmmss")}.csv`,
);
message.success("目录导出已开始");
setExportVisible(false);
@ -929,7 +929,7 @@ const EvalReportDatabase = (props) => {
return (
<ConfigProvider theme={PROTO_THEME}>
<PageLayout title="安评报告数据库">
<PageLayout title="历史报告数据库">
<div className="redb-page">
<div className="redb-summary-cards">
<div className="redb-summary-card">
@ -1265,7 +1265,7 @@ const EvalReportDatabase = (props) => {
{/* 导出目录 — 对齐原型 export */}
<Modal
title="安评报告数据库目录"
title="历史报告数据库目录"
open={exportVisible}
onCancel={() => setExportVisible(false)}
onOk={handleExportOk}