Accept Merge Request #81: (hyc -> master)

Merge Request: 【新增】三个操作题出题、考点页面

Created By: @华允传
Accepted By: @华允传
URL: https://g-iswv8783.coding.net/p/education/d/pengchen-ui-exam-vue3/git/merge/81?initial=true
This commit is contained in:
华允传
2025-05-25 00:30:19 +08:00
committed by Coding
5 changed files with 1746 additions and 404 deletions

View File

@@ -111,3 +111,23 @@ export const noauditQue = (ids: string[]) => {
export const importQueTemplate = () => {
return request.download({ url: '/exam/question/get-import-template' })
}
//查询得到考点
export const getListByQuId = (id) => {
return request.get({ url: `/exam/getPoints/get_Point_id/${id}` })
}
export const getMysqlPoint = (data) => {
return request.post({ url: `/exam/getPoints/get_mysql_point`,data })
}
//设置mysql考点
export const saveSelectedKaodian = (data) => {
return request.post({ url: `/exam/getPoints/update_mysql_point`,data })
}
//设置浏览器考点
export const setBrowserPoint = (data) => {
return request.post({ url: `/exam/getPoints/set_browser_point`,data })
}
export const getFilePoint = (data) => {
return request.post({ url: `/exam/getPoints/get_filePoint`,data })
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="edit-dialog">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh" class="custom-dialog">
<el-scrollbar>
<div class="main">
<el-form
@@ -29,9 +29,23 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="题型难度" prop="quLevel">
<el-input v-model="formData.quLevel" placeholder="请输入题型难度" disabled/>
</el-form-item>
<!-- <el-form-item label="题型难度" prop="quLevel">
<el-input v-model="formData.quLevel" placeholder="请输入题型难度" />
</el-form-item> -->
<el-form-item label="题型难度" prop="quLevel">
<el-select
v-model="formData.quLevel"
placeholder="请选择题型难度"
clearable
>
<el-option label="简单" :value="'0'" />
<el-option label="一般" :value="'1'" />
<el-option label="困难" :value="'2'" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
@@ -43,9 +57,9 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="12">
<el-form-item label="章节名称" prop="chapteridDictText">
<el-input v-model="formData.chapteridDictText" placeholder="请输入章节名称" />
<el-input v-model="formData.chapteridDictText" placeholder="请输入章节名称" disabled />
</el-form-item>
</el-col>
</el-row>
@@ -53,18 +67,14 @@
<el-row>
<el-col :span="12">
<el-form-item label="启用状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-form-item label="启用状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="'0'">启用</el-radio>
<el-radio :label="'1'">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
@@ -73,57 +83,20 @@
<el-tabs v-model="leftActiveName" class="demo-tabs">
<el-tab-pane label="试题描述" name="desc">
<div class="block">
<Editor v-model="formData.content" height="150px" />
<Editor v-model="formData.content" height="250px" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<div class="edit-right bottom-common">
<el-tabs v-model="rightActiveName" class="demo-tabs" @tab-click="rightHandleClick">
<!-- <el-tab-pane class="answer" label="试题答案" name="answer">
<div class="block">
<div class="tip">
<p>答案设置&nbsp;&nbsp;|&nbsp;选项数</p>
<el-select
v-model="optionNumVal"
class="m-2"
placeholder="Select"
size="large"
style="width: 30%"
>
<el-option
v-for="item in optionNumber"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</div>
<el-radio-group v-model="radio">
<div class="options" v-for="item in optionNumVal" :key="item">
<div class="content">
<el-radio :label="String.fromCharCode(64 + item)">
{{ String.fromCharCode(64 + item) }}
</el-radio>
<el-input
class="text"
v-model="optionsContent[item]"
:rows="3"
type="textarea"
placeholder="请输入答案内容"
/>
</div>
<el-button class="more-btn" @click="moreEdit">更多</el-button>
</div>
</el-radio-group>
</div>
</el-tab-pane> -->
<el-tab-pane label="试题解析" name="analysis">
<div class="block">
<Editor v-model="formData.analysis" height="150px" />
<Editor v-model="formData.analysis" height="250px" />
</div>
</el-tab-pane>
<el-tab-pane name="keyword">
<el-tab-pane name="keyword">
<template #label>
<div class="custom-tabs-label">
<p>关键字</p>
@@ -134,7 +107,7 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="editKeyword('create')">新建</el-dropdown-item>
<el-dropdown-item @click="editKeyword('create')">编辑</el-dropdown-item>
<el-dropdown-item @click="editKeyword('update')">编辑</el-dropdown-item>
<el-dropdown-item @click="editKeyword('delete')">删除</el-dropdown-item>
<el-dropdown-item @click="editKeyword('deleteall')"
>删除全部</el-dropdown-item
@@ -174,7 +147,7 @@
</el-dialog>
</div>
</el-tab-pane>
<el-tab-pane name="medium">
<!-- <el-tab-pane name="medium">
<template #label>
<div class="custom-tabs-label">
<p>媒体文件</p>
@@ -216,7 +189,135 @@
<el-table-column prop="size" label="大小" />
</el-table>
</div>
</el-tab-pane>
</el-tab-pane> -->
<el-tab-pane v-if="formType === 'update'" name="point">
<template #label>
<div class="custom-tabs-label">
<p>考点设置</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="setKao()">考点设置</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block" style="height: 300px;width: 100% ;">
<el-table
:data="kaodianList"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="scoreRate" label="权值" width="90" />
<el-table-column prop="content" label="文件名(不带后缀)" width="260" />
<el-table-column prop="contentIn" label="考察类型" width="300" />
</el-table>
</div>
<!-- 弹框 -->
<el-dialog
title="考点设置"
v-model="kaoDialogVisible"
width="60%"
:close-on-click-modal="false"
class="custom-dialog"
>
<!-- 可滚动容器 -->
<div style="height: 400px; overflow-y: auto;">
<el-table
:data="kaodianList"
style="width: 100%;"
row-key="answerId"
:default-expand-all="false"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column label="文件名(不带后缀)" width="260">
<template #default="scope">
<el-input
v-model="scope.row.content"
size="small"
type="input"
/>
</template>
</el-table-column>
<el-table-column label="权值" width="100">
<template #default="scope">
<el-input
v-model="scope.row.scoreRate"
size="small"
type="number"
/>
</template>
</el-table-column>
<el-table-column label="考察类型" width="300">
<template #default="scope">
<el-select
v-model="scope.row.contentIn"
placeholder="请选择类型"
size="small"
style="width: 100%"
>
<el-option label="添加到收藏夹" value="添加到收藏夹" />
<el-option label="添加到文件夹" value="添加到文件夹" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button
type="danger"
size="small"
@click="removeKaodian(scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 增加按钮 -->
<div style="margin-top: 12px; text-align: center;">
<el-button type="primary" @click="addKaodianRow">
添加考点
</el-button>
</div>
</div>
<template #footer>
<el-button @click="kaoDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmKao">确定</el-button>
</template>
</el-dialog>
</el-tab-pane>
<el-tab-pane name="annex">
<template #label>
<div class="custom-tabs-label">
@@ -237,17 +338,72 @@
</el-dropdown>
</div>
</template>
<div class="block">
<el-table
:data="documentList"
style="width: 100%"
@selection-change="handleDocumentSelectionChange"
<!-- 提示 -->
<el-alert type="warning" show-icon :closable="false">
<template #default>
<span>提示文件名称可默认不设置</span>
</template>
</el-alert>
<div class="block">
<el-table :data="formData.fileUploads" style="width: 100%">
<el-table-column type="index" label="#" width="50" />
<el-table-column label="文件名称" width="250">
<template #default="scope">
<el-input
v-model="scope.row.fileName"
size="small"
placeholder="请输入文件名称"
/>
</template>
</el-table-column>
<el-table-column
prop="fileType"
label="类型"
:formatter="fileTypeFormatter"
/>
<el-table-column prop="url" label="地址" width="200">
<template #default="{ row }">
<div
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"
:title="row.url"
>
{{ row.url }}
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="320">
<template #default="scope">
<el-button
type="primary"
plain
@click="openForm(scope.row.fileType)"
size="small"
>
<el-table-column type="index" width="50" />
<el-table-column prop="type" label="附件" />
<el-table-column prop="size" label="大小" width="80" />
</el-table>
</div>
<Icon icon="ep:upload" class="mr-5px" /> 上传
</el-button>
<el-button
type="success"
plain
@click="downloadFile(scope.row.url)"
size="small"
>
<Icon icon="ep:download" class="mr-5px" /> 下载
</el-button>
<el-button
type="danger"
plain
@click="deleteUrl(scope.$index)"
size="small"
><Icon icon="ep:download" class="mr-5px" />删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
</div>
@@ -260,25 +416,29 @@
</template>
</Dialog>
</div>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="FileRef" @success="handleUploadSuccess"/>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { FormRules } from 'element-plus'
import * as QuestionApi from '@/api/paper/question'
import FileForm from './components/FileForm.vue';
defineOptions({ name: 'ChoiceForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
//右键菜单状态
const kaoDialogVisible = ref(false)
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
content: `---------------------------------------------------------------------
请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。
---------------------------------------------------------------------`,
content: '',
specialtyName: '',
courseName: '',
quBankName: '',
@@ -289,9 +449,80 @@ const formData = ref({
pointNames: '',
audit: '',
subjectName: '',
status: '',
resourceValue: ''
status: ' ',
keywords: '',
resourceValue: '',
fileUploads: [ {
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}]
})
const kaodianList = ref<any[]>([])
function fileTypeFormatter(row, column, cellValue) {
if (cellValue === '1') return '考试文件'
return '未知类型'
}
//考点
// formData.value.quId 中包含 quId假设它已有值
const kaodianData = ref({
quId: '', // 你打开编辑弹窗时会赋值
})
function setKao() {
kaoDialogVisible.value = true
}
function confirmKao() {
if (!kaodianList.value.length) {
ElMessage.warning("没有考点数据");
return;
}
// 构建 questionAnswerList
const questionAnswerList = kaodianList.value.map(item => ({
answerId: item.answerId,
content: item.content,
quId:kaodianData.value.quId,
contentIn:item.contentIn,
scoreRate:item.scoreRate
}));
const payload = {
quId:kaodianData.value.quId,
questionAnswerList
};
console.log('确认后的结果:', payload)
QuestionApi.setBrowserPoint(payload)
kaoDialogVisible.value= false;
}
// 删除行
function removeKaodian(index: number) {
kaodianList.value.splice(index, 1)
}
// 增加行
function addKaodianRow() {
kaodianList.value.push({
answerId: '', // 用你项目里生成 ID 的方法
content: '',
quId:kaodianData.value.quId,
scoreRate: 1,
contentIn: ''
})
}
const formRules = reactive<FormRules>({
// specialtyName: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }]
})
@@ -299,19 +530,23 @@ const formRef = ref() // 表单 Ref
// 左侧试题描述
const leftActiveName = ref('desc')
// 右侧tab
const rightActiveName = ref('answer')
const rightActiveName = ref('analysis')
const rightHandleClick = (tab, e) => {
rightActiveName.value = tab.paneName.value
if (tab.paneName === 'point' ) {
// 发起考点请求
QuestionApi.getListByQuId(kaodianData.value.quId).then(res => {
// res 是接口返回的数据数组
kaodianList.value = res;
});
}
}
const optionNumVal = ref(4)
const optionNumber = ref(26)
const radio = ref('A')
const annexList = ref([])
// 保留选项的值
const optionsContent = reactive({})
// 关键字
const keywordList = ref([] as any)
const multipleKeywordSelection = ref([] as any)
const handleKeywordSelectionChange = (val: any) => {
@@ -320,12 +555,47 @@ const handleKeywordSelectionChange = (val: any) => {
const keyVisible = ref(false)
const keyEditType = ref('')
const keyWord = ref([null])
const keyWord = ref('')
const editKeyword = (key) => {
const editKeyword = (key: string) => {
keyEditType.value = key
keyVisible.value = true
if (key === 'create') {
keyWord.value = ''
keyVisible.value = true
} else if (key === 'update') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择一个要编辑的关键字')
return
}
keyWord.value = multipleKeywordSelection.value[0].keyword
keyVisible.value = true
} else if (key === 'delete') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择要删除的关键字')
return
}
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
ElMessage.success('已删除选中项')
} else if (key === 'deleteall') {
keywordList.value = []
ElMessage.success('已清空关键字列表')
}
updateKeywordsToForm()
}
const updateKeywordsToForm = () => {
const keywordStr = keywordList.value
.map(item => item.keyword)
.filter(k => k && k.trim() !== '')
.join(',')
formData.value.keywords = keywordStr
console.log(formData.value.keywords+"formData.value.keywords")
}
const keyDialogClose = () => {
keyVisible.value = false
}
@@ -334,11 +604,30 @@ const confirmKeyDialogVisible = () => {
keywordList.value.push({
keyword: keyWord.value
})
} else if (keyEditType.value === 'update') {
multipleKeywordSelection.value.forEach(item => {
item.keyword = keyWord.value
})
} else if (keyEditType.value === 'delete') {
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
} else if (keyEditType.value === 'deleteall') {
keywordList.value = []
}
updateKeywordsToForm()
keyVisible.value = false
}
/** 添加/修改操作 */
const FileRef = ref()
const openForm = (type: string) => {
FileRef.value.open(type)
}
// 媒体文件
const mediumList = ref([] as any)
const multipleMediumSelection = ref([] as any)
@@ -347,46 +636,112 @@ const handleMediumSelectionChange = (val: any) => {
}
const fileList = []
// 媒体文件
const documentList = ref([] as any)
const multipleDocumentSelection = ref([] as any)
const handleDocumentSelectionChange = (val: any) => {
multipleDocumentSelection.value = val
}
//文件
const handleUploadSuccess = ({ url, fileType }) => {
const index = formData.value.fileUploads.findIndex(item => item.fileType === fileType)
if (index !== -1) {
formData.value.fileUploads[index].url = url
}
}
const downloadFile = async (url: string) => {
if (!url) {
ElMessage.warning('暂无可下载的文件地址')
return
}
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error('下载失败')
}
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
// 提取文件名
const filename = url.substring(url.lastIndexOf('/') + 1).split('?')[0]
// 创建 a 标签并下载
const a = document.createElement('a')
a.href = blobUrl
a.download = filename
a.style.display = 'none'
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(blobUrl)
} catch (err: any) {
ElMessage.error(`下载失败:${err.message}`)
}
}
const deleteUrl = (index: number) => {
formData.value.fileUploads[index].url = ''
formData.value.fileUploads[index].fileName = ''
}
/** 打开弹窗 */
const open = async (queryParams: any ,type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
rightActiveName.value = 'analysis'
// 修改时,设置数据
if (id) {
kaodianData.value.quId=id;
formLoading.value = true
try {
formData.value = await QuestionApi.getQuestion(id)
for (let i = 0; i < formData.value.answerList.length; i++) {
if (formData.value.answerList[i].isRight === '0') {
let num = Number(i);
let nums = '';
if (num >= 0 && num <= 25) {
nums = String.fromCharCode(num + 65);
} else {
return '请输入 0-25 之间的数字';
}
radio.value = nums;
}
optionsContent[i + 1] = formData.value.answerList[i].content;
}
} finally {
try {
const res = await QuestionApi.getQuestion(id);
// 默认两个类型
const fileTypes = ['1'];
// 后端返回的上传文件列表(可能为空)
const documentList = res.fileUploads ?? [];
// 遍历两种类型,找到对应的上传文件,如果没有就用默认值
const fileUploads= fileTypes.map(type => {
const match = documentList.find(file => file.fileType === type);
return {
quId: match?.quId ?? res.quId ?? '',
url: match?.url ?? '',
fileType: type,
fileName: match?.fileName ?? ''
};
});
formData.value = {
...res,
fileUploads,
};
} catch (error) {
console.error("获取问题失败:", error);
} finally {
formLoading.value = false
}
} else {
resetForm()
formData.value.specialtyName = queryParams.specialtyName
formData.value.courseName = queryParams.courseName
formData.value.subjectName = queryParams.subjectName
formData.value.pointNames=queryParams.pointNames
formData.value.chapteridDictText=queryParams.chapteridDictText
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
// 转换函数:将大写字母 A-Z 映射为 0-25
@@ -404,65 +759,74 @@ const answerData = ref([]);
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
const values = Object.values(optionsContent);
console.log(values)
// 组合数据 (选择题)
// 创建临时字段
for (let i = 0; i < values.length; i++) {
let data = {};
data.content = values[i];
data.sort = i;
// 判断正确答案
if (mappedNumber.value === i) {
data.isRight = '0';
} else {
data.isRight = '1';
}
answerData.value.push(data);
// 校验表单
if (!formRef) return;
const valid = await formRef.value.validate();
if (!valid) return;
formLoading.value = true;
try {
// 深拷贝一份 formData
const data = JSON.parse(JSON.stringify(formData.value));
// 过滤掉 url 为空的文件项
data.fileUploads = data.fileUploads?.filter(file => file.url && file.url.trim() !== '');
console.log(data, "提交的数据");
if (formType.value === 'create') {
await QuestionApi.addQuestion(data);
message.success(t('common.createSuccess'));
} else {
await QuestionApi.editQuestion(data);
message.success(t('common.updateSuccess'));
}
formData.value.answerList = answerData.value;
console.log(formData.value.answerList)
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown
if (formType.value === 'create') {
await QuestionApi.addQuestion(data)
message.success(t('common.createSuccess'))
} else {
await QuestionApi.editQuestion(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
dialogVisible.value = false;
emit('success');
} finally {
formLoading.value = false;
}
};
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: '',
content: `---------------------------------------------------------------------
请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。
---------------------------------------------------------------------
`,
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: '',
quLevel: '0',
pointNames: '',
audit: '',
subjectName: '',
status: '',
resourceValue: ''
status: '0',
keywords: '',
resourceValue: '',
fileUploads: [ {
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}]
}
keywordList.value=[],
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
@@ -505,7 +869,7 @@ const resetForm = () => {
overflow-y: auto;
.block {
width: 100%;
height: 200px;
height: 80%;
overflow: auto;
}
.answer {

View File

@@ -1,6 +1,6 @@
<template>
<div class="edit-dialog">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh" class="custom-dialog">
<el-scrollbar>
<div class="main">
<el-form
@@ -29,9 +29,23 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="题型难度" prop="quLevel">
<el-input v-model="formData.quLevel" placeholder="请输入题型难度" disabled/>
</el-form-item>
<!-- <el-form-item label="题型难度" prop="quLevel">
<el-input v-model="formData.quLevel" placeholder="请输入题型难度" />
</el-form-item> -->
<el-form-item label="题型难度" prop="quLevel">
<el-select
v-model="formData.quLevel"
placeholder="请选择题型难度"
clearable
>
<el-option label="简单" :value="'0'" />
<el-option label="一般" :value="'1'" />
<el-option label="困难" :value="'2'" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
@@ -43,9 +57,9 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="12">
<el-form-item label="章节名称" prop="chapteridDictText">
<el-input v-model="formData.chapteridDictText" placeholder="请输入章节名称" />
<el-input v-model="formData.chapteridDictText" placeholder="请输入章节名称" disabled />
</el-form-item>
</el-col>
</el-row>
@@ -53,18 +67,14 @@
<el-row>
<el-col :span="12">
<el-form-item label="启用状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-form-item label="启用状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="'0'">启用</el-radio>
<el-radio :label="'1'">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
@@ -73,57 +83,20 @@
<el-tabs v-model="leftActiveName" class="demo-tabs">
<el-tab-pane label="试题描述" name="desc">
<div class="block">
<Editor v-model="formData.content" height="150px" />
<Editor v-model="formData.content" height="250px" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<div class="edit-right bottom-common">
<el-tabs v-model="rightActiveName" class="demo-tabs" @tab-click="rightHandleClick">
<!-- <el-tab-pane class="answer" label="试题答案" name="answer">
<div class="block">
<div class="tip">
<p>答案设置&nbsp;&nbsp;|&nbsp;选项数</p>
<el-select
v-model="optionNumVal"
class="m-2"
placeholder="Select"
size="large"
style="width: 30%"
>
<el-option
v-for="item in optionNumber"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</div>
<el-radio-group v-model="radio">
<div class="options" v-for="item in optionNumVal" :key="item">
<div class="content">
<el-radio :label="String.fromCharCode(64 + item)">
{{ String.fromCharCode(64 + item) }}
</el-radio>
<el-input
class="text"
v-model="optionsContent[item]"
:rows="3"
type="textarea"
placeholder="请输入答案内容"
/>
</div>
<el-button class="more-btn" @click="moreEdit">更多</el-button>
</div>
</el-radio-group>
</div>
</el-tab-pane> -->
<el-tab-pane label="试题解析" name="analysis">
<div class="block">
<Editor v-model="formData.analysis" height="150px" />
<Editor v-model="formData.analysis" height="250px" />
</div>
</el-tab-pane>
<el-tab-pane name="keyword">
<el-tab-pane name="keyword">
<template #label>
<div class="custom-tabs-label">
<p>关键字</p>
@@ -134,7 +107,7 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="editKeyword('create')">新建</el-dropdown-item>
<el-dropdown-item @click="editKeyword('create')">编辑</el-dropdown-item>
<el-dropdown-item @click="editKeyword('update')">编辑</el-dropdown-item>
<el-dropdown-item @click="editKeyword('delete')">删除</el-dropdown-item>
<el-dropdown-item @click="editKeyword('deleteall')"
>删除全部</el-dropdown-item
@@ -174,7 +147,7 @@
</el-dialog>
</div>
</el-tab-pane>
<el-tab-pane name="medium">
<!-- <el-tab-pane name="medium">
<template #label>
<div class="custom-tabs-label">
<p>媒体文件</p>
@@ -216,7 +189,130 @@
<el-table-column prop="size" label="大小" />
</el-table>
</div>
</el-tab-pane>
</el-tab-pane> -->
<el-tab-pane v-if="formType === 'update'" name="point">
<template #label>
<div class="custom-tabs-label">
<p>考点设置</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="setKao()">考点设置</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block" style="height: 300px;width: 100% ;">
<el-table
:data="kaodianList"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="scoreRate" label="权值" width="90" />
<el-table-column prop="content" label="文件/文件夹名(带后缀)" width="260" />
<el-table-column prop="contentIn" label="考察类型" width="300" />
</el-table>
</div>
<!-- 弹框 -->
<el-dialog
title="考点设置"
v-model="kaoDialogVisible"
width="60%"
:close-on-click-modal="false"
class="custom-dialog"
>
<!-- 可滚动容器 -->
<div style="height: 400px; overflow-y: auto;">
<el-table
:data="kaodianList"
style="width: 100%;"
row-key="answerId"
:default-expand-all="false"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column label="文件/文件夹名(带后缀)" width="260">
<template #default="scope">
<el-input
v-model="scope.row.content"
size="small"
type="input"
/>
</template>
</el-table-column>
<el-table-column label="权值" width="100">
<template #default="scope">
<el-input
v-model="scope.row.scoreRate"
size="small"
type="number"
/>
</template>
</el-table-column>
<el-table-column label="考察类型" width="300">
<template #default="scope">
<el-select
v-model="scope.row.contentIn"
placeholder="请选择类型"
size="small"
style="width: 100%"
>
<el-option label="考察名称" value="考察名称" />
<el-option label="考察删除" value="考察删除" />
<el-option label="考察属性" value="考察属性" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button
type="danger"
size="small"
@click="removeKaodian(scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 增加按钮 -->
<div style="margin-top: 12px; text-align: center;">
<el-button type="primary" @click="setKaodianRow">
导入考点
</el-button>
<el-button type="primary" @click="addKaodianRow">
添加考点
</el-button>
</div>
</div>
<template #footer>
<el-button @click="kaoDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmKao">确定</el-button>
</template>
</el-dialog>
</el-tab-pane>
<el-tab-pane name="annex">
<template #label>
<div class="custom-tabs-label">
@@ -237,17 +333,72 @@
</el-dropdown>
</div>
</template>
<div class="block">
<el-table
:data="documentList"
style="width: 100%"
@selection-change="handleDocumentSelectionChange"
<!-- 提示 -->
<el-alert type="warning" show-icon :closable="false">
<template #default>
<span>提示文件名称可默认不设置</span>
</template>
</el-alert>
<div class="block">
<el-table :data="formData.fileUploads" style="width: 100%">
<el-table-column type="index" label="#" width="50" />
<el-table-column label="文件名称" width="250">
<template #default="scope">
<el-input
v-model="scope.row.fileName"
size="small"
placeholder="请输入文件名称"
/>
</template>
</el-table-column>
<el-table-column
prop="fileType"
label="类型"
:formatter="fileTypeFormatter"
/>
<el-table-column prop="url" label="地址" width="200">
<template #default="{ row }">
<div
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"
:title="row.url"
>
{{ row.url }}
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="320">
<template #default="scope">
<el-button
type="primary"
plain
@click="openForm(scope.row.fileType)"
size="small"
>
<el-table-column type="index" width="50" />
<el-table-column prop="type" label="附件" />
<el-table-column prop="size" label="大小" width="80" />
</el-table>
</div>
<Icon icon="ep:upload" class="mr-5px" /> 上传
</el-button>
<el-button
type="success"
plain
@click="downloadFile(scope.row.url)"
size="small"
>
<Icon icon="ep:download" class="mr-5px" /> 下载
</el-button>
<el-button
type="danger"
plain
@click="deleteUrl(scope.$index)"
size="small"
><Icon icon="ep:download" class="mr-5px" />删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
</div>
@@ -260,12 +411,16 @@
</template>
</Dialog>
</div>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="FileRef" @success="handleUploadSuccess"/>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { FormRules } from 'element-plus'
import * as QuestionApi from '@/api/paper/question'
import FileForm from './components/FileForm.vue';
defineOptions({ name: 'ChoiceForm' })
const { t } = useI18n() // 国际化
@@ -287,9 +442,105 @@ const formData = ref({
pointNames: '',
audit: '',
subjectName: '',
status: '',
resourceValue: ''
status: ' ',
keywords: '',
resourceValue: '',
fileUploads: [ {
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}]
})
function fileTypeFormatter(row, column, cellValue) {
if (cellValue === '1') return '考试文件'
if (cellValue === '2') return '结果文件'
return '未知类型'
}
//考点
const kaoDialogVisible = ref(false)
const kaodianData = ref({
quId: '', // 你打开编辑弹窗时会赋值
})
function setKao() {
kaoDialogVisible.value = true
}
const kaodianList = ref<any[]>([])
function confirmKao() {
if (!kaodianList.value.length) {
ElMessage.warning("没有考点数据");
return;
}
// 构建 questionAnswerList
const questionAnswerList = kaodianList.value.map(item => ({
answerId: item.answerId,
content: item.content,
quId:kaodianData.value.quId,
contentIn:item.contentIn,
scoreRate:item.scoreRate
}));
const payload = {
quId:kaodianData.value.quId,
questionAnswerList
};
console.log('确认后的结果:', payload)
QuestionApi.setBrowserPoint(payload)
kaoDialogVisible.value= false;
}
// 增加行
function addKaodianRow() {
kaodianList.value.push({
answerId: '', // 用你项目里生成 ID 的方法
content: '',
quId:kaodianData.value.quId,
scoreRate: 1,
contentIn: ''
})
}
// 删除行
function removeKaodian(index: number) {
kaodianList.value.splice(index, 1)
}
const setKaodianRow =async () => {
const uploads = formData.value.fileUploads;
if (!uploads[0].url || !uploads[1].url) {
ElMessage.error('请先上传两个文件再导入考点');
return;
}
const fileUrl1 = uploads[0].url;
const fileUrl2 = uploads[1].url;
console.log('导入考点的文件地址为:', fileUrl1, fileUrl2);
const params = {
answerPath: fileUrl1,
shucaiPath: fileUrl2 // 如果不传可以是空字符串,也可以删除这个字段(根据后端是否必填)
};
const res = await QuestionApi.getFilePoint(params);
kaodianList.value=res;
// 示例:调用后端接口进行导入
// importKaodianFromFiles(fileUrl1, fileUrl2);
};
const formRules = reactive<FormRules>({
// specialtyName: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }]
})
@@ -297,19 +548,22 @@ const formRef = ref() // 表单 Ref
// 左侧试题描述
const leftActiveName = ref('desc')
// 右侧tab
const rightActiveName = ref('answer')
const rightActiveName = ref('analysis')
const rightHandleClick = (tab, e) => {
rightActiveName.value = tab.paneName.value
if (tab.paneName === 'point' ) {
// 发起考点请求
QuestionApi.getListByQuId(kaodianData.value.quId).then(res => {
// res 是接口返回的数据数组
kaodianList.value = res;
});
}
}
const optionNumVal = ref(4)
const optionNumber = ref(26)
const radio = ref('A')
const annexList = ref([])
// 保留选项的值
const optionsContent = reactive({})
// 关键字
const keywordList = ref([] as any)
const multipleKeywordSelection = ref([] as any)
const handleKeywordSelectionChange = (val: any) => {
@@ -318,12 +572,47 @@ const handleKeywordSelectionChange = (val: any) => {
const keyVisible = ref(false)
const keyEditType = ref('')
const keyWord = ref([null])
const keyWord = ref('')
const editKeyword = (key) => {
const editKeyword = (key: string) => {
keyEditType.value = key
keyVisible.value = true
if (key === 'create') {
keyWord.value = ''
keyVisible.value = true
} else if (key === 'update') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择一个要编辑的关键字')
return
}
keyWord.value = multipleKeywordSelection.value[0].keyword
keyVisible.value = true
} else if (key === 'delete') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择要删除的关键字')
return
}
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
ElMessage.success('已删除选中项')
} else if (key === 'deleteall') {
keywordList.value = []
ElMessage.success('已清空关键字列表')
}
updateKeywordsToForm()
}
const updateKeywordsToForm = () => {
const keywordStr = keywordList.value
.map(item => item.keyword)
.filter(k => k && k.trim() !== '')
.join(',')
formData.value.keywords = keywordStr
console.log(formData.value.keywords+"formData.value.keywords")
}
const keyDialogClose = () => {
keyVisible.value = false
}
@@ -332,11 +621,30 @@ const confirmKeyDialogVisible = () => {
keywordList.value.push({
keyword: keyWord.value
})
} else if (keyEditType.value === 'update') {
multipleKeywordSelection.value.forEach(item => {
item.keyword = keyWord.value
})
} else if (keyEditType.value === 'delete') {
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
} else if (keyEditType.value === 'deleteall') {
keywordList.value = []
}
updateKeywordsToForm()
keyVisible.value = false
}
/** 添加/修改操作 */
const FileRef = ref()
const openForm = (type: string) => {
FileRef.value.open(type)
}
// 媒体文件
const mediumList = ref([] as any)
const multipleMediumSelection = ref([] as any)
@@ -345,46 +653,113 @@ const handleMediumSelectionChange = (val: any) => {
}
const fileList = []
// 媒体文件
const documentList = ref([] as any)
const multipleDocumentSelection = ref([] as any)
const handleDocumentSelectionChange = (val: any) => {
multipleDocumentSelection.value = val
}
//文件
const handleUploadSuccess = ({ url, fileType }) => {
const index = formData.value.fileUploads.findIndex(item => item.fileType === fileType)
if (index !== -1) {
formData.value.fileUploads[index].url = url
}
}
const downloadFile = async (url: string) => {
if (!url) {
ElMessage.warning('暂无可下载的文件地址')
return
}
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error('下载失败')
}
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
// 提取文件名
const filename = url.substring(url.lastIndexOf('/') + 1).split('?')[0]
// 创建 a 标签并下载
const a = document.createElement('a')
a.href = blobUrl
a.download = filename
a.style.display = 'none'
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(blobUrl)
} catch (err: any) {
ElMessage.error(`下载失败:${err.message}`)
}
}
const deleteUrl = (index: number) => {
formData.value.fileUploads[index].url = ''
formData.value.fileUploads[index].fileName = ''
}
/** 打开弹窗 */
const open = async (queryParams: any ,type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
rightActiveName.value = 'analysis'
// 修改时,设置数据
if (id) {
kaodianData.value.quId=id;
formLoading.value = true
try {
formData.value = await QuestionApi.getQuestion(id)
for (let i = 0; i < formData.value.answerList.length; i++) {
if (formData.value.answerList[i].isRight === '0') {
let num = Number(i);
let nums = '';
if (num >= 0 && num <= 25) {
nums = String.fromCharCode(num + 65);
} else {
return '请输入 0-25 之间的数字';
}
radio.value = nums;
}
optionsContent[i + 1] = formData.value.answerList[i].content;
}
const res = await QuestionApi.getQuestion(id);
// 默认两个类型
const fileTypes = ['1', '2'];
// 后端返回的上传文件列表(可能为空)
const documentList = res.fileUploads ?? [];
// 遍历两种类型,找到对应的上传文件,如果没有就用默认值
const fileUploads = fileTypes.map(type => {
const match = documentList.find(file => file.fileType === type);
return {
quId: match?.quId ?? res.quId ?? '',
url: match?.url ?? '',
fileType: type,
fileName: match?.fileName ?? ''
};
});
formData.value = {
...res,
fileUploads,
};
keywordList.value = res.keywords.split(',').map(item => ({ keyword: item.trim() }));
} finally {
formLoading.value = false
}
} else {
resetForm()
formData.value.specialtyName = queryParams.specialtyName
formData.value.courseName = queryParams.courseName
formData.value.subjectName = queryParams.subjectName
formData.value.pointNames=queryParams.pointNames
formData.value.chapteridDictText=queryParams.chapteridDictText
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
// 转换函数:将大写字母 A-Z 映射为 0-25
@@ -402,68 +777,73 @@ const answerData = ref([]);
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
const values = Object.values(optionsContent);
console.log(values)
// 组合数据 (选择题)
// 创建临时字段
for (let i = 0; i < values.length; i++) {
let data = {};
data.content = values[i];
data.sort = i;
// 判断正确答案
if (mappedNumber.value === i) {
data.isRight = '0';
} else {
data.isRight = '1';
}
answerData.value.push(data);
// 校验表单
if (!formRef) return;
const valid = await formRef.value.validate();
if (!valid) return;
formLoading.value = true;
try {
// 深拷贝一份 formData
const data = JSON.parse(JSON.stringify(formData.value));
// 过滤掉 url 为空的文件项
data.fileUploads = data.fileUploads?.filter(file => file.url && file.url.trim() !== '');
console.log(data, "提交的数据");
if (formType.value === 'create') {
await QuestionApi.addQuestion(data);
message.success(t('common.createSuccess'));
} else {
await QuestionApi.editQuestion(data);
message.success(t('common.updateSuccess'));
}
formData.value.answerList = answerData.value;
console.log(formData.value.answerList)
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown
if (formType.value === 'create') {
await QuestionApi.addQuestion(data)
message.success(t('common.createSuccess'))
} else {
await QuestionApi.editQuestion(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
dialogVisible.value = false;
emit('success');
} finally {
formLoading.value = false;
}
};
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: `---------------------------------------------------------------------
请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。
---------------------------------------------------------------------`,
请在打开的窗口中,进行下列操作,完成所有操作后,请关闭窗口。
---------------------------------------------------------------------
`,
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: '',
quLevel: '0',
pointNames: '',
audit: '',
subjectName: '',
status: '',
resourceValue: ''
status: '0',
keywords: '',
resourceValue: '',
fileUploads: [ {
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}]
}
keywordList.value=[],
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
@@ -506,7 +886,7 @@ const resetForm = () => {
overflow-y: auto;
.block {
width: 100%;
height: 200px;
height: 80%;
overflow: auto;
}
.answer {

View File

@@ -1,6 +1,6 @@
<template>
<div class="edit-dialog">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh" class="custom-dialog">
<el-scrollbar>
<div class="main">
<el-form
@@ -29,9 +29,23 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="题型难度" prop="quLevel">
<el-input v-model="formData.quLevel" placeholder="请输入题型难度" disabled/>
</el-form-item>
<!-- <el-form-item label="题型难度" prop="quLevel">
<el-input v-model="formData.quLevel" placeholder="请输入题型难度" />
</el-form-item> -->
<el-form-item label="题型难度" prop="quLevel">
<el-select
v-model="formData.quLevel"
placeholder="请选择题型难度"
clearable
>
<el-option label="简单" :value="'0'" />
<el-option label="一般" :value="'1'" />
<el-option label="困难" :value="'2'" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
@@ -43,7 +57,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="12">
<el-form-item label="章节名称" prop="chapteridDictText">
<el-input v-model="formData.chapteridDictText" placeholder="请输入章节名称" disabled />
</el-form-item>
@@ -53,20 +67,14 @@
<el-row>
<el-col :span="12">
<el-form-item label="启用状态" prop="status">
<el-radio-group v-model="formData.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-item label="启用状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="'0'">启用</el-radio>
<el-radio :label="'1'">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-col>
</el-row>
</el-form>
@@ -75,7 +83,7 @@
<el-tabs v-model="leftActiveName" class="demo-tabs">
<el-tab-pane label="试题描述" name="desc">
<div class="block">
<Editor v-model="formData.content" height="150px" />
<Editor v-model="formData.content" height="250px" />
</div>
</el-tab-pane>
</el-tabs>
@@ -88,8 +96,183 @@
<Editor v-model="formData.analysis" height="150px" />
</div>
</el-tab-pane>
<el-tab-pane name="keyword">
<template #label>
<div class="custom-tabs-label">
<p>关键字</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="editKeyword('create')">新建</el-dropdown-item>
<el-dropdown-item @click="editKeyword('update')">编辑</el-dropdown-item>
<el-dropdown-item @click="editKeyword('delete')">删除</el-dropdown-item>
<el-dropdown-item @click="editKeyword('deleteall')"
>删除全部</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block">
<el-table
:data="keywordList"
style="width: 100%"
@selection-change="handleKeywordSelectionChange"
>
<el-table-column type="index" width="50" />
<el-table-column type="selection" width="55" />
<el-table-column prop="keyword" label="关键字" />
</el-table>
<el-dialog
v-model="keyVisible"
title="编辑关键字"
width="50%"
:before-close="keyDialogClose"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div class="main" style="width: 100%; height: 100%">
<el-input v-model="keyWord" placeholder="请输入关键字" size="large" />
<div class="dialog-footer" style="margin-top: 20px;">
<el-button @click="keyDialogClose">取消</el-button>
<el-button type="primary" @click="confirmKeyDialogVisible">
确定
</el-button>
</div>
</div>
</el-dialog>
</div>
</el-tab-pane>
<el-tab-pane v-if="formType === 'update'" name="point">
<el-tab-pane name="medium">
<template #label>
<div class="custom-tabs-label">
<p>考点设置</p>
<el-dropdown>
<span class="el-dropdown-link" @click.stop="false">
<div class="setting_icon"></div>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="setKao()">考点设置</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block" style="height: 300px;width: 100% ;">
<el-table
:data="kaodianList"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="score" label="权值" width="60" />
<el-table-column prop="content" label="考点语句" width="800" />
</el-table>
</div>
<!-- 弹框 -->
<el-dialog
title="考点设置"
v-model="kaoDialogVisible"
width="60%"
:close-on-click-modal="false"
class="custom-dialog"
>
<!-- 可滚动容器 -->
<div style="height: 400px; overflow-y: auto;">
<el-table
:data="kaodianList"
style="width: 100%;"
row-key="answerId"
:default-expand-all="false"
@row-contextmenu="(row, column, event) => handleTextRightClick(event, row)"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="score" label="权值" width="60" />
<el-table-column prop="content" label="考点语句" width="600" />
<!-- 展开行 -->
<el-table-column type="expand">
<template #default="props">
<el-table
:data="props.row.examMysqlKeywordList"
style="width: 100%;"
border
size="small"
>
<el-table-column type="index" label="序号" width="50" />
<el-table-column prop="keyword" label="关键词" />
<el-table-column label="权值" width="80">
<template #default="scope">
<el-input
v-model="scope.row.scoreRate"
size="small"
style="width: 70px;"
@input="updateScore(props.row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="60">
<template #default="scope">
<el-button type="text" size="small" @click="removeKeyword(props.row, scope.$index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</template>
</el-table-column>
</el-table>
</div>
<!-- 自定义右键菜单 -->
<div
v-if="contextMenuVisible"
class="context-menu"
:style="contextMenuStyle"
>
<div class="context-menu-item" @click="addKaodian(rightClickRow)">
设置此考点
</div>
</div>
<template #footer>
<el-upload
ref="uploadRef"
class="upload"
:action="uploadUrl"
:limit="1"
:before-upload="beforeUpload"
:on-success="handleUploadSuccessFile"
:http-request="httpRequest"
drag
>
<el-button type="primary" style="margin-bottom: 16px;">上传文件</el-button>
</el-upload>
<el-button @click="kaoDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmKao">确定</el-button>
</template>
</el-dialog>
</el-tab-pane>
<!-- <el-tab-pane name="medium">
<template #label>
<div class="custom-tabs-label">
<p>媒体文件</p>
@@ -131,7 +314,7 @@
<el-table-column prop="size" label="大小" />
</el-table>
</div>
</el-tab-pane>
</el-tab-pane> -->
<el-tab-pane name="annex">
<template #label>
<div class="custom-tabs-label">
@@ -160,7 +343,7 @@
</el-alert>
<div class="block">
<el-table :data="documentList" style="width: 100%">
<el-table :data="formData.fileUploads" style="width: 100%">
<el-table-column type="index" label="#" width="50" />
<el-table-column label="文件名称" width="250">
<template #default="scope">
@@ -187,13 +370,35 @@
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button type="primary" plain @click="openForm(scope.row.fileType)">
<Icon icon="ep:upload" class="mr-5px" /> 上传
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="320">
<template #default="scope">
<el-button
type="primary"
plain
@click="openForm(scope.row.fileType)"
size="small"
>
<Icon icon="ep:upload" class="mr-5px" /> 上传
</el-button>
<el-button
type="success"
plain
@click="downloadFile(scope.row.url)"
size="small"
>
<Icon icon="ep:download" class="mr-5px" /> 下载
</el-button>
<el-button
type="danger"
plain
@click="deleteUrl(scope.$index)"
size="small"
><Icon icon="ep:download" class="mr-5px" />删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
@@ -219,10 +424,11 @@ import { FormRules } from 'element-plus'
import * as QuestionApi from '@/api/paper/question'
import FileForm from './components/FileForm.vue';
defineOptions({ name: 'ChoiceForm' })
import type { TabPaneName } from 'element-plus'
import { useUpload } from '@/components/UploadFile/src/useUpload'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const { uploadUrl, httpRequest } = useUpload()
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
@@ -240,29 +446,35 @@ const formData = ref({
audit: '',
subjectName: '',
status: ' ',
resourceValue: ''
keywords: '',
resourceValue: '',
fileUploads: [ {
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}]
})
function fileTypeFormatter(row, column, cellValue) {
if (cellValue === '1') return '考试文件'
if (cellValue === '2') return '结果文件'
return '未知类型'
}
const documentList = ref([
{
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}
])
// 当前激活的 tab
const activeTab = ref('basic') // 初始 tab name根据实际设置
// formData.value.quId 中包含 quId假设它已有值
const kaodianData = ref({
quId: '', // 你打开编辑弹窗时会赋值
})
const formRules = reactive<FormRules>({
// specialtyName: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }]
})
@@ -270,22 +482,281 @@ const formRef = ref() // 表单 Ref
// 左侧试题描述
const leftActiveName = ref('desc')
// 右侧tab
const rightActiveName = ref('answer')
const rightActiveName = ref('analysis')
const rightHandleClick = (tab, e) => {
rightActiveName.value = tab.paneName.value
rightActiveName.value = tab.paneName;
if (tab.paneName === 'point' ) {
// 发起考点请求
QuestionApi.getListByQuId(kaodianData.value.quId).then(res => {
// res 是接口返回的数据数组
kaodianList.value = res.map(item => {
const totalScore = item.examMysqlKeywordList?.reduce((sum, kw) => {
return sum + Number(kw.scoreRate || 0);
}, 0);
return {
...item,
score: isNaN(totalScore) ? 0 : totalScore, // 如果是 NaN 则显示为 0
};
});
});
}
};
function updateScore(parentRow: any) {
parentRow.score = (parentRow.examMysqlKeywordList || []).reduce((sum, kw) => {
const rate = parseFloat(kw.scoreRate);
return sum + (isNaN(rate) ? 0 : rate);
}, 0);
}
//右键菜单状态
const kaoDialogVisible = ref(false) // 测试时默认打开
const kaodianList = ref<any[]>([])
const contextMenuVisible = ref(false)
const contextMenuStyle = ref({ top: '0px', left: '0px' })
const rightClickRow = ref<any>(null)
function handleTextRightClick(event: MouseEvent, row: any) {
event.preventDefault()
const selection = window.getSelection()
const selectedText = selection?.toString().trim()
if (selectedText) {
rightClickRow.value = row // 记录当前行
rightClickRow.value.selectedText = selectedText // 保存选中的文本
contextMenuStyle.value = {
top: `${event.clientY}px`,
left: `${event.clientX}px`
}
contextMenuVisible.value = true
document.addEventListener('click', closeContextMenu)
} else {
contextMenuVisible.value = false
}
}
function closeContextMenu() {
contextMenuVisible.value = false
document.removeEventListener('click', closeContextMenu)
}
// 设置考点:添加到下方
function addKaodian(row: any) {
const targetRow = rightClickRow.value;
const keyword = targetRow.selectedText;
if (!targetRow.examMysqlKeywordList) {
targetRow.examMysqlKeywordList = [];
}
const exists = targetRow.examMysqlKeywordList.find((item: any) => item.keyword === keyword);
if (!exists) {
const newKeyword = {
keyword,
scoreRate: 1,
answerId: targetRow.answerId // 添加 answerId
};
targetRow.examMysqlKeywordList.push(newKeyword);
// 累加关键字权值到语句的总权值
if (!targetRow.score) {
targetRow.score = 0;
}
targetRow.score += newKeyword.scoreRate;
}
contextMenuVisible.value = false;
}
function confirmKao() {
if (!kaodianList.value.length) {
ElMessage.warning("没有考点数据");
return;
}
// 构建 questionAnswerList
const questionAnswerList = kaodianList.value.map(item => ({
answerId: item.answerId,
content: item.content,
examMysqlKeywordList: (item.examMysqlKeywordList || []).map(kw => ({
keywordId: kw.keywordId,
answerId: kw.answerId,
keyword: kw.keyword,
scoreRate: kw.scoreRate
}))
}));
const payload = {
quId:kaodianData.value.quId,
questionAnswerList
};
console.log('确认后的结果:', payload)
QuestionApi.saveSelectedKaodian(payload)
kaoDialogVisible.value= false;
}
const handleUploadSuccessFile = (response: any) => {
console.log('上传成功,返回:', response);
const fileUrl = response.data; // 取决于你的后端返回结构
if (fileUrl) {
// 拿到URL后调用你要请求的方法
fetchKaodianByUrl(fileUrl);
} else {
ElMessage.error('上传失败:没有返回文件地址');
}
};
const fetchKaodianByUrl = async (url: string) => {
try {
const params = {
answerPath: url,
shucaiPath: "" // 如果不传可以是空字符串,也可以删除这个字段(根据后端是否必填)
};
const res = await QuestionApi.getMysqlPoint(params);
// 根据返回更新 kaodianList 或弹出提示等
kaodianList.value = res || [];
const sqlList = res || [];
// 将 SQL 字符串数组转换为 kaodianList 所需结构
kaodianList.value = sqlList.map((sql, index) => ({
answerId: index + 1, // 可自定义唯一ID生成逻辑
quId: kaodianData.value.quId, // 如果有题目ID可以在这里设置
content: sql,
examMysqlKeywordList: [] // 默认空关键词列表
}));
ElMessage.success('考点导入成功');
} catch (err) {
ElMessage.error('考点导入失败');
}
};
const beforeUpload = (file: File) => {
const isTxt = file.type === 'text/plain';
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isTxt) {
ElMessage.error('只能上传 TXT 文本文件');
return false;
}
if (!isLt5M) {
ElMessage.error('文件大小不能超过 5MB');
return false;
}
return true;
};
function removeKeyword(parentRow: any, keywordIndex: number) {
parentRow.examMysqlKeywordList.splice(keywordIndex, 1);
updateScore(parentRow); // 删除后更新score
}
const radio = ref('A')
// 保留选项的值
const optionsContent = reactive({})
// 关键字
const keywordList = ref([] as any)
const multipleKeywordSelection = ref([] as any)
const handleKeywordSelectionChange = (val: any) => {
multipleKeywordSelection.value = val
}
const keyVisible = ref(false)
const keyEditType = ref('')
const keyWord = ref('')
const editKeyword = (key: string) => {
keyEditType.value = key
if (key === 'create') {
keyWord.value = ''
keyVisible.value = true
} else if (key === 'update') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择一个要编辑的关键字')
return
}
keyWord.value = multipleKeywordSelection.value[0].keyword
keyVisible.value = true
} else if (key === 'delete') {
if (multipleKeywordSelection.value.length === 0) {
ElMessage.warning('请先选择要删除的关键字')
return
}
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
ElMessage.success('已删除选中项')
} else if (key === 'deleteall') {
keywordList.value = []
ElMessage.success('已清空关键字列表')
}
updateKeywordsToForm()
}
const updateKeywordsToForm = () => {
const keywordStr = keywordList.value
.map(item => item.keyword)
.filter(k => k && k.trim() !== '')
.join(',')
formData.value.keywords = keywordStr
console.log(formData.value.keywords+"formData.value.keywords")
}
function setKao() {
kaoDialogVisible.value = true
}
const keyDialogClose = () => {
keyVisible.value = false
}
const confirmKeyDialogVisible = () => {
if (keyEditType.value === 'create') {
keywordList.value.push({
keyword: keyWord.value
})
} else if (keyEditType.value === 'update') {
multipleKeywordSelection.value.forEach(item => {
item.keyword = keyWord.value
})
} else if (keyEditType.value === 'delete') {
keywordList.value = keywordList.value.filter(
item => !multipleKeywordSelection.value.includes(item)
)
} else if (keyEditType.value === 'deleteall') {
keywordList.value = []
}
updateKeywordsToForm()
keyVisible.value = false
}
const keyWord = ref([null])
/** 添加/修改操作 */
const FileRef = ref()
@@ -306,25 +777,98 @@ const multipleDocumentSelection = ref([] as any)
const handleDocumentSelectionChange = (val: any) => {
multipleDocumentSelection.value = val
}
//文件
function fileTypeFormatter(row, column, cellValue) {
if (cellValue === '1') return '考试文件'
if (cellValue === '2') return '结果文件'
return '未知类型'
}
const handleUploadSuccess = ({ url, fileType }) => {
const index = documentList.value.findIndex(item => item.fileType === fileType)
const index = formData.value.fileUploads.findIndex(item => item.fileType === fileType)
if (index !== -1) {
documentList.value[index].url = url
formData.value.fileUploads[index].url = url
}
}
const downloadFile = async (url: string) => {
if (!url) {
ElMessage.warning('暂无可下载的文件地址')
return
}
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error('下载失败')
}
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
// 提取文件名
const filename = url.substring(url.lastIndexOf('/') + 1).split('?')[0]
// 创建 a 标签并下载
const a = document.createElement('a')
a.href = blobUrl
a.download = filename
a.style.display = 'none'
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(blobUrl)
} catch (err: any) {
ElMessage.error(`下载失败:${err.message}`)
}
}
const deleteUrl = (index: number) => {
formData.value.fileUploads[index].url = ''
formData.value.fileUploads[index].fileName = ''
}
/** 打开弹窗 */
const open = async (queryParams: any ,type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
rightActiveName.value = 'analysis'
// 修改时,设置数据
if (id) {
kaodianData.value.quId=id;
formLoading.value = true
try {
const res = await QuestionApi.getQuestion(id)
formData.value =res
documentList.value=res.fileUploads
const res = await QuestionApi.getQuestion(id);
// 默认两个类型
const fileTypes = ['1', '2'];
// 后端返回的上传文件列表(可能为空)
const documentList = res.fileUploads ?? [];
// 遍历两种类型,找到对应的上传文件,如果没有就用默认值
const fileUploads = fileTypes.map(type => {
const match = documentList.find(file => file.fileType === type);
return {
quId: match?.quId ?? res.quId ?? '',
url: match?.url ?? '',
fileType: type,
fileName: match?.fileName ?? ''
};
});
formData.value = {
...res,
fileUploads,
};
} finally {
formLoading.value = false
@@ -357,31 +901,36 @@ const answerData = ref([]);
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
const values = Object.values(optionsContent);
console.log(values)
// 校验表单
if (!formRef) return;
const valid = await formRef.value.validate();
if (!valid) return;
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown
if (formType.value === 'create') {
await QuestionApi.addQuestion(data)
message.success(t('common.createSuccess'))
} else {
await QuestionApi.editQuestion(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
formLoading.value = true;
try {
// 深拷贝一份 formData
const data = JSON.parse(JSON.stringify(formData.value));
// 过滤掉 url 为空的文件项
data.fileUploads = data.fileUploads?.filter(file => file.url && file.url.trim() !== '');
console.log(data, "提交的数据");
if (formType.value === 'create') {
await QuestionApi.addQuestion(data);
message.success(t('common.createSuccess'));
} else {
await QuestionApi.editQuestion(data);
message.success(t('common.updateSuccess'));
}
}
dialogVisible.value = false;
emit('success');
} finally {
formLoading.value = false;
}
};
/** 重置表单 */
@@ -401,33 +950,62 @@ MySQL密码为空
required: '',
chapteridDictText: '',
analysis: '',
quLevel: '',
quLevel: '0',
pointNames: '',
audit: '',
subjectName: '',
status: '',
resourceValue: ''
status: '0',
keywords: '',
resourceValue: '',
fileUploads: [
{
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}
]
}
documentList .value =([
{
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}
])
kaodianList.value=[],
keywordList.value=[],
formRef.value?.resetFields()
}
onMounted(() => {
document.addEventListener('click', () => {
contextMenuVisible.value = false
})
})
</script>
<style lang="scss" scoped>
.context-menu {
position: fixed;
background-color: white;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 9999;
padding: 5px 0;
width: 120px;
}
.context-menu-item {
padding: 6px 12px;
cursor: pointer;
}
.context-menu-item:hover {
background-color: #f5f5f5;
}
.edit-dialog {
:deep(.el-dialog) {
display: flex;
@@ -467,7 +1045,7 @@ MySQL密码为空
overflow-y: auto;
.block {
width: 100%;
height: 200px;
height: 80%;
overflow: auto;
}
.answer {

View File

@@ -53,7 +53,7 @@
/>
</el-select>
</el-form-item> -->
<el-form-item>
<el-form-item>
<!-- 状态切换单独一行 -->
<el-radio-group v-model="queryParams.audit" @change="handleQuery">
<el-radio-button :label="''">全部</el-radio-button>