jinxia.mo 3 dní pred
rodič
commit
386862cd23

+ 20 - 0
back-ui/src/api/dz/papers.js

@@ -657,6 +657,14 @@ export function getPaperStudentRecords(params) {
     })
 }
 
+export function getPapers(params) {
+    return request({
+        url: '/learn/teaching/getPapers',
+        method: 'get',
+        params
+    })
+}
+
 export function getPaperStudentDetail(params) {
     // // TODO: remove test code
     // return Promise.resolve({
@@ -678,6 +686,18 @@ export function getPaperStudentDetail(params) {
     })
 }
 
+// 获取试卷详情(题目列表)- 前台接口
+export function getFrontPaperDetail(type, id) {
+    return request({
+        url: '/front/paper/paper',
+        method: 'get',
+        params: {
+            type: type || '',
+            id: id
+        }
+    })
+}
+
 export function sendUnfinishAlarm(data) {
     return request({
         url: '/learn/teaching/postUnfinishAlarm',

+ 486 - 0
back-ui/src/views/dz/papers/components/paper-records.vue

@@ -0,0 +1,486 @@
+<template>
+    <el-form ref="queryRef" :model="queryParams" :rules="rules" label-width="68px" inline>
+        <el-form-item label="批次" prop="batchId">
+            <el-select v-model="batchId" clearable @change="handleQuery" style="width: 172px">
+                <el-option v-for="b in batchList" :label="formatBatchName(b)" :value="b.batchId"/>
+            </el-select>
+        </el-form-item>
+        <el-form-item label="考生类型" prop="examType">
+                <el-select v-model="queryParams.examType" clearable @change="handleQuery" style="width: 172px">
+                    <el-option
+                        v-for="e in exam_type"
+                        :key="e.dictValue || e.value || e.dictCode"
+                        :label="e.dictLabel || e.label"
+                        :value="e.dictValue || e.value"
+                    />
+                </el-select>
+        </el-form-item>
+        <el-form-item label="组卷类型" prop="buildType">
+                <el-select v-model="queryParams.buildType" clearable @change="handleQuery" style="width: 172px">
+                    <el-option
+                        v-for="b in build_type"
+                        :key="b.dictValue || b.value || b.dictCode"
+                        :label="b.dictLabel || b.label"
+                        :value="b.dictValue || b.value"
+                    />
+                </el-select>
+        </el-form-item>
+        <el-form-item>
+                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+        </el-form>
+        
+        <Table 
+            :data="list" 
+            :columns="columns" 
+            :actions="actions"
+            :total="total"
+            :query-params="queryParams"
+            @get-list="getList"
+            @action="handleAction"
+        >
+            <template #batchName="{row}">
+                <span>{{ getBatchDisplayName(row) }}</span>
+            </template>
+            <template #buildType="{row}">
+                <dict-tag v-if="row && row.buildType && build_type" :options="build_type" :value="row.buildType" />
+                <span v-else>-</span>
+            </template>
+            <template #examType="{row}">
+                <dict-tag v-if="row && row.examType && exam_type" :options="exam_type" :value="row.examType" />
+                <span v-else>-</span>
+            </template>
+            <template #questionInfo="{row}">
+                <span class="question-info-link" @click="handleShowTypes(row)">{{ getQuestionInfo(row) }}</span>
+            </template>
+            <template #duration="{row}">
+                <span>{{ getDuration(row) }}</span>
+            </template>
+            <template #createTime="{row}">
+                <span>{{ row.createTime ? parseTime(row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}</span>
+            </template>
+            <template #paperName="{row}">
+                <span class="paper-name-link" @click="handleShowPaperDetail(row)">{{ row.paperName || '-' }}</span>
+            </template>
+        </Table>
+        
+        <!-- 题目类型详情弹窗 -->
+        <el-dialog v-model="typesDialogVisible" title="题目类型详情" width="600px" destroy-on-close>
+            <el-table :data="typesList" style="width: 100%">
+                <el-table-column label="题目类型" prop="type" width="150" align="center"/>
+                <el-table-column label="数量" prop="count" width="120" align="center"/>
+                <el-table-column label="分数" prop="score" width="120" align="center"/>
+            </el-table>
+        </el-dialog>
+        
+        <!-- 试卷题目详情弹窗 -->
+        <el-dialog v-model="paperDetailDialogVisible" :title="currentPaperName || '试卷题目详情'" width="80%" destroy-on-close>
+            <div class="paper-detail-container" v-loading="paperDetailLoading">
+                <div class="paper-info-header">
+                    <div class="paper-info-item">
+                        <span class="paper-info-label">试卷名称:</span>
+                        <span class="paper-info-value">{{ currentPaperName || '-' }}</span>
+                    </div>
+                    <div class="paper-info-item">
+                        <span class="paper-info-label">科目名称:</span>
+                        <span class="paper-info-value">{{ currentSubjectName || '-' }}</span>
+                    </div>
+                </div>
+                <div v-for="(question, index) in questionList" :key="question.id" class="question-item">
+                    <div class="question-header">
+                        <span class="question-index">{{ index + 1 }}.</span>
+                        <span class="question-id">题目ID: {{ question.id }}</span>
+                        <span class="question-type">题型: {{ question.type }}</span>
+                        <span class="question-score">分数: {{ question.score }}</span>
+                        <span class="question-knowledge" v-if="question.knowledgeId">知识点ID: {{ question.knowledgeId }}</span>
+                    </div>
+                    <div class="question-title">{{ question.title }}</div>
+                    <div class="question-options" v-if="question.options && question.options.length > 0">
+                        <div v-for="(option, optIndex) in question.options" :key="optIndex" class="option-item">
+                            <span class="option-label">{{ getOptionLabel(optIndex) }}.</span>
+                            <span class="option-content">{{ option }}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </el-dialog>
+</template>
+
+<script setup name="PaperRecords">
+import { ref, computed, getCurrentInstance, onMounted } from 'vue'
+import { useInjectGlobalLoading } from "@/views/hooks/useGlobalLoading.js"
+import { getPapers, getFrontPaperDetail } from "@/api/dz/papers.js"
+import { ElMessage } from 'element-plus'
+import Table from '@/components/Table/index.vue'
+import DictTag from '@/components/DictTag/index.vue'
+import consts from "@/utils/consts.js"
+import { listToMap } from "@/utils/index.js"
+import { useProvidePaperBatchCondition } from "@/views/dz/papers/hooks/usePaperBatchCondition.js"
+import { parseTime } from "@/utils/ruoyi.js"
+
+const { proxy } = getCurrentInstance()
+const { exam_type } = proxy.useDict("exam_type")
+const { build_type } = proxy.useDict("build_type")
+const { loading } = useInjectGlobalLoading()
+
+// 批次相关 - 使用 useProvidePaperBatchCondition hook
+const {
+    batchList,
+    batchId,
+    formatBatchName,
+    getBatchDisplayName
+} = useProvidePaperBatchCondition(null, false)
+
+// 查询参数
+const queryParams = ref({
+    examType: '',
+    buildType: '',
+    pageNum: 1,
+    pageSize: 10
+})
+
+// 表单验证规则
+const rules = {}
+
+// 列表数据
+const list = ref([])
+const total = ref(0)
+
+// 题目类型详情弹窗
+const typesDialogVisible = ref(false)
+const typesList = ref([])
+
+// 试卷题目详情弹窗
+const paperDetailDialogVisible = ref(false)
+const paperDetailLoading = ref(false)
+const questionList = ref([])
+const currentPaperName = ref('')
+const currentSubjectName = ref('')
+
+
+// 获取题数/总分显示
+const getQuestionInfo = (row) => {
+    if (!row) return '-'
+    const number = row.number || 0
+    const totalScore = row.totalScore || 0
+    return `${number}/${totalScore}`
+}
+
+// 获取时长(分钟)
+const getDuration = (row) => {
+    if (!row || !row.paperInfo) return '-'
+    try {
+        // paperInfo 可能是 JSON 字符串或对象
+        const paperInfo = typeof row.paperInfo === 'string' ? JSON.parse(row.paperInfo) : row.paperInfo
+        if (paperInfo && paperInfo.time) {
+            const minutes = Math.round(paperInfo.time / 60)
+            return `${minutes}分钟`
+        }
+    } catch (e) {
+        console.error('解析 paperInfo 失败:', e)
+    }
+    return '-'
+}
+
+// 显示题目类型详情
+const handleShowTypes = (row) => {
+    if (!row || !row.paperInfo) {
+        typesList.value = []
+        typesDialogVisible.value = true
+        return
+    }
+    try {
+        // paperInfo 可能是 JSON 字符串或对象
+        const paperInfo = typeof row.paperInfo === 'string' ? JSON.parse(row.paperInfo) : row.paperInfo
+        if (paperInfo && Array.isArray(paperInfo.types)) {
+            typesList.value = paperInfo.types.map(t => ({
+                type: t.type || '-',
+                count: t.count || 0,
+                score: t.score || 0
+            }))
+        } else {
+            typesList.value = []
+        }
+    } catch (e) {
+        console.error('解析 paperInfo 失败:', e)
+        typesList.value = []
+    }
+    typesDialogVisible.value = true
+}
+
+// 显示试卷题目详情
+const handleShowPaperDetail = async (row) => {
+    if (!row || !row.paperId) {
+        ElMessage.warning('试卷ID不存在')
+        return
+    }
+    
+    // 保存试卷名称和科目名称
+    currentPaperName.value = row.paperName || ''
+    currentSubjectName.value = row.subjectName || ''
+    
+    paperDetailDialogVisible.value = true
+    paperDetailLoading.value = true
+    questionList.value = []
+    
+    try {
+        const response = await getFrontPaperDetail('', row.paperId)
+        if (response && response.code === 200 && response.data && response.data.questions) {
+            // 处理题目数据
+            questionList.value = response.data.questions.map((q, index) => ({
+                index: index + 1,
+                id: q.id || '-',
+                title: q.title || '-',
+                type: q.type || '-',
+                options: q.options || [],
+                knowledgeId: q.knowledgeId || '-',
+                score: q.score || 0
+            }))
+        } else {
+            ElMessage.warning('获取试卷详情失败')
+            questionList.value = []
+        }
+    } catch (error) {
+        console.error('获取试卷详情失败:', error)
+        ElMessage.error('获取试卷详情失败')
+        questionList.value = []
+    } finally {
+        paperDetailLoading.value = false
+    }
+}
+
+// 获取选项标签(A, B, C, D, E, F, G...)
+const getOptionLabel = (index) => {
+    const labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
+    return labels[index] || String(index + 1)
+}
+
+// 表格列定义
+const columns = [
+    { label: 'ID', prop: 'id', width: 80 },
+    { label: '批次', prop: 'batchName', minWidth: 150, type: 'slot', slotName: 'batchName' },
+    { label: '组卷类型', prop: 'buildType', width: 120, type: 'slot', slotName: 'buildType' },
+    { label: '科目名称', prop: 'subjectName', minWidth: 120 },
+    { label: '考生类型', prop: 'examType', width: 120, type: 'slot', slotName: 'examType' },
+    { label: '试卷名称', prop: 'paperName', minWidth: 200, showOverflowTooltip: true, type: 'slot', slotName: 'paperName' },
+    { label: '试卷年份', prop: 'year', width: 100 },
+    { label: '试卷类型', prop: 'paperType', width: 120 },
+    { label: '题数/总分', prop: 'questionInfo', width: 100, type: 'slot', slotName: 'questionInfo' },
+    { label: '时长(分钟)', prop: 'duration', width: 120, type: 'slot', slotName: 'duration' },
+    { label: '创建时间', prop: 'createTime', width: 160, type: 'slot', slotName: 'createTime' }
+]
+
+// 操作按钮
+const actions = []
+
+// 搜索
+const handleQuery = () => {
+    queryParams.value.pageNum = 1
+    queryParams.value.buildType = queryParams.value.buildType || ''
+    getList()
+}
+
+// 重置
+const resetQuery = () => {
+    proxy.resetForm('queryRef')
+    batchId.value = ''
+    queryParams.value.examType = ''
+    queryParams.value.buildType = ''
+    queryParams.value.pageNum = 1
+    handleQuery()
+}
+
+// 获取列表数据
+const getList = () => {
+    loading.value = true
+    
+    const params = {
+        batchId: batchId.value || '',
+        examType: queryParams.value.examType || '',
+        buildType: queryParams.value.buildType || '',
+        pageNum: queryParams.value.pageNum || 1,
+        pageSize: queryParams.value.pageSize || 10
+    }
+    
+    getPapers(params).then(response => {
+        // 自动补全组卷类型名称
+        const buildTypeMap = listToMap(consts.enums.buildTypes, i => i.id, i => i.name)
+        if (response.rows) {
+            response.rows.forEach(r => {
+                if (r.buildTypeName) return
+                r.buildTypeName = buildTypeMap[r.buildType] || r.buildType
+            })
+            list.value = response.rows
+            total.value = response.total
+        } else if (response.data) {
+            // 处理分页数据结构
+            const data = response.data
+            if (Array.isArray(data)) {
+                list.value = data.map(r => {
+                    if (r.buildTypeName) return r
+                    r.buildTypeName = buildTypeMap[r.buildType] || r.buildType
+                    return r
+                })
+                total.value = response.total || data.length
+            } else {
+                list.value = []
+                total.value = 0
+            }
+        } else {
+            list.value = []
+            total.value = 0
+        }
+    }).catch(error => {
+        console.error('获取试卷列表失败:', error)
+        list.value = []
+        total.value = 0
+    }).finally(() => {
+        loading.value = false
+    })
+}
+
+// 处理操作
+const handleAction = (action, row) => {
+    // 可以根据需要添加操作逻辑
+    console.log('Action:', action, row)
+}
+
+// 刷新方法(供父组件调用)
+const refresh = () => {
+    getList()
+}
+
+// 暴露方法
+defineExpose({ refresh })
+
+// 初始化加载数据
+onMounted(() => {
+    getList()
+})
+</script>
+
+<style scoped>
+.question-info-link {
+    color: #409eff;
+    cursor: pointer;
+    text-decoration: underline;
+}
+
+.question-info-link:hover {
+    color: #66b1ff;
+}
+
+.paper-name-link {
+    color: #409eff;
+    cursor: pointer;
+    text-decoration: underline;
+}
+
+.paper-name-link:hover {
+    color: #66b1ff;
+}
+
+.paper-detail-container {
+    max-height: 70vh;
+    overflow-y: auto;
+    padding: 20px;
+}
+
+.paper-info-header {
+    display: flex;
+    gap: 30px;
+    margin-bottom: 30px;
+    padding-bottom: 20px;
+    border-bottom: 2px solid #e4e7ed;
+}
+
+.paper-info-item {
+    display: flex;
+    align-items: center;
+    font-size: 16px;
+}
+
+.paper-info-label {
+    font-weight: 600;
+    color: #606266;
+    margin-right: 8px;
+}
+
+.paper-info-value {
+    color: #303133;
+    font-weight: 500;
+}
+
+.question-item {
+    margin-bottom: 30px;
+    padding-bottom: 20px;
+    border-bottom: 1px solid #e4e7ed;
+    page-break-inside: avoid;
+}
+
+.question-item:last-child {
+    border-bottom: none;
+}
+
+.question-header {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+    margin-bottom: 10px;
+    font-size: 14px;
+    color: #606266;
+}
+
+.question-index {
+    font-weight: bold;
+    font-size: 16px;
+    color: #303133;
+}
+
+.question-id,
+.question-type,
+.question-score,
+.question-knowledge {
+    font-size: 13px;
+    color: #909399;
+}
+
+.question-title {
+    font-size: 15px;
+    line-height: 1.6;
+    color: #303133;
+    margin-bottom: 15px;
+    margin-left: 25px;
+}
+
+.question-options {
+    margin-left: 25px;
+    margin-bottom: 10px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+}
+
+.option-item {
+    display: flex;
+    align-items: flex-start;
+    line-height: 1.6;
+    font-size: 14px;
+    width: calc(50% - 10px);
+    min-width: 200px;
+}
+
+.option-label {
+    font-weight: 500;
+    color: #606266;
+    margin-right: 8px;
+    min-width: 20px;
+}
+
+.option-content {
+    color: #303133;
+    flex: 1;
+}
+</style>
+

+ 7 - 1
back-ui/src/views/dz/papers/list.vue

@@ -9,12 +9,13 @@
 </template>
 
 <script setup name="PaperList">
-
+import {ref, watch, markRaw} from "vue";
 import {useProvideGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
 import ListExactIntelligent from "@/views/dz/papers/components/list-exact-intelligent.vue";
 import ListFullIntelligent from "@/views/dz/papers/components/list-full-intelligent.vue";
 import ListExactHand from "@/views/dz/papers/components/list-exact-hand.vue";
 import ListFullHand from "@/views/dz/papers/components/list-full-hand.vue";
+import PaperRecords from "@/views/dz/papers/components/paper-records.vue";
 
 const {loading} = useProvideGlobalLoading()
 
@@ -38,6 +39,11 @@ const tabs = ref([{
     label: '全量手动',
     page: markRaw(ListFullHand),
     visited: false
+}, {
+    name: 'PaperRecords',
+    label: '组卷记录',
+    page: markRaw(PaperRecords),
+    visited: false
 }])
 const currentTab = ref('ExactIntelligent')
 

+ 9 - 0
ie-admin/src/main/java/com/ruoyi/web/controller/learn/LearnTeacherController.java

@@ -186,6 +186,15 @@ public class LearnTeacherController extends BaseController {
         return getDataTable(list);
     }
 
+    @GetMapping("/getPapers")
+    @ApiOperation("获取试卷列表")
+    public TableDataInfo getPapers(TestPaperVO.TestPaperBuildReq req)
+    {
+        startPage();
+        List<JSONObject> list = learnTeacherService.getPapers(req, SecurityUtils.getLoginUser().getUser().getUserId());
+        return getDataTable(list);
+    }
+
     @GetMapping("/getPaperStudentRecords")
     @ApiOperation("班级详情")
     public AjaxResult getPaperStudentRecords(TestPaperVO.TestPaperBuildReq req)

+ 9 - 0
ie-admin/src/main/java/com/ruoyi/web/service/LearnTeacherService.java

@@ -109,6 +109,15 @@ public class LearnTeacherService {
         return dzClassesMapper.selectClassesBuildStats(req.toMap());
     }
 
+    public List<JSONObject> getPapers(TestPaperVO.TestPaperBuildReq req, Long creatorId) {
+        Map<String, Object> params = Maps.newHashMap();
+        params.put("creatorId", creatorId);
+        params.put("examType", req.getExamType());
+        params.put("buildType", req.getBuildType());
+        params.put("batchId", req.getBatchId());
+        return learnTestPaperMapper.selectPapers(params);
+    }
+
     public List<JSONObject> getPaperStudentRecords(TestPaperVO.TestPaperBuildReq req) {
         return dzClassesMapper.getPaperStudentRecords(req.toMap());
     }

+ 8 - 0
ie-system/src/main/java/com/ruoyi/learn/mapper/LearnTestPaperMapper.java

@@ -62,4 +62,12 @@ public interface LearnTestPaperMapper
      * @return 结果
      */
     public int deleteLearnTestPaperByIds(String[] ids);
+
+    /**
+     * 查询试卷列表(关联查询)
+     * 
+     * @param params 查询参数(creatorId, buildType)
+     * @return 试卷列表
+     */
+    public List<com.alibaba.fastjson2.JSONObject> selectPapers(java.util.Map<String, Object> params);
 }

+ 48 - 4
ie-system/src/main/resources/mapper/learn/LearnTestPaperMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.ruoyi.learn.mapper.LearnTestPaperMapper">
-    
+
     <resultMap type="LearnTestPaper" id="LearnTestPaperResult">
         <result property="id"    column="id"    />
         <result property="batchId"    column="batch_id"    />
@@ -25,7 +25,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectLearnTestPaperList" parameterType="LearnTestPaper" resultMap="LearnTestPaperResult">
         <include refid="selectLearnTestPaperVo"/>
-        <where>  
+        <where>
             <if test="batchId != null "> and batch_id = #{batchId}</if>
             <if test="buildType != null "> and build_type = #{buildType}</if>
             <if test="subjectId != null "> and subject_id = #{subjectId}</if>
@@ -105,9 +105,53 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteLearnTestPaperByIds" parameterType="String">
-        delete from learn_test_paper where id in 
+        delete from learn_test_paper where id in
         <foreach item="id" collection="array" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>
-</mapper>
+
+    <select id="selectPapers" parameterType="java.util.Map" resultType="com.alibaba.fastjson2.JSONObject">
+        SELECT
+            t1.id,
+            t1.batch_id batchId,
+            t3.name batchName,
+            t3.year batchYear,
+            t1.build_type buildType,
+            t1.subject_id subjectId,
+            ds.subject_name subjectName,
+            t1.exam_type examType,
+            t1.teacher_id teacherId,
+            t1.university_id universityId,
+            t1.direct_key directKey,
+            t1.paper_id paperId,
+            t1.creator_id creatorId,
+            t1.create_time createTime,
+            t2.paperName,
+            t2.year,
+            t2.paperType,
+            t2.number,
+            t2.fenshu totalScore,
+            t2.direct_key paperDirectKey,
+            t2.paper_info paperInfo,
+            t2.relate_id relateId
+        FROM learn_test_paper t1
+        JOIN learn_paper t2 ON t1.paper_id = t2.id
+        JOIN dz_subject ds ON ds.subject_id = t1.subject_id
+        JOIN learn_test t3 ON t1.batch_id = t3.batch_id
+        WHERE 1=1
+        <if test="creatorId != null and creatorId != ''">
+            AND t1.creator_id = #{creatorId}
+        </if>
+        <if test="batchId != null and batchId != ''">
+            AND t1.batch_id = #{batchId}
+        </if>
+        <if test="buildType != null and buildType != ''">
+            AND t1.build_type = #{buildType}
+        </if>
+        <if test="examType != null and examType != ''">
+            AND t1.exam_type = #{examType}
+        </if>
+        ORDER BY t1.create_time DESC
+    </select>
+</mapper>