diff --git a/src/api/review/meeting.ts b/src/api/review/meeting.ts index 186e8664a..227a67198 100644 --- a/src/api/review/meeting.ts +++ b/src/api/review/meeting.ts @@ -25,6 +25,10 @@ export interface ReviewMeetingSaveReqVO { materialViewEndTime?: string | number materialViewRemark?: string location: string + agendaAttachmentName?: string + agendaAttachmentUrl?: string + agendaAttachmentType?: string + agendaAttachmentSize?: number expertIds: number[] projects?: ReviewProjectItemVO[] } @@ -48,13 +52,25 @@ export interface ReviewMeetingRespVO { materialViewEndTime?: string materialViewRemark?: string location: string + agendaAttachmentName?: string + agendaAttachmentUrl?: string + agendaAttachmentType?: string + agendaAttachmentSize?: number status: number // 0-草稿 1-已邀约 2-已结束 3-已取消 expertIds: number[] expertCount: number projectCount: number + mailSent?: boolean createTime: string } +export interface ReviewMeetingAgendaAttachmentRespVO { + name: string + url: string + type: string + size: number +} + // 短信发送状态 VO export interface ReviewMeetingSmsLogRespVO { id: number @@ -67,6 +83,18 @@ export interface ReviewMeetingSmsLogRespVO { sendTime: string } +// 邮件发送状态 VO +export interface ReviewMeetingMailLogRespVO { + id: number + expertId: number + expertName: string + mail: string + status: number // 0-待发送 1-成功 2-失败 3-已忽略 + errorMsg: string + retryCount: number + sendTime: string +} + // ============================================================ // API 调用 // ============================================================ @@ -99,6 +127,10 @@ export const getReviewMeetingPage = (params: ReviewMeetingPageReqVO) => export const sendSmsInvitation = (id: number) => request.post({ url: '/project/review-meeting/send-sms', params: { id } }) +/** 手动发送邮件邀请函 */ +export const sendMailInvitation = (id: number) => + request.post({ url: '/project/review-meeting/send-mail', params: { id } }) + /** 手动重发失败短信 */ export const retrySmsLog = (smsLogId: number) => request.post({ url: '/project/review-meeting/retry-sms', params: { smsLogId } }) @@ -107,11 +139,25 @@ export const retrySmsLog = (smsLogId: number) => export const getSmsLogList = (reviewMeetingId: number) => request.get({ url: '/project/review-meeting/sms-log-list', params: { reviewMeetingId } }) +/** 获取邮件发送状态列表 */ +export const getMailLogList = (reviewMeetingId: number) => + request.get({ url: '/project/review-meeting/mail-log-list', params: { reviewMeetingId } }) + /** 解析 Excel 导入评审项目(返回项目列表,不落库) */ -export const importProjectsFromExcel = (file: File) => { +export const importProjectsFromExcel = async (file: File): Promise => { const formData = new FormData() formData.append('file', file) - return request.upload({ url: '/project/review-meeting/import-projects', data: formData }) + const res = await request.upload({ url: '/project/review-meeting/import-projects', data: formData }) + return res?.data || [] +} + +/** 上传固定议程附件(图片/PDF) */ +export const uploadAgendaAttachment = (file: File): Promise => { + const formData = new FormData() + formData.append('file', file) + return request + .upload({ url: '/project/review-meeting/upload-agenda-attachment', data: formData }) + .then((res) => res?.data as ReviewMeetingAgendaAttachmentRespVO) } /** 下载导入模板 */ diff --git a/src/views/review/meeting/MailStatusDialog.vue b/src/views/review/meeting/MailStatusDialog.vue new file mode 100644 index 000000000..94cb94572 --- /dev/null +++ b/src/views/review/meeting/MailStatusDialog.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/views/review/meeting/MeetingForm.vue b/src/views/review/meeting/MeetingForm.vue index fa4be8e3c..3d96084de 100644 --- a/src/views/review/meeting/MeetingForm.vue +++ b/src/views/review/meeting/MeetingForm.vue @@ -33,6 +33,32 @@ + + + +
+ + 上传议程附件 + + 仅支持图片或 PDF,且固定单附件 +
+ + {{ formData.agendaAttachmentName }} + + {{ (formData.agendaAttachmentType || '').toUpperCase() }} + {{ formatFileSize(formData.agendaAttachmentSize) }} + 移除 +
+
+
+
+
@@ -119,6 +145,7 @@ import { getReviewMeeting, importProjectsFromExcel, getImportTemplate, + uploadAgendaAttachment, type ReviewMeetingSaveReqVO, type ReviewProjectItemVO } from '@/api/review/meeting' @@ -147,6 +174,10 @@ const formData = reactive({ startTime: undefined, endTime: undefined, location: '', + agendaAttachmentName: undefined, + agendaAttachmentUrl: undefined, + agendaAttachmentType: undefined, + agendaAttachmentSize: undefined, materialViewStartTime: undefined, materialViewEndTime: undefined, materialViewRemark: undefined, @@ -204,6 +235,10 @@ const resetForm = () => { formData.startTime = undefined formData.endTime = undefined formData.location = '' + formData.agendaAttachmentName = undefined + formData.agendaAttachmentUrl = undefined + formData.agendaAttachmentType = undefined + formData.agendaAttachmentSize = undefined formData.materialViewStartTime = undefined formData.materialViewEndTime = undefined formData.materialViewRemark = undefined @@ -241,6 +276,50 @@ 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 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 @@ -286,4 +365,6 @@ defineExpose({ open }) .import-section { margin-top: 8px; display: flex; align-items: center; gap: 8px; } .ml-10 { margin-left: 10px; } .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; } diff --git a/src/views/review/meeting/index.vue b/src/views/review/meeting/index.vue index 3483d410d..36fcb215e 100644 --- a/src/views/review/meeting/index.vue +++ b/src/views/review/meeting/index.vue @@ -66,7 +66,7 @@ 进入项目列表 - +