fix(review-meeting): 修复编辑草稿重复数据与时间同步
parent
0975fa44bc
commit
ac37b9eb26
|
|
@ -0,0 +1,53 @@
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import {
|
||||||
|
buildProjectTimeBatchPayload,
|
||||||
|
mapCopiedProjectItems,
|
||||||
|
mapPersistedProjectItems,
|
||||||
|
shouldSyncPersistedProjectTimes
|
||||||
|
} from '../src/views/review/meeting/meetingEditHelpers'
|
||||||
|
|
||||||
|
const baseProject = {
|
||||||
|
id: 101,
|
||||||
|
seqNo: 1,
|
||||||
|
startTime: '09:00',
|
||||||
|
endTime: '09:15',
|
||||||
|
agendaCategory: '项目申报论证',
|
||||||
|
projectTitle: '项目A',
|
||||||
|
reporter: '张三',
|
||||||
|
reporterUnit: '软件学院',
|
||||||
|
reviewDate: '2026-04-01'
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistedProjects = mapPersistedProjectItems([baseProject])
|
||||||
|
assert.equal(
|
||||||
|
persistedProjects[0].sourceProjectId,
|
||||||
|
undefined,
|
||||||
|
'编辑已有会议时,不应该把现有项目 id 回填到 sourceProjectId'
|
||||||
|
)
|
||||||
|
|
||||||
|
const copiedProjects = mapCopiedProjectItems([baseProject])
|
||||||
|
assert.equal(copiedProjects[0].sourceProjectId, 101, '复制会议时,应该保留源项目 id 用于首次复制资料')
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
shouldSyncPersistedProjectTimes({
|
||||||
|
isEdit: true,
|
||||||
|
isProjectsModified: false,
|
||||||
|
originalMeetingStart: 1711942800000,
|
||||||
|
currentMeetingStart: 1711944600000,
|
||||||
|
projects: [baseProject]
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
'编辑已有会议且仅修改会议开始时间时,应该同步已落库项目时间'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(buildProjectTimeBatchPayload([baseProject]), {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
startTime: '09:00',
|
||||||
|
endTime: '09:15'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('reviewMeetingEditHelpers checks passed')
|
||||||
|
|
@ -192,7 +192,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import type { FormRules } from 'element-plus'
|
import type { FormRules } from 'element-plus'
|
||||||
|
|
@ -210,7 +210,7 @@ import {
|
||||||
type ReviewMeetingSaveReqVO,
|
type ReviewMeetingSaveReqVO,
|
||||||
type ReviewProjectItemVO
|
type ReviewProjectItemVO
|
||||||
} from '@/api/review/meeting'
|
} from '@/api/review/meeting'
|
||||||
import { getReviewProjectPage } from '@/api/review/project'
|
import { getReviewProjectPage, updateReviewProjectTimeBatch } from '@/api/review/project'
|
||||||
import { getExpertUserList } from '@/api/system/user/index'
|
import { getExpertUserList } from '@/api/system/user/index'
|
||||||
import download from '@/utils/download'
|
import download from '@/utils/download'
|
||||||
import ExpertSelectTable from './components/ExpertSelectTable.vue'
|
import ExpertSelectTable from './components/ExpertSelectTable.vue'
|
||||||
|
|
@ -219,6 +219,13 @@ import {
|
||||||
buildScheduledProjectItems,
|
buildScheduledProjectItems,
|
||||||
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||||
} from './projectSchedule'
|
} from './projectSchedule'
|
||||||
|
import {
|
||||||
|
buildProjectTimeBatchPayload,
|
||||||
|
mapCopiedProjectItems,
|
||||||
|
mapPersistedProjectItems,
|
||||||
|
shouldSyncPersistedProjectTimes,
|
||||||
|
type MeetingEditProjectItem
|
||||||
|
} from './meetingEditHelpers'
|
||||||
|
|
||||||
defineOptions({ name: 'ReviewMeetingEdit' })
|
defineOptions({ name: 'ReviewMeetingEdit' })
|
||||||
|
|
||||||
|
|
@ -254,6 +261,7 @@ type FormData = ReviewMeetingSaveReqVO & {
|
||||||
organizationUnit?: string
|
organizationUnit?: string
|
||||||
meetingTimeRange?: any[]
|
meetingTimeRange?: any[]
|
||||||
materialViewTimeRange?: any[]
|
materialViewTimeRange?: any[]
|
||||||
|
projects: MeetingEditProjectItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = reactive<FormData>({
|
const formData = reactive<FormData>({
|
||||||
|
|
@ -292,26 +300,28 @@ const rules: FormRules = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
const originalMeetingStart = ref<string | number | undefined>()
|
||||||
|
|
||||||
const mapProjectItems = (projects: any[]): ReviewProjectItemVO[] =>
|
const resetProjectReviewDate = (projects: MeetingEditProjectItem[]): MeetingEditProjectItem[] =>
|
||||||
(projects || []).map((item: any) => ({
|
|
||||||
sourceProjectId: item.sourceProjectId ?? item.id,
|
|
||||||
seqNo: item.seqNo,
|
|
||||||
startTime: item.startTime,
|
|
||||||
endTime: item.endTime,
|
|
||||||
agendaCategory: item.agendaCategory,
|
|
||||||
projectTitle: item.projectTitle,
|
|
||||||
reporter: item.reporter,
|
|
||||||
reporterUnit: item.reporterUnit,
|
|
||||||
reviewDate: item.reviewDate
|
|
||||||
}))
|
|
||||||
|
|
||||||
const resetProjectReviewDate = (projects: ReviewProjectItemVO[]): ReviewProjectItemVO[] =>
|
|
||||||
(projects || []).map((item) => ({
|
(projects || []).map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
reviewDate: undefined
|
reviewDate: undefined
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const buildPreviewScheduledProjects = (projects: MeetingEditProjectItem[] = formData.projects): MeetingEditProjectItem[] => {
|
||||||
|
const projectsWithReviewDate = applyDefaultReviewDate(projects, formData.meetingTimeRange) as MeetingEditProjectItem[]
|
||||||
|
return (
|
||||||
|
buildScheduledProjectItems(
|
||||||
|
projectsWithReviewDate,
|
||||||
|
formData.meetingTimeRange?.[0],
|
||||||
|
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||||
|
) || projectsWithReviewDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripProjectTransientFields = (projects: MeetingEditProjectItem[]): ReviewProjectItemVO[] =>
|
||||||
|
(projects || []).map(({ id: _id, ...item }) => item)
|
||||||
|
|
||||||
const loadDetail = async (id: number) => {
|
const loadDetail = async (id: number) => {
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
|
@ -320,7 +330,7 @@ const loadDetail = async (id: number) => {
|
||||||
getReviewProjectPage({ reviewMeetingId: id, pageNo: 1, pageSize: 200 })
|
getReviewProjectPage({ reviewMeetingId: id, pageNo: 1, pageSize: 200 })
|
||||||
])
|
])
|
||||||
Object.assign(formData, detail)
|
Object.assign(formData, detail)
|
||||||
formData.projects = mapProjectItems(projectData?.list ?? [])
|
formData.projects = mapPersistedProjectItems(projectData?.list ?? [])
|
||||||
if (detail.startTime && detail.endTime) {
|
if (detail.startTime && detail.endTime) {
|
||||||
formData.meetingTimeRange = [
|
formData.meetingTimeRange = [
|
||||||
new Date(detail.startTime.replace(' ', 'T')).getTime(),
|
new Date(detail.startTime.replace(' ', 'T')).getTime(),
|
||||||
|
|
@ -333,6 +343,7 @@ const loadDetail = async (id: number) => {
|
||||||
new Date(detail.materialViewEndTime.replace(' ', 'T')).getTime()
|
new Date(detail.materialViewEndTime.replace(' ', 'T')).getTime()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
originalMeetingStart.value = formData.meetingTimeRange?.[0]
|
||||||
} finally {
|
} finally {
|
||||||
formLoading.value = false
|
formLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -370,7 +381,7 @@ const loadCopySource = async (id: number) => {
|
||||||
formData.minutesAttachmentType = undefined
|
formData.minutesAttachmentType = undefined
|
||||||
formData.minutesAttachmentSize = undefined
|
formData.minutesAttachmentSize = undefined
|
||||||
formData.expertIds = detail.expertIds || []
|
formData.expertIds = detail.expertIds || []
|
||||||
formData.projects = resetProjectReviewDate(mapProjectItems(projectData?.list ?? []))
|
formData.projects = resetProjectReviewDate(mapCopiedProjectItems(projectData?.list ?? []))
|
||||||
isProjectsModified.value = false
|
isProjectsModified.value = false
|
||||||
|
|
||||||
if (detail.startTime && detail.endTime) {
|
if (detail.startTime && detail.endTime) {
|
||||||
|
|
@ -389,6 +400,7 @@ const loadCopySource = async (id: number) => {
|
||||||
} else {
|
} else {
|
||||||
formData.materialViewTimeRange = undefined
|
formData.materialViewTimeRange = undefined
|
||||||
}
|
}
|
||||||
|
originalMeetingStart.value = formData.meetingTimeRange?.[0]
|
||||||
|
|
||||||
ElMessage.info('已带入会议信息和评审项目;保存草稿后将同步复制项目资料,议程附件与会议纪要不会复制')
|
ElMessage.info('已带入会议信息和评审项目;保存草稿后将同步复制项目资料,议程附件与会议纪要不会复制')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -407,6 +419,16 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => formData.meetingTimeRange?.[0],
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
if (newValue === oldValue || oldValue === undefined || !formData.projects.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formData.projects = buildPreviewScheduledProjects(formData.projects)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const handleExcelChange = async (uploadFile: UploadFile) => {
|
const handleExcelChange = async (uploadFile: UploadFile) => {
|
||||||
if (!uploadFile.raw) return
|
if (!uploadFile.raw) return
|
||||||
if (formData.projects && formData.projects.length > 0) {
|
if (formData.projects && formData.projects.length > 0) {
|
||||||
|
|
@ -416,7 +438,8 @@ const handleExcelChange = async (uploadFile: UploadFile) => {
|
||||||
try {
|
try {
|
||||||
const result = await importProjectsFromExcel(uploadFile.raw)
|
const result = await importProjectsFromExcel(uploadFile.raw)
|
||||||
const projects = applyDefaultReviewDate(result as ReviewProjectItemVO[], formData.meetingTimeRange)
|
const projects = applyDefaultReviewDate(result as ReviewProjectItemVO[], formData.meetingTimeRange)
|
||||||
formData.projects = buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) || projects
|
formData.projects = (buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) ||
|
||||||
|
projects) as MeetingEditProjectItem[]
|
||||||
isProjectsModified.value = true
|
isProjectsModified.value = true
|
||||||
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
|
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -522,8 +545,8 @@ const formatFileSize = (bytes?: number): string => {
|
||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildScheduledProjects = (): ReviewProjectItemVO[] => {
|
const buildScheduledProjects = (): MeetingEditProjectItem[] => {
|
||||||
const projectsWithReviewDate = applyDefaultReviewDate(formData.projects, formData.meetingTimeRange)
|
const projectsWithReviewDate = applyDefaultReviewDate(formData.projects, formData.meetingTimeRange) as MeetingEditProjectItem[]
|
||||||
const hasCompleteSchedule = projectsWithReviewDate.every((item) => item.startTime && item.endTime)
|
const hasCompleteSchedule = projectsWithReviewDate.every((item) => item.startTime && item.endTime)
|
||||||
if (hasCompleteSchedule) {
|
if (hasCompleteSchedule) {
|
||||||
return projectsWithReviewDate
|
return projectsWithReviewDate
|
||||||
|
|
@ -620,18 +643,34 @@ const submitForm = async () => {
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const projects = buildScheduledProjects()
|
const projects = buildScheduledProjects()
|
||||||
|
const submitProjects = stripProjectTransientFields(projects)
|
||||||
const submitData: ReviewMeetingSaveReqVO & { projects?: ReviewProjectItemVO[] } = {
|
const submitData: ReviewMeetingSaveReqVO & { projects?: ReviewProjectItemVO[] } = {
|
||||||
...formData,
|
...formData,
|
||||||
projects
|
projects: submitProjects
|
||||||
}
|
}
|
||||||
if (isEdit.value && !isProjectsModified.value) {
|
if (isEdit.value && !isProjectsModified.value) {
|
||||||
delete submitData.projects
|
delete submitData.projects
|
||||||
}
|
}
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await updateReviewMeeting(submitData)
|
await updateReviewMeeting(submitData)
|
||||||
|
const shouldSyncProjectTimes = shouldSyncPersistedProjectTimes({
|
||||||
|
isEdit: isEdit.value,
|
||||||
|
isProjectsModified: isProjectsModified.value,
|
||||||
|
originalMeetingStart: originalMeetingStart.value,
|
||||||
|
currentMeetingStart: formData.meetingTimeRange?.[0],
|
||||||
|
projects
|
||||||
|
})
|
||||||
|
if (shouldSyncProjectTimes) {
|
||||||
|
const timeBatchPayload = buildProjectTimeBatchPayload(projects)
|
||||||
|
if (timeBatchPayload) {
|
||||||
|
await updateReviewProjectTimeBatch(timeBatchPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
originalMeetingStart.value = formData.meetingTimeRange?.[0]
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
} else {
|
} else {
|
||||||
await createReviewMeeting(submitData)
|
await createReviewMeeting(submitData)
|
||||||
|
originalMeetingStart.value = formData.meetingTimeRange?.[0]
|
||||||
ElMessage.success('创建成功,会议已保存为草稿')
|
ElMessage.success('创建成功,会议已保存为草稿')
|
||||||
}
|
}
|
||||||
router.push({ name: 'ReviewMeeting' })
|
router.push({ name: 'ReviewMeeting' })
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
export type MeetingEditProjectItem = {
|
||||||
|
id?: number
|
||||||
|
sourceProjectId?: number
|
||||||
|
seqNo: number
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
agendaCategory: string
|
||||||
|
projectTitle: string
|
||||||
|
reporter: string
|
||||||
|
reporterUnit: string
|
||||||
|
reviewDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MeetingProjectSource = {
|
||||||
|
id?: number
|
||||||
|
seqNo?: number
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
agendaCategory?: string
|
||||||
|
projectTitle?: string
|
||||||
|
reporter?: string
|
||||||
|
reporterUnit?: string
|
||||||
|
reviewDate?: string
|
||||||
|
sourceProjectId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectTimeBatchPayload = {
|
||||||
|
items: Array<{
|
||||||
|
id: number
|
||||||
|
startTime: string
|
||||||
|
endTime: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersistedProjectScheduleSyncArgs = {
|
||||||
|
isEdit: boolean
|
||||||
|
isProjectsModified: boolean
|
||||||
|
originalMeetingStart?: string | number
|
||||||
|
currentMeetingStart?: string | number
|
||||||
|
projects: MeetingProjectSource[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapProjectItems = (projects: MeetingProjectSource[], copySourceProjectId: boolean): MeetingEditProjectItem[] =>
|
||||||
|
(projects || []).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
sourceProjectId: copySourceProjectId ? (item.sourceProjectId ?? item.id) : item.sourceProjectId,
|
||||||
|
seqNo: item.seqNo || 0,
|
||||||
|
startTime: item.startTime,
|
||||||
|
endTime: item.endTime,
|
||||||
|
agendaCategory: item.agendaCategory || '',
|
||||||
|
projectTitle: item.projectTitle || '',
|
||||||
|
reporter: item.reporter || '',
|
||||||
|
reporterUnit: item.reporterUnit || '',
|
||||||
|
reviewDate: item.reviewDate
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const mapPersistedProjectItems = (projects: MeetingProjectSource[] = []): MeetingEditProjectItem[] =>
|
||||||
|
mapProjectItems(projects, false)
|
||||||
|
|
||||||
|
export const mapCopiedProjectItems = (projects: MeetingProjectSource[] = []): MeetingEditProjectItem[] =>
|
||||||
|
mapProjectItems(projects, true)
|
||||||
|
|
||||||
|
const normalizeComparableValue = (value?: string | number) =>
|
||||||
|
value === undefined || value === null || value === '' ? undefined : String(value)
|
||||||
|
|
||||||
|
export const shouldSyncPersistedProjectTimes = ({
|
||||||
|
isEdit,
|
||||||
|
isProjectsModified,
|
||||||
|
originalMeetingStart,
|
||||||
|
currentMeetingStart,
|
||||||
|
projects
|
||||||
|
}: PersistedProjectScheduleSyncArgs) => {
|
||||||
|
if (!isEdit || isProjectsModified) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!projects.some((item) => typeof item.id === 'number')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return normalizeComparableValue(originalMeetingStart) !== normalizeComparableValue(currentMeetingStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildProjectTimeBatchPayload = (
|
||||||
|
projects: MeetingProjectSource[] = []
|
||||||
|
): ProjectTimeBatchPayload | undefined => {
|
||||||
|
const items = projects
|
||||||
|
.filter((item) => item.id && item.startTime && item.endTime)
|
||||||
|
.map((item) => ({
|
||||||
|
id: item.id as number,
|
||||||
|
startTime: item.startTime as string,
|
||||||
|
endTime: item.endTime as string
|
||||||
|
}))
|
||||||
|
return items.length > 0 ? { items } : undefined
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue