Files
pengcheng-exam-teacher/src/views/paper/question/WpsXlsxForm.vue
2025-08-15 15:33:15 +08:00

937 lines
27 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 v-if="isIndependent" class="independent-form">
<div class="independent-header">
<h2>{{ dialogTitle }}</h2>
</div>
<div class="independent-content">
<div class="program-edit">
<el-scrollbar height="calc(100vh - 150px)">
<div class="main">
<div class="tabsTip">
<el-icon><InfoFilled /></el-icon>WPS表格处理题目编辑
</div>
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="基本内容" name="common">
<div class="line">
<div class="title-text">试题题目</div>
<div class="buttons">
<ElButton type="primary" plain>
<el-icon><Document /></el-icon>
更换题目模板
</ElButton>
<ElButton type="primary" plain>
<el-icon><Search /></el-icon>
预览
</ElButton>
</div>
<div class="block">
<Editor
v-model="formData.content"
:height="150"
:key="`editor-content-${formType}`"
/>
</div>
<div class="buttons">
<ElButton type="primary" plain>
<el-icon><Paperclip /></el-icon>
添加附件
</ElButton>
</div>
</div>
<div class="line">
<div class="title-text">基本信息</div>
<el-form
label-width="80px"
:inline="true"
label-position="right"
class="formList"
>
<el-form-item label="专业">
<el-input v-model="formData.specialtyName" readonly />
</el-form-item>
<el-form-item label="课程">
<el-input v-model="formData.courseName" readonly />
</el-form-item>
<el-form-item label="章节">
<el-input v-model="formData.chapteridDictText" readonly />
</el-form-item>
<el-form-item label="科目">
<el-input v-model="formData.subjectName" readonly />
</el-form-item>
<el-form-item label="知识点">
<el-input v-model="formData.pointNames" readonly />
</el-form-item>
<el-form-item label="题目类型">
<el-input v-model="formData.quType" placeholder="WPS表格处理题目" readonly />
</el-form-item>
<el-form-item label="难度">
<el-select
v-model="formData.quLevel"
placeholder="请选择难度"
style="width: 200px"
>
<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-form>
</div>
</el-tab-pane>
<el-tab-pane label="文件管理" name="files">
<div class="line">
<div class="title-text">上传Excel文件</div>
<div class="buttons">
<el-upload
ref="uploadRef"
:action="uploadAction"
:headers="uploadHeaders"
:data="{ type: 'excel' }"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:show-file-list="false"
accept=".xls,.xlsx"
>
<ElButton type="primary">
<el-icon><Upload /></el-icon>
上传Excel文件
</ElButton>
</el-upload>
<ElButton
type="success"
@click="parseExcelFile"
:disabled="!formData.excelFileUrl"
>
<el-icon><View /></el-icon>
解析文件
</ElButton>
</div>
<div class="file-info" v-if="formData.excelFileUrl">
<p>已上传文件: {{ formData.excelFileName }}</p>
<p>文件路径: {{ formData.excelFileUrl }}</p>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-scrollbar>
</div>
</div>
<div class="independent-footer">
<el-button @click="submitForm" :loading="formLoading" :disabled="formLoading">
{{ formLoading ? '提交中...' : '确 定' }}
</el-button>
<el-button @click="handleCancel"> </el-button>
</div>
</div>
<!-- 对话框模式 -->
<el-dialog
v-else
v-model="dialogVisible"
:title="dialogTitle"
width="80%"
:before-close="handleCancel"
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
class="question-form"
v-loading="formLoading"
>
<el-tabs v-model="activeName" type="border-card" @tab-click="handleClick">
<!-- 基本信息 -->
<el-tab-pane label="基本信息" name="common">
<div class="form-section">
<div class="section-title">题目信息</div>
<div class="form-grid">
<el-form-item label="专业" prop="specialtyName">
<el-input v-model="formData.specialtyName" readonly />
</el-form-item>
<el-form-item label="课程" prop="courseName">
<el-input v-model="formData.courseName" readonly />
</el-form-item>
<el-form-item label="章节" prop="chapteridDictText">
<el-input v-model="formData.chapteridDictText" readonly />
</el-form-item>
<el-form-item label="科目" prop="subjectName">
<el-input v-model="formData.subjectName" readonly />
</el-form-item>
<el-form-item label="知识点" prop="pointNames">
<el-input v-model="formData.pointNames" readonly />
</el-form-item>
<el-form-item label="题目类型" prop="quType">
<el-input v-model="formData.quType" placeholder="WPS表格处理题目" readonly />
</el-form-item>
<el-form-item label="难度" prop="quLevel">
<el-select v-model="formData.quLevel" placeholder="请选择难度" style="width: 60px">
<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>
</div>
</div>
<div class="form-section">
<div class="section-title">题目内容</div>
<el-form-item label="题目描述" prop="content">
<Editor v-model="formData.content" :height="200" />
</el-form-item>
</div>
</el-tab-pane>
<!-- 文件管理 -->
<el-tab-pane label="文件管理" name="files">
<div class="form-section">
<div class="section-title">Excel文件上传</div>
<el-form-item label="上传Excel文件">
<el-upload
ref="uploadRef"
:action="uploadAction"
:headers="uploadHeaders"
:data="{ type: 'excel' }"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:show-file-list="false"
accept=".xls,.xlsx"
>
<el-button type="primary">
<el-icon><Upload /></el-icon>
选择Excel文件
</el-button>
</el-upload>
</el-form-item>
<el-form-item label="文件信息" v-if="formData.excelFileUrl">
<div class="file-info">
<p><strong>文件名:</strong> {{ formData.excelFileName }}</p>
<p><strong>文件路径:</strong> {{ formData.excelFileUrl }}</p>
<el-button type="success" @click="parseExcelFile" size="small">
<el-icon><View /></el-icon>
解析文件内容
</el-button>
</div>
</el-form-item>
</div>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="submitForm" :loading="formLoading" :disabled="formLoading">
{{ formLoading ? '提交中...' : '确 定' }}
</el-button>
<el-button @click="handleCancel"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { getAccessToken } from '@/utils/auth'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as QuestionApi from '@/api/paper/question'
import { Editor } from '@/components/Editor'
import { InfoFilled, Document, Search, Paperclip, Upload, View } from '@element-plus/icons-vue'
defineOptions({ name: 'WpsXlsxForm' })
const { t } = useI18n()
// 组件参数
interface Props {
isIndependent?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isIndependent: false
})
const emit = defineEmits(['success'])
// 基础状态
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formType = ref('')
const formLoading = ref(false)
const formRef = ref()
const activeName = ref('common')
// 表单数据
const formData = ref({
quId: undefined,
content: '',
specialtyName: '',
courseName: '',
subjectName: '',
chapteridDictText: '',
pointNames: '',
chapteridDictTextVo: '',
pointNamesVo: '',
quType: 'WPS表格处理题目',
quLevel: '',
excelFileUrl: '',
excelFileName: '',
parsedContent: ''
})
// 表单验证规则
const formRules = reactive({
content: [{ required: true, message: '请输入题目内容', trigger: 'blur' }]
})
// 上传相关
const uploadAction = computed(() => import.meta.env.VITE_UPLOAD_URL)
const uploadHeaders = computed(() => ({
Authorization: 'Bearer ' + getAccessToken()
}))
// 独立窗口相关
let isListeningForInit = false
onMounted(async () => {
if (props.isIndependent && !isListeningForInit) {
isListeningForInit = true
// 发送窗口准备就绪信号
try {
const { emit: emitEvent } = await import('@tauri-apps/api/event')
await emitEvent('wps-xlsx-window-ready', { message: 'Excel form window is ready' })
console.log('Excel form window ready signal sent')
} catch (error) {
console.error('Failed to send window ready signal:', error)
}
// 监听初始化事件
try {
const { listen } = await import('@tauri-apps/api/event')
await listen('init-wps-xlsx-form', (event: any) => {
console.log('Received init-wps-xlsx-form event:', event.payload)
const { queryParams, type, id } = event.payload
nextTick(() => {
open(queryParams, type, id)
})
})
// 请求初始化数据
const { emit: emitEvent } = await import('@tauri-apps/api/event')
await emitEvent('request-wps-xlsx-init', { message: 'Requesting Excel form initialization' })
console.log('Requested Excel form initialization')
} catch (error) {
console.error('Failed to set up Tauri event listeners:', error)
}
}
})
// 表单操作
const resetForm = () => {
formData.value = {
quId: undefined,
content: '',
specialtyName: '',
courseName: '',
subjectName: '',
chapteridDictText: '',
pointNames: '',
chapteridDictTextVo: '',
pointNamesVo: '',
quType: 'WPS表格处理题目',
quLevel: '',
excelFileUrl: '',
excelFileName: '',
parsedContent: ''
}
if (formRef.value) {
formRef.value.resetFields()
}
}
const handleClick = (tab: any) => {
activeName.value = tab.name
}
const handleCancel = async () => {
if (props.isIndependent) {
// 独立窗口模式:发送取消事件并关闭窗口
try {
const { emit } = await import('@tauri-apps/api/event')
const { getCurrentWindow } = await import('@tauri-apps/api/window')
await emit('wps-xlsx-form-cancel')
const appWindow = getCurrentWindow()
await appWindow.close()
} catch (error) {
console.error('Failed to close independent window:', error)
}
} else {
// 对话框模式
dialogVisible.value = false
}
}
// 文件上传处理
const beforeUpload = (file: File) => {
const isExcel =
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.type === 'application/vnd.ms-excel'
if (!isExcel) {
ElMessage.error('只能上传Excel文件!')
return false
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
ElMessage.error('文件大小不能超过10MB!')
return false
}
return true
}
const handleUploadSuccess = (response: any) => {
if (response.code === 0) {
formData.value.excelFileUrl = response.data.url
formData.value.excelFileName = response.data.fileName
ElMessage.success('文件上传成功!')
} else {
ElMessage.error('文件上传失败: ' + response.msg)
}
}
const parseExcelFile = async () => {
if (!formData.value.excelFileUrl) {
ElMessage.warning('请先上传Excel文件')
return
}
try {
formLoading.value = true
// 临时注释掉API调用直接显示成功
// const response = await WpsApi.getXlsxDataInfo({
// filePath: formData.value.excelFileUrl
// })
formData.value.parsedContent = '文件解析成功 - ' + formData.value.excelFileName
ElMessage.success('Excel文件解析成功!')
} catch (error) {
console.error('解析Excel文件失败:', error)
ElMessage.error('Excel文件解析失败')
} finally {
formLoading.value = false
}
}
// 提交表单
const submitForm = async () => {
console.log('WpsXlsxForm submitForm called, isIndependent:', props.isIndependent)
// 独立窗口模式下,基于表单数据进行简单验证
if (props.isIndependent) {
// 基本必填项检查
if (!formData.value.content) {
ElMessage.warning('请填写题目内容')
return
}
} else {
// 对话框模式下使用formRef验证
if (!formRef.value) return
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
}
formLoading.value = true
try {
const dataToSubmit = JSON.parse(JSON.stringify(formData.value))
// 确保基础必需字段
dataToSubmit.quLevel = dataToSubmit.quLevel || 0
dataToSubmit.status = dataToSubmit.status || '0'
// 确保字符串字段不为undefined
dataToSubmit.specialtyName = dataToSubmit.specialtyName || ''
dataToSubmit.courseName = dataToSubmit.courseName || ''
dataToSubmit.subjectName = dataToSubmit.subjectName || ''
dataToSubmit.pointNames = dataToSubmit.pointNames || ''
dataToSubmit.chapteridDictText = dataToSubmit.chapteridDictText || ''
dataToSubmit.analysis = dataToSubmit.analysis || ''
dataToSubmit.resourceValue = dataToSubmit.resourceValue || ''
console.log('Submitting WPS Excel data:', dataToSubmit)
if (formType.value === 'create') {
await QuestionApi.addQuestion(dataToSubmit)
ElMessage.success(t('common.createSuccess'))
} else {
await QuestionApi.editQuestion(dataToSubmit)
ElMessage.success(t('common.updateSuccess'))
}
if (props.isIndependent) {
// 独立窗口模式:发送成功事件并关闭窗口
try {
const { emit: tauriEmit } = await import('@tauri-apps/api/event')
const { getCurrentWindow } = await import('@tauri-apps/api/window')
await tauriEmit('wps-xlsx-form-success')
const appWindow = getCurrentWindow()
await appWindow.close()
} catch (error) {
console.error('Failed to close independent window:', error)
}
} else {
// 对话框模式
dialogVisible.value = false
emit('success')
}
} catch (error: any) {
console.error('Submit error:', error)
// 更详细的错误处理
let errorMessage = '提交失败'
if (error?.response?.data?.msg) {
errorMessage += '' + error.response.data.msg
} else if (error?.message) {
errorMessage += '' + error.message
} else if (typeof error === 'string') {
errorMessage += '' + error
} else {
errorMessage += ':未知错误'
}
ElMessage.error(errorMessage)
} finally {
formLoading.value = false
}
}
// 打开表单
const open = async (queryParams: any, type: string, id?: number) => {
if (!props.isIndependent) {
dialogVisible.value = true
}
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
const res = await QuestionApi.getQuestion(id)
formData.value = { ...formData.value, ...res }
} finally {
formLoading.value = false
}
} else {
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 })
</script>
<style lang="scss" scoped>
// 独立窗口模式样式
.independent-form {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--app-content-bg-color);
overflow: hidden;
.independent-header {
background: var(--el-bg-color);
border-bottom: 1px solid var(--el-border-color);
padding: 16px 24px;
box-shadow: var(--el-box-shadow-light);
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.independent-content {
flex: 1;
overflow: hidden;
background: var(--el-bg-color);
margin: 16px 16px 0 16px;
border-radius: 8px;
box-shadow: var(--el-box-shadow);
.program-edit {
height: 100%;
padding: 20px;
.main {
height: 100%;
position: relative;
.tabsTip {
position: absolute;
top: 0;
right: 0;
color: #4f9dfd;
line-height: 100%;
text-align: center;
align-items: center;
display: flex;
background: rgba(79, 157, 253, 0.1);
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
z-index: 10;
}
.el-tabs {
height: 100%;
padding-top: 40px; // 为tabsTip预留空间
:deep(.el-tabs__header) {
margin-bottom: 20px;
}
:deep(.el-tabs__content) {
height: calc(100% - 60px);
overflow-y: auto;
padding-right: 8px;
.line {
margin-bottom: 24px;
.title-text {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: var(--el-text-color-primary);
padding-bottom: 8px;
border-bottom: 2px solid var(--el-border-color);
}
.buttons {
margin: 16px 0;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.block {
padding: 16px;
background: var(--el-fill-color-lighter);
border-radius: 6px;
border: 1px solid var(--el-border-color);
}
.formList {
background: var(--el-fill-color-lighter);
padding: 20px;
border-radius: 6px;
border: 1px solid var(--el-border-color);
}
.file-info {
background: rgba(79, 157, 253, 0.1);
border: 1px solid #4f9dfd;
border-radius: 6px;
padding: 16px;
color: #4f9dfd;
p {
margin: 4px 0;
font-size: 14px;
}
}
}
}
}
}
}
}
.independent-footer {
background: #ffffff !important;
border-top: 1px solid #e4e7ed !important;
margin: 0 16px 16px 16px !important;
padding: 16px 24px !important;
display: flex !important;
justify-content: flex-end !important;
gap: 12px !important;
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1) !important;
z-index: 1000 !important;
border-radius: 0 0 8px 8px !important;
.el-button {
border-radius: 6px !important;
font-weight: 500 !important;
padding: 10px 20px !important;
font-size: 14px !important;
transition: all 0.3s !important;
min-width: 80px !important;
height: 36px !important;
&:first-child {
background: #409eff !important;
border-color: #409eff !important;
color: #ffffff !important;
&:hover {
background: #66b1ff !important;
border-color: #66b1ff !important;
color: #ffffff !important;
}
&:active {
background: #3a8ee6 !important;
border-color: #3a8ee6 !important;
}
}
&:last-child {
background: #ffffff !important;
border: 1px solid #dcdfe6 !important;
color: #606266 !important;
&:hover {
background: #ecf5ff !important;
border-color: #409eff !important;
color: #409eff !important;
}
&:active {
background: #e6f7ff !important;
border-color: #3a8ee6 !important;
color: #3a8ee6 !important;
}
}
}
}
}
/* 对话框模式样式 */
.question-form {
max-height: 60vh;
overflow-y: auto;
}
.form-section {
margin-bottom: 24px;
padding: 20px;
background: var(--el-fill-color-lighter);
border-radius: 8px;
border: 1px solid var(--el-border-color);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid var(--el-border-color);
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.file-info {
background: rgba(79, 157, 253, 0.1);
border: 1px solid #4f9dfd;
border-radius: 6px;
padding: 16px;
color: #4f9dfd;
p {
margin: 4px 0;
font-size: 14px;
}
}
.dialog-footer {
display: flex !important;
justify-content: flex-end !important;
gap: 12px !important;
padding: 16px 0 !important;
.el-button {
border-radius: 6px !important;
font-weight: 500 !important;
padding: 10px 20px !important;
font-size: 14px !important;
transition: all 0.3s !important;
min-width: 80px !important;
height: 36px !important;
&:first-child {
background: #409eff !important;
border-color: #409eff !important;
color: #ffffff !important;
&:hover {
background: #66b1ff !important;
border-color: #66b1ff !important;
color: #ffffff !important;
}
&:active {
background: #3a8ee6 !important;
border-color: #3a8ee6 !important;
}
}
&:last-child {
background: #ffffff !important;
border: 1px solid #dcdfe6 !important;
color: #606266 !important;
&:hover {
background: #ecf5ff !important;
border-color: #409eff !important;
color: #409eff !important;
}
&:active {
background: #e6f7ff !important;
border-color: #3a8ee6 !important;
color: #3a8ee6 !important;
}
}
}
}
/* 全局按钮样式强制覆盖 */
.el-button {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 6px !important;
font-weight: 500 !important;
padding: 10px 20px !important;
font-size: 14px !important;
transition: all 0.3s !important;
min-width: 80px !important;
height: 36px !important;
border: 1px solid !important;
cursor: pointer !important;
text-decoration: none !important;
outline: none !important;
opacity: 1 !important;
visibility: visible !important;
&.el-button--primary {
background: #409eff !important;
border-color: #409eff !important;
color: #ffffff !important;
&:hover {
background: #66b1ff !important;
border-color: #66b1ff !important;
color: #ffffff !important;
}
&:active {
background: #3a8ee6 !important;
border-color: #3a8ee6 !important;
}
}
&.el-button--default {
background: #ffffff !important;
border-color: #dcdfe6 !important;
color: #606266 !important;
&:hover {
background: #ecf5ff !important;
border-color: #409eff !important;
color: #409eff !important;
}
&:active {
background: #e6f7ff !important;
border-color: #3a8ee6 !important;
color: #3a8ee6 !important;
}
}
}
/* 全局样式 */
:deep(.el-tabs) {
.el-tabs__header {
position: sticky;
top: 0;
z-index: 100;
background: var(--el-bg-color);
border-bottom: 1px solid var(--el-border-color-lighter);
margin-bottom: 0;
}
.el-tabs__item {
padding: 0 20px;
height: 40px;
line-height: 40px;
font-weight: 500;
&.is-active {
color: var(--el-color-primary);
font-weight: 600;
}
}
.el-tabs__active-bar {
height: 3px;
border-radius: 2px;
}
.el-tabs__content {
padding-top: 20px;
}
}
:deep(.el-form-item__label) {
font-weight: 500;
color: var(--el-text-color-primary);
}
:deep(.el-input),
:deep(.el-select) {
width: 100%;
}
:deep(.demo-tabs) {
margin-top: 16px;
}
:deep(.demo-tabs .el-tabs__header) {
margin: 0 0 20px 0;
}
</style>