admin-vue3/src/views/review/meeting/MeetingEdit.vue

395 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<ContentWrap>
<div class="meeting-edit-header">
<span class="page-title">{{ pageTitle }}</span>
</div>
<el-form
ref="formRef"
:model="formData"
:rules="isView ? {} : rules"
label-width="100px"
v-loading="formLoading"
>
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="会议名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入会议名称" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="组织单位" prop="organizationUnit">
<el-input v-model="formData.organizationUnit" placeholder="请输入组织单位" :disabled="isView" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="会议时间" prop="meetingTimeRange">
<el-date-picker
v-model="formData.meetingTimeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm"
value-format="x"
:disabled="isView"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="会议地点" prop="location">
<el-input v-model="formData.location" placeholder="请输入会议地点" :disabled="isView" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="资料查看时限" prop="materialViewTimeRange">
<el-date-picker
v-model="formData.materialViewTimeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm"
value-format="x"
:disabled="isView"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="议程附件">
<div class="agenda-attachment-wrap">
<el-upload
v-if="!isView"
:auto-upload="false"
:show-file-list="false"
accept=".pdf,.png,.jpg,.jpeg,.gif,.bmp,.webp"
:on-change="handleAgendaAttachmentChange"
>
<el-button type="primary" plain size="small">上传议程附件</el-button>
</el-upload>
<el-text type="info" size="small">仅支持图片或 PDF固定单附件</el-text>
<div v-if="formData.agendaAttachmentUrl" class="agenda-file-line">
<el-link type="primary" :underline="false" @click="previewAgendaAttachment">
{{ formData.agendaAttachmentName }}
</el-link>
<el-tag size="small">{{ (formData.agendaAttachmentType || '').toUpperCase() }}</el-tag>
<el-text type="info" size="small">{{ formatFileSize(formData.agendaAttachmentSize) }}</el-text>
<el-button v-if="!isView" type="danger" link @click="clearAgendaAttachment">移除</el-button>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 评审专家 -->
<el-divider content-position="left">评审专家</el-divider>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="参会专家" prop="expertIds">
<ExpertSelectTable
v-model="formData.expertIds"
:experts="expertOptions"
:disabled="isView"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 评审项目 -->
<el-divider content-position="left">评审项目</el-divider>
<div v-if="!isView" class="import-section">
<el-upload
:auto-upload="false"
:on-change="handleExcelChange"
:show-file-list="false"
accept=".xls,.xlsx"
>
<el-button type="primary" plain>导入验收申请 Excel</el-button>
</el-upload>
<el-button type="success" plain @click="handleDownloadTemplate">下载导入模板</el-button>
<el-text type="info" size="small">格式:序号、开始时间、结束时间、议程分类、项目标题、汇报人、报告人单位</el-text>
</div>
<div v-if="formData.projects && formData.projects.length > 0" class="mt-10">
<el-table :data="formData.projects" border size="small" max-height="360">
<el-table-column label="序号" prop="seqNo" width="60" align="center" />
<el-table-column label="开始时间" prop="startTime" width="80" align="center" />
<el-table-column label="结束时间" prop="endTime" width="80" align="center" />
<el-table-column label="议程分类" prop="agendaCategory" width="110" />
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip />
<el-table-column label="汇报人" prop="reporter" width="80" />
<el-table-column label="报告单位" prop="reporterUnit" show-overflow-tooltip width="130" />
</el-table>
</div>
<el-empty v-else-if="isView" description="暂无评审项目" :image-size="60" />
</el-form>
<!-- 底部操作区 -->
<div class="form-footer">
<el-button v-if="!isView" type="primary" :loading="formLoading" @click="submitForm">保存草稿</el-button>
<el-button @click="handleBack">返回</el-button>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormRules } from 'element-plus'
import type { UploadFile } from 'element-plus'
import {
createReviewMeeting,
updateReviewMeeting,
getReviewMeeting,
importProjectsFromExcel,
getImportTemplate,
uploadAgendaAttachment,
type ReviewMeetingSaveReqVO,
type ReviewProjectItemVO
} from '@/api/review/meeting'
import { getExpertUserList } from '@/api/system/user/index'
import download from '@/utils/download'
import ExpertSelectTable from './components/ExpertSelectTable.vue'
defineOptions({ name: 'ReviewMeetingEdit' })
const router = useRouter()
const route = useRoute()
const formLoading = ref(false)
const expertOptions = ref<any[]>([])
const isProjectsModified = ref(false)
// 从路由参数判断模式
const meetingId = computed(() => {
const id = route.params.id
return id ? Number(id) : undefined
})
const isView = computed(() => route.query.mode === 'view')
const isEdit = computed(() => !!meetingId.value && !isView.value)
const pageTitle = computed(() => {
if (isView.value) return '查看会议'
return isEdit.value ? '编辑会议' : '新建会议'
})
type FormData = ReviewMeetingSaveReqVO & {
organizationUnit?: string
meetingTimeRange?: any[]
materialViewTimeRange?: any[]
}
const formData = reactive<FormData>({
id: undefined,
name: '',
organizationUnit: undefined,
startTime: undefined,
endTime: undefined,
location: '',
agendaAttachmentName: undefined,
agendaAttachmentUrl: undefined,
agendaAttachmentType: undefined,
agendaAttachmentSize: undefined,
materialViewStartTime: undefined,
materialViewEndTime: undefined,
materialViewRemark: undefined,
expertIds: [],
meetingTimeRange: undefined,
materialViewTimeRange: undefined,
projects: []
})
const rules: FormRules = {
name: [{ required: true, message: '会议名称不能为空', trigger: 'blur' }],
meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }],
location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }],
expertIds: [{ required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' }]
}
const formRef = ref()
const loadDetail = async (id: number) => {
formLoading.value = true
try {
const detail = await getReviewMeeting(id)
Object.assign(formData, detail)
if (detail.startTime && detail.endTime) {
formData.meetingTimeRange = [
new Date(detail.startTime.replace(' ', 'T')).getTime(),
new Date(detail.endTime.replace(' ', 'T')).getTime()
]
}
if (detail.materialViewStartTime && detail.materialViewEndTime) {
formData.materialViewTimeRange = [
new Date(detail.materialViewStartTime.replace(' ', 'T')).getTime(),
new Date(detail.materialViewEndTime.replace(' ', 'T')).getTime()
]
}
} finally {
formLoading.value = false
}
}
onMounted(async () => {
expertOptions.value = await getExpertUserList().catch(() => [])
if (meetingId.value) {
await loadDetail(meetingId.value)
}
})
const handleExcelChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) return
if (formData.projects && formData.projects.length > 0) {
await ElMessageBox.confirm('重新导入将覆盖已有评审项目列表,是否继续?', '提示', { type: 'warning' })
}
formLoading.value = true
try {
const result = await importProjectsFromExcel(uploadFile.raw)
formData.projects = result as ReviewProjectItemVO[]
isProjectsModified.value = true
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
} catch {
ElMessage.error('Excel 解析失败,请检查文件格式')
} finally {
formLoading.value = false
}
}
const handleDownloadTemplate = async () => {
try {
const data = await getImportTemplate()
download.excel(data, '评审项目导入模板.xls')
} catch {
ElMessage.error('下载模板失败')
}
}
const handleAgendaAttachmentChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) return
const ext = uploadFile.raw.name.includes('.')
? uploadFile.raw.name.substring(uploadFile.raw.name.lastIndexOf('.') + 1).toLowerCase()
: ''
const isPdf = ext === 'pdf' || uploadFile.raw.type === 'application/pdf'
const isImage = uploadFile.raw.type?.startsWith('image/')
if (!isPdf && !isImage) {
ElMessage.error('议程附件仅支持图片或 PDF')
return
}
formLoading.value = true
try {
const attachment = await uploadAgendaAttachment(uploadFile.raw)
formData.agendaAttachmentName = attachment.name
formData.agendaAttachmentUrl = attachment.url
formData.agendaAttachmentType = attachment.type
formData.agendaAttachmentSize = attachment.size
ElMessage.success('议程附件上传成功')
} finally {
formLoading.value = false
}
}
const clearAgendaAttachment = () => {
formData.agendaAttachmentName = undefined
formData.agendaAttachmentUrl = undefined
formData.agendaAttachmentType = undefined
formData.agendaAttachmentSize = undefined
}
const previewAgendaAttachment = () => {
if (formData.agendaAttachmentUrl) {
window.open(formData.agendaAttachmentUrl, '_blank')
}
}
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '-'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
const submitForm = async () => {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
if (formData.meetingTimeRange?.length === 2) {
formData.startTime = formData.meetingTimeRange[0]
formData.endTime = formData.meetingTimeRange[1]
}
if (formData.materialViewTimeRange?.length === 2) {
formData.materialViewStartTime = formData.materialViewTimeRange[0]
formData.materialViewEndTime = formData.materialViewTimeRange[1]
} else if (formData.meetingTimeRange?.length === 2) {
formData.materialViewStartTime = formData.meetingTimeRange[0]
formData.materialViewEndTime = formData.meetingTimeRange[1]
} else {
formData.materialViewStartTime = undefined
formData.materialViewEndTime = undefined
}
formLoading.value = true
try {
const submitData = { ...formData }
if (isEdit.value && !isProjectsModified.value) {
delete submitData.projects
}
if (isEdit.value) {
await updateReviewMeeting(submitData)
ElMessage.success('更新成功')
} else {
await createReviewMeeting(submitData)
ElMessage.success('创建成功,会议已保存为草稿')
}
router.push({ name: 'ReviewMeeting' })
} finally {
formLoading.value = false
}
}
const handleBack = () => {
router.back()
}
</script>
<style scoped>
.meeting-edit-header {
margin-bottom: 16px;
}
.page-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.import-section {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.mt-10 {
margin-top: 10px;
}
.agenda-attachment-wrap {
display: flex;
flex-direction: column;
gap: 6px;
}
.agenda-file-line {
display: flex;
align-items: center;
gap: 8px;
}
.form-footer {
margin-top: 24px;
display: flex;
gap: 10px;
}
</style>