feat(review-frontend): 优化会议评审页面交互
parent
5ef10cfd6e
commit
36b34d6695
|
|
@ -19,16 +19,22 @@ export interface ReviewProjectItemVO {
|
|||
export interface ReviewMeetingSaveReqVO {
|
||||
id?: number
|
||||
name: string
|
||||
organizationUnit?: string
|
||||
startTime?: string | number
|
||||
endTime?: string | number
|
||||
materialViewStartTime?: string | number
|
||||
materialViewEndTime?: string | number
|
||||
materialViewRemark?: string
|
||||
location: string
|
||||
host?: string
|
||||
agendaAttachmentName?: string
|
||||
agendaAttachmentUrl?: string
|
||||
agendaAttachmentType?: string
|
||||
agendaAttachmentSize?: number
|
||||
minutesAttachmentName?: string
|
||||
minutesAttachmentUrl?: string
|
||||
minutesAttachmentType?: string
|
||||
minutesAttachmentSize?: number
|
||||
expertIds: number[]
|
||||
projects?: ReviewProjectItemVO[]
|
||||
}
|
||||
|
|
@ -46,16 +52,22 @@ export interface ReviewMeetingPageReqVO {
|
|||
export interface ReviewMeetingRespVO {
|
||||
id: number
|
||||
name: string
|
||||
organizationUnit?: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
materialViewStartTime?: string
|
||||
materialViewEndTime?: string
|
||||
materialViewRemark?: string
|
||||
location: string
|
||||
host?: string
|
||||
agendaAttachmentName?: string
|
||||
agendaAttachmentUrl?: string
|
||||
agendaAttachmentType?: string
|
||||
agendaAttachmentSize?: number
|
||||
minutesAttachmentName?: string
|
||||
minutesAttachmentUrl?: string
|
||||
minutesAttachmentType?: string
|
||||
minutesAttachmentSize?: number
|
||||
status: number // 0-草稿 1-已邀约 2-已结束 3-已取消
|
||||
expertIds: number[]
|
||||
expertCount: number
|
||||
|
|
@ -115,6 +127,10 @@ export const cancelReviewMeeting = (id: number) =>
|
|||
export const finishReviewMeeting = (id: number) =>
|
||||
request.put({ url: '/project/review-meeting/finish', params: { id } })
|
||||
|
||||
/** 复制会议(仅已结束/已取消) */
|
||||
export const copyReviewMeeting = (id: number) =>
|
||||
request.post({ url: '/project/review-meeting/copy', params: { id } })
|
||||
|
||||
/** 获取会议详情 */
|
||||
export const getReviewMeeting = (id: number) =>
|
||||
request.get({ url: '/project/review-meeting/get', params: { id } })
|
||||
|
|
@ -160,6 +176,19 @@ export const uploadAgendaAttachment = (file: File): Promise<ReviewMeetingAgendaA
|
|||
.then((res) => res?.data as ReviewMeetingAgendaAttachmentRespVO)
|
||||
}
|
||||
|
||||
/** 上传会议纪要附件(Word/PDF/图片) */
|
||||
export const uploadMinutesAttachment = (
|
||||
id: number,
|
||||
file: File
|
||||
): Promise<ReviewMeetingAgendaAttachmentRespVO> => {
|
||||
const formData = new FormData()
|
||||
formData.append('id', String(id))
|
||||
formData.append('file', file)
|
||||
return request
|
||||
.upload<any>({ url: '/project/review-meeting/upload-minutes-attachment', data: formData })
|
||||
.then((res) => res?.data as ReviewMeetingAgendaAttachmentRespVO)
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
export const getImportTemplate = () =>
|
||||
request.download({ url: '/project/review-meeting/get-import-template' })
|
||||
|
|
|
|||
|
|
@ -16,7 +16,14 @@ export interface ReviewMeetingProjectRespVO {
|
|||
projectTitle: string
|
||||
reporter: string
|
||||
reporterUnit: string
|
||||
host: string
|
||||
host?: string
|
||||
reviewDate?: string
|
||||
reviewResult?: 'PASS' | 'REJECT'
|
||||
}
|
||||
|
||||
export interface ReviewMeetingProjectSeqUpdateReqVO {
|
||||
id: number
|
||||
seqNo: number
|
||||
}
|
||||
|
||||
export interface ReviewMeetingProjectPageReqVO {
|
||||
|
|
@ -79,6 +86,10 @@ export const createReviewProject = (data: Partial<ReviewMeetingProjectRespVO>) =
|
|||
export const updateReviewProject = (data: Partial<ReviewMeetingProjectRespVO>) =>
|
||||
request.put({ url: '/project/review-project/update', data })
|
||||
|
||||
/** 批量更新会中序号 */
|
||||
export const updateReviewProjectSeqBatch = (data: ReviewMeetingProjectSeqUpdateReqVO[]) =>
|
||||
request.put({ url: '/project/review-project/update-seq-batch', data })
|
||||
|
||||
/** 删除评审项目 */
|
||||
export const deleteReviewProject = (ids: number[]) =>
|
||||
request.delete({ url: '/project/review-project/delete', params: { ids: ids.join(',') } })
|
||||
|
|
|
|||
|
|
@ -72,14 +72,10 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="报告人" prop="reporter" width="80" />
|
||||
<el-table-column label="报告单位" prop="reporterUnit" show-overflow-tooltip width="130" />
|
||||
<el-table-column label="主持人" width="150">
|
||||
<el-table-column label="评审日期" prop="reviewDate" width="120" align="center" />
|
||||
<el-table-column label="评审结果" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.host"
|
||||
placeholder="填写主持人"
|
||||
size="small"
|
||||
@blur="handleHostBlur(row)"
|
||||
/>
|
||||
{{ REVIEW_RESULT_LABEL[row.reviewResult as keyof typeof REVIEW_RESULT_LABEL] || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
|
|
@ -121,8 +117,14 @@
|
|||
<el-form-item label="报告单位" prop="reporterUnit">
|
||||
<el-input v-model="formData.reporterUnit" placeholder="请输入报告单位" />
|
||||
</el-form-item>
|
||||
<el-form-item label="主持人" prop="host">
|
||||
<el-input v-model="formData.host" placeholder="请输入主持人" />
|
||||
<el-form-item label="评审日期" prop="reviewDate">
|
||||
<el-date-picker v-model="formData.reviewDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择评审日期" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="评审结果" prop="reviewResult">
|
||||
<el-select v-model="formData.reviewResult" placeholder="请选择评审结果" clearable style="width: 100%">
|
||||
<el-option label="通过" value="PASS" />
|
||||
<el-option label="不通过" value="REJECT" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
|
@ -142,7 +144,6 @@ import { useRouter } from 'vue-router'
|
|||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getReviewProjectPageStandalone,
|
||||
updateProjectHost,
|
||||
updateReviewProject,
|
||||
createReviewProject,
|
||||
deleteReviewProject,
|
||||
|
|
@ -158,7 +159,8 @@ const router = useRouter()
|
|||
const loading = ref(false)
|
||||
const list = ref<ReviewMeetingProjectRespVO[]>([])
|
||||
const total = ref(0)
|
||||
const meetingOptions = ref<{ id: number; name: string }[]>([])
|
||||
const meetingOptions = ref<{ id: number; name: string; host?: string }[]>([])
|
||||
const REVIEW_RESULT_LABEL = { PASS: '通过', REJECT: '不通过' }
|
||||
|
||||
const queryParams = reactive<ReviewProjectPageReqVO & { pageNo: number; pageSize: number }>({
|
||||
pageNo: 1,
|
||||
|
|
@ -183,7 +185,7 @@ const getList = async () => {
|
|||
|
||||
const loadMeetingOptions = async () => {
|
||||
const data = await getReviewMeetingPage({ pageNo: 1, pageSize: 200 }).catch(() => ({ list: [] }))
|
||||
meetingOptions.value = (data.list || []).map((m: any) => ({ id: m.id, name: m.name }))
|
||||
meetingOptions.value = (data.list || []).map((m: any) => ({ id: m.id, name: m.name, host: m.host }))
|
||||
}
|
||||
|
||||
const handleQuery = () => { queryParams.pageNo = 1; getList() }
|
||||
|
|
@ -217,11 +219,6 @@ const handleDeleteBatch = async () => {
|
|||
getList()
|
||||
}
|
||||
|
||||
const handleHostBlur = async (row: ReviewMeetingProjectRespVO) => {
|
||||
await updateProjectHost(row.id, row.host || '')
|
||||
ElMessage.success('主持人已更新')
|
||||
}
|
||||
|
||||
const formVisible = ref(false)
|
||||
const formType = ref<'create' | 'update'>('create')
|
||||
const formLoading = ref(false)
|
||||
|
|
@ -274,7 +271,7 @@ const goToDetail = (row: ReviewMeetingProjectRespVO) => {
|
|||
agendaCategory: row.agendaCategory,
|
||||
reporter: row.reporter,
|
||||
reporterUnit: row.reporterUnit,
|
||||
host: row.host,
|
||||
meetingHost: meeting?.host,
|
||||
meetingName: meeting?.name
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -44,11 +44,27 @@
|
|||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="会议地点" prop="location">
|
||||
<el-input v-model="formData.location" placeholder="请输入会议地点" :disabled="isView" />
|
||||
<el-select
|
||||
v-model="formData.location"
|
||||
placeholder="请选择或输入会议地点"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
:disabled="isView"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in meetingLocationOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="会议主持人" prop="host">
|
||||
<el-input v-model="formData.host" placeholder="请输入会议主持人" :disabled="isView" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资料查看时限" prop="materialViewTimeRange">
|
||||
<el-date-picker
|
||||
|
|
@ -88,6 +104,31 @@
|
|||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="会议纪要" prop="minutesAttachmentUrl">
|
||||
<div class="agenda-attachment-wrap">
|
||||
<el-upload
|
||||
v-if="!isView"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept=".doc,.docx,.pdf,.png,.jpg,.jpeg,.gif,.bmp,.webp"
|
||||
:on-change="handleMinutesAttachmentChange"
|
||||
>
|
||||
<button type="button" class="btn-upload">上传会议纪要</button>
|
||||
</el-upload>
|
||||
<span class="upload-hint">支持 Word、PDF、图片;结束会议前必须上传</span>
|
||||
<span v-if="!meetingId && !isView" class="upload-hint">请先保存会议后再上传纪要</span>
|
||||
<div v-if="formData.minutesAttachmentUrl" class="agenda-file-line">
|
||||
<el-link type="primary" :underline="false" @click="previewMinutesAttachment">
|
||||
{{ formData.minutesAttachmentName }}
|
||||
</el-link>
|
||||
<el-tag size="small">{{ (formData.minutesAttachmentType || '').toUpperCase() }}</el-tag>
|
||||
<el-text type="info" size="small">{{ formatFileSize(formData.minutesAttachmentSize) }}</el-text>
|
||||
<el-button v-if="!isView" type="danger" link @click="clearMinutesAttachment">移除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 评审专家 -->
|
||||
|
|
@ -156,6 +197,7 @@ import {
|
|||
importProjectsFromExcel,
|
||||
getImportTemplate,
|
||||
uploadAgendaAttachment,
|
||||
uploadMinutesAttachment,
|
||||
type ReviewMeetingSaveReqVO,
|
||||
type ReviewProjectItemVO
|
||||
} from '@/api/review/meeting'
|
||||
|
|
@ -172,16 +214,25 @@ const route = useRoute()
|
|||
const formLoading = ref(false)
|
||||
const expertOptions = ref<any[]>([])
|
||||
const isProjectsModified = ref(false)
|
||||
const meetingLocationOptions = ['东5楼326', '南6楼203', '南6楼207']
|
||||
|
||||
// 从路由参数判断模式
|
||||
const meetingId = computed(() => {
|
||||
const id = route.params.id
|
||||
return id ? Number(id) : undefined
|
||||
})
|
||||
const copyFromId = computed(() => {
|
||||
const queryValue = route.query.copyFromId
|
||||
const raw = Array.isArray(queryValue) ? queryValue[0] : queryValue
|
||||
const parsed = raw ? Number(raw) : NaN
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined
|
||||
})
|
||||
const isView = computed(() => route.query.mode === 'view')
|
||||
const isEdit = computed(() => !!meetingId.value && !isView.value)
|
||||
const isCopyMode = computed(() => !meetingId.value && !!copyFromId.value && !isView.value)
|
||||
const pageTitle = computed(() => {
|
||||
if (isView.value) return '查看会议'
|
||||
if (isCopyMode.value) return '复制会议'
|
||||
return isEdit.value ? '编辑会议' : '新建会议'
|
||||
})
|
||||
|
||||
|
|
@ -197,11 +248,16 @@ const formData = reactive<FormData>({
|
|||
organizationUnit: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
location: '',
|
||||
location: '东5楼326',
|
||||
host: undefined,
|
||||
agendaAttachmentName: undefined,
|
||||
agendaAttachmentUrl: undefined,
|
||||
agendaAttachmentType: undefined,
|
||||
agendaAttachmentSize: undefined,
|
||||
minutesAttachmentName: undefined,
|
||||
minutesAttachmentUrl: undefined,
|
||||
minutesAttachmentType: undefined,
|
||||
minutesAttachmentSize: undefined,
|
||||
materialViewStartTime: undefined,
|
||||
materialViewEndTime: undefined,
|
||||
materialViewRemark: undefined,
|
||||
|
|
@ -216,12 +272,24 @@ const rules: FormRules = {
|
|||
organizationUnit: [{ required: true, message: '组织单位不能为空', trigger: 'blur' }],
|
||||
meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }],
|
||||
location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }],
|
||||
host: [{ required: true, message: '会议主持人不能为空', trigger: 'blur' }],
|
||||
agendaAttachmentUrl: [{ required: true, message: '议程附件不能为空', trigger: 'change' }],
|
||||
expertIds: [{ required: true, type: 'array', min: 1, message: '至少选择一位专家', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
|
||||
const mapProjectItems = (projects: any[]): ReviewProjectItemVO[] =>
|
||||
(projects || []).map((item: any) => ({
|
||||
seqNo: item.seqNo,
|
||||
startTime: item.startTime,
|
||||
endTime: item.endTime,
|
||||
agendaCategory: item.agendaCategory,
|
||||
projectTitle: item.projectTitle,
|
||||
reporter: item.reporter,
|
||||
reporterUnit: item.reporterUnit
|
||||
}))
|
||||
|
||||
const loadDetail = async (id: number) => {
|
||||
formLoading.value = true
|
||||
try {
|
||||
|
|
@ -230,7 +298,7 @@ const loadDetail = async (id: number) => {
|
|||
getReviewProjectPage({ reviewMeetingId: id, pageNo: 1, pageSize: 200 })
|
||||
])
|
||||
Object.assign(formData, detail)
|
||||
formData.projects = (projectData?.list ?? []) as ReviewProjectItemVO[]
|
||||
formData.projects = mapProjectItems(projectData?.list ?? [])
|
||||
if (detail.startTime && detail.endTime) {
|
||||
formData.meetingTimeRange = [
|
||||
new Date(detail.startTime.replace(' ', 'T')).getTime(),
|
||||
|
|
@ -248,10 +316,68 @@ const loadDetail = async (id: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
const loadCopySource = async (id: number) => {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const detail = await getReviewMeeting(id)
|
||||
if (![2, 3].includes(detail.status)) {
|
||||
ElMessage.warning('仅支持从已结束或已取消会议复制')
|
||||
router.push({ name: 'ReviewMeeting' })
|
||||
return
|
||||
}
|
||||
|
||||
formData.id = undefined
|
||||
formData.name = detail.name
|
||||
formData.organizationUnit = detail.organizationUnit
|
||||
formData.startTime = detail.startTime
|
||||
formData.endTime = detail.endTime
|
||||
formData.materialViewStartTime = detail.materialViewStartTime
|
||||
formData.materialViewEndTime = detail.materialViewEndTime
|
||||
formData.materialViewRemark = detail.materialViewRemark
|
||||
formData.location = detail.location || '东5楼326'
|
||||
formData.host = detail.host
|
||||
formData.agendaAttachmentName = undefined
|
||||
formData.agendaAttachmentUrl = undefined
|
||||
formData.agendaAttachmentType = undefined
|
||||
formData.agendaAttachmentSize = undefined
|
||||
formData.minutesAttachmentName = undefined
|
||||
formData.minutesAttachmentUrl = undefined
|
||||
formData.minutesAttachmentType = undefined
|
||||
formData.minutesAttachmentSize = undefined
|
||||
formData.expertIds = detail.expertIds || []
|
||||
formData.projects = []
|
||||
|
||||
if (detail.startTime && detail.endTime) {
|
||||
formData.meetingTimeRange = [
|
||||
new Date(detail.startTime.replace(' ', 'T')).getTime(),
|
||||
new Date(detail.endTime.replace(' ', 'T')).getTime()
|
||||
]
|
||||
} else {
|
||||
formData.meetingTimeRange = undefined
|
||||
}
|
||||
if (detail.materialViewStartTime && detail.materialViewEndTime) {
|
||||
formData.materialViewTimeRange = [
|
||||
new Date(detail.materialViewStartTime.replace(' ', 'T')).getTime(),
|
||||
new Date(detail.materialViewEndTime.replace(' ', 'T')).getTime()
|
||||
]
|
||||
} else {
|
||||
formData.materialViewTimeRange = undefined
|
||||
}
|
||||
|
||||
ElMessage.info('已带入会议信息;议程附件和评审项目不会复制,请补充后再保存草稿')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
expertOptions.value = await getExpertUserList().catch(() => [])
|
||||
if (meetingId.value) {
|
||||
await loadDetail(meetingId.value)
|
||||
} else if (copyFromId.value) {
|
||||
await loadCopySource(copyFromId.value)
|
||||
} else if (!formData.location) {
|
||||
formData.location = '东5楼326'
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -315,12 +441,52 @@ const clearAgendaAttachment = () => {
|
|||
formRef.value?.validateField('agendaAttachmentUrl')
|
||||
}
|
||||
|
||||
const handleMinutesAttachmentChange = async (uploadFile: UploadFile) => {
|
||||
if (!uploadFile.raw) return
|
||||
if (!meetingId.value) {
|
||||
ElMessage.warning('请先保存会议后再上传纪要')
|
||||
return
|
||||
}
|
||||
const ext = uploadFile.raw.name.includes('.')
|
||||
? uploadFile.raw.name.substring(uploadFile.raw.name.lastIndexOf('.') + 1).toLowerCase()
|
||||
: ''
|
||||
const allowed = ['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
|
||||
if (!allowed.includes(ext)) {
|
||||
ElMessage.error('会议纪要仅支持 Word、PDF 或图片')
|
||||
return
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
const attachment = await uploadMinutesAttachment(meetingId.value, uploadFile.raw)
|
||||
formData.minutesAttachmentName = attachment.name
|
||||
formData.minutesAttachmentUrl = attachment.url
|
||||
formData.minutesAttachmentType = attachment.type
|
||||
formData.minutesAttachmentSize = attachment.size
|
||||
ElMessage.success('会议纪要上传成功')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearMinutesAttachment = () => {
|
||||
formData.minutesAttachmentName = undefined
|
||||
formData.minutesAttachmentUrl = undefined
|
||||
formData.minutesAttachmentType = undefined
|
||||
formData.minutesAttachmentSize = undefined
|
||||
}
|
||||
|
||||
const previewAgendaAttachment = () => {
|
||||
if (formData.agendaAttachmentUrl) {
|
||||
window.open(formData.agendaAttachmentUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
const previewMinutesAttachment = () => {
|
||||
if (formData.minutesAttachmentUrl) {
|
||||
window.open(formData.minutesAttachmentUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes?: number): string => {
|
||||
if (!bytes) return '-'
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
<div class="info-footer-item">
|
||||
<span class="footer-label">主持人</span>
|
||||
<span class="footer-value">{{ projectInfo.host || '-' }}</span>
|
||||
<span class="footer-value">{{ projectInfo.meetingHost || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -81,6 +81,7 @@ import { ref, onMounted } from 'vue'
|
|||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import { getReviewMeeting } from '@/api/review/meeting'
|
||||
import {
|
||||
getMeetingFileList,
|
||||
uploadMeetingFile,
|
||||
|
|
@ -105,7 +106,7 @@ const projectInfo = ref({
|
|||
agendaCategory: state.agendaCategory as string | undefined,
|
||||
reporter: state.reporter as string | undefined,
|
||||
reporterUnit: state.reporterUnit as string | undefined,
|
||||
host: state.host as string | undefined,
|
||||
meetingHost: state.meetingHost as string | undefined,
|
||||
meetingName: state.meetingName as string | undefined
|
||||
})
|
||||
|
||||
|
|
@ -163,6 +164,14 @@ const handleBack = () => {
|
|||
|
||||
onMounted(() => {
|
||||
loadFiles()
|
||||
getReviewMeeting(reviewMeetingId)
|
||||
.then((meeting) => {
|
||||
if (!projectInfo.value.meetingName) {
|
||||
projectInfo.value.meetingName = meeting.name
|
||||
}
|
||||
projectInfo.value.meetingHost = meeting.host
|
||||
})
|
||||
.catch(() => {})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -60,8 +60,13 @@
|
|||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table v-loading="loading" :data="list" border class="review-table" @selection-change="handleSelectionChange">
|
||||
<el-table ref="tableRef" v-loading="loading || sortLoading" :data="list" row-key="id" border class="review-table" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column label="拖拽" width="60" align="center">
|
||||
<template #default>
|
||||
<span class="drag-handle" title="拖拽排序">⋮⋮</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="立项编号" prop="id" width="80" align="center" />
|
||||
<el-table-column label="会中序号" prop="seqNo" width="80" align="center" />
|
||||
<el-table-column label="起止时间" width="110" align="center">
|
||||
|
|
@ -78,14 +83,10 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="报告人" prop="reporter" width="80" />
|
||||
<el-table-column label="报告单位" prop="reporterUnit" show-overflow-tooltip width="130" />
|
||||
<el-table-column label="主持人" width="150">
|
||||
<el-table-column label="评审日期" prop="reviewDate" width="120" align="center" />
|
||||
<el-table-column label="评审结果" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.host"
|
||||
placeholder="填写主持人"
|
||||
size="small"
|
||||
@blur="handleHostBlur(row)"
|
||||
/>
|
||||
{{ REVIEW_RESULT_LABEL[row.reviewResult as keyof typeof REVIEW_RESULT_LABEL] || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
|
|
@ -122,8 +123,14 @@
|
|||
<el-form-item label="报告单位" prop="reporterUnit">
|
||||
<el-input v-model="formData.reporterUnit" placeholder="请输入报告单位" />
|
||||
</el-form-item>
|
||||
<el-form-item label="主持人" prop="host">
|
||||
<el-input v-model="formData.host" placeholder="请输入主持人" />
|
||||
<el-form-item label="评审日期" prop="reviewDate">
|
||||
<el-date-picker v-model="formData.reviewDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择评审日期" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="评审结果" prop="reviewResult">
|
||||
<el-select v-model="formData.reviewResult" placeholder="请选择评审结果" clearable style="width: 100%">
|
||||
<el-option label="通过" value="PASS" />
|
||||
<el-option label="不通过" value="REJECT" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
|
@ -138,11 +145,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import Sortable from 'sortablejs'
|
||||
import { getReviewMeeting } from '@/api/review/meeting'
|
||||
import { getReviewProjectPage, updateProjectHost, updateReviewProject, createReviewProject, deleteReviewProject, type ReviewMeetingProjectRespVO } from '@/api/review/project'
|
||||
import { getReviewProjectPage, updateReviewProject, updateReviewProjectSeqBatch, createReviewProject, deleteReviewProject, type ReviewMeetingProjectRespVO } from '@/api/review/project'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
|
||||
defineOptions({ name: 'ReviewMeetingProject' })
|
||||
|
|
@ -152,11 +160,15 @@ const router = useRouter()
|
|||
const reviewMeetingId = Number(route.params.meetingId)
|
||||
|
||||
const loading = ref(false)
|
||||
const sortLoading = ref(false)
|
||||
const list = ref<ReviewMeetingProjectRespVO[]>([])
|
||||
const total = ref(0)
|
||||
const meetingInfo = ref<any>({})
|
||||
const tableRef = ref()
|
||||
let sortableInstance: Sortable | null = null
|
||||
|
||||
const STATUS_LABEL: Record<number, string> = { 0: '待召开', 1: '正在召开', 2: '已结束', 3: '已取消' }
|
||||
const REVIEW_RESULT_LABEL = { PASS: '通过', REJECT: '不通过' }
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
|
|
@ -175,6 +187,75 @@ const getList = async () => {
|
|||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
await nextTick()
|
||||
initSortable()
|
||||
}
|
||||
}
|
||||
|
||||
const initSortable = () => {
|
||||
if (!tableRef.value?.$el) return
|
||||
const tbody = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody')
|
||||
if (!tbody) return
|
||||
sortableInstance?.destroy()
|
||||
sortableInstance = Sortable.create(tbody, {
|
||||
animation: 150,
|
||||
handle: '.drag-handle',
|
||||
ghostClass: 'drag-ghost',
|
||||
chosenClass: 'drag-chosen',
|
||||
onEnd: ({ oldIndex, newIndex }) => {
|
||||
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) return
|
||||
handleSortEnd(oldIndex, newIndex)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSortEnd = async (oldIndex: number, newIndex: number) => {
|
||||
if (queryParams.projectTitle || queryParams.agendaCategory || queryParams.reporter) {
|
||||
ElMessage.warning('请先清空筛选条件后再拖拽排序')
|
||||
await getList()
|
||||
return
|
||||
}
|
||||
const moved = list.value.splice(oldIndex, 1)[0]
|
||||
if (!moved) return
|
||||
list.value.splice(newIndex, 0, moved)
|
||||
list.value = [...list.value]
|
||||
|
||||
sortLoading.value = true
|
||||
try {
|
||||
const pageStart = (queryParams.pageNo - 1) * queryParams.pageSize
|
||||
const allData = await getReviewProjectPage({
|
||||
reviewMeetingId,
|
||||
pageNo: 1,
|
||||
pageSize: Math.max(total.value || 0, queryParams.pageSize, 200)
|
||||
})
|
||||
const fullList = [...(allData.list || [])]
|
||||
list.value.forEach((item, index) => {
|
||||
fullList[pageStart + index] = item
|
||||
})
|
||||
|
||||
const changedRows: ReviewMeetingProjectRespVO[] = []
|
||||
fullList.forEach((item, index) => {
|
||||
const nextSeq = index + 1
|
||||
if (item.seqNo !== nextSeq) {
|
||||
item.seqNo = nextSeq
|
||||
changedRows.push(item)
|
||||
}
|
||||
})
|
||||
if (changedRows.length === 0) return
|
||||
|
||||
list.value = fullList.slice(pageStart, pageStart + queryParams.pageSize)
|
||||
await updateReviewProjectSeqBatch(
|
||||
changedRows.map((row) => ({
|
||||
id: row.id,
|
||||
seqNo: row.seqNo
|
||||
}))
|
||||
)
|
||||
ElMessage.success('排序已更新')
|
||||
} catch {
|
||||
ElMessage.error('排序保存失败,已恢复原列表')
|
||||
await getList()
|
||||
} finally {
|
||||
sortLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,11 +281,6 @@ const handleDelete = async (id?: number) => {
|
|||
getList()
|
||||
}
|
||||
|
||||
const handleHostBlur = async (row: ReviewMeetingProjectRespVO) => {
|
||||
await updateProjectHost(row.id, row.host || '')
|
||||
ElMessage.success('主持人已更新')
|
||||
}
|
||||
|
||||
const formVisible = ref(false)
|
||||
const formType = ref<'create' | 'update'>('create')
|
||||
const formLoading = ref(false)
|
||||
|
|
@ -256,7 +332,7 @@ const goToDetail = (row: ReviewMeetingProjectRespVO) => {
|
|||
agendaCategory: row.agendaCategory,
|
||||
reporter: row.reporter,
|
||||
reporterUnit: row.reporterUnit,
|
||||
host: row.host,
|
||||
meetingHost: meetingInfo.value?.host,
|
||||
meetingName: meetingInfo.value?.name
|
||||
}
|
||||
})
|
||||
|
|
@ -266,6 +342,11 @@ onMounted(async () => {
|
|||
meetingInfo.value = await getReviewMeeting(reviewMeetingId)
|
||||
await getList()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
sortableInstance?.destroy()
|
||||
sortableInstance = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -437,4 +518,22 @@ onMounted(async () => {
|
|||
:deep(.review-table .el-table__body tr:hover > td) {
|
||||
background-color: rgba(41, 90, 188, 0.04);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: inline-block;
|
||||
color: #7a869a;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
:deep(.drag-ghost > td) {
|
||||
background-color: rgba(41, 90, 188, 0.1) !important;
|
||||
}
|
||||
:deep(.drag-chosen > td) {
|
||||
background-color: rgba(41, 90, 188, 0.06);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -71,12 +71,14 @@
|
|||
<a v-hasPermi="['review:meeting:send-sms']" class="op-link" @click="openSmsStatus(row)">短信状态</a>
|
||||
<a v-if="row.mailSent" v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="openMailStatus(row)">邮件状态</a>
|
||||
<a v-else v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="handleSendMail(row)">发送议程</a>
|
||||
<a v-hasPermi="['review:meeting:update']" class="op-link" @click="triggerUploadMinutes(row)">上传纪要</a>
|
||||
<a v-hasPermi="['review:meeting:finish']" class="op-link" @click="handleFinish(row)">结束</a>
|
||||
<a v-hasPermi="['review:meeting:cancel']" class="op-link op-danger" @click="handleCancel(row)">取消</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a class="op-link" @click="goToEdit(row.id, 'view')">查看</a>
|
||||
<a class="op-link" @click="goToProjectList(row)">项目列表</a>
|
||||
<a v-hasPermi="['review:meeting:create']" class="op-link" @click="handleCopy(row)">复制新会议</a>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -90,6 +92,14 @@
|
|||
|
||||
<!-- 邮件状态弹窗 -->
|
||||
<MailStatusDialog ref="mailStatusRef" />
|
||||
|
||||
<input
|
||||
ref="minutesUploadInputRef"
|
||||
type="file"
|
||||
style="display: none"
|
||||
accept=".doc,.docx,.pdf,.png,.jpg,.jpeg,.gif,.bmp,.webp"
|
||||
@change="handleMinutesFileChange"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
|
|
@ -103,6 +113,7 @@ import {
|
|||
finishReviewMeeting,
|
||||
sendSmsInvitation,
|
||||
sendMailInvitation,
|
||||
uploadMinutesAttachment,
|
||||
type ReviewMeetingRespVO,
|
||||
type ReviewMeetingPageReqVO
|
||||
} from '@/api/review/meeting'
|
||||
|
|
@ -133,9 +144,10 @@ const queryParams = reactive<ReviewMeetingPageReqVO & { pageNo: number; pageSize
|
|||
startTime: undefined
|
||||
})
|
||||
|
||||
const queryFormRef = ref()
|
||||
const smsStatusRef = ref()
|
||||
const mailStatusRef = ref()
|
||||
const minutesUploadInputRef = ref<HTMLInputElement>()
|
||||
const pendingMinutesMeeting = ref<ReviewMeetingRespVO>()
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
|
|
@ -182,6 +194,41 @@ const handleFinish = async (row: ReviewMeetingRespVO) => {
|
|||
getList()
|
||||
}
|
||||
|
||||
const handleCopy = async (row: ReviewMeetingRespVO) => {
|
||||
router.push({
|
||||
name: 'ReviewMeetingEdit',
|
||||
query: { copyFromId: String(row.id) }
|
||||
})
|
||||
}
|
||||
|
||||
const triggerUploadMinutes = (row: ReviewMeetingRespVO) => {
|
||||
pendingMinutesMeeting.value = row
|
||||
minutesUploadInputRef.value?.click()
|
||||
}
|
||||
|
||||
const handleMinutesFileChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file || !pendingMinutesMeeting.value) {
|
||||
return
|
||||
}
|
||||
const ext = file.name.includes('.') ? file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase() : ''
|
||||
const allowed = ['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
|
||||
if (!allowed.includes(ext)) {
|
||||
ElMessage.error('会议纪要仅支持 Word、PDF 或图片')
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
try {
|
||||
await uploadMinutesAttachment(pendingMinutesMeeting.value.id, file)
|
||||
ElMessage.success('会议纪要上传成功')
|
||||
getList()
|
||||
} finally {
|
||||
input.value = ''
|
||||
pendingMinutesMeeting.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendSms = async (row: ReviewMeetingRespVO) => {
|
||||
await ElMessageBox.confirm(
|
||||
`确认向 ${row.expertCount} 位专家发送「${row.name}」会议邀约短信?`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue