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 @@
+
+
+
+ {{ row.preMeetingMaterialsComplete ? '✓' : '✗' }}
+
+
+
+
+
+
+ {{ row.postMeetingMaterialsComplete ? '✓' : '✗' }}
+
+
+
- {{ REVIEW_RESULT_LABEL[row.reviewResult as keyof typeof REVIEW_RESULT_LABEL] || '-' }}
+
+ {{ REVIEW_RESULT_LABEL[row.reviewResult as keyof typeof REVIEW_RESULT_LABEL] || '-' }}
+
@@ -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 @@
-
-
-
+
+
+
⋮⋮
-
-
-
+
+
+
- {{ row.startTime || '' }} - {{ row.endTime || '' }}
- -
+
+
+ -
+
+
-
-
+
- {{ row.projectTitle }}
+
+
+
-
-
-
-
+
- {{ REVIEW_RESULT_LABEL[row.reviewResult as keyof typeof REVIEW_RESULT_LABEL] || '-' }}
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.preMeetingMaterialsComplete ? '✓' : '✗' }}
+
+
+
+
+
+
+ {{ row.postMeetingMaterialsComplete ? '✓' : '✗' }}
+
+
+
+
+
+
+
+
+
+
+
+
- 编辑
上传项目资料
删除
@@ -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);