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

716 lines
20 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>邮件题目编辑
</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="邮件题目" 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-item label="启用状态">
<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-form>
</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>
<!-- 对话框模式 -->
<div v-else 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="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="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.pointNames" placeholder="请选择知识点" readonly />
</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-row>
<el-col :span="24">
<el-form-item label="试题内容" prop="content">
<Editor v-model="formData.content" :height="150" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="试题解析" prop="analysis">
<Editor v-model="formData.analysis" :height="150" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading"> </el-button>
</div>
</template>
</Dialog>
</div>
</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 } from '@element-plus/icons-vue'
import { Dialog } from '@/components/Dialog'
defineOptions({ name: 'EmailForm' })
const { t } = useI18n()
// 组件参数
interface Props {
isIndependent?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isIndependent: false
})
// Tauri 独立窗口相关
let WebviewWindow: any = null
const isDesktop = ref(false)
// 基础数据
const activeName = ref('common')
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
// 表单数据
const formData = ref({
content: '',
specialtyName: '',
courseName: '',
chapteridDictText: '',
subjectName: '',
pointNames: '',
quType: '邮件题目',
quLevel: 0,
status: '0',
analysis: ''
})
// 表单引用和规则
const formRef = ref()
const formRules = reactive({
content: [{ required: true, message: '请输入题目内容', trigger: 'blur' }],
quLevel: [{ required: true, message: '请选择难度', trigger: 'change' }]
})
// 检测是否为桌面环境
onMounted(async () => {
try {
if (typeof window !== 'undefined' && (window as any).__TAURI__) {
const { WebviewWindow: TauriWebviewWindow } = await import('@tauri-apps/api/webviewWindow')
WebviewWindow = TauriWebviewWindow
isDesktop.value = true
}
} catch (error) {
console.log('非桌面环境')
}
})
// 计算属性
const uploadAction = computed(
() => import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
)
const uploadHeaders = computed(() => ({ Authorization: 'Bearer ' + getAccessToken() }))
// Tab 切换
const handleClick = (tab: any) => {
console.log('切换到tab:', tab.name)
}
// 表单提交
const submitForm = async () => {
// 独立窗口模式下,基于表单数据进行简单验证
if (props.isIndependent) {
// 基本必填项检查
if (!formData.value.content) {
ElMessage.warning('请填写题目内容')
return
}
} else {
// 对话框模式下使用formRef验证
if (!formRef.value) return
const valid = await formRef.value.validate()
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 Email 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('email-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 handleCancel = async () => {
if (props.isIndependent) {
try {
if (isDesktop.value && WebviewWindow) {
const currentWindow = WebviewWindow.getCurrent()
await currentWindow.close()
}
} catch (error) {
console.error('关闭窗口失败:', error)
}
} else {
dialogVisible.value = false
}
}
// 事件定义
const emit = defineEmits<{
success: []
}>()
// 打开表单
const open = async (type: string, id?: number, rowData?: any) => {
if (!props.isIndependent) {
dialogVisible.value = true
}
formType.value = type
dialogTitle.value = type === 'create' ? '新增邮件题目' : '修改邮件题目'
// 重置表单
await resetForm()
if (rowData) {
// 从父组件传入的数据
Object.assign(formData.value, {
specialtyName: rowData.specialtyName || '',
courseName: rowData.courseName || '',
chapteridDictText: rowData.chapteridDictText || '',
subjectName: rowData.subjectName || '',
pointNames: rowData.pointNames || ''
})
}
if (id) {
formLoading.value = true
try {
const res = await QuestionApi.getQuestion(id)
Object.assign(formData.value, res)
} finally {
formLoading.value = false
}
}
}
// 重置表单
const resetForm = async () => {
formData.value = {
content: '',
specialtyName: '',
courseName: '',
chapteridDictText: '',
subjectName: '',
pointNames: '',
quType: '邮件题目',
quLevel: 0,
status: '0',
analysis: ''
}
await nextTick()
formRef.value?.resetFields()
}
// 暴露方法
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 {
.el-form-item {
margin-bottom: 20px !important;
:deep(.el-form-item__label) {
font-weight: 600 !important;
color: var(--el-text-color-primary) !important;
}
:deep(.el-input__wrapper) {
border-radius: 6px !important;
}
:deep(.el-select) {
width: 200px !important;
.el-input__wrapper {
border-radius: 6px !important;
}
}
}
}
}
}
}
}
}
}
.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;
}
}
}
}
}
// 对话框模式样式
.edit-dialog {
.main {
padding: 20px;
.el-form {
.el-form-item {
margin-bottom: 20px;
:deep(.el-form-item__label) {
font-weight: 600;
color: var(--el-text-color-primary);
}
:deep(.el-input__wrapper) {
border-radius: 6px;
}
:deep(.el-select) {
width: 100%;
.el-input__wrapper {
border-radius: 6px;
}
}
}
}
}
:deep(.el-dialog) {
border-radius: 8px;
.el-dialog__header {
padding: 20px 24px 16px;
border-bottom: 1px solid var(--el-border-color);
.el-dialog__title {
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.el-dialog__body {
padding: 24px;
}
.el-dialog__footer {
padding: 16px 24px 20px;
border-top: 1px solid var(--el-border-color);
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
.el-button {
height: 36px;
padding: 0 20px;
border-radius: 6px;
font-weight: 500;
}
}
}
}
}
// Tab样式
:deep(.demo-tabs) {
.el-tabs__header {
margin-bottom: 20px;
.el-tabs__nav-wrap {
&::after {
background-color: var(--el-border-color);
}
}
.el-tabs__item {
font-weight: 500;
color: var(--el-text-color-secondary);
&.is-active {
color: var(--el-color-primary);
font-weight: 600;
}
}
.el-tabs__active-bar {
background-color: var(--el-color-primary);
}
}
}
</style>