From 261173e76e5c69a74dc92c0a059b8831921c1e99 Mon Sep 17 00:00:00 2001 From: Codewoc <947380458@qq.com> Date: Thu, 26 Mar 2026 08:27:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(review-frontend):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E8=AF=84=E5=AE=A1=E8=B5=84=E6=96=99=E6=B8=85=E5=8D=95=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=B8=8E=E5=9B=BA=E5=AE=9A=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/review/project.ts | 97 ++++++ src/views/review/meeting/AllProjectList.vue | 23 +- src/views/review/meeting/ProjectDetail.vue | 361 ++++++++++++-------- src/views/review/meeting/ProjectList.vue | 25 +- 4 files changed, 338 insertions(+), 168 deletions(-) diff --git a/src/api/review/project.ts b/src/api/review/project.ts index 1f906e3c3..6016fce5f 100644 --- a/src/api/review/project.ts +++ b/src/api/review/project.ts @@ -21,6 +21,9 @@ export interface ReviewMeetingProjectRespVO { reviewResult?: 'PASS' | 'REJECT' } +export const REVIEW_AGENDA_CATEGORY_OPTIONS = ['项目立项', '预验收', '项目终验'] as const +export type ReviewAgendaCategory = (typeof REVIEW_AGENDA_CATEGORY_OPTIONS)[number] + export interface ReviewMeetingProjectSeqUpdateReqVO { id: number seqNo: number @@ -48,7 +51,12 @@ export interface ReviewProjectPageReqVO { export interface ReviewMeetingFileRespVO { id: number + reviewMeetingId: number reviewMeetingProjectId: number + materialCode?: string + materialNameSnapshot?: string + agendaTypeSnapshot?: string + version?: number fileName: string fileUrl: string fileSize: number @@ -66,6 +74,37 @@ export interface ReviewMeetingFileRegisterReqVO { fileType?: string } +export interface ReviewMeetingMaterialItemRespVO { + materialCode: string + materialName: string + required: boolean + acceptExts: string + tabletVisible: boolean + latestFile?: ReviewMeetingFileRespVO +} + +export interface ReviewMeetingMaterialSummaryRespVO { + agendaType: string + templateAvailable: boolean + materials: ReviewMeetingMaterialItemRespVO[] +} + +export interface ReviewMeetingMaterialHistoryRespVO { + materialCode: string + materialName: string + file: ReviewMeetingFileRespVO +} + +export interface ReviewMeetingMaterialUploadReqVO { + reviewMeetingId: number + reviewMeetingProjectId: number + materialCode: string + fileName: string + fileUrl: string + fileSize: number + fileType?: string +} + // ============================================================ // API 调用 // ============================================================ @@ -122,6 +161,64 @@ export const getMeetingFileList = (reviewMeetingProjectId: number) => export const deleteMeetingFile = (id: number) => request.delete({ url: '/project/review-project/delete-file', params: { id } }) +/** 获取结构化材料汇总 */ +export const getProjectMaterialSummary = (reviewMeetingProjectId: number) => + request.get({ + url: '/project/review-project/material-summary', + params: { reviewMeetingProjectId } + }) + +/** 获取材料历史版本 */ +export const getProjectMaterialHistory = (reviewMeetingProjectId: number, materialCode: string) => + request.get({ + url: '/project/review-project/material-history', + params: { reviewMeetingProjectId, materialCode } + }) + +/** 下载当前议程分类模板包 */ +export const downloadProjectTemplateBundle = (agendaType: string) => + request.download({ + url: '/project/review-project/download-template-bundle', + params: { agendaType } + }) + +/** 上传结构化材料(预签名直传) */ +export const uploadProjectMaterial = async ( + reviewMeetingId: number, + reviewMeetingProjectId: number, + materialCode: string, + file: File +) => { + const presignedInfo = await FileApi.getFilePresignedUrl(file.name, 'review-meeting') + await axios.put(presignedInfo.uploadUrl, file, { + headers: { + 'Content-Type': file.type || 'application/octet-stream' + } + }) + + await FileApi.createFile({ + configId: presignedInfo.configId, + url: presignedInfo.url, + path: presignedInfo.path, + name: file.name, + type: file.type || 'application/octet-stream', + size: file.size + }) + + return request.post({ + url: '/project/review-project/upload-material', + data: { + reviewMeetingId, + reviewMeetingProjectId, + materialCode, + fileName: file.name, + fileUrl: presignedInfo.url, + fileSize: file.size, + fileType: file.name.includes('.') ? file.name.split('.').pop()?.toLowerCase() : '' + } satisfies ReviewMeetingMaterialUploadReqVO + }) +} + const uploadMeetingFileByPresignedUrl = async ( reviewMeetingId: number, reviewMeetingProjectId: number, diff --git a/src/views/review/meeting/AllProjectList.vue b/src/views/review/meeting/AllProjectList.vue index f1edf5103..48b2bbec1 100644 --- a/src/views/review/meeting/AllProjectList.vue +++ b/src/views/review/meeting/AllProjectList.vue @@ -33,13 +33,16 @@ class="search-input-sm" @keyup.enter="handleQuery" /> - + + + + @@ -106,7 +109,9 @@ - + + + @@ -143,6 +148,7 @@ import { ref, reactive, onMounted } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { + REVIEW_AGENDA_CATEGORY_OPTIONS, getReviewProjectPageStandalone, updateReviewProject, createReviewProject, @@ -226,6 +232,7 @@ const formRef = ref() const formData = reactive>({}) const formRules = { reviewMeetingId: [{ required: true, message: '请选择所属会议', trigger: 'change' }], + agendaCategory: [{ required: true, message: '请选择议程分类', trigger: 'change' }], projectTitle: [{ required: true, message: '项目标题不能为空', trigger: 'blur' }] } diff --git a/src/views/review/meeting/ProjectDetail.vue b/src/views/review/meeting/ProjectDetail.vue index 3d8f44f71..aa0be1e72 100644 --- a/src/views/review/meeting/ProjectDetail.vue +++ b/src/views/review/meeting/ProjectDetail.vue @@ -1,12 +1,10 @@ diff --git a/src/views/review/meeting/ProjectList.vue b/src/views/review/meeting/ProjectList.vue index 64bee7c71..c1c4c687a 100644 --- a/src/views/review/meeting/ProjectList.vue +++ b/src/views/review/meeting/ProjectList.vue @@ -38,13 +38,9 @@ class="search-input" @keyup.enter="handleQuery" /> - + + + @@ -112,7 +108,9 @@ - + + + @@ -150,7 +148,15 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import Sortable from 'sortablejs' import { getReviewMeeting } from '@/api/review/meeting' -import { getReviewProjectPage, updateReviewProject, updateReviewProjectSeqBatch, createReviewProject, deleteReviewProject, type ReviewMeetingProjectRespVO } from '@/api/review/project' +import { + REVIEW_AGENDA_CATEGORY_OPTIONS, + getReviewProjectPage, + updateReviewProject, + updateReviewProjectSeqBatch, + createReviewProject, + deleteReviewProject, + type ReviewMeetingProjectRespVO +} from '@/api/review/project' import { formatDate } from '@/utils/formatTime' defineOptions({ name: 'ReviewMeetingProject' }) @@ -287,6 +293,7 @@ const formLoading = ref(false) const formRef = ref() const formData = reactive>({}) const formRules = { + agendaCategory: [{ required: true, message: '请选择议程分类', trigger: 'change' }], projectTitle: [{ required: true, message: '项目标题不能为空', trigger: 'blur' }] }