【新增】试卷任务前端分类,监控管理前端

This commit is contained in:
YOHO\20373
2025-05-05 00:05:46 +08:00
parent 7a6472d8d4
commit e8a418b9f7
134 changed files with 19516 additions and 493 deletions

View File

@@ -0,0 +1,369 @@
<template>
<Dialog
v-model="visible"
:title="isUpdate ? '修改试卷方案' : '添加试卷方案'"
width="460"
center
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
@submit.prevent=""
>
<!-- 知识点 -->
<el-form-item label="知识点" prop="pointName">
<el-tree
ref="treeRef"
:data="pointOptions"
show-checkbox
node-key="value"
default-expand-all
:props="{ label: 'label', children: 'children' }"
highlight-current
:disabled="isUpdate"
@check="handleCheckChange"
/>
</el-form-item>
<!-- 题型 -->
<el-form-item label="题型" prop="spName">
<el-select v-model="form.spName" placeholder="请选择题型" clearable :disabled="isUpdate">
<el-option
v-for="item in CourseOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<!-- 难度 -->
<el-form-item label="难度" prop="quLevel">
<el-select v-model="form.quLevel" placeholder="请选择难度" :disabled="isUpdate">
<el-option label="全部" :value="3" />
<el-option label="简单" :value="0" />
<el-option label="一般" :value="1" />
<el-option label="困难" :value="2" />
</el-select>
</el-form-item>
<!-- 关键字 -->
<el-form-item label="关键字" prop="keyword">
<el-select
v-model="form.keyword"
placeholder="请选择关键字"
multiple
clearable
:disabled="isUpdate"
>
<el-option
v-for="item in keyOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="试题别名" prop="quTitle">
<el-input v-model="form.quTitle" placeholder="请输入试题别名" :disabled="isUpdate" />
</el-form-item>
<!-- 数量分数 -->
<el-form-item label="试题数量" prop="quNumbers">
<el-input-number v-model="form.quNumbers" placeholder="请输入试题数量" :disabled="isUpdate" />
</el-form-item>
<el-form-item label="每题分数" prop="quScores">
<el-input-number v-model="form.quScores" placeholder="请输入每题分数" :disabled="isUpdate" />
</el-form-item>
<el-form-item label="小计分数" prop="subtotalScore">
<el-input-number v-model="form.subtotalScore" :disabled="true" />
</el-form-item>
<el-form-item label="可用数量">
<el-text>{{ availableCount }}</el-text>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false"> </el-button>
<el-button type="primary" :loading="loading" @click="save" :disabled="isUpdate">保存</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import {
addScheme,
updateScheme,
getCourseList,
getKeyList,
getPoints,
fetchQuestionCount
} from '@/api/system/scheme'
const message = useMessage() // 消息弹窗
const emit = defineEmits(['done'])
const props = defineProps({
taskSpecialty: String,
taskId: String
})
/** --------- 状态定义 --------- **/
const visible = defineModel({ type: Boolean });
const isUpdate = ref(false)
const loading = ref(false)
const formRef = ref()
const treeRef = ref()
const availableCount = ref<number | string>('')
const CourseOptions = ref<string[]>([])
const keyOptions = ref<string[]>([])
// 扁平接口返回的原始结构
interface PointRaw {
spId: number
spName: string
parentId: number
orderNum?: number
ancestors?: string
[key: string]: any
}
// 构造给 el-tree 用的节点结构
interface TreeNode extends PointRaw {
label: string
value: number
children: TreeNode[]
}
const pointOptions = ref<TreeNode[]>([])
const form = ref<{
schemeId?: string
taskId?: string
spName?: string
quTitle?: string
quLevel?: number
keyword: string[]
pointName: string[]
quNumbers?: number
quScores?: number
sort?: number
subtotalScore?: number
taskSpecialty?: string
}>({
keyword: [],
pointName: []
})
const rules = reactive({
spName: [{ required: true, message: '请选择题型', trigger: 'change' }],
quLevel: [{ required: true, message: '请选择难度', trigger: 'change' }],
sort: [{ required: true, message: '请设置排序', trigger: 'change' }],
quTitle: [{ required: true, message: '请输入别名', trigger: 'change' }],
pointName: [{ required: true, message: '请选择知识点', trigger: 'change' }],
quNumbers: [{ required: true, message: '请输入试题数量', trigger: 'blur' }],
quScores: [{ required: true, message: '请输入每题分数', trigger: 'blur' }]
})
/** --------- 打开弹窗 --------- **/
const open = async (type: 'create' | 'update', data?: any) => {
visible.value = true
isUpdate.value = type === 'update'
// 1. 并行拉下拉所需的选项
const [courses, keywords] = await Promise.all([
getCourseList(),
getKeyList()
])
CourseOptions.value = courses
keyOptions.value = keywords
// 2. 拉知识点并建树
await loadPointTree()
// 3. 填表单
if (isUpdate.value && data) {
form.value = {
...data,
keyword: data.keywords?.split(',') || [],
pointName: data.pointNames?.split(',') || [],
taskId: props.taskId,
taskSpecialty: props.taskSpecialty
}
// 4. 等树节点数据都生效了,再设置默认选中
await nextTick()
treeRef.value?.setCheckedKeys(form.value.pointName)
calcSubtotal()
fetchCount()
} else {
// 新增时重置
form.value = {
schemeId: undefined,
taskId: props.taskId,
spName: undefined,
quLevel: undefined,
quTitle:undefined,
keyword: [],
pointName: [],
quNumbers: undefined,
quScores: undefined,
subtotalScore: undefined,
sort: undefined,
taskSpecialty: props.taskSpecialty
}
}
// 清除校验
await nextTick()
formRef.value?.clearValidate?.()
}
/** --------- 拉取扁平知识点并建树 --------- **/
async function loadPointTree() {
try {
const res = await getPoints(props.taskSpecialty)
// 兼容getPoints 有可能直接返回数组,也可能返回 { data: [] }
const raw: PointRaw[] = Array.isArray(res)
? res
: Array.isArray((res as any).data)
? (res as any).data
: []
pointOptions.value = buildTree(raw)
} catch (e: any) {
message.error('获取知识点失败:' + (e.message || e))
pointOptions.value = []
}
}
/** 扁平 -> 树 */
function buildTree(data: PointRaw[]): TreeNode[] {
const idMap = new Map<number, TreeNode>()
data.forEach(item => {
idMap.set(item.spId, {
...item,
label: item.spName,
value: item.spId,
children: []
})
})
const tree: TreeNode[] = []
data.forEach(item => {
const node = idMap.get(item.spId)!
const parent = idMap.get(item.parentId)
if (parent) {
parent.children.push(node)
} else {
tree.push(node)
}
})
// (可选)按 orderNum 排序
const sortRecursively = (nodes: TreeNode[]) => {
nodes.sort((a, b) => (a.orderNum ?? 0) - (b.orderNum ?? 0))
nodes.forEach(n => sortRecursively(n.children))
}
sortRecursively(tree)
return tree
}
/** 计算小计 */
const calcSubtotal = () => {
const n = Number(form.value.quNumbers ?? 0)
const s = Number(form.value.quScores ?? 0)
form.value.subtotalScore = n * s
}
/** 选中回调 */
const handleCheckChange = () => {
form.value.pointName = treeRef.value.getCheckedKeys()
}
/** 拉取可用题目数量 */
const fetchCount = async () => {
const res = await fetchQuestionCount({
taskSpecialty: props.taskSpecialty,
pointIds: form.value.pointName, // 知识点
spName: form.value.spName, // 题型
quLevel: form.value.quLevel, // 难度
keyword: form.value.keyword // **关键字**,加上这一行
})
availableCount.value = res ?? 0
}
/** 保存 */
const save = async () => {
// 判断试题数量是否小于等于可用题目数量
if (Number(form.value.quNumbers) > Number(availableCount.value)) {
message.warning('试题数量不能超过可用题目数量');
return;
}
if (Number(availableCount.value) === 0) {
message.warning('当前条件下没有可用的试题,请调整条件后再试');
return;
}
await formRef.value.validate()
loading.value = true
try {
const payload = {
...form.value,
keywords: form.value.keyword.join(','),
pointNames: form.value.pointName.join(',')
}
const fn = isUpdate.value ? updateScheme : addScheme
await fn(payload)
emit('done')
visible.value = false
} finally {
loading.value = false
}
}
watch(
() => [
form.value.spName,
form.value.quLevel,
form.value.pointName,
form.value.keyword,
props.taskSpecialty
],
([spName, quLevel, pointName, keyword, taskSpecialty]) => {
// 当题型、难度、专业、知识点、关键字都有值时,才去请求可用题目数
if (
spName &&
quLevel !== null &&
quLevel !== undefined &&
taskSpecialty &&
pointName!== null &&
keyword!== null // **关键字也要有选项**
// 如果你也想加关键字限制:&& keyword.length > 0
) {
fetchCount()
}
},
{ deep: true }
)
watch(
() => [form.value.quNumbers, form.value.quScores],
() => {
calcSubtotal()
}
)
defineExpose({ open })
</script>

View File

@@ -0,0 +1,82 @@
<!-- 搜索表单 -->
<template>
<ele-card :body-style="{ paddingBottom: '2px' }">
<el-form label-width="72px" @keyup.enter="search" @submit.prevent="">
<el-row :gutter="8">
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="题型">
<el-input
clearable
v-model.trim="form.spName"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="难度">
<el-input
clearable
v-model.trim="form.quLevel"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="关键字">
<el-input
clearable
v-model.trim="form.keywords"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="知识点">
<el-input
clearable
v-model.trim="form.pointNames"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label-width="16px">
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</ele-card>
</template>
<script setup>
import { useFormData } from '@/utils/use-form-data';
const emit = defineEmits(['search']);
/** 表单数据 */
const [form, resetFields] = useFormData({
taskId: '',
spName: '',
quLevel: '',
keywords: '',
pointNames: '',
quNumbers: '',
quScores: '',
subtotalScore: ''
});
/** 搜索 */
const search = () => {
emit('search', { ...form });
};
/** 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -0,0 +1,223 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="题型" prop="spName">
<el-select v-model="queryParams.spName" placeholder="请选择题型" clearable class="!w-240px">
<el-option
v-for="item in CourseOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item>
<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
type="primary"
plain
@click="openEdit('create')"
v-hasPermi="['system:sms-channel:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增</el-button
>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="方案ID" align="center" prop="taskId" />
<el-table-column label="题型" align="center" prop="spName" />
<el-table-column label="难度" align="center" prop="quLevel">
<template #default="scope">
<span v-if="scope.row.quLevel === '0'">简单</span>
<span v-else-if="scope.row.quLevel === '1'">一般</span>
<span v-else-if="scope.row.quLevel === '2'">困难</span>
<span v-else-if="scope.row.quLevel === '3'">全部</span>
<span v-else>未知</span>
</template>
</el-table-column>
<!-- <el-table-column label="关键字" align="center" prop="keywords" /> -->
<el-table-column label="试题数量" align="center" prop="quNumbers"/>
<el-table-column label="每题分数" align="center" prop="quScores"/>
<el-table-column label="小计分数" align="center" prop="subtotalScore"/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openEdit('update', scope.row)"
v-hasPermi="['system:sms-channel:update']"
>
查看
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.schemeId)"
v-hasPermi="['system:sms-channel:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<scheme-edit v-model="showEdit" v-if="showEdit" :form-data="props.formData" :task-specialty="props.taskSpecialty" :task-Id="props.taskId" ref="taskEditRef" @done="reload"/>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as SmsChannelApi from '@/api/system/scheme';
import SchemeEdit from './components/step-edit.vue';
import SchemeSearch from './components/step-search.vue';
const CourseOptions = ref<string[]>([])
import {
getCourseList,
} from '@/api/system/scheme'
defineOptions({ name: 'SystemSmsChannel' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const props = defineProps({
taskSpecialty: {
type: String,
default: ''
},
taskId: {
type: String,
default: ''
},
formData:{
type: Object,
default: null
}
})
/** 当前编辑数据 */
const current = ref<object>();
/** 是否显示编辑弹窗 */
const showEdit = ref(false);
const showOpenTemplate = ref(false);
const showAdd = ref(false);
const smsChannelFormRef = ref()
const taskEditRef = ref()
const taskAddRef = ref()
const taskTempRef = ref()
const loading = ref(false) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
signature: undefined,
status: undefined,
createTime: [],
taskId: props.taskId ,
spName:undefined
})
/** 查询列表 */
const getList = async () => {
// 1. 并行拉下拉所需的选项
const [courses] = await Promise.all([
getCourseList(),
])
CourseOptions.value = courses
loading.value = true
try {
const res = await SmsChannelApi.pageSchemes(queryParams)
console.log(res)
list.value = res
total.value = res.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
const reload = () => {
getList()
}
/** 添加/修改操作 */
const formRef = ref();
const openEdit = (type: string, row?: object) => {
showEdit.value = true
current.value = row
nextTick(() => {
taskEditRef.value?.open(type, row)
})
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
console.log(id+"idididid")
await SmsChannelApi.removeScheme(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,106 @@
<!-- 编辑弹窗 -->
<template>
<Dialog v-model="isVisible" :title="'添加试卷'" width="460" @open="handleOpen" center>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
@submit.prevent=""
>
<el-form-item label="试卷数目" prop="num">
<el-input
clearable
v-model="form.num"
placeholder="请输入试卷数目"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="save">
保存
</el-button>
</template>
</Dialog>
</template>
<script setup>
import { ref, reactive, nextTick } from 'vue';
import { useFormData } from '@/utils/use-form-data';
import { addPaper } from '@/api/system/paper';
const message = useMessage() // 消息弹窗
const props = defineProps({
/** 修改回显的数据 */
data: Object,
taskId: String ,
taskSpecialty: String,
});
const emit = defineEmits(['done']);
/** 弹窗是否打开 */
const isVisible = defineModel({ type: Boolean });
/** 提交状态 */
const loading = ref(false);
/** 表单实例 */
const formRef = ref(null);
/** 表单数据 */
const [form, resetFields, assignFields] = useFormData({
num: '',
taskId: ''
});
/** 表单验证规则 */
const rules = reactive({
num: [
{ required: true, message: '请输入试卷数目', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' }
]
});
/** 关闭弹窗 */
const handleCancel = () => {
isVisible.value = false;
};
/** 保存编辑 */
const save = () => {
formRef.value?.validate?.((valid) => {
if (!valid) {
return;
}
loading.value = true;
addPaper({ num: form.num,taskid: props.taskId ,taskSpecialty:form.taskSpecialty})
.then((msg) => {
loading.value = false;
message.success(msg);
handleCancel();
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
});
};
/** 弹窗打开事件 */
const handleOpen = () => {
resetFields();
form.taskId = props.taskId; // 新增时赋值 taskId
form.taskSpecialty = props.taskSpecialty; // 新增时赋值 taskSpecialty
nextTick(() => {
nextTick(() => {
formRef.value?.clearValidate?.();
});
});
};
defineExpose({ open })
</script>

View File

@@ -0,0 +1,181 @@
<template>
<Dialog v-model="visible" :title="'修改试卷'" width="460" @open="handleOpen" center>
<div style="margin-bottom: 16px">
<strong>试卷ID</strong>{{ paperId }}
</div>
<!-- 滚动区域 -->
<div style="max-height: 400px; overflow-y: auto;">
<!-- 分组展示题目 -->
<div
v-for="(items, subject) in sortedGroupedQuestions"
:key="subject"
>
<h4 class="mb-2 text-base text-gray-700 font-medium">
{{ subject }}
<span v-if="schemeMap[subject]" class="text-sm text-gray-500 font-normal">
每小题 {{ parseFloat(schemeMap[subject].quScores).toFixed(1) }} {{ parseFloat(schemeMap[subject].subtotalScore).toFixed(1) }}
</span>
</h4>
<el-card
v-for="(item, index) in items"
:key="item.quId"
class="mb-3"
shadow="never"
>
<div class="text-gray-500 mb-2">题目 {{ index + 1 }}</div>
<div v-html="item.content" class="mb-2"></div>
<!-- 🎯 判断是否为编程题 -->
<div v-if="item.subjectName === '编程题'">
<!-- 编程题解析 -->
<div class="mt-2">
<div class="font-bold text-blue-600">解析</div>
<pre
style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;"
>{{ item.analysis }}</pre>
</div>
<div
v-for="(answer, aIndex) in item.answerList"
:key="answer.answerId"
class="mb-1"
>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}%
</div>
</div>
<!-- 📝 普通题目的选项显示 -->
<div v-else-if="item.answerList && item.answerList.length">
<div
v-for="(answer, aIndex) in item.answerList.slice().sort((a, b) => a.sort - b.sort)"
:key="answer.answerId"
:style="{
color: answer.isRight === '0' ? 'green' : 'inherit',
fontWeight: answer.isRight === '0' ? 'bold' : 'normal'
}"
class="mb-1"
>
{{ String.fromCharCode(65 + aIndex) }}. {{ answer.content }}
</div>
</div>
</el-card>
</div>
</div>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
</template>
</Dialog>
</template>
<script setup>
import { ref, reactive, nextTick, computed } from 'vue';
import { useFormData } from '@/utils/use-form-data';
import { getPaperDetailByTaskId } from '@/api/system/paper';
const message = useMessage() // 消息弹窗
const props = defineProps({
paperId: String
});
const emit = defineEmits(['done']);
const visible = defineModel({ type: Boolean });
const loading = ref(false);
const formRef = ref(null);
const questionList = ref([]); // 用于存放试题列表
const schemeMap = ref({});
const [form, resetFields, assignFields] = useFormData({
paperId: void 0,
taskId: '',
counts: '',
rollUp: '',
isAb: '',
status: ''
});
const rules = reactive({});
const handleCancel = () => {
visible.value = false;
};
const groupedQuestions = ref({});
const subjectPriority = {
'选择题': 1,
'多选题': 2,
'判断题': 3,
'编程题': 4,
'其他': 5
};
const groupBySubjectName = (list) => {
const group = {};
list.forEach((item) => {
const subject = item.subjectName || '其他'; // 默认为'其他'
if (!group[subject]) {
group[subject] = [];
}
group[subject].push(item);
});
groupedQuestions.value = group;
};
const sortedGroupedQuestions = computed(() => {
return Object.keys(groupedQuestions.value)
.sort((a, b) => (subjectPriority[a] || subjectPriority['其他']) - (subjectPriority[b] || subjectPriority['其他']))
.reduce((acc, subject) => {
acc[subject] = groupedQuestions.value[subject];
return acc;
}, {});
});
const handleOpen = () => {
resetFields();
questionList.value = [];
if (props.paperId) {
loading.value = true;
getPaperDetailByTaskId(props.paperId)
.then((res) => {
const { educationPaperSchemeList, examQuestionList } = res.data || {};
if (Array.isArray(examQuestionList)) {
questionList.value = examQuestionList;
groupBySubjectName(examQuestionList); // 分类
}
if (Array.isArray(educationPaperSchemeList)) {
// 构建以题型名为 key 的映射表spName 与 scheme 对应)
schemeMap.value = educationPaperSchemeList.reduce((acc, item) => {
acc[item.spName] = item;
return acc;
}, {});
}
assignFields(res);
})
.catch((e) => {
message.error(`获取试卷详情失败: ${e.message}`);
})
.finally(() => {
loading.value = false;
nextTick(() => {
formRef.value?.clearValidate?.();
});
});
}
};
</script>

View File

@@ -0,0 +1,183 @@
<template>
<Dialog v-model="visible" :title="'查看试卷'" width="860" @open="handleOpen" center>
<div style="margin-bottom: 16px">
<strong>试卷ID</strong>
{{ paperId ? paperId : '未提供试卷ID' }}
</div>
<!-- 滚动区域 -->
<div style="max-height: 400px; overflow-y: auto;">
<!-- 分组展示题目 -->
<div
v-for="(items, subject) in sortedGroupedQuestions"
:key="subject"
>
<h4 class="mb-2 text-base text-gray-700 font-medium">
{{ subject }}
<span v-if="schemeMap[subject]" class="text-sm text-gray-500 font-normal">
每小题 {{ parseFloat(schemeMap[subject].quScores).toFixed(1) }} {{ parseFloat(schemeMap[subject].subtotalScore).toFixed(1) }}
</span>
</h4>
<el-card
v-for="(item, index) in items"
:key="item.quId"
class="mb-3"
shadow="never"
>
<div class="text-gray-500 mb-2">题目 {{ index + 1 }}</div>
<div v-html="item.content" class="mb-2"></div>
<!-- 🎯 判断是否为编程题 -->
<div v-if="item.subjectName === '编程题'">
<!-- 编程题解析 -->
<div class="mt-2">
<div class="font-bold text-blue-600">解析</div>
<pre
style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;"
>{{ item.analysis }}</pre>
</div>
<div
v-for="(answer, aIndex) in item.answerList"
:key="answer.answerId"
class="mb-1"
>
测试用例 {{ aIndex + 1 }}输入: {{ answer.contentIn }} | 输出: {{ answer.content }} | 占比: {{ answer.scoreRate }}%
</div>
</div>
<!-- 📝 普通题目的选项显示 -->
<div v-else-if="item.answerList && item.answerList.length">
<div
v-for="(answer, aIndex) in item.answerList.slice().sort((a, b) => a.sort - b.sort)"
:key="answer.answerId"
:style="{
color: answer.isRight === '0' ? 'green' : 'inherit',
fontWeight: answer.isRight === '0' ? 'bold' : 'normal'
}"
class="mb-1"
>
{{ String.fromCharCode(65 + aIndex) }}. {{ answer.content }}
</div>
</div>
</el-card>
</div>
</div>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
</template>
</Dialog>
</template>
<script setup>
import { ref, reactive, nextTick, computed } from 'vue';
import { useFormData } from '@/utils/use-form-data';
import { getPaperDetailByTaskId } from '@/api/system/paper';
const message = useMessage(); // 消息弹窗
const props = defineProps({
paperId: String // 接收父组件传来的 paperId
});
const emit = defineEmits(['done']);
const visible = defineModel({ type: Boolean });
const loading = ref(false);
const formRef = ref(null);
const questionList = ref([]); // 用于存放试题列表
const schemeMap = ref({});
const [form, resetFields, assignFields] = useFormData({
paperId: void 0,
taskId: '',
counts: '',
rollUp: '',
isAb: '',
status: ''
});
const rules = reactive({});
const handleCancel = () => {
visible.value = false;
};
const groupedQuestions = ref({});
const subjectPriority = {
'选择题': 1,
'多选题': 2,
'判断题': 3,
'编程题': 4,
'其他': 5
};
// 根据试题的类型进行分组
const groupBySubjectName = (list) => {
const group = {};
list.forEach((item) => {
const subject = item.subjectName || '其他'; // 默认为'其他'
if (!group[subject]) {
group[subject] = [];
}
group[subject].push(item);
});
groupedQuestions.value = group;
};
// 按题型排序
const sortedGroupedQuestions = computed(() => {
return Object.keys(groupedQuestions.value)
.sort((a, b) => (subjectPriority[a] || subjectPriority['其他']) - (subjectPriority[b] || subjectPriority['其他']))
.reduce((acc, subject) => {
acc[subject] = groupedQuestions.value[subject];
return acc;
}, {});
});
// 打开弹窗时的处理逻辑
const handleOpen = (type, paperId) => {
resetFields();
questionList.value = [];
console.log(paperId+"paperIdpaperId")
if (props.paperId) {
loading.value = true;
getPaperDetailByTaskId(props.paperId)
.then((res) => {
const { educationPaperSchemeList, examQuestionList } = res || {};
if (Array.isArray(examQuestionList)) {
questionList.value = examQuestionList;
groupBySubjectName(examQuestionList); // 分类
}
if (Array.isArray(educationPaperSchemeList)) {
// 构建以题型名为 key 的映射表spName 与 scheme 对应)
schemeMap.value = educationPaperSchemeList.reduce((acc, item) => {
acc[item.spName] = item;
return acc;
}, {});
}
assignFields(res);
})
.catch((e) => {
message.error(`获取试卷详情失败: ${e.message}`);
})
.finally(() => {
loading.value = false;
nextTick(() => {
formRef.value?.clearValidate?.();
});
});
}
};
</script>

View File

@@ -0,0 +1,72 @@
<!-- 搜索表单 -->
<template>
<ele-card :body-style="{ paddingBottom: '2px' }">
<el-form label-width="72px" @keyup.enter="search" @submit.prevent="">
<el-row :gutter="8">
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="试卷编号">
<el-input
clearable
v-model.trim="form.paperId"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="抽卷方式">
<el-select v-model="form.rollUp" placeholder="请选择抽卷方式">
<el-option :label="'固定'" :value="0" />
<el-option :label="'AB卷'" :value="1" />
<el-option :label="'随机'" :value="2" />
<el-option :label="'自选'" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="状态">
<dict-data
code="sys_common_status_other"
type="select"
v-model="form.status"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label-width="16px">
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</ele-card>
</template>
<script setup>
import { useFormData } from '@/utils/use-form-data';
const emit = defineEmits(['search']);
/** 表单数据 */
const [form, resetFields] = useFormData({
paperId: '',
counts: '',
rollUp: '',
isAb: '',
status: '',
});
/** 搜索 */
const search = () => {
emit('search', { ...form });
};
/** 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -0,0 +1,336 @@
<!-- 编辑弹窗 -->
<template>
<Dialog v-model="visible" :title="'试卷调整'" width="460" @open="handleOpen" center>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
@submit.prevent=""
>
<!-- 抽卷方式 -->
<el-form-item label="抽卷方式" prop="type">
<el-radio-group v-model="form.type">
<el-radio :label="0">固定</el-radio>
<el-radio :label="1">AB卷</el-radio>
<el-radio :label="2">随机</el-radio>
<el-radio :label="3">自选</el-radio>
</el-radio-group>
</el-form-item>
<!-- 固定卷显示一个下拉 -->
<el-form-item label="选择试卷" v-if="form.type === 0" prop="paper">
<el-select v-model="form.paper" placeholder="请选择试卷">
<el-option
v-for="item in PaperOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- AB卷显示两个下拉 -->
<el-form-item label="试卷A" v-if="form.type === 1" prop="paperA">
<el-select v-model="form.paperA" placeholder="请选择试卷A">
<el-option
v-for="item in PaperOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="试卷B" v-if="form.type === 1" prop="paperB">
<el-select v-model="form.paperB" placeholder="请选择试卷B">
<el-option
v-for="item in PaperOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="save">保存</el-button>
</template>
</Dialog>
</template>
<script setup>
import { ref, reactive, nextTick,onMounted,watch} from 'vue';
import { useFormData } from '@/utils/use-form-data';
import { updatePaper ,getPaperList} from '@/api/system/paper';
const message = useMessage() // 消息弹窗
const props = defineProps({
/** 修改回显的数据 */
data: Object,
taskId: String,
});
const emit = defineEmits(['done']);
/** 弹窗是否打开 */
const visible = defineModel({ type: Boolean });
/** 提交状态 */
const loading = ref(false);
/** 表单实例 */
const formRef = ref(null);
/** 试卷下拉*/
const PaperOptions = ref([]);
/** 模拟试卷列表(建议替换为真实接口) */
/** 表单数据 */
const [form, resetFields, assignFields] = useFormData({
num: '',
taskId: '',
type: 0, // 抽卷方式0固定 1AB卷 2随机
paper: '', // 固定使用
paperA: '', // AB卷使用
paperB: '' // AB卷使用
});
/** 表单验证规则 */
const rules = reactive({
type: [{ required: true, message: '请选择抽卷方式', trigger: 'change' }],
paper: [
{
required: true,
trigger: 'change',
validator: (rule, value, callback) => {
if (form.type === 0 && !value) {
callback(new Error('请选择试卷'));
} else {
callback();
}
}
}
],
paperA: [
{
required: true,
trigger: 'change',
validator: (rule, value, callback) => {
if (form.type === 1 && !value) {
callback(new Error('请选择试卷A'));
} else {
callback();
}
}
}
],
paperB: [
{
required: true,
trigger: 'change',
validator: (rule, value, callback) => {
if (form.type === 1 && !value) {
callback(new Error('请选择试卷B'));
} else {
callback();
}
}
}
]
});
/** 取消关闭弹窗 */
const handleCancel = () => {
visible.value = false;
};
/** 保存编辑(不关闭弹窗) */
const save = () => {
formRef.value?.validate?.((valid) => {
if (!valid) return;
loading.value = true;
const type = form.type;
let payload = {
rollUp: 0, // 默认值
};
if (type === 0) {
console.log("0放大请求")
// 固定卷
if (form.paper) {
payload = {
paperId: form.paper, // 修改为字符串,不是数组
rollUp: 0
};
send(payload)
.then(() => {
message.success('保存成功');
emit('done');
})
.catch((e) => {
message.error(e.message);
})
.finally(() => {
loading.value = false;
});
} else {
message.error('请选择试卷');
loading.value = false;
}
} else if (type === 1) {
console.log("1放大请求")
// AB卷分别发送 A卷 和 B卷
if (form.paperA === form.paperB) {
message.error('试卷A和试卷B不能相同');
loading.value = false;
return;
}
if (form.paperA && form.paperB) {
const payloadA = {
paperId: form.paperA, // 修改为字符串,不是数组
rollUp: 1,
isAb: 0
};
const payloadB = {
paperId: form.paperB, // 修改为字符串,不是数组
rollUp: 1,
isAb: 1
};
Promise.all([send(payloadA), send(payloadB)])
.then(() => {
message.success('保存成功');
emit('done');
})
.catch((e) => {
message.error(e.message);
})
.finally(() => {
loading.value = false;
});
} else {
message.error('请选择试卷A和试卷B');
loading.value = false;
}
} else if (type === 2) {
console.log("2放大请求")
// 随机卷
payload = {
rollUp: 2
};
send(payload)
.then(() => {
message.success('保存成功');
emit('done');
})
.catch((e) => {
message.error(e.message);
})
.finally(() => {
loading.value = false;
});
}else if(type === 3)// 自选卷
{
payload = {
rollUp: 3
};
send(payload)
.then(() => {
console.log("3放大请求")
message.success('保存成功');
emit('done');
})
.catch((e) => {
message.error(e.message);
})
.finally(() => {
loading.value = false;
});
}
});
};
const send = async (payload) => {
console.log("12312312"+payload)
try {
await updatePaper({
...payload,
taskId: props.taskId,
});
} catch (e) {
message.error('保存失败:' + e.message); // 提示错误
throw e; // 确保错误能被捕获
} finally {
loading.value = false; // 确保最终关闭loading
}
};
// 获取题型下拉数据
const fetchpaperOptions = async () => {
try {
console.log(props.taskId+"props.taskIdprops.taskIdprops.taskId")
const res = await getPaperList(props.taskId);
PaperOptions.value = res
.filter(item => item)
.map(item => ({ label: item, value: item }));
} catch (e) {
message.error('获取题型失败:' + e.message);
}
};
/** 弹窗打开时初始化 */
const handleOpen = () => {
resetFields();
form.taskId = props.taskId;
nextTick(() => {
formRef.value?.clearValidate?.();
});
};
onMounted(() => {
console.log("taskId:", props.taskId);
if (props.taskId) {
fetchpaperOptions();
} else {
console.error("taskId is not defined");
}
});
watch(() => form.type, (val) => {
if (val === 0) {
form.paperA = '';
form.paperB = '';
} else if (val === 1) {
form.paper = '';
} else {
form.paper = '';
form.paperA = '';
form.paperB = '';
}
});
</script>

View File

@@ -0,0 +1,211 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item>
<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
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:sms-channel:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增</el-button
>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="试卷编号" align="center" prop="paperId" />
<el-table-column label="使用次数" align="center" prop="counts" />
<el-table-column label="抽卷方式" align="center" prop="rollUp">
<template #default="scope">
<span v-if="scope.row.quLevel === '0'">固定</span>
<span v-else-if="scope.row.quLevel === '1'">AB卷</span>
<span v-else-if="scope.row.quLevel === '2'">随机</span>
<span v-else-if="scope.row.quLevel === '3'">自选</span>
<span v-else>未知</span>
</template>
</el-table-column>
<el-table-column label="AB卷" align="center" prop="isAb">
<template #default="scope">
<span v-if="scope.row.quLevel === '0'">A卷</span>
<span v-else-if="scope.row.quLevel === '1'">B卷</span>
<span v-else>未知</span>
</template>
</el-table-column>
<el-table-column label="是否启用" align="center" prop="status" >
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYS_YES_NO" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openEdit('update', scope.row)"
v-hasPermi="['system:sms-channel:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:sms-channel:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<paper-add v-model="showAdd" :task-Id="taskId" :task-specialty="taskSpecialty" />
<paper-edit v-model="showEdit" :data="current" />
<!-- <paper-look v-model="showLook" :paper-id="paperId" /> -->
<paper-set v-model="showSet" :task-Id="taskId" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as SmsChannelApi from '@/api/system/paper';
import PaperEdit from './components/step-edit.vue';
import PaperAdd from './components/step-add.vue';
import PaperLook from './components/step-look.vue';
import PaperSet from './components/step-set.vue';
import PaperSearch from './components/step-search.vue';
import { pagePapers, removePapers, exportPapers } from '@/api/system/paper';
defineOptions({ name: 'SystemPaper' });
const props = defineProps({
taskSpecialty: {
type: String,
default: ''
},
taskId: {
type: String,
default: ''
}
})
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
/** 当前编辑数据 */
const current = ref<object>();
/** 是否显示编辑弹窗 */
const showEdit = ref(false);
const showLook = ref(false);
const showAdd = ref(false);
const showSet = ref(false);
const smsChannelFormRef = ref()
const taskEditRef = ref()
const taskAddRef = ref()
const taskTempRef = ref()
const loading = ref(false) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
signature: undefined,
status: undefined,
createTime: []
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const res = await SmsChannelApi.pagePapers(queryParams)
console.log(res)
list.value = res
total.value = res.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref();
const openForm = (type: string, id?: number) => {
showAdd.value = true
taskAddRef.value?.open(type, id)
}
const openEdit = (type: string, row?: object) => {
showEdit.value = true
current.value = row
console.log( current.value )
taskEditRef.value?.open(type, row)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SmsChannelApi.removePaper(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,359 @@
<template>
<ContentWrap>
<!-- 搜索表单 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="试卷编号" prop="paperId">
<el-input
v-model="queryParams.paperId"
placeholder="请输入试卷编号"
clearable
class="!w-240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="启用状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择启用状态"
class="!w-240px"
clearable
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<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
type="primary"
plain
@click="openAddForm"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="primary"
plain
@click="openSet"
>
<Icon icon="ep:plus" class="mr-5px" /> 试卷调整
</el-button>
<!-- 批量删除按钮 -->
<!-- <el-button
type="danger"
:disabled="selectedRows.length === 0"
@click="batchDelete"
>
批量删除
</el-button> -->
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表展示 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :row-key="row => row.id" ref="tableRef" :selected-rows="selectedRows">
<!-- 多选列 -->
<el-table-column type="selection" width="55" />
<el-table-column label="试卷id" align="center" prop="paperId" />
<el-table-column label="试卷编号" align="center" prop="num" />
<el-table-column label="使用次数" align="center" prop="counts" />
<el-table-column label="抽卷方式" align="center" prop="rollUp">
<template #default="scope">
<span v-if="scope.row.rollUp === '0'">固定</span>
<span v-else-if="scope.row.rollUp === '1'">AB卷</span>
<span v-else-if="scope.row.rollUp === '2'">随机</span>
<span v-else-if="scope.row.rollUp === '3'">自选</span>
</template>
</el-table-column>
<el-table-column label="AB卷" align="center" prop="isAb">
<template #default="scope">
<span v-if="scope.row.isAb === '0'">A卷</span>
<span v-else-if="scope.row.isAb === '1'">B卷</span>
</template>
</el-table-column>
<el-table-column label="启用状态" align="center" prop="status" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="'0'"
:inactive-value="'1'"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openLook('look', scope.row.paperId)"
v-hasPermi="['system:sms-channel:update']"
>
查看
</el-button>
<el-button
link
type="primary"
@click="openEdit('update', scope.row)"
v-hasPermi="['system:sms-channel:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.paperId)"
v-hasPermi="['system:sms-channel:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 弹窗组件 -->
<paper-add
v-model="showAdd"
:task-Id="props.taskId"
:task-specialty="props.taskSpecialty"
ref="taskAddRef"
@done="reload"
/>
<paper-edit
v-model="showEdit"
:data="current"
ref="taskEditRef"
@done="reload"
/>
<paper-look
v-model="showLook"
ref="taskLookRef"
:paper-Id="currentPaperId"
/>
<paper-set
v-model="showSet"
:task-Id="taskId"
ref="taskSetRef"
@done="reload"
/>
</template>
<script lang="ts" setup>
import { ref, reactive, nextTick,onMounted,watch} from 'vue';
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as PaperApi from '@/api/system/paper';
import PaperEdit from './components/step-edit.vue';
import PaperAdd from './components/step-add.vue';
import PaperLook from './components/step-look.vue';
import PaperSet from './components/step-set.vue';
import { useI18n } from 'vue-i18n';
defineOptions({ name: 'SystemPaper' });
const { t } = useI18n();
const message = useMessage();
const props = defineProps({
taskSpecialty: {
type: String,
default: '',
},
taskId: {
type: String,
default: '',
},
});
// 定义数据项的类型
interface PaperItem {
paperId: number; // 假设每个项目都有 paperId
counts: number;
rollUp: string;
isAb: string;
status: string;
num:string;
// 你可以根据需要扩展更多字段
}
// 当前编辑的数据
const current = ref<object>();
// 弹窗显示状态
const showEdit = ref(false);
const showLook = ref(false);
const showAdd = ref(false);
const showSet = ref(false);
// ref 对应的组件实例
const taskAddRef = ref();
const taskEditRef = ref();
const taskLookRef = ref();
const taskSetRef = ref();
const loading = ref(false); // 列表的加载状态
const total = ref(0); // 列表的总页数
const list = ref<PaperItem[]>([]); // 列表的数据,明确指定类型
const queryFormRef = ref(); // 搜索表单的 ref
// 多选框绑定的变量,指定为 PaperItem 类型的数组
const selectedRows = ref<PaperItem[]>([]);
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
paperId: undefined,
status: undefined,
createTime: [],
taskId:props.taskId
});
// 获取列表数据
const getList = async () => {
loading.value = true;
try {
const res = await PaperApi.pagePapers(queryParams);
list.value = res.list;
total.value = res.total;
} finally {
loading.value = false;
}
};
const reload = () => {
getList();
};
// 搜索操作
const handleQuery = () => {
queryParams.pageNo = 1;
getList();
};
// 重置操作
const resetQuery = () => {
queryFormRef.value.resetFields();
handleQuery();
};
// 打开新增表单
const openAddForm = () => {
showAdd.value = true;
nextTick(() => {
taskAddRef.value?.open();
});
};
const openSet = () => {
showSet.value = true;
nextTick(() => {
taskSetRef.value?.open();
});
};
// 打开编辑弹窗
const openEdit = (type: string, row?: object) => {
showEdit.value = true;
current.value = row;
nextTick(() => {
taskEditRef.value?.open(type, row);
});
};
const currentPaperId = ref<number | null>(null);
const openLook = (type: string, paperId?: number) => {
showLook.value = true;
currentPaperId.value = paperId ?? null; // 设置当前选中的 paperId
nextTick(() => {
taskLookRef.value?.open(type, paperId);
});
};
// 删除操作
const handleDelete = async (id: number) => {
try {
await message.delConfirm();
await PaperApi.removePaper(id);
message.success(t('common.delSuccess'));
await getList();
} catch (error) {
console.error(error);
}
};
// 批量删除操作
const batchDelete = async () => {
try {
const ids = selectedRows.value.map(item => item.paperId); // 使用 paperId 属性
if (ids.length > 0) {
await message.delConfirm();
await PaperApi.removePapers(ids); // 调用批量删除接口
message.success(t('common.delSuccess'));
await getList();
selectedRows.value = []; // 清空选择的行
}
} catch (error) {
console.error(error);
}
};
const handleStatusChange = async (row: any) => {
console.log(row.paperId, row.status+"row.taskId, row.status");
try {
loading.value = true;
// 调用API更新状态
await PaperApi.updatePaperStatus(row.paperId, row.status);
message.success(row.status === '0' ? '已启用' : '已停用');
// 刷新列表
await getList();
} catch (error) {
// 如果请求失败,恢复原来的状态
row.status = row.status === '0' ? '1' : '0';
message.error('状态更新失败');
} finally {
loading.value = false;
}
};
// 初始化操作
onMounted(() => {
getList();
});
watch(selectedRows, (newVal) => {
console.log('选中的行:', newVal);
});
</script>

View File

@@ -0,0 +1,193 @@
<template>
<div>
<!-- 使用 el-tabs 组件创建可切换的选项卡 -->
<el-tabs v-model="activeTab" style="margin-top: 20px;">
<!-- 第一个 tab - 通用参数 -->
<el-tab-pane label="通用参数" name="tab1">
<el-form :model="form" label-width="200px" style="margin-top: 20px;">
<!-- 是否使用监考密码验证 -->
<el-form-item label="是否使用监考密码验证">
<el-switch
v-model="form.isExamPassword"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
<!-- 是否使用监考密码验证 -->
<el-form-item label="是否启用考场设置">
<el-switch
v-model="form.isSession"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
<!-- 监考密码 -->
<el-form-item label="监考密码" v-if="form.isExamPassword === '0'">
<el-input v-model="form.examPassword" placeholder="请输入监考密码" @input="handleFormChange" />
</el-form-item>
<!-- 禁止学生使用U盘 -->
<el-form-item label="禁止学生使用U盘">
<el-switch
v-model="form.usb"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
<!-- 练习成绩保存 -->
<el-form-item label="练习成绩保存">
<el-radio-group v-model="form.saveGrades" @change="handleFormChange">
<el-radio label="0">最高成绩</el-radio>
<el-radio label="1">最新成绩</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否显示重答按钮">
<el-switch
v-model="form.isRepeat"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
<el-form-item label="是否显示答案按钮">
<el-switch
v-model="form.isAnswer"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
<el-form-item label="学生是否可以查看试卷">
<el-switch
v-model="form.isLook"
active-value="0"
inactive-value="1"
active-text=""
inactive-text=""
@change="handleFormChange" />
</el-form-item>
<el-form-item label="定时检查与学生端联通性,每">
<el-input-number v-model="form.isConnect" label="分钟" @change="handleFormChange" />
<span>分钟传一次,断联直接交卷</span>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 第二个 tab - 试卷答题文件 -->
<el-tab-pane label="试卷答题文件" name="tab2">
<el-form :model="form" label-width="200px" style="margin-top: 20px;">
<!-- 学生文件存放系统盘 -->
<el-form-item label="驱动器为学生文件存放系统盘">
<el-radio-group v-model="form.driver" @change="handleFormChange">
<el-radio label="C">C</el-radio>
<el-radio label="D">D</el-radio>
<el-radio label="E">E</el-radio>
<el-radio label="F">F</el-radio>
</el-radio-group>
</el-form-item>
<!-- 考试目录名称 -->
<el-form-item label="考试目录名称">
<el-input v-model="form.directory" placeholder="请输入考试目录名称" @input="handleFormChange" />
</el-form-item>
<!-- 定时上传考试目录 -->
<el-form-item label="定时上传考试目录每">
<el-input-number v-model="form.uploadTime" :min="1" label="分钟" @change="handleFormChange" />
<span>分钟传一次</span>
</el-form-item>
<!-- 完成考试后是否删除考试目录 -->
<el-form-item label="完成考试后是否删除考试目录">
<el-radio-group v-model="form.isDel" @change="handleFormChange">
<el-radio label="0"></el-radio>
<el-radio label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getParam, updateParam } from '@/api/system/param' // 引入后台请求方法
const props = defineProps({
taskId: {
type: String,
default: ''
}
})
// 字段初始值设置为空或空字符串
const form = ref({
isExamPassword: '', // 是否使用监考密码验证
examPassword: '', // 监考密码
usb: '', // 是否禁止U盘
saveGrades: '', // 成绩保存方式
driver: '', // 存放系统盘
directory: '', // 考试目录名称
uploadTime: '', // 上传间隔时间
isDel: '',
isRepeat:'', // 是否删除目录
isAnswer:'',
isLook:'',
isConnect:''
})
const activeTab = ref('tab1')
// 页面加载时用 taskId 请求后端并赋值
onMounted(async () => {
if (props.taskId) {
try {
const res = await getParam({ taskId: props.taskId })
const data = res || {}
console.log('任务详情:', data)
// 用 Object.assign 将后端数据合并到 form
Object.assign(form.value, data)
} catch (error) {
console.error('获取任务详情失败', error)
}
}
})
// 表单变化时触发请求
const handleFormChange = async () => {
if (form.value.isExamPassword === '1') {
form.value.examPassword = ''
}
try {
// 发送请求以更新数据
const response = await updateParam(form.value)
console.log('更新结果:', response)
} catch (error) {
console.error('更新数据失败', error)
}
}
</script>
<style scoped>
/* 可以为 tab 设置额外的样式,具体根据需求定制 */
</style>

View File

@@ -0,0 +1,197 @@
<template>
<Dialog
v-model="visible"
:title="isUpdate ? '修改考场设置' : '添加考场设置'"
width="460"
center
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
@submit.prevent=""
>
<el-form-item label="试卷ID" prop="taskId">
<el-input clearable v-model="form.taskId" placeholder="请输入试卷ID" disabled/>
</el-form-item>
<el-form-item label="批次" prop="batch">
<el-input clearable v-model="form.batch" placeholder="请输入批次" disabled/>
</el-form-item>
<el-form-item label="考试开始时间" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择考试开始时间"
class="ele-fluid"
/>
</el-form-item>
<el-form-item label="考试结束时间" prop="endTime">
<el-date-picker
v-model="form.endTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择考试结束时间"
class="ele-fluid"
/>
</el-form-item>
<el-form-item label="开始前(分钟)允许入场" prop="allowEntry">
<el-input
clearable
v-model="form.allowEntry"
placeholder="请输入允许提前入场的分钟数"
/>
</el-form-item>
<el-form-item label="开始后(分钟)禁止入场" prop="endAllowEntry">
<el-input
clearable
v-model="form.endAllowEntry"
placeholder="请输入开始后禁止入场的分钟数"
/>
</el-form-item>
<el-form-item label="开始后(分钟)允许交卷" prop="allowSubmission">
<el-input
clearable
v-model="form.allowSubmission"
placeholder="请输入允许交卷的分钟数"
/>
</el-form-item>
<el-form-item label="是否启用" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.SYS_YES_NO)"
:key="dict.value"
:label="String(dict.value)"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
:loading="loading"
@click="save"
:disabled="false"
>
保存
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import { addSession, updateSession } from '@/api/system/session'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const emit = defineEmits(['done'])
const props = defineProps({
data: Object,
taskId: String,
})
const visible = defineModel({ type: Boolean })
const isUpdate = ref(false)
const loading = ref(false)
const formRef = ref()
// 表单模型
const form = ref({
sessionId: undefined,
taskId: props.taskId ?? '',
batch: '',
startTime: '',
endTime: '',
allowEntry: '',
endAllowEntry: '',
allowSubmission: '',
status: '', // 状态0禁用1启用
})
// 表单校验规则
const rules = reactive({
taskId: [{ required: true, message: '请输入试卷ID', trigger: 'blur' }],
batch: [{ required: true, message: '请输入批次', trigger: 'blur' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
allowEntry: [{ required: true, message: '请输入允许入场时间', trigger: 'blur' }],
endAllowEntry: [{ required: true, message: '请输入禁止入场时间', trigger: 'blur' }],
allowSubmission: [{ required: true, message: '请输入允许交卷时间', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
})
// 打开弹窗方法
const open = async (type: 'create' | 'update', data?: any) => {
console.log(type+"isUpdate.valueisUpdate.valueisUpdate.value")
visible.value = true
isUpdate.value = type === 'update'
if (isUpdate.value && data) {
form.value = { ...data }
} else {
form.value = {
sessionId: undefined,
taskId: props.taskId ?? '',
batch: '',
startTime: '',
endTime: '',
allowEntry: '',
endAllowEntry: '',
allowSubmission: '',
status: '',
}
// 设置批次为 当前时间 + 5位随机数
const now = new Date();
const pad = (n) => n.toString().padStart(2, '0');
const datetimeStr = [
now.getFullYear(),
pad(now.getMonth() + 1),
pad(now.getDate()),
pad(now.getHours()),
pad(now.getMinutes()),
pad(now.getSeconds())
].join('');
const randomNum = Math.floor(Math.random() * 100000).toString().padStart(5, '0');
//把batch 赋值 randomNum
// 把 batch 设置为当前时间+随机数
form.value.batch = `${datetimeStr}${randomNum}`;
}
await nextTick()
formRef.value?.clearValidate?.()
}
// 保存按钮
const save = async () => {
await formRef.value.validate()
loading.value = true
try {
const fn = isUpdate.value ? updateSession : addSession
await fn({ ...form.value })
emit('done')
visible.value = false
} finally {
loading.value = false
}
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,64 @@
<!-- 搜索表单 -->
<template>
<ele-card :body-style="{ paddingBottom: '2px' }">
<el-form label-width="72px" @keyup.enter="search" @submit.prevent="">
<el-row :gutter="8">
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="批次">
<el-input
clearable
v-model.trim="form.batch"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="状态">
<dict-data
code="sys_common_status_other"
type="select"
v-model="form.status"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label-width="16px">
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</ele-card>
</template>
<script setup>
import { useFormData } from '@/utils/use-form-data';
const emit = defineEmits(['search']);
/** 表单数据 */
const [form, resetFields] = useFormData({
startTime: '',
batch: '',
endTime: '',
allowEntry: '',
endAllowEntry: '',
allowSubmission: '',
status: '',
});
/** 搜索 */
const search = () => {
emit('search', { ...form });
};
/** 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -0,0 +1,104 @@
<template>
<Dialog v-model="visible" :title="'试卷调整'" width="860" @open="handleOpen" center>
<el-transfer v-model="value" :data="data" :titles="['未选试卷', '已选试卷']" />
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="save">保存</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { ref, nextTick } from 'vue';
import { useFormData } from '@/utils/use-form-data';
import { getPaperListByType } from '@/api/system/paper';
import { setSessions } from '@/api/system/session';
import type { FormInstance } from 'element-plus'
const message = useMessage();
const props = defineProps({
taskId: String,
rows: {
type: Array,
default: () => []
}
});
interface Option {
key: string;
label: string;
disabled: boolean;
}
const data = ref<Option[]>([]);
const value = ref<string[]>([]);
const emit = defineEmits(['done']);
const visible = defineModel({ type: Boolean });
const loading = ref(false);
// ✅ 给 formRef 添加类型注解
const formRef = ref<FormInstance | null>(null);
const [form, resetFields] = useFormData({
taskId: '',
});
const handleOpen = async () => {
resetFields();
form.taskId = props.taskId;
value.value = [];
try {
const res = await getPaperListByType({
taskId: props.taskId,
rows: props.rows
});
data.value = Object.entries(res?.paperId || {}).map(([id, name]) => ({
key: id,
label: String(name), // 👈 关键在这里
disabled: false
}));
value.value = res?.selectedIds || [];
} catch (e) {
message.error('获取试卷列表失败');
}
nextTick(() => {
// ✅ 类型安全地调用 clearValidate
formRef.value?.clearValidate?.();
});
};
const handleCancel = () => {
visible.value = false;
};
const save = async () => {
if (!value.value.length) {
message.warning('请至少选择一个试卷');
return;
}
try {
const params = {
selectedIds: value.value,
rows: props.rows
};
await setSessions(params);
message.success('保存成功');
emit('done');
visible.value = false;
} catch (error) {
message.error('保存失败');
}
};
</script>

View File

@@ -0,0 +1,251 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item>
<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
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:sms-channel:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增</el-button
>
<!-- <el-button
type="warning"
class="ele-btn-icon"
:disabled="!selections.length"
@click="papreSet()"
>
批量分配
</el-button> -->
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="考试批次" align="center" prop="batch" />
<el-table-column label="考试开始时间" align="center" prop="startTime" />
<el-table-column label="考试结束时间" align="center" prop="endTime" />
<el-table-column label="开始考试前分钟,允许入场" align="center" prop="allowEntry" />
<el-table-column label="开始考试后分钟,禁止入场" align="center" prop="endAllowEntry" />
<el-table-column label="开始考试后分钟,允许交卷" align="center" prop="allowSubmission" />
<el-table-column label="启用状态" align="center" prop="status" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="'0'"
:inactive-value="'1'"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openEdit('update', scope.row)"
v-hasPermi="['system:sms-channel:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.sessionId)"
v-hasPermi="['system:sms-channel:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<session-edit v-model="showEdit" :data="current" :task-Id="taskId" @done="reload" ref="taskEditRef" />
<paper-set v-model="showSet" :task-Id="taskId" :rows="selectedRows" @done="reload"/>
</template>
<script lang="ts" setup>
import * as SmsChannelApi from '@/api/system/session';
import SessionSearch from './components/step-search.vue';
import SessionEdit from './components/step-edit.vue';
import PaperSet from './components/step-set.vue';
import { DICT_TYPE } from '@/utils/dict';
defineOptions({ name: 'SystemPaper' });
const props = defineProps({
taskSpecialty: {
type: String,
default: ''
},
taskId: {
type: String,
default: ''
}
})
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const sessionAddRef = ref();
/** 当前编辑数据 */
const current = ref<object>();
/** 是否显示编辑弹窗 */
const showEdit = ref(false);
const showLook = ref(false);
const showAdd = ref(false);
const showSet = ref(false);
const taskEditRef = ref()
const loading = ref(false) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
signature: undefined,
status: undefined,
createTime: [],
taskId: props.taskId
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const res = await SmsChannelApi.pageSessions(queryParams)
console.log(res)
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 表格选中数据 */
const selections = ref([]);
const selectedRows = ref<string[]>([]);
const papreSet = () => {
const rows = selections.value;
if (!rows.length) {
message.error('请至少选择一条数据');
return;
}
selectedRows.value = rows.map((d: any) => d.sessionId); // 保存选中的行数据
showSet.value = true; // 打开弹窗
};
const handleSelectionChange = (rows) => {
selections.value = rows;
}
/** 添加/修改操作 */
const formRef = ref();
const openForm = (type: string, ) => {
showEdit.value = true
console.log( current.value )
taskEditRef.value?.open(type)
}
const openEdit = (type: string, row?: object) => {
showEdit.value = true
current.value = row
console.log( current.value )
taskEditRef.value?.open(type, row)
}
const handleStatusChange = async (row: any) => {
try {
loading.value = true;
// 调用API更新状态
await SmsChannelApi.updateSessionStatus(row.sessionId, row.status);
message.success(row.status === '0' ? '已启用' : '已停用');
// 刷新列表
await getList();
} catch (error) {
// 如果请求失败,恢复原来的状态
row.status = row.status === '0' ? '1' : '0';
message.error('状态更新失败');
} finally {
loading.value = false;
}
};
const reload = (where) => {
getList()
};
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SmsChannelApi.removeSession(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,232 @@
<template>
<Dialog
title="人员分配"
v-model:visible="visible"
width="80%"
append-to-body
lock-scroll
:destroy-on-close="false"
:draggable="false"
@close="handleDialogClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<ele-page style="height: 65vh;">
<el-row :gutter="16" style="height: 100%;">
<!-- 左侧任务列表 -->
<el-col :span="10" style="height: 100%;">
<div style="height: 100%; overflow: auto;">
<ele-card title="试卷任务列表" :body-style="{ paddingTop: '8px' }">
<ContentWrap style="overflow-x: auto;">
<el-table
table-layout="fixed"
ref="taskTableRef"
:data="mockTaskList"
highlight-current-row
@row-click="handleTaskRowClick"
style="min-width: 1000px; width: 100%;"
>
<el-table-column type="index" label="#" width="50" align="center" />
<el-table-column label="考试批次" align="center" prop="batch" />
<el-table-column label="考试开始时间" align="center" prop="startTime" />
<el-table-column label="考试结束时间" align="center" prop="endTime" />
<el-table-column label="允许入场(分钟)" align="center" prop="allowEntry" />
<el-table-column label="禁止入场(分钟)" align="center" prop="endAllowEntry" />
<el-table-column label="允许交卷(分钟)" align="center" prop="allowSubmission" />
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</ele-card>
</div>
</el-col>
<!-- 右侧人员分配列表 -->
<el-col :span="14" style="height: 100%;">
<div style="height: 100%; overflow: auto;">
<ele-card :body-style="{ paddingTop: '8px' }">
<div class="mb-2">
<el-button type="primary" @click="openAssignDialog()">分配人员</el-button>
<el-button type="danger" @click="removeBatch()">删除</el-button>
<el-button @click="exportData()">导出</el-button>
</div>
<ContentWrap style="overflow-x: auto;">
<el-table
table-layout="fixed"
ref="personTableRef"
:data="filteredPersonList"
style="min-width: 1000px; width: 100%"
highlight-current-row
@selection-change="selections = $event"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column type="index" label="#" width="50" align="center" />
<el-table-column label="用户名称" align="center" prop="username" />
<el-table-column label="用户昵称" align="center" prop="nickname" />
<el-table-column label="班级名称" align="center" prop="className" />
<el-table-column label="分配考场" align="center" prop="batch" />
</el-table>
<Pagination
:total="stutotal"
v-model:page="stuQuery.pageNo"
v-model:limit="stuQuery.pageSize"
@pagination="getPersonList"
/>
</ContentWrap>
<el-empty
v-if="filteredPersonList.length === 0"
description="暂无分配人员"
/>
</ele-card>
</div>
</el-col>
</el-row>
</ele-page>
<PersonSearch
:session-id="selectedSessionId"
:session-batch="selectedSessionBanch"
:task-id="props.taskId"
v-model="showPersonEdit"
ref="stuAddRef"
@done="reload"
/>
</el-form>
</Dialog>
</template>
<script setup>
import { ref, onMounted, nextTick, reactive } from 'vue';
import PersonSearch from './person-serach.vue';
import * as SmsChannelApi from '@/api/system/session';
import * as PersonApi from '@/api/system/person';
const visible = ref(true);
const selectedSessionId = ref(null);
const selectedSessionBanch = ref(null);
const selections = ref([]);
const showEdit = ref(false);
const current = ref(null);
const taskTableRef = ref();
const personTableRef = ref();
const stuAddRef = ref();
const showPersonEdit = ref(false);
const emit = defineEmits(['done']);
const props = defineProps({
taskId: {
type: String,
default: ''
}
});
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
status: '0',
taskId: props.taskId,
sessionId: ''
});
const stuQuery = reactive({
pageNo: 1,
pageSize: 10
});
const mockTaskList = ref([]);
const total = ref(0);
const filteredPersonList = ref([]);
const stutotal = ref(0);
const openAssignDialog = () => {
if (!selectedSessionId.value) {
ElMessage.warning('请先选择左侧任务!');
return;
}
showPersonEdit.value = true;
nextTick(() => {
stuAddRef.value?.open();
});
};
const handleTaskRowClick = async (row) => {
selectedSessionId.value = row.sessionId;
selectedSessionBanch.value = row.batch;
queryParams.sessionId = row.sessionId;
stuQuery.pageNo = 1; // 重置分页
await getPersonList();
};
const getList = async () => {
const res = await SmsChannelApi.pageSessions(queryParams);
mockTaskList.value = res.list;
total.value = res.total;
};
const getPersonList = async () => {
const personParams = {
...stuQuery,
taskId: props.taskId,
sessionId: selectedSessionId.value
};
const res = await SmsChannelApi.getSessionStu(personParams);
filteredPersonList.value = res.list;
stutotal.value = res.total;
};
const removeBatch = async () => {
const selectedIds = selections.value.map(item => item.id);
if (selectedIds.length === 0) {
ElMessage.warning('请选择要删除的人员');
return;
}
await ElMessageBox.confirm(
'确定要删除选中的人员吗?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
const data = {
studentIds: selectedIds,
sessionId: selectedSessionId.value,
taskId: props.taskId,
batch: selectedSessionBanch.value
};
await PersonApi.removeSessionStu(data);
await getPersonList(); // 删除后刷新右侧列表
emit('done');
ElMessage.success('删除成功');
};
const exportData = () => {
console.log('导出功能触发');
};
const handleDialogClose = () => {
visible.value = false;
};
const reload = () => {
getList();
if (selectedSessionId.value) {
getPersonList();
}
emit('done');
};
onMounted(() => {
getList();
});
</script>

View File

@@ -0,0 +1,331 @@
<template>
<!-- 搜索 -->
<Dialog
title="人员分配"
v-model="visible"
width="80%"
append-to-body
lock-scroll
:destroy-on-close="false"
:draggable="false"
@close="handleDialogClose"
>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="学生名称" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入学生名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="班级" prop="className">
<el-select
v-model="queryParams.className"
filterable
allow-create
default-first-option
placeholder="请选择或输入"
style="width: 300px"
>
<el-option
v-for="item in classNameList"
:key="item.className"
:label="item.className"
:value="item.className"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择学生状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
<!-- <el-button
type="primary"
plain
@click="openForm('create')"
>
<Icon icon="ep:plus" /> 新增
</el-button> -->
<!-- <el-button
type="warning"
plain
@click="handleImport"
>
<Icon icon="ep:upload" /> 导入
</el-button> -->
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
>
<Icon icon="ep:download" />导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" ref="tableRef" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="用户编号" align="center" key="id" prop="id" />
<el-table-column
label="用户名称"
align="center"
prop="username"
:show-overflow-tooltip="true"
/>
<el-table-column
label="用户昵称"
align="center"
prop="nickname"
:show-overflow-tooltip="true"
/>
<el-table-column
label="班级名称"
align="center"
prop="className"
:show-overflow-tooltip="true"
/>
<el-table-column label="状态" key="status" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
disabled
/>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="open"
/>
</ContentWrap>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleDialogClose">取消</el-button>
<el-button type="primary" @click="confirmSelection">确认</el-button>
</span>
</template>
</Dialog>
<!-- 用户导入对话框 -->
<!-- <StudentImportForm ref="importFormRef" @success="open" /> -->
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as UserApi from '@/api/system/user'
import * as ClassApi from '@/api/exam/class'
import * as PersonApi from '@/api/system/person'
defineOptions({ name: 'SystemUser' })
const props = defineProps({
sessionId: {
type: String,
default: ''
},
taskId: {
type: String,
default: ''
},
sessionBatch:{
type: String,
default: ''
}
})
const tableRef = ref()
const visible = defineModel({ type: Boolean });
const classNameList = ref();
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
className: undefined,
nickname: undefined,
username: undefined,
mobile: undefined,
status: undefined,
deptId: undefined,
userType: "2",
createTime: [],
taskId:'',
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const open = async () => {
loading.value = true
classNameList.value = await ClassApi.ClassApi.getClassName()
queryParams.taskId=props.taskId
console.log(props.taskId+"props.taskId")
console.log(props.sessionId+"props.sessionId")
try {
const params: any = {
...queryParams,
sessionId:props.sessionId,
startTime: queryParams.createTime?.[0] || null,
endTime: queryParams.createTime?.[1] || null,
}
const data = await PersonApi.getSessionStuBySearch(params)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const emit = defineEmits(['done'])
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
open()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 用户导入 */
const importFormRef = ref()
const handleImport = () => {
importFormRef.value.open()
}
const multipleSelection = ref<UserApi.UserVO[]>([])
const handleSelectionChange = (val: UserApi.UserVO[]) => {
multipleSelection.value = val
}
/** 导出按钮操作 */
const exportLoading = ref(false)
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await UserApi.exportUser(queryParams)
download.excel(data, '用户数据.xls')
} catch {
} finally {
exportLoading.value = false
}
}
const handleDialogClose = () => {
visible.value = false;
multipleSelection.value = [] // 清空选中的数据
tableRef.value?.clearSelection()
};
const confirmSelection = async () => {
const selectedIds = multipleSelection.value.map(item => item.id)
const count = selectedIds.length
if (count === 0) {
ElMessage.warning('请先选择学生')
return
}
try {
await ElMessageBox.confirm(
`已选中 ${count} 个学生,是否确认分配?`,
'确认分配',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
const data = {
studentIds: selectedIds,
sessionId: props.sessionId,
taskId: props.taskId,
batch:props.sessionBatch
}
await PersonApi.setSessionStu(data)
ElMessage.success(`成功分配 ${count} 个学生`)
emit('done')
visible.value = false
multipleSelection.value = []
tableRef.value?.clearSelection()
} catch (err) {
// 用户取消时会进入这里,不需要处理
console.log('用户取消了分配')
}
}
defineExpose({ open })
/** 初始化 */
onMounted(() => {
open()
})
</script>

View File

@@ -0,0 +1,250 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item>
<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
type="primary"
plain
@click="openSearch('create')"
v-hasPermi="['system:sms-channel:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 添加学生</el-button
>
<el-button
v-if="showAssignButton"
type="primary"
plain
@click="openEdit('create')"
v-hasPermi="['system:sms-channel:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 分配场次</el-button
>
<el-button
type="danger"
class="ele-btn-del"
:disabled="!selections.length"
@click="handleDeletes()"
>
批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="用户名称" align="center" prop="username" />
<el-table-column label="用户昵称" align="center" prop="nickname" />
<el-table-column label="班级名称" align="center" prop="className"/>
<el-table-column label="分配考场" align="center" prop="batch"/>
<!-- <el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openEdit('update', scope.row)"
v-hasPermi="['system:sms-channel:update']"
>
查看
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.schemeId)"
v-hasPermi="['system:sms-channel:delete']"
>
删除
</el-button>
</template>
</el-table-column> -->
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 编辑弹窗 -->
<person-edit v-model="showEdit" :task-Id="props.taskId" @done="reload" />
<PersonSearch :task-id="props.taskId" v-model="showPersonEdit" ref="stuAddRef" @done="reload" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as SmsPersonlApi from '@/api/system/person';
import PersonEdit from './components/person-edit.vue';
import PersonSearch from './components/person-serach.vue'
import * as SmsChannelApi from '@/api/system/session';
import { fa } from 'element-plus/es/locale';
defineOptions({ name: 'SystemSmsChannel' })
const showAssignButton = ref(true) // 控制按钮显示
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const props = defineProps({
taskSpecialty: {
type: String,
default: ''
},
taskId: {
type: String,
default: ''
},
formData:{
type: Object,
default: null
}
})
const showPersonEdit = ref(false)
/** 当前编辑数据 */
const current = ref<object>();
/** 是否显示编辑弹窗 */
const showEdit = ref(false);
const stuAddRef = ref();
const taskEditRef = ref()
const loading = ref(false) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
signature: undefined,
status: undefined,
createTime: [],
taskId: props.taskId
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const resButton = await SmsChannelApi.fetchNoMes(props.taskId)
if (resButton==='200') {
showAssignButton.value = true
} else {
showAssignButton.value = false
}
const res = await SmsPersonlApi.pagePersons(queryParams)
console.log(res)
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
const reload = () => {
getList()
}
/** 表格选中数据 */
const selections = ref([]);
const handleSelectionChange = (rows) => {
selections.value = rows;
}
const selectedRows = ref<string[]>([]);
const handleDeletes = async () => {
try {
const rows = selections.value;
if (!rows.length) {
message.error('请至少选择一条数据');
return;
}
selectedRows.value = rows.map((d: any) => d.id); // 保存选中的行数据
const deleteData = {
studentIds: selectedRows.value, // 选中的 ID 列表
taskId: props.taskId // 任务 ID
}
await SmsPersonlApi.removePersons(deleteData)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
const openEdit = (type: string, row?: object) => {
showEdit.value = true
current.value = row
nextTick(() => {
taskEditRef.value?.open(type, row)
})
}
const openSearch = (type: string, row?: object) => {
showPersonEdit.value = true
current.value = row
nextTick(() => {
stuAddRef.value?.open(type, row)
})
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
console.log(id+"idididid")
await SmsPersonlApi.removePerson(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,216 @@
<!-- 编辑弹窗 -->
<template>
<Dialog v-model="visible" :title="'添加试卷任务'" width="460" @open="handleOpen" center>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
@submit.prevent=""
>
<el-form-item label="任务名称" prop="taskName">
<el-input
clearable
v-model="form.taskName"
placeholder="请输入任务名称"
/>
</el-form-item>
<el-form-item label="专业" prop="taskSpecialty">
<el-select
v-model="form.taskSpecialty"
placeholder="请选择专业"
clearable
>
<el-option
v-for="item in specialtyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- <el-form-item label="试卷任务模式" prop="taskType">
<el-radio-group v-model="form.taskType">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.TASK_TYPE)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="是否为模板" prop="isTemplate">
<el-radio-group v-model="form.isTemplate">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.SYS_YES_NO)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否启用" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="save">
保存
</el-button>
</template>
</Dialog>
</template>
<script setup>
import { ref, reactive, nextTick } from 'vue';
import { onMounted } from 'vue';
import { useFormData } from '@/utils/use-form-data';
import { addTask, updateTask,getSpecialtyList } from '@/api/system/task';
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const props = defineProps({
/** 修改回显的数据 */
data: Object
});
// 专业选项列表
const specialtyOptions = ref([]);
const emit = defineEmits(['done']);
const message = useMessage() // 消息弹窗
/** 弹窗是否打开 */
const visible = defineModel({ type: Boolean });
/** 是否是修改 */
const isUpdate = ref(false);
/** 提交状态 */
const loading = ref(false);
/** 表单实例 */
const formRef = ref(null);
/** 表单数据 */
const [form, resetFields, assignFields] = useFormData({
taskId: void 0,
taskName: '',
taskSpecialty: '',
taskType: '5',
isTemplate: '',
status: '',
updateTime: '',
updateBy: '',
deptId: '',
userId: ''
});
/** 表单验证规则 */
const rules = reactive({
taskName: [
{ required: true, message: '任务名称不能为空', trigger: 'blur' }
],
// taskSpecialty: [
// { required: true, message: '请选择专业', trigger: 'change' }
// ],
isTemplate: [
{ required: true, message: '请选择是否为模板', trigger: 'change' }
],
status: [
{ required: true, message: '请选择是否启用', trigger: 'change' }
]
});
/** 关闭弹窗 */
const handleCancel = () => {
visible.value = false;
};
// 获取专业下拉列表
const fetchSpecialtyOptions = async () => {
try {
const data = await getSpecialtyList();
// 假设返回格式为 [{ label: '计算机', value: 'computer' }, ...]
// 过滤空字符串并转成下拉格式
console.log(data)
specialtyOptions.value = data
.filter(item => item) // 过滤空字符串
.map(item => ({
label: item,
value: item
}));
} catch (e) {
message.error('获取专业列表失败:' + e.message);
}
};
/** 保存编辑 */
const save = () => {
formRef.value?.validate?.((valid) => {
if (!valid) {
return;
}
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateTask : addTask;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
handleCancel();
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
});
};
/** 弹窗打开事件 */
const handleOpen = () => {
if (props.data) {
assignFields(props.data);
isUpdate.value = true;
} else {
resetFields();
isUpdate.value = false;
}
nextTick(() => {
nextTick(() => {
formRef.value?.clearValidate?.();
});
});
};
onMounted(() => {
fetchSpecialtyOptions();
});
</script>

View File

@@ -0,0 +1,119 @@
<template>
<Dialog v-model="isVisible" :title="'修改试卷任务'" width="1460" >
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
>
<div>
<!-- 步骤条 -->
<el-steps :active="activeStep" finish-status="success" align-center>
<template v-for="(title, index) in stepTitles" :key="index">
<el-step>
<template #title>
<div @click="handleStepClick(index)" style="cursor: pointer;">
{{ title }}
</div>
</template>
</el-step>
</template>
</el-steps>
<div style="margin-top: 30px;">
<component
:is="currentComponent"
:form-data="props.data"
:task-specialty="props.data.taskSpecialty"
:task-id="props.data.taskId"
/>
</div>
</div>
</el-form>
<template #footer>
<el-button @click="handleCancel"> </el-button>
</template>
</Dialog>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { useFormData } from '@/utils/use-form-data'
import StepOne from './steps/step1/index.vue'
import StepTwo from './steps/step2/index.vue'
import StepThree from './steps/step3/index.vue'
import StepFour from './steps/step4/index.vue'
import StepFive from './steps/step5/index.vue'
import * as SmsChannelApi from '@/api/system/session';
// 模拟接口(你需要改成实际请求)
async function validateBeforeStepFour(taskId) {
// 示例:替换为你实际的 API 请求
const res = await SmsChannelApi.fetch(taskId)
console.log(res+"resres")
if (res==='200') {
return true
} else {
ElMessage.error(res || '无法进入考场设置步骤')
return false
}
}
const props = defineProps({
data: Object
})
const isVisible = defineModel({ type: Boolean })
const isUpdate = ref(false)
const activeStep = ref(0)
const stepTitles = ['试卷方案', '参数设置', '考场设置', '人员设置']
const stepComponents = [StepOne, StepThree, StepFour, StepFive]
const currentComponent = computed(() => stepComponents[activeStep.value])
const formRef = ref(null)
const [form, resetFields, assignFields] = useFormData({
taskId: '',
taskSpecialty: ''
})
async function handleStepClick(index) {
// 点击第四步index=3时先校验
if (index === 2) {
const pass = await validateBeforeStepFour(props.data.taskId)
if (!pass) return
}
activeStep.value = index
}
/** 打开弹窗 */
const open = async (type, row) => {
isVisible.value = true
assignFields(row)
isUpdate.value = true
activeStep.value = 0
nextTick(() => {
formRef.value?.clearValidate?.()
})
}
defineExpose({ open })
const handleCancel = () => {
isVisible.value = false
}
</script>
<style scoped>
/* 可选:让步骤标题更像按钮 */
.el-step__title:hover {
color: #409eff;
}
</style>

View File

@@ -0,0 +1,88 @@
<!-- 搜索表单 -->
<template>
<ele-card :body-style="{ paddingBottom: '2px' }">
<el-form label-width="72px" @keyup.enter="search" @submit.prevent="">
<el-row :gutter="8">
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="试卷名称">
<el-input
clearable
v-model.trim="form.taskName"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="试卷专业">
<el-input
clearable
v-model.trim="form.taskSpecialty"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="试卷任务模式">
<el-input
clearable
v-model.trim="form.taskType"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="模板">
<el-input
clearable
v-model.trim="form.isTemplate"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label="是否启用">
<el-input
clearable
v-model.trim="form.status"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xs="24">
<el-form-item label-width="16px">
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</ele-card>
</template>
<script setup>
import { useFormData } from '@/utils/use-form-data';
const emit = defineEmits(['search']);
/** 表单数据 */
const [form, resetFields] = useFormData({
taskName: '',
taskSpecialty: '',
taskType: '',
isTemplate: '',
status: '',
deptId: '',
userId: ''
});
/** 搜索 */
const search = () => {
emit('search', { ...form });
};
/** 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -0,0 +1,151 @@
<template>
<!-- 模板库对话框 -->
<Dialog v-model="dialogVisible" title="模板库" width="80%" :destroy-on-close="true">
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="任务名称" align="center" prop="taskName" />
<el-table-column label="专业" align="center" prop="taskSpecialty" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openEdit('update', scope.row)"
v-hasPermi="['system:sms-channel:update']"
>
使用
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</Dialog>
<!-- 选择参数的对话框 -->
<Dialog v-model="showDialog" title="选择所需参数" width="50%" :destroy-on-close="true">
<el-checkbox-group v-model="selectedOptions">
<el-checkbox label="1">试卷方案</el-checkbox>
<el-checkbox label="2">试卷管理</el-checkbox>
<el-checkbox label="3">参数设置</el-checkbox>
<el-checkbox label="4">考场设置</el-checkbox>
<!-- <el-checkbox label="5">人员设置</el-checkbox> -->
</el-checkbox-group>
<!-- 全选取消全选按钮 -->
<el-button @click="selectAll">全选</el-button>
<el-button @click="deselectAll">取消全选</el-button>
<!-- 底部按钮 -->
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="confirmSelection">确认</el-button>
</template>
</Dialog>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from 'vue';
import { pageTasks, submitSelection } from '@/api/system/task';
const message = useMessage() // 消息弹窗
const props = defineProps({
data: Object
});
const emit = defineEmits(['update:modelValue', 'done']);
// 处理模板库对话框的显隐
const dialogVisible = defineModel({ type: Boolean });// 控制模板库对话框
const showDialog = ref(false); // 控制选择参数对话框
const selectedOptions = ref([]); // 存储用户选中的复选框内容
const selectedTaskId = ref(null); // 用来存储选中行的 taskId
// 加载中状态
const loading = ref(false);
// 表格数据和分页参数
const list = ref([]); // 表格数据
const total = ref(0); // 总记录数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
status: '0',
isTemplate: '0',
taskType:'2'
});
// 获取数据
const getList = async () => {
loading.value = true;
try {
const res = await pageTasks(queryParams);
list.value = res.list; // 假设 res.data 是你需要的数据
total.value = res.total; // 假设 res.total 是总记录数
} finally {
loading.value = false;
}
};
// 监听父组件传递的 modelValue
watch(
() => props.modelValue,
(val) => {
dialogVisible.value = val;
}
);
watch(dialogVisible, (val) => {
emit('update:modelValue', val); // 更新父组件的 modelValue
});
// 打开编辑表单,弹出选择参数对话框
const openEdit = (action, row) => {
selectedTaskId.value = row.taskId; // 记录选中的 taskId
showDialog.value = true; // 弹出选择参数对话框
};
// 全选按钮的处理方法
const selectAll = () => {
selectedOptions.value = ['1', '2', '3', '4']; // 选择所有复选框
};
// 取消全选按钮的处理方法
const deselectAll = () => {
selectedOptions.value = []; // 清空选中的复选框
};
// 确认选择的操作
const confirmSelection = () => {
// 发送请求,包含 taskId 和选中的选项
const requestData = {
taskId: selectedTaskId.value, // 选中的 taskId
options: selectedOptions.value // 选中的复选框内容
};
submitSelection(requestData)
.then((msg) => {
showDialog.value = false; // 关闭选择参数对话框
message.success(msg); // 提示成功
emit('done'); // 通知父组件
})
.catch((e) => {
showDialog.value = false; // 关闭选择参数对话框
message.error(e.message); // 提示错误
});
};
// 关闭选择参数对话框
const handleCancel = () => {
showDialog.value = false;
};
// 初始化获取数据
onMounted(() => {
getList();
});
</script>

View File

@@ -0,0 +1,298 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="任务名称" prop="taskName">
<el-input
v-model="queryParams.taskName"
placeholder="请输入任务名称"
clearable
class="!w-240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="启用状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择启用状态"
class="!w-240px"
clearable
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<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="openBank"><Icon icon="ep:download" class="mr-5px" /> 模板库</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:sms-channel:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增</el-button
>
<el-button
type="danger"
class="ele-btn-del"
:disabled="!selections.length"
@click="handleDeletes()"
>
批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="任务名称" align="center" prop="taskName" />
<el-table-column label="专业" align="center" prop="taskSpecialty" />
<el-table-column label="试卷任务模式" align="center" prop="taskType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.TASK_TYPE" :value="scope.row.taskType" />
</template>
</el-table-column>
<el-table-column label="是否为模板" align="center" prop="isTemplate" >
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYS_YES_NO" :value="scope.row.isTemplate" />
</template>
</el-table-column>
<el-table-column label="启用状态" align="center" prop="status" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="'0'"
:inactive-value="'1'"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openEdit('update', scope.row)"
v-hasPermi="['system:sms-channel:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.taskId)"
v-hasPermi="['system:sms-channel:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<SmsChannelForm ref="smsChannelFormRef" @success="getList" />
<task-edit v-if="showEdit" v-model="showEdit" ref="taskEditRef" :data="current" @done="reload"/>
<task-add v-if="showAdd" v-model="showAdd" ref="taskAddRef" :data="current" @done="reload"/>
<task-temp v-if="showOpenTemplate" v-model="showOpenTemplate" ref="taskTempRef" :data="current" @done="reload"/>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as SmsChannelApi from '@/api/system/task/index';
import TaskEdit from './components/task-edit.vue';
import TaskTemp from './components/task-temp.vue';
import TaskAdd from './components/task-add.vue';
defineOptions({ name: 'SystemSmsChannel' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
/** 当前编辑数据 */
const current = ref<object>();
/** 是否显示编辑弹窗 */
const showEdit = ref(false);
const showOpenTemplate = ref(false);
const showAdd = ref(false);
const smsChannelFormRef = ref()
const taskEditRef = ref()
const taskAddRef = ref()
const taskTempRef = ref()
const loading = ref(false) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
signature: undefined,
status: undefined,
taskName:undefined,
createTime: [],
taskType:'5'
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const res = await SmsChannelApi.pageTasks(queryParams)
console.log(res)
list.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 表格选中数据 */
const selections = ref([]);
const handleSelectionChange = (rows) => {
selections.value = rows;
}
/** 添加/修改操作 */
const formRef = ref();
const openForm = (type: string, id?: number) => {
showAdd.value = true
taskAddRef.value?.open(type, id)
}
/** 状态切换处理 */
const handleStatusChange = async (row: any) => {
console.log(row.taskId, row.status+"row.taskId, row.status");
try {
loading.value = true;
// 调用API更新状态
await SmsChannelApi.updateTaskStatus(row.taskId, row.status);
message.success(row.status === '0' ? '已启用' : '已停用');
// 刷新列表
await getList();
} catch (error) {
// 如果请求失败,恢复原来的状态
row.status = row.status === '0' ? '1' : '0';
message.error('状态更新失败');
} finally {
loading.value = false;
}
};
const openEdit = (type: string, row?: object) => {
showEdit.value = true
current.value = row
console.log( current.value )
taskEditRef.value?.open(type, row)
}
const openBank = () => {
showOpenTemplate.value = true
taskTempRef.value?.open()
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SmsChannelApi.removeTasks(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
const selectedRows = ref<string[]>([]);
const handleDeletes = async () => {
try {
const rows = selections.value;
if (!rows.length) {
message.error('请至少选择一条数据');
return;
}
selectedRows.value = rows.map((d: any) => d.taskId); // 保存选中的行数据
await SmsChannelApi.removeTasks(selectedRows.value)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
const reload = () => {
getList()
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>