<template>
|
<div class="detail-container">
|
<el-card v-loading="loading">
|
<template #header>
|
<div class="card-header">
|
<h3 class="card-title">项目评审详情</h3>
|
<el-button @click="goBack">返回</el-button>
|
</div>
|
</template>
|
|
<el-row :gutter="20" v-if="projectDetail">
|
<!-- 左侧:项目信息 -->
|
<el-col :span="16">
|
<div class="project-section">
|
<!-- 项目基本信息 -->
|
<h4>项目信息</h4>
|
<el-descriptions :column="2" border>
|
<el-descriptions-item label="项目名称">
|
{{ projectDetail.projectName || '未填写' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="比赛名称">
|
{{ projectDetail.activityName }}
|
</el-descriptions-item>
|
<el-descriptions-item label="项目描述" :span="2">
|
<div class="description-content">
|
{{ projectDetail.description || '暂无描述' }}
|
</div>
|
</el-descriptions-item>
|
</el-descriptions>
|
|
<!-- 项目附件 -->
|
<h4 style="margin-top: 20px;">项目附件</h4>
|
<div class="attachments" v-if="projectDetail.submissionFiles && projectDetail.submissionFiles.length > 0">
|
<div v-for="file in projectDetail.submissionFiles" :key="file.id" class="attachment-item">
|
<div class="file-info">
|
<el-icon class="file-icon"><Document /></el-icon>
|
<span class="file-name">{{ file.name }}</span>
|
<span class="file-size">{{ formatFileSize(file.fileSize) }}</span>
|
</div>
|
<div class="file-actions">
|
<el-button type="primary" link @click="previewFile(file)">
|
预览
|
</el-button>
|
<el-button type="primary" link @click="downloadFile(file)">
|
下载
|
</el-button>
|
</div>
|
</div>
|
</div>
|
<div v-else class="no-attachments">
|
<el-empty description="暂无附件" />
|
</div>
|
|
<!-- 参赛人信息 -->
|
<h4 style="margin-top: 20px;">参赛人信息</h4>
|
<el-descriptions :column="2" border v-if="projectDetail.playerInfo">
|
<el-descriptions-item label="头像">
|
<el-avatar
|
:src="projectDetail.playerInfo.avatarUrl"
|
:size="60"
|
:icon="UserFilled"
|
/>
|
</el-descriptions-item>
|
<el-descriptions-item label="姓名">
|
{{ projectDetail.playerInfo.name }}
|
</el-descriptions-item>
|
<el-descriptions-item label="联系电话">
|
{{ projectDetail.playerInfo.phone }}
|
</el-descriptions-item>
|
<el-descriptions-item label="所属区域" v-if="projectDetail.regionInfo">
|
{{ projectDetail.regionInfo.name }}
|
</el-descriptions-item>
|
<el-descriptions-item label="报名状态">
|
<el-tag :type="getStateType(projectDetail.state)">
|
{{ getStateName(projectDetail.state) }}
|
</el-tag>
|
</el-descriptions-item>
|
</el-descriptions>
|
</div>
|
</el-col>
|
|
<!-- 右侧:评分信息 -->
|
<el-col :span="8">
|
<div class="rating-section">
|
<!-- 评审统计 -->
|
<h4>评审信息</h4>
|
<el-card class="rating-summary">
|
<div class="rating-item">
|
<span class="label">已评审次数:</span>
|
<span class="value">{{ ratingStats.ratingCount }}</span>
|
</div>
|
<div class="rating-item">
|
<span class="label">当前平均分:</span>
|
<span class="value score">
|
{{ ratingStats.averageScore > 0 ? ratingStats.averageScore.toFixed(1) : '未评分' }}
|
</span>
|
</div>
|
</el-card>
|
|
<!-- 评分模板 -->
|
<h4 style="margin-top: 20px;">评分模板</h4>
|
<div class="rating-template" v-if="projectDetail.ratingForm">
|
<div class="template-header">
|
<span class="template-name">{{ projectDetail.ratingForm.schemeName }}</span>
|
<span class="template-total">总分:{{ projectDetail.ratingForm.totalMaxScore }}分</span>
|
</div>
|
|
<div v-for="item in ratingItems" :key="item.id" class="template-item">
|
<div class="item-header">
|
<span class="item-name">{{ item.name }}</span>
|
<span class="item-score">{{ item.maxScore }}分</span>
|
</div>
|
<el-input-number
|
v-model="item.score"
|
:min="0"
|
:max="item.maxScore"
|
:precision="1"
|
:step="0.5"
|
size="small"
|
style="width: 100%; margin-top: 8px;"
|
/>
|
</div>
|
|
<!-- 评语 -->
|
<div class="comment-section">
|
<h5>评语</h5>
|
<el-input
|
v-model="ratingComment"
|
type="textarea"
|
:rows="4"
|
placeholder="请输入评语(可选)"
|
maxlength="500"
|
show-word-limit
|
/>
|
</div>
|
|
<!-- 提交按钮 -->
|
<div class="submit-section">
|
<el-button type="primary" @click="handleSubmitRating" :loading="submitting" style="width: 100%;">
|
提交评分
|
</el-button>
|
</div>
|
</div>
|
<div v-else class="no-template">
|
<el-empty description="暂无评分模板" />
|
</div>
|
</div>
|
</el-col>
|
</el-row>
|
</el-card>
|
|
<!-- 文件预览对话框 -->
|
<el-dialog v-model="previewVisible" title="文件预览" width="80%" center>
|
<div class="preview-content">
|
<iframe
|
v-if="previewUrl"
|
:src="previewUrl"
|
style="width: 100%; height: 500px; border: none;"
|
></iframe>
|
<div v-else class="preview-error">
|
<el-empty description="无法预览此文件类型" />
|
</div>
|
</div>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, onMounted, computed } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { Document, UserFilled } from '@element-plus/icons-vue'
|
import { getProjectDetail, getRatingStats, submitRating } from '@/api/projectReview'
|
|
const route = useRoute()
|
const router = useRouter()
|
|
// 响应式数据
|
const loading = ref(false)
|
const submitting = ref(false)
|
const projectDetail = ref(null)
|
const ratingStats = ref({ ratingCount: 0, averageScore: 0 })
|
const ratingItems = ref([])
|
const ratingComment = ref('')
|
const previewVisible = ref(false)
|
const previewUrl = ref('')
|
|
// 计算属性
|
const projectId = computed(() => route.params.id)
|
|
// 加载项目详情
|
const loadProjectDetail = async () => {
|
loading.value = true
|
try {
|
const data = await getProjectDetail(projectId.value)
|
projectDetail.value = data
|
|
// 初始化评分项
|
if (data.ratingForm && data.ratingForm.items) {
|
ratingItems.value = data.ratingForm.items.map(item => ({
|
...item,
|
score: 0
|
}))
|
}
|
} catch (error) {
|
ElMessage.error('加载项目详情失败')
|
console.error(error)
|
} finally {
|
loading.value = false
|
}
|
}
|
|
// 加载评分统计
|
const loadRatingStats = async () => {
|
try {
|
const stats = await getRatingStats(projectId.value)
|
ratingStats.value = stats
|
} catch (error) {
|
console.error('加载评分统计失败:', error)
|
}
|
}
|
|
// 提交评分
|
const handleSubmitRating = async () => {
|
// 验证评分
|
const hasEmptyScore = ratingItems.value.some(item => item.score === 0 || item.score === null)
|
if (hasEmptyScore) {
|
ElMessage.warning('请为所有评分项打分')
|
return
|
}
|
|
try {
|
await ElMessageBox.confirm('确定要提交评分吗?提交后将无法修改。', '确认提交', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
|
submitting.value = true
|
|
const ratingData = {
|
activityPlayerId: projectId.value,
|
ratings: ratingItems.value.map(item => ({
|
itemId: item.id,
|
score: item.score
|
})),
|
comment: ratingComment.value
|
}
|
|
await submitRating(ratingData)
|
ElMessage.success('评分提交成功')
|
|
// 重新加载评分统计
|
await loadRatingStats()
|
|
} catch (error) {
|
if (error !== 'cancel') {
|
ElMessage.error('评分提交失败')
|
console.error(error)
|
}
|
} finally {
|
submitting.value = false
|
}
|
}
|
|
// 文件预览
|
const previewFile = (file) => {
|
// 根据文件类型决定预览方式
|
const fileExtension = file.name.split('.').pop().toLowerCase()
|
const previewableTypes = ['pdf', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'webm']
|
|
if (previewableTypes.includes(fileExtension)) {
|
// 在新窗口中打开预览
|
window.open(file.url, '_blank')
|
} else {
|
ElMessage.warning('此文件类型不支持预览,请下载查看')
|
}
|
}
|
|
// 文件下载
|
const downloadFile = (file) => {
|
const link = document.createElement('a')
|
link.href = file.url
|
link.download = file.name
|
document.body.appendChild(link)
|
link.click()
|
document.body.removeChild(link)
|
}
|
|
// 格式化文件大小
|
const formatFileSize = (bytes) => {
|
if (!bytes) return '0 B'
|
const k = 1024
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
}
|
|
// 获取状态类型
|
const getStateType = (state) => {
|
const stateMap = {
|
0: 'danger', // 已拒绝
|
1: 'warning', // 待审核
|
2: 'success', // 已通过
|
3: 'info' // 已结束
|
}
|
return stateMap[state] || 'info'
|
}
|
|
// 获取状态名称
|
const getStateName = (state) => {
|
const stateMap = {
|
0: '已拒绝',
|
1: '待评审',
|
2: '已通过',
|
3: '已结束'
|
}
|
return stateMap[state] || '未知'
|
}
|
|
// 返回上一页
|
const goBack = () => {
|
router.back()
|
}
|
|
// 组件挂载时加载数据
|
onMounted(() => {
|
loadProjectDetail()
|
loadRatingStats()
|
})
|
</script>
|
|
<style scoped>
|
.detail-container {
|
padding: 20px;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.card-title {
|
margin: 0;
|
font-size: 18px;
|
font-weight: 500;
|
}
|
|
.project-section h4,
|
.rating-section h4 {
|
margin: 0 0 16px 0;
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
border-left: 4px solid #409eff;
|
padding-left: 12px;
|
}
|
|
.description-content {
|
line-height: 1.6;
|
color: #606266;
|
}
|
|
.attachments {
|
border: 1px solid #dcdfe6;
|
border-radius: 4px;
|
padding: 16px;
|
}
|
|
.attachment-item {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 12px 0;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.attachment-item:last-child {
|
border-bottom: none;
|
}
|
|
.file-info {
|
display: flex;
|
align-items: center;
|
flex: 1;
|
}
|
|
.file-icon {
|
margin-right: 8px;
|
color: #409eff;
|
}
|
|
.file-name {
|
font-weight: 500;
|
margin-right: 12px;
|
}
|
|
.file-size {
|
color: #909399;
|
font-size: 12px;
|
}
|
|
.file-actions {
|
display: flex;
|
gap: 8px;
|
}
|
|
.no-attachments {
|
text-align: center;
|
padding: 40px 0;
|
}
|
|
.rating-summary {
|
margin-bottom: 20px;
|
}
|
|
.rating-item {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 12px;
|
}
|
|
.rating-item:last-child {
|
margin-bottom: 0;
|
}
|
|
.rating-item .label {
|
color: #606266;
|
}
|
|
.rating-item .value {
|
font-weight: 600;
|
}
|
|
.rating-item .score {
|
color: #67c23a;
|
font-size: 18px;
|
}
|
|
.rating-template {
|
border: 1px solid #dcdfe6;
|
border-radius: 4px;
|
padding: 16px;
|
}
|
|
.template-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 16px;
|
padding-bottom: 12px;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.template-name {
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.template-total {
|
color: #409eff;
|
font-weight: 600;
|
}
|
|
.template-item {
|
margin-bottom: 16px;
|
padding: 12px;
|
background-color: #f8f9fa;
|
border-radius: 4px;
|
}
|
|
.template-item:last-child {
|
margin-bottom: 0;
|
}
|
|
.item-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 8px;
|
}
|
|
.item-name {
|
font-weight: 500;
|
color: #303133;
|
}
|
|
.item-score {
|
color: #909399;
|
font-size: 12px;
|
}
|
|
.comment-section {
|
margin-top: 20px;
|
}
|
|
.comment-section h5 {
|
margin: 0 0 12px 0;
|
font-size: 14px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.submit-section {
|
margin-top: 20px;
|
}
|
|
.no-template {
|
text-align: center;
|
padding: 40px 0;
|
}
|
|
.preview-content {
|
text-align: center;
|
}
|
|
.preview-error {
|
padding: 40px 0;
|
}
|
|
:deep(.el-descriptions__label) {
|
font-weight: 600;
|
}
|
|
:deep(.el-card__body) {
|
padding: 16px;
|
}
|
</style>
|