feat(meeting): 新增项目详情页 ProjectDetail.vue + 路由

- 新增 ProjectDetail.vue:el-descriptions 展示项目基本信息 + 文件资料表格
- 支持上传/下载/删除资料文件,复用 project API
- 项目基本信息通过 router state 从列表页携带(无需额外接口)
- remaining.ts 新增 ReviewProjectDetail 路由
- ProjectList.vue 更新 goToDetail 携带完整 row 数据到 state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pull/874/head
Codewoc 2026-03-23 14:01:06 +08:00
parent db5253b3e6
commit 75be833554
3 changed files with 205 additions and 1 deletions

View File

@ -771,6 +771,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
activeMenu: '/review/meeting'
},
component: () => import('@/views/review/meeting/MeetingEdit.vue')
},
{
path: 'review-meeting/project/:meetingId(\\d+)/detail/:projectId(\\d+)',
name: 'ReviewProjectDetail',
meta: {
title: '项目详情',
noCache: true,
hidden: true,
canTo: true,
activeMenu: '/review/meeting'
},
component: () => import('@/views/review/meeting/ProjectDetail.vue')
}
]
},

View File

@ -0,0 +1,182 @@
<template>
<ContentWrap>
<!-- 项目基本信息 -->
<div class="detail-header">
<span class="page-title">项目详情</span>
</div>
<el-descriptions :column="3" border size="small" class="project-info">
<el-descriptions-item label="项目标题" :span="3">
<span class="project-title-text">{{ projectInfo.projectTitle || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="会中序号">{{ projectInfo.seqNo ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="起止时间">
{{ projectInfo.startTime && projectInfo.endTime ? `${projectInfo.startTime} - ${projectInfo.endTime}` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="议程分类">{{ projectInfo.agendaCategory || '-' }}</el-descriptions-item>
<el-descriptions-item label="汇报人">{{ projectInfo.reporter || '-' }}</el-descriptions-item>
<el-descriptions-item label="报告单位">{{ projectInfo.reporterUnit || '-' }}</el-descriptions-item>
<el-descriptions-item label="主持人">{{ projectInfo.host || '-' }}</el-descriptions-item>
<el-descriptions-item label="所属会议" :span="2">{{ projectInfo.meetingName || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 项目资料 -->
<div class="section-header">
<span class="section-title">项目资料</span>
<el-upload
:auto-upload="false"
:on-change="handleFileChange"
:show-file-list="false"
multiple
accept=".doc,.docx,.xls,.xlsx,.pdf,.ppt,.pptx"
>
<el-button type="primary" plain size="small">上传资料</el-button>
</el-upload>
<el-text type="info" size="small">支持 docdocxxlsxlsxpdfpptpptx单文件不超过 50MB</el-text>
</div>
<el-table :data="fileList" v-loading="fileLoading" border size="small">
<el-table-column label="文件名" prop="fileName" show-overflow-tooltip min-width="220" />
<el-table-column label="大小" width="90" align="right">
<template #default="{ row }">{{ formatFileSize(row.fileSize) }}</template>
</el-table-column>
<el-table-column label="类型" prop="fileType" width="70" align="center" />
<el-table-column label="上传人" prop="creator" width="90" />
<el-table-column label="上传时间" prop="createTime" width="165" />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="handleDownload(row)"></el-button>
<el-button type="danger" link @click="handleDelete(row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!fileLoading && fileList.length === 0" description="暂无资料文件" :image-size="60" />
<!-- 底部操作 -->
<div class="form-footer">
<el-button @click="handleBack"></el-button>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { UploadFile } from 'element-plus'
import {
getMeetingFileList,
uploadMeetingFile,
deleteMeetingFile,
type ReviewMeetingFileRespVO
} from '@/api/review/project'
defineOptions({ name: 'ReviewProjectDetail' })
const route = useRoute()
const router = useRouter()
const reviewMeetingId = Number(route.params.meetingId)
const reviewMeetingProjectId = Number(route.params.projectId)
// history state ProjectList.vue
const state = window.history.state || {}
const projectInfo = ref({
projectTitle: state.projectTitle as string | undefined,
seqNo: state.seqNo as number | undefined,
startTime: state.startTime as string | undefined,
endTime: state.endTime as string | undefined,
agendaCategory: state.agendaCategory as string | undefined,
reporter: state.reporter as string | undefined,
reporterUnit: state.reporterUnit as string | undefined,
host: state.host as string | undefined,
meetingName: state.meetingName as string | undefined
})
const fileList = ref<ReviewMeetingFileRespVO[]>([])
const fileLoading = ref(false)
const loadFiles = async () => {
fileLoading.value = true
try {
fileList.value = await getMeetingFileList(reviewMeetingProjectId)
} finally {
fileLoading.value = false
}
}
const handleFileChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) return
if (uploadFile.raw.size > 50 * 1024 * 1024) {
ElMessage.error('文件大小不能超过 50MB')
return
}
fileLoading.value = true
try {
await uploadMeetingFile(reviewMeetingId, reviewMeetingProjectId, uploadFile.raw)
ElMessage.success('上传成功')
await loadFiles()
} catch {
ElMessage.error('上传失败')
} finally {
fileLoading.value = false
}
}
const handleDownload = (row: ReviewMeetingFileRespVO) => {
window.open(row.fileUrl, '_blank')
}
const handleDelete = async (row: ReviewMeetingFileRespVO) => {
await ElMessageBox.confirm(`确定要删除文件「${row.fileName}」吗?`, '提示', { type: 'warning' })
await deleteMeetingFile(row.id)
ElMessage.success('删除成功')
await loadFiles()
}
const formatFileSize = (bytes: number): string => {
if (!bytes) return '-'
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
const handleBack = () => {
router.back()
}
onMounted(() => {
loadFiles()
})
</script>
<style scoped>
.detail-header {
margin-bottom: 16px;
}
.page-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.project-info {
margin-bottom: 24px;
}
.project-title-text {
font-weight: 600;
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.form-footer {
margin-top: 24px;
}
</style>

View File

@ -236,7 +236,17 @@ const goToDetail = (row: ReviewMeetingProjectRespVO) => {
router.push({
name: 'ReviewProjectDetail',
params: { meetingId: reviewMeetingId, projectId: row.id },
query: { projectTitle: row.projectTitle }
state: {
projectTitle: row.projectTitle,
seqNo: row.seqNo,
startTime: row.startTime,
endTime: row.endTime,
agendaCategory: row.agendaCategory,
reporter: row.reporter,
reporterUnit: row.reporterUnit,
host: row.host,
meetingName: meetingInfo.value?.name
}
})
}