Files
pengchen-exam-vue/src/views/paper/question/WpsWordForm.vue
2025-06-05 16:21:57 +08:00

879 lines
28 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">
<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="pointNames">
<el-input v-model="formData.pointNames" 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 label="简单" :value="'0'" />
<el-option label="一般" :value="'1'" />
<el-option label="困难" :value="'2'" />
</el-select> -->
<el-select v-model="formData.quLevel" placeholder="请选择题型难度">
<el-option
v-for="dict in getStrDictOptions(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="chapteridDictText">
<el-input
v-model="formData.chapteridDictText"
placeholder="请输入章节名称"
disabled
/>
</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-col :span="12">
<el-form-item label="审核状态" prop="audit">
<el-select v-model="formData.audit" placeholder="请选择审核状态">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.QUESTION_AUDIT)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</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="150px" />
</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="150px" />
</div>
</el-tab-pane>
<el-tab-pane name="answer">
<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-button @click="addWordInfo">添加</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<!-- <el-button type="danger">删除</el-button> -->
<div class="block">
<el-table
v-loading="loading"
:data="list"
@selection-change="handleSelectionChange"
>
<!-- <el-table-column type="selection" width="55" /> -->
<el-table-column label="考点" align="center" prop="contentIn" width="360px" />
<el-table-column label="权值" align="center" prop="scoreRate" width="100px" />
<el-table-column label="操作" align="center" width="100px">
<template #default="scope">
<el-button type="primary" link @click="handleDelete(scope.row)">
<Icon icon="ep:delete" />删除
</el-button>
</template>
</el-table-column>
</el-table>
</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 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="documentList" 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="150">
<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="dialogFormVisibleWordInfo" title="考点设置" width="400px">
<el-skeleton :rows="5" animated v-if="wordPointsList.length < 0 && isLoading" />
<el-tree
style="max-width: 600px"
:data="wordPointsList"
:props="defaultProps"
:expand-on-click-node="false"
@node-click="handleNodelClick"
/>
</el-dialog>
<el-dialog v-model="dialogFormVisibleWordInfos" :title="titles" width="300px">
<el-tree
style="max-width: 600px"
:data="wordPointsInfoList"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
@check-change="handleCheckChange"
/>
<template #footer>
<el-button type="primary" @click="submitWordPoints"> </el-button>
<el-button @click="dialogFormVisibleWordInfos = false"> </el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import { FormRules } from 'element-plus'
import * as QuestionApi from '@/api/paper/question'
import * as WordApi from '@/api/wps/word'
import FileForm from './components/FileForm.vue'
import WordInfo from './components/WordInfo.vue'
import WordInfos from './components/WordInfos.vue'
import { defaultProps, handleTree } from '@/utils/tree'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'WpsWordFrom' })
const wordPointsList = ref<Tree[]>([]) // 树形结构
const wordPointsInfoList = ref<Tree[]>([]) // 树形结构
const list = ref([]) // 列表的数
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const loading = ref(false) // 列表的加载中
const dialogVisible = ref(false) // 弹窗的是否展示
const isLoading = ref(false)
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
content: '',
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: '',
pointNames: '',
audit: '',
subjectName: '',
status: ' ',
resourceValue: '',
answerList: [
{
image: '',
content: '',
contentIn: '',
scoreRate: ''
}
],
fileUploads: [
{
quId: '',
url: '',
fileType: '0',
fileName: ''
},
{
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}
]
})
const wordPointsFun = ref({
chineseName: '',
function: ''
})
const wordPoints = ref({
name: '',
englishName: '',
filePath: '',
type: '',
belongTo: '',
isboo: '',
function: ''
})
let wordPointsInfosList: (typeof wordPoints)[] = []
function fileTypeFormatter(row, column, cellValue) {
if (cellValue === '0') return '素材文件(上传ZIP)'
if (cellValue === '1') return '考试文件'
if (cellValue === '2') return '结果文件'
return '未知类型'
}
const documentList = ref([
{
quId: '',
url: '',
fileType: '0',
fileName: ''
},
{
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}
])
const dialogFormVisibleWordInfo = ref(false)
const dialogFormVisibleWordInfos = ref(false)
const nodeFunction = ref('')
const titles = ref('')
const englishName = ref('')
const filePath = ref('')
const upFilePath = ref('')
const functionList = ref<string[]>([])
const wordData = reactive({
chineseName: '',
englishName: '',
filePath: '',
function: [] as string[]
})
const handleCheckChange = (data: Tree, checked: boolean, indeterminate: boolean) => {
// if (checked || indeterminate) {
// wordPoints.value.belongTo = data.belongTo
// wordPoints.value.parameter = data.isboo
// wordPoints.value.type = data.type
// wordPointsFun.value = {
// chineseName: '',
// function: ''
// }
// wordPointsFun.value.chineseName = data.toChinese
// wordPointsFun.value.function = data.nodeFunction
// wordPoints.value.function.push(cloneDeep(wordPointsFun.value))
// console.log(wordPoints)
// }
if (data.titleType == '2') {
wordPoints.value.name = chineseName.value + data.toChinese
wordPoints.value.filePath = filePath.value
wordPoints.value.function = englishNames.value + data.nodeFunction
wordPoints.value.englishName = englishNames.value
wordPoints.value.belongTo = data.belongTo
wordPoints.value.isboo = data.isboo
wordPoints.value.type = data.type
wordPointsInfosList.push(cloneDeep(wordPoints.value))
}
console.log(data, checked, indeterminate)
}
const addWordInfo = async () => {
var filePath = ''
for (var i = 0; i < documentList.value.length; i++) {
if (documentList.value[i].fileType == '2') {
filePath = documentList.value[i].url
}
}
// http://115.120.213.238:9000/exam/9f7d8f5d7c68cc2bfd03a23c19045efe7ba13a4bebeb833abece146908bcd0c6.docx documentList.value[1].url
if (filePath == '' || filePath == null) {
return
}
dialogFormVisibleWordInfo.value = true
isLoading.value = true
if (wordPointsList.value.length <= 0) {
const res = await WordApi.getWordInfo(filePath)
isLoading.value = false
wordPointsList.value = []
wordPointsList.value.push(...handleTree(res))
}
}
const queryParams = reactive({
nodeFunction: undefined
})
const chineseName = ref('')
const nodeFunctions = ref('')
const englishNames = ref('')
const handleNodelClick = async (row: any) => {
console.log(row)
queryParams.nodeFunction = row.selectName
chineseName.value = '【' + row.name + '】'
filePath.value = row.filePath
nodeFunctions.value = row.selectName
englishNames.value = row.englishName
const res = await WordApi.getWordInfos(queryParams)
wordPointsInfoList.value = []
wordPointsInfoList.value.push(...handleTree(res))
dialogFormVisibleWordInfos.value = true
}
const handleDelete = (row) => {
console.log(row)
for (let i = 0; i < list.value.length; i++) {
if (row.content == list.value[i].content) {
list.value.splice(i, 1)
}
}
}
const submitWordPoints = async () => {
const res = await WordApi.getWordListInfos(wordPointsInfosList)
wordPoints.value = {
name: '',
englishName: '',
filePath: '',
type: '',
belongTo: '',
isboo: '',
function: ''
}
wordPointsInfosList = []
for (let i = 0; i < res.length; i++) {
var indexFlag = false
for (let x = 0; x < list.value.length; x++) {
if (res[i].content == list.value[x].content) {
// 如果存在相同的数据话 不进入
indexFlag = true
}
}
if (!indexFlag) {
list.value.push(res[i])
}
}
dialogFormVisibleWordInfo.value = false
dialogFormVisibleWordInfos.value = false
wordPointsList.value = []
}
const formRules = reactive<FormRules>({
status: [{ required: true, message: '启用状态必填', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
// 左侧试题描述
const leftActiveName = ref('desc')
// 右侧tab
const rightActiveName = ref('annex')
const rightHandleClick = (tab, e) => {
rightActiveName.value = tab.paneName.value
}
// 关键字
const multipleKeywordSelection = ref([] as any)
const handleKeywordSelectionChange = (val: any) => {
multipleKeywordSelection.value = val
}
const selections = ref([])
const handleSelectionChange = (rows) => {
selections.value = rows
}
const keyWord = ref([null])
/** 添加/修改操作 */
const FileRef = ref()
const openForm = (type: string) => {
FileRef.value.open(type)
}
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 mediumList = ref([] as any)
const multipleMediumSelection = ref([] as any)
const handleMediumSelectionChange = (val: any) => {
multipleMediumSelection.value = val
}
const fileList = []
const multipleDocumentSelection = ref([] as any)
const handleDocumentSelectionChange = (val: any) => {
multipleDocumentSelection.value = val
}
const handleUploadSuccess = ({ url, fileType }) => {
const index = documentList.value.findIndex((item) => item.fileType === fileType)
if (index !== -1) {
documentList.value[index].url = url
}
}
/** 打开弹窗 */
const open = async (queryParams: any, type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
const res = await QuestionApi.getQuestion(id)
formData.value = res
console.log(formData.value)
list.value = formData.value.answerList
documentList.value = res.fileUploads
} 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
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 emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
formData.value.answerList = list.value
formData.value.fileUploads = documentList.value
const values = Object.values(formData)
console.log(values)
// 校验表单
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
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
content: '',
specialtyName: '',
courseName: '',
quBankName: '',
required: '',
chapteridDictText: '',
analysis: '',
quLevel: '',
pointNames: '',
audit: '',
subjectName: '',
status: '0',
resourceValue: '',
answerList: [
{
image: '',
content: '',
contentIn: '',
scoreRate: ''
}
],
fileUploads: [
{
quId: '',
url: '',
fileType: '0',
fileName: ''
},
{
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}
]
}
documentList.value = [
{
quId: '',
url: '',
fileType: '0',
fileName: ''
},
{
quId: '',
url: '',
fileType: '1',
fileName: ''
},
{
quId: '',
url: '',
fileType: '2',
fileName: ''
}
]
list.value = []
formRef.value?.resetFields()
}
</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: 200px;
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>