diff --git a/.env.local b/.env.local index 1a84e6942..aaa364b23 100644 --- a/.env.local +++ b/.env.local @@ -4,7 +4,8 @@ NODE_ENV=development VITE_DEV=true # 请求路径 -VITE_BASE_URL='http://localhost:48080' +# 本地开发改为相对路径,便于通过局域网访问当前 Mac 上的 Vite 代理 +VITE_BASE_URL='' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务 VITE_UPLOAD_TYPE=client diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts index 49aa9e04e..e14e58a50 100644 --- a/src/api/system/user/index.ts +++ b/src/api/system/user/index.ts @@ -6,6 +6,7 @@ export interface UserVO { nickname: string deptId: number deptName?: string + parentDeptName?: string postIds: string[] email: string mobile: string diff --git a/src/views/review/meeting/MeetingEdit.vue b/src/views/review/meeting/MeetingEdit.vue index 7a454e805..9329c9f0a 100644 --- a/src/views/review/meeting/MeetingEdit.vue +++ b/src/views/review/meeting/MeetingEdit.vue @@ -74,104 +74,6 @@ - - -
-
- - - - -
- 支持 DOCX、PDF、图片;固定单附件,可生成后再手工替换 -
- - {{ formData.agendaAttachmentName }} - - {{ - (formData.agendaAttachmentType || '').toUpperCase() - }} - {{ - formatFileSize(formData.agendaAttachmentSize) - }} - 移除 -
-
-
-
- - -
- - - - 支持 Word、PDF、图片;结束会议前必须上传 - 请先保存会议后再上传纪要 -
- - {{ formData.minutesAttachmentName }} - - {{ - (formData.minutesAttachmentType || '').toUpperCase() - }} - {{ - formatFileSize(formData.minutesAttachmentSize) - }} - 移除 -
-
- - {{ formData.minutesAiStatusName }} - - - 最近生成:{{ formData.minutesAiUpdatedTime }} - - - {{ formData.minutesAiErrorMessage }} - -
-
-
-
- - - -
评审专家
- - - - - - @@ -206,6 +108,77 @@ + + +
评审专家
+ + + +
会议附件
+
+ +
+
+ 议程附件 +
+
+
+ + {{ formData.agendaAttachmentName }} + + {{ + (formData.agendaAttachmentType || '').toUpperCase() + }} + {{ + formatFileSize(formData.agendaAttachmentSize) + }} + 移除 +
+ +
+
+ + +
+
+ 会议纪要 +
+
+
+ + {{ formData.minutesAttachmentName }} + + {{ + (formData.minutesAttachmentType || '').toUpperCase() + }} + {{ + formatFileSize(formData.minutesAttachmentSize) + }} +
+ +
+ + {{ formData.minutesAiStatusName }} + + + 最近生成:{{ formData.minutesAiUpdatedTime }} + + + {{ formData.minutesAiErrorMessage }} + +
+
+
+
@@ -217,10 +190,60 @@ :disabled="formLoading" @click="submitForm" > - {{ formLoading ? '保存中...' : '保存草稿' }} + {{ formLoading ? '提交中...' : '提交' }} + + + +
+
+
+ + + +
+
+ +
+
+ 支持 DOCX、PDF、图片;固定单附件 +
+ + {{ formData.agendaAttachmentName }} + + {{ + (formData.agendaAttachmentType || '').toUpperCase() + }} + {{ + formatFileSize(formData.agendaAttachmentSize) + }} +
+
+ +
@@ -238,7 +261,6 @@ import { getImportTemplate, generateAgendaAttachment, uploadAgendaAttachment, - uploadMinutesAttachment, type ReviewMeetingAgendaGenerateReqVO, type ReviewMeetingSaveReqVO, type ReviewProjectItemVO @@ -270,6 +292,10 @@ const expertOptions = ref([]) const isProjectsModified = ref(false) const meetingLocationOptions = ['东5楼326', '南6楼203', '南6楼207'] +// 上传会议附件弹窗 +const agendaDialogVisible = ref(false) +const agendaDialogLoading = ref(false) + // 从路由参数判断模式 const meetingId = computed(() => { const id = route.params.id @@ -331,7 +357,6 @@ const rules: FormRules = { meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }], location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }], host: [{ required: true, message: '会议主持人不能为空', trigger: 'blur' }], - agendaAttachmentUrl: [{ required: true, message: '议程附件不能为空', trigger: 'change' }], expertIds: [ { required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' } ] @@ -502,80 +527,11 @@ const handleDownloadTemplate = async () => { } } -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 isDocx = ext === 'docx' - const isPdf = ext === 'pdf' || uploadFile.raw.type === 'application/pdf' - const isImage = uploadFile.raw.type?.startsWith('image/') - if (!isDocx && !isPdf && !isImage) { - ElMessage.error('议程附件仅支持 DOCX、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 - formRef.value?.clearValidate('agendaAttachmentUrl') - ElMessage.success('议程附件上传成功') - } finally { - formLoading.value = false - } -} - const clearAgendaAttachment = () => { formData.agendaAttachmentName = undefined formData.agendaAttachmentUrl = undefined formData.agendaAttachmentType = undefined formData.agendaAttachmentSize = undefined - formRef.value?.validateField('agendaAttachmentUrl') -} - -const handleMinutesAttachmentChange = async (uploadFile: UploadFile) => { - if (!uploadFile.raw) return - if (!meetingId.value) { - ElMessage.warning('请先保存会议后再上传纪要') - return - } - const ext = uploadFile.raw.name.includes('.') - ? uploadFile.raw.name.substring(uploadFile.raw.name.lastIndexOf('.') + 1).toLowerCase() - : '' - const allowed = ['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'] - if (!allowed.includes(ext)) { - ElMessage.error('会议纪要仅支持 Word、PDF 或图片') - return - } - formLoading.value = true - try { - const attachment = await uploadMinutesAttachment(meetingId.value, uploadFile.raw) - formData.minutesAttachmentName = attachment.name - formData.minutesAttachmentUrl = attachment.url - formData.minutesAttachmentType = attachment.type - formData.minutesAttachmentSize = attachment.size - formData.minutesAiStatus = 1 - formData.minutesAiStatusName = '生成中' - formData.minutesAiErrorMessage = undefined - formData.minutesAiUpdatedTime = undefined - ElMessage.success('会议纪要上传成功,整改通知后台生成中') - } finally { - formLoading.value = false - } -} - -const clearMinutesAttachment = () => { - formData.minutesAttachmentName = undefined - formData.minutesAttachmentUrl = undefined - formData.minutesAttachmentType = undefined - formData.minutesAttachmentSize = undefined - formData.minutesAiStatus = undefined - formData.minutesAiStatusName = undefined - formData.minutesAiErrorMessage = undefined - formData.minutesAiUpdatedTime = undefined } const previewAgendaAttachment = () => { @@ -662,24 +618,6 @@ const buildAgendaGenerateData = (): ReviewMeetingAgendaGenerateReqVO | undefined } } -const handleGenerateAgenda = async () => { - if (formLoading.value) return - const generateData = buildAgendaGenerateData() - if (!generateData) return - formLoading.value = true - try { - const attachment = await generateAgendaAttachment(generateData) - formData.agendaAttachmentName = attachment.name - formData.agendaAttachmentUrl = attachment.url - formData.agendaAttachmentType = attachment.type - formData.agendaAttachmentSize = attachment.size - formRef.value?.clearValidate('agendaAttachmentUrl') - ElMessage.success('议程附件已生成') - } finally { - formLoading.value = false - } -} - const submitForm = async () => { if (formLoading.value) return const valid = await formRef.value?.validate().catch(() => false) @@ -688,18 +626,19 @@ const submitForm = async () => { formData.startTime = formData.meetingTimeRange[0] formData.endTime = formData.meetingTimeRange[1] } - formLoading.value = true - try { - const projects = buildScheduledProjects() - const submitProjects = stripProjectTransientFields(projects) - const submitData: ReviewMeetingSaveReqVO & { projects?: ReviewProjectItemVO[] } = { - ...formData, - projects: submitProjects - } - if (isEdit.value && !isProjectsModified.value) { - delete submitData.projects - } - if (isEdit.value) { + // 编辑模式且有议程附件:直接保存 + if (isEdit.value && formData.agendaAttachmentUrl) { + formLoading.value = true + try { + const projects = buildScheduledProjects() + const submitProjects = stripProjectTransientFields(projects) + const submitData: ReviewMeetingSaveReqVO & { projects?: ReviewProjectItemVO[] } = { + ...formData, + projects: submitProjects + } + if (!isProjectsModified.value) { + delete submitData.projects + } await updateReviewMeeting(submitData) const shouldSyncProjectTimes = shouldSyncPersistedProjectTimes({ isEdit: isEdit.value, @@ -716,14 +655,87 @@ const submitForm = async () => { } originalMeetingStart.value = formData.meetingTimeRange?.[0] ElMessage.success('更新成功') - } else { - await createReviewMeeting(submitData) - originalMeetingStart.value = formData.meetingTimeRange?.[0] - ElMessage.success('创建成功,会议已保存为草稿') + router.push({ name: 'ReviewMeeting' }) + } finally { + formLoading.value = false } + } else { + // 新建模式或编辑模式下议程附件为空:弹出上传议程附件弹窗 + agendaDialogVisible.value = true + } +} + +// 弹窗内上传议程附件 +const handleDialogAgendaUpload = async (uploadFile: UploadFile) => { + if (!uploadFile.raw) return + const ext = uploadFile.raw.name.includes('.') + ? uploadFile.raw.name.substring(uploadFile.raw.name.lastIndexOf('.') + 1).toLowerCase() + : '' + const isDocx = ext === 'docx' + const isPdf = ext === 'pdf' || uploadFile.raw.type === 'application/pdf' + const isImage = uploadFile.raw.type?.startsWith('image/') + if (!isDocx && !isPdf && !isImage) { + ElMessage.error('议程附件仅支持 DOCX、PDF 或图片') + return + } + agendaDialogLoading.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 { + agendaDialogLoading.value = false + } +} + +// 弹窗内自动生成议程 +const handleDialogGenerateAgenda = async () => { + if (agendaDialogLoading.value) return + const generateData = buildAgendaGenerateData() + if (!generateData) return + agendaDialogLoading.value = true + try { + const attachment = await generateAgendaAttachment(generateData) + formData.agendaAttachmentName = attachment.name + formData.agendaAttachmentUrl = attachment.url + formData.agendaAttachmentType = attachment.type + formData.agendaAttachmentSize = attachment.size + ElMessage.success('议程附件已生成') + } finally { + agendaDialogLoading.value = false + } +} + +// 弹窗确定按钮 - 真正保存草稿 +const handleDialogConfirm = async () => { + if (!formData.agendaAttachmentUrl) { + ElMessage.warning('请上传或生成议程附件') + return + } + agendaDialogLoading.value = true + try { + const projects = buildScheduledProjects() + const submitProjects = stripProjectTransientFields(projects) + const submitData: ReviewMeetingSaveReqVO & { projects?: ReviewProjectItemVO[] } = { + ...formData, + projects: submitProjects + } + if (isEdit.value) { + // 编辑模式:更新会议 + await updateReviewMeeting(submitData) + ElMessage.success('更新成功') + } else { + // 新建模式:创建会议 + await createReviewMeeting(submitData) + ElMessage.success('保存成功,会议已保存为草稿') + } + agendaDialogVisible.value = false router.push({ name: 'ReviewMeeting' }) } finally { - formLoading.value = false + agendaDialogLoading.value = false } } @@ -931,9 +943,62 @@ const handleBack = () => { .form-footer { margin-top: 20px; display: flex; + justify-content: flex-end; gap: 12px; } +/* ── 上传议程附件弹窗 ── */ +.agenda-dialog-content { + display: flex; + flex-direction: column; + gap: 12px; +} +.agenda-dialog-buttons { + display: flex; + gap: 12px; +} +.agenda-dialog-buttons .btn-wrapper { + flex: 1; +} +.agenda-dialog-buttons .btn-wrapper :deep(.el-upload) { + width: 100%; +} +.agenda-dialog-buttons .btn-wrapper :deep(.el-upload .el-upload__input) { + display: none; +} +.agenda-dialog-buttons .btn-dialog-action { + width: 100%; + height: 40px; + padding: 0 16px; + background-color: #f5f7fa; + border: 1px solid #dcdfe6; + border-radius: 8px; + color: #606266; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + box-sizing: border-box; +} +.agenda-dialog-buttons .btn-dialog-action:hover { + background-color: #e6f0ff; + border-color: #295abc; + color: #295abc; +} +.agenda-dialog-buttons .btn-dialog-action:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.agenda-dialog-buttons .btn-wrapper:last-child .btn-dialog-action { + background-color: #e6f7f0; + border-color: #67c23a; + color: #67c23a; +} +.agenda-dialog-buttons .btn-wrapper:last-child .btn-dialog-action:hover { + background-color: #d1f0e0; + border-color: #5daf34; + color: #5daf34; +} + /* ── 项目预览表格 ── */ :deep(.projects-preview-table .el-table__header-wrapper th) { background-color: #eef2fb; @@ -961,4 +1026,43 @@ const handleBack = () => { :deep(.projects-preview-table .el-table__header-wrapper th .cell) { line-height: 1.5; } + +/* ── 会议附件卡片 ── */ +.attachment-cards { + display: flex; + gap: 12px; +} +.attachment-card { + flex: 1; + background-color: #fff; + border: 1px solid #e1e7f0; + border-radius: 4px; + overflow: hidden; +} +.attachment-card-header { + padding: 1px 6px; + background-color: #f5f7fa; + border-bottom: 1px solid #e1e7f0; +} +.attachment-card-title { + font-size: 12px; + font-weight: 600; + color: #333; +} +.attachment-card-body { + padding: 2px 6px; +} +.attachment-file-info { + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; +} +.minutes-ai-line { + margin-top: 2px; + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; +} diff --git a/src/views/review/meeting/components/ExpertSelectTable.vue b/src/views/review/meeting/components/ExpertSelectTable.vue index e10adcf55..7e736be02 100644 --- a/src/views/review/meeting/components/ExpertSelectTable.vue +++ b/src/views/review/meeting/components/ExpertSelectTable.vue @@ -50,7 +50,11 @@ > - + + + @@ -59,7 +63,11 @@ - + + + @@ -76,6 +84,7 @@ interface Expert { id: number nickname: string deptName?: string + parentDeptName?: string title?: string position?: string remark?: string @@ -143,7 +152,10 @@ const removeExpert = (id: number) => { } const buildExpertSummary = (expert: Expert) => { - return [expert.deptName, expert.position, expert.title, expert.remark].filter(Boolean).join(' / ') + const deptDisplay = expert.parentDeptName + ? `${expert.parentDeptName}${expert.deptName}` + : expert.deptName + return [deptDisplay, expert.position, expert.title, expert.remark].filter(Boolean).join(' / ') } @@ -222,13 +234,19 @@ const buildExpertSummary = (expert: Expert) => { background-color: #eef2fb; color: #333; font-weight: 600; - font-size: 14px; + font-size: 18px; border-color: #e1e7f0; } +:deep(.el-table .el-table__header-wrapper th.el-table__cell) { + padding-top: 12px; + padding-bottom: 12px; +} :deep(.el-table .el-table__body td) { - font-size: 14px; + font-size: 18px; color: #333; border-color: #e1e7f0; + padding-top: 12px; + padding-bottom: 12px; } :deep(.el-table .el-table__body tr.active > td) { background-color: rgba(41, 90, 188, 0.06);