feat(meeting): 新增会议编辑全页面 MeetingEdit.vue,替代弹窗形式
- 新增 MeetingEdit.vue,容器改为 ContentWrap 全页面 - 增加组织单位(选填)字段 - 模式由路由参数决定:无 id=新建,有 id=编辑,query.mode=view=查看 - 底部操作区:保存草稿 + 返回按钮 - 在 remaining.ts 中新增 ReviewMeetingEdit / ReviewMeetingEditById 两条路由 - 修正 index.vue 中 goToEdit 路由名对应关系 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>pull/874/head
parent
83adecdcb8
commit
bc8258a149
|
|
@ -747,6 +747,30 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||||
activeMenu: '/review/meeting'
|
activeMenu: '/review/meeting'
|
||||||
},
|
},
|
||||||
component: () => import('@/views/review/meeting/ProjectList.vue')
|
component: () => import('@/views/review/meeting/ProjectList.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'review-meeting/edit',
|
||||||
|
name: 'ReviewMeetingEdit',
|
||||||
|
meta: {
|
||||||
|
title: '新建会议',
|
||||||
|
noCache: true,
|
||||||
|
hidden: true,
|
||||||
|
canTo: true,
|
||||||
|
activeMenu: '/review/meeting'
|
||||||
|
},
|
||||||
|
component: () => import('@/views/review/meeting/MeetingEdit.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'review-meeting/edit/:id(\\d+)',
|
||||||
|
name: 'ReviewMeetingEditById',
|
||||||
|
meta: {
|
||||||
|
title: '编辑会议',
|
||||||
|
noCache: true,
|
||||||
|
hidden: true,
|
||||||
|
canTo: true,
|
||||||
|
activeMenu: '/review/meeting'
|
||||||
|
},
|
||||||
|
component: () => import('@/views/review/meeting/MeetingEdit.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,405 @@
|
||||||
|
<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">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.expertIds"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
placeholder="请选择参会专家"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="isView"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="expert in expertOptions"
|
||||||
|
:key="expert.id"
|
||||||
|
:label="expertLabel(expert)"
|
||||||
|
:value="expert.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
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 expertLabel = (e: any) =>
|
||||||
|
`${e.nickname}${e.title ? `(${e.title})` : ''}${e.deptName ? ` ${e.deptName}` : ''}`
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
@ -177,7 +177,11 @@ const handleQuery = () => { queryParams.pageNo = 1; getList() }
|
||||||
const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery() }
|
const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery() }
|
||||||
|
|
||||||
const goToEdit = (id?: number, mode?: string) => {
|
const goToEdit = (id?: number, mode?: string) => {
|
||||||
router.push({ name: 'ReviewMeetingEdit', params: id ? { id } : {}, query: mode ? { mode } : {} })
|
if (id) {
|
||||||
|
router.push({ name: 'ReviewMeetingEditById', params: { id }, query: mode ? { mode } : {} })
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'ReviewMeetingEdit' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToProjectList = (row: ReviewMeetingRespVO) => {
|
const goToProjectList = (row: ReviewMeetingRespVO) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue