diff --git a/src/api/review/meeting.ts b/src/api/review/meeting.ts index 2bbc3bc18..2b12a40a4 100644 --- a/src/api/review/meeting.ts +++ b/src/api/review/meeting.ts @@ -6,6 +6,7 @@ import request from '@/config/axios' // 评审项目条目(Excel 导入 & 保存时使用) export interface ReviewProjectItemVO { + sourceProjectId?: number seqNo: number startTime: string endTime: string diff --git a/src/api/review/project.ts b/src/api/review/project.ts index 6016fce5f..26c074c7e 100644 --- a/src/api/review/project.ts +++ b/src/api/review/project.ts @@ -19,6 +19,8 @@ export interface ReviewMeetingProjectRespVO { host?: string reviewDate?: string reviewResult?: 'PASS' | 'REJECT' + preMeetingMaterialsComplete?: boolean + postMeetingMaterialsComplete?: boolean } export const REVIEW_AGENDA_CATEGORY_OPTIONS = ['项目立项', '预验收', '项目终验'] as const @@ -36,6 +38,7 @@ export interface ReviewMeetingProjectPageReqVO { projectTitle?: string agendaCategory?: string reporter?: string + reviewDate?: string } /** 独立项目列表查询(meetingId 可选,用于独立菜单页) */ @@ -47,6 +50,7 @@ export interface ReviewProjectPageReqVO { agendaCategory?: string reporter?: string reporterUnit?: string + reviewDate?: string } export interface ReviewMeetingFileRespVO { diff --git a/src/views/review/meeting/AllProjectList.vue b/src/views/review/meeting/AllProjectList.vue index 48b2bbec1..906b25f9a 100644 --- a/src/views/review/meeting/AllProjectList.vue +++ b/src/views/review/meeting/AllProjectList.vue @@ -43,6 +43,14 @@ + @@ -76,9 +84,25 @@ + + + + + + @@ -167,6 +191,15 @@ const list = ref([]) const total = ref(0) const meetingOptions = ref<{ id: number; name: string; host?: string }[]>([]) const REVIEW_RESULT_LABEL = { PASS: '通过', REJECT: '不通过' } +const getReviewResultClass = (result?: string) => { + if (result === 'PASS') return 'review-result-pass' + if (result === 'REJECT') return 'review-result-reject' + return '' +} +const getMaterialCompleteClass = (complete?: boolean) => { + if (complete) return 'material-complete' + return 'material-incomplete' +} const queryParams = reactive({ pageNo: 1, @@ -175,7 +208,8 @@ const queryParams = reactive { @@ -201,6 +235,7 @@ const resetQuery = () => { queryParams.agendaCategory = undefined queryParams.reporter = undefined queryParams.reporterUnit = undefined + queryParams.reviewDate = undefined handleQuery() } @@ -398,6 +433,25 @@ onMounted(async () => { } .project-name-text:hover { text-decoration: underline; } +.review-result-pass { + color: #67c23a; + font-weight: 500; +} +.review-result-reject { + color: #f56c6c; + font-weight: 500; +} +.material-complete { + color: #67c23a !important; + font-weight: 600; + font-size: 16px; +} +.material-incomplete { + color: #f56c6c !important; + font-weight: 600; + font-size: 16px; +} + /* ── 操作链接 ── */ .op-link { display: inline-block; diff --git a/src/views/review/meeting/MeetingEdit.vue b/src/views/review/meeting/MeetingEdit.vue index 6e1efd357..d6aaa8270 100644 --- a/src/views/review/meeting/MeetingEdit.vue +++ b/src/views/review/meeting/MeetingEdit.vue @@ -281,6 +281,7 @@ const formRef = ref() const mapProjectItems = (projects: any[]): ReviewProjectItemVO[] => (projects || []).map((item: any) => ({ + sourceProjectId: item.sourceProjectId ?? item.id, seqNo: item.seqNo, startTime: item.startTime, endTime: item.endTime, @@ -319,7 +320,10 @@ const loadDetail = async (id: number) => { const loadCopySource = 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 }) + ]) if (![2, 3].includes(detail.status)) { ElMessage.warning('仅支持从已结束或已取消会议复制') router.push({ name: 'ReviewMeeting' }) @@ -345,7 +349,8 @@ const loadCopySource = async (id: number) => { formData.minutesAttachmentType = undefined formData.minutesAttachmentSize = undefined formData.expertIds = detail.expertIds || [] - formData.projects = [] + formData.projects = mapProjectItems(projectData?.list ?? []) + isProjectsModified.value = false if (detail.startTime && detail.endTime) { formData.meetingTimeRange = [ @@ -364,7 +369,7 @@ const loadCopySource = async (id: number) => { formData.materialViewTimeRange = undefined } - ElMessage.info('已带入会议信息;议程附件和评审项目不会复制,请补充后再保存草稿') + ElMessage.info('已带入会议信息和评审项目;保存草稿后将同步复制项目资料,议程附件与会议纪要不会复制') } finally { formLoading.value = false } diff --git a/src/views/review/meeting/ProjectDetail.vue b/src/views/review/meeting/ProjectDetail.vue index a1943dc2f..411cafbaf 100644 --- a/src/views/review/meeting/ProjectDetail.vue +++ b/src/views/review/meeting/ProjectDetail.vue @@ -36,11 +36,6 @@
-
-
上传资料
- 下载模板 -
-
会前资料
@@ -127,7 +122,6 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { getReviewMeeting } from '@/api/review/meeting' import { - downloadProjectTemplateBundle, getProjectMaterialHistory, getProjectMaterialSummary, uploadProjectMaterial, @@ -215,15 +209,6 @@ const loadSummary = async () => { } } -const handleDownloadTemplate = async () => { - const agendaType = materialSummary.value.agendaType || projectInfo.value.agendaCategory - if (!agendaType) { - ElMessage.warning('未获取到议程分类') - return - } - await downloadProjectTemplateBundle(agendaType) -} - const triggerUpload = (row: ReviewMeetingMaterialItemRespVO) => { uploadTarget.value = row if (!uploadInputRef.value) return @@ -403,19 +388,6 @@ onMounted(async () => { padding: 16px; } -.material-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 14px; -} - -.toolbar-title { - font-size: 16px; - font-weight: 600; - color: #303133; -} - .group-title { margin: 8px 0 10px; font-size: 14px; diff --git a/src/views/review/meeting/ProjectList.vue b/src/views/review/meeting/ProjectList.vue index c1c4c687a..130097e9b 100644 --- a/src/views/review/meeting/ProjectList.vue +++ b/src/views/review/meeting/ProjectList.vue @@ -56,38 +56,139 @@
- - - + + + - - - + + + - - + - - - - + - + + + + + + + + + + + + + + + + + + + @@ -174,7 +275,22 @@ const tableRef = ref() let sortableInstance: Sortable | null = null const STATUS_LABEL: Record = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' } -const REVIEW_RESULT_LABEL = { PASS: '通过', REJECT: '不通过' } +const getMaterialCompleteClass = (complete?: boolean) => { + if (complete) return 'material-complete' + return 'material-incomplete' +} +type InlineEditableFields = { + seqNo?: number + startTime?: string + endTime?: string + agendaCategory?: string + projectTitle?: string + reporter?: string + reporterUnit?: string + reviewDate?: string + reviewResult?: 'PASS' | 'REJECT' +} +const inlineSnapshotMap = ref>({}) const queryParams = reactive({ pageNo: 1, @@ -191,6 +307,7 @@ const getList = async () => { const data = await getReviewProjectPage(queryParams) list.value = data.list total.value = data.total + syncInlineSnapshots(data.list || []) } finally { loading.value = false await nextTick() @@ -198,6 +315,70 @@ const getList = async () => { } } +const buildInlineFields = (row: ReviewMeetingProjectRespVO): InlineEditableFields => ({ + seqNo: row.seqNo, + startTime: row.startTime, + endTime: row.endTime, + agendaCategory: row.agendaCategory, + projectTitle: row.projectTitle, + reporter: row.reporter, + reporterUnit: row.reporterUnit, + reviewDate: row.reviewDate, + reviewResult: row.reviewResult +}) + +const syncInlineSnapshots = (rows: ReviewMeetingProjectRespVO[]) => { + const snapshot: Record = {} + rows.forEach((row) => { + snapshot[row.id] = buildInlineFields(row) + }) + inlineSnapshotMap.value = snapshot +} + +const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditableFields) => { + const normalize = (value: unknown) => (value ?? '') as string | number + return normalize(current.seqNo) !== normalize(snapshot.seqNo) + || normalize(current.startTime) !== normalize(snapshot.startTime) + || normalize(current.endTime) !== normalize(snapshot.endTime) + || normalize(current.agendaCategory) !== normalize(snapshot.agendaCategory) + || normalize(current.projectTitle) !== normalize(snapshot.projectTitle) + || normalize(current.reporter) !== normalize(snapshot.reporter) + || normalize(current.reporterUnit) !== normalize(snapshot.reporterUnit) + || normalize(current.reviewDate) !== normalize(snapshot.reviewDate) + || normalize(current.reviewResult) !== normalize(snapshot.reviewResult) +} + +const saveInlineRow = async (row: ReviewMeetingProjectRespVO, options: { reload?: boolean } = {}) => { + const snapshot = inlineSnapshotMap.value[row.id] + if (!snapshot) return + row.projectTitle = row.projectTitle?.trim() + if (!row.agendaCategory) { + ElMessage.warning('议程分类不能为空') + Object.assign(row, snapshot) + return + } + if (!row.projectTitle) { + ElMessage.warning('项目名称不能为空') + Object.assign(row, snapshot) + return + } + const current = buildInlineFields(row) + if (!isInlineChanged(current, snapshot)) return + try { + await updateReviewProject({ + id: row.id, + ...current + }) + inlineSnapshotMap.value[row.id] = buildInlineFields(row) + if (options.reload) { + await getList() + } + } catch { + Object.assign(row, snapshot) + ElMessage.error('保存失败,已恢复') + } +} + const initSortable = () => { if (!tableRef.value?.$el) return const tbody = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody') @@ -403,6 +584,25 @@ onBeforeUnmount(() => { .status-2 { color: #999; } .status-3 { color: #999; } +.review-result-pass { + color: #67c23a; + font-weight: 500; +} +.review-result-reject { + color: #f56c6c; + font-weight: 500; +} +.material-complete { + color: #67c23a !important; + font-weight: 600; + font-size: 16px; +} +.material-incomplete { + color: #f56c6c !important; + font-weight: 600; + font-size: 16px; +} + /* ── 搜索栏 ── */ .search-bar { display: flex; @@ -497,6 +697,42 @@ onBeforeUnmount(() => { font-size: 14px; } +.inline-time-range { + display: flex; + align-items: center; + gap: 6px; +} +.inline-time-sep { + color: #999; +} +.inline-time { + width: 92px; +} +.inline-field { + width: 100%; +} +:deep(.inline-field .el-input__wrapper), +:deep(.inline-field .el-select__wrapper), +:deep(.inline-time .el-input__wrapper) { + border-radius: 6px; + border: 1px solid transparent; + box-shadow: none; + background-color: #f8fafc; + transition: border-color 0.2s, background-color 0.2s; +} +:deep(.review-table .el-table__body tr:hover .inline-field .el-input__wrapper), +:deep(.review-table .el-table__body tr:hover .inline-field .el-select__wrapper), +:deep(.review-table .el-table__body tr:hover .inline-time .el-input__wrapper) { + border-color: #d8e0ec; + background-color: #fff; +} +:deep(.inline-field .el-input__wrapper.is-focus), +:deep(.inline-field .el-select__wrapper.is-focused), +:deep(.inline-time .el-input__wrapper.is-focus) { + border-color: #295abc; + background-color: #fff; +} + /* ── 操作链接 ── */ .op-link { display: inline-block; @@ -521,6 +757,8 @@ onBeforeUnmount(() => { font-size: 14px; color: #333; border-color: #e1e7f0; + padding-top: 10px; + padding-bottom: 10px; } :deep(.review-table .el-table__body tr:hover > td) { background-color: rgba(41, 90, 188, 0.04);