【新增】 监控管理下载学生文件

This commit is contained in:
dlaren
2025-08-19 16:04:01 +08:00
parent 7ad2ee0b67
commit 1431a96dad
2 changed files with 172 additions and 161 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,16 +1,15 @@
<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>
@@ -18,22 +17,21 @@
</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-select>
</el-form-item> </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-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.temporaryId)">
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,66 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
const selectedRows = ref<string[]>([]); const downloadFile = async (temporaryId: string) => {
const res = await MonitorApi.getMonitorStuFileUrl(temporaryId)
const url = res
try {
const response = await fetch(temporaryId)
if (!response.ok) {
throw new Error('下载失败')
}
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
// 提取文件名
const filename = url.substring(url.lastIndexOf('/') + 1).split('?')[0]
// 创建 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 +454,8 @@ const handleNextStepCancel = () => {
initDialogVisible.value = false initDialogVisible.value = false
} }
/** 初始化 **/ /** 初始化 **/
onMounted(() => { onMounted(() => {
initDialogVisible.value = true; // 显示初始弹框 initDialogVisible.value = true // 显示初始弹框
}) })
</script> </script>