This commit is contained in:
陆光LG
2025-08-20 21:31:24 +08:00
5 changed files with 179 additions and 230 deletions

View File

@@ -40,6 +40,10 @@ export const MonitorApi = {
deleteMonitor: async (id: number) => { deleteMonitor: async (id: number) => {
return await request.delete({ url: `/exam/monitor/delete?id=` + id }) return await request.delete({ url: `/exam/monitor/delete?id=` + id })
}, },
// 获取学生文件
getMonitorStuFileUrl: async (temporaryId: string) => {
return await request.get({ url: `/exam/monitor/getMonitorStuFileUrl?temporaryId=` + temporaryId })
},
// 导出监控管理 Excel // 导出监控管理 Excel
exportMonitor: async (params) => { exportMonitor: async (params) => {

View File

@@ -1,39 +1,37 @@
<template> <template>
<!-- 状态选择弹窗 --> <!-- 状态选择弹窗 -->
<el-dialog v-model="dialogVisible" title="请选择考试状态" width="400px"> <el-dialog v-model="dialogVisible" title="请选择考试状态" width="400px">
<el-form label-width="80px" class="px-4 pt-2">
<el-form label-width="80px" class="px-4 pt-2"> <el-form-item label="考试状态">
<el-form-item label="考试状态"> <el-radio-group v-model="selectedStatus">
<el-radio-group v-model="selectedStatus"> <el-radio :label="'0'">待考</el-radio>
<el-radio :label="'0'"></el-radio> <el-radio :label="'1'">试中</el-radio>
<el-radio :label="'1'">考试</el-radio> <el-radio :label="'2'">考试完成</el-radio>
<el-radio :label="'2'">考试完成</el-radio> </el-radio-group>
</el-radio-group> </el-form-item>
</el-form-item> </el-form>
</el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmChange">确认</el-button> <el-button type="primary" @click="confirmChange">确认</el-button>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog <el-dialog
v-model="initDialogVisible" v-model="initDialogVisible"
title="试卷任务监控列表" title="试卷任务监控列表"
width="90%" width="90%"
top="5vh" top="5vh"
class="-mb-15px" class="-mb-15px"
:close-on-click-modal="false" :close-on-click-modal="false"
:show-close="true" :show-close="true"
@close="handleNextStepCancel" @close="handleNextStepCancel"
> >
<taskMonitor @row-clicked="handleTaskSelected" />
<taskMonitor @row-clicked="handleTaskSelected" /> <template #footer>
<template #footer> <el-button @click="handleNextStepCancel">取消</el-button>
<el-button @click="handleNextStepCancel">取消</el-button> <el-button type="primary" @click="handleNextStep">下一步</el-button>
<el-button type="primary" @click="handleNextStep">下一步</el-button> </template>
</template> </el-dialog>
</el-dialog>
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
@@ -82,10 +80,6 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="考试状态" prop="examStatus"> <el-form-item label="考试状态" prop="examStatus">
<el-select <el-select
v-model="queryParams.examStatus" v-model="queryParams.examStatus"
@@ -93,15 +87,19 @@
clearable clearable
class="!w-240px" class="!w-240px"
> >
<el-option label="待考" :value="0" /> <el-option label="待考" :value="0" />
<el-option label="考试中" :value="1" /> <el-option label="考试中" :value="1" />
<el-option label="考试结束" :value="2" /> <el-option label="考试结束" :value="2" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="任务类别" prop="taskType"> <el-form-item label="任务类别" prop="taskType">
<el-select v-model="queryParams.taskType" placeholder="任务类别" class="!w-240px" @change="fetchTaskList"> <el-select
v-model="queryParams.taskType"
placeholder="任务类别"
class="!w-240px"
@change="fetchTaskList"
>
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.TASK_TYPE)" v-for="dict in getIntDictOptions(DICT_TYPE.TASK_TYPE)"
:key="dict.value" :key="dict.value"
@@ -111,42 +109,44 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="试卷任务" prop="taskName"> <el-form-item label="试卷任务" prop="taskName">
<el-select v-model="queryParams.taskName" placeholder="请选择试卷任务" class="!w-240px"> <el-select v-model="queryParams.taskName" placeholder="请选择试卷任务" class="!w-240px">
<el-option
v-for="item in taskList"
:key="item.taskName"
:label="item.taskName"
:value="item.taskName"
/>
</el-select>
</el-form-item>
<el-form-item label="班级" prop="className">
<el-select
v-model="queryParams.className"
filterable
allow-create
default-first-option
placeholder="请选择或输入"
class="!w-240px"
>
<el-option <el-option
v-for="item in classNameList" v-for="item in taskList"
:key="item.className" :key="item.taskName"
:label="item.className" :label="item.taskName"
:value="item.className" :value="item.taskName"
/>
</el-select>
</el-form-item>
<el-form-item label="班级" prop="className">
<el-select
v-model="queryParams.className"
filterable
allow-create
default-first-option
placeholder="请选择或输入"
class="!w-240px"
>
<el-option
v-for="item in classNameList"
:key="item.className"
:label="item.className"
:value="item.className"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" @click="openDialog"><Icon icon="ep:edit" class="mr-5px" />考试状态改变</el-button> <el-button type="primary" @click="openDialog"
<el-button type="primary" @click="returnTop"><Icon icon="ep:top" class="mr-5px" />返回上级列表</el-button> ><Icon icon="ep:edit" class="mr-5px" />考试状态改变</el-button
<!-- <el-button >
<el-button type="primary" @click="returnTop"
><Icon icon="ep:top" class="mr-5px" />返回上级列表</el-button
>
<!-- <el-button
type="primary" type="primary"
plain plain
@click="openForm('create')" @click="openForm('create')"
@@ -154,30 +154,28 @@
> >
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> --> </el-button> -->
<el-button <el-button type="success" plain @click="handleExport" :loading="exportLoading">
type="success"
plain
@click="handleExport"
:loading="exportLoading"
>
<Icon icon="ep:download" class="mr-5px" /> 导出 <Icon icon="ep:download" class="mr-5px" /> 导出
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @selection-change="handleSelectionChange"> <el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column label="用户账号" align="center" prop="username" /> <el-table-column label="用户账号" align="center" prop="username" />
<el-table-column label="用户姓名" align="center" prop="nickname" /> <el-table-column label="用户姓名" align="center" prop="nickname" />
<el-table-column label="班级" align="center" prop="className" /> <el-table-column label="班级" align="center" prop="className" />
<el-table-column label="考试状态" align="center" prop="examStatus"> <el-table-column label="考试状态" align="center" prop="examStatus">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.EXAM_STATUS" :value="scope.row.examStatus" /> <dict-tag :type="DICT_TYPE.EXAM_STATUS" :value="scope.row.examStatus" />
</template> </template>
</el-table-column> </el-table-column>
@@ -205,24 +203,14 @@
:formatter="dateFormatter" :formatter="dateFormatter"
width="180px" width="180px"
/> />
<!-- <el-table-column label="操作" align="center" min-width="120px">
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope"> <template #default="scope">
<el-button <el-button link type="primary" @click="downloadFile(scope.row)">
link 下载
type="primary"
@click="openForm('update', scope.row.monitorId)"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.monitorId)"
>
删除
</el-button> </el-button>
</template> </template>
</el-table-column> --> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <Pagination
@@ -252,31 +240,31 @@ const selectedTaskId = ref<string | null>(null)
const fixedTaskId = ref('') const fixedTaskId = ref('')
// 接收子组件传递的taskId // 接收子组件传递的taskId
const handleTaskSelected = (taskId: string) => { const handleTaskSelected = (taskId: string) => {
fixedTaskId.value = taskId fixedTaskId.value = taskId
queryParams.taskId = taskId queryParams.taskId = taskId
console.log('父组件接收到的taskId:', queryParams.taskId) console.log('父组件接收到的taskId:', queryParams.taskId)
} }
/** 监控管理 列表 */ /** 监控管理 列表 */
defineOptions({ name: 'Monitor' }) defineOptions({ name: 'Monitor' })
// 新增:初始弹框开关 // 新增:初始弹框开关
const initDialogVisible = ref(true); const initDialogVisible = ref(true)
// 弹窗开关 // 弹窗开关
const dialogVisible = ref(false) const dialogVisible = ref(false)
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const selectedStatus = ref<string | null>(null); const selectedStatus = ref<string | null>(null)
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const list = ref<MonitorVO[]>([]) // 列表的数据 const list = ref<MonitorVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const classNameList = ref(); const classNameList = ref()
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
monitorId:undefined, monitorId: undefined,
taskId:'', taskId: '',
username: undefined, username: undefined,
nickname: undefined, nickname: undefined,
taskType:undefined, taskType: undefined,
className: undefined, className: undefined,
examStatus: undefined, examStatus: undefined,
score: undefined, score: undefined,
@@ -284,7 +272,7 @@ const queryParams = reactive({
taskName: undefined, taskName: undefined,
ip: undefined, ip: undefined,
remainingTime: [], remainingTime: [],
createTime: [], createTime: []
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
@@ -293,13 +281,12 @@ const taskTypeyOptions = ref<string[]>([])
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
queryParams.taskId = fixedTaskId.value queryParams.taskId = fixedTaskId.value
const data = await MonitorApi.getMonitorPage(queryParams) const data = await MonitorApi.getMonitorPage(queryParams)
// queryParams.taskId = '' // queryParams.taskId = ''
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
classNameList.value = await ClassApi.ClassApi.getClassName() classNameList.value = await ClassApi.ClassApi.getClassName()
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -308,41 +295,38 @@ const getList = async () => {
// 打开弹窗 // 打开弹窗
const openDialog = async () => { const openDialog = async () => {
const rows = selections.value; const rows = selections.value
if (!rows.length) { if (!rows.length) {
message.error('请至少选择一条数据'); message.error('请至少选择一条数据')
return; return
} }
dialogVisible.value = true dialogVisible.value = true
selections.value = rows; selections.value = rows
} }
const returnTop = async () => { const returnTop = async () => {
initDialogVisible.value = true initDialogVisible.value = true
} }
/** 表格选中数据 */
const selections = ref([])
/** 表格选中数据 */ const taskList = ref([])
const selections = ref([]);
const taskList = ref([]);
const handleSelectionChange = (rows) => { const handleSelectionChange = (rows) => {
selections.value = rows; selections.value = rows
} }
const dateFormatterMin = (row, column, cellValue) => { const dateFormatterMin = (row, column, cellValue) => {
if (cellValue == null || isNaN(cellValue)) return '-'; if (cellValue == null || isNaN(cellValue)) return '-'
const hours = Math.floor(cellValue / 3600); const hours = Math.floor(cellValue / 3600)
const minutes = Math.floor((cellValue % 3600) / 60); const minutes = Math.floor((cellValue % 3600) / 60)
const seconds = cellValue % 60; const seconds = cellValue % 60
// 补零处理 // 补零处理
const pad = (n) => n.toString().padStart(2, '0'); const pad = (n) => n.toString().padStart(2, '0')
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
}; }
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { const handleQuery = () => {
@@ -362,28 +346,25 @@ const openForm = (type: string, id?: number) => {
formRef.value.open(type, id) formRef.value.open(type, id)
} }
// 提交状态变更 // 提交状态变更
const confirmChange = async () => { const confirmChange = async () => {
if (selectedStatus.value === null) {
if (selectedStatus.value === null) { ElMessage.error('请选择考试状态')
ElMessage.error('请选择考试状态'); return
return;
}
const rows = selections.value;
const monitorIds = rows.map((row: any) => row.monitorId);
await MonitorApi.updateMonitorStatus({
monitorIds, // 这是数组
status: selectedStatus.value // 这是 0,1,2
});
ElMessage.success('考试状态更新成功')
dialogVisible.value = false
getList()
} }
const rows = selections.value
const monitorIds = rows.map((row: any) => row.monitorId)
await MonitorApi.updateMonitorStatus({
monitorIds, // 这是数组
status: selectedStatus.value // 这是 0,1,2
})
ElMessage.success('考试状态更新成功')
dialogVisible.value = false
getList()
}
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
try { try {
@@ -397,39 +378,67 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
const selectedRows = ref<string[]>([]); const downloadFile = async (row: any) => {
const res = await MonitorApi.getMonitorStuFileUrl(row.temporaryId)
console.log(res)
const url = res
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error('下载失败')
}
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
// 提取文件名
const filename = row.username
// 创建 a 标签并下载
const a = document.createElement('a')
a.href = blobUrl
a.download = filename
a.style.display = 'none'
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(blobUrl)
} catch (err: any) {
ElMessage.error(`下载失败:${err.message}`)
}
}
/** 导出按钮操作 */ /** 导出按钮操作 */
const handleExport = async () => { const handleExport = async () => {
const rows = selections.value; const rows = selections.value
if (!rows.length) { if (!rows.length) {
message.error('请至少选择一条数据'); message.error('请至少选择一条数据')
return; return
} }
console.log(rows.length) console.log(rows.length)
try { try {
// 导出的二次确认 // 导出的二次确认
await message.exportConfirm(); await message.exportConfirm()
// 将选中的 monitorId 填入 queryParams // 将选中的 monitorId 填入 queryParams
queryParams.monitorId = rows.map((row: any) => row.monitorId).join(','); queryParams.monitorId = rows.map((row: any) => row.monitorId).join(',')
exportLoading.value = true; exportLoading.value = true
const data = await MonitorApi.exportMonitor(queryParams); const data = await MonitorApi.exportMonitor(queryParams)
download.excel(data, '监控管理.xls'); download.excel(data, '监控管理.xls')
} catch { } catch {
// 可选:错误处理 // 可选:错误处理
} finally { } finally {
exportLoading.value = false; exportLoading.value = false
// 导出后重置 monitorId避免影响后续查询 // 导出后重置 monitorId避免影响后续查询
queryParams.monitorId = ''; queryParams.monitorId = ''
} }
} }
const fetchTaskList = async (taskType: string) => { const fetchTaskList = async (taskType: string) => {
try { try {
const res = await MonitorApi.getPaperTaskList( taskType ) const res = await MonitorApi.getPaperTaskList(taskType)
taskList.value = res taskList.value = res
} catch (error) { } catch (error) {
taskList.value = [] taskList.value = []
@@ -446,9 +455,8 @@ const handleNextStepCancel = () => {
initDialogVisible.value = false initDialogVisible.value = false
} }
/** 初始化 **/ /** 初始化 **/
onMounted(() => { onMounted(() => {
initDialogVisible.value = true; // 显示初始弹框 initDialogVisible.value = true // 显示初始弹框
}) })
</script> </script>

View File

@@ -205,13 +205,12 @@
</el-table> </el-table>
</div> </div>
</el-tab-pane> --> </el-tab-pane> -->
<el-tab-pane name="annex"> <!-- <el-tab-pane name="annex">
<template #label> <template #label>
<div class="custom-tabs-label"> <div class="custom-tabs-label">
<p>试题附件</p> <p>试题附件</p>
</div> </div>
</template> </template>
<!-- 提示 -->
<el-alert type="warning" show-icon :closable="false"> <el-alert type="warning" show-icon :closable="false">
<template #default> <template #default>
<span>提示文件名称可默认不设置</span> <span>提示文件名称可默认不设置</span>
@@ -273,7 +272,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
</el-tab-pane> </el-tab-pane> -->
</el-tabs> </el-tabs>
</div> </div>
</div> </div>
@@ -285,8 +284,6 @@
</template> </template>
</Dialog> </Dialog>
</div> </div>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="FileRef" @success="handleUploadSuccess" />
<el-dialog <el-dialog
v-model="dialogFormVisibleWordInfo" v-model="dialogFormVisibleWordInfo"
title="邮件设置" title="邮件设置"
@@ -391,26 +388,6 @@ function fileTypeFormatter(row, column, cellValue) {
if (cellValue === '2') return '结果文件' if (cellValue === '2') return '结果文件'
return '未知类型' return '未知类型'
} }
const documentList = ref([
{
quId: '',
url: '',
fileType: '0',
fileName: ''
},
{
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}
])
const dialogFormVisibleWordInfo = ref(false) const dialogFormVisibleWordInfo = ref(false)
const addEmailInfo = async () => { const addEmailInfo = async () => {
@@ -492,24 +469,6 @@ const deleteUrl = (index: number) => {
formData.value.fileUploads[index].url = '' formData.value.fileUploads[index].url = ''
formData.value.fileUploads[index].fileName = '' formData.value.fileUploads[index].fileName = ''
} }
// 媒体文件
const mediumList = ref([] as any)
const multipleMediumSelection = ref([] as any)
const handleMediumSelectionChange = (val: any) => {
multipleMediumSelection.value = val
}
const fileList = []
const multipleDocumentSelection = ref([] as any)
const handleDocumentSelectionChange = (val: any) => {
multipleDocumentSelection.value = val
}
const handleUploadSuccess = ({ url, fileType }) => {
const index = documentList.value.findIndex((item) => item.fileType === fileType)
if (index !== -1) {
documentList.value[index].url = url
}
}
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (queryParams: any, type: string, id?: number) => { const open = async (queryParams: any, type: string, id?: number) => {
dialogVisible.value = true dialogVisible.value = true
@@ -524,7 +483,6 @@ const open = async (queryParams: any, type: string, id?: number) => {
formData.value = res formData.value = res
console.log(formData.value) console.log(formData.value)
list.value = formData.value.answerList list.value = formData.value.answerList
documentList.value = res.fileUploads
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
@@ -560,7 +518,6 @@ const submitForm = async () => {
item.scoreRate = '1' // 设置默认权值为1 item.scoreRate = '1' // 设置默认权值为1
} }
}) })
formData.value.fileUploads = documentList.value
const values = Object.values(formData) const values = Object.values(formData)
console.log(values) console.log(values)
// 校验表单 // 校验表单
@@ -630,26 +587,6 @@ const resetForm = () => {
} }
] ]
} }
documentList.value = [
{
quId: '',
url: '',
fileType: '0',
fileName: ''
},
{
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}
]
list.value = [] list.value = []
formRef.value?.resetFields() formRef.value?.resetFields()
} }

View File

@@ -965,12 +965,12 @@ const openPoints = async () => {
} }
} }
/* 固定 el-dialog 高度,内容超出可滚动 */ /* 固定 el-dialog 高度,内容超出可滚动 */
.fixed-dialog-height >>> .el-dialog { .fixed-dialog-height :deep(.el-dialog) {
height: 500px; height: 500px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.fixed-dialog-height >>> .el-dialog__body { :deep(.fixed-dialog-height .el-dialog__body) {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
padding: 0 20px 20px 20px; padding: 0 20px 20px 20px;

View File

@@ -2,10 +2,10 @@
<el-row :gutter="20"> <el-row :gutter="20">
<!-- 左侧部门树 --> <!-- 左侧部门树 -->
<el-col :span="4" :xs="24"> <el-col :span="4" :xs="24">
<ContentWrap style="height: 50%; overflow-y: auto"> <ContentWrap style="height: 30%; overflow-y: auto">
<SpecialtyTree @node-click="handleSpecialtyNodeClick" /> <SpecialtyTree @node-click="handleSpecialtyNodeClick" />
</ContentWrap> </ContentWrap>
<ContentWrap style="height: 50%; overflow-y: auto"> <ContentWrap style="height: 30%; overflow-y: auto">
<DeptTree @node-click="handleDeptNodeClick" /> <DeptTree @node-click="handleDeptNodeClick" />
</ContentWrap> </ContentWrap>
</el-col> </el-col>