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
parent
db5253b3e6
commit
75be833554
|
|
@ -771,6 +771,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||||
activeMenu: '/review/meeting'
|
activeMenu: '/review/meeting'
|
||||||
},
|
},
|
||||||
component: () => import('@/views/review/meeting/MeetingEdit.vue')
|
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')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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">支持 doc、docx、xls、xlsx、pdf、ppt、pptx,单文件不超过 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>
|
||||||
|
|
@ -236,7 +236,17 @@ const goToDetail = (row: ReviewMeetingProjectRespVO) => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'ReviewProjectDetail',
|
name: 'ReviewProjectDetail',
|
||||||
params: { meetingId: reviewMeetingId, projectId: row.id },
|
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
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue