From 405d461061826b4d429ea18e888fefb255bb1f75 Mon Sep 17 00:00:00 2001 From: Codewoc <947380458@qq.com> Date: Tue, 31 Mar 2026 15:11:11 +0800 Subject: [PATCH] feat(review-meeting): update expert selection and acceptance detail pages --- src/api/review/meeting.ts | 17 +- src/api/system/user/index.ts | 3 +- .../project/acceptance/detail/AdminView.vue | 34 ++++ .../project/acceptance/detail/ExpertView.vue | 38 +++++ .../project/acceptance/detail/LiaisonView.vue | 56 +++++++ .../acceptance/detail/ReviewerView.vue | 38 +++++ .../detail/components/AcceptanceHeader.vue | 37 +++++ .../detail/components/AuditDialog.vue | 43 ++++++ .../components/VersionHistoryDialog.vue | 23 +++ src/views/review/meeting/MeetingEdit.vue | 146 +++++++++++++++--- .../meeting/components/ExpertSelectTable.vue | 75 +++++++-- 11 files changed, 476 insertions(+), 34 deletions(-) create mode 100644 src/views/project/acceptance/detail/AdminView.vue create mode 100644 src/views/project/acceptance/detail/ExpertView.vue create mode 100644 src/views/project/acceptance/detail/LiaisonView.vue create mode 100644 src/views/project/acceptance/detail/ReviewerView.vue create mode 100644 src/views/project/acceptance/detail/components/AcceptanceHeader.vue create mode 100644 src/views/project/acceptance/detail/components/AuditDialog.vue create mode 100644 src/views/project/acceptance/detail/components/VersionHistoryDialog.vue diff --git a/src/api/review/meeting.ts b/src/api/review/meeting.ts index d2325ee0f..0036d3f3b 100644 --- a/src/api/review/meeting.ts +++ b/src/api/review/meeting.ts @@ -87,6 +87,17 @@ export interface ReviewMeetingAgendaAttachmentRespVO { size: number } +export interface ReviewMeetingAgendaGenerateReqVO { + name: string + organizationUnit: string + startTime: string | number + endTime: string | number + location: string + host: string + expertIds: number[] + projects: ReviewProjectItemVO[] +} + const REVIEW_MEETING_ATTACHMENT_DIR = 'review-meeting/attachment' // 短信发送状态 VO @@ -177,13 +188,17 @@ export const importProjectsFromExcel = async (file: File): Promise => { return uploadMeetingAttachmentByPresignedUrl(file) } +/** 根据当前表单内容自动生成议程附件 */ +export const generateAgendaAttachment = (data: ReviewMeetingAgendaGenerateReqVO) => + request.post({ url: '/project/review-meeting/generate-agenda', data }) + /** 上传会议纪要附件(Word/PDF/图片) */ export const uploadMinutesAttachment = ( id: number, diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts index 3847391a8..49aa9e04e 100644 --- a/src/api/system/user/index.ts +++ b/src/api/system/user/index.ts @@ -5,6 +5,7 @@ export interface UserVO { username: string nickname: string deptId: number + deptName?: string postIds: string[] email: string mobile: string @@ -40,7 +41,7 @@ export const updateUser = (data: UserVO) => { } // 获取专家列表 -export const getExpertUserList = () => { +export const getExpertUserList = (): Promise => { return request.get({ url: '/system/user/expert-list' }) } diff --git a/src/views/project/acceptance/detail/AdminView.vue b/src/views/project/acceptance/detail/AdminView.vue new file mode 100644 index 000000000..3e065ac97 --- /dev/null +++ b/src/views/project/acceptance/detail/AdminView.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/views/project/acceptance/detail/ExpertView.vue b/src/views/project/acceptance/detail/ExpertView.vue new file mode 100644 index 000000000..354f2ad9e --- /dev/null +++ b/src/views/project/acceptance/detail/ExpertView.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/views/project/acceptance/detail/LiaisonView.vue b/src/views/project/acceptance/detail/LiaisonView.vue new file mode 100644 index 000000000..d432c6ed8 --- /dev/null +++ b/src/views/project/acceptance/detail/LiaisonView.vue @@ -0,0 +1,56 @@ + + + diff --git a/src/views/project/acceptance/detail/ReviewerView.vue b/src/views/project/acceptance/detail/ReviewerView.vue new file mode 100644 index 000000000..ec81855a5 --- /dev/null +++ b/src/views/project/acceptance/detail/ReviewerView.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/views/project/acceptance/detail/components/AcceptanceHeader.vue b/src/views/project/acceptance/detail/components/AcceptanceHeader.vue new file mode 100644 index 000000000..88f6427f9 --- /dev/null +++ b/src/views/project/acceptance/detail/components/AcceptanceHeader.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/views/project/acceptance/detail/components/AuditDialog.vue b/src/views/project/acceptance/detail/components/AuditDialog.vue new file mode 100644 index 000000000..887d88a60 --- /dev/null +++ b/src/views/project/acceptance/detail/components/AuditDialog.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/views/project/acceptance/detail/components/VersionHistoryDialog.vue b/src/views/project/acceptance/detail/components/VersionHistoryDialog.vue new file mode 100644 index 000000000..8573e4d25 --- /dev/null +++ b/src/views/project/acceptance/detail/components/VersionHistoryDialog.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/views/review/meeting/MeetingEdit.vue b/src/views/review/meeting/MeetingEdit.vue index f633a43c7..db08ec2a2 100644 --- a/src/views/review/meeting/MeetingEdit.vue +++ b/src/views/review/meeting/MeetingEdit.vue @@ -83,16 +83,20 @@
- - - - 仅支持图片或 PDF,固定单附件 +
+ + + + +
+ 支持 DOCX、PDF、图片;固定单附件,可生成后再手工替换
{{ formData.agendaAttachmentName }} @@ -199,8 +203,10 @@ import { getReviewMeeting, importProjectsFromExcel, getImportTemplate, + generateAgendaAttachment, uploadAgendaAttachment, uploadMinutesAttachment, + type ReviewMeetingAgendaGenerateReqVO, type ReviewMeetingSaveReqVO, type ReviewProjectItemVO } from '@/api/review/meeting' @@ -434,10 +440,11 @@ const handleAgendaAttachmentChange = async (uploadFile: UploadFile) => { 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 (!isPdf && !isImage) { - ElMessage.error('议程附件仅支持图片或 PDF') + if (!isDocx && !isPdf && !isImage) { + ElMessage.error('议程附件仅支持 DOCX、PDF 或图片') return } formLoading.value = true @@ -515,6 +522,86 @@ const formatFileSize = (bytes?: number): string => { return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } +const buildScheduledProjects = (): ReviewProjectItemVO[] => { + const projectsWithReviewDate = applyDefaultReviewDate(formData.projects, formData.meetingTimeRange) + const hasCompleteSchedule = projectsWithReviewDate.every((item) => item.startTime && item.endTime) + if (hasCompleteSchedule) { + return projectsWithReviewDate + } + return ( + buildScheduledProjectItems( + projectsWithReviewDate, + formData.meetingTimeRange?.[0], + DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES + ) || projectsWithReviewDate + ) +} + +const buildAgendaGenerateData = (): ReviewMeetingAgendaGenerateReqVO | undefined => { + if (!formData.name?.trim()) { + ElMessage.warning('请先填写会议名称') + return + } + if (!formData.organizationUnit?.trim()) { + ElMessage.warning('请先填写组织单位') + return + } + if (!formData.location?.trim()) { + ElMessage.warning('请先填写会议地点') + return + } + if (!formData.host?.trim()) { + ElMessage.warning('请先填写会议主持人') + return + } + if (!formData.meetingTimeRange || formData.meetingTimeRange.length !== 2) { + ElMessage.warning('请先填写会议时间') + return + } + if (!formData.expertIds?.length) { + ElMessage.warning('请先选择参会专家') + return + } + if (!formData.projects?.length) { + ElMessage.warning('请先导入评审项目') + return + } + const projects = buildScheduledProjects() + const hasMissingSchedule = projects.some((item) => !item.startTime || !item.endTime) + if (hasMissingSchedule) { + ElMessage.warning('评审项目起止时间未生成完成,请检查会议时间和项目列表') + return + } + return { + name: formData.name, + organizationUnit: formData.organizationUnit, + startTime: formData.meetingTimeRange[0], + endTime: formData.meetingTimeRange[1], + location: formData.location, + host: formData.host, + expertIds: formData.expertIds, + projects + } +} + +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) @@ -532,12 +619,8 @@ const submitForm = async () => { } formLoading.value = true try { - const projects = buildScheduledProjectItems( - applyDefaultReviewDate(formData.projects, formData.meetingTimeRange), - formData.meetingTimeRange?.[0], - DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES - ) || applyDefaultReviewDate(formData.projects, formData.meetingTimeRange) - const submitData = { + const projects = buildScheduledProjects() + const submitData: ReviewMeetingSaveReqVO & { projects?: ReviewProjectItemVO[] } = { ...formData, projects } @@ -628,6 +711,12 @@ const handleBack = () => { align-items: center; gap: 8px; } +.agenda-action-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} .upload-hint { font-size: 12px; color: #999; @@ -663,6 +752,27 @@ const handleBack = () => { } .btn-upload:hover { background-color: rgba(41, 90, 188, 0.08); } +.btn-generate { + display: inline-flex; + align-items: center; + height: 34px; + padding: 0 14px; + background-color: #fff; + border: 1px solid #3aa76d; + border-radius: 6px; + color: #3aa76d; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} +.btn-generate:hover { + background-color: rgba(58, 167, 109, 0.08); +} +.btn-generate:disabled { + opacity: 0.6; + cursor: not-allowed; +} + .btn-default { display: inline-flex; align-items: center; diff --git a/src/views/review/meeting/components/ExpertSelectTable.vue b/src/views/review/meeting/components/ExpertSelectTable.vue index 2adb04279..3f52c4199 100644 --- a/src/views/review/meeting/components/ExpertSelectTable.vue +++ b/src/views/review/meeting/components/ExpertSelectTable.vue @@ -3,16 +3,25 @@
已选 {{ selectedExperts.length }} 人: - - {{ expert.nickname }} - +
+ {{ expert.nickname }} + {{ buildExpertSummary(expert) }} +
+ + 移除 + +
@@ -41,14 +50,18 @@ + + + +
@@ -63,6 +76,8 @@ interface Expert { nickname: string deptName?: string title?: string + position?: string + remark?: string } const props = defineProps<{ @@ -86,7 +101,9 @@ const filteredExperts = computed(() => { (e) => e.nickname?.toLowerCase().includes(kw) || e.deptName?.toLowerCase().includes(kw) || - e.title?.toLowerCase().includes(kw) + e.position?.toLowerCase().includes(kw) || + e.title?.toLowerCase().includes(kw) || + e.remark?.toLowerCase().includes(kw) ) }) @@ -124,6 +141,10 @@ const handleSelectionChange = (selected: Expert[]) => { const removeExpert = (id: number) => { emit('update:modelValue', props.modelValue.filter((v) => v !== id)) } + +const buildExpertSummary = (expert: Expert) => { + return [expert.deptName, expert.position, expert.title, expert.remark].filter(Boolean).join(' / ') +}