Files
pengcheng-exam-teacher/src/views/paper/question/PsForm.vue
2025-08-13 09:02:55 +08:00

1235 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="edit-dialog">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="85%" top="10vh" class="custom-dialog">
<el-dialog v-model="dialogVisiblePoints" title="选择知识点" width="30%">
<el-tree
ref="treeRef"
:data="deptList"
node-key="id"
:props="{ label: 'name', children: 'children' }"
highlight-current
default-expand-all
@node-click="handleNodeClick"
/>
</el-dialog>
<el-scrollbar>
<div class="main">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-row>
<el-col :span="12">
<el-form-item label="专业" prop="specialtyName">
<el-input v-model="formData.specialtyName" placeholder="请输入专业" disabled/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="章节名称" prop="chapteridDictText">
<el-input v-model="formData.chapteridDictTextVo" placeholder="请输入章节名称" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="课程" prop="courseName">
<el-input v-model="formData.courseName" placeholder="请输入课程" disabled/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="题型难度" prop="quLevel">
<el-select
v-model="formData.quLevel"
placeholder="请选择题型难度"
clearable
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.EXAM_QUE_DIFF)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="题型" prop="subjectName">
<el-input v-model="formData.subjectName" placeholder="请输入题型" disabled/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="知识点" prop="pointNames">
<el-input
v-model="formData.pointNamesVo"
placeholder="请选择知识点"
readonly
@click="openPoints()"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<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>
<div class="edit-bottom">
<div class="edit-left bottom-common">
<el-tabs v-model="leftActiveName" class="demo-tabs">
<el-tab-pane label="试题描述" name="desc">
<div class="block">
<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 label="试题解析" name="analysis">
<div class="block">
<Editor v-model="formData.analysis" height="250px" />
</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 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>
<el-upload
v-model:file-list="fileList"
class="upload-demo"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
>
<el-button>导入</el-button>
</el-upload>
</el-dropdown-item>
<el-dropdown-item>导出</el-dropdown-item>
<el-dropdown-item>播放</el-dropdown-item>
<el-dropdown-item>编辑</el-dropdown-item>
<el-dropdown-item>删除选中</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<div class="block">
<el-table
:data="mediumList"
style="width: 100%"
@selection-change="handleMediumSelectionChange"
>
<el-table-column type="index" width="50" />
<el-table-column type="selection" width="55" />
<el-table-column prop="type" label="媒体类型" width="80" />
<el-table-column prop="title" label="标题" />
<el-table-column prop="displayIndex" label="显示序号" />
<el-table-column prop="size" label="大小" />
</el-table>
</div>
</el-tab-pane> -->
<el-tab-pane v-if="formType === 'update'" name="point">
<el-alert type="warning" show-icon :closable="false">
<template #default>
<span>提示考点导入需上传考试和结果文件</span>
</template>
</el-alert>
<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="label" label="值" width="90" />
<el-table-column prop="rate" label="权值" width="180" />
</el-table>
</div>
<!-- 弹框 -->
<el-dialog
title="考点设置"
v-model="kaoDialogVisible"
width="60%"
:close-on-click-modal="false"
class="custom-dialog"
draggable
>
<!-- 可滚动容器 -->
<div style="height: 400px; overflow: hidden; display: flex; gap: 16px;">
<div style="flex: 1; overflow: auto; border: 1px solid #eee; padding: 8px;">
<h3>答案树</h3>
<el-tree
ref="treeRef"
:data="answerList"
:props="treeProps"
node-key="key"
default-expand-all
:render-content="renderTreeNode"
@node-click="handleNodeClick"
style="min-width: 300px;"
/>
</div>
<div style="flex: 1; overflow: auto; border: 1px solid #eee; padding: 8px;">
<h3>考生表格右侧</h3>
<el-table :data="flatPointList" style="width: 100%; min-width: 300px;">
<el-table-column prop="label" label="值" />
<el-table-column prop="rate" label="权值">
<template #default="{ row }">
<el-input
v-model="row.rate"
size="small"
style="width: 100px;"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="60">
<template #default="{ row }">
<span
@click="removePoint(row)"
style="cursor: pointer; font-weight: bold; font-size: 18px;"
title="点击删除"
></span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 按钮放容器外 -->
<div style="margin-top: 12px; text-align: center;">
<el-button type="primary" @click="setKaodianRow">导入考点</el-button>
</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">
<p>试题附件</p>
</div>
</template>
<!-- 提示 -->
<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"
>
<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>
</div>
</div>
</el-scrollbar>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</div>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="FileRef" @success="handleUploadSuccess"/>
<el-dialog
v-model="fileTreeDialogVisible"
title="选择文件"
width="40%"
:close-on-click-modal="false"
draggable
>
<div style="margin-bottom: 10px; font-weight: bold;">答案目录</div>
<el-tree
:data="fileTreeData"
node-key="id"
:props="{ label: 'name', children: 'children' }"
@node-click="handleFileNodeClick"
default-expand-all
/>
<div style="margin: 20px 0 10px; font-weight: bold;">原始目录</div>
<el-tree
:data="fileTreeStu"
node-key="id"
:props="{ label: 'name', children: 'children' }"
@node-click="handleFileNodeStuClick"
default-expand-all
/>
</el-dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { FormRules } from 'element-plus'
import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue'
import * as QuestionApi from '@/api/paper/question'
import FileForm from './components/FileForm.vue';
import { defaultProps, handleTree } from '@/utils/tree'
import * as SpecialtyApi from '@/api/points'
import { toRaw } from 'vue';
defineOptions({ name: 'ChoiceForm' })
// 定义一个缓存对象
const stuListCache: Record<number, any> = {};
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const fileTreeDialogVisible = ref(false);
const fileTreeData = ref<Tree[]>([]);
const fileTreeStu = ref<Tree[]>([]);
const currentEditingRow = ref<any>(null); // 当前点击的行
const formData = ref({
pointNamesVo:'',
chapteridDictTextVo:'',
content: '',
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: 0,
pointNames: '',
subjectName: '',
status: ' ',
keywords: '',
resourceValue: '',
fileUploads: [
{
quId: '',
url: '',
fileType: '0',
fileName: '素材'
}, {
quId: '',
url: '',
fileType: '1',
fileName: '原始'
},
{
quId: '',
url: '',
fileType: '2',
fileName: '结果'
}]
})
function fileTypeFormatter(row, column, cellValue) {
if (cellValue === '0') return '素材文件'
if (cellValue === '1') return '考试文件'
if (cellValue === '2') return '结果文件'
return '未知类型'
}
//考点
const openFileTree = (row: any) => {
console.log("111")
const quId = kaodianData.value.quId;
currentEditingRow.value = row;
fileTreeDialogVisible.value = true;
};
const handleFileNodeClick = (data) => {
const pathArray = findFilePathById(fileTreeData.value, data.id)
if (!pathArray) return
const fullPath = pathArray.join('\\')
if (currentEditingRow.value) {
currentEditingRow.value.content = fullPath
currentEditingRow.value.attribute = data.attribute || ''
}
fileTreeDialogVisible.value = false
}
const handleFileNodeStuClick = (data) => {
const pathArray = findFilePathById(fileTreeStu.value, data.id)
if (!pathArray) return
const fullPath = pathArray.join('\\')
if (currentEditingRow.value) {
currentEditingRow.value.content = fullPath
currentEditingRow.value.attribute = data.attribute || ''
}
fileTreeDialogVisible.value = false
}
const findFilePathById = (
tree: any[],
targetId: number,
path: string[] = []
): string[] | null => {
for (const node of tree) {
const currentPath = [...path, node.name]
if (node.id === targetId) {
return currentPath
}
if (node.children && node.children.length > 0) {
const result = findFilePathById(node.children, targetId, currentPath)
if (result) return result
}
}
return null
}
const kaoDialogVisible = ref(false)
const kaodianData = ref({
quId: '', // 你打开编辑弹窗时会赋值
})
const answerList = ref([]); // 从接口赋值
const pointList = ref([]); // 从接口赋值
const kaodianList = ref([]); // 从接口赋值
const treeProps = {
label: 'key',
children: 'children'
};
function setKao() {
kaoDialogVisible.value = true
}
const confirmKao = async () => {
const treeData = buildTreeFromFlatList(flatPointList.value);
console.log('📦 还原后的树状结构:', JSON.stringify(treeData, null, 2));
console.log(answerList)
const payload = {
quId: kaodianData.value.quId,
questionAnswerList: treeData,
type:'1',
};
const payloadAnswer ={
quId: kaodianData.value.quId,
questionAnswerList:toRaw(answerList.value),
type:'2',
}
QuestionApi.setPsPoint(payload);
QuestionApi.setPsPoint(payloadAnswer);
// const res = await QuestionApi.getListByQuId(kaodianData.value.quId);
// kaodianList.value = res;
kaoDialogVisible.value = false;
};
function buildTreeFromFlatList(flatList) {
const root = [];
flatList.forEach(item => {
const pathAndValue = item.label.split(':'); // 分离路径和值
const pathStr = pathAndValue[0].trim(); // 例如 "图层信息 -> 蒙版 -> 可见"
const value = pathAndValue[1]?.trim(); // 例如 "✅" 或 "true"
const path = pathStr.split('->').map(p => p.trim()); // 拆成 ["图层信息", "蒙版", "可见"]
let current = root;
path.forEach((key, index) => {
let node = current.find(n => n.key === key);
if (!node) {
node = { key };
if (index < path.length - 1) {
node.children = [];
} else {
node.value = value;
node.rate = item.rate;
}
current.push(node);
}
current = node.children || [];
});
});
return root;
}
// 定义右侧表格数据,初始时根据 pointList 构造
const flatPointList = ref<{ key: string; label: string; rate: number }[]>([]);
function flattenPsVoList(list, parentPath = '') {
const result = [];
list.forEach(item => {
const currentPath = parentPath ? `${parentPath} -> ${item.key}` : item.key;
if (item.children && item.children.length > 0) {
result.push(...flattenPsVoList(item.children, currentPath));
} else {
const symbol = item.value === true ? '✅' : item.value === false ? '❌' : item.value;
result.push({
key: item.key, // 一定要有 key
label: `${currentPath}: ${symbol}`,
rate: 1,
});
}
});
return result;
}
// 可自定义树节点渲染显示key + value
function renderTreeNode(h, { node, data }) {
return h(
'span',
{
ondblclick: () => handleNodeClick(data,node), // 手动绑定双击
style: {
cursor: 'pointer'
}
},
`${data.key}${data.value ? ': ' + data.value : ''}`);
}
const setKaodianRow =async () => {
const uploads = formData.value.fileUploads;
console.log(formData.value.fileUploads+"formData.value.fileUploads")
if (!uploads[1].url || !uploads[2].url) {
ElMessage.error('请先上传两个文件再导入考点');
return;
}
const fileUrl1 = uploads[1].url;
const fileUrl2 = uploads[2].url;
console.log('导入考点的文件地址为:', fileUrl1, fileUrl2);
const params = {
shucaiPath: fileUrl1,
answerPath : fileUrl2 // 如果不传可以是空字符串,也可以删除这个字段(根据后端是否必填)
};
const res = await QuestionApi.getPsPoint(params);
answerList.value = res.answerList; // 注意字段名
pointList.value = res.pointList;
};
const formRules = reactive<FormRules>({
// specialtyName: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
// 左侧试题描述
const leftActiveName = ref('desc')
// 右侧tab
const rightActiveName = ref('analysis')
const rightHandleClick = (tab, e) => {
rightActiveName.value = tab.paneName.value
if (tab.paneName === 'point' ) {
// 发起考点请求
QuestionApi.getPsPointByQuId(kaodianData.value.quId).then(res => {
// res 是接口返回的数据数组
kaodianList.value = res.answerList;
console.log(kaodianList.value +"kaodianList.value ")
});
}
}
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")
}
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 FileRef = ref()
const openForm = (type: string) => {
FileRef.value.open(type)
}
// 媒体文件
const mediumList = ref([] as any)
const multipleMediumSelection = ref([] as any)
const handleMediumSelectionChange = (val: any) => {
multipleMediumSelection.value = val
}
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
rightActiveName.value = 'analysis'
// 修改时,设置数据
if (id) {
kaodianData.value.quId=id;
formLoading.value = true
try {
const res = await QuestionApi.getQuestion(id);
// 默认两个类型
const fileTypes = ['0','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
? res.keywords.split(',').filter(item => item.trim() !== '').map(item => ({ keyword: item.trim() }))
: []
} finally {
formLoading.value = false
}
} else {
resetForm()
console.log(queryParams.pointNamesVo+"queryParams")
console.log(queryParams.chapteridDictTextVo+"queryParams")
console.log(queryParams.pointNames+"queryParams")
console.log(queryParams.chapteridDictText+"queryParams")
formData.value.specialtyName = queryParams.specialtyName
formData.value.courseName = queryParams.courseName
formData.value.subjectName = queryParams.subjectName
formData.value.pointNames=queryParams.pointNamesVo
formData.value.pointNamesVo=queryParams.pointNames
formData.value.chapteridDictText=queryParams.chapteridDictTextVo
formData.value.chapteridDictTextVo=queryParams.chapteridDictText
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
// 转换函数:将大写字母 A-Z 映射为 0-25
const mappedNumber = computed(() => {
const char = radio.value.toUpperCase();
const code = char.charCodeAt(0);
if (code >= 65 && code <= 90) {
return code - 65;
} else {
return '请输入 A-Z 的字母';
}
});
// 选择答案数据
const answerData = ref([]);
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
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'));
}
dialogVisible.value = false;
emit('success');
} finally {
formLoading.value = false;
}
};
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: '',
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: 0,
pointNames: '',
subjectName: '',
status: '0',
keywords: '',
resourceValue: '',
fileUploads: [
{
quId: '',
url: '',
fileType: '0',
fileName: '素材'
},{
quId: '',
url: '',
fileType: '1',
fileName: '原始'
},
{
quId: '',
url: '',
fileType: '2',
fileName: '结果'
}]
}
keywordList.value=[],
formRef.value?.resetFields()
}
const dialogVisiblePoints = ref(false)
// 添加层级信息
const handleTreeWithLevel = (list, level = 1) => {
return list.map(item => {
const node = { ...item, level }
if (item.children && item.children.length > 0) {
node.children = handleTreeWithLevel(item.children, level + 1)
}
return node
})
}
// 只允许点击第三级节点
const treeRef = ref() // 引用 el-tree
const handleNodeClick = (data, node) => {
// 设置选中节点的 ID 和名称
formData.value.pointNames = data.id;
formData.value.pointNamesVo = data.name;
// 获取父节点
const currentNode = treeRef.value?.getNode(data);
const parentNode = currentNode?.parent;
if (parentNode && parentNode.data) {
formData.value.chapteridDictTextVo = parentNode.data.name;
formData.value.chapteridDictText = parentNode.data.id;
} else {
formData.value.chapteridDictText = '';
formData.value.chapteridDictTextVo = '';
}
// 构造一个 label用 key 和 value 显示
const symbol = data.value === true ? '✅' : data.value === false ? '❌' : data.value;
const label = `${data.key}: ${symbol}`;
// 判断是否已存在,避免重复添加
const exists = flatPointList.value.some(item => item.key === data.key);
if (!exists) {
flatPointList.value.push({
key: data.key,
label: label,
rate: 1,
});
}
// 可选:关闭弹窗
dialogVisiblePoints.value = false;
};
const removePoint = (index: number) => {
flatPointList.value.splice(index, 1);
};
const deptList = ref<Tree[]>([]) // 树形结构
/** 获得部门树 */
const getTree = async () => {
const res = await SpecialtyApi.listPoints()
const tree = handleTree(res)
deptList.value = []
deptList.value = handleTreeWithLevel(tree)
}
const openPoints = async () => {
await getTree();
dialogVisiblePoints.value = true;
}
watch(
() => pointList.value,
(val) => {
flatPointList.value = flattenPsVoList(val);
},
{ immediate: true }
);
</script>
<style lang="scss" scoped>
.edit-dialog {
:deep(.el-dialog) {
display: flex;
flex-direction: column;
.el-dialog__header {
border-bottom: 1px solid #ededed;
}
.el-dialog__footer {
border-top: 1px solid #ededed;
}
.el-dialog__body {
flex: 1;
overflow: hidden;
.main {
.el-form {
padding: 10px;
.el-row {
justify-content: space-between;
margin-bottom: 10px;
}
.el-form-item {
width: 100%;
margin-bottom: 0;
align-items: center;
}
}
.edit-bottom {
display: flex;
justify-content: space-between;
padding-bottom: 10px;
.bottom-common {
width: calc(50% - 10px);
.el-tabs {
height: 100%;
.el-tabs__content {
flex: 1;
overflow-y: auto;
.block {
width: 100%;
height: 80%;
overflow: auto;
}
.answer {
.tip {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
padding: 10px 20px;
display: flex;
align-items: center;
> p {
flex-shrink: 0;
}
}
.el-radio-group {
width: 100%;
display: inline-flex;
align-items: flex-start;
font-size: 0;
flex-direction: column;
.options {
width: 100%;
margin-top: 15px;
.content {
display: flex;
.text {
width: 100%;
height: 70px;
border: 1px solid #ededed;
.el-textarea__inner {
resize: none;
}
}
}
.more-btn {
margin-top: 8px;
background-color: #ffffff;
border-color: #007bff;
color: #007bff;
width: auto;
height: auto;
padding: 10px 10px;
}
}
}
}
}
.custom-tabs-label {
display: flex;
align-items: center;
.setting_icon {
width: 16px;
height: 16px;
background: url('@/assets/icon/setting_blue.png') no-repeat center;
background-size: 100%;
margin-left: 3px;
display: none;
}
}
.is-active {
.custom-tabs-label {
.setting_icon {
display: block;
}
}
}
:deep(.ele-pro-table) {
flex: 1;
margin-top: 10px;
.el-table--fit {
height: 100%;
}
}
}
}
}
}
}
}
}
:deep(.tox-tinymce) {
.tox-statusbar {
display: none;
}
}
:deep(.el-table) {
.el-table__header-wrapper {
.el-table__header {
thead {
tr {
th {
background: #ebebeb;
}
}
}
}
}
}
</style>