From 46a6289836bf366310ab9801db096d84d41b20d0 Mon Sep 17 00:00:00 2001 From: Codewoc <947380458@qq.com> Date: Mon, 23 Mar 2026 22:02:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/review/project.ts | 17 +- src/views/review/meeting/AllProjectList.vue | 425 ++++++++++++++++++ src/views/review/meeting/MeetingEdit.vue | 196 ++++++-- src/views/review/meeting/ProjectDetail.vue | 248 ++++++++-- src/views/review/meeting/ProjectList.vue | 316 ++++++++++--- .../meeting/components/ExpertSelectTable.vue | 55 ++- src/views/review/meeting/index.vue | 302 +++++++++---- 7 files changed, 1334 insertions(+), 225 deletions(-) create mode 100644 src/views/review/meeting/AllProjectList.vue diff --git a/src/api/review/project.ts b/src/api/review/project.ts index ba8a3b9e1..27280eeb9 100644 --- a/src/api/review/project.ts +++ b/src/api/review/project.ts @@ -26,6 +26,17 @@ export interface ReviewMeetingProjectPageReqVO { reporter?: string } +/** 独立项目列表查询(meetingId 可选,用于独立菜单页) */ +export interface ReviewProjectPageReqVO { + pageNo?: number + pageSize?: number + reviewMeetingId?: number + projectTitle?: string + agendaCategory?: string + reporter?: string + reporterUnit?: string +} + export interface ReviewMeetingFileRespVO { id: number reviewMeetingProjectId: number @@ -41,10 +52,14 @@ export interface ReviewMeetingFileRespVO { // API 调用 // ============================================================ -/** 分页查询评审项目列表 */ +/** 分页查询评审项目列表(需要 meetingId) */ export const getReviewProjectPage = (params: ReviewMeetingProjectPageReqVO) => request.get({ url: '/project/review-project/page', params }) +/** 独立分页查询评审项目列表(meetingId 可选) */ +export const getReviewProjectPageStandalone = (params: ReviewProjectPageReqVO) => + request.get({ url: '/project/review-project/page', params }) + /** 创建评审项目 */ export const createReviewProject = (data: Partial) => request.post({ url: '/project/review-project/create', data }) diff --git a/src/views/review/meeting/AllProjectList.vue b/src/views/review/meeting/AllProjectList.vue new file mode 100644 index 000000000..bf93c31b7 --- /dev/null +++ b/src/views/review/meeting/AllProjectList.vue @@ -0,0 +1,425 @@ + + + + + diff --git a/src/views/review/meeting/MeetingEdit.vue b/src/views/review/meeting/MeetingEdit.vue index 44d3f0afc..f3bc8b8a7 100644 --- a/src/views/review/meeting/MeetingEdit.vue +++ b/src/views/review/meeting/MeetingEdit.vue @@ -1,6 +1,7 @@ @@ -156,6 +159,7 @@ import { type ReviewMeetingSaveReqVO, type ReviewProjectItemVO } from '@/api/review/meeting' +import { getReviewProjectPage } from '@/api/review/project' import { getExpertUserList } from '@/api/system/user/index' import download from '@/utils/download' import ExpertSelectTable from './components/ExpertSelectTable.vue' @@ -209,8 +213,10 @@ const formData = reactive({ const rules: FormRules = { name: [{ required: true, message: '会议名称不能为空', trigger: 'blur' }], + organizationUnit: [{ required: true, message: '组织单位不能为空', trigger: 'blur' }], meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }], location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }], + agendaAttachmentUrl: [{ required: true, message: '议程附件不能为空', trigger: 'change' }], expertIds: [{ required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' }] } @@ -219,8 +225,12 @@ const formRef = ref() const loadDetail = async (id: number) => { formLoading.value = true try { - const detail = await getReviewMeeting(id) + const [detail, projectData] = await Promise.all([ + getReviewMeeting(id), + getReviewProjectPage({ reviewMeetingId: id, pageNo: 1, pageSize: 200 }) + ]) Object.assign(formData, detail) + formData.projects = (projectData?.list ?? []) as ReviewProjectItemVO[] if (detail.startTime && detail.endTime) { formData.meetingTimeRange = [ new Date(detail.startTime.replace(' ', 'T')).getTime(), @@ -290,6 +300,7 @@ const handleAgendaAttachmentChange = async (uploadFile: UploadFile) => { formData.agendaAttachmentUrl = attachment.url formData.agendaAttachmentType = attachment.type formData.agendaAttachmentSize = attachment.size + formRef.value?.clearValidate('agendaAttachmentUrl') ElMessage.success('议程附件上传成功') } finally { formLoading.value = false @@ -301,6 +312,7 @@ const clearAgendaAttachment = () => { formData.agendaAttachmentUrl = undefined formData.agendaAttachmentType = undefined formData.agendaAttachmentSize = undefined + formRef.value?.validateField('agendaAttachmentUrl') } const previewAgendaAttachment = () => { @@ -317,6 +329,7 @@ const formatFileSize = (bytes?: number): string => { } const submitForm = async () => { + if (formLoading.value) return const valid = await formRef.value?.validate().catch(() => false) if (!valid) return if (formData.meetingTimeRange?.length === 2) { @@ -326,9 +339,6 @@ const submitForm = async () => { 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 @@ -358,37 +368,159 @@ const handleBack = () => { diff --git a/src/views/review/meeting/ProjectDetail.vue b/src/views/review/meeting/ProjectDetail.vue index 13e1a34b7..d15a37d67 100644 --- a/src/views/review/meeting/ProjectDetail.vue +++ b/src/views/review/meeting/ProjectDetail.vue @@ -1,28 +1,45 @@ @@ -79,7 +96,6 @@ const router = useRouter() const reviewMeetingId = Number(route.params.meetingId) const reviewMeetingProjectId = Number(route.params.projectId) -// 从 history state 读取项目基本信息(由 ProjectList.vue 跳转时携带) const state = window.history.state || {} const projectInfo = ref({ projectTitle: state.projectTitle as string | undefined, @@ -151,32 +167,190 @@ onMounted(() => { diff --git a/src/views/review/meeting/ProjectList.vue b/src/views/review/meeting/ProjectList.vue index 87c45591a..e8d8828ae 100644 --- a/src/views/review/meeting/ProjectList.vue +++ b/src/views/review/meeting/ProjectList.vue @@ -1,61 +1,82 @@ @@ -123,10 +141,8 @@ import { ref, reactive, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { Plus, Delete } from '@element-plus/icons-vue' import { getReviewMeeting } from '@/api/review/meeting' import { getReviewProjectPage, updateProjectHost, updateReviewProject, createReviewProject, deleteReviewProject, type ReviewMeetingProjectRespVO } from '@/api/review/project' -import FileListDialog from './FileListDialog.vue' import { formatDate } from '@/utils/formatTime' defineOptions({ name: 'ReviewMeetingProject' }) @@ -141,7 +157,6 @@ const total = ref(0) const meetingInfo = ref({}) const STATUS_LABEL: Record = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' } -const STATUS_TAG_TYPE: Record = { 0: 'warning', 1: 'success', 2: 'info', 3: 'danger' } const queryParams = reactive({ pageNo: 1, @@ -152,9 +167,6 @@ const queryParams = reactive({ reporter: undefined as string | undefined }) -const queryFormRef = ref() -const fileDialogRef = ref() - const getList = async () => { loading.value = true try { @@ -167,7 +179,12 @@ const getList = async () => { } const handleQuery = () => { queryParams.pageNo = 1; getList() } -const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery() } +const resetQuery = () => { + queryParams.projectTitle = undefined + queryParams.agendaCategory = undefined + queryParams.reporter = undefined + handleQuery() +} const selectedIds = ref([]) const handleSelectionChange = (val: ReviewMeetingProjectRespVO[]) => { @@ -206,7 +223,6 @@ const openForm = (type: 'create' | 'update', row?: ReviewMeetingProjectRespVO) = Object.assign(formData, row) } formVisible.value = true - // Reset form validation state if needed by deferring to next tick } const submitForm = async () => { @@ -228,10 +244,6 @@ const submitForm = async () => { } } -const openFileDialog = (row: ReviewMeetingProjectRespVO) => { - fileDialogRef.value?.open(row.id, row.projectTitle) -} - const goToDetail = (row: ReviewMeetingProjectRespVO) => { router.push({ name: 'ReviewProjectDetail', @@ -257,6 +269,172 @@ onMounted(async () => { diff --git a/src/views/review/meeting/components/ExpertSelectTable.vue b/src/views/review/meeting/components/ExpertSelectTable.vue index 5cafac3b7..2adb04279 100644 --- a/src/views/review/meeting/components/ExpertSelectTable.vue +++ b/src/views/review/meeting/components/ExpertSelectTable.vue @@ -77,6 +77,7 @@ const emit = defineEmits<{ const tableRef = ref() const searchKeyword = ref('') +const isUpdating = ref(false) const filteredExperts = computed(() => { const kw = searchKeyword.value.trim().toLowerCase() @@ -99,17 +100,20 @@ watch( async () => { await nextTick() if (!tableRef.value) return + isUpdating.value = true tableRef.value.clearSelection() filteredExperts.value.forEach((row) => { if (props.modelValue.includes(row.id)) { tableRef.value.toggleRowSelection(row, true) } }) + isUpdating.value = false }, { immediate: true } ) const handleSelectionChange = (selected: Expert[]) => { + if (isUpdating.value) return const selectedIds = selected.map((e) => e.id) // 合并:保留不在当前过滤列表中的已选项 const filteredIds = filteredExperts.value.map((e) => e.id) @@ -127,23 +131,66 @@ const removeExpert = (id: number) => { width: 100%; display: flex; flex-direction: column; - gap: 10px; + gap: 12px; } + +/* 已选标签行 */ .selected-row { display: flex; align-items: center; flex-wrap: wrap; - gap: 6px; + gap: 8px; + padding: 8px 12px; + background-color: rgba(41, 90, 188, 0.04); + border: 1px solid rgba(41, 90, 188, 0.12); + border-radius: 6px; } .selected-label { - font-size: 13px; - color: var(--el-text-color-secondary); + font-size: 14px; + color: #666; white-space: nowrap; } .expert-tag { margin: 0; + background-color: rgba(41, 90, 188, 0.1); + border-color: rgba(41, 90, 188, 0.2); + color: #295abc; } + +/* 搜索框 */ .search-input { max-width: 320px; } +:deep(.search-input .el-input__wrapper) { + height: 38px; + border-radius: 6px; + border-color: #dcdedf; +} +:deep(.search-input .el-input__inner) { + font-size: 14px; +} + +/* 专家表格 */ +:deep(.el-table .el-table__header-wrapper th) { + background-color: #eef2fb; + color: #333; + font-weight: 600; + font-size: 14px; + border-color: #e1e7f0; +} +:deep(.el-table .el-table__body td) { + font-size: 14px; + color: #333; + border-color: #e1e7f0; +} +:deep(.el-table .el-table__body tr.active > td) { + background-color: rgba(41, 90, 188, 0.06); +} +:deep(.el-table .el-table__body .el-checkbox__inner) { + border-color: #295abc; +} +:deep(.el-table .el-table__body .el-checkbox__input.is-checked .el-checkbox__inner) { + background-color: #295abc; + border-color: #295abc; +} diff --git a/src/views/review/meeting/index.vue b/src/views/review/meeting/index.vue index b901cfc8e..56b197445 100644 --- a/src/views/review/meeting/index.vue +++ b/src/views/review/meeting/index.vue @@ -1,105 +1,82 @@