feat(review-meeting): 优化项目排程与导入展示
parent
9113f446e6
commit
9251c64067
|
|
@ -10,12 +10,13 @@ import * as FileApi from '@/api/infra/file'
|
||||||
export interface ReviewProjectItemVO {
|
export interface ReviewProjectItemVO {
|
||||||
sourceProjectId?: number
|
sourceProjectId?: number
|
||||||
seqNo: number
|
seqNo: number
|
||||||
startTime: string
|
startTime?: string
|
||||||
endTime: string
|
endTime?: string
|
||||||
agendaCategory: string
|
agendaCategory: string
|
||||||
projectTitle: string
|
projectTitle: string
|
||||||
reporter: string
|
reporter: string
|
||||||
reporterUnit: string
|
reporterUnit: string
|
||||||
|
reviewDate?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 会议保存 VO(新增/编辑)
|
// 会议保存 VO(新增/编辑)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,19 @@ export interface ReviewMeetingProjectSeqUpdateReqVO {
|
||||||
seqNo: number
|
seqNo: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReviewMeetingProjectResultBatchUpdateReqVO {
|
||||||
|
ids: number[]
|
||||||
|
reviewResult: 'PASS' | 'REJECT'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewMeetingProjectTimeBatchUpdateReqVO {
|
||||||
|
items: Array<{
|
||||||
|
id: number
|
||||||
|
startTime: string
|
||||||
|
endTime: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReviewMeetingProjectPageReqVO {
|
export interface ReviewMeetingProjectPageReqVO {
|
||||||
pageNo?: number
|
pageNo?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
|
|
@ -141,6 +154,14 @@ export const updateReviewProject = (data: Partial<ReviewMeetingProjectRespVO>) =
|
||||||
export const updateReviewProjectSeqBatch = (data: ReviewMeetingProjectSeqUpdateReqVO[]) =>
|
export const updateReviewProjectSeqBatch = (data: ReviewMeetingProjectSeqUpdateReqVO[]) =>
|
||||||
request.put({ url: '/project/review-project/update-seq-batch', data })
|
request.put({ url: '/project/review-project/update-seq-batch', data })
|
||||||
|
|
||||||
|
/** 批量更新评审结果 */
|
||||||
|
export const updateReviewProjectResultBatch = (data: ReviewMeetingProjectResultBatchUpdateReqVO) =>
|
||||||
|
request.put({ url: '/project/review-project/update-result-batch', data })
|
||||||
|
|
||||||
|
/** 批量更新评审项目起止时间 */
|
||||||
|
export const updateReviewProjectTimeBatch = (data: ReviewMeetingProjectTimeBatchUpdateReqVO) =>
|
||||||
|
request.put({ url: '/project/review-project/update-time-batch', data })
|
||||||
|
|
||||||
/** 删除评审项目 */
|
/** 删除评审项目 */
|
||||||
export const deleteReviewProject = (ids: number[]) =>
|
export const deleteReviewProject = (ids: number[]) =>
|
||||||
request.delete({ url: '/project/review-project/delete', params: { ids: ids.join(',') } })
|
request.delete({ url: '/project/review-project/delete', params: { ids: ids.join(',') } })
|
||||||
|
|
|
||||||
|
|
@ -157,14 +157,17 @@
|
||||||
<button type="button" class="btn-default">导入验收申请 Excel</button>
|
<button type="button" class="btn-default">导入验收申请 Excel</button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<button type="button" class="btn-default" @click="handleDownloadTemplate">下载导入模板</button>
|
<button type="button" class="btn-default" @click="handleDownloadTemplate">下载导入模板</button>
|
||||||
<span class="import-hint">格式:序号、开始时间、结束时间、议程分类、项目标题、汇报人、报告人单位</span>
|
<span class="import-hint">格式:序号、议程分类、项目标题、汇报人、报告人单位</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="formData.projects && formData.projects.length > 0" class="mt-10">
|
<div v-if="formData.projects && formData.projects.length > 0" class="mt-10">
|
||||||
<el-table :data="formData.projects" border max-height="360" class="projects-preview-table">
|
<el-table :data="formData.projects" border max-height="360" class="projects-preview-table">
|
||||||
<el-table-column label="序号" prop="seqNo" width="60" align="center" />
|
<el-table-column label="序号" prop="seqNo" width="60" align="center" />
|
||||||
<el-table-column label="开始时间" prop="startTime" width="80" align="center" />
|
<el-table-column label="起止时间" width="120" align="center">
|
||||||
<el-table-column label="结束时间" prop="endTime" width="80" align="center" />
|
<template #default="{ row }">
|
||||||
|
{{ row.startTime || '--:--' }} - {{ row.endTime || '--:--' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="议程分类" prop="agendaCategory" width="110" />
|
<el-table-column label="议程分类" prop="agendaCategory" width="110" />
|
||||||
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip />
|
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip />
|
||||||
<el-table-column label="汇报人" prop="reporter" width="80" />
|
<el-table-column label="汇报人" prop="reporter" width="80" />
|
||||||
|
|
@ -205,6 +208,11 @@ import { getReviewProjectPage } 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'
|
||||||
|
import { applyDefaultReviewDate } from './projectReviewDate'
|
||||||
|
import {
|
||||||
|
buildScheduledProjectItems,
|
||||||
|
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||||
|
} from './projectSchedule'
|
||||||
|
|
||||||
defineOptions({ name: 'ReviewMeetingEdit' })
|
defineOptions({ name: 'ReviewMeetingEdit' })
|
||||||
|
|
||||||
|
|
@ -288,7 +296,14 @@ const mapProjectItems = (projects: any[]): ReviewProjectItemVO[] =>
|
||||||
agendaCategory: item.agendaCategory,
|
agendaCategory: item.agendaCategory,
|
||||||
projectTitle: item.projectTitle,
|
projectTitle: item.projectTitle,
|
||||||
reporter: item.reporter,
|
reporter: item.reporter,
|
||||||
reporterUnit: item.reporterUnit
|
reporterUnit: item.reporterUnit,
|
||||||
|
reviewDate: item.reviewDate
|
||||||
|
}))
|
||||||
|
|
||||||
|
const resetProjectReviewDate = (projects: ReviewProjectItemVO[]): ReviewProjectItemVO[] =>
|
||||||
|
(projects || []).map((item) => ({
|
||||||
|
...item,
|
||||||
|
reviewDate: undefined
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const loadDetail = async (id: number) => {
|
const loadDetail = async (id: number) => {
|
||||||
|
|
@ -349,7 +364,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 = mapProjectItems(projectData?.list ?? [])
|
formData.projects = resetProjectReviewDate(mapProjectItems(projectData?.list ?? []))
|
||||||
isProjectsModified.value = false
|
isProjectsModified.value = false
|
||||||
|
|
||||||
if (detail.startTime && detail.endTime) {
|
if (detail.startTime && detail.endTime) {
|
||||||
|
|
@ -394,7 +409,8 @@ const handleExcelChange = async (uploadFile: UploadFile) => {
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await importProjectsFromExcel(uploadFile.raw)
|
const result = await importProjectsFromExcel(uploadFile.raw)
|
||||||
formData.projects = result as ReviewProjectItemVO[]
|
const projects = applyDefaultReviewDate(result as ReviewProjectItemVO[], formData.meetingTimeRange)
|
||||||
|
formData.projects = buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) || projects
|
||||||
isProjectsModified.value = true
|
isProjectsModified.value = true
|
||||||
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
|
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -516,7 +532,15 @@ const submitForm = async () => {
|
||||||
}
|
}
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const submitData = { ...formData }
|
const projects = buildScheduledProjectItems(
|
||||||
|
applyDefaultReviewDate(formData.projects, formData.meetingTimeRange),
|
||||||
|
formData.meetingTimeRange?.[0],
|
||||||
|
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||||
|
) || applyDefaultReviewDate(formData.projects, formData.meetingTimeRange)
|
||||||
|
const submitData = {
|
||||||
|
...formData,
|
||||||
|
projects
|
||||||
|
}
|
||||||
if (isEdit.value && !isProjectsModified.value) {
|
if (isEdit.value && !isProjectsModified.value) {
|
||||||
delete submitData.projects
|
delete submitData.projects
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,15 +110,18 @@
|
||||||
<el-button type="primary" plain>导入验收申请 Excel</el-button>
|
<el-button type="primary" plain>导入验收申请 Excel</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<el-button type="success" plain @click="handleDownloadTemplate">下载导入模板</el-button>
|
<el-button type="success" plain @click="handleDownloadTemplate">下载导入模板</el-button>
|
||||||
<el-text type="info" size="small" class="ml-10">格式:序号、开始时间、结束时间、议程分类、项目标题、汇报人、报告人单位</el-text>
|
<el-text type="info" size="small" class="ml-10">格式:序号、议程分类、项目标题、汇报人、报告人单位</el-text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 评审项目预览列表 -->
|
<!-- 评审项目预览列表 -->
|
||||||
<div v-if="formData.projects && formData.projects.length > 0" class="mt-10">
|
<div v-if="formData.projects && formData.projects.length > 0" class="mt-10">
|
||||||
<el-table :data="formData.projects" border size="small" max-height="300">
|
<el-table :data="formData.projects" border size="small" max-height="300">
|
||||||
<el-table-column label="序号" prop="seqNo" width="60" align="center" />
|
<el-table-column label="序号" prop="seqNo" width="60" align="center" />
|
||||||
<el-table-column label="开始时间" prop="startTime" width="80" align="center" />
|
<el-table-column label="起止时间" width="120" align="center">
|
||||||
<el-table-column label="结束时间" prop="endTime" width="80" align="center" />
|
<template #default="{ row }">
|
||||||
|
{{ row.startTime || '--:--' }} - {{ row.endTime || '--:--' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="议程分类" prop="agendaCategory" width="100" />
|
<el-table-column label="议程分类" prop="agendaCategory" width="100" />
|
||||||
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip />
|
<el-table-column label="评审项目标题" prop="projectTitle" show-overflow-tooltip />
|
||||||
<el-table-column label="汇报人" prop="reporter" width="80" />
|
<el-table-column label="汇报人" prop="reporter" width="80" />
|
||||||
|
|
@ -151,6 +154,11 @@ import {
|
||||||
} from '@/api/review/meeting'
|
} from '@/api/review/meeting'
|
||||||
import { getExpertUserList } from '@/api/system/user'
|
import { getExpertUserList } from '@/api/system/user'
|
||||||
import download from '@/utils/download'
|
import download from '@/utils/download'
|
||||||
|
import { applyDefaultReviewDate } from './projectReviewDate'
|
||||||
|
import {
|
||||||
|
buildScheduledProjectItems,
|
||||||
|
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||||
|
} from './projectSchedule'
|
||||||
|
|
||||||
defineOptions({ name: 'ReviewMeetingForm' })
|
defineOptions({ name: 'ReviewMeetingForm' })
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
|
|
@ -257,7 +265,11 @@ const handleExcelChange = async (uploadFile: UploadFile) => {
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await importProjectsFromExcel(uploadFile.raw)
|
const result = await importProjectsFromExcel(uploadFile.raw)
|
||||||
formData.projects = ((result as any).data || result) as ReviewProjectItemVO[]
|
const projects = applyDefaultReviewDate(
|
||||||
|
((result as any).data || result) as ReviewProjectItemVO[],
|
||||||
|
formData.meetingTimeRange
|
||||||
|
)
|
||||||
|
formData.projects = buildScheduledProjectItems(projects, formData.meetingTimeRange?.[0]) || projects
|
||||||
isProjectsModified.value = true
|
isProjectsModified.value = true
|
||||||
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
|
ElMessage.success(`成功解析 ${formData.projects.length} 个评审项目`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -340,7 +352,15 @@ const submitForm = async () => {
|
||||||
}
|
}
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const submitData = { ...formData }
|
const projects = buildScheduledProjectItems(
|
||||||
|
applyDefaultReviewDate(formData.projects, formData.meetingTimeRange),
|
||||||
|
formData.meetingTimeRange?.[0],
|
||||||
|
DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||||
|
) || applyDefaultReviewDate(formData.projects, formData.meetingTimeRange)
|
||||||
|
const submitData = {
|
||||||
|
...formData,
|
||||||
|
projects
|
||||||
|
}
|
||||||
if (formType.value === 'update' && !isProjectsModified.value) {
|
if (formType.value === 'update' && !isProjectsModified.value) {
|
||||||
delete submitData.projects
|
delete submitData.projects
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,35 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
|
<div class="table-toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<div class="duration-card">
|
||||||
|
<span class="toolbar-label">项目汇报时长</span>
|
||||||
|
<el-select
|
||||||
|
v-model="scheduleInterval"
|
||||||
|
class="interval-select"
|
||||||
|
size="small"
|
||||||
|
@change="handleIntervalChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in REVIEW_MEETING_INTERVAL_OPTIONS"
|
||||||
|
:key="item"
|
||||||
|
:label="`${item}分钟`"
|
||||||
|
:value="item"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="selectedRows.length === 0"
|
||||||
|
:loading="batchPassLoading"
|
||||||
|
@click="handleBatchPass"
|
||||||
|
>
|
||||||
|
批量通过
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<span class="selection-summary">已选 {{ selectedRows.length }} 项</span>
|
||||||
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
v-loading="loading || sortLoading"
|
v-loading="loading || sortLoading"
|
||||||
|
|
@ -46,7 +75,9 @@
|
||||||
size="small"
|
size="small"
|
||||||
border
|
border
|
||||||
class="review-table"
|
class="review-table"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
>
|
>
|
||||||
|
<el-table-column type="selection" width="52" align="center" />
|
||||||
<el-table-column label="拖拽" width="54" align="center">
|
<el-table-column label="拖拽" width="54" align="center">
|
||||||
<template #default>
|
<template #default>
|
||||||
<span class="drag-handle" title="拖拽排序">⋮⋮</span>
|
<span class="drag-handle" title="拖拽排序">⋮⋮</span>
|
||||||
|
|
@ -56,26 +87,10 @@
|
||||||
<el-table-column label="会中序号" prop="seqNo" width="72" align="center" />
|
<el-table-column label="会中序号" prop="seqNo" width="72" align="center" />
|
||||||
<el-table-column label="起止时间" width="236" align="center">
|
<el-table-column label="起止时间" width="236" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="inline-time-range">
|
<div class="readonly-time-range">
|
||||||
<el-time-picker
|
<span class="readonly-time">{{ row.startTime || '--:--' }}</span>
|
||||||
v-model="row.startTime"
|
|
||||||
class="inline-time"
|
|
||||||
size="small"
|
|
||||||
format="HH:mm"
|
|
||||||
value-format="HH:mm"
|
|
||||||
placeholder="开始"
|
|
||||||
@change="saveInlineRow(row)"
|
|
||||||
/>
|
|
||||||
<span class="inline-time-sep">-</span>
|
<span class="inline-time-sep">-</span>
|
||||||
<el-time-picker
|
<span class="readonly-time">{{ row.endTime || '--:--' }}</span>
|
||||||
v-model="row.endTime"
|
|
||||||
class="inline-time"
|
|
||||||
size="small"
|
|
||||||
format="HH:mm"
|
|
||||||
value-format="HH:mm"
|
|
||||||
placeholder="结束"
|
|
||||||
@change="saveInlineRow(row)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
@ -197,24 +212,34 @@ import {
|
||||||
REVIEW_AGENDA_CATEGORY_OPTIONS,
|
REVIEW_AGENDA_CATEGORY_OPTIONS,
|
||||||
getReviewProjectPage,
|
getReviewProjectPage,
|
||||||
updateReviewProject,
|
updateReviewProject,
|
||||||
|
updateReviewProjectResultBatch,
|
||||||
updateReviewProjectSeqBatch,
|
updateReviewProjectSeqBatch,
|
||||||
|
updateReviewProjectTimeBatch,
|
||||||
deleteReviewProject,
|
deleteReviewProject,
|
||||||
type ReviewMeetingProjectRespVO
|
type ReviewMeetingProjectRespVO
|
||||||
} from '@/api/review/project'
|
} from '@/api/review/project'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatDate } from '@/utils/formatTime'
|
||||||
|
import {
|
||||||
|
buildScheduledProjects,
|
||||||
|
REVIEW_MEETING_INTERVAL_OPTIONS,
|
||||||
|
type ReviewMeetingIntervalMinutes
|
||||||
|
} from './projectSchedule'
|
||||||
|
|
||||||
defineOptions({ name: 'ReviewMeetingProject' })
|
defineOptions({ name: 'ReviewMeetingProject' })
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const reviewMeetingId = Number(route.params.meetingId)
|
const reviewMeetingId = Number(route.params.meetingId)
|
||||||
|
const scheduleInterval = ref<ReviewMeetingIntervalMinutes>(15)
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const sortLoading = ref(false)
|
const sortLoading = ref(false)
|
||||||
|
const batchPassLoading = ref(false)
|
||||||
const list = ref<ReviewMeetingProjectRespVO[]>([])
|
const list = ref<ReviewMeetingProjectRespVO[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const meetingInfo = ref<any>({})
|
const meetingInfo = ref<any>({})
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
|
const selectedRows = ref<ReviewMeetingProjectRespVO[]>([])
|
||||||
let sortableInstance: Sortable | null = null
|
let sortableInstance: Sortable | null = null
|
||||||
|
|
||||||
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
|
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
|
||||||
|
|
@ -225,8 +250,6 @@ const getMaterialCompleteClass = (complete?: boolean) => {
|
||||||
}
|
}
|
||||||
type InlineEditableFields = {
|
type InlineEditableFields = {
|
||||||
seqNo?: number
|
seqNo?: number
|
||||||
startTime?: string
|
|
||||||
endTime?: string
|
|
||||||
agendaCategory?: string
|
agendaCategory?: string
|
||||||
projectTitle?: string
|
projectTitle?: string
|
||||||
reporter?: string
|
reporter?: string
|
||||||
|
|
@ -248,18 +271,18 @@ const getList = async () => {
|
||||||
const data = await getReviewProjectPage(queryParams)
|
const data = await getReviewProjectPage(queryParams)
|
||||||
list.value = data.list
|
list.value = data.list
|
||||||
total.value = data.total
|
total.value = data.total
|
||||||
|
selectedRows.value = []
|
||||||
syncInlineSnapshots(data.list || [])
|
syncInlineSnapshots(data.list || [])
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
tableRef.value?.clearSelection?.()
|
||||||
initSortable()
|
initSortable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildInlineFields = (row: ReviewMeetingProjectRespVO): InlineEditableFields => ({
|
const buildInlineFields = (row: ReviewMeetingProjectRespVO): InlineEditableFields => ({
|
||||||
seqNo: row.seqNo,
|
seqNo: row.seqNo,
|
||||||
startTime: row.startTime,
|
|
||||||
endTime: row.endTime,
|
|
||||||
agendaCategory: row.agendaCategory,
|
agendaCategory: row.agendaCategory,
|
||||||
projectTitle: row.projectTitle,
|
projectTitle: row.projectTitle,
|
||||||
reporter: row.reporter,
|
reporter: row.reporter,
|
||||||
|
|
@ -279,8 +302,6 @@ const syncInlineSnapshots = (rows: ReviewMeetingProjectRespVO[]) => {
|
||||||
const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditableFields) => {
|
const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditableFields) => {
|
||||||
const normalize = (value: unknown) => (value ?? '') as string | number
|
const normalize = (value: unknown) => (value ?? '') as string | number
|
||||||
return normalize(current.seqNo) !== normalize(snapshot.seqNo)
|
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.agendaCategory) !== normalize(snapshot.agendaCategory)
|
||||||
|| normalize(current.projectTitle) !== normalize(snapshot.projectTitle)
|
|| normalize(current.projectTitle) !== normalize(snapshot.projectTitle)
|
||||||
|| normalize(current.reporter) !== normalize(snapshot.reporter)
|
|| normalize(current.reporter) !== normalize(snapshot.reporter)
|
||||||
|
|
@ -289,6 +310,35 @@ const isInlineChanged = (current: InlineEditableFields, snapshot: InlineEditable
|
||||||
|| normalize(current.reviewResult) !== normalize(snapshot.reviewResult)
|
|| normalize(current.reviewResult) !== normalize(snapshot.reviewResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFullProjectList = async () => {
|
||||||
|
const data = await getReviewProjectPage({
|
||||||
|
reviewMeetingId,
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: Math.max(total.value || 0, queryParams.pageSize, 200)
|
||||||
|
})
|
||||||
|
return [...(data.list || [])]
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistScheduledTime = async (projects: ReviewMeetingProjectRespVO[]) => {
|
||||||
|
if (!meetingInfo.value?.startTime) {
|
||||||
|
ElMessage.warning('会议开始时间为空,无法自动编排起止时间')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const scheduledProjects = buildScheduledProjects(projects, meetingInfo.value.startTime, scheduleInterval.value)
|
||||||
|
if (!scheduledProjects) {
|
||||||
|
ElMessage.warning('会议开始时间格式无效,无法自动编排起止时间')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
await updateReviewProjectTimeBatch({
|
||||||
|
items: scheduledProjects.map((project) => ({
|
||||||
|
id: project.id,
|
||||||
|
startTime: project.startTime,
|
||||||
|
endTime: project.endTime
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
return scheduledProjects
|
||||||
|
}
|
||||||
|
|
||||||
const saveInlineRow = async (row: ReviewMeetingProjectRespVO, options: { reload?: boolean } = {}) => {
|
const saveInlineRow = async (row: ReviewMeetingProjectRespVO, options: { reload?: boolean } = {}) => {
|
||||||
const snapshot = inlineSnapshotMap.value[row.id]
|
const snapshot = inlineSnapshotMap.value[row.id]
|
||||||
if (!snapshot) return
|
if (!snapshot) return
|
||||||
|
|
@ -346,12 +396,7 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
||||||
sortLoading.value = true
|
sortLoading.value = true
|
||||||
try {
|
try {
|
||||||
const pageStart = (queryParams.pageNo - 1) * queryParams.pageSize
|
const pageStart = (queryParams.pageNo - 1) * queryParams.pageSize
|
||||||
const allData = await getReviewProjectPage({
|
const fullList = await getFullProjectList()
|
||||||
reviewMeetingId,
|
|
||||||
pageNo: 1,
|
|
||||||
pageSize: Math.max(total.value || 0, queryParams.pageSize, 200)
|
|
||||||
})
|
|
||||||
const fullList = [...(allData.list || [])]
|
|
||||||
list.value.forEach((item, index) => {
|
list.value.forEach((item, index) => {
|
||||||
fullList[pageStart + index] = item
|
fullList[pageStart + index] = item
|
||||||
})
|
})
|
||||||
|
|
@ -366,13 +411,20 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
||||||
})
|
})
|
||||||
if (changedRows.length === 0) return
|
if (changedRows.length === 0) return
|
||||||
|
|
||||||
list.value = fullList.slice(pageStart, pageStart + queryParams.pageSize)
|
|
||||||
await updateReviewProjectSeqBatch(
|
await updateReviewProjectSeqBatch(
|
||||||
changedRows.map((row) => ({
|
changedRows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
seqNo: row.seqNo
|
seqNo: row.seqNo
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
const scheduledProjects = await persistScheduledTime(fullList)
|
||||||
|
if (scheduledProjects) {
|
||||||
|
list.value = scheduledProjects.slice(pageStart, pageStart + queryParams.pageSize)
|
||||||
|
syncInlineSnapshots(list.value)
|
||||||
|
} else {
|
||||||
|
list.value = fullList.slice(pageStart, pageStart + queryParams.pageSize)
|
||||||
|
syncInlineSnapshots(list.value)
|
||||||
|
}
|
||||||
ElMessage.success('排序已更新')
|
ElMessage.success('排序已更新')
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('排序保存失败,已恢复原列表')
|
ElMessage.error('排序保存失败,已恢复原列表')
|
||||||
|
|
@ -382,6 +434,23 @@ const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleIntervalChange = async () => {
|
||||||
|
sortLoading.value = true
|
||||||
|
try {
|
||||||
|
const fullList = await getFullProjectList()
|
||||||
|
const scheduledProjects = await persistScheduledTime(fullList)
|
||||||
|
if (!scheduledProjects) return
|
||||||
|
const pageStart = (queryParams.pageNo - 1) * queryParams.pageSize
|
||||||
|
list.value = scheduledProjects.slice(pageStart, pageStart + queryParams.pageSize)
|
||||||
|
syncInlineSnapshots(list.value)
|
||||||
|
ElMessage.success('起止时间已按新间隔重算')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('时间重算失败,请重试')
|
||||||
|
} finally {
|
||||||
|
sortLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
await ElMessageBox.confirm('确认删除该项目吗?', '警告', { type: 'warning' })
|
await ElMessageBox.confirm('确认删除该项目吗?', '警告', { type: 'warning' })
|
||||||
await deleteReviewProject([id])
|
await deleteReviewProject([id])
|
||||||
|
|
@ -389,6 +458,30 @@ const handleDelete = async (id: number) => {
|
||||||
getList()
|
getList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelectionChange = (rows: ReviewMeetingProjectRespVO[]) => {
|
||||||
|
selectedRows.value = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBatchPass = async () => {
|
||||||
|
if (selectedRows.value.length === 0) return
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认将当前页勾选的 ${selectedRows.value.length} 个项目统一设为通过吗?`,
|
||||||
|
'批量通过确认',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
batchPassLoading.value = true
|
||||||
|
try {
|
||||||
|
await updateReviewProjectResultBatch({
|
||||||
|
ids: selectedRows.value.map((row) => row.id),
|
||||||
|
reviewResult: 'PASS'
|
||||||
|
})
|
||||||
|
ElMessage.success('已批量设为通过')
|
||||||
|
await getList()
|
||||||
|
} finally {
|
||||||
|
batchPassLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const goToDetail = (row: ReviewMeetingProjectRespVO) => {
|
const goToDetail = (row: ReviewMeetingProjectRespVO) => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'ReviewProjectDetail',
|
name: 'ReviewProjectDetail',
|
||||||
|
|
@ -496,6 +589,41 @@ onBeforeUnmount(() => {
|
||||||
gap: 10px 14px;
|
gap: 10px 14px;
|
||||||
grid-template-columns: repeat(4, minmax(140px, 1fr));
|
grid-template-columns: repeat(4, minmax(140px, 1fr));
|
||||||
}
|
}
|
||||||
|
.table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.toolbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.duration-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: linear-gradient(180deg, #f7faff 0%, #eef4ff 100%);
|
||||||
|
border: 1px solid #cfdcf8;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(41, 90, 188, 0.08);
|
||||||
|
}
|
||||||
|
.toolbar-label {
|
||||||
|
color: #295abc;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.interval-select {
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
.selection-summary {
|
||||||
|
color: #5c6f91;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
.meta-item {
|
.meta-item {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid rgba(41, 90, 188, 0.1);
|
border: 1px solid rgba(41, 90, 188, 0.1);
|
||||||
|
|
@ -570,33 +698,50 @@ onBeforeUnmount(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
.readonly-time-range {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.readonly-time {
|
||||||
|
min-width: 44px;
|
||||||
|
color: #20314f;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.inline-time-sep {
|
.inline-time-sep {
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
.inline-time {
|
|
||||||
width: 92px;
|
|
||||||
}
|
|
||||||
.inline-field {
|
.inline-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
:deep(.inline-field .el-input__wrapper),
|
:deep(.inline-field .el-input__wrapper),
|
||||||
:deep(.inline-field .el-select__wrapper),
|
:deep(.inline-field .el-select__wrapper),
|
||||||
:deep(.inline-time .el-input__wrapper) {
|
:deep(.interval-select .el-select__wrapper) {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
transition: border-color 0.2s, background-color 0.2s;
|
transition: border-color 0.2s, background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
:deep(.interval-select .el-select__wrapper) {
|
||||||
|
border-color: #b7c9f2;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 1px rgba(41, 90, 188, 0.04);
|
||||||
|
}
|
||||||
|
:deep(.interval-select .el-select__placeholder),
|
||||||
|
:deep(.interval-select .el-select__selected-item) {
|
||||||
|
color: #20314f;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
:deep(.review-table .el-table__body tr:hover .inline-field .el-input__wrapper),
|
: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-field .el-select__wrapper) {
|
||||||
:deep(.review-table .el-table__body tr:hover .inline-time .el-input__wrapper) {
|
|
||||||
border-color: #d8e0ec;
|
border-color: #d8e0ec;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
:deep(.inline-field .el-input__wrapper.is-focus),
|
:deep(.inline-field .el-input__wrapper.is-focus),
|
||||||
:deep(.inline-field .el-select__wrapper.is-focused),
|
:deep(.inline-field .el-select__wrapper.is-focused),
|
||||||
:deep(.inline-time .el-input__wrapper.is-focus) {
|
:deep(.interval-select .el-select__wrapper.is-focused) {
|
||||||
border-color: #295abc;
|
border-color: #295abc;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import type { ReviewProjectItemVO } from '@/api/review/meeting'
|
||||||
|
import { resolveMeetingStartTime } from './projectSchedule'
|
||||||
|
|
||||||
|
const resolveMeetingStartDate = (meetingTimeRange?: Array<string | number | undefined>) => {
|
||||||
|
const meetingStart = meetingTimeRange?.[0]
|
||||||
|
if (meetingStart === undefined || meetingStart === null || meetingStart === '') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const parsed = resolveMeetingStartTime(meetingStart)
|
||||||
|
if (!parsed.isValid()) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return parsed.format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const applyDefaultReviewDate = (
|
||||||
|
projects: ReviewProjectItemVO[] = [],
|
||||||
|
meetingTimeRange?: Array<string | number | undefined>
|
||||||
|
): ReviewProjectItemVO[] => {
|
||||||
|
const defaultReviewDate = resolveMeetingStartDate(meetingTimeRange)
|
||||||
|
if (!defaultReviewDate) {
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
return projects.map((item) => ({
|
||||||
|
...item,
|
||||||
|
reviewDate: item.reviewDate || defaultReviewDate
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import type { ReviewProjectItemVO } from '@/api/review/meeting'
|
||||||
|
import type { ReviewMeetingProjectRespVO } from '@/api/review/project'
|
||||||
|
|
||||||
|
export const REVIEW_MEETING_INTERVAL_OPTIONS = [10, 15, 20, 30] as const
|
||||||
|
export type ReviewMeetingIntervalMinutes = (typeof REVIEW_MEETING_INTERVAL_OPTIONS)[number]
|
||||||
|
export const DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES: ReviewMeetingIntervalMinutes = 15
|
||||||
|
|
||||||
|
type SchedulableProject = {
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseMeetingStartTime = (meetingStartTime?: string | number) => {
|
||||||
|
if (!meetingStartTime) return null
|
||||||
|
const normalized = typeof meetingStartTime === 'string'
|
||||||
|
? (/^\d+$/.test(meetingStartTime) ? Number(meetingStartTime) : meetingStartTime.replace(' ', 'T'))
|
||||||
|
: meetingStartTime
|
||||||
|
const parsed = dayjs(normalized)
|
||||||
|
if (!parsed.isValid()) return null
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveMeetingStartTime = parseMeetingStartTime
|
||||||
|
|
||||||
|
const buildScheduledItems = <T extends SchedulableProject>(
|
||||||
|
projects: T[],
|
||||||
|
meetingStartTime: string | number | undefined,
|
||||||
|
intervalMinutes: ReviewMeetingIntervalMinutes
|
||||||
|
) => {
|
||||||
|
const meetingStart = parseMeetingStartTime(meetingStartTime)
|
||||||
|
if (!meetingStart) return null
|
||||||
|
|
||||||
|
return projects.map((project, index) => {
|
||||||
|
const start = meetingStart.add(index * intervalMinutes, 'minute')
|
||||||
|
const end = start.add(intervalMinutes, 'minute')
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
startTime: start.format('HH:mm'),
|
||||||
|
endTime: end.format('HH:mm')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildScheduledProjects = (
|
||||||
|
projects: ReviewMeetingProjectRespVO[],
|
||||||
|
meetingStartTime: string | undefined,
|
||||||
|
intervalMinutes: ReviewMeetingIntervalMinutes
|
||||||
|
) => buildScheduledItems(projects, meetingStartTime, intervalMinutes)
|
||||||
|
|
||||||
|
export const buildScheduledProjectItems = (
|
||||||
|
projects: ReviewProjectItemVO[],
|
||||||
|
meetingStartTime: string | number | undefined,
|
||||||
|
intervalMinutes: ReviewMeetingIntervalMinutes = DEFAULT_REVIEW_MEETING_INTERVAL_MINUTES
|
||||||
|
) => buildScheduledItems(projects, meetingStartTime, intervalMinutes)
|
||||||
Loading…
Reference in New Issue