wy пре 1 година
родитељ
комит
6496382355

+ 176 - 0
src/views/platform/examination/constants.js

@@ -0,0 +1,176 @@
+export const paperTypeOptions = [
+    {
+        label: '专业组考试',
+        value: '专业组考试'
+    },
+    {
+        label: '职能岗位考题',
+        value: '职能岗位考题'
+    },
+    {
+        label: 'ISO15189相关考试',
+        value: 'ISO15189相关考试'
+    }
+    // {
+    //     label: '数学题库',
+    //     value: '数学题库'
+    // }
+]
+
+export const examTypeOptions = [
+    {
+        label: '培训考试',
+        value: '培训考试'
+    },
+    {
+        label: '自主考核',
+        value: '自主考核'
+    },
+    {
+        label: '常规',
+        value: '常规'
+    }
+]
+
+export const scoringType = [
+    {
+        label: '平均分',
+        value: '平均分'
+    },
+    {
+        label: '最高分',
+        value: '最高分'
+    },
+    {
+        label: '最近得分',
+        value: '最近得分'
+    }
+]
+
+export const questionType = [
+    {
+        label: '单选题',
+        value: '单选题'
+    },
+    {
+        label: '多选题',
+        value: '多选题'
+    },
+    {
+        label: '判断题',
+        value: '判断题'
+    },
+    {
+        label: '填空题',
+        value: '填空题'
+    },
+    {
+        label: '简答题',
+        value: '简答题'
+    }
+]
+
+export const rateType = {
+    '单选题': '自动',
+    '多选题': '自动',
+    '判断题': '自动',
+    '填空题': '手动',
+    '简答题': '手动'
+}
+
+export const defaultOptions = [
+    {
+        value: 'A',
+        radio: 'A',
+        checkbox: [],
+        content: ''
+    },
+    {
+        value: 'B',
+        radio: '',
+        checkbox: [],
+        content: ''
+    },
+    {
+        value: 'C',
+        radio: '',
+        checkbox: [],
+        content: ''
+    },
+    {
+        value: 'D',
+        radio: '',
+        checkbox: [],
+        content: ''
+    }
+]
+
+export const statusOption = [
+    {
+        label: '未发布',
+        value: '未发布',
+        type: 'info'
+    },
+    {
+        label: '已取消',
+        value: '已取消',
+        type: 'warning'
+    },
+    {
+        label: '已发布',
+        value: '已发布',
+        type: 'primary'
+    },
+    {
+        label: '已结束',
+        value: '已结束',
+        type: 'success'
+    }
+]
+
+const qualifiedType = [
+    {
+        label: '达标',
+        value: '达标',
+        type: 'success'
+    },
+    {
+        label: '未达标',
+        value: '未达标',
+        type: 'danger'
+    },
+    {
+        label: '考试未结束',
+        value: '考试未结束',
+        type: 'warning'
+    }
+]
+
+export const basicColumn = [
+    { prop: 'examName', label: '考试名称', minWidth: 200 },
+    { prop: 'examType', label: '考试类型', width: 90 },
+    { prop: 'bankName', label: '考试题库', width: 160 },
+    { prop: 'trainId', label: '关联培训', width: 160 },
+    { prop: 'examState', label: '状态', width: 80, tags: statusOption },
+    { prop: 'totalScore', label: '总分', width: 65 }
+]
+
+export const infoColumn = [
+    { prop: 'questionCount', label: '题数', width: 65 },
+    { prop: 'duration', label: '考试时长', width: 85 },
+    { prop: 'limitCount', label: '限考次数', width: 85 },
+    { prop: 'limitDate', label: '限考时间', width: 120 },
+    { prop: 'isRand', label: '是否随机', slotName: 'isRand', width: 65 },
+    { prop: 'randWay', label: '随机方式', slotName: 'randWay', width: 90 }
+]
+
+export const resultColumn = [
+    { prop: 'qualifiedRadio', label: '达标分值占比', width: 75 },
+    { prop: 'scoringType', label: '计分方式', width: 85 },
+    { prop: 'examineeCount', label: '考试人数', width: 65 },
+    { prop: 'examFinishCount', label: '完考人数', width: 65 },
+    { prop: 'maxScore', label: '最高分 ', width: 70 },
+    { prop: 'minScore', label: '最低分', width: 70 },
+    { prop: 'avgScore', label: '平均分', width: 70 },
+    { prop: 'passRate', label: '达标率', width: 70 }
+]

+ 736 - 0
src/views/platform/examination/exam/detail.vue

@@ -0,0 +1,736 @@
+<template>
+    <el-dialog
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        fullscreen
+        class="dialog paper-detail-dialog"
+        top="0"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <div class="container">
+            <div class="paper-info">
+                <div class="info-item">
+                    <span class="label">考试名称:</span>
+                    <el-tooltip class="item" effect="dark" :content="paperData.examName" placement="top-start">
+                        <span class="value">{{ paperData.examName }}</span>
+                    </el-tooltip>
+                </div>
+                <div class="info-item">
+                    <span class="label">创建人:</span>
+                    <span class="value">{{ transformUser(paperData.createBy) }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">创建时间:</span>
+                    <span class="value">{{ paperData.createTime }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">发布时间:</span>
+                    <span class="value">{{ paperData.publishDate }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">限考时间:</span>
+                    <span class="value">{{ paperData.limitDate }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">考试时长:</span>
+                    <span class="value">{{ transformTime(paperData.duration) }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">限考次数:</span>
+                    <span class="value">{{ paperData.limitCount }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">题库名称:</span>
+                    <el-tooltip class="item" effect="dark" :content="paperData.bankName" placement="top-start">
+                        <span class="value">{{ paperData.bankName }}</span>
+                    </el-tooltip>
+                </div>
+                <div class="info-item">
+                    <span class="label">题目数量:</span>
+                    <span class="value">{{ paperData.isRand==='1'?paperData.randTotal:paperData.questionCount }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">总分:</span>
+                    <span class="value">{{ paperData.isRand==='1'?paperData.randScore:paperData.totalScore }}分</span>
+                </div>
+                <div class="info-item">
+                    <el-tooltip
+                        effect="dark"
+                        content="达标分值占总分的比率"
+                        placement="top"
+                    >
+                        <span class="label">达标占比:</span>
+                    </el-tooltip>
+                    <span class="value">{{ paperData.qualifiedRadio }}%</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">是否随机:</span>
+                    <span class="value">{{ paperData.isRand==='1'?'是':'否' }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">最高分:</span>
+                    <span class="value">{{ maxScore }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">最低分:</span>
+                    <span class="value">{{ minScore }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">平均分:</span>
+                    <span class="value">{{ avgScore }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">计分方式:</span>
+                    <span class="value">{{ paperData.scoringType }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">应参考人数:</span>
+                    <span class="value">{{ paperList.length }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">实参考人数:</span>
+                    <span class="value">{{ joinRate.count }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">参考率:</span>
+                    <span class="value">{{ joinRate.rate }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">达标率:</span>
+                    <span class="value">{{ passRate }}</span>
+                </div>
+            </div>
+            <div class="paper-status">
+                <div>
+                    考试状态:<el-select
+                        v-model="selectValue"
+                        placeholder="请选择查询状态"
+                        @change="onSelectChange"
+                    >
+                        <el-option
+                            v-for="item in paperOptions"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value"
+                        />
+                    </el-select>
+                </div>
+                <div class="table-bar">
+                    <el-button
+                        type="primary"
+                        size="mini"
+                        icon="ibps-icon-sign-out"
+                        @click="exportData()"
+                    >导出数据</el-button>
+                    <el-button
+                        type="success"
+                        size="mini"
+                        icon="el-icon-s-data"
+                        @click="showStatistics"
+                    >数据统计</el-button>
+                </div>
+            </div>
+            <div class="paper-table">
+                <el-table
+                    ref="elTable"
+                    :data="showPaperList"
+                    border
+                    stripe
+                    highlight-current-row
+                    header-row-class-name="exam-table-header"
+                    style="width: 100%"
+                    class="exam-table"
+                    @row-dblclick="handleRowDblclick"
+                >
+                    <el-table-column label="序号" type="index" :index="showIndex" width="50" />
+                    <el-table-column prop="userName" label="考生姓名" width="100" />
+                    <el-table-column prop="positionId" label="部门" width="100">
+                        <template slot-scope="{ row }">
+                            <ibps-user-selector
+                                type="position"
+                                :value="row.positionId"
+                                readonly-text="text"
+                                :disabled="true"
+                                :multiple="true"
+                            />
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="count" label="参考次数/完成次数" width="90" />
+                    <el-table-column prop="" label="考试成绩">
+                        <el-table-column prop="max" label="最高得分" width="90" />
+                        <el-table-column prop="min" label="最低得分" width="90" />
+                        <el-table-column prop="avg" label="平均得分" width="90" />
+                        <el-table-column prop="latest" label="最近得分" width="90" />
+                    </el-table-column>
+                    <el-table-column prop="resultScore" label="最终成绩" width="90" />
+                    <el-table-column prop="isQualified" label="是否达标">
+                        <template slot-scope="scope">
+                            <el-tag
+                                :type="getTagType(scope.row)"
+                                size="medium"
+                                class="score"
+                            >{{ scope.row.isQualified }}</el-tag>
+                        </template>
+                    </el-table-column>
+                    <!-- <el-table-column
+                        fixed="right"
+                        label="操作"
+                        width="60"
+                    >
+                        <template slot-scope="scope">
+                            <el-button type="text" size="small" @click="handleRowDblclick(scope.row)">详情</el-button>
+                        </template>
+                    </el-table-column> -->
+                </el-table>
+
+                <el-pagination
+                    style="margin-top: 5px; padding-bottom: 10px"
+                    :current-page="currentPage"
+                    :page-sizes="[10, 15, 20, 25, 30, 50]"
+                    :page-size="pageSize"
+                    layout="prev,pager,next,jumper,sizes,->,total"
+                    :total="filterPaperList.length"
+                    @size-change="handleSizeChange"
+                    @current-change="handleCurrentChange"
+                />
+            </div>
+        </div>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar :actions="toolbars" @action-event="handleActionEvent" />
+        </div>
+        <paper-detail
+            v-if="paperDialogVisible"
+            :id="paperId"
+            :visible.sync="paperDialogVisible"
+            :bank-id="bankId"
+            :exam-id="examId"
+            :examinee-id="examineeId"
+            @close="paperDialogVisible = false"
+        />
+        <Statistics
+            ref="statisticsRef"
+            :department-data="departmentData"
+        />
+    </el-dialog>
+</template>
+
+<script>
+import IbpsUserSelector from '@/business/platform/org/selector'
+// 数据统计组件
+import Statistics from './statistics.vue'
+import IbpsExport from '@/plugins/export'
+import ActionUtils from '@/utils/action'
+import { max, min, mean, sum, maxBy, minBy, meanBy, round } from 'lodash'
+const qualifiedType = [
+    {
+        value: '达标',
+        type: 'success'
+    },
+    {
+        value: '未达标',
+        type: 'danger'
+    },
+    {
+        value: '考试未结束',
+        type: 'warning'
+    }
+]
+export default {
+    components: {
+        IbpsImage: () => import('@/business/platform/file/image'),
+        PaperDetail: () => import('../questionBank/detail'),
+        IbpsUserSelector,
+        Statistics
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        bankId: {
+            type: String,
+            default: ''
+        },
+        examId: {
+            type: String,
+            default: ''
+        }
+    },
+    data () {
+        const { userList, mainPosition } = this.$store.getters || {}
+        return {
+            mainPosition,
+            userList,
+            title: '考试详情',
+            dialogVisible: this.visible,
+            paperDialogVisible: false,
+            toolbars: [{ key: 'cancel', label: '退出' }],
+            paperList: [],
+            paperData: [],
+            paperId: '',
+            examineeId: '',
+            maxScore: '',
+            minScore: '',
+            avgScore: '',
+            passRate: '',
+            paperOptions: [
+                {
+                    value: 'all',
+                    label: '全部'
+                },
+                {
+                    value: 'good',
+                    label: '达标'
+                },
+                {
+                    value: 'bad',
+                    label: '未达标'
+                },
+                {
+                    value: 'noFinish',
+                    label: '考试未结束'
+                },
+                {
+                    value: 'wait',
+                    label: '试卷待评阅'
+                }
+            ],
+            selectValue: 'all',
+            pageSize: 10,
+            currentPage: 1,
+            departmentData: [] // 数据统计
+        }
+    },
+    computed: {
+        filterPaperList () {
+            return this.paperList.filter((paper) => {
+                if (this.selectValue === 'all') return this.paperList
+                const { label } = this.paperOptions.find((option) => option.value === this.selectValue)
+                return paper.isQualified === label
+            })
+        },
+        showPaperList () {
+            const start = (this.currentPage - 1) * this.pageSize
+            const end = start + this.pageSize
+            return this.filterPaperList.slice(start, end)
+        },
+        joinRate () {
+            let count = 0
+            this.paperList.forEach((item) => {
+                if (item.count && item.count.split('/')[1] !== '0') {
+                    count++
+                }
+            })
+            return {
+                count,
+                rate: ((count / this.paperList.length) * 100).toFixed(2) + '%'
+            }
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+        }
+    },
+    mounted () {
+        this.loadData()
+        // 监听键盘事件
+        window.addEventListener('keyup', this.handleKeyPress)
+    },
+    beforeDestroy () {
+        window.removeEventListener('keyup', this.handleKeyPress)
+    },
+    methods: {
+        // 获取题库数据
+        async loadData () {
+            if (!this.examId) {
+                this.$message.error('获取题目信息失败,请重试!')
+                this.closeDialog()
+                return
+            }
+            this.paperList = await this.getQuestionData()
+            console.log(this.paperList)
+            this.paperData = this.paperList[0]
+            this.posData()
+        },
+        // 获取部门数据
+        async posData () {
+            const sql = `select p.*,r.SUB_PID_ as userId FROM IBPS_PARTY_POSITION p INNER JOIN ibps_party_rel r ON p.id_ = r.MAIN_PID_ WHERE r.BIZ_ = 'mainPost' ORDER BY p.CREATE_TIME_ DESC`
+            const { variables: { data }} = await this.$common.request('sql', sql)
+            this.paperList.forEach((item) => {
+                const mainUser = data.find(i => i.userId === item.examineeId)
+                // 如果有主部门  只统计主部门
+                if (mainUser) {
+                    this.$set(item, 'positionId', mainUser?.ID_ || '')
+                    this.$set(item, 'positionName', mainUser?.NAME_ || '')
+                } else {
+                    // 没有主部门 则统计全部
+                    const user = this.userList.find((u) => u.userId === item.examineeId)
+                    this.$set(item, 'positionId', user?.positionId || '')
+                    this.$set(item, 'positionName', user?.positions || '')
+                }
+            })
+        },
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        handleKeyPress (event) {
+            if (event.keyCode === 27 || event.key === 'Esc') {
+                this.closeDialog()
+            }
+        },
+        getTagType (row) {
+            const temp = qualifiedType.find(i => i.value === row.isQualified)
+            return temp ? temp.type : 'default'
+        },
+        transformUser (userId) {
+            const user = this.userList.find(u => u.userId === userId) || {}
+            return user.userName || '-'
+        },
+        transformTime (timeStamp) {
+            if (timeStamp === '不限' || !timeStamp) return '不限'
+            const hours = Math.floor(parseInt(timeStamp) / 3600000)
+            const minutes = Math.floor((parseInt(timeStamp) % 3600000) / 60000)
+            return hours + '小时' + minutes + '分钟'
+        },
+        getPassRate (list) {
+            const passScore = parseFloat(list[0].qualifiedRadio) / 100 * parseFloat(list[0].totalScore)
+            const passList = list.filter(i => i.resultScore >= passScore)
+            return (passList.length / list.length * 100).toFixed(2) + '%'
+        },
+        getQuestionData () {
+            const sql = `select qb.ti_ku_ming_cheng_ as bankName, ex.id_ as examId, ex.ti_ku_id_ as bankId, e.id_ as paperId, ex.zhuang_tai_ as examState, e.zhuang_tai_ as paperState, qb.ti_shu_ as questionCount, qb.zong_fen_ as totalScore, ex.kao_shi_ming_chen as examName, ex.can_kao_ren_yuan_ as examinee, e.kao_shi_ren_ as examineeId, ex.create_by_ as createBy, ex.chuang_jian_shi_j as createTime, ex.fa_bu_shi_jian_ as publishDate, ex.xian_kao_shi_jian as limitDate, ex.kao_shi_shi_chang as duration, ex.xian_kao_ci_shu_ as limitCount, ex.da_biao_zhan_bi_ as qualifiedRadio, ex.ji_fen_fang_shi_ as scoringType, ex.yun_xu_bao_ming_ as allowRegist, ex.kao_shi_miao_shu_ as examDesc,ex.sui_ji_chou_ti_ as isRand, ex.sui_ji_ti_shu_ as randNumber,ex.chou_ti_zong_fen_ as randScore,ex.ti_mu_zong_shu_ as randTotal, e.de_fen_ as score, e.bao_ming_shi_jian as applyTime, e.kai_shi_shi_jian_ as startTime, e.jie_shu_shi_jian_ as endTime from t_exams ex, t_question_bank qb, t_examination e where ex.ti_ku_id_ = qb.id_ and e.exam_id_ = ex.id_ and ex.id_ = '${this.examId}' order by e.kao_shi_ren_ desc, e.jie_shu_shi_jian_ desc`
+            return new Promise((resolve, reject) => {
+                this.$common.request('sql', sql).then(res => {
+                    const { data = [] } = res.variables || {}
+                    if (!data.length) {
+                        this.$message.warning('未查询到已提交的考试记录,请先完成考试!')
+                        this.closeDialog()
+                        return
+                    }
+                    const result = []
+                    const scorrType = {
+                        '最高分': 'max',
+                        '平均分': 'avg',
+                        '最近得分': 'latest'
+                    }
+                    data.forEach(item => {
+                        const index = result.findIndex(i => i.examineeId === item.examineeId)
+                        if (index === -1) {
+                            result.push({
+                                ...item,
+                                totalCount: data.length,
+                                totalScore: item.isRand === '1' ? parseFloat(item.randScore) : parseFloat(item.totalScore),
+                                statusList: [item.paperState],
+                                scoreList: [parseFloat(item.score || -1)]
+                            })
+                        } else {
+                            result[index].scoreList.push(parseFloat(item.score || -1))
+                            result[index].statusList.push(item.paperState)
+                        }
+                    })
+                    const nodatadesc = '/'
+                    result.forEach((item, index) => {
+                        const finishScore = item.scoreList.filter(i => i !== -1)
+                        item.userName = this.transformUser(item.examineeId)
+                        item.max = finishScore.length ? round(max(finishScore), 2) : nodatadesc
+                        item.min = finishScore.length ? round(min(finishScore), 2) : nodatadesc
+                        item.avg = finishScore.length ? round(mean(finishScore), 2) : nodatadesc
+                        item.sum = finishScore.length ? round(sum(finishScore), 2) : nodatadesc
+                        item.latest = finishScore.length ? round(finishScore[0], 2) : nodatadesc
+                        item.examCount = item.scoreList.length
+                        item.finishCount = finishScore.length
+                        item.count = `${item.examCount}/${item.finishCount}`
+                        // 修改考试结束的判断逻辑:只要有一次交卷记录即为已完成
+                        item.examStatus = item.finishCount > 0 ? '已完成' : '未完成'
+                        item.resultScore = item[scorrType[item.scoringType]]
+                        item.isQualified = item.examStatus === '已完成' ? item.resultScore >= (parseFloat(item.qualifiedRadio) / 100 * parseFloat(item.totalScore)) ? '达标' : '未达标' : '考试未结束'
+                        if (item.isQualified === '考试未结束') {
+                            // 细化考试未结束的情况
+                            if (item.statusList.includes('已交卷')) {
+                                // 有已交卷记录 试卷等待评阅
+                                item.isQualified = '试卷待评阅'
+                            }
+                        }
+                    })
+                    const finishList = result.filter(i => i.examStatus === '已完成')
+                    this.maxScore = finishList.length ? maxBy(finishList, 'max').max : nodatadesc
+                    this.minScore = finishList.length ? minBy(finishList, 'min').min : nodatadesc
+                    this.avgScore = finishList.length ? meanBy(finishList, 'avg').toFixed(2) : nodatadesc
+                    this.passRate = finishList.length ? this.getPassRate(finishList) : nodatadesc
+                    resolve(result)
+                }).catch(error => {
+                    reject(error)
+                })
+            })
+        },
+        handleRowDblclick (row) {
+            this.paperId = row.paperId
+            this.examineeId = row.examineeId
+            this.paperDialogVisible = true
+        },
+        // 关闭当前窗口
+        closeDialog () {
+            window.removeEventListener('keyup', this.handleKeyPress)
+            this.$emit('close', false)
+        },
+        // 选择器状态改变
+        onSelectChange (select) {
+            this.pageSize = 10
+            this.currentPage = 1
+        },
+        // 当前页码改变
+        handleCurrentChange (val) {
+            this.currentPage = val
+        },
+        // 页码选择器改变
+        handleSizeChange (val) {
+            this.pageSize = val
+            this.currentPage = 1
+        },
+        // 分页连续序号
+        showIndex (index) {
+            return index + 1 + (this.currentPage - 1) * this.pageSize
+        },
+        // 部门参考人数
+        deptJoined (dept) {
+            return this.paperList.filter(paper => paper.positionName.split(',').includes(dept)).length
+        },
+        // 部门已考人数
+        deptFinished (dept) {
+            return this.paperList.filter(paper => paper.positionName.split(',').includes(dept) && paper.finishCount !== 0).length
+        },
+        // 部门达标人数
+        deptPassRate (dept) {
+            return this.paperList.filter(paper => paper.positionName.split(',').includes(dept) && paper.isQualified === '达标').length
+        },
+        // 部门分数,最高分、最低分、总分
+        deptScore (dept) {
+            const validPapers = this.paperList.filter(paper => paper.positionName.split(',').includes(dept) && paper.resultScore !== '/')
+            const scores = validPapers.map(paper => +paper.resultScore)
+            return {
+                max: Math.max(...scores, 0),
+                min: scores.length ? Math.min(...scores) : 0,
+                sum: validPapers.reduce((sum, paper) => sum + +paper.resultScore, 0)
+            }
+        },
+        departmentMinScore (dept) {
+            const validPapers = this.paperList.filter(paper => paper.positionName.split(',').includes(dept) && paper.resultScore !== '/')
+            const scores = validPapers.map(paper => +paper.resultScore)
+            return scores.length ? Math.min(...scores) : 0
+        },
+        // 数据统计
+        showStatistics () {
+            let deptList = this.paperList.map(paper => paper.positionName).join(',').split(',')
+            deptList = [...new Set(deptList)]
+            this.departmentData = deptList.map(item => {
+                const plannedAttendees = this.deptJoined(item)
+                const actualAttendees = this.deptFinished(item)
+                const PassPeople = this.deptPassRate(item)
+                const score = this.deptScore(item)
+
+                return {
+                    department: item,
+                    plannedAttendees,
+                    actualAttendees,
+                    joinRate: actualAttendees / plannedAttendees,
+                    minScore: score.min,
+                    maxScore: score.max,
+                    passRate: actualAttendees === 0 ? 0 : PassPeople / actualAttendees,
+                    avgScore: actualAttendees === 0 ? 0 : score.sum / actualAttendees
+                }
+            })
+            this.$refs.statisticsRef.open()
+        },
+        // 导出统计数据
+        exportData () {
+            const exportColumns = [
+                {
+                    field_name: 'userName',
+                    label: '考生姓名',
+                    name: 'userName'
+                },
+                {
+                    field_name: 'positionName',
+                    label: '部门',
+                    name: 'positionName'
+                },
+                {
+                    field_name: 'examCount',
+                    label: '参考次数',
+                    name: 'examCount'
+                },
+                {
+                    field_name: 'finishCount',
+                    label: '完成次数',
+                    name: 'finishCount'
+                },
+                {
+                    field_name: 'max',
+                    label: '最高得分',
+                    name: 'max'
+                },
+                {
+                    field_name: 'min',
+                    label: '最低得分',
+                    name: 'min'
+                },
+                {
+                    field_name: 'avg',
+                    label: '平均得分',
+                    name: 'avg'
+                },
+                {
+                    field_name: 'latest',
+                    label: '最近得分',
+                    name: 'latest'
+                },
+                {
+                    field_name: 'resultScore',
+                    label: '最终成绩',
+                    name: 'resultScore'
+                },
+                {
+                    field_name: 'isQualified',
+                    label: '是否达标',
+                    name: 'isQualified'
+                }
+            ]
+            this.handleExport(exportColumns, this.filterPaperList, `考试【${this.paperData.examName}】统计数据`)
+        },
+        handleExport (columns, data, title, message, nameKey = 'name') {
+            IbpsExport.excel({
+                columns: columns,
+                data: data,
+                nameKey: nameKey,
+                title: title
+            }).then(() => {
+                const msg = message || '导出成功'
+                ActionUtils.success(msg)
+            })
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .paper-detail-dialog {
+        ::v-deep {
+            .el-dialog__body {
+                position: relative;
+                height: calc(100vh - 115px);
+                width: 1080px;
+                margin: 0 auto;
+                box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+                .el-radio, .el-checkbox {
+                    display: block;
+                    margin-bottom: 10px;
+                    pointer-events: none;
+                    &:last-child {
+                        margin-bottom: 0;
+                    }
+                }
+                .el-radio-button {
+                    pointer-events: none;
+                }
+                .el-radio__label, .el-checkbox__label {
+                    font-size: 16px;
+                    white-space: normal;
+                    line-height: 1.5;
+                }
+                .el-input {
+                    margin-bottom: 10px;
+                    pointer-events: none;
+                    &:last-child {
+                        margin-bottom: 0;
+                    }
+                }
+                .el-tag {
+                    margin-right: 0;
+                }
+                .ibps-p-0 {
+                    margin: 0;
+                    .list-group {
+                        display: inline-block;
+                        width: 100%;
+                    }
+                }
+            }
+            .el-dialog__header {
+                text-align: center;
+            }
+        }
+        .container {
+            position: relative;
+            height: calc(100vh - 160px);
+            padding: 20px;
+            .paper-info {
+                display: flex;
+                flex-wrap: wrap;
+                justify-content: flex-start;
+                margin-bottom: 20px;
+                background-color: #f5f5f5;
+                border: 1px solid #ddd;
+                padding: 10px;
+                border-radius: 5px;
+                font-size: 16px;
+            }
+
+            .info-item {
+                width: 33.3%;
+                margin-bottom: 20px;
+                display: flex;
+                align-items: center;
+                .label {
+                    width: 100px;
+                    font-weight: bold;
+                    color: #333;
+                }
+                .value {
+                    flex: 1;
+                    color: #666;
+                    white-space: nowrap;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                }
+            }
+
+            .paper-table {
+                .exam-table-header {
+                    text-align: center;
+                }
+                ::v-deep {
+                    .el-table {
+                        th {
+                            font-size: 14px !important;
+                            text-align: center;
+                        }
+                        td {
+                            font-size: 14px !important;
+                        }
+                        .el-table__row {
+                            cursor: pointer;
+                        }
+                    }
+                }
+            }
+            .paper-status {
+                margin-bottom: 10px;
+                display: flex;
+                justify-content: space-between;
+            }
+        }
+    }
+</style>

+ 993 - 0
src/views/platform/examination/exam/edit.vue

@@ -0,0 +1,993 @@
+<template>
+    <el-dialog
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        append-to-body
+        width="60%"
+        class="dialog exam-dialog"
+        top="6vh"
+        @close="closeDialog"
+        @open="getExamData"
+    >
+        <el-form
+            ref="form"
+            :label-width="formLabelWidth"
+            :model="form"
+            :rules="rules"
+            class="exam-form"
+            @submit.native.prevent
+        >
+            <el-form-item label="考试名称:" prop="kao_shi_ming_chen">
+                <el-input
+                    v-model="form.kao_shi_ming_chen"
+                    type="text"
+                    :maxlength="256"
+                    placeholder="请输入考试名称"
+                    :disabled="isDisabled"
+                />
+            </el-form-item>
+            <!-- <el-form-item label="考试类型:" prop="kao_shi_lei_xing_">
+                <el-select
+                    v-model="form.kao_shi_lei_xing_"
+                    filterable
+                    allow-create
+                    width="100%"
+                    placeholder="请选择考试类型"
+                >
+                    <el-option
+                        v-for="item in examTypeOptions"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                    />
+                </el-select>
+            </el-form-item> -->
+            <el-form-item label="考试题库:">
+                <template slot="label">
+                    考试题库
+                    <el-tooltip
+                        effect="dark"
+                        content="如果选择多个题库或手动添加选题,最终生成的考试将是所选题目的总和,并将自动生成一个组合题库。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <div class="tiku">
+                    <div class="item">
+                        <ibps-custom-dialog
+                            v-model="allQuestions.tiKu"
+                            size="small"
+                            template-key="tkdhk"
+                            :multiple="true"
+                            :disabled="isDisabled"
+                            type="dialog"
+                            class="custom-dialog"
+                            placeholder="请选择考试题库"
+                            style="width:90%"
+                        />
+                        <div class="number">题数:{{ allQuestionsNumber.tiKuNumber }}</div>
+                    </div>
+                </div>
+            </el-form-item>
+            <el-form-item label="手动选题:">
+                <template slot="label">
+                    手动选题
+                    <el-tooltip
+                        effect="dark"
+                        content="如果选择多个题库或手动添加选题,最终生成的考试将是所选题目的总和,并将自动生成一个组合题库。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <div class="tiku">
+                    <div class="item">
+                        <ibps-custom-dialog
+                            v-model="allQuestions.handQuestions"
+                            size="small"
+                            template-key="tmlb"
+                            multiple
+                            type="dialog"
+                            class="custom-dialog"
+                            placeholder="请选择考试题目"
+                            style="width:90%"
+                        />
+                        <div class="number">题数:{{ allQuestions.handQuestions?allQuestions.handQuestions.split(',').length:0 }}</div>
+                        <div class="number" style="margin-left:-20px">去重后:{{ allQuestionsNumber.handQuestionsNumber }}</div>
+                    </div>
+                </div>
+            </el-form-item>
+            <el-form-item>
+                <template slot="label">
+                    随机抽题
+                    <el-tooltip
+                        effect="dark"
+                        content="是否开启从题库中随机抽题。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.sui_ji_chou_ti_" :disabled="isDisabled">
+                    <el-radio label="0">关闭</el-radio>
+                    <el-radio label="1" :disabled="allQuestionsNumber.tiKuNumber+allQuestionsNumber.handQuestionsNumber==0">开启</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item v-if="form.sui_ji_chou_ti_ === '1'">
+                <template slot="label">
+                    抽题方式
+                    <el-tooltip
+                        effect="dark"
+                        content="随机抽题的依据。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.chou_ti_fang_shi_" :disabled="isDisabled">
+                    <el-radio label="1">按照题型分类抽题</el-radio>
+                    <el-radio label="2">按照难度分类抽题</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item v-if="form.sui_ji_chou_ti_ === '1'" class="handrand">
+                <template slot="label">
+                    抽题规则
+                    <el-tooltip
+                        effect="dark"
+                        :content="ruleContent"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <template v-if="form.chou_ti_fang_shi_==='1'">
+                    <div v-for="item in handList" :key="item.itemType" class="hand-item">
+                        <template v-if="item.list.length>0">
+                            <span>{{ item.itemType }}:</span>
+                            <el-input-number
+                                v-model="item.questionNumber"
+                                style="width:120px"
+                                :min="0"
+                                :max="item.list.length"
+                                :precision="0"
+                                :disabled="form.sui_ji_chou_ti_ === '0' || isDisabled"
+                                placeholder="请输入抽题数量"
+                                :step="item.step"
+                                step-strictly
+                            />
+                        </template>
+                    </div>
+                </template>
+                <template v-if="form.chou_ti_fang_shi_==='2'">
+                    <div v-for="item in levelList" :key="item.itemType" class="hand-item">
+                        <template v-if="item.list.length>0">
+                            <span>{{ item.label }}:</span>
+                            <el-input-number
+                                v-model="item.questionNumber"
+                                style="width:120px"
+                                :min="0"
+                                :max="item.list.length"
+                                :precision="0"
+                                :disabled="form.sui_ji_chou_ti_ === '0' || isDisabled"
+                                placeholder="请输入抽题数量"
+                                :step="item.step"
+                                step-strictly
+                            />
+                        </template>
+                    </div>
+                </template>
+
+            </el-form-item>
+            <el-form-item prop="xian_kao_shi_jian">
+                <template slot="label">
+                    限考时间
+                    <el-tooltip
+                        effect="dark"
+                        content="设置该考试的提交截至时间。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group
+                    v-model="form.isDateLimit"
+                    @change="changeLimit(form.isDateLimit, 'xian_kao_shi_jian', null)"
+                >
+                    <el-radio label="0">不限</el-radio>
+                    <el-radio label="1">限制</el-radio>
+                </el-radio-group>
+                <el-date-picker
+                    v-if="form.isDateLimit === '1'"
+                    v-model="form.xian_kao_shi_jian"
+                    type="datetime"
+                    placeholder="请选择考试限制提交时间"
+                    align="right"
+                    default-time="12:00:00"
+                    class="date-picker"
+                    :picker-options="pickerOptions"
+                />
+            </el-form-item>
+            <el-form-item label="参考人员:" prop="can_kao_ren_yuan_">
+                <ibps-custom-dialog
+                    v-model="form.can_kao_ren_yuan_"
+                    size="small"
+                    template-key="gjxcxryk_copy"
+                    multiple
+                    :disabled="isDisabled"
+                    type="dialog"
+                    class="custom-dialog"
+                    placeholder="请选择需参加考试的人员"
+                />
+            </el-form-item>
+            <el-form-item prop="ping_fen_ren_">
+                <template slot="label">
+                    评分人
+                    <el-tooltip effect="dark" content="设置该考试的评分人,选择的评分人与题目评分人都可以进行试题评阅" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <!-- <el-cascader
+                    v-model="form.ping_fen_ren_"
+                    :options="getRaterOptions()"
+                    :show-all-levels="false"
+                    collapse-tags
+                    :props="{
+                        value: 'value',
+                        label: 'label',
+                        multiple: true,
+                        checkStrictly: false
+                    }"
+                /> -->
+                <el-select
+                    v-model="form.ping_fen_ren_"
+                    filterable
+                    style="width:100%"
+                    clearable
+                    multiple
+                    :multiple-limit="16"
+                    :disabled="isDisabled"
+                    placeholder="请选择评分人"
+                >
+                    <el-option
+                        v-for="item in userList"
+                        :key="item.userId"
+                        :label="item.userName"
+                        :value="item.userId"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item prop="isCountLimit" class="inline-item">
+                <template slot="label">
+                    限考次数
+                    <el-tooltip
+                        effect="dark"
+                        content="限制是否可重复参加,以及可参加考试的最大次数。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group
+                    v-model="form.isCountLimit"
+                    :disabled="isDisabled"
+                    @change="changeLimit(form.isCountLimit, 'xian_kao_ci_shu_', 1)"
+                >
+                    <el-radio label="0">不限</el-radio>
+                    <el-radio label="1">限制</el-radio>
+                </el-radio-group>
+                <div v-if="form.isCountLimit === '1'" class="time">
+                    <el-input-number
+                        v-model="form.xian_kao_ci_shu_"
+                        :min="1"
+                        :precision="0"
+                        :disabled="isDisabled"
+                        placeholder="请输入单个用户最大限考次数"
+                    />
+                    <div class="unit">次</div>
+                </div>
+            </el-form-item>
+            <el-form-item v-if="form.isCountLimit === '0' || form.isCountLimit === '1' && form.xian_kao_ci_shu_ > 1" prop="ji_fen_fang_shi_">
+                <template slot="label">
+                    计分方式
+                    <el-tooltip
+                        effect="dark"
+                        content="设置对于可重复参加的考试,最终得分的计算方式(仅限考次数大于1时有效)。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.ji_fen_fang_shi_" :disabled="isDisabled">
+                    <el-radio label="平均分">平均分</el-radio>
+                    <el-radio label="最高分">最高分</el-radio>
+                    <el-radio label="最近得分">最近得分</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item prop="isTimeLimit" class="inline-item">
+                <template slot="label">
+                    考试时长
+                    <el-tooltip
+                        effect="dark"
+                        content="设置该考试单次作答的最大时长。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.isTimeLimit" :disabled="isDisabled">
+                    <el-radio label="0">不限</el-radio>
+                    <el-radio label="1">限制</el-radio>
+                </el-radio-group>
+                <template v-if="form.isTimeLimit === '1'">
+                    <div class="time">
+                        <el-input-number
+                            v-model="form.hours"
+                            :min="0"
+                            :max="72"
+                            :precision="0"
+                            :disabled="isDisabled"
+                        />
+                        <div class="unit">小时</div>
+                    </div>
+                    <div class="time">
+                        <el-input-number
+                            v-model="form.minutes"
+                            :min="0"
+                            :max="59"
+                            :precision="0"
+                            :disabled="isDisabled"
+                        />
+                        <div class="unit">分钟</div>
+                    </div>
+                </template>
+            </el-form-item>
+            <el-form-item prop="da_biao_zhan_bi_">
+                <template slot="label">
+                    达标分值占比
+                    <el-tooltip
+                        effect="dark"
+                        content="设置该考试的达标分数线占试题总分的百分比。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-input-number
+                    v-model="form.da_biao_zhan_bi_"
+                    :min="50"
+                    :max="100"
+                    :precision="0"
+                    :disabled="isDisabled"
+                    placeholder="请输入达标分值占比"
+                />
+                <div class="unit">%</div>
+            </el-form-item>
+            <el-form-item prop="yun_xu_bao_ming_">
+                <template slot="label">
+                    允许自主报名
+                    <el-tooltip
+                        effect="dark"
+                        content="限制非本考试的参考人员是否可报名参加该考试。"
+                        placement="top"
+                    >
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.yun_xu_bao_ming_" :disabled="isDisabled">
+                    <el-radio label="是">是</el-radio>
+                    <el-radio label="否">否</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item v-if="!isDisabled || form.guan_lian_id_" prop="guan_lian_id_">
+                <template slot="label">
+                    关联培训记录
+                    <el-tooltip effect="dark" content="关联培训记录。" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <ibps-custom-dialog
+                    v-model="form.guan_lian_id_"
+                    size="small"
+                    template-key="pxjldhk"
+                    :multiple="false"
+                    :disabled="isDisabled"
+                    type="dialog"
+                    class="custom-dialog"
+                    placeholder="请选择培训记录"
+                />
+            </el-form-item>
+            <el-form-item
+                v-if="!isDisabled || form.kao_shi_miao_shu_"
+                label="考试描述:"
+                prop="kao_shi_miao_shu_"
+            >
+                <el-input
+                    v-model="form.kao_shi_miao_shu_"
+                    type="textarea"
+                    :rows="4"
+                    :maxlength="512"
+                    placeholder="请输入描述内容"
+                    :disabled="isDisabled"
+                />
+            </el-form-item>
+        </el-form>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar :actions="toolbars" @action-event="handleActionEvent" />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import dayjs from 'dayjs'
+import ActionUtils from '@/utils/action'
+import { examTypeOptions, scoringType } from '../constants'
+export default {
+    components: {
+        IbpsCustomDialog: () => import('@/business/platform/data/templaterender/custom-dialog')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        id: {
+            type: String,
+            default: ''
+        },
+        isDisabled: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const {
+            userList = [],
+            deptList = [],
+            userId,
+            level
+        } = this.$store.getters || {}
+        return {
+            level: level.second || level.first,
+            userId: userId,
+            allQuestions: {
+                handQuestions: '',
+                tiKu: ''
+            },
+            allQuestionsNumber: {
+                handQuestionsNumber: 0,
+                tiKuNumber: 0
+            },
+            curQuestions: [],
+            handList: [
+                { itemType: '单选题', questionNumber: 0, list: [], step: 1 },
+                { itemType: '多选题', questionNumber: 0, list: [], step: 1 },
+                { itemType: '判断题', questionNumber: 0, list: [], step: 1 },
+                { itemType: '填空题', questionNumber: 0, list: [], step: 1 },
+                { itemType: '简答题', questionNumber: 0, list: [], step: 1 }
+            ],
+            levelList: [
+                { label: '未知', itemType: '0', questionNumber: 0, list: [], step: 1 },
+                { label: '易', itemType: '1', questionNumber: 0, list: [], step: 1 },
+                { label: '偏易', itemType: '2', questionNumber: 0, list: [], step: 1 },
+                { label: '适中', itemType: '3', questionNumber: 0, list: [], step: 1 },
+                { label: '偏难', itemType: '4', questionNumber: 0, list: [], step: 1 },
+                { label: '难', itemType: '5', questionNumber: 0, list: [], step: 1 }
+            ],
+            userList,
+            examTypeOptions,
+            deptList: deptList.filter((i) => i.depth === 4),
+            title: this.id ? '编辑考试' : '新建考试',
+            formLabelWidth: '150px',
+            dialogVisible: this.visible,
+            dialogLoading: false,
+            randButtonDisabled: false,
+            isFirst: true,
+            form: {
+                di_dian_: level.second || level.first,
+                chuang_jian_shi_j: '',
+                kao_shi_ming_chen: '',
+                kao_shi_lei_xing_: '常规',
+                ti_ku_id_: '',
+                guan_lian_id_: '',
+                xian_kao_shi_jian: '不限',
+                xian_kao_ci_shu_: '不限',
+                can_kao_ren_yuan_: '',
+                kao_shi_shi_chang: '',
+                zhuang_tai_: '未发布',
+                da_biao_zhan_bi_: 60,
+                ji_fen_fang_shi_: scoringType.length ? scoringType[0].value : '',
+                kao_shi_miao_shu_: '',
+                yun_xu_bao_ming_: '否',
+                isCountLimit: '0',
+                isTimeLimit: '0',
+                isDateLimit: '0',
+                sui_ji_chou_ti_: '0',
+                chou_ti_zong_fen_: 0,
+                ti_mu_zong_shu_: 0,
+                sui_ji_ti_shu_: [],
+                hours: 2,
+                minutes: 30,
+                chou_ti_fang_shi_: '1',
+                ping_fen_ren_: ''
+            },
+            pickerOptions: {
+                disabledDate (time) {
+                    // 禁用当前日期之前的日期
+                    return time.getTime() < Date.now() - 8.64e7
+                }
+            },
+            toolbars: [
+                {
+                    key: 'submit',
+                    icon: 'ibps-icon-save',
+                    label: '保存',
+                    hidden: () => {
+                        return this.readonly
+                    }
+                },
+                { key: 'cancel', label: '关闭' }
+            ],
+            rules: {
+                kao_shi_ming_chen: [
+                    { required: true, message: this.$t('validate.required') }
+                ],
+                // kao_shi_lei_xing_: [{ required: true, message: this.$t('validate.required') }],
+                ti_ku_id_: [{ required: true, message: this.$t('validate.required') }],
+                xian_kao_shi_jian: [
+                    { required: true, message: this.$t('validate.required') }
+                ],
+                xian_kao_ci_shu_: [
+                    { required: true, message: this.$t('validate.required') }
+                ],
+                can_kao_ren_yuan_: [
+                    { required: true, message: this.$t('validate.required') }
+                ],
+                kao_shi_shi_chang: [
+                    { required: true, message: this.$t('validate.required') }
+                ],
+                da_biao_zhan_bi_: [
+                    { required: true, message: this.$t('validate.required') }
+                ]
+            }
+        }
+    },
+    computed: {
+        ruleContent () {
+            switch (this.form.chou_ti_fang_shi_) {
+                case '1':
+                    return '设置每类题型的抽题数量,相同题型相同分值可以自由选择抽题数量,相同题型不同分值只能全抽或不抽。'
+                case '2':
+                    return '设置每种难度的抽题数量,相同难度相同分值可以自由选择抽题数量,相同难度不同分值只能全抽或不抽。未设置难度的题目会自动过滤。'
+            }
+            return '设置每类题型的抽题数量,相同题型相同分值可以自由选择抽题数量,相同题型不同分值只能全抽或不抽。'
+        }
+    },
+    watch: {
+        visible: {
+            handler: function (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+            // immediate: true
+        },
+        'allQuestions': {
+            handler (val) {
+                const allRequests = []
+                const { tiKu, handQuestions } = val
+                if (tiKu) {
+                    const sql = `select * from t_questions where parent_id_ in (${tiKu.split(',').map(i => `'${i}'`)}) and zhuang_tai_ = '启用' order by field(ti_xing_, '单选题', '多选题', '判断题', '填空题', '简答题')`
+                    allRequests.push(this.$common.request('sql', sql))
+                }
+                if (handQuestions) {
+                    let sql = ''
+                    if (tiKu) {
+                        sql = `select * from t_questions where id_ in (${handQuestions.split(',').map(i => `'${i}'`)}) and parent_id_ not in (${tiKu.split(',').map(i => `'${i}'`)}) and zhuang_tai_ = '启用' order by field(ti_xing_, '单选题', '多选题', '判断题', '填空题', '简答题')`
+                    } else {
+                        sql = `select * from t_questions where id_ in (${handQuestions.split(',').map(i => `'${i}'`)}) and zhuang_tai_ = '启用' order by field(ti_xing_, '单选题', '多选题', '判断题', '填空题', '简答题')`
+                    }
+
+                    allRequests.push(this.$common.request('sql', sql))
+                }
+                if (allRequests.length) {
+                    Promise.all(allRequests).then(res => {
+                        const { data: data1 = [] } = res?.[0]?.variables || {}
+                        const { data: data2 = [] } = res?.[1]?.variables || {}
+                        if (!this.allQuestions.tiKu) {
+                            this.allQuestionsNumber.handQuestionsNumber = data1.length
+                        } else if (!this.allQuestions.handQuestions) {
+                            this.allQuestionsNumber.tiKuNumber = data1.length
+                        } else {
+                            this.allQuestionsNumber.tiKuNumber = data1.length
+                            this.allQuestionsNumber.handQuestionsNumber = data2.length
+                        }
+                        this.curQuestions = [...data1, ...data2]
+                        this.getHandList()
+                    }).catch(err => {
+                        console.error(err)
+                    })
+                } else {
+                    this.allQuestionsNumber.tiKuNumber = 0
+                    this.allQuestionsNumber.handQuestionsNumber = 0
+                    this.form.sui_ji_chou_ti_ = '0'
+                    this.curQuestions = []
+                    this.getHandList()
+                }
+            },
+            deep: true
+        }
+    },
+    mounted () {
+        this.getExamData()
+    },
+    methods: {
+        // 分类计算
+        getHandList () {
+            const data = this.curQuestions
+            // 确定每种类型的题目
+            this.handList.forEach(hand => {
+                hand.list = data.filter(item => item.ti_xing_ === hand.itemType)
+            })
+            // 确定每种难度的题目
+            this.levelList.forEach(level => {
+                level.list = data.filter(item => item.nan_du_ === level.itemType)
+            })
+
+            if (this.$utils.isEmpty(this.id)) {
+                // 设置每种类型题目的默认题数
+                this.handList.forEach(hand => {
+                    hand.questionNumber = hand.list.length
+                })
+                // 设置每种难度题目的默认题数
+                this.levelList.forEach(level => {
+                    level.questionNumber = level.list.length
+                })
+            } else {
+                if (!this.isFirst) {
+                    this.handList.forEach(hand => {
+                        hand.questionNumber = hand.list.length
+                    })
+                    this.levelList.forEach(level => {
+                        level.questionNumber = level.list.length
+                    })
+                } else {
+                    // 根据当前的抽题方式给另外一种抽题方式填充默认值
+                    this.isFirst = false
+                    if (this.form.chou_ti_fang_shi_ === '1') {
+                        this.levelList.forEach(level => {
+                            level.questionNumber = level.list.length
+                        })
+                    } else if (this.form.chou_ti_fang_shi_ === '2') {
+                        this.handList.forEach(hand => {
+                            hand.questionNumber = hand.list.length
+                        })
+                    }
+                }
+            }
+
+            // 设置每种类型题目的步数
+            this.handList.forEach(hand => {
+                if (hand.list.length > 0) {
+                    const isSame = hand.list.every(item => item.fen_zhi_ === hand.list[0].fen_zhi_)
+                    hand.step = isSame ? 1 : hand.list.length
+                }
+            })
+            // 设置每种难度题目的步数
+            this.levelList.forEach(level => {
+                if (level.list.length > 0) {
+                    const isSame = level.list.every(item => item.fen_zhi_ === level.list[0].fen_zhi_)
+                    level.step = isSame ? 1 : level.list.length
+                }
+            })
+        },
+        changeLimit (e, type, defaultValue) {
+            this.form[type] = e === '1' ? defaultValue : '不限'
+        },
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'submit':
+                    this.handleSubmit()
+                    break
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        // 获取考试数据
+        getExamData () {
+            if (this.$utils.isEmpty(this.id)) {
+                return
+            }
+            if (this.isDisabled) {
+                this.randButtonDisabled = true
+                this.$message.info('非未发布状态的考试仅可修改限考时间!')
+            }
+            const sql = `select id_, create_by_, ti_ku_id_, guan_lian_id_, kao_shi_ming_chen, kao_shi_lei_xing_, chuang_jian_shi_j, fa_bu_shi_jian_, fa_bu_ren_, xian_kao_shi_jian, xian_kao_ci_shu_, kao_shi_shi_chang, can_kao_ren_yuan_, zhuang_tai_, da_biao_zhan_bi_, ji_fen_fang_shi_, kao_shi_miao_shu_, yun_xu_bao_ming_, sui_ji_chou_ti_, sui_ji_ti_shu_,chou_ti_zong_fen_,ti_mu_zong_shu_,chou_ti_fang_shi_,ping_fen_ren_ from t_exams where id_ = '${this.id}'`
+            this.$common.request('sql', sql).then((res) => {
+                const { data = [] } = res.variables || {}
+                if (!data.length) {
+                    this.$message.error('数据不存在')
+                    return
+                }
+                data[0].isCountLimit = data[0].xian_kao_ci_shu_ === '不限' ? '0' : '1'
+                data[0].isDateLimit = data[0].xian_kao_shi_jian === '不限' ? '0' : '1'
+                if (data[0].kao_shi_shi_chang === '不限') {
+                    data[0].isTimeLimit = '0'
+                    data[0].hours = null
+                    data[0].minutes = null
+                } else {
+                    data[0].isTimeLimit = '1'
+                    data[0].hours = Math.floor(data[0].kao_shi_shi_chang / (1000 * 60 * 60))
+                    data[0].minutes = (data[0].kao_shi_shi_chang % (1000 * 60 * 60)) / (60 * 1000)
+                }
+                if (data[0].ping_fen_ren_) {
+                    data[0].ping_fen_ren_ = data[0].ping_fen_ren_.split(',') || []
+                }
+                this.form = data[0]
+                if (this.form.sui_ji_chou_ti_ === '1') {
+                    this.form.sui_ji_ti_shu_ = this.form.sui_ji_ti_shu_.split(',')
+                    if (this.form.chou_ti_fang_shi_ === '1') {
+                        this.form.sui_ji_ti_shu_.forEach((item, index) => {
+                            this.handList[index].questionNumber = item
+                        })
+                    } else if (this.form.chou_ti_fang_shi_ === '2') {
+                        this.form.sui_ji_ti_shu_.forEach((item, index) => {
+                            this.levelList[index].questionNumber = item
+                        })
+                    }
+                }
+                this.allQuestions.tiKu = this.form.ti_ku_id_
+            })
+        },
+        // 随机题库 弃用
+        randTiku () {
+            this.randButtonDisabled = true
+            const sql = `select id_ from t_question_bank where ti_ku_zhuang_tai_='可用'`
+            this.$common.request('sql', sql).then((res) => {
+                const { data = [] } = res.variables || {}
+                const randNumber = Math.floor(Math.random() * data.length)
+                this.form.ti_ku_id_ = data[randNumber].id_
+                this.randButtonDisabled = false
+            })
+        },
+        handleSubmit () {
+            this.$refs.form.validate(async (valid) => {
+                if (valid) {
+                    if (this.allQuestionsNumber.tiKuNumber + this.allQuestionsNumber.handQuestionsNumber === 0) {
+                        return this.$message.warning('请先选择题目!')
+                    }
+                    this.form.sui_ji_ti_shu_ = []
+                    this.form.chou_ti_zong_fen_ = 0
+                    this.form.ti_mu_zong_shu_ = 0
+                    if (this.form.sui_ji_chou_ti_ === '1') {
+                        // 计算总分 总数量
+                        if (this.form.chou_ti_fang_shi_ === '1') {
+                            this.handList.forEach(item => {
+                                if (item.list.length > 0) {
+                                    this.form.sui_ji_ti_shu_.push(item.questionNumber || 0)
+                                    this.form.ti_mu_zong_shu_ += item.questionNumber || 0
+                                    this.form.chou_ti_zong_fen_ += item.list.slice(0, item.questionNumber || 0).reduce((pre, cur) => {
+                                        return pre + +cur.fen_zhi_
+                                    }, 0)
+                                } else {
+                                    this.form.sui_ji_ti_shu_.push(0)
+                                }
+                            })
+                        } else if (this.form.chou_ti_fang_shi_ === '2') {
+                            this.levelList.forEach(item => {
+                                if (item.list.length > 0) {
+                                    this.form.sui_ji_ti_shu_.push(item.questionNumber || 0)
+                                    this.form.ti_mu_zong_shu_ += item.questionNumber || 0
+                                    this.form.chou_ti_zong_fen_ += item.list.slice(0, item.questionNumber || 0).reduce((pre, cur) => {
+                                        return pre + +cur.fen_zhi_
+                                    }, 0)
+                                } else {
+                                    this.form.sui_ji_ti_shu_.push(0)
+                                }
+                            })
+                        }
+
+                        if (this.form.ti_mu_zong_shu_ === 0) {
+                            return this.$message.warning('请填写需要抽取的题目数量!')
+                        }
+                    }
+                    this.form.sui_ji_ti_shu_ = this.form.sui_ji_ti_shu_.join(',')
+                    this.form.ping_fen_ren_ = this.form.ping_fen_ren_ ? this.form.ping_fen_ren_.join(',') : ''
+
+                    const { isTimeLimit, xian_kao_shi_jian = '' } = this.form || {}
+                    // 转换考试时长
+                    if (isTimeLimit === '0') {
+                        this.form.kao_shi_shi_chang = '不限'
+                    } else {
+                        this.form.kao_shi_shi_chang = (this.form.hours * 60 + this.form.minutes) * 60 * 1000
+                    }
+                    delete this.form.isDateLimit
+                    delete this.form.isCountLimit
+                    delete this.form.isTimeLimit
+                    delete this.form.hours
+                    delete this.form.minutes
+                    this.form.chuang_jian_shi_j = this.$common.getDateNow(19)
+                    this.form.xian_kao_shi_jian = xian_kao_shi_jian !== '不限' ? this.$common.getFormatDate('string', 16, xian_kao_shi_jian) : xian_kao_shi_jian
+                    // 这里表示为组合题 需要生成一个新的题库
+                    if (this.allQuestions.tiKu.indexOf(',') !== -1 || this.allQuestions.handQuestions) {
+                        const tiKuId = await this.createBank()
+                        console.log(tiKuId)
+                        this.form.ti_ku_id_ = tiKuId
+                    } else {
+                        this.form.ti_ku_id_ = this.allQuestions.tiKu
+                    }
+
+                    // 表单验证通过,提交表单
+                    this.submitForm()
+                } else {
+                    ActionUtils.saveErrorMessage()
+                }
+            })
+        },
+        async createBank () {
+            // 1. 新建题库
+            const addParams = {
+                tableName: 't_question_bank',
+                paramWhere: [{
+                    ti_ku_ming_cheng_: '组合题库' + dayjs().format('YYYYMMDDHHmmss'),
+                    shi_fou_gong_kai_: '是',
+                    bian_zhi_ren_: this.userId,
+                    bian_zhi_shi_jian: this.$common.getDateNow(19),
+                    ti_ku_fen_lei_: '专业组考试',
+                    suo_shu_fan_wei_: '科级',
+                    ti_ku_zhuang_tai_: '可用',
+                    xian_kao_ci_shu_: this.form.xian_kao_ci_shu_,
+                    da_biao_zhan_bi_: this.form.da_biao_zhan_bi_,
+                    miao_shu_: this.form.kao_shi_miao_shu_,
+                    ping_fen_ren_: '',
+                    di_dian_: this.level,
+                    bian_zhi_bu_men_: '',
+                    kao_shi_shi_chang: this.form.kao_shi_shi_chang,
+                    ti_shu_: this.curQuestions.length,
+                    zong_fen_: this.curQuestions.reduce((sum, item) => sum + parseInt(item.fen_zhi_), 0)
+                }]
+            }
+            const { variables: { cont }} = await this.$common.request('add', addParams)
+            if (cont.length > 0) {
+                const addParams = {
+                    tableName: 't_questions',
+                    paramWhere: this.curQuestions.map(item => ({
+                        di_dian_: this.level,
+                        parent_id_: cont[0].id_,
+                        bu_men_: item.bu_men_ || '',
+                        chu_ti_ren_: item.chu_ti_ren_,
+                        chu_ti_shi_jian_: item.chu_ti_shi_jian_,
+                        xu_hao_: item.xu_hao_ || '',
+                        ti_gan_: item.ti_gan_,
+                        ti_xing_: item.ti_xing_,
+                        fu_tu_: item.fu_tu_ || '',
+                        xuan_xiang_lei_xi: item.xuan_xiang_lei_xi || '',
+                        da_an_: item.da_an_ || '',
+                        fen_zhi_: item.fen_zhi_,
+                        xuan_xiang_shu_: item.xuan_xiang_shu_ || '',
+                        zheng_que_da_an_: item.zheng_que_da_an_,
+                        ping_fen_fang_shi: item.ping_fen_fang_shi || '',
+                        ping_fen_ren_: item.ping_fen_ren_ || '',
+                        bei_zhu_: item.bei_zhu_ || '',
+                        zhuang_tai_: item.zhuang_tai_,
+                        biao_qian_: item.biao_qian_ || '',
+                        nan_du_: item.nan_du_ || 0
+                    }))
+                }
+                await this.$common.request('add', addParams)
+                return cont[0].id_
+            }
+
+            // 2.新建题目
+        },
+        submitForm () {
+            const addParams = {
+                tableName: 't_exams',
+                paramWhere: [this.form]
+            }
+            const updateParams = {
+                tableName: 't_exams',
+                updList: [
+                    {
+                        where: {
+                            id_: this.id
+                        },
+                        param: this.form
+                    }
+                ]
+            }
+            const type = this.id ? 'update' : 'add'
+            const params = type === 'add' ? addParams : updateParams
+            this.$common.request(type, params).then(() => {
+                this.$message.success(this.id ? '修改考试成功' : '新建考试成功')
+                this.closeDialog(true)
+            })
+        },
+        // 关闭当前窗口
+        closeDialog (refresh) {
+            this.$emit('close', false, refresh)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .exam-dialog {
+        ::v-deep {
+            .el-dialog {
+                max-width: 1080px;
+            }
+            .el-dialog__body {
+                height: calc(88vh - 150px);
+            }
+            .el-form-item {
+                margin-bottom: 14px !important;
+                &:last-child {
+                    margin-bottom: 0 !important;
+                }
+                .el-form-item__label {
+                    font-size: 14px !important;
+                }
+            }
+            .el-form-item--small .el-form-item__error {
+                padding-top: 6px;
+            }
+        }
+        .exam-form {
+            padding: 20px;
+            .question-icon {
+                font-size: 13px;
+                color: #606060;
+            }
+            .date-picker {
+                margin-left: 20px;
+            }
+            .custom-dialog {
+                ::v-deep {
+                    .el-input--prefix .el-input__inner {
+                        padding-left: 15px;
+                    }
+                }
+            }
+        }
+        .inline-item {
+            ::v-deep {
+                .el-radio-group {
+                    margin-right: 20px;
+                }
+            }
+            .time {
+                display: inline-block;
+            }
+        }
+        .unit {
+            display: inline-block;
+            margin: 0 20px 0 5px;
+        }
+        .handrand{
+            ::v-deep .el-form-item__content{
+                display: flex;
+                flex-wrap: wrap;
+            }
+            .hand-item{
+                display: flex;
+                margin-right: 20px;
+                margin-bottom: 10px;
+            }
+        }
+    }
+    .tiku {
+        .el-button {
+            margin-left: 20px;
+        }
+        .item{
+            display: flex;
+            gap: 20px;
+            .number{
+                width: 100px;
+            }
+        }
+    }
+</style>

+ 783 - 0
src/views/platform/examination/exam/list.vue

@@ -0,0 +1,783 @@
+<template>
+    <div class="main-container">
+        <ibps-crud
+            v-if="showTable"
+            ref="crud"
+            :display-field="title"
+            :height="height"
+            :data="listData"
+            :toolbars="listConfig.toolbars"
+            :search-form="listConfig.searchForm"
+            :pk-key="pkKey"
+            :index-row="false"
+            :columns="listConfig.columns"
+            :row-handle="listConfig.rowHandle"
+            :pagination="pagination"
+            :loading="loading"
+            @action-event="handleAction"
+            @sort-change="handleSortChange"
+            @pagination-change="handlePaginationChange"
+            @row-dblclick="handleRowDblclick"
+        >
+            <!-- 查询参数插槽 -->
+            <template slot="examBankId">
+                <ibps-custom-dialog
+                    v-model="examBankId"
+                    size="mini"
+                    template-key="tkdhk"
+                    :multiple="true"
+                    :temp-search="true"
+                    type="dialog"
+                    class="custom-dialog"
+                    placeholder="请选择"
+                    icon="ibps-icon-search"
+                    style="width: 150px;"
+                />
+            </template>
+            <template slot="isRand" slot-scope="scope">
+                <div>{{ scope.row.isRand==='1' ? '是' : '否' }}</div>
+            </template>
+            <template slot="randWay" slot-scope="{row}">
+                <div>{{ row.isRand==='1' ?['未知','题型','难度'][+row.randWay]:'/' }}</div>
+            </template>
+        </ibps-crud>
+        <exam-edit
+            v-if="showEditDialog"
+            :id="examInfo.examId"
+            :visible.sync="showEditDialog"
+            :is-disabled="examInfo.state !== '未发布'"
+            @callback="search"
+            @close="dialogClose"
+        />
+        <exam-detail
+            v-if="showDetailDialog"
+            :visible.sync="showDetailDialog"
+            :bank-id="examInfo.bankId"
+            :exam-id="examInfo.examId"
+            @close="visible => showDetailDialog = visible"
+        />
+        <exam-judge
+            v-if="showJudgeDialog"
+            :id="selection"
+            :visible.sync="showJudgeDialog"
+            @close="visible => showJudgeDialog = visible"
+        />
+    </div>
+</template>
+
+<script>
+import { examTypeOptions, statusOption, basicColumn, infoColumn, resultColumn } from '../constants'
+import ActionUtils from '@/utils/action'
+import FixHeight from '@/mixins/height'
+import { max, min, mean, sum, maxBy, minBy, meanBy, round, keyBy, mapValues } from 'lodash'
+
+const sortField = {
+    CREATE_TIME_: 'ex.chuang_jian_shi_j',
+    PUBLISH_DATE_: 'ex.fa_bu_shi_jian_'
+}
+
+export default {
+    components: {
+        ExamEdit: () => import('./edit'),
+        ExamDetail: () => import('./detail'),
+        ExamJudge: () => import('../question/judge'),
+        IbpsCustomDialog: () => import('@/business/platform/data/templaterender/custom-dialog')
+    },
+    mixins: [FixHeight],
+    data () {
+        // const roleList = ['xtgljs', 'pxglxzfzr']
+        const roleList = ['xtgljs']
+        const { isSuper, role, userId, userList = [] } = this.$store.getters || {}
+        const { first, second } = this.$store.getters.level || {}
+        const hasRole = isSuper || role.some(r => roleList.includes(r.alias))
+        const userOption = userList.map(item => ({ label: item.userName, value: item.userId }))
+        const endColumn = [
+            { prop: 'createTime', label: '创建时间', dateFormat: 'yyyy-MM-dd HH:mm', sortable: 'custom', width: 120 },
+            { prop: 'publishDate', label: '发布时间', dateFormat: 'yyyy-MM-dd HH:mm', sortable: 'custom', width: 120 },
+            { prop: 'createBy', label: '创建人', tags: userOption, width: 90 }
+        ]
+        const showType = 'info'
+        return {
+            userId,
+            userList,
+            userOption,
+            hasRole,
+            showType,
+            endColumn,
+            level: second || first,
+            title: '考试管理',
+            pkKey: 'id', // 主键  如果主键不是pk需要传主键
+            loading: true,
+            height: document.clientHeight,
+            listData: [],
+            pagination: {},
+            sorts: {},
+            showEditDialog: false,
+            showDetailDialog: false,
+            showJudgeDialog: false,
+            params: {},
+            selection: '',
+            examInfo: {
+                examId: '',
+                bankId: '',
+                state: ''
+            },
+            examBankId: '',
+            showTable: true,
+            listConfig: {
+                toolbars: [
+                    { key: 'search' },
+                    { key: 'create', label: '新建考试', type: 'success', icon: 'ibps-icon-plus' },
+                    { key: 'remove' },
+                    { key: 'judge', label: '试题评阅', type: 'info', icon: 'ibps-icon-adjust' },
+                    { key: 'change', label: `${this.showType === 'info' ? '考试结果' : '考试信息'}`, type: 'warning', icon: 'el-icon-s-operation' }
+                ],
+                searchForm: {
+                    forms: [
+                        { prop: 'examName', label: '考试名称', itemWidth: 150 },
+                        { prop: 'examType', label: '考试类型', itemWidth: 150, fieldType: 'select', multiple: 'Y', options: examTypeOptions },
+                        { prop: 'examBank', label: '考试题库', fieldType: 'slot', slotName: 'examBankId', itemWidth: 150 },
+                        { prop: 'examState', label: '考试状态', itemWidth: 150, fieldType: 'select', multiple: 'Y', options: statusOption },
+                        { prop: ['createTime0', 'createTime1'], label: '创建时间', fieldType: 'daterange' },
+                        { prop: ['publishDate0', 'publishDate1'], label: '发布时间', fieldType: 'daterange' }
+                    ]
+                },
+                // 表格字段配置
+                columns: [
+                    ...basicColumn,
+                    ...infoColumn,
+                    ...endColumn
+                ],
+                rowHandle: {
+                    // effect: 'display',
+                    actions: [
+                        {
+                            key: 'publish',
+                            label: '发布考试',
+                            type: 'primary',
+                            icon: 'ibps-icon-send',
+                            hidden: (row, index) => {
+                                return row.examState !== '未发布'
+                            }
+                        },
+                        {
+                            key: 'close',
+                            label: '取消考试',
+                            type: 'primary',
+                            icon: 'ibps-icon-trash',
+                            hidden: (row, index) => {
+                                return row.examState !== '未发布'
+                            }
+                        },
+                        {
+                            key: 'attend',
+                            label: '报名考试',
+                            type: 'primary',
+                            icon: 'ibps-icon-bookmark',
+                            hidden: (row, index) => {
+                                // 未发布且未报名
+                                return row.examState !== '未发布' || row.examinee.includes(this.userId) || row.allowRegist !== '是'
+                            }
+                        },
+                        {
+                            key: 'cancel',
+                            label: '取消报名',
+                            type: 'primary',
+                            icon: 'ibps-icon-minus-square',
+                            hidden: (row, index) => {
+                                return row.examState !== '未发布' || !row.examinee.includes(this.userId)
+                            }
+                        },
+                        {
+                            key: 'modify',
+                            label: '编辑考试',
+                            type: 'primary',
+                            icon: 'ibps-icon-edit',
+                            hidden: (row, index) => {
+                                return row.examState !== '未发布' || (!this.hasRole && this.userId !== row.createBy)
+                            }
+                        },
+                        {
+                            key: 'info',
+                            label: '考试明细',
+                            type: 'primary',
+                            icon: 'ibps-icon-list-alt',
+                            hidden: (row, index) => {
+                                return ['已取消', '未发布'].includes(row.examState)
+                            }
+                        }
+                    ]
+                }
+            }
+        }
+    },
+    created () {
+    },
+    mounted () {
+        this.loadData()
+    },
+    methods: {
+        // 组件关闭事件
+        dialogClose (visible, refresh) {
+            this.showEditDialog = visible
+            if (refresh) {
+                this.loadData()
+            }
+        },
+        // 加载数据
+        loadData () {
+            this.loading = true
+            this.getData(this.getSearchFormData()).then(res => {
+                this.loading = false
+                ActionUtils.handleListData(this, res.data)
+            })
+        },
+        getData ({ parameters, requestPage, sorts }) {
+            const { pageNo = 1, limit = 20 } = requestPage || {}
+            let sortParams = ''
+            if (sorts && sorts.length) {
+                sortParams = sorts.map(i => `${sortField[i.field]} ${i.order}`).join(',')
+            } else {
+                sortParams = 'ex.chuang_jian_shi_j desc, ex.fa_bu_shi_jian_ desc'
+            }
+            const params = this.getParams(parameters)
+            const sql = `select qb.ti_ku_ming_cheng_ as bankName,f.pei_xun_nei_rong_ as trainId, ex.id_ as examId, ex.ti_ku_id_ as bankId, e.id_ as paperId, ex.zhuang_tai_ as examState, e.zhuang_tai_ as paperState, qb.ti_shu_ as questionCount, qb.zong_fen_ as totalScore, ex.kao_shi_ming_chen as examName, ex.kao_shi_lei_xing_ as examType, ex.can_kao_ren_yuan_ as examinee, e.kao_shi_ren_ as examineeId, ex.create_by_ as createBy, ex.chuang_jian_shi_j as createTime, ex.fa_bu_shi_jian_ as publishDate, ex.fa_bu_ren_ as publisher, ex.xian_kao_shi_jian as limitDate, ex.kao_shi_shi_chang as duration, ex.xian_kao_ci_shu_ as limitCount, ex.da_biao_zhan_bi_ as qualifiedRadio, ex.ji_fen_fang_shi_ as scoringType, ex.yun_xu_bao_ming_ as allowRegist, ex.kao_shi_miao_shu_ as examDesc, ex.shu_ju_yong_tu_ as dataType, ex.sui_ji_chou_ti_ as isRand,ex.chou_ti_fang_shi_ as randWay, ex.sui_ji_ti_shu_ as randNumber, ex.chou_ti_zong_fen_ as randScore, ex.ti_mu_zong_shu_ as randTotal, e.de_fen_ as score, e.bao_ming_shi_jian as applyTime, e.kai_shi_shi_jian_ as startTime, e.jie_shu_shi_jian_ as endTime from t_exams ex left join t_question_bank qb on ex.ti_ku_id_ = qb.id_ left join t_examination e on e.exam_id_ = ex.id_ left join t_rypxcjb f on f.id_=ex.guan_lian_id_ where ex.di_dian_ = '${this.level}'${params} order by ${sortParams}`
+            return new Promise((resolve, reject) => {
+                this.$common.request('sql', sql).then(res => {
+                    const { data = [] } = res.variables || {}
+                    if (!data.length) {
+                        resolve({
+                            dataResult: [],
+                            pageResult: {
+                                limit: 20,
+                                page: 1,
+                                totalCount: 0,
+                                totalPages: 0
+                            }
+                        })
+                        return
+                    }
+                    const archiveData = []
+                    const scorrType = {
+                        '最高分': 'max',
+                        '平均分': 'avg',
+                        '最近得分': 'latest'
+                    }
+                    data.forEach(item => {
+                        const examIndex = archiveData.findIndex(i => i.examId === item.examId)
+                        const temp = {
+                            paperId: item.paperId,
+                            paperState: item.paperState,
+                            examineeId: item.examineeId,
+                            score: item.score,
+                            applyTime: item.applyTime,
+                            startTime: item.startTime,
+                            endTime: item.endTime,
+                            qualifiedRadio: item.qualifiedRadio,
+                            totalScore: item.isRand === '1' ? parseFloat(item.randScore) : parseFloat(item.totalScore),
+                            scoringType: item.scoringType
+                        }
+                        if (examIndex === -1) {
+                            archiveData.push({
+                                trainId: item.trainId,
+                                examId: item.examId,
+                                examName: item.examName,
+                                examType: item.examType,
+                                examState: item.examState,
+                                bankId: item.bankId,
+                                bankName: item.bankName,
+                                examinee: item.examinee,
+                                questionCount: item.isRand === '1' ? parseFloat(item.randTotal) : parseFloat(item.questionCount),
+                                duration: this.transformTime(item.duration),
+                                limitCount: item.limitCount,
+                                limitDate: item.limitDate,
+                                qualifiedRadio: item.qualifiedRadio,
+                                scoringType: item.scoringType,
+                                allowRegist: item.allowRegist,
+                                isRand: item.isRand,
+                                randWay: item.randWay,
+                                examDesc: item.examDesc,
+                                createTime: item.createTime,
+                                publishDate: item.publishDate,
+                                publisher: item.publisher,
+                                createBy: item.createBy,
+                                dataType: item.dataType,
+                                totalScore: item.isRand === '1' ? parseFloat(item.randScore) : parseFloat(item.totalScore),
+                                paperList: [temp],
+                                scoreList: [parseFloat(item.score || -1)],
+                                statusList: [item.paperState]
+                            })
+                        } else {
+                            archiveData[examIndex].scoreList.push(parseFloat(item.score || -1))
+                            archiveData[examIndex].statusList.push(item.paperState)
+                            archiveData[examIndex].paperList.push(temp)
+                        }
+                    })
+                    const nodatadesc = '/'
+                    archiveData.forEach((item, index) => {
+                        const finishScore = item.scoreList.filter(i => i !== -1)
+                        const examCount = item.scoreList.length
+                        item.paperList.forEach((paper, index) => {
+                            paper.cj = item.scoreList[index]
+                            paper.userName = this.transformUser(paper.examineeId)
+                            paper.max = finishScore.length ? round(max(finishScore), 2) : nodatadesc
+                            paper.min = finishScore.length ? round(min(finishScore), 2) : nodatadesc
+                            paper.avg = finishScore.length ? round(mean(finishScore), 2) : nodatadesc
+                            paper.sum = finishScore.length ? round(sum(finishScore), 2) : nodatadesc
+                            paper.latest = finishScore.length ? round(finishScore[0], 2) : nodatadesc
+                            paper.examCount = examCount
+                            paper.finishCount = finishScore.length
+                            paper.count = `${paper.examCount}/${paper.finishCount}`
+                            // 修改考试结束的判断逻辑:只要有一次交卷记录即为已完成
+                            paper.examStatus = paper.finishCount > 0 ? '已完成' : '未完成'
+                            paper.resultScore = paper[scorrType[paper.scoringType]]
+                            paper.isQualified = paper.examStatus === '已完成' ? paper.resultScore >= (parseFloat(paper.qualifiedRadio) / 100 * parseFloat(paper.totalScore)) ? '达标' : '未达标' : '考试未结束'
+                        })
+                        const finishList = item.paperList.filter(i => i.cj !== -1)
+                        item.maxScore = finishList.length ? maxBy(finishList, 'max').max : nodatadesc
+                        item.minScore = finishList.length ? minBy(finishList, 'min').min : nodatadesc
+                        item.avgScore = finishList.length ? meanBy(finishList, 'avg').toFixed(2) : nodatadesc
+                        item.passRate = finishList.length ? this.getPassRate(finishList) : nodatadesc
+                        item.examineeCount = item.examinee ? item.examinee.split(',').length : 0
+                        item.examFinishCount = [...new Set(finishList.map(ii => ii.examineeId))].length
+                    })
+                    const page = {
+                        limit,
+                        page: pageNo,
+                        totalCount: archiveData.length,
+                        totalPages: Math.ceil(archiveData.length / limit)
+                    }
+                    const result = {
+                        data: {
+                            dataResult: archiveData.slice((pageNo - 1) * limit, pageNo * limit),
+                            pageResult: page
+                        }
+                    }
+                    resolve(result)
+                }).catch(error => {
+                    reject(error)
+                })
+            })
+        },
+        // 组装SQL查询参数
+        getParams (parameters) {
+            const temp = mapValues(keyBy(parameters, 'key'), 'value')
+            let params = ''
+
+            const addCondition = (condition, value, isArray = false) => {
+                if (this.$utils.isNotEmpty(value)) {
+                    if (isArray) {
+                        const conditions = value.map(v => `${condition} = '${v}'`).join(' or ')
+                        params += ` and (${conditions})`
+                    } else {
+                        params += ` and ${condition} like '%${value}%'`
+                    }
+                }
+            }
+            addCondition('ex.kao_shi_ming_chen', temp.examName)
+            addCondition('ex.kao_shi_lei_xing_', temp.examType, true)
+            addCondition('ex.zhuang_tai_', temp.examState, true)
+
+            if (this.examBankId) {
+                const conditions = this.examBankId.split(',').map(id => `ex.ti_ku_id_ = '${id}'`).join(' or ')
+                params += ` and (${conditions})`
+            }
+
+            const addDateCondition = (key, field) => {
+                const dateParam = parameters.find(i => i.key.includes(key))
+                if (dateParam) {
+                    params += ` and (${field} >= '${temp[key + '0']}' and ${field} <= '${temp[key + '1']}' or ${field} is null)`
+                }
+            }
+            addDateCondition('createTime', 'ex.chuang_jian_shi_j')
+            addDateCondition('publishDate', 'ex.fa_bu_shi_jian_')
+            console.log(params)
+            return params
+        },
+        /**
+         * 获取格式化参数
+         */
+        getSearchFormData () {
+            return ActionUtils.formatParams(
+                this.$refs['crud'] ? this.$refs['crud'].getSearcFormData() : {},
+                this.pagination,
+                this.sorts
+            )
+        },
+        /**
+         * 处理分页事件
+         */
+        handlePaginationChange (page) {
+            ActionUtils.setPagination(this.pagination, page)
+            this.loadData()
+        },
+        /**
+         * 处理排序
+         */
+        handleSortChange (sort) {
+            ActionUtils.setSorts(this.sorts, sort)
+            this.loadData()
+        },
+        /**
+         * 查询
+         */
+        search () {
+            this.loadData()
+        },
+        handleRowDblclick (row) {
+            if (['已取消', '未发布'].includes(row.examState)) {
+                return this.$message.info('暂无详情数据!')
+            }
+            this.examInfo = {
+                examId: row.examId,
+                bankId: row.bankId
+            }
+            this.showDetailDialog = true
+        },
+        /**
+         * 处理按钮事件
+         */
+        handleAction (command, position, selection, data) {
+            const ids = data && data.length ? data.map(i => i.examId) : []
+            switch (command) {
+                case 'search':
+                    ActionUtils.setFirstPagination(this.pagination)
+                    this.search()
+                    break
+                case 'create':
+                    this.examInfo = {
+                        examId: '',
+                        state: '未发布'
+                    }
+                    this.showEditDialog = true
+                    break
+                case 'remove':
+                    this.handleRemove(ids)
+                    break
+                case 'judge':
+                    this.selection = ids.join(',')
+                    this.showJudgeDialog = true
+                    break
+                case 'change':
+                    this.handleColumnChange()
+                    break
+                case 'publish':
+                    this.handlePublish(data)
+                    break
+                case 'close':
+                    this.handleClose(data)
+                    break
+                case 'attend':
+                    this.handleAttend(data)
+                    break
+                case 'cancel':
+                    this.handleCancel(data)
+                    break
+                case 'modify':
+                    if (!this.hasRole && this.userId !== data.createBy) {
+                        return this.$message.warning('您无权编辑这场考试,请联系考试创建人或系统管理员!')
+                    }
+                    this.examInfo = {
+                        examId: data.examId,
+                        state: data.examState
+                    }
+                    this.showEditDialog = true
+                    break
+                case 'info':
+                    this.examInfo = {
+                        examId: data.examId,
+                        bankId: data.bankId
+                    }
+                    this.showDetailDialog = true
+                    break
+                default:
+                    break
+            }
+        },
+        /**
+         * 切换显示信息
+         */
+        handleColumnChange () {
+            let temp = []
+            if (this.showType === 'info') {
+                temp = resultColumn
+                this.showType = 'result'
+            } else {
+                temp = infoColumn
+                this.showType = 'info'
+            }
+            this.listConfig.columns = [
+                ...basicColumn,
+                ...temp,
+                ...this.endColumn
+            ]
+            this.updateToolbarLabel()
+            this.showTable = false
+            setTimeout(() => {
+                this.showTable = true
+            }, 10)
+        },
+        /**
+         * 更新按钮名称
+         */
+        updateToolbarLabel () {
+            this.listConfig.toolbars = this.listConfig.toolbars.map(toolbar => {
+                if (toolbar.key === 'change') {
+                    toolbar.label = this.showType === 'info' ? '考试结果' : '考试信息'
+                }
+                return toolbar
+            })
+        },
+        /**
+         * 发布考试
+         */
+        async handlePublish (data) {
+            const { examId, bankId, bankName, questionCount, totalScore, examName, limitCount, limitDate, duration, examinee, qualifiedRadio, scoringType, allowRegist, createBy } = data || {}
+            if (!this.hasRole && this.userId !== createBy) {
+                return this.$message.warning('您无权发布这场考试,请联系考试创建人或系统管理员!')
+            }
+            const examNameDesc = `<div><span style="font-weight: 600;">考试名称:</span><span style="color: #f56c6c;">${examName}</span></div>`
+            const bankNameDesc = `<div><span style="font-weight: 600;">考试题库:</span><span style="color: #f56c6c;">${bankName}</span></div>`
+            const quesitonCountDesc = `<div><span style="font-weight: 600;">题目数量:</span><span style="color: #f56c6c;">${questionCount || 0}</span></div>`
+            const scoreDesc = `<div><span style="font-weight: 600;">考试总分:</span><span style="color: #f56c6c;">${totalScore || 0}</span></div>`
+            const examineeDesc = `<div><span style="font-weight: 600;">考试人数:</span><span style="color: #f56c6c;">${examinee ? examinee.split(',').length : 0}</span></div>`
+            const limitDateDesc = `<div><span style="font-weight: 600;">限考时间:</span><span style="color: #f56c6c;">${limitDate}</span></div>`
+            const limitCountDesc = `<div><span style="font-weight: 600;">限考次数:</span><span style="color: #f56c6c;">${limitCount}</span></div>`
+            const temp = duration === '不限' || !duration ? '不限' : this.transformTime(duration)
+            const durationDesc = `<div><span style="font-weight: 600;">考试时长:</span><span style="color: #f56c6c;">${temp}</span></div>`
+            const qualifiedRadioDesc = `<div><span style="font-weight: 600;">达标分数占比:</span><span style="color: #f56c6c;">${qualifiedRadio}%</span></div>`
+            const scoringTypeDesc = `<div><span style="font-weight: 600;">计分方式:</span><span style="color: #f56c6c;">${scoringType}</span></div>`
+            const allowRegistDesc = `<div><span style="font-weight: 600;">是否允许报名:</span><span style="color: #f56c6c;">${allowRegist}</span></div>`
+            const tip = `<div style="font-size: 16px;">发布后将为所有参考人员发放试卷并通知考试,考试发布后不可取消,是否确认</div>`
+            this.$confirm(`<p style="font-size: 16px;font-weight: 600;">当前考试信息如下:</p>${examNameDesc}${bankNameDesc}${quesitonCountDesc}${scoreDesc}${examineeDesc}${limitDateDesc}${limitCountDesc}${durationDesc}${qualifiedRadioDesc}${scoringTypeDesc}${allowRegistDesc}${tip}`, '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'info',
+                showClose: false,
+                closeOnClickModal: false,
+                dangerouslyUseHTMLString: true
+            }).then(() => {
+                const params = {
+                    tableName: 't_exams',
+                    updList: [{
+                        where: {
+                            id_: examId
+                        },
+                        param: {
+                            fa_bu_ren_: this.userId,
+                            fa_bu_shi_jian_: this.$common.getDateNow(19),
+                            zhuang_tai_: '已发布'
+                        }
+                    }]
+                }
+                // 更新考试状态
+                this.$common.request('update', params).then(() => {
+                    this.$message.success('发布考试成功!')
+                    this.search()
+                    // 发放试卷
+                    this.createPaper(data, examinee)
+                })
+            }).catch(() => {})
+        },
+        /**
+         * 创建试卷
+         */
+        createPaper (data, examinees) {
+            const { examId, bankId, examName, questionCount, totalScore, duration, qualifiedRadio, limitDate, limitCount } = data || {}
+            const examineeList = examinees.split(',')
+            const currentTime = this.$common.getDateNow(19)
+            const paramWhere = examineeList.map(i => ({
+                exam_id_: examId,
+                ti_ku_id_: bankId,
+                di_dian_: this.level,
+                kao_shi_ren_: i,
+                bu_men_: '',
+                bao_ming_shi_jian: currentTime,
+                ti_ku_zong_fen_: totalScore,
+                zhuang_tai_: '未开始',
+                sheng_yu_shi_chan: duration,
+                da_biao_zhan_bi_: qualifiedRadio
+            }))
+            const addParams = {
+                tableName: 't_examination',
+                paramWhere
+            }
+            this.$common.request('add', addParams).then(() => {
+                // 发送通知
+                const limitTime = duration === '不限' || !duration ? '不限' : this.transformTime(duration)
+                const msgContent = `您参加的考试【${examName}】已发布,该考试限考时间【${limitDate}】,限考次数【${limitCount}】,考试时长【${limitTime}】,题数【${questionCount}】,总分【${totalScore}】,请及时完成考试!`
+                examineeList.map(i => this.$common.sendMsg({
+                    subject: '考试信息提醒',
+                    content: msgContent,
+                    receiverId: i,
+                    canreplay: '0'
+                }))
+                // 添加日程
+                const now = this.$common.getDateNow(10)
+                const paramsList = examineeList.map(i => ({
+                    title_: `考试【${examName}】`,
+                    content_: `考试名称:【${examName}】\n开始时间:【${now}】\n结束时间:【${limitDate}】\n限考次数:【${limitCount}】\n考试时长:【${limitTime}】\n题数:【${questionCount}】\n总分:【${totalScore}】`,
+                    start_time_: now,
+                    end_time_: limitDate !== '不限' ? limitDate.slice(0, 10) : now,
+                    emergency_state_: '2',
+                    user_id_: i,
+                    user_name_: this.transformUser(i)
+                }))
+                this.$common.request('add', {
+                    tableName: 'ibps_party_user_calendar',
+                    paramWhere: paramsList
+                }).then(() => {
+                    console.log('添加日程数据成功')
+                })
+            })
+        },
+        /**
+         * 取消考试
+         */
+        handleClose ({ examId, createBy }) {
+            if (!this.hasRole && this.userId !== createBy) {
+                return this.$message.warning('您无权取消这场考试,请联系考试创建人或系统管理员!')
+            }
+            const params = {
+                tableName: 't_exams',
+                updList: [{
+                    where: {
+                        id_: examId
+                    },
+                    param: {
+                        zhuang_tai_: '已取消'
+                    }
+                }]
+            }
+            this.$common.request('update', params).then(() => {
+                this.$message.success('取消考试成功!')
+                this.search()
+            })
+        },
+        /**
+         * 报名考试
+         */
+        handleAttend (data) {
+            const { examId, examState, examinee } = data
+            const examineeList = examinee ? examinee.split(',') : []
+            examineeList.push(this.userId)
+            const params = {
+                tableName: 't_exams',
+                updList: [{
+                    where: {
+                        id_: examId
+                    },
+                    param: {
+                        can_kao_ren_yuan_: examineeList.join(',')
+                    }
+                }]
+            }
+            // 更新参考人员
+            this.$common.request('update', params).then(() => {
+                if (examState === '未发布') {
+                    this.$message.success('报名考试成功!该考试还未发布,请等待考试发布后再开始考试~')
+                } else {
+                    this.createPaper(data, this.userId)
+                    this.$message.success('报名考试成功!请前往【我的考试】开始考试')
+                }
+                this.search()
+            })
+        },
+        /**
+         * 取消报名
+         */
+        handleCancel (data) {
+            const { examId, examinee } = data
+            const examineeList = examinee.split(',').filter(item => item !== this.userId).join(',')
+            const params = {
+                tableName: 't_exams',
+                updList: [{
+                    where: {
+                        id_: examId
+                    },
+                    param: {
+                        can_kao_ren_yuan_: examineeList
+                    }
+                }]
+            }
+            this.$common.request('update', params).then(() => {
+                this.$message.success('取消报名成功!')
+                this.search()
+            })
+        },
+        /**
+         * 删除考试
+         */
+        handleRemove (ids) {
+            if (!ids || !ids.length) {
+                return this.$message.warning('请选择要删除的考试!')
+            }
+            this.$confirm('数据一旦删除无法恢复,确定要删除选中的考试吗?', '提示', {
+                confirmButtonText: '确认',
+                cancelButtonText: '取消',
+                type: 'warning',
+                showClose: false,
+                closeOnClickModal: false
+            }).then(() => {
+                this.$common.request('delete', {
+                    tableName: 't_exams',
+                    paramWhere: { id_: ids.join(',') }
+                }).then(() => {
+                    ActionUtils.removeSuccessMessage()
+                    this.search()
+                })
+            })
+        },
+        transformUser (userId) {
+            const user = this.userList.find(u => u.userId === userId) || {}
+            return user.userName || '-'
+        },
+        transformTime (timeStamp) {
+            if (timeStamp === '不限' || !timeStamp) return '不限'
+            const hours = Math.floor(parseInt(timeStamp) / 3600000)
+            const minutes = Math.floor((parseInt(timeStamp) % 3600000) / 60000)
+            return hours + '小时' + minutes + '分钟'
+        },
+        getPassRate (list) {
+            const passScore = parseFloat(list[0].qualifiedRadio) / 100 * parseFloat(list[0].totalScore)
+            const result = []
+            // 按照考试人进行分类
+            const scorrType = {
+                '最高分': 'max',
+                '平均分': 'avg',
+                '最近得分': 'latest'
+            }
+            list.forEach(item => {
+                const { examineeId, userName, scoringType } = item
+                const t = result.find(i => i.examineeId === examineeId)
+                if (t) {
+                    t.data.push(item)
+                } else {
+                    result.push({
+                        examineeId,
+                        userName,
+                        scoringType,
+                        data: [item]
+                    })
+                }
+            })
+            result.forEach(item => {
+                item.max = maxBy(item.data, 'cj').cj
+                item.min = minBy(item.data, 'cj').cj
+                item.avg = meanBy(item.data, 'cj').toFixed(2)
+                item.latest = item.data[item.data.length - 1].cj
+                item.resultScore = item[scorrType[item.scoringType]]
+            })
+            const passList = result.filter(i => +i.resultScore >= passScore)
+            return (passList.length / result.length * 100).toFixed(2) + '%'
+        }
+    }
+}
+</script>
+<style lang="scss">
+    .attachment-uploader-dialog {
+        .el-dialog__body {
+            height: calc(57vh - 100px) !important;
+        }
+    }
+</style>

+ 250 - 0
src/views/platform/examination/exam/statistics.vue

@@ -0,0 +1,250 @@
+<template>
+    <div>
+        <el-dialog
+            center
+            append-to-body
+            title="数据统计"
+            :visible.sync="dialogVisible"
+            width="80%"
+            top
+            :close-on-click-modal="false"
+            :close-on-press-escape="false"
+        >
+            <div class="chart">
+                <div class="chart1">
+                    <div id="echart1" style="width: 1000px;height:420px;" />
+
+                </div>
+                <div class="chart2">
+                    <div id="echart2" style="width: 1000px;height:420px;" />
+                </div>
+            </div>
+
+            <span slot="footer" class="dialog-footer">
+                <el-button type="info" @click="dialogVisible = false">关闭</el-button>
+            </span>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+export default {
+    props: {
+        departmentData: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data () {
+        return {
+            dialogVisible: false
+        }
+    },
+    watch: {
+        departmentData () {
+            if (this.departmentData.length) {
+                this.$nextTick(() => {
+                    console.log(this.departmentData)
+                    const echart1 = echarts.init(document.getElementById('echart1'))
+                    const echart2 = echarts.init(document.getElementById('echart2'))
+                    const option1 = this.formatData1()
+                    const option2 = this.formatData2()
+                    echart1.setOption(option1)
+                    echart2.setOption(option2)
+                    echart1.resize({ width: 1000, height: 420 })
+                    echart2.resize({ width: 1000, height: 420 })
+                })
+            }
+        }
+    },
+    methods: {
+        open () {
+            this.dialogVisible = true
+        },
+        close () {
+            this.dialogVisible = false
+        },
+        formatData1 () {
+            const xAxisData = this.departmentData.map(item => item.department)
+            const seriesData = [
+                {
+                    name: '应参考人数',
+                    type: 'bar',
+                    data: this.departmentData.map(item => item.plannedAttendees)
+                },
+                {
+                    name: '实参考人数',
+                    type: 'bar',
+                    data: this.departmentData.map(item => item.actualAttendees)
+                },
+                {
+                    name: '参考率',
+                    type: 'line',
+                    yAxisIndex: 1,
+                    data: this.departmentData.map(item => (item.joinRate * 100).toFixed(2)),
+                    label: {
+                        show: true,
+                        color: '#1f1f1f',
+                        fontSize: 12,
+                        formatter: '{c}%'
+                    }
+                }
+            ]
+
+            return {
+                dataZoom: [
+                    {
+                        type: 'slider',
+                        show: true,
+                        xAxisIndex: [0],
+                        start: 0,
+                        end: 200
+                    }
+                ],
+                title: {
+                    text: '各部门参考人数信息统计'
+                },
+                tooltip: {
+                    trigger: 'axis',
+                    axisPointer: {
+                        type: 'shadow'
+                    }
+                },
+                legend: {
+                    data: ['应参考人数', '实参考人数', '参考率']
+                },
+                xAxis: {
+                    type: 'category',
+                    data: xAxisData,
+                    axisLabel: {
+                        rotate: 20
+                    }
+                },
+                yAxis: [
+                    {
+                        type: 'value',
+                        name: '人数',
+                        axisLabel: {
+                            formatter: '{value}'
+                        },
+                        minInterval: 1 // 人数最少为1 不能为小数
+                    },
+                    {
+                        type: 'value',
+                        name: '参考率 (%)',
+                        axisLabel: {
+                            formatter: '{value}%'
+                        }
+                    }
+                ],
+                series: seriesData
+            }
+        },
+        formatData2 () {
+            const xAxisData = this.departmentData.map(item => item.department)
+            const seriesData = [
+                {
+                    name: '最高分',
+                    type: 'bar',
+                    data: this.departmentData.map(item => item.maxScore)
+                },
+                {
+                    name: '最低分',
+                    type: 'bar',
+                    data: this.departmentData.map(item => item.minScore)
+                },
+                {
+                    name: '平均分',
+                    type: 'bar',
+                    data: this.departmentData.map(item => item.avgScore.toFixed(2))
+                },
+                {
+                    name: '合格率',
+                    type: 'line',
+                    yAxisIndex: 1,
+                    data: this.departmentData.map(item => (item.passRate * 100).toFixed(2)),
+                    label: {
+                        show: true,
+                        color: '#1f1f1f',
+                        fontSize: 12,
+                        formatter: '{c}%'
+                    }
+                }
+            ]
+
+            return {
+                dataZoom: [
+                    {
+                        type: 'slider',
+                        show: true,
+                        xAxisIndex: [0],
+                        start: 0,
+                        end: 200
+                    }
+                ],
+                title: {
+                    text: '各部门成绩信息统计'
+                },
+                tooltip: {
+                    trigger: 'axis',
+                    axisPointer: {
+                        type: 'shadow'
+                    }
+                },
+                legend: {
+                    data: ['最高分', '最低分', '平均分', '合格率']
+                },
+                xAxis: {
+                    type: 'category',
+                    data: xAxisData,
+                    axisLabel: {
+                        rotate: 20
+                    }
+                },
+                yAxis: [
+                    {
+                        type: 'value',
+                        name: '分数',
+                        axisLabel: {
+                            formatter: '{value}'
+                        }
+                    },
+                    {
+                        type: 'value',
+                        name: '合格率 (%)',
+                        axisLabel: {
+                            formatter: '{value}%'
+                        }
+                    }
+                ],
+                series: seriesData
+            }
+        }
+    }
+}
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+  display: flex;
+  justify-content: center;
+}
+.chart {
+  display: flex;
+  justify-content: center;
+  /* align-items: center; */
+  flex-direction: column;
+  width: 100%;
+  // height: 60vh;
+  .chart1,
+  .chart2 {
+    display: flex;
+    justify-content: center;
+    margin: 10px 0 10px 0;
+  }
+  .chart1 {
+    margin-bottom: 60px;
+  }
+}
+</style>

+ 854 - 0
src/views/platform/examination/question/edit.vue

@@ -0,0 +1,854 @@
+<template>
+    <el-dialog
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        append-to-body
+        width="60%"
+        class="dialog question-dialog"
+        top="6vh"
+        @close="closeDialog"
+        @open="getQuestionData"
+    >
+        <el-form
+            ref="form"
+            :label-width="formLabelWidth"
+            :model="form"
+            :rules="rules"
+            class="question-form"
+            :class="readonly ? 'readonly-form' : ''"
+            @submit.native.prevent
+        >
+            <div class="inline-item">
+                <el-form-item label="题型:" prop="ti_xing_">
+                    <el-select
+                        v-model="form.ti_xing_"
+                        filterable
+                        width="100%"
+                        placeholder="请选择题型"
+                        :disabled="readonly"
+                        @change="changeQuestionType"
+                    >
+                        <el-option
+                            v-for="item in questionType"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value"
+                        />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="出题人:" prop="chu_ti_ren_">
+                    <el-select
+                        v-model="form.chu_ti_ren_"
+                        filterable
+                        width="100%"
+                        :disabled="readonly"
+                        placeholder="请选择出题人"
+                    >
+                        <el-option
+                            v-for="item in userList"
+                            :key="item.userId"
+                            :label="item.userName"
+                            :value="item.userId"
+                        />
+                    </el-select>
+                </el-form-item>
+            </div>
+            <div class="inline-item" :class="['单选题', '多选题', '判断题'].includes(form.ti_xing_) ? 'mb-20' : ''">
+                <el-form-item prop="ping_fen_fang_shi">
+                    <template slot="label">
+                        评分方式
+                        <el-tooltip effect="dark" content="单选、多选和判断题为系统自动评分,其余为手动评分类型,需选择评分人。" placement="top">
+                            <i class="el-icon-question question-icon">:</i>
+                        </el-tooltip>
+                    </template>
+                    <el-radio-group v-model="questionRateType" disabled>
+                        <el-radio-button label="自动">自动</el-radio-button>
+                        <el-radio-button label="手动">手动</el-radio-button>
+                    </el-radio-group>
+                </el-form-item>
+                <el-form-item v-if="questionRateType === '手动'" label="评分人:" prop="ping_fen_ren_">
+                    <el-select
+                        v-model="form.ping_fen_ren_"
+                        filterable
+                        width="100%"
+                        clearable
+                        multiple
+                        collapse-tags
+                        :multiple-limit="16"
+                        :disabled="readonly"
+                        placeholder="请选择评分人"
+                    >
+                        <el-option
+                            v-for="item in userList"
+                            :key="item.userId"
+                            :label="item.userName"
+                            :value="item.userId"
+                        />
+                    </el-select>
+                </el-form-item>
+            </div>
+            <el-form-item label="题干:" prop="ti_gan_">
+                <el-input
+                    v-model="form.ti_gan_"
+                    type="textarea"
+                    :maxlength="512"
+                    :rows="2"
+                    :autosize="readonly"
+                    :disabled="readonly"
+                    placeholder="请输入题干内容"
+                />
+            </el-form-item>
+            <el-form-item prop="biao_qian_">
+                <template slot="label">
+                    标签
+                    <el-tooltip effect="dark" content="可为题目设置标签便于后续检索。" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-tag
+                    v-for="tag in questionTags"
+                    :key="tag"
+                    closable
+                    size="medium"
+                    class="qustion-tag"
+                    :disable-transitions="false"
+                    :disabled="readonly"
+                    @close="handleTagDelete(tag)"
+                >
+                    {{ tag }}
+                </el-tag>
+                <el-input
+                    v-if="tagInputVisible"
+                    ref="saveTagInput"
+                    v-model="tagValue"
+                    class="input-new-tag"
+                    size="small"
+                    :disabled="readonly"
+                    @keyup.enter.native="handleTagConfirm"
+                    @blur="handleTagConfirm"
+                />
+                <el-button v-else class="button-new-tag" size="small" @click="showTagEdit">+ 添 加</el-button>
+            </el-form-item>
+            <el-form-item label="附图:" prop="fu_tu_">
+                <ibps-image
+                    v-model="form.fu_tu_"
+                    height="100"
+                    width="100"
+                    accept=".jpg,.jpeg,.png,.gif,.bmp,.webp"
+                    multiple
+                    download
+                    :disabled="readonly"
+                    size=""
+                />
+            </el-form-item>
+            <el-form-item label="难度:" prop="nan_du_">
+                <el-rate
+                    v-model="form.nan_du_"
+                    show-text
+                    :texts="texts"
+                    :colors="colors"
+                />
+            </el-form-item>
+            <el-form-item label="分值:" prop="fen_zhi_" :maxlength="8">
+                <el-input-number
+                    v-model="form.fen_zhi_"
+                    :min="1"
+                    :max="100"
+                    :precision="0"
+                    type="number"
+                    :disabled="readonly"
+                    placeholder="请输入题目分值"
+                />
+            </el-form-item>
+            <!-- <div class="inline-item" :class="['单选题', '多选题'].includes(form.ti_xing_) ? '' : 'mb-20'">
+                <el-form-item label="分值:" prop="fen_zhi_" :maxlength="8">
+                    <el-input-number
+                        v-model="form.fen_zhi_"
+                        :min="1"
+                        :max="100"
+                        :precision="0"
+                        type="number"
+                        placeholder="请输入题目分值"
+                    />
+                </el-form-item>
+                <el-form-item v-if="['单选题', '多选题'].includes(form.ti_xing_)" label="选项类型:" prop="xuan_xiang_lei_xi">
+                    <el-radio-group v-model="form.xuan_xiang_lei_xi" disabled>
+                        <el-radio-button label="文本">文本</el-radio-button>
+                        <el-radio-button label="图片">图片</el-radio-button>
+                    </el-radio-group>
+                </el-form-item>
+            </div> -->
+            <template v-if="['单选题', '多选题'].includes(form.ti_xing_)">
+                <el-form-item
+                    v-for="(item, index) in optionList"
+                    :key="`${item.value}${index}`"
+                    class="option-item"
+                >
+                    <template slot="label">
+                        <div class="custom-label">{{ `选项${item.value}:` }}</div>
+                    </template>
+                    <el-input
+                        v-model="item.content"
+                        type="textarea"
+                        :rows="1"
+                        :autosize="readonly"
+                        :disabled="readonly"
+                        placeholder="请输入选项内容,最多可配置8个选项"
+                    />
+                    <el-radio-group v-if="form.ti_xing_ === '单选题'" v-model="item.radio">
+                        <el-radio :label="item.value" @change="changeRadio(index, item.value)">正确选项</el-radio>
+                    </el-radio-group>
+                    <el-checkbox-group v-else-if="form.ti_xing_ === '多选题'" v-model="item.checkbox">
+                        <el-checkbox :label="item.value">正确选项</el-checkbox>
+                    </el-checkbox-group>
+                    <div class="operate-btn">
+                        <el-button
+                            v-if="index === 0 && optionList.length < 8"
+                            type="primary"
+                            :tabindex="-1"
+                            icon="el-icon-plus"
+                            circle
+                            @click="addOption"
+                        />
+                        <el-button
+                            v-else-if="index === optionList.length - 1"
+                            type="danger"
+                            :tabindex="-1"
+                            icon="el-icon-delete"
+                            circle
+                            @click="subOption"
+                        />
+                    </div>
+                </el-form-item>
+            </template>
+            <template v-else-if="form.ti_xing_ === '填空题'">
+                <el-form-item
+                    v-for="(item, index) in optionList"
+                    :key="`${item.value}${index}`"
+                    class="option-item"
+                >
+                    <template slot="label">
+                        <div class="custom-label">{{ `答案${index + 1}:` }}</div>
+                    </template>
+                    <el-input
+                        v-model="item.content"
+                        type="textarea"
+                        :rows="1"
+                        :autosize="readonly"
+                        :disabled="readonly"
+                        placeholder="请输入答案内容,最多可配置20个答案"
+                    />
+                    <div v-if="!readonly" class="operate-btn">
+                        <el-button
+                            v-if="index === 0 && optionList.length < 20"
+                            type="primary"
+                            :tabindex="-1"
+                            icon="el-icon-plus"
+                            circle
+                            @click="addOption"
+                        />
+                        <el-button
+                            v-else-if="index === optionList.length - 1"
+                            type="danger"
+                            :tabindex="-1"
+                            icon="el-icon-delete"
+                            circle
+                            @click="subOption"
+                        />
+                    </div>
+                </el-form-item>
+            </template>
+            <template v-else-if="form.ti_xing_ === '判断题'">
+                <el-form-item prop="zheng_que_da_an_" label="答案:">
+                    <el-radio-group v-model="form.zheng_que_da_an_">
+                        <el-radio-button label="√">√</el-radio-button>
+                        <el-radio-button label="×">×</el-radio-button>
+                    </el-radio-group>
+                </el-form-item>
+            </template>
+            <template v-else-if="form.ti_xing_ === '简答题'">
+                <el-form-item prop="zheng_que_da_an_" label="答案:">
+                    <el-input
+                        v-model="form.zheng_que_da_an_"
+                        type="textarea"
+                        :rows="4"
+                        :autosize="readonly"
+                        :disabled="readonly"
+                        placeholder="请输入答案内容"
+                    />
+                </el-form-item>
+            </template>
+            <el-form-item label="备注:" prop="bei_zhu_">
+                <el-input
+                    v-model="form.bei_zhu_"
+                    type="textarea"
+                    :rows="2"
+                    :autosize="readonly"
+                    :disabled="readonly"
+                    placeholder="请输入题目备注信息"
+                />
+            </el-form-item>
+        </el-form>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar
+                :actions="toolbars"
+                @action-event="handleActionEvent"
+            />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import ActionUtils from '@/utils/action'
+import { questionType, rateType, defaultOptions } from '../constants'
+export default {
+    components: {
+        IbpsImage: () => import('@/business/platform/file/image')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        bankId: {
+            type: String,
+            default: ''
+        },
+        id: {
+            type: String,
+            default: ''
+        },
+        // 是否复制题目
+        isCopy: {
+            type: Boolean,
+            default: false
+        },
+        // 题库题目数据
+        quesData: {
+            type: Array,
+            default: () => []
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const { userList = [], deptList = [], userId } = this.$store.getters || {}
+        const defaultType = questionType.length ? questionType[0].value : ''
+        return {
+            colors: { 1: '#00FF00', 2: '#7FFF00', 3: '#FFFF00', 4: '#FFA500', 5: '#FF0000' },
+            texts: ['易', '偏易', '适中', '偏难', '难'],
+            userId,
+            userList,
+            questionType,
+            rateType,
+            deptList: deptList.filter(i => i.depth === 4),
+            title: this.readonly ? '题目详情' : this.id ? '编辑题目' : '添加题目',
+            formLabelWidth: '120px',
+            dialogVisible: this.visible,
+            dialogLoading: false,
+            tagInputVisible: false,
+            tagValue: '',
+            form: {
+                // bu_men_: '',
+                parent_id_: this.bankId,
+                chu_ti_ren_: userId,
+                chu_ti_shi_jian_: this.$common.getDateNow(19),
+                ti_gan_: '',
+                ti_xing_: defaultType,
+                fu_tu_: '',
+                xuan_xiang_lei_xi: '文本',
+                da_an_: '',
+                xuan_xiang_shu_: '',
+                zheng_que_da_an_: '',
+                ping_fen_fang_shi: rateType[defaultType] || '',
+                ping_fen_ren_: [userId],
+                fen_zhi_: '',
+                bei_zhu_: '',
+                zhuang_tai_: '启用',
+                nan_du_: 0
+            },
+            questionTags: [],
+            toolbars: [
+                {
+                    key: 'submit',
+                    icon: 'ibps-icon-save',
+                    label: '保存',
+                    hidden: () => {
+                        return this.readonly
+                    }
+                },
+                {
+                    key: 'submitAndContinue',
+                    icon: 'ibps-icon-send',
+                    label: '保存并继续',
+                    type: 'warning',
+                    hidden: () => {
+                        return this.readonly || this.id
+                    }
+                },
+                {
+                    key: 'reset',
+                    icon: 'ibps-icon-refresh',
+                    label: '重置',
+                    type: 'info',
+                    hidden: () => {
+                        return this.readonly || this.id
+                    }
+                },
+                { key: 'cancel', label: '关闭' }
+            ],
+            rules: {
+                ti_gan_: [{ required: true, message: this.$t('validate.required') }],
+                ti_xing_: [{ required: true, message: this.$t('validate.required') }],
+                da_an_: [{ required: true, message: this.$t('validate.required') }],
+                zheng_que_da_an_: [{ required: true, message: this.$t('validate.required') }],
+                xuan_xiang_lei_xi: [{ required: true, message: this.$t('validate.required') }],
+                ping_fen_fang_shi: [{ required: true, message: this.$t('validate.required') }],
+                ping_fen_ren_: [{ required: true, message: this.$t('validate.required') }],
+                chu_ti_ren_: [{ required: true, message: this.$t('validate.required') }],
+                fen_zhi_: [{ required: true, message: this.$t('validate.required') }]
+            },
+            optionList: JSON.parse(JSON.stringify(defaultOptions))
+        }
+    },
+    computed: {
+        formData () {
+            return this.data
+        },
+        questionRateType () {
+            return this.rateType[this.form.ti_xing_] || ''
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+            // immediate: true
+        }
+    },
+    mounted () {
+        this.getQuestionData()
+    },
+    methods: {
+        addOption () {
+            this.optionList.push({
+                value: String.fromCharCode(this.optionList[0].value.charCodeAt(0) + this.optionList.length),
+                radio: null,
+                checkbox: [],
+                content: ''
+            })
+        },
+        subOption () {
+            this.optionList.pop()
+        },
+        changeRadio (index, value) {
+            this.optionList.map(i => (i.radio = null))
+            this.optionList[index].radio = value
+        },
+        changeQuestionType (value) {
+            if (value === '填空题') {
+                this.optionList = [{
+                    value: '',
+                    radio: null,
+                    checkbox: [],
+                    content: ''
+                }]
+            } else {
+                this.optionList = JSON.parse(JSON.stringify(defaultOptions))
+            }
+            this.form.da_an_ = ''
+            this.form.zheng_que_da_an_ = ''
+            this.questionTags = []
+        },
+        handleTagDelete (tag) {
+            this.questionTags.splice(this.questionTags.indexOf(tag), 1)
+        },
+        showTagEdit () {
+            this.tagInputVisible = true
+            this.$nextTick(_ => {
+                this.$refs.saveTagInput.$refs.input.focus()
+            })
+        },
+        handleTagConfirm () {
+            if (this.tagValue) {
+                this.questionTags.push(this.tagValue)
+            }
+            this.tagInputVisible = false
+            this.tagValue = ''
+        },
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'submit':
+                    this.handleSubmit(key)
+                    break
+                case 'submitAndContinue':
+                    this.handleSubmit(key)
+                    break
+                case 'reset':
+                    this.resetForm()
+                    break
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        // 获取题库数据
+        getQuestionData () {
+            if (this.$utils.isEmpty(this.id)) {
+                return
+            }
+            const sql = `select id_, chu_ti_ren_, bu_men_, chu_ti_shi_jian_, ti_gan_, ti_xing_, xuan_xiang_lei_xi, biao_qian_, da_an_, zheng_que_da_an_, ping_fen_fang_shi, ping_fen_ren_, fen_zhi_, zhuang_tai_, xuan_xiang_shu_, fu_tu_, bei_zhu_,nan_du_ from t_questions where id_ = '${this.id}'`
+            this.$common.request('sql', sql).then(res => {
+                const { data = [] } = res.variables || {}
+                if (!data.length) {
+                    return this.$message.error('获取题目数据失败!')
+                }
+                const item = data[0]
+                if (item.ti_xing_ === '填空题') {
+                    const rightKey = item.zheng_que_da_an_ ? JSON.parse(item.zheng_que_da_an_) : []
+                    this.optionList = rightKey.map(i => ({
+                        value: '',
+                        radio: null,
+                        checkbox: [],
+                        content: i
+                    }))
+                } else if (item.ti_xing_ === '单选题') {
+                    const rightKey = item.zheng_que_da_an_ || ''
+                    const options = item.da_an_ ? JSON.parse(item.da_an_) : {}
+                    this.optionList = []
+                    Object.keys(options).forEach(key => {
+                        this.optionList.push({
+                            value: key,
+                            radio: key === rightKey ? key : null,
+                            checkbox: [],
+                            content: options[key]
+                        })
+                    })
+                } else if (item.ti_xing_ === '多选题') {
+                    const rightKey = item.zheng_que_da_an_ ? item.zheng_que_da_an_.split(',') : []
+                    const options = item.da_an_ ? JSON.parse(item.da_an_) : {}
+                    this.optionList = []
+                    Object.keys(options).forEach(key => {
+                        this.optionList.push({
+                            value: key,
+                            radio: null,
+                            checkbox: rightKey.indexOf(key) > -1 ? [key] : [],
+                            content: options[key]
+                        })
+                    })
+                }
+                item.nan_du_ = +item.nan_du_ || 0
+                item.fen_zhi_ = parseInt(item.fen_zhi_)
+                item.fu_tu_ = item.fu_tu_ ? JSON.parse(item.fu_tu_) : ''
+                item.ping_fen_ren_ = item.ping_fen_ren_ ? item.ping_fen_ren_.split(',') : []
+                this.questionTags = item.biao_qian_ ? item.biao_qian_.split(',') : []
+                this.form = item
+            })
+        },
+        handleSubmit (action) {
+            this.$refs.form.validate((valid) => {
+                if (!valid) {
+                    return ActionUtils.saveErrorMessage()
+                }
+                // 验证选项、答案内容(未被表单校验)
+                const { ti_xing_: questionType } = this.form
+                if (['单选题', '多选题', '填空题'].includes(questionType)) {
+                    const emptyIndex = this.optionList.findIndex(item => !item.content)
+                    if (emptyIndex !== -1) {
+                        const tip1 = `答案${(emptyIndex + 1)}内容为空,请填写后再保存!`
+                        const tip2 = `选项${(String.fromCharCode('A'.charCodeAt(0) + emptyIndex))}内容为空,请填写后再保存!`
+                        return ActionUtils.saveErrorMessage(questionType === '填空题' ? tip1 : tip2)
+                    }
+                    const hasRadioKey = this.optionList.some(item => item.radio)
+                    const hasMultipleKey = this.optionList.some(item => item.checkbox && item.checkbox.length)
+                    const hasKey = {
+                        '单选题': hasRadioKey,
+                        '多选题': hasMultipleKey
+                    }
+                    if (['单选题', '多选题'].includes(questionType) && !hasKey[questionType]) {
+                        return ActionUtils.saveErrorMessage('请至少选择一个正确答案!')
+                    }
+                }
+                this.getSubmitData()
+                // 复制题目保存时不经过接口提交
+                if (this.isCopy) {
+                    return this.updateQuesData()
+                }
+                this.submitForm(action)
+            })
+        },
+        getSubmitData () {
+            let a1 = ''
+            const a2 = []
+            const a3 = []
+            const options = {}
+            this.optionList.forEach(item => {
+                options[item.value] = item.content
+                if (item.radio) {
+                    a1 = item.radio
+                }
+                if (item.checkbox && item.checkbox.length) {
+                    a2.push(item.checkbox[0])
+                }
+                a3.push(item.content)
+            })
+            const { first, second } = this.$store.getters.level || {}
+            this.form.di_dian_ = second || first
+            this.form.biao_qian_ = this.questionTags.join(',')
+            this.form.ping_fen_fang_shi = this.questionRateType
+            switch (this.form.ti_xing_) {
+                case '单选题':
+                    this.form.ping_fen_ren_ = ''
+                    this.form.da_an_ = JSON.stringify(options)
+                    this.form.zheng_que_da_an_ = a1
+                    break
+                case '多选题':
+                    this.form.ping_fen_ren_ = ''
+                    this.form.da_an_ = JSON.stringify(options)
+                    this.form.zheng_que_da_an_ = a2.join(',')
+                    break
+                case '填空题':
+                    this.form.ping_fen_ren_ = this.form.ping_fen_ren_ ? this.form.ping_fen_ren_.join(',') : ''
+                    this.form.xuan_xiang_lei_xi = ''
+                    this.form.da_an_ = ''
+                    this.form.xuan_xiang_shu_ = a3.length
+                    this.form.zheng_que_da_an_ = JSON.stringify(a3)
+                    break
+                case '判断题':
+                    this.form.ping_fen_ren_ = ''
+                    this.form.xuan_xiang_lei_xi = ''
+                    this.form.da_an_ = ''
+                    break
+                case '简答题':
+                    this.form.ping_fen_ren_ = this.form.ping_fen_ren_ ? this.form.ping_fen_ren_.join(',') : ''
+                    this.form.xuan_xiang_lei_xi = ''
+                    this.form.da_an_ = ''
+                    break
+                default:
+                    break
+            }
+        },
+        updateQuesData () {
+            const newData = {
+                quesId: this.id,
+                creator: this.userId,
+                createDept: '',
+                createTime: this.$common.getDateNow(19),
+                sn: '',
+                content: this.form.ti_gan_,
+                quesType: this.form.ti_xing_,
+                img: this.form.fu_tu_,
+                optionType: this.form.xuan_xiang_lei_xi,
+                answer: this.form.da_an_,
+                rightKey: this.form.zheng_que_da_an_,
+                rateType: this.form.ping_fen_fang_shi,
+                rater: this.form.ping_fen_ren_,
+                score: this.form.fen_zhi_,
+                note: this.form.bei_zhu_,
+                optionCount: this.form.xuan_xiang_shu_,
+                quesTag: this.form.biao_qian_,
+                quesState: this.form.zhuang_tai_
+            }
+            const temp = JSON.parse(JSON.stringify(this.quesData))
+            temp.forEach((item, index) => {
+                if (item.quesId === this.id) {
+                    temp[index] = newData
+                }
+            })
+            this.$emit('update', temp)
+            this.closeDialog()
+        },
+        submitForm (action) {
+            const addParams = {
+                tableName: 't_questions',
+                paramWhere: [this.form]
+            }
+            const updateParams = {
+                tableName: 't_questions',
+                updList: [
+                    {
+                        where: {
+                            id_: this.id
+                        },
+                        param: this.form
+                    }
+                ]
+            }
+            // console.log(this.form, addParams)
+            // return
+            const type = this.id && !this.isCopy ? 'update' : 'add'
+            const params = type === 'add' ? addParams : updateParams
+            this.$common.request(type, params).then(() => {
+                this.$message.success(this.id ? '保存题目成功' : '添加题目成功')
+                this.updatePaper()
+                // 题库中编辑后更新题库处显示的题目信息
+                if (this.id && !this.isCopy) {
+                    return this.updateQuesData()
+                }
+                if (action === 'submit') {
+                    this.closeDialog()
+                } else {
+                    this.resetForm()
+                }
+            })
+        },
+        resetForm () {
+            this.id = ''
+            const quesType = this.form.ti_xing_
+            this.$refs.form.resetFields()
+            this.changeQuestionType(quesType)
+        },
+        updatePaper () {
+            const sql = `select fen_zhi_ from t_questions where parent_id_ = '${this.bankId}'`
+            this.$common.request('sql', sql).then(res => {
+                const { data = [] } = res.variables || {}
+                const params = {
+                    tableName: 't_question_bank',
+                    updList: [
+                        {
+                            where: {
+                                id_: this.bankId
+                            },
+                            param: {
+                                ti_shu_: data.length,
+                                zong_fen_: data.reduce((sum, item) => sum + parseInt(item.fen_zhi_), 0)
+                            }
+                        }
+                    ]
+                }
+                this.$common.request('update', params).then(() => {
+                    console.log('更新题库信息成功')
+                })
+            })
+        },
+        // 关闭当前窗口
+        closeDialog () {
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .question-dialog {
+        ::v-deep {
+            .el-rate{
+                padding: 6px 0 0 0;
+            }
+            .el-dialog {
+                min-width: 1080px;
+            }
+            .el-dialog__body {
+                height: calc(88vh - 200px);
+            }
+            .el-form-item {
+                margin-bottom: 14px !important;
+                &:last-child {
+                    margin-bottom: 0 !important;
+                }
+                .el-form-item__label {
+                    font-size: 14px !important;
+                }
+            }
+            .el-form-item--small .el-form-item__error {
+                padding-top: 6px;
+            }
+            .el-radio-button__orig-radio {
+                &:disabled + .el-radio-button__inner {
+                    color: #606266;
+                }
+                &:disabled:checked + .el-radio-button__inner {
+                    color: #fff;
+                    background-color: #409EFF;
+                }
+            }
+        }
+        .readonly-form {
+            ::v-deep {
+                .el-radio, .el-checkbox, .el-radio-button, .el-input, .el-select, .el-textarea, .el-input-number, .el-button, .el-upload {
+                    pointer-events: none;
+                }
+            }
+        }
+        .qustion-tag {
+            margin-right: 10px;
+            height: 32px;
+            line-height: 30px;
+            ::v-deep {
+                .el-icon-close {
+                    top: 0px;
+                }
+            }
+        }
+        .button-new-tag {
+            height: 32px;
+            line-height: 30px;
+            padding-top: 0;
+            padding-bottom: 0;
+        }
+        .input-new-tag {
+            width: 100px;
+            vertical-align: bottom;
+        }
+        .question-form {
+            padding: 20px;
+        }
+        .ibps-image {
+            font-size: 0;
+            ::v-deep {
+                .el-upload--picture-card {
+                    font-size: 24px;
+                }
+            }
+        }
+        .inline-item {
+            display: flex;
+            .el-form-item {
+                flex: 1;
+                .el-select {
+                    width: 100%;
+                }
+            }
+        }
+        .mb-20 {
+            margin-bottom: 20px;
+        }
+        .option-item {
+            ::v-deep {
+                .el-form-item__content {
+                    display: flex;
+                    .el-input {
+                        flex: 1;
+                    }
+                    .el-radio-group, .el-checkbox-group {
+                        width: 100px;
+                        margin-left: 10px;
+                        line-height: 28px;
+                        .el-radio {
+                            line-height: 28px;
+                        }
+                    }
+                    .operate-btn {
+                        width: 32px;
+                        min-width: 32px;
+                        margin-left: 10px;
+                        line-height: 30px;
+                    }
+                }
+            }
+            .custom-label {
+                &::before {
+                    content: '*';
+                    color: #F56C6C;
+                    margin-right: 4px;
+                }
+            }
+        }
+    }
+</style>

+ 568 - 0
src/views/platform/examination/question/judge.vue

@@ -0,0 +1,568 @@
+<template>
+    <el-dialog
+        v-loading="loading"
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        fullscreen
+        class="dialog paper-judge-dialog"
+        top="0"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <div class="container">
+            <div class="question">
+                <template v-for="(item, index) in questionList">
+                    <div
+                        v-if="showIndex === index + 1"
+                        :key="item.dataId"
+                        class="question-item"
+                    >
+                        <div class="type">{{ item.questionType }}</div>
+                        <div class="stem">
+                            <span>{{ `【${index + 1}】${item.stem}` }}</span>
+                            <el-tag type="info" size="small">{{ `${item.score}分` }}</el-tag>
+                        </div>
+                        <div v-if="item.img && item.img.length" class="img">
+                            <ibps-image
+                                :value="item.img"
+                                height="100"
+                                width="100"
+                                accept=".jpg,.jpeg,.png,.gif,.bmp,.webp"
+                                download
+                                disabled
+                            />
+                        </div>
+                        <div class="answer">
+                            <div class="mine">
+                                <div class="title">考生答案:</div>
+                                <div class="content">
+                                    <el-input
+                                        v-for="(o, i) in item.answer"
+                                        :key="`${index}${i}`"
+                                        :value="o"
+                                        :type="item.questionType === '简答题' ? 'textarea' : 'text'"
+                                        :rows="item.questionType === '简答题' ? 12 : 1"
+                                        readonly
+                                    />
+                                </div>
+                            </div>
+                            <div class="right">
+                                <div class="title">参考答案:</div>
+                                <div class="content">
+                                    <el-input
+                                        v-for="(o, i) in item.rightKey"
+                                        :key="`${index}${i}`"
+                                        :value="o"
+                                        :type="item.questionType === '简答题' ? 'textarea' : 'text'"
+                                        :rows="item.questionType === '简答题' ? 12 : 1"
+                                        readonly
+                                    />
+                                </div>
+                            </div>
+                        </div>
+                        <div class="result">
+                            <div class="title">试题得分
+                                <el-tooltip effect="dark" content="请输入根据考生答案与参考答案给出题目得分" placement="top">
+                                    <i class="el-icon-question" />
+                                </el-tooltip>
+                            </div>
+                            <el-tooltip effect="dark" content="错误,该题计0分" placement="top">
+                                <el-button type="danger" icon="el-icon-close" circle @click="changeScore(index, 0)" />
+                            </el-tooltip>
+                            <el-input-number
+                                v-model="item.resultScore"
+                                :min="0"
+                                :max="item.score"
+                                :precision="1"
+                                class="score-input"
+                                placeholder=""
+                            />
+                            <el-tooltip effect="dark" content="正确,该题计满分" placement="top">
+                                <el-button type="success" icon="el-icon-check" circle @click="changeScore(index, item.score)" />
+                            </el-tooltip>
+                            <el-tooltip effect="dark" content="取消已有评分,未评分的题目不会被提交" placement="top">
+                                <el-button type="warning" icon="el-icon-delete" @click="changeScore(index, undefined)">取消评分</el-button>
+                            </el-tooltip>
+                        </div>
+                    </div>
+                </template>
+            </div>
+
+            <div class="question-link">
+                <div class="tabs">
+                    <el-tabs v-model="activeName">
+                        <el-tab-pane v-for="(item) in pageTotal" :key="item" :label="pageLabel(item)" :name="item+''" />
+                    </el-tabs>
+                </div>
+                <div class="question">
+                    <template v-for="(item, index) in questionList">
+                        <div
+                            v-if="index+1>=pageRange(pagination.currentPage).start &&index+1<=pageRange(pagination.currentPage).end"
+                            :key="item.dataId"
+                            class="link-item"
+                            :class="setClassName(item, index)"
+                            @click="showIndex = index + 1"
+                        >{{ index + 1 }}</div>
+                    </template>
+                </div>
+            </div>
+        </div>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar
+                :actions="toolbars"
+                @action-event="handleActionEvent"
+            />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+export default {
+    components: {
+        IbpsImage: () => import('@/business/platform/file/image')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        id: {
+            type: String,
+            default: ''
+        }
+    },
+    data () {
+        const { userId } = this.$store.getters || {}
+        return {
+            pagination: {
+                currentPage: 1,
+                pageSize: 100
+            },
+            activeName: '1',
+            title: '试题评阅',
+            dialogVisible: this.visible,
+            loading: false,
+            toolbars: [
+                {
+                    key: 'prev',
+                    icon: 'ibps-icon-chevron-left',
+                    label: '上一题',
+                    disabled: () => {
+                        return this.showIndex === 1
+                    }
+                },
+                {
+                    key: 'next',
+                    icon: 'ibps-icon-chevron-right',
+                    label: '下一题',
+                    disabled: () => {
+                        return this.showIndex === this.questionList.length
+                    }
+                },
+                {
+                    key: 'submit',
+                    icon: 'ibps-icon-send',
+                    label: '提交',
+                    hidden: () => {
+                        return this.readonly
+                    }
+                },
+                { key: 'cancel', label: '退出' }
+            ],
+            questionList: [],
+            showIndex: 1,
+            userId
+        }
+    },
+    computed: {
+        formData () {
+            return this.data
+        },
+        pageTotal () {
+            return Math.ceil(this.questionList.length / this.pagination.pageSize)
+        }
+    },
+    watch: {
+        activeName: {
+            handler (val) {
+                this.pagination.currentPage = +val
+            }
+        },
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+            // immediate: true
+        },
+        showIndex: {
+            handler (val, oldVal) {
+                this.pagination.currentPage = Math.ceil(val / this.pagination.pageSize)
+                this.activeName = this.pagination.currentPage + ''
+                const temp = this.questionList[oldVal - 1]
+            }
+        }
+    },
+    mounted () {
+        this.loadData()
+        // 监听键盘事件
+        window.addEventListener('keyup', this.handleKeyPress)
+    },
+    beforeDestroy () {
+        window.removeEventListener('keyup', this.handleKeyPress)
+    },
+    methods: {
+        pageRange (pageNo) {
+            return {
+                start: (pageNo - 1) * this.pagination.pageSize + 1,
+                end: pageNo * this.pagination.pageSize > this.questionList.length ? this.questionList.length : pageNo * this.pagination.pageSize
+            }
+        },
+        pageLabel (pageNo) {
+            const { start, end } = this.pageRange(pageNo)
+            return `${start}-${end}`
+        },
+        // 获取题库数据
+        async loadData () {
+            this.questionList = await this.getQuestionData()
+            console.log(this.questionList)
+        },
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'prev':
+                    this.showIndex--
+                    break
+                case 'next':
+                    this.showIndex++
+                    break
+                case 'submit':
+                    this.handleSubmit()
+                    break
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        getQuestionData () {
+            this.loading = true
+            const param = this.id ? ` and find_in_set(ex.exam_id_, '${this.id}')` : ''
+            // const sql = `select id_ as dataId, parent_id_ as paperId, ti_mu_id_ as questionId, ti_gan_ as stem, ti_xing_ as questionType, fu_tu_ as img, xuan_xiang_lei_xi as optionType, fen_zhi_ as score, ping_fen_fang_shi as rateType, ping_fen_ren_ as rater, hui_da_ as answer, can_kao_da_an_ as rightKey, shi_fou_yi_yue_ as isRate from t_examination_detail where ping_fen_ren_ = '${this.userId}' and shi_fou_yi_yue_ = '否' and parent_id_ in (select id_ from t_examination where zhuang_tai_ = '已交卷'${param}) order by field(ti_xing_, '单选题', '多选题', '判断题', '填空题', '简答题')`
+            // const sql = `select ed.id_ as dataId, ed.parent_id_ as paperId, ed.ti_mu_id_ as questionId, ed.ti_gan_ as stem, ed.ti_xing_ as questionType, ed.fu_tu_ as img, ed.xuan_xiang_lei_xi as optionType, ed.fen_zhi_ as score, ed.ping_fen_fang_shi as rateType, ed.ping_fen_ren_ as rater, ed.hui_da_ as answer, ed.can_kao_da_an_ as rightKey, ed.shi_fou_yi_yue_ as isRate from t_examination_detail ed join t_examination ex on ed.parent_id_ = ex.id_ left join t_question_bank qb on ex.ti_ku_id_ = qb.id_ where ex.zhuang_tai_ = '已交卷' and ed.shi_fou_yi_yue_ = '否' and (ed.ping_fen_ren_ like '%${this.userId}%' or qb.ping_fen_ren_ like '%${this.userId}%')${param} order by field(ed.ti_xing_, '单选题', '多选题', '判断题', '填空题', '简答题')`
+            const sql = `select ed.id_ as dataId, ed.parent_id_ as paperId, ed.ti_mu_id_ as questionId, ed.ti_gan_ as stem, ed.ti_xing_ as questionType, ed.fu_tu_ as img, ed.xuan_xiang_lei_xi as optionType, ed.fen_zhi_ as score, ed.ping_fen_fang_shi as rateType, ed.ping_fen_ren_ as rater, ed.hui_da_ as answer, ed.can_kao_da_an_ as rightKey, ed.shi_fou_yi_yue_ as isRate from t_examination_detail ed join t_examination ex on ed.parent_id_ = ex.id_ LEFT JOIN t_exams exam ON ex.exam_id_ = exam.id_ where ex.zhuang_tai_ = '已交卷' and ed.shi_fou_yi_yue_ = '否' and (ed.ping_fen_ren_ like '%${this.userId}%' or exam.ping_fen_ren_ like '%${this.userId}%')${param} order by field(ed.ti_xing_, '单选题', '多选题', '判断题', '填空题', '简答题')`
+            return new Promise((resolve, reject) => {
+                this.$common.request('sql', sql).then(res => {
+                    const { data = [] } = res.variables || {}
+                    if (!data.length) {
+                        this.$message.warning('没有试卷需要评阅!')
+                        return this.closeDialog()
+                    }
+                    data.map(item => {
+                        if (item.questionType === '填空题') {
+                            item.rightKey = item.rightKey ? JSON.parse(item.rightKey) : []
+                            item.answer = item.answer ? JSON.parse(item.answer) : Array(item.rightKey.length).fill('')
+                        } else {
+                            item.answer = [item.answer]
+                            item.rightKey = [item.rightKey]
+                        }
+                        item.score = parseInt(item.score)
+                        item.img = item.img ? JSON.parse(item.img) : ''
+                        item.resultScore = undefined
+                    })
+                    this.loading = false
+                    resolve(data)
+                }).catch(error => {
+                    this.loading = false
+                    reject(error)
+                })
+            })
+        },
+        setClassName (item, index) {
+            const result = []
+            if (item.resultScore !== undefined) {
+                result.push('finished')
+            }
+            if (this.showIndex === index + 1) {
+                result.push('active')
+            }
+            return result.join(' ')
+        },
+        handleKeyPress (event) {
+            if (event.keyCode === 37 || event.key === 'ArrowLeft') {
+                if (this.showIndex === 1) {
+                    this.$message.warning('已经是第一题了!')
+                    return
+                }
+                this.showIndex--
+            } else if (event.keyCode === 39 || event.key === 'ArrowRight') {
+                if (this.showIndex === this.questionList.length) {
+                    this.$message.warning('已经是最后一题了!')
+                    return
+                }
+                this.showIndex++
+            } else if (event.keyCode === 27 || event.key === 'Esc') {
+                this.closeDialog()
+            }
+        },
+        changeScore (index, score) {
+            this.questionList[index].resultScore = score
+            if (this.showIndex !== this.questionList.length) {
+                this.showIndex++
+            }
+        },
+        getScore ({ questionType, answer, rightKey, score }) {
+            if (questionType === '多选题') {
+                const temp = JSON.parse(rightKey)
+                return answer.length === temp.length && answer.every(i => temp.includes(i)) ? score : 0
+            } else {
+                return answer === rightKey ? score : 0
+            }
+        },
+        dealData (data) {
+            const paperIdList = []
+            const updList = []
+            const time = this.$common.getDateNow(19)
+            data.forEach(item => {
+                if (paperIdList.indexOf(item.paperId) === -1) {
+                    paperIdList.push(item.paperId)
+                }
+                updList.push({
+                    where: {
+                        id_: item.dataId
+                    },
+                    param: {
+                        shi_fou_yi_yue_: '是',
+                        ping_yue_shi_jian: time,
+                        de_fen_: item.resultScore
+                    }
+                })
+            })
+            return { updList, paperIdList }
+        },
+        handleSubmit () {
+            const completeList = this.questionList.filter(item => item.resultScore !== undefined && item.resultScore !== null && item.resultScore !== '')
+            console.log(completeList)
+            if (!completeList.length) {
+                return this.$message.warning('您还未评阅试题!')
+            }
+            this.$confirm('提交后已评阅试题将不再显示,是否确认提交?', '提示', {
+                type: 'info',
+                confirmButtonText: '确认',
+                cancelButtonText: '取消',
+                showClose: false,
+                closeOnClickModal: false
+            }).then(() => {
+                const submitData = this.dealData(completeList)
+                this.submitForm(submitData)
+            }).catch(() => {})
+        },
+        submitForm ({ updList, paperIdList }) {
+            const updateParams = {
+                tableName: 't_examination_detail',
+                updList
+            }
+            console.log(updateParams)
+            this.$common.request('update', updateParams).then(() => {
+                this.$message.success('提交成功!')
+                this.updatePaperInfo(paperIdList.join(','))
+                this.closeDialog()
+            })
+        },
+        updatePaperInfo (ids) {
+            const sql = `select parent_id_ as paperId, shi_fou_yi_yue_ as isRate, de_fen_ as score from t_examination_detail where find_in_set(parent_id_, '${ids}')`
+            this.$common.request('sql', sql).then(res => {
+                const { data = [] } = res.variables || {}
+                if (!data.length) {
+                    return
+                }
+                const temp = {}
+                data.forEach(item => {
+                    if (temp[item.paperId]) {
+                        temp[item.paperId].push(item)
+                    } else {
+                        temp[item.paperId] = [item]
+                    }
+                })
+                const updateObj = {}
+                Object.keys(temp).forEach(key => {
+                    const isAllRate = temp[key].every(item => item.isRate === '是')
+                    if (isAllRate) {
+                        updateObj[key] = [{
+                            where: { id_: key },
+                            param: {
+                                zhuang_tai_: '已完成',
+                                de_fen_: temp[key].reduce((sum, item) => sum + parseFloat(item.score), 0)
+                            }
+                        }]
+                    }
+                })
+                console.log(updateObj)
+                // return
+                Object.keys(updateObj).forEach(key => {
+                    const updateParams = {
+                        tableName: 't_examination',
+                        updList: updateObj[key]
+                    }
+                    this.$common.request('update', updateParams)
+                })
+            })
+        },
+        // 关闭当前窗口
+        closeDialog () {
+            window.removeEventListener('keyup', this.handleKeyPress)
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .paper-judge-dialog {
+        ::v-deep {
+            .el-dialog__body {
+                height: calc(100vh - 55px);
+                width: 1080px;
+                margin: 0 auto;
+                box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+                .el-radio {
+                    display: block;
+                    margin-bottom: 10px;
+                }
+                .el-radio__label, .el-checkbox__label {
+                    font-size: 16px;
+                    white-space: normal;
+                    line-height: 1.5;
+                }
+                .el-input {
+                    margin-bottom: 10px;
+                }
+                .el-tag {
+                    margin-right: 0;
+                }
+                .list-group {
+                    display: inline-block;
+                    width: 100%;
+                }
+            }
+            .el-dialog__header {
+                text-align: center;
+            }
+        }
+        .container {
+            position: relative;
+            height: calc(100vh - 160px);
+            padding: 20px;
+            .question {
+                font-size: 16px;
+                .question-item {
+                    margin-bottom: 10px;
+                    .type {
+                        font-size: 20px;
+                        font-weight: 600;
+                        margin-bottom: 20px;
+                    }
+                    .stem {
+                        display: flex;
+                        margin-bottom: 20px;
+                        align-items: flex-start;
+                        > span:first-child {
+                            line-height: 1.5;
+                            margin-right: 10px;
+                        }
+                    }
+                    .img {
+                        position: relative;
+                        width: 100%;
+                        margin-bottom: 10px;
+                    }
+                    .answer {
+                        border: 1px solid #ccc;
+                        display: flex;
+                        .mine, .right {
+                            flex: 1;
+                            padding: 10px;
+                            .title {
+                                font-size: 16px;
+                                color: #01a39e;
+                                margin-bottom: 10px;
+                            }
+                        }
+                        .mine {
+                            background-color: #f5f5f5;
+                            border-right: 1px solid #ccc;
+                        }
+                    }
+                    .result {
+                        position: absolute;
+                        bottom: 170px;
+                        margin-top: 20px;
+                        .title {
+                            display: inline-block;
+                            font-size: 16px;
+                            margin-right: 20px;
+                            margin-bottom: 0;
+                        }
+                        .score-input {
+                            margin: 0 10px;
+                            width: 150px;
+                        }
+                    }
+                }
+            }
+            .question-link {
+                display: flex;
+                flex-direction: column;
+                position: absolute;
+                width: calc(100% - 40px);
+                bottom: 20px;
+                height: 140px;
+
+                .question{
+                    display: flex;
+                    flex-wrap: wrap;
+                    align-content: flex-start;
+                    height: 80px;
+                    overflow-y: auto;
+                }
+
+                .el-progress {
+                    width: 100%;
+                    margin-bottom: 10px;
+                }
+                .link-item {
+                    width: 32px;
+                    height: 32px;
+                    text-align: center;
+                    line-height: 32px;
+                    border: 1px solid #DCDFE6;
+                    margin: 3px;
+                    border-radius: 2px;
+                    cursor: pointer;
+                    &:hover {
+                        background-color: #F5F7FA;
+                    }
+                }
+                $button-themes: (
+                    active: (#409EFF, #fff, #409EFF),
+                    incomplete: (#E6A23C, #fff, #E6A23C),
+                    finished: (#67C23A, #fff, #67C23A)
+                );
+                @each $theme, $colors in $button-themes {
+                    .#{$theme} {
+                        background-color: nth($colors, 1);
+                        color: nth($colors, 2);
+                        border-color: nth($colors, 3);
+
+                        &:hover {
+                            background-color: lighten(nth($colors, 1), 10%);
+                            border-color: lighten(nth($colors, 1), 10%);
+                        }
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 670 - 0
src/views/platform/examination/questionBank/detail.vue

@@ -0,0 +1,670 @@
+<template>
+    <el-dialog
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        fullscreen
+        class="dialog paper-detail-dialog"
+        top="0"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <div class="container">
+            <div class="paper-info">
+                <div class="info-item">
+                    <span class="label">题库名称:</span>
+                    <el-tooltip class="item" effect="dark" :content="paperData.paperName" placement="top-start">
+                        <span class="value">{{ paperData.paperName }}</span>
+                    </el-tooltip>
+                </div>
+                <div class="info-item">
+                    <span class="label">题目数量:</span>
+                    <span class="value">{{ paperData.totalCount }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">总分:</span>
+                    <span class="value">{{ paperData.totalScore }}分</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">达标占比:</span>
+                    <span class="value">{{ paperData.qualifiedRadio }}%</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">计分方式:</span>
+                    <span class="value">{{ paperData.scoringType }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">得分:</span>
+                    <span class="value">{{ paperData.status !== '已完成' ? '未评分' : `${paperData.resultScore}分` }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">报考时间:</span>
+                    <span class="value">{{ paperData.applyTime }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">开考时间:</span>
+                    <span class="value">{{ paperData.startTime }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">结束时间:</span>
+                    <span class="value">{{ paperData.endTime }}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">考生姓名:</span>
+                    <span class="value">{{ transformUser(paperData.examinee) }}</span>
+                </div>
+                <!-- <div class="info-item">
+                    <span class="label">部门:</span>
+                    <span class="value">{{ paperData.dept }}</span>
+                </div> -->
+            </div>
+            <div class="select">
+                答题状态:<el-select v-model="paperStatus" placeholder="请选择">
+                    <el-option
+                        v-for="item in paperoptions"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                    />
+                </el-select>
+            </div>
+
+            <div class="question">
+                <div
+                    v-for="(item, index) in showPaperData.list"
+                    :key="index"
+                    class="question-item"
+                >
+                    <div v-if="item.questions.length > 0" class="type">
+                        {{ item.type }}<el-tag
+                            v-if="paperStatus === 'all'"
+                            type="primary"
+                            size="small"
+                            class="score"
+                        >{{ `共${item.totalScore}分` }}</el-tag>
+                    </div>
+                    <div
+                        v-for="(q, qIndex) in item.questions"
+                        :key="`${index}${qIndex}`"
+                        class="card"
+                    >
+                        <div class="stem">
+                            <span>{{ `【${qIndex + 1}】${q.stem}` }}</span>
+                            <el-tag type="primary" size="small">{{ `${q.questionScore}分` }}</el-tag>
+                        </div>
+                        <div v-if="q.img && q.img.length" class="img">
+                            <ibps-image
+                                v-model="q.img"
+                                height="100"
+                                width="100"
+                                accept=".jpg,.jpeg,.png,.gif,.bmp,.webp"
+                                download
+                                disabled
+                            />
+                        </div>
+                        <div class="answer">
+                            <div class="mine">
+                                <div class="title">
+                                    考生答案:<el-tag
+                                        :type="q.score && q.score !== '0' ? 'success' : 'danger'"
+                                        size="small"
+                                        class="score"
+                                    >{{
+                                        !q.score && q.score !== "0" ? "未评分" : `得${q.score}分`
+                                    }}</el-tag>
+                                </div>
+                                <div class="answer-content">
+                                    <el-radio-group v-if="q.questionType === '单选题'" :value="q.answer">
+                                        <el-radio
+                                            v-for="(o, i) in q.options"
+                                            :key="`${index}${qIndex}${i}`"
+                                            :label="o.label"
+                                        >{{ `${o.label}.${o.value}` }}</el-radio>
+                                    </el-radio-group>
+                                    <el-checkbox-group v-else-if="q.questionType === '多选题'" :value="q.answer">
+                                        <el-checkbox
+                                            v-for="(o, i) in q.options"
+                                            :key="`${index}${qIndex}${i}`"
+                                            :label="o.label"
+                                        >{{ `${o.label}.${o.value}` }}</el-checkbox>
+                                    </el-checkbox-group>
+                                    <el-radio-group v-else-if="q.questionType === '判断题'" :value="q.answer">
+                                        <el-radio-button label="√">√</el-radio-button>
+                                        <el-radio-button label="×">×</el-radio-button>
+                                    </el-radio-group>
+                                    <template v-else>
+                                        <el-input
+                                            v-for="(o, i) in q.answer"
+                                            :key="`${index}${qIndex}${i}`"
+                                            :value="o"
+                                            :type="q.questionType === '简答题' ? 'textarea' : 'text'"
+                                            :rows="q.questionType === '简答题' ? 12 : 1"
+                                            readonly
+                                        />
+                                    </template>
+                                </div>
+                            </div>
+                            <div class="right">
+                                <div class="title">参考答案:</div>
+                                <div class="answer-content">
+                                    <el-radio-group v-if="q.questionType === '单选题'" :value="q.rightKey">
+                                        <el-radio
+                                            v-for="(o, i) in q.options"
+                                            :key="`${index}${qIndex}${i}`"
+                                            :label="o.label"
+                                        >{{ `${o.label}.${o.value}` }}</el-radio>
+                                    </el-radio-group>
+                                    <el-checkbox-group v-else-if="q.questionType === '多选题'" :value="q.rightKey">
+                                        <el-checkbox
+                                            v-for="(o, i) in q.options"
+                                            :key="`${index}${qIndex}${i}`"
+                                            :label="o.label"
+                                        >{{ `${o.label}.${o.value}` }}</el-checkbox>
+                                    </el-checkbox-group>
+                                    <el-radio-group v-else-if="q.questionType === '判断题'" :value="q.rightKey">
+                                        <el-radio-button label="√">√</el-radio-button>
+                                        <el-radio-button label="×">×</el-radio-button>
+                                    </el-radio-group>
+                                    <template v-else>
+                                        <el-input
+                                            v-for="(o, i) in q.rightKey"
+                                            :key="`${index}${qIndex}${i}`"
+                                            :value="o"
+                                            :type="q.questionType === '简答题' ? 'textarea' : 'text'"
+                                            :rows="q.questionType === '简答题' ? 12 : 1"
+                                            readonly
+                                        />
+                                    </template>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="date-line">
+            <el-timeline>
+                <el-timeline-item
+                    v-for="(paper, index) in paperList"
+                    :key="index"
+                    :timestamp="paper.applyTime"
+                    :icon="paper.status === '已完成' ? 'el-icon-circle-check' : 'el-icon-view'"
+                    :type="paper.status === '已完成' ? 'success' : 'primary'"
+                    placement="top"
+                    size="large"
+                >
+                    <el-card
+                        class="timeline-card"
+                        :class="paper.dataId === showPaperId ? 'active-card' : ''"
+                        @click.native="changePaper(paper.dataId,paper.status)"
+                    >
+                        <div class="card-item">{{ `开始时间:${paper.startTime}` }}</div>
+                        <div class="card-item">{{ `结束时间:${paper.endTime}` }}</div>
+                        <div v-if="paper.status === '已完成'" class="card-item">
+                            <span>{{ `得分:${paper.resultScore}` }}</span>
+                            <el-tag
+                                :type="paper.isQualified ? 'success' : 'danger'"
+                                size="mini"
+                                class="score"
+                            >{{ paper.isQualified ? `达标` : "未达标" }}</el-tag>
+                            <el-tag
+                                v-if="paper.resultScore === maxScore"
+                                type="success"
+                                size="mini"
+                                class="score"
+                            >{{ `最高分` }}</el-tag>
+                            <el-tag
+                                v-if="paper.resultScore === minScore"
+                                type="warning"
+                                size="mini"
+                                class="score"
+                            >{{ `最低分` }}</el-tag>
+                        </div>
+                    </el-card>
+                </el-timeline-item>
+            </el-timeline>
+        </div>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar :actions="toolbars" @action-event="handleActionEvent" />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { cloneDeep } from 'lodash'
+export default {
+    components: {
+        IbpsImage: () => import('@/business/platform/file/image')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        bankId: {
+            type: String,
+            default: ''
+        },
+        examId: {
+            type: String,
+            default: ''
+        },
+        id: {
+            type: String,
+            default: ''
+        },
+        examineeId: {
+            type: String,
+            default: ''
+        }
+    },
+    data () {
+        return {
+            title: '考试详情',
+            dialogVisible: this.visible,
+            toolbars: [{ key: 'cancel', label: '退出' }],
+            paperList: [],
+            paperData: [],
+            maxScore: '',
+            minScore: '',
+            showPaperId: '',
+            paperoptions: [
+                { value: 'all', label: '全部' },
+                { value: 'false', label: '错误' },
+                { value: 'true', label: '正确' },
+                { value: 'noFinished', label: '未评分' }
+            ],
+            paperStatus: 'all'
+        }
+    },
+    computed: {
+        formData () {
+            return this.data
+        },
+        showPaperData: {
+            set () {}, // 解决报错
+            get () {
+                const showPaperData = cloneDeep(this.paperData)
+                showPaperData.list?.forEach((papers) => {
+                    papers.questions = papers.questions.filter((paper) => {
+                        switch (this.paperStatus) {
+                            case 'all':
+                                return true
+                            case 'false':
+                                return paper.score && paper.score === '0'
+                            case 'true':
+                                return paper.score && paper.score !== '0'
+                            case 'noFinished':
+                                return paper.score === ''
+                            default:
+                                break
+                        }
+                    })
+                })
+                return showPaperData
+            }
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+        }
+    },
+    mounted () {
+        this.loadData()
+        // 监听键盘事件
+        window.addEventListener('keyup', this.handleKeyPress)
+    },
+    beforeDestroy () {
+        window.removeEventListener('keyup', this.handleKeyPress)
+    },
+    methods: {
+        // 获取题库数据
+        async loadData () {
+            if (!this.bankId) {
+                this.$message.error('获取题目信息失败,请重试!')
+                this.closeDialog()
+                return
+            }
+            this.paperList = await this.getQuestionData()
+            this.paperData = this.paperList.find(i => i.dataId === this.id) || this.paperList[0]
+            this.showPaperId = this.paperData.dataId
+
+            const isAllNotFinish = this.paperList.every(item => item.status !== '已完成')
+            if (isAllNotFinish) {
+                this.$message.error('考试未完成或未评分,请稍后再试!')
+                this.closeDialog()
+                return
+            }
+        },
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        handleKeyPress (event) {
+            if (event.keyCode === 27 || event.key === 'Esc') {
+                this.closeDialog()
+            }
+        },
+        transformUser (userId) {
+            const { userList = [] } = this.$store.getters
+            const user = userList.find(u => u.userId === userId) || {}
+            return user.userName || '-'
+        },
+        getQuestionData () {
+            const param = this.examId ? `and e.exam_id_ = '${this.examId}'` : 'and e.exam_id_ is null'
+            const sql = `select e.id_ as dataId, e.exam_id_ as examId, e.kao_shi_ren_ as examinee, e.bu_men_ as dept, e.zhuang_tai_ as status, e.bao_ming_shi_jian as applyTime, e.kai_shi_shi_jian_ as startTime, e.jie_shu_shi_jian_ as endTime, e.ti_ku_zong_fen_ as totalScore, e.de_fen_ as resultScore, ed.ti_mu_id_ as questionId, ed.ti_gan_ as stem, ed.ti_xing_ as questionType, ed.fen_zhi_ as questionScore, ed.fu_tu_ as img, ed.xuan_xiang_lei_xi as optionsType, ed.xuan_xiang_ as options, ed.can_kao_da_an_ as rightKey, ed.ping_fen_fang_shi as rateType, ed.ping_fen_ren_ as rater, ed.hui_da_ as answer, ed.ping_yue_shi_jian as rateTime, ed.de_fen_ as score, ed.jie_xi_ as analysis, q.ti_ku_ming_cheng_ as paperName, case when e.exam_id_ is not null then ex.da_biao_zhan_bi_ else q.da_biao_zhan_bi_ end as qualifiedRadio, case when e.exam_id_ is not null then ex.ji_fen_fang_shi_ else q.ji_fen_fang_shi_ end as scoringType, case when e.exam_id_ is not null then ex.kao_shi_ming_chen else '自主考核' end as examName, case when e.exam_id_ is not null then ex.xian_kao_shi_jian else '不限' end as limitDate from t_examination e left join t_examination_detail ed on e.id_ = ed.parent_id_ left join  t_question_bank q on e.ti_ku_id_ = q.id_ left join  t_exams ex on e.exam_id_ = ex.id_ where e.ti_ku_id_ = '${this.bankId}' ${param} and e.kao_shi_ren_ = '${this.examineeId}' and (e.zhuang_tai_ = '已完成' or e.zhuang_tai_ = '已交卷') order by field(ed.ti_xing_, '单选题', '多选题', '判断题', '填空题', '简答题')`
+            return new Promise((resolve, reject) => {
+                this.$common.request('sql', sql).then(res => {
+                    const { data = [] } = res.variables || {}
+                    if (!data.length) {
+                        this.$message.error('未查询到已提交的考试记录,请先完成考试!')
+                        this.closeDialog()
+                        return
+                    }
+                    const result = []
+                    data.map(item => {
+                        // 数据转换
+                        if (['单选题', '多选题'].includes(item.questionType)) {
+                            item.options = JSON.parse(item.options)
+                            item.rightKey = item.questionType === '多选题' ? item.rightKey.split(',') : item.rightKey
+                            item.answer = item.questionType === '多选题' ? JSON.parse(item.answer) : item.answer
+                        } else if (item.questionType === '填空题') {
+                            item.rightKey = item.rightKey ? JSON.parse(item.rightKey) : []
+                            item.answer = item.answer ? JSON.parse(item.answer) : Array(item.rightKey.length).fill('')
+                        } else if (item.questionType === '简答题') {
+                            item.rightKey = [item.rightKey]
+                            item.answer = [item.answer]
+                        }
+                        item.img = item.img ? JSON.parse(item.img) : ''
+
+                        // 数据分组
+                        const index = result.findIndex(i => i.dataId === item.dataId)
+                        if (index === -1) {
+                            const { dataId, examinee, dept, status, applyTime, startTime, endTime, qualifiedRadio, paperName, totalScore, resultScore, scoringType } = item || {}
+                            result.push({
+                                dataId,
+                                examinee,
+                                dept,
+                                status,
+                                applyTime,
+                                startTime,
+                                endTime,
+                                qualifiedRadio,
+                                isQualified: status === '已完成' ? parseFloat(resultScore) >= (parseFloat(qualifiedRadio) / 100 * parseFloat(totalScore)) : '',
+                                paperName,
+                                totalScore: parseFloat(totalScore),
+                                resultScore: parseFloat(resultScore),
+                                // totalCount: data.length,
+                                scoringType,
+                                list: [{
+                                    type: item.questionType,
+                                    questions: [item]
+                                }]
+                            })
+                        } else {
+                            const qIndex = result[index].list.findIndex(i => i.type === item.questionType)
+                            if (qIndex === -1) {
+                                result[index].list.push({
+                                    type: item.questionType,
+                                    questions: [item]
+                                })
+                            } else {
+                                result[index].list[qIndex].questions.push(item)
+                            }
+                        }
+                    })
+                    result.forEach(item => {
+                        let count = 0
+                        item.list.forEach(i => {
+                            count += i.questions.length
+                            i.totalScore = i.questions.reduce((a, b) => a + parseFloat(b.questionScore), 0)
+                        })
+                        item.totalCount = count
+                    })
+                    // 获取最高分最低分
+                    const { maxScore, minScore } = result.filter(i => i.status === '已完成').reduce((acc, curr) => {
+                        if (curr.resultScore > acc.maxScore) {
+                            acc.maxScore = curr.resultScore
+                        }
+                        if (curr.resultScore < acc.minScore) {
+                            acc.minScore = curr.resultScore
+                        }
+                        return acc
+                    }, { maxScore: -Infinity, minScore: Infinity })
+                    this.maxScore = maxScore
+                    this.minScore = minScore
+                    resolve(result)
+                }).catch(error => {
+                    reject(error)
+                })
+            })
+        },
+        changePaper (id, status) {
+            if (status !== '已完成') {
+                this.$message.error('考试未完成或未评分,暂时无法查看!')
+                return
+            }
+            this.showPaperId = id
+            this.paperData = this.paperList.find(i => i.dataId === id)
+            console.log(id)
+            this.paperStatus = 'all'
+        },
+        // 关闭当前窗口
+        closeDialog () {
+            window.removeEventListener('keyup', this.handleKeyPress)
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .paper-detail-dialog {
+        ::v-deep {
+            .el-dialog__body {
+                position: relative;
+                height: calc(100vh - 115px);
+                width: 1080px;
+                margin: 0 auto;
+                box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+                .el-radio, .el-checkbox {
+                    display: block;
+                    margin-bottom: 10px;
+                    pointer-events: none;
+                    &:last-child {
+                        margin-bottom: 0;
+                    }
+                }
+                .el-radio-button {
+                    pointer-events: none;
+                }
+                .el-radio__label, .el-checkbox__label {
+                    font-size: 16px;
+                    white-space: normal;
+                    line-height: 1.5;
+                }
+                .el-input {
+                    margin-bottom: 10px;
+                    pointer-events: none;
+                    &:last-child {
+                        margin-bottom: 0;
+                    }
+                }
+                .el-tag {
+                    margin-right: 0;
+                }
+                .ibps-p-0 {
+                    margin: 0;
+                    .list-group {
+                        display: inline-block;
+                        width: 100%;
+                    }
+                }
+            }
+            .el-dialog__header {
+                text-align: center;
+            }
+        }
+        .container {
+            position: relative;
+            height: calc(100vh - 160px);
+            padding: 20px;
+            .paper-info {
+                display: flex;
+                flex-wrap: wrap;
+                justify-content: flex-start;
+                margin-bottom: 20px;
+                background-color: #f5f5f5;
+                border: 1px solid #ddd;
+                padding: 10px;
+                border-radius: 5px;
+                font-size: 16px;
+            }
+
+            .info-item {
+                width: 33.3%;
+                margin-bottom: 10px;
+                display: flex;
+                align-items: center;
+                .label {
+                    width: 100px;
+                    font-weight: bold;
+                    color: #333;
+                }
+                .value {
+                    flex: 1;
+                    color: #666;
+                    white-space: nowrap;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                }
+            }
+
+            .question {
+                font-size: 16px;
+                .question-item {
+                    padding-top: 20px;
+                    &:last-child {
+                        padding-bottom: 20px;
+                    }
+                    .card {
+                        padding-bottom: 10px;
+                        &:last-child {
+                            padding-bottom: 0;
+                        }
+                    }
+                    .type {
+                        font-size: 20px;
+                        font-weight: 600;
+                        margin-bottom: 20px;
+                        .score {
+                            margin-left: 10px;
+                            vertical-align: bottom;
+                        }
+                    }
+                    .stem {
+                        display: flex;
+                        margin-bottom: 10px;
+                        align-items: flex-start;
+                        > span:first-child {
+                            line-height: 1.5;
+                            margin-right: 10px;
+                        }
+                    }
+                    .img {
+                        position: relative;
+                        width: 100%;
+                        margin-bottom: 10px;
+                    }
+                    .answer {
+                        display: flex;
+                        border: 1px solid #ccc;
+                        border-radius: 5px;
+                        .mine, .right {
+                            flex: 1;
+                            padding: 10px;
+                            .title {
+                                font-size: 16px;
+                                color: #01a39e;
+                                margin-bottom: 10px;
+                                .score {
+                                    vertical-align: middle;
+                                }
+                            }
+                        }
+                        .mine {
+                            background-color: #f5f5f5;
+                            border-right: 1px solid #ccc;
+                        }
+                    }
+                }
+            }
+        }
+        .date-line {
+            position: fixed;
+            width: 280px;
+            height: calc(100vh - 120px);
+            overflow-y: auto;
+            top: 60px;
+            left: calc(50vw + 540px);
+            padding-right: 8px;
+            ::v-deep {
+                .el-timeline {
+                    padding-left: 10px;
+                }
+                .el-card__body {
+                    padding: 10px 10px;
+                }
+                .el-tag {
+                    margin-left: 5px;
+                }
+            }
+            .timeline-card {
+                cursor: pointer;
+                .card-item {
+                    margin-bottom: 10px;
+                    &:last-child {
+                        margin-bottom: 0;
+                    }
+                }
+            }
+            .active-card {
+                box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 1);
+            }
+        }
+    }
+    @media screen and (max-width: 1600px) {
+        .paper-detail-dialog {
+            ::v-deep {
+                .el-dialog__body {
+                    width: 880px;
+                }
+            }
+            .date-line {
+                left: calc(50vw + 440px);
+            }
+        }
+    }
+    @media screen and (max-width: 1400px) {
+        .paper-detail-dialog {
+            ::v-deep {
+                .el-dialog__body {
+                    width: 800px;
+                }
+            }
+            .date-line {
+                left: calc(50vw + 400px);
+            }
+        }
+    }
+</style>

+ 921 - 0
src/views/platform/examination/questionBank/edit.vue

@@ -0,0 +1,921 @@
+<template>
+    <el-dialog
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        append-to-body
+        width="60%"
+        class="dialog paper-dialog"
+        top="6vh"
+        @close="closeDialog"
+        @open="getQuestionData"
+    >
+        <el-form
+            ref="form"
+            :label-width="formLabelWidth"
+            :model="form"
+            :rules="rules"
+            class="paper-form"
+            :class="readonly ? 'readonly-form' : ''"
+            @submit.native.prevent
+        >
+            <el-form-item label="题库名称:" prop="ti_ku_ming_cheng_">
+                <el-input
+                    v-model="form.ti_ku_ming_cheng_"
+                    type="text"
+                    :maxlength="128"
+                    :disabled="readonly"
+                />
+            </el-form-item>
+            <el-form-item label="题库分类:" prop="ti_ku_fen_lei_">
+                <el-select
+                    v-model="form.ti_ku_fen_lei_"
+                    filterable
+                    allow-create
+                    width="100%"
+                    :disabled="readonly"
+                    placeholder="请选择题库分类"
+                >
+                    <el-option
+                        v-for="item in paperTypeOptions"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item prop="ti_ku_zhuang_tai_">
+                <template slot="label">
+                    题库状态
+                    <el-tooltip effect="dark" content="限制该题库是否可用于创建考试及自主考核。" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.ti_ku_zhuang_tai_" :disabled="readonly">
+                    <el-radio label="可用">可用</el-radio>
+                    <el-radio label="禁用">禁用</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item prop="shi_fou_gong_kai_">
+                <template slot="label">
+                    是否公开
+                    <el-tooltip effect="dark" content="限制该题库是否可自主考核。若您希望该题库仅用于考试中,则需要设置为“否”。" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.shi_fou_gong_kai_" :disabled="readonly">
+                    <el-radio label="是">是&nbsp;&nbsp;&nbsp;&nbsp;</el-radio>
+                    <el-radio label="否">否</el-radio>
+                </el-radio-group>
+            </el-form-item>
+            <el-form-item prop="suo_shu_fan_wei_" class="inline-item">
+                <template slot="label">
+                    所属范围
+                    <el-tooltip effect="dark" content="设置题库的所属范围,组级则需要选择具体的专业组。" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.suo_shu_fan_wei_" :disabled="readonly">
+                    <el-radio label="科级">科级</el-radio>
+                    <el-radio label="组级">组级</el-radio>
+                </el-radio-group>
+                <el-select
+                    v-if="form.suo_shu_fan_wei_ === '组级'"
+                    v-model="form.bian_zhi_bu_men_"
+                    width="100%"
+                    :disabled="readonly"
+                    placeholder="请选择专业组"
+                >
+                    <el-option
+                        v-for="item in deptList"
+                        :key="item.positionId"
+                        :label="item.positionName"
+                        :value="item.positionId"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item prop="isLimit" class="inline-item">
+                <template slot="label">
+                    限考次数
+                    <el-tooltip effect="dark" content="限制是否可重复考核,以及可自主考核的最大次数。" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.isLimit" :disabled="readonly" @change="changeLimit">
+                    <el-radio label="0">不限</el-radio>
+                    <el-radio label="1">限制</el-radio>
+                </el-radio-group>
+                <div v-if="form.isLimit === '1'" class="time">
+                    <el-input-number
+                        v-model="form.xian_kao_ci_shu_"
+                        :min="1"
+                        :precision="0"
+                        :disabled="readonly"
+                        placeholder="请输入单个用户最大限考次数"
+                    />
+                    <div class="unit">次</div>
+                </div>
+            </el-form-item>
+            <el-form-item prop="limitTime" class="inline-item">
+                <template slot="label">
+                    考试时长
+                    <el-tooltip effect="dark" content="设置该题库对于单次用户自主考核的最大考试时长。" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-radio-group v-model="form.limitTime" :disabled="readonly">
+                    <el-radio label="0">不限</el-radio>
+                    <el-radio label="1">限制</el-radio>
+                </el-radio-group>
+                <template v-if="form.limitTime === '1'">
+                    <div class="time">
+                        <el-input-number
+                            v-model="form.hours"
+                            :min="0"
+                            :max="72"
+                            :precision="0"
+                            :disabled="readonly"
+                        />
+                        <div class="unit">小时</div>
+                    </div>
+                    <div class="time">
+                        <el-input-number
+                            v-model="form.minutes"
+                            :min="0"
+                            :max="59"
+                            :precision="0"
+                            :disabled="readonly"
+                        />
+                        <div class="unit">分钟</div>
+                    </div>
+                </template>
+            </el-form-item>
+            <el-form-item prop="da_biao_zhan_bi_">
+                <template slot="label">
+                    达标分值占比
+                    <el-tooltip effect="dark" content="设置该题库的达标分数线占题库总分的百分比。" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template>
+                <el-input-number
+                    v-model="form.da_biao_zhan_bi_"
+                    :min="50"
+                    :max="100"
+                    :precision="0"
+                    :disabled="readonly"
+                    placeholder="请输入达标分值占比"
+                />
+                <div class="unit">%</div>
+            </el-form-item>
+            <!-- <el-form-item v-if="!readonly || form.ping_fen_ren_" prop="ping_fen_ren_">
+                <template slot="label">
+                    默认评分人
+                    <el-tooltip effect="dark" content="设置该题库试题的默认评分人,该题库下所有未设置评分人的题目都以该评分人为准。" placement="top">
+                        <i class="el-icon-question question-icon">:</i>
+                    </el-tooltip>
+                </template> -->
+            <!-- <el-cascader
+                    v-model="form.ping_fen_ren_"
+                    :options="getRaterOptions()"
+                    :show-all-levels="false"
+                    collapse-tags
+                    :props="{
+                        value: 'value',
+                        label: 'label',
+                        multiple: true,
+                        checkStrictly: false
+                    }"
+                /> -->
+            <!-- <el-select
+                    v-model="form.ping_fen_ren_"
+                    filterable
+                    width="100%"
+                    clearable
+                    multiple
+                    collapse-tags
+                    :multiple-limit="16"
+                    :disabled="readonly"
+                    placeholder="请选择默认评分人"
+                >
+                    <el-option
+                        v-for="item in userList"
+                        :key="item.userId"
+                        :label="item.userName"
+                        :value="item.userId"
+                    />
+                </el-select>
+            </el-form-item> -->
+            <el-form-item v-if="!readonly || form.miao_shu_" label="题库描述:" prop="miao_shu_">
+                <el-input
+                    v-model="form.miao_shu_"
+                    type="textarea"
+                    :autosize="readonly"
+                    :rows="4"
+                    :disabled="readonly"
+                    placeholder="请输入描述内容"
+                />
+            </el-form-item>
+        </el-form>
+        <div class="question-table">
+            <div v-if="!readonly" class="question-select">
+                <div class="label">选择试题:</div>
+                <ibps-custom-dialog
+                    v-model="quesIdList"
+                    size="small"
+                    template-key="tmlb"
+                    multiple
+                    type="dialog"
+                    class="custom-dialog"
+                    placeholder="请选择需要关联的题目信息"
+                />
+            </div>
+            <div class="table-title">
+                <span>题库试题信息</span>
+                <el-button v-if="!readonly" type="danger" size="mini" icon="el-icon-delete" @click="removeItems">删除</el-button>
+            </div>
+            <el-table
+                ref="elTable"
+                :data="showPaperList"
+                border
+                stripe
+                highlight-current-row
+                style="width: 100%"
+                max-height="400px"
+                class="exam-table"
+                @row-dblclick="handleRowDblclick"
+                @selection-change="handleSelectionChange"
+            >
+                <el-table-column
+                    v-if="!readonly"
+                    width="50"
+                    type="selection"
+                />
+                <el-table-column label="序号" type="index" :index="showIndex" width="60" />
+                <el-table-column
+                    prop="content"
+                    label="题干"
+                    min-width="150"
+                >
+                    <template slot-scope="scope">
+                        <el-popover trigger="hover" placement="top">
+                            <div class="question-info">
+                                <div class="left">
+                                    <div class="question-info-item">
+                                        <div class="label">出题人</div>
+                                        <div class="value">{{ transformUser(scope.row.creator) }}</div>
+                                    </div>
+                                    <div class="question-info-item">
+                                        <div class="label">选项类型</div>
+                                        <div class="value">{{ scope.row.optionType || '—' }}</div>
+                                    </div>
+                                    <div class="question-info-item">
+                                        <div class="label">评分方式</div>
+                                        <div class="value">{{ scope.row.rateType }}</div>
+                                    </div>
+                                    <div class="question-info-item">
+                                        <div class="label">评分人</div>
+                                        <div class="value">{{ transformUser(scope.row.rater) }}</div>
+                                    </div>
+                                    <div class="question-info-item">
+                                        <div class="label">标签</div>
+                                        <div class="value">{{ scope.row.quesTag }}</div>
+                                    </div>
+                                    <div class="question-info-item">
+                                        <div class="label">状态</div>
+                                        <div class="value">{{ scope.row.quesState }}</div>
+                                    </div>
+                                </div>
+                                <div class="right">
+                                    <ibps-image
+                                        :value="showImgs(scope,3)"
+                                        height="180"
+                                        width="180"
+                                        :disabled="true"
+                                    />
+                                </div>
+
+                            </div>
+                            <div slot="reference" class="name-wrapper">{{ scope.row.content }}</div>
+                        </el-popover>
+                    </template>
+                </el-table-column>
+                <el-table-column
+                    prop="quesType"
+                    label="题型"
+                    width="70"
+                />
+                <el-table-column
+                    prop="score"
+                    label="分值"
+                    width="75"
+                    sortable
+                />
+                <el-table-column
+                    prop="questionLevel"
+                    label="难度"
+                    width="75"
+                    sortable
+                >
+                    <template slot-scope="{row}">
+                        <span>{{ ['未知','易','偏易','适中','偏难','难'][+row.questionLevel] }}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column
+                    prop="createTime"
+                    label="出题时间"
+                    width="160"
+                    sortable
+                />
+                <el-table-column
+                    fixed="right"
+                    label="操作"
+                    width="100"
+                >
+                    <template slot-scope="scope">
+                        <el-button
+                            v-if="!readonly"
+                            type="text"
+                            size="medium"
+                            @click="handleColumnAction(scope.row, false)"
+                        >修改</el-button>
+                        <!-- <el-button
+                            v-if="!readonly"
+                            type="text"
+                            style="color: #f56c6c;"
+                            size="medium"
+                            @click="handleRemove(scope.row)"
+                        >删除</el-button> -->
+                        <el-button
+                            v-if="readonly"
+                            type="text"
+                            style="color: #909399;"
+                            size="medium"
+                            @click="handleColumnAction(scope.row, true)"
+                        >详情</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <el-pagination
+                style="margin-top: 5px; padding-bottom: 10px"
+                :current-page="currentPage"
+                :page-sizes="[10, 20,30, 50,100]"
+                :page-size="pageSize"
+                layout="prev,pager,next,jumper,sizes,->,total"
+                :total="questionData.length"
+                @size-change="handleSizeChange"
+                @current-change="handleCurrentChange"
+            />
+
+        </div>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar
+                :actions="toolbars"
+                @action-event="handleActionEvent"
+            />
+        </div>
+        <ques-edit
+            v-if="questionDialogVisible"
+            :id="quesId"
+            :bank-id="id"
+            :visible.sync="questionDialogVisible"
+            :is-copy="isCopy"
+            :ques-data="questionData"
+            :readonly="quesReadonly"
+            @update="updateData"
+            @close="questionDialogVisible = false"
+        />
+    </el-dialog>
+</template>
+
+<script>
+import ActionUtils from '@/utils/action'
+import { paperTypeOptions } from '../constants'
+export default {
+    components: {
+        QuesEdit: () => import('../question/edit'),
+        IbpsCustomDialog: () => import('@/business/platform/data/templaterender/custom-dialog'),
+        IbpsImage: () => import('@/business/platform/file/image')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        id: {
+            type: String,
+            default: ''
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const { userList = [], deptList = [], userId, level = {}} = this.$store.getters || {}
+        return {
+            multipleSelection: [],
+            pageSize: 10,
+            currentPage: 1,
+            userList,
+            paperTypeOptions,
+            level: level.second || level.first,
+            deptList: deptList.filter(i => i.depth === 4),
+            title: this.readonly ? '题库明细' : this.id ? '编辑题库' : '创建题库',
+            formLabelWidth: '150px',
+            dialogVisible: this.visible,
+            dialogLoading: false,
+            questionData: [],
+            initialData: [],
+            questionDialogVisible: false,
+            quesId: '',
+            quesIdList: '',
+            isCopy: false,
+            quesReadonly: false,
+            form: {
+                bian_zhi_bu_men_: '',
+                bian_zhi_ren_: userId,
+                bian_zhi_shi_jian: this.$common.getDateNow(19),
+                ti_ku_ming_cheng_: '',
+                suo_shu_fan_wei_: '科级',
+                ti_ku_fen_lei_: paperTypeOptions.length ? paperTypeOptions[0].value : '',
+                ti_ku_zhuang_tai_: '可用',
+                xian_kao_ci_shu_: '不限',
+                shi_fou_gong_kai_: '是',
+                ping_fen_ren_: '',
+                da_biao_zhan_bi_: 60,
+                miao_shu_: '',
+                ti_shu_: 0,
+                isLimit: '0',
+                limitTime: '0',
+                hours: 2,
+                minutes: 30
+            },
+            toolbars: [
+                {
+                    key: 'submit',
+                    icon: 'ibps-icon-save',
+                    label: '保存',
+                    hidden: () => {
+                        return this.readonly
+                    }
+                },
+                { key: 'cancel', label: '关闭' }
+            ],
+            rules: {
+                ti_ku_ming_cheng_: [{ required: true, message: this.$t('validate.required') }],
+                suo_shu_fan_wei_: [{ required: true, message: this.$t('validate.required') }],
+                bian_zhi_bu_men_: [{ required: true, message: this.$t('validate.required') }],
+                ti_ku_fen_lei_: [{ required: true, message: this.$t('validate.required') }],
+                shi_fou_gong_kai_: [{ required: true, message: this.$t('validate.required') }],
+                ti_ku_zhuang_tai_: [{ required: true, message: this.$t('validate.required') }],
+                xian_kao_ci_shu_: [{ required: true, message: this.$t('validate.required') }],
+                kao_shi_shi_chang: [{ required: true, message: this.$t('validate.required') }],
+                da_biao_zhan_bi_: [{ required: true, message: this.$t('validate.required') }]
+            }
+
+        }
+    },
+    computed: {
+        formId () {
+            return this.id
+        },
+        showPaperList () {
+            const start = (this.currentPage - 1) * this.pageSize
+            const end = start + this.pageSize
+            return this.questionData.slice(start, end)
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+            // immediate: true
+        },
+        quesIdList: {
+            handler (val, oldVal) {
+                this.addSelectQuestion(val)
+            }
+        }
+    },
+    mounted () {
+        this.getQuestionData()
+    },
+    methods: {
+        // 默认展示三张图片
+        showImgs (scope, showNumber = 3) {
+            if (scope.row.img) {
+                return JSON.parse(scope.row.img).slice(0, showNumber)
+            }
+            return ''
+        },
+        // 分页连续序号
+        showIndex (index) {
+            return index + 1 + (this.currentPage - 1) * this.pageSize
+        },
+        // 当前页码改变
+        handleCurrentChange (val) {
+            this.currentPage = val
+        },
+        // 页码选择器改变
+        handleSizeChange (val) {
+            this.pageSize = val
+            this.currentPage = 1
+        },
+        changeLimit (e) {
+            this.form.xian_kao_ci_shu_ = e === '1' ? 1 : '不限'
+        },
+        getRaterOptions () {
+            const { userList = [], deptList = [] } = this.$store.getters || {}
+            const depts = deptList.filter(i => i.depth > 2)
+            const temp = depts.map(item => {
+                item.value = item.positionId
+                item.label = item.positionName
+                item.children = userList.map(i => {
+                    if (i.positionId.includes(item.positionId)) {
+                        i.value = i.userId
+                        i.label = i.userName
+                        return i
+                    }
+                }).filter(i => i)
+                return item
+            })
+            const res = []
+            temp.forEach(item => {
+                const index = res.findIndex(i => item.path === `${i.path}${item.value}.`)
+                if (index !== -1) {
+                    res[index].children.unshift(item)
+                } else {
+                    res.push(item)
+                }
+            })
+            return res
+        },
+        transformUser (user) {
+            const idList = user.split(',')
+            const { userList = [] } = this.$store.getters
+            const nameList = idList.map(id => {
+                const user = userList.find(u => u.userId === id) || {}
+                return user.userName || '-'
+            })
+            return nameList.join(',')
+        },
+        formatNum (num) {
+            return num === null || num === undefined || num === '' ? 0 : num
+        },
+        handleRowDblclick (row) {
+            this.handleColumnAction(row, true)
+        },
+        handleColumnAction (row, readonly) {
+            this.quesId = row.quesId
+            this.quesReadonly = readonly
+            this.isCopy = this.quesIdList.includes(this.quesId)
+            this.questionDialogVisible = true
+        },
+        handleRemove (row) {
+            this.$confirm('确定要删除该题目吗?删除操作将在题库信息保存后生效', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                this.questionData = this.questionData.filter(item => item.quesId !== row.quesId)
+            })
+        },
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'submit':
+                    this.handleSubmit()
+                    break
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        // 获取题库数据
+        getQuestionData () {
+            if (this.$utils.isEmpty(this.formId)) {
+                return
+            }
+            const sql1 = `select id_, bian_zhi_ren_, bian_zhi_bu_men_, bian_zhi_shi_jian, ti_ku_ming_cheng_, ti_ku_fen_lei_, ti_ku_zhuang_tai_, shi_fou_gong_kai_, xian_kao_ci_shu_, ping_fen_ren_, miao_shu_, suo_shu_fan_wei_, kao_shi_shi_chang, da_biao_zhan_bi_ from t_question_bank where id_ = '${this.formId}'`
+            const sql2 = `select id_ as quesId, chu_ti_ren_ as creator, bu_men_ as createDept, chu_ti_shi_jian_ as createTime, xu_hao_ as sn, ti_gan_ as content, ti_xing_ as quesType, fu_tu_ as img, xuan_xiang_lei_xi as optionType, da_an_ as answer, zheng_que_da_an_ as rightKey, ping_fen_fang_shi as rateType, ping_fen_ren_ as rater, fen_zhi_ as score, bei_zhu_ as note, xuan_xiang_shu_ as optionCount, biao_qian_ as quesTag, zhuang_tai_ as quesState,nan_du_ as questionLevel from t_questions where parent_id_ = '${this.formId}' order by chu_ti_shi_jian_ desc`
+            Promise.all([this.$common.request('sql', sql1), this.$common.request('sql', sql2)]).then(([res1, res2]) => {
+                const { data: bankData = [] } = res1.variables || {}
+                const { data: questionData = [] } = res2.variables || {}
+                if (!bankData.length) {
+                    this.$message.error('数据不存在')
+                    return
+                }
+                const bank = bankData[0]
+                bank.isLimit = bank.xian_kao_ci_shu_ === '不限' ? '0' : '1'
+                bank.ping_fen_ren_ = bank.ping_fen_ren_ ? bank.ping_fen_ren_.split(',') : []
+                // bank.suo_shu_fan_wei_ = bank.suo_shu_fan_wei_ === '不限' ? '不限' : '科级'
+                if (bank.kao_shi_shi_chang === '不限') {
+                    bank.limitTime = '0'
+                    bank.hours = null
+                    bank.minutes = null
+                } else {
+                    bank.limitTime = '1'
+                    bank.hours = Math.floor(bank.kao_shi_shi_chang / (1000 * 60 * 60))
+                    bank.minutes = (bank.kao_shi_shi_chang % (1000 * 60 * 60)) / (60 * 1000)
+                }
+                // console.log(questionData)
+                this.questionData = questionData
+                this.initialData = JSON.parse(JSON.stringify(questionData))
+                this.form = bank
+            })
+        },
+        addSelectQuestion () {
+            const sql = `select id_ as quesId, chu_ti_ren_ as creator, bu_men_ as createDept, chu_ti_shi_jian_ as createTime, xu_hao_ as sn, ti_gan_ as content, ti_xing_ as quesType, fu_tu_ as img, xuan_xiang_lei_xi as optionType, da_an_ as answer, zheng_que_da_an_ as rightKey, ping_fen_fang_shi as rateType, ping_fen_ren_ as rater, fen_zhi_ as score, bei_zhu_ as note, xuan_xiang_shu_ as optionCount, biao_qian_ as quesTag, zhuang_tai_ as quesState,nan_du_ as questionLevel from t_questions where find_in_set(id_, '${this.quesIdList}')`
+            this.$common.request('sql', sql).then(res => {
+                const { data = [] } = res.variables || {}
+                this.questionData = data.concat(this.questionData)
+            })
+        },
+        handleSubmit () {
+            this.$refs.form.validate((valid) => {
+                if (valid) {
+                    // 表单验证通过,提交表单
+                    this.submitForm()
+                } else {
+                    ActionUtils.saveErrorMessage()
+                }
+            })
+        },
+        async updatePaper (bankId) {
+            const sql = `select fen_zhi_ from t_questions where parent_id_ = '${bankId}'`
+            this.$common.request('sql', sql).then(res => {
+                const { data = [] } = res.variables || {}
+                const params = {
+                    tableName: 't_question_bank',
+                    updList: [
+                        {
+                            where: {
+                                id_: bankId
+                            },
+                            param: {
+                                ti_shu_: data.length,
+                                zong_fen_: data.reduce((sum, item) => sum + parseInt(item.fen_zhi_), 0)
+                            }
+                        }
+                    ]
+                }
+                this.$common.request('update', params).then(() => {
+                    console.log('更新题库信息成功')
+                    this.closeDialog()
+                })
+            })
+        },
+        async createQuestion (list, bankId) {
+            const paramWhere = list.map(item => ({
+                di_dian_: this.level,
+                parent_id_: bankId,
+                bu_men_: item.createDept || '',
+                chu_ti_ren_: item.creator,
+                chu_ti_shi_jian_: item.createTime,
+                xu_hao_: item.sn || '',
+                ti_gan_: item.content,
+                ti_xing_: item.quesType,
+                fu_tu_: item.img || '',
+                xuan_xiang_lei_xi: item.optionType || '',
+                da_an_: item.answer || '',
+                fen_zhi_: item.score,
+                xuan_xiang_shu_: item.optionCount || '',
+                zheng_que_da_an_: item.rightKey,
+                ping_fen_fang_shi: item.rateType || '',
+                ping_fen_ren_: item.rater || '',
+                bei_zhu_: item.note || '',
+                zhuang_tai_: item.quesState,
+                biao_qian_: item.quesTag || '',
+                nan_du_: item.questionLevel || 0
+            }))
+            await this.$common.request('add', {
+                tableName: 't_questions',
+                paramWhere
+            })
+        },
+        // 软删除,删除关联关系
+        async deleteQuestion (list) {
+            const updList = list.map(item => ({
+                where: {
+                    id_: item.quesId
+                },
+                param: {
+                    parent_id_: ''
+                }
+            }))
+            await this.$common.request('update', {
+                tableName: 't_questions',
+                updList
+            })
+        },
+        submitForm () {
+            if (this.form.limitTime === '0') {
+                this.form.kao_shi_shi_chang = '不限'
+            } else {
+                this.form.kao_shi_shi_chang = (this.formatNum(this.form.hours) * 60 + this.formatNum(this.form.minutes)) * 60 * 1000
+            }
+            this.form.bian_zhi_bu_men_ = this.form.suo_shu_fan_wei_ === '科级' ? '' : this.form.bian_zhi_bu_men_
+            this.form.di_dian_ = this.level
+            this.form.ping_fen_ren_ = this.form.ping_fen_ren_ ? this.form.ping_fen_ren_.join(',') : ''
+            delete this.form.isLimit
+            delete this.form.limitTime
+            delete this.form.hours
+            delete this.form.minutes
+            const addParams = {
+                tableName: 't_question_bank',
+                paramWhere: [{
+                    ...this.form,
+                    ti_shu_: 0,
+                    zong_fen_: 0
+                }]
+            }
+            const updateParams = {
+                tableName: 't_question_bank',
+                updList: [
+                    {
+                        where: {
+                            id_: this.formId
+                        },
+                        param: this.form
+                    }
+                ]
+            }
+            const type = this.formId ? 'update' : 'add'
+            const params = type === 'add' ? addParams : updateParams
+            const temp = this.questionData.filter(i => !this.quesIdList.includes(i.quesId)).map(i => i.quesId)
+            const deleteList = this.initialData.filter(item => !temp.includes(item.quesId))
+            const createList = this.questionData.filter(i => this.quesIdList.includes(i.quesId))
+            this.$common.request(type, params).then(async res => {
+                const { cont = [] } = res.variables || {}
+                let dataId = this.id
+                if (cont.length) {
+                    dataId = cont[0].id_
+                }
+                const PromiseList = []
+                if (createList.length) {
+                    PromiseList.push(this.createQuestion(createList, dataId))
+                }
+                if (deleteList.length) {
+                    PromiseList.push(this.deleteQuestion(deleteList))
+                }
+                Promise.all(PromiseList).then(() => {
+                    this.updatePaper(dataId)
+                })
+                this.$message.success(this.formId ? '保存题库信息成功' : '新增题库成功')
+            })
+        },
+        // table复选框
+        handleSelectionChange (val) {
+            this.multipleSelection = val
+        },
+        // 批量删除
+        removeItems () {
+            if (this.multipleSelection.length === 0) {
+                return this.$message.warning('请选择需要删除的题目!')
+            }
+            this.$confirm('确定要删除所选中的题目吗?删除操作将在题库信息保存后生效', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                this.questionData = this.questionData.filter(item => !this.multipleSelection.includes(item))
+                this.multipleSelection = []
+            })
+        },
+        // 关闭当前窗口
+        closeDialog () {
+            this.$emit('close', false)
+        },
+        updateData (data) {
+            this.questionData = data
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .paper-dialog {
+        ::v-deep {
+            .el-dialog {
+                min-width: 1080px;
+            }
+            .el-dialog__body {
+                height: calc(88vh - 200px);
+            }
+            .el-form-item {
+                margin-bottom: 14px !important;
+                &:last-child {
+                    margin-bottom: 0 !important;
+                }
+                .el-form-item__label {
+                    font-size: 14px !important;
+                }
+            }
+            .el-form-item--small .el-form-item__error {
+                padding-top: 6px;
+            }
+        }
+        .readonly-form {
+            ::v-deep {
+                .el-radio, .el-checkbox, .el-radio-button, .el-input, .el-select, .el-textarea, .el-input-number {
+                    pointer-events: none;
+                }
+            }
+        }
+        .paper-form {
+            padding: 20px 20px 14px 20px;
+        }
+        .inline-item {
+            ::v-deep {
+                .el-radio-group {
+                    margin-right: 20px;
+                }
+            }
+            .time {
+                display: inline-block;
+            }
+        }
+        .unit {
+            display: inline-block;
+            margin: 0 20px 0 5px;
+        }
+        .question-table {
+            margin-bottom: 20px;
+            padding: 0 20px;
+            ::v-deep {
+                .el-table {
+                    th {
+                        font-size: 14px !important;
+                        text-align: center;
+                    }
+                    td {
+                        font-size: 14px !important;
+                    }
+                    .el-table__row {
+                        cursor: pointer;
+                    }
+                }
+            }
+            .question-select {
+                display: flex;
+                align-items: center;
+                .label {
+                    width: 138px;
+                    padding-right: 12px;
+                    text-align: right;
+                }
+                .custom-dialog {
+                    flex: 1;
+                }
+                margin-bottom: 10px;
+            }
+            .table-title {
+                font-size: 16px;
+                font-weight: 600;
+                margin: 20px 0 10px;
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+            }
+        }
+    }
+    .question-info {
+        overflow:auto;
+        max-width: 536px;
+        display: flex;
+        .question-info-item {
+            display: flex;
+            margin-bottom: 10px;
+            max-width: 200px;
+            font-size: 14px;
+            .label {
+                width: 100px;
+                font-size: 14px;
+                color: #606266;
+                text-align: left;
+            }
+            .value {
+                font-weight: 600;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+            }
+        }
+        .right{
+            display: flex;
+            margin-left: 20px;
+            .ibps-image{
+                font-size: 0;
+                ::v-deep{
+                    .list-group{
+                        display: flex;
+                    }
+                }
+
+            }
+        }
+    }
+</style>

+ 792 - 0
src/views/platform/examination/questionBank/test.vue

@@ -0,0 +1,792 @@
+<template>
+    <el-dialog
+        v-loading="loading"
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        fullscreen
+        class="dialog test-dialog"
+        top="0"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <div class="container">
+            <div class="question">
+                <template v-for="(item, index) in questionList">
+                    <div
+                        v-if="showIndex === index + 1"
+                        :key="index"
+                        class="question-item"
+                    >
+                        <div class="type">{{ item.questionType }}</div>
+                        <div class="stem">
+                            <span>{{ `【${index + 1}】${item.stem}` }}</span>
+                            <el-tag type="info" size="small">{{ `${item.score}分` }}</el-tag>
+                        </div>
+                        <div v-if="item.img && item.img.length" class="img">
+                            <ibps-image
+                                v-model="item.img"
+                                height="100"
+                                width="100"
+                                accept=".jpg,.jpeg,.png,.gif,.bmp,.webp"
+                                download
+                                disabled
+                            />
+                        </div>
+                        <div class="answer">
+                            <el-radio-group v-if="item.questionType === '单选题'" v-model="item.answer" @change="goNext">
+                                <el-radio
+                                    v-for="(o, i) in item.options"
+                                    :key="`${index}${i}`"
+                                    :label="o.label"
+                                >{{ `${o.label}.${o.value}` }}</el-radio>
+                            </el-radio-group>
+                            <el-checkbox-group v-else-if="item.questionType === '多选题'" v-model="item.answer" @change="changeOptions">
+                                <el-checkbox
+                                    v-for="(o, i) in item.options"
+                                    :key="`${index}${i}`"
+                                    :label="o.label"
+                                >{{ `${o.label}.${o.value}` }}</el-checkbox>
+                            </el-checkbox-group>
+                            <el-radio-group v-else-if="item.questionType === '判断题'" v-model="item.answer" @change="goNext">
+                                <el-radio-button label="√">√</el-radio-button>
+                                <el-radio-button label="×">×</el-radio-button>
+                            </el-radio-group>
+                            <template v-else-if="item.questionType === '填空题'">
+                                <el-input
+                                    v-for="(o, i) in item.options"
+                                    :key="`${index}${i}`"
+                                    v-model="o.answer"
+                                    type="text"
+                                    placeholder="请输入您的答案"
+                                    @focus="textareaFocused=true"
+                                    @blur="textareaFocused=false"
+                                />
+                            </template>
+                            <el-input
+                                v-else-if="item.questionType === '简答题'"
+                                v-model="item.answer"
+                                type="textarea"
+                                :autosize="{ minRows: 4, maxRows: 8}"
+                                placeholder="请输入您的答案"
+                                @focus="textareaFocused=true"
+                                @blur="textareaFocused=false"
+                            />
+                        </div>
+                    </div>
+                </template>
+            </div>
+            <div class="question-link">
+                <div class="tabs">
+                    <el-tabs v-model="activeName">
+                        <el-tab-pane v-for="(item) in pageTotal" :key="item" :label="pageLabel(item)" :name="item+''" />
+                    </el-tabs>
+                </div>
+                <el-progress
+                    type="line"
+                    class="progress"
+                    :text-inside="true"
+                    :stroke-width="20"
+                    :percentage="getProgress()"
+                />
+                <div class="question">
+                    <template v-for="(item, index) in questionList">
+                        <div
+                            v-if="index+1>=pageRange(pagination.currentPage).start &&index+1<=pageRange(pagination.currentPage).end"
+                            :key="index"
+                            class="link-item"
+                            :class="setClassName(item, index)"
+                            @click="showIndex = index + 1"
+                        >{{ index + 1 }}</div>
+                    </template>
+
+                </div>
+
+            </div>
+        </div>
+        <div class="tips">
+            <div>
+                <div class="tips-title">温馨提示:</div>
+                <div class="tips-content">最下方题序方块无背景颜色时,表示该题还未作答;</div>
+                <div class="tips-content">题序方块背景颜色为<span style="background: #409EFF;">蓝色</span>时,表示题序对应题母为当前显示的题目;</div>
+                <div class="tips-content">题序方块背景颜色为<span style="background: #E6A23C;">黄色</span>时,表示该题已作答但是未完全答完(如填空题);</div>
+                <div class="tips-content">题序方块背景颜色为<span style="background: #67C23A;">绿色</span>时,表示该题已作答完成。</div>
+                <div class="tips-content">祝愿您获得满意的成绩🎉</div>
+            </div>
+        </div>
+        <div slot="title" class="custom-title">
+            <div class="dialog-title">{{ title }}</div>
+            <div v-if="examData.duration !== '不限'" class="countdown">
+                <div class="desc">⏲距离考试结束还有:{{ formattedCountdown }}</div>
+                <div class="countdown-tips">超时将自动提交, 请注意考试时间!</div>
+            </div>
+        </div>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar
+                :actions="toolbars"
+                @action-event="handleActionEvent"
+            />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+// import Watermark from '@/layout/header-aside/components/header-message/watermark/watermark-cont'
+import { shuffle, round } from 'lodash'
+
+export default {
+    components: {
+        IbpsImage: () => import('@/business/platform/file/image')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        bankId: {
+            type: String,
+            default: ''
+        },
+        id: {
+            type: String,
+            default: ''
+        },
+        examData: {
+            type: Object,
+            default: () => {}
+        }
+    },
+    data () {
+        const { userId } = this.$store.getters || {}
+        const { duration } = this.examData || {}
+        const countdown = duration === '不限' ? 0 : parseInt(duration / 1000)
+        return {
+            pagination: {
+                currentPage: 1,
+                pageSize: 100
+            },
+            activeName: '1',
+            countdown,
+            title: this.examData.examName || '参加考试',
+            dialogVisible: this.visible,
+            loading: false,
+            toolbars: [
+                {
+                    key: 'prev',
+                    icon: 'ibps-icon-chevron-left',
+                    label: '上一题',
+                    disabled: () => {
+                        return this.showIndex === 1
+                    }
+                },
+                {
+                    key: 'next',
+                    icon: 'ibps-icon-chevron-right',
+                    label: '下一题',
+                    disabled: () => {
+                        return this.showIndex === this.questionList.length
+                    }
+                },
+                {
+                    key: 'submit',
+                    icon: 'ibps-icon-send',
+                    label: '交卷',
+                    hidden: () => {
+                        return this.readonly
+                    }
+                },
+                { key: 'cancel', label: '退出' }
+            ],
+            questionList: [],
+            showIndex: 1,
+            userId,
+            countdownNotify: false,
+            textareaFocused: false // input 聚焦状态
+        }
+    },
+    computed: {
+        formData () {
+            return this.data
+        },
+        pageTotal () {
+            return Math.ceil(this.questionList.length / this.pagination.pageSize)
+        },
+        formattedCountdown () {
+            const h = this.formatNum(parseInt(this.countdown / 60 / 60))
+            const m = this.formatNum(parseInt(this.countdown / 60 % 60))
+            const s = this.formatNum(parseInt(this.countdown % 60))
+            return `${h}时${m}分${s}妙`
+        }
+    },
+    watch: {
+        activeName: {
+            handler (val) {
+                this.pagination.currentPage = +val
+            }
+        },
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+            // immediate: true
+        },
+        showIndex: {
+            handler (val, oldVal) {
+                this.pagination.currentPage = Math.ceil(val / this.pagination.pageSize)
+                this.activeName = this.pagination.currentPage + ''
+                const temp = this.questionList[oldVal - 1]
+                if (['填空题'].includes(temp.questionType)) {
+                    temp.answer = temp.options.map(item => item.answer)
+                }
+            }
+        }
+    },
+    mounted () {
+        this.loadData()
+        this.startCountdown()
+        // Watermark.set(`123`, `123123`)
+        // 监听键盘事件
+        window.addEventListener('keyup', this.handleKeyPress)
+        window.addEventListener('beforeunload', this.handleBeforeUnload)
+    },
+    beforeDestroy () {
+        window.removeEventListener('keyup', this.handleKeyPress)
+        window.removeEventListener('beforeunload', this.handleBeforeUnload)
+        clearInterval(this.timer)
+        // Watermark.set('', '')
+    },
+    methods: {
+        pageRange (pageNo) {
+            return {
+                start: (pageNo - 1) * this.pagination.pageSize + 1,
+                end: pageNo * this.pagination.pageSize > this.questionList.length ? this.questionList.length : pageNo * this.pagination.pageSize
+            }
+        },
+        pageLabel (pageNo) {
+            const { start, end } = this.pageRange(pageNo)
+            return `${start}-${end}`
+        },
+        // 获取题库数据
+        async loadData () {
+            if (!this.bankId) {
+                this.$message.error('获取题目信息失败,请重试!')
+                this.closeDialog()
+                return
+            }
+            this.questionList = await this.getQuestionData()
+            // console.log(this.questionList)
+            // console.log(this.examData)
+        },
+        startCountdown () {
+            const timer = setInterval(() => {
+                if (this.countdown > 0) {
+                    this.countdown--
+                    // 判断是否到达指定时间
+                    if (this.countdown <= 60 && !this.countdownNotify) {
+                        this.$notify({
+                            title: '温馨提示',
+                            message: `距离自动提交还有${this.countdown}秒,如有未完成的试题,请尽快填写答案!`,
+                            type: 'warning'
+                        })
+                        this.countdownNotify = true
+                    }
+                    if (this.countdown === 0) {
+                        this.submitForm(this.dealFormData())
+                        clearInterval(timer)
+                    }
+                }
+            }, 1000)
+        },
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'prev':
+                    this.showIndex--
+                    break
+                case 'next':
+                    this.showIndex++
+                    break
+                case 'submit':
+                    this.handleSubmit()
+                    break
+                case 'cancel':
+                    this.handleCancel()
+                    break
+                default:
+                    break
+            }
+        },
+        formatNum (num) {
+            return num < 10 ? `0${num}` : num
+        },
+        // 随机题目
+        getRandQuestionData (data) {
+            let temp = []
+            data = shuffle(data)
+            const type = ['单选题', '多选题', '判断题', '填空题', '简答题']
+            this.examData.randNumber = this.examData.randNumber.split(',')
+            for (let i = 0; i < this.examData.randNumber.length; i++) {
+                let t = +this.examData.randNumber[i]
+                // console.log(t)
+                if (t !== 0) {
+                    // 题型分类
+                    if (this.examData.randWay === '1') {
+                        for (let j = 0; j < data.length && t; j++) {
+                            const item = data[j]
+                            if (item.questionType === type[i]) {
+                                temp.push(item)
+                                t--
+                            }
+                        } // 难度分类
+                    } else if (this.examData.randWay === '2') {
+                        for (let j = 0; j < data.length && t; j++) {
+                            const item = data[j]
+                            if (item.questionLevel === i + '') {
+                                temp.push(item)
+                                t--
+                            }
+                        }
+                    }
+                }
+            }
+            temp = shuffle(temp)
+            temp.sort((a, b) => {
+                return type.indexOf(a.questionType) - type.indexOf(b.questionType)
+            })
+            return temp
+        },
+        getQuestionData () {
+            this.loading = true
+            const sql = `select id_ as questionId, ti_gan_ as stem, ti_xing_ as questionType, fu_tu_ as img, xuan_xiang_lei_xi as optionType, da_an_ as options, xuan_xiang_shu_ as optionsLength, fen_zhi_ as score, ping_fen_fang_shi as rateType, ping_fen_ren_ as rater, zheng_que_da_an_ as rightKey,nan_du_ as questionLevel from t_questions where parent_id_ = '${this.bankId}' and zhuang_tai_ = '启用' order by field(ti_xing_, '单选题', '多选题', '判断题', '填空题', '简答题')`
+            return new Promise((resolve, reject) => {
+                this.$common.request('sql', sql).then(res => {
+                    let { data = [] } = res.variables || {}
+                    if (!data.length) {
+                        this.$message.error('获取题目信息失败!')
+                        this.closeDialog()
+                        return
+                    }
+                    if (this.examData.isRand === '1') {
+                        data = this.getRandQuestionData(data)
+                    }
+                    data.map(item => {
+                        if (item.options) {
+                            const temp = JSON.parse(item.options) || {}
+                            const o = []
+                            Object.keys(temp).forEach(key => {
+                                o.push({
+                                    label: key,
+                                    value: temp[key]
+                                })
+                            })
+                            item.options = o
+                        } else {
+                            item.options = Array.from({ length: item.optionsLength }, (v, k) => {
+                                return { answer: null }
+                            })
+                        }
+                        item.img = item.img ? JSON.parse(item.img) : ''
+                        item.answer = item.questionType === '多选题' ? [] : null
+                    })
+                    this.loading = false
+                    resolve(data)
+                }).catch(error => {
+                    this.loading = false
+                    reject(error)
+                })
+            })
+        },
+        getProgress () {
+            const finished = []
+            this.questionList.forEach(item => {
+                if (item.questionType === '填空题') {
+                    finished.push(item.answer && !item.answer.some(i => i === '' || i === null))
+                } else if (item.questionType === '多选题') {
+                    finished.push(item.answer.some(i => i) && true)
+                } else {
+                    finished.push(item.answer && true)
+                }
+            })
+            // const finished = this.questionList.filter(item => (item.questionType !== '填空题' && item.answer) || (item.questionType === '填空题' && item.answer && !item.answer.some(i => i === '' || i === null))).length
+            const finishedCount = finished.filter(i => i).length
+            const progress = round(parseFloat(finishedCount / this.questionList.length * 100), 2)
+            return progress ? progress > 100 ? 100 : progress : 0
+        },
+        changeOptions (val) {
+            this.questionList[this.showIndex - 1].answer = val
+        },
+        setClassName (item, index) {
+            const result = []
+            if (item.answer) {
+                // 填空题且未填写所有答案
+                if (['填空题'].includes(item.questionType) && (item.options.every(i => !i.answer))) {
+                    result.push('')
+                } else if (['多选题'].includes(item.questionType) && !item.answer.length) {
+                    result.push('')
+                } else if (item.questionType === '填空题' && (item.options.some(i => !i.answer))) {
+                    result.push('incomplete')
+                } else {
+                    result.push('finished')
+                }
+            }
+            if (this.showIndex === index + 1) {
+                result.push('active')
+            }
+            return result.join(' ')
+        },
+        handleKeyPress (event) {
+            // console.log(event.keyCode, event.key)
+            if (event.keyCode === 37 || event.key === 'ArrowLeft') {
+                if (this.textareaFocused === true) return // 如果是聚焦状态 那么键盘的左键不应该执行上一题操作
+                if (this.showIndex === 1) {
+                    this.$message.warning('已经是第一题了!')
+                } else {
+                    this.showIndex--
+                }
+            } else if (event.keyCode === 39 || event.key === 'ArrowRight') {
+                if (this.textareaFocused === true) return // 如果是聚焦状态 那么键盘的右键不应该执行下一题操作
+                if (this.showIndex === this.questionList.length) {
+                    this.$message.warning('已经是最后一题了!')
+                } else {
+                    this.showIndex++
+                }
+            } else if (event.keyCode === 27 || event.key === 'Escape') {
+                this.handleCancel()
+            }
+
+            // 更新键盘状态
+            this.lastLastKey = this.lastKey
+            this.lastKey = event.key
+        },
+        handleBeforeUnload (event) {
+            const confirmationMessage = '离开将自动提交当前数据,确定要离开吗?';
+            (event || window.event).returnValue = confirmationMessage
+            window.addEventListener('unload', this.submitDataAfterConfirmation)
+            return confirmationMessage
+        },
+        submitDataAfterConfirmation () {
+            this.submitData(this.dealFormData())
+            // 移除事件监听器,避免重复触发提交逻辑
+            window.removeEventListener('unload', this.submitDataAfterConfirmation)
+        },
+        goNext () {
+            if (this.showIndex === this.questionList.length) {
+                return
+            }
+            setTimeout(() => {
+                this.showIndex++
+            }, 200)
+        },
+        getScore ({ questionType, answer, rightKey, score }) {
+            if (questionType === '多选题') {
+                const temp = rightKey.split(',')
+                return answer.length === temp.length && answer.every(i => temp.includes(i)) ? score : 0
+            } else {
+                return answer === rightKey ? score : 0
+            }
+        },
+        handleSubmit () {
+            const nextHandle = () => {
+                let incompleteList = []
+                this.questionList.forEach((item, index) => {
+                    if (item.questionType === '填空题') {
+                        const t = item.answer && !item.answer.some(i => i === '' || i === null)
+                        incompleteList.push(!t ? index + 1 : '')
+                    } else if (item.questionType === '多选题') {
+                        const t = item.answer && item.answer.length
+                        incompleteList.push(!t ? index + 1 : '')
+                    } else {
+                        incompleteList.push(!item.answer ? index + 1 : '')
+                    }
+                })
+                incompleteList = incompleteList.filter(i => i)
+                const tip = incompleteList.length ? `还有第${incompleteList.join('、')}题未作答,您确定要直接交卷吗?` : '您已完成作答,确定要交卷吗?'
+                this.$confirm(tip, '提示', {
+                    type: incompleteList.length ? 'warning' : 'info',
+                    confirmButtonText: '确定',
+                    cancelButtonText: '取消',
+                    showClose: false,
+                    closeOnClickModal: false
+                }).then(() => {
+                    this.submitForm(this.dealFormData())
+                }).catch(() => {})
+            }
+            // 处理最后一题无法自动提交的bug
+            if (this.questionList[this.questionList.length - 1] !== '判断题' && this.questionList[this.questionList.length - 1] !== '单选题') {
+                const temp = this.showIndex
+                if (this.showIndex !== this.questionList.length) {
+                    this.showIndex = this.questionList.length
+                } else {
+                    this.showIndex = 1
+                }
+                this.$nextTick(() => {
+                    this.showIndex = temp
+                    nextHandle()
+                })
+            } else {
+                nextHandle()
+            }
+        },
+        dealFormData () {
+            const submitData = []
+            const time = this.$common.getDateNow(19)
+            this.questionList.forEach((item, index) => {
+                const autoType = ['单选题', '多选题', '判断题'].includes(item.questionType)
+                const multipleType = ['多选题', '填空题'].includes(item.questionType)
+                const selectType = ['多选题', '单选题'].includes(item.questionType)
+                submitData.push({
+                    parent_id_: this.id,
+                    ti_mu_id_: item.questionId,
+                    ti_gan_: item.stem,
+                    ti_xing_: item.questionType,
+                    fen_zhi_: item.score,
+                    fu_tu_: item.img instanceof Array ? JSON.stringify(item.img) : item.img,
+                    xuan_xiang_lei_xi: item.optionType,
+                    xuan_xiang_: selectType ? JSON.stringify(item.options) : '',
+                    can_kao_da_an_: item.rightKey,
+                    ping_fen_fang_shi: item.rateType,
+                    ping_fen_ren_: item.rater,
+                    hui_da_: multipleType && item.answer ? JSON.stringify(item.answer) : item.answer,
+                    shi_fou_yi_yue_: autoType ? '是' : '否',
+                    ping_yue_shi_jian: autoType ? time : '',
+                    de_fen_: autoType ? this.getScore(item) : '',
+                    jie_xi_: '',
+                    nan_du_: item.questionLevel || 0
+                })
+            })
+            return submitData
+        },
+        submitForm (data) {
+            const addParams = {
+                tableName: 't_examination_detail',
+                paramWhere: data
+            }
+            const isAllReviewed = !data.some(i => i.shi_fou_yi_yue_ === '否')
+            const updateParams = {
+                tableName: 't_examination',
+                updList: [
+                    {
+                        where: {
+                            id_: this.id
+                        },
+                        param: {
+                            jie_shu_shi_jian_: this.$common.getDateNow(19),
+                            ti_ku_zong_fen_: data.reduce((sum, item) => sum + parseFloat(item.fen_zhi_), 0),
+                            zhuang_tai_: isAllReviewed ? '已完成' : '已交卷',
+                            de_fen_: isAllReviewed ? data.reduce((sum, item) => sum + parseFloat(item.de_fen_), 0) : ''
+                            // sheng_yu_shi_chan: ''
+                        }
+                    }
+                ]
+            }
+            console.log(addParams, updateParams)
+            this.$common.request('add', addParams).then(() => {
+                this.$common.request('update', updateParams).then(() => {
+                    this.$message.success('提交成功!')
+                    this.closeDialog()
+                })
+            })
+        },
+        handleCancel () {
+            const nextHandle = () => {
+                this.$confirm('中途退出将自动提交考试数据,是否确认操作?', '提示', {
+                    confirmButtonText: '确定',
+                    cancelButtonText: '取消',
+                    type: 'warning',
+                    showClose: false,
+                    closeOnClickModal: false,
+                    closeOnPressEscape: false
+                }).then(() => {
+                    this.submitForm(this.dealFormData())
+                })
+            }
+            // 处理最后一题无法自动提交的bug
+            if (this.questionList[this.questionList.length - 1] !== '判断题' && this.questionList[this.questionList.length - 1] !== '单选题') {
+                if (this.showIndex !== this.questionList.length) {
+                    this.showIndex = this.questionList.length
+                } else {
+                    this.showIndex = 1
+                }
+                this.$nextTick(nextHandle)
+            } else {
+                nextHandle()
+            }
+        },
+        hilarity () {
+            this.$notify({
+                title: '提示',
+                message: '考试时间已到,即将自动交卷,请勿关闭页面防止数据丢失!',
+                duration: 0
+            })
+        },
+        // 关闭当前窗口
+        closeDialog () {
+            window.removeEventListener('keyup', this.handleKeyPress)
+            window.removeEventListener('beforeunload', this.handleBeforeUnload)
+            clearInterval(this.timer)
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .test-dialog {
+        ::v-deep {
+            .el-dialog__body {
+                height: calc(100vh - 55px);
+                width: 1080px;
+                margin: 0 auto;
+                box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+                .el-radio, .el-checkbox {
+                    display: block;
+                    margin-bottom: 10px;
+                }
+                .el-radio__label, .el-checkbox__label {
+                    font-size: 16px;
+                    white-space: normal;
+                    line-height: 1.5;
+                }
+                .el-input {
+                    margin-bottom: 10px;
+                }
+                .el-tag {
+                    margin-right: 0;
+                }
+                .list-group {
+                    display: inline-block;
+                    width: 100%;
+                }
+            }
+            .el-dialog__header {
+                text-align: center;
+            }
+        }
+        .container {
+            position: relative;
+            height: calc(100vh - 160px);
+            padding: 20px;
+            .question {
+                font-size: 16px;
+                .question-item {
+                    margin-bottom: 10px;
+                    .type {
+                        font-size: 20px;
+                        font-weight: 600;
+                        margin-bottom: 20px;
+                    }
+                    .stem {
+                        display: flex;
+                        margin-bottom: 20px;
+                        align-items: flex-start;
+                        > span:first-child {
+                            line-height: 1.5;
+                            margin-right: 10px;
+                        }
+                    }
+                    .img {
+                        position: relative;
+                        width: 100%;
+                        margin-bottom: 10px;
+                    }
+                }
+            }
+            .question-link {
+                display: flex;
+                flex-direction: column;
+                position: absolute;
+                width: calc(100% - 40px);
+                bottom: 20px;
+                .question{
+                    display: flex;
+                    flex-wrap: wrap;
+                    align-content: flex-start;
+                    height: 120px;
+                    overflow-y: auto;
+                }
+                .progress {
+                    width: 100%;
+                    margin-bottom: 10px;
+                    ::v-deep .el-progress-bar__innerText {
+                        color: #fff !important;
+                    }
+                }
+                .link-item {
+                    width: 32px;
+                    height: 32px;
+                    text-align: center;
+                    line-height: 32px;
+                    border: 1px solid #DCDFE6;
+                    margin: 3px;
+                    border-radius: 2px;
+                    cursor: pointer;
+                    &:hover {
+                        background-color: #F5F7FA;
+                    }
+                }
+                $button-themes: (
+                    active: (#409EFF, #fff, #409EFF),
+                    incomplete: (#E6A23C, #fff, #E6A23C),
+                    finished: (#67C23A, #fff, #67C23A)
+                );
+                @each $theme, $colors in $button-themes {
+                    .#{$theme} {
+                        background-color: nth($colors, 1);
+                        color: nth($colors, 2);
+                        border-color: nth($colors, 3);
+
+                        &:hover {
+                            background-color: lighten(nth($colors, 1), 10%);
+                            border-color: lighten(nth($colors, 1), 10%);
+                        }
+                    }
+                }
+            }
+        }
+        .tips {
+            position: fixed;
+            font-size: 12px;
+            right: calc(1100px + (100% - 1080px) / 2);
+            top: 50%;
+            width: auto;
+            max-width: 200px;
+            transform: translate(0, -50%);
+            background-color: rgba(0, 0, 0, .6);
+            color: #DCDFE6;
+            margin-left: 20px;
+            padding: 10px;
+            border-radius: 5px;
+            .tips-title {
+                font-size: 16px;
+                font-weight: 600;
+                margin-bottom: 10px;
+            }
+            .tips-content {
+                font-size: 14px;
+                line-height: 20px;
+                margin-bottom: 8px;
+                > span {
+                    border-radius: 2px;
+                    padding: 2px 5px;
+                    margin: 0 2px;
+                }
+            }
+        }
+        .custom-title {
+            display: flex;
+            position: relative;
+            justify-content: center;
+            .dialog-title {
+                font-size: 18px;
+                line-height: 24px;
+            }
+            .countdown {
+                position: absolute;
+                // width: 200px;
+                right: 0;
+                top: -10px;
+            }
+        }
+    }
+</style>