<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 v-if="isDocument(file.mediaType)" />
|
<Picture v-else-if="isImage(file.mediaType)" />
|
<VideoPlay v-else-if="isVideo(file.mediaType)" />
|
<Document v-else />
|
</el-icon>
|
<div class="file-details">
|
<div class="file-name">{{ file.name }}</div>
|
<div class="file-size">{{ formatFileSize(file.fileSize) }}</div>
|
</div>
|
</div>
|
<div class="file-actions">
|
<el-button
|
type="primary"
|
link
|
@click="previewFile(file)"
|
v-if="canPreview(file.mediaType)"
|
>
|
预览
|
</el-button>
|
<el-button type="primary" link @click="downloadFile(file)">
|
下载
|
</el-button>
|
</div>
|
</div>
|
</div>
|
<div v-else class="no-attachments">
|
<el-empty description="暂无附件" :image-size="80" />
|
</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.gender || '未填写' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="出生日期">
|
{{ formatDate(projectDetail.playerInfo.birthday) }}
|
</el-descriptions-item>
|
<el-descriptions-item label="电话">
|
{{ projectDetail.playerInfo.phone }}
|
</el-descriptions-item>
|
<el-descriptions-item label="学历">
|
{{ projectDetail.playerInfo.education || '未填写' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="区域">
|
{{ projectDetail.regionInfo?.name || '未填写' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="个人简介" :span="2">
|
<div class="bio-content">
|
{{ projectDetail.playerInfo.introduction || '暂无简介' }}
|
</div>
|
</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"
|
style="width: 100%"
|
@change="calculateTotalScore"
|
/>
|
</div>
|
|
<div class="total-score">
|
<span class="label">总分:</span>
|
<span class="value">{{ totalScore }} / {{ projectDetail.ratingForm.totalMaxScore }}</span>
|
</div>
|
</div>
|
|
<!-- 评语 -->
|
<h4 style="margin-top: 20px;">评语</h4>
|
<el-input
|
v-model="comments"
|
type="textarea"
|
:rows="4"
|
placeholder="请输入评语(可选)"
|
maxlength="500"
|
show-word-limit
|
/>
|
|
<!-- 提交按钮 -->
|
<div class="submit-section">
|
<el-button
|
type="primary"
|
@click="submitRating"
|
:loading="submitting"
|
:disabled="!canSubmit"
|
size="large"
|
style="width: 100%"
|
>
|
提交评分
|
</el-button>
|
</div>
|
</div>
|
</el-col>
|
</el-row>
|
</el-card>
|
|
<!-- 文件预览对话框 -->
|
<el-dialog v-model="previewVisible" title="文件预览" width="80%" center>
|
<div class="preview-content">
|
<img
|
v-if="previewFile && isImage(previewFile.mediaType)"
|
:src="previewFile.fullUrl"
|
style="max-width: 100%; max-height: 500px;"
|
/>
|
<video
|
v-else-if="previewFile && isVideo(previewFile.mediaType)"
|
:src="previewFile.fullUrl"
|
controls
|
style="max-width: 100%; max-height: 500px;"
|
/>
|
<div v-else class="unsupported-preview">
|
<el-icon size="48"><Document /></el-icon>
|
<p>该文件类型不支持预览,请下载后查看</p>
|
</div>
|
</div>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, computed, onMounted } from 'vue'
|
import { useRouter, useRoute } from 'vue-router'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { Document, Picture, VideoPlay, UserFilled } from '@element-plus/icons-vue'
|
import { getProjectDetail, getRatingStats, getCurrentJudgeRating, submitRating } from '@/api/projectReview'
|
|
const router = useRouter()
|
const route = useRoute()
|
|
// 响应式数据
|
const loading = ref(true)
|
const projectDetail = ref(null)
|
const ratingStats = ref({ ratingCount: 0, averageScore: 0 })
|
const ratingItems = ref([])
|
const comments = ref('')
|
const submitting = ref(false)
|
const previewVisible = ref(false)
|
const previewFile = ref(null)
|
|
// 计算属性
|
const totalScore = computed(() => {
|
return ratingItems.value.reduce((sum, item) => sum + (item.score || 0), 0)
|
})
|
|
const canSubmit = computed(() => {
|
return ratingItems.value.length > 0 && ratingItems.value.every(item => item.score >= 0)
|
})
|
|
// 加载项目详情
|
const loadProjectDetail = async () => {
|
try {
|
const projectId = route.params.id
|
const [detail, stats] = await Promise.all([
|
getProjectDetail(projectId),
|
getRatingStats(projectId)
|
])
|
|
projectDetail.value = detail
|
ratingStats.value = stats
|
|
// 初始化评分项
|
if (detail.ratingForm && detail.ratingForm.items) {
|
ratingItems.value = detail.ratingForm.items.map(item => ({
|
...item,
|
score: 0
|
}))
|
}
|
|
// 加载当前评委的评分(如果已评分)
|
try {
|
const currentRating = await getCurrentJudgeRating(projectId)
|
if (currentRating) {
|
comments.value = currentRating.comments || ''
|
// 填充已有评分
|
if (currentRating.ratingItems) {
|
currentRating.ratingItems.forEach(ratingItem => {
|
const item = ratingItems.value.find(i => i.id === ratingItem.itemId)
|
if (item) {
|
item.score = ratingItem.score
|
}
|
})
|
}
|
}
|
} catch (error) {
|
// 如果没有评分记录,忽略错误
|
console.log('未找到当前评委的评分记录')
|
}
|
|
} catch (error) {
|
ElMessage.error(error.message)
|
} finally {
|
loading.value = false
|
}
|
}
|
|
// 计算总分
|
const calculateTotalScore = () => {
|
// 总分计算在computed中自动完成
|
}
|
|
// 提交评分
|
const submitRating = async () => {
|
try {
|
await ElMessageBox.confirm('确认提交评分吗?提交后不可修改。', '确认提交', {
|
confirmButtonText: '确认',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
|
submitting.value = true
|
|
const ratingInput = {
|
activityPlayerId: route.params.id,
|
totalScore: totalScore.value,
|
comments: comments.value,
|
ratingItems: ratingItems.value.map(item => ({
|
itemId: item.id,
|
score: item.score
|
}))
|
}
|
|
await submitRating(ratingInput)
|
|
ElMessage.success('评分提交成功!')
|
|
// 重新加载数据
|
await loadProjectDetail()
|
|
} catch (error) {
|
if (error !== 'cancel') {
|
ElMessage.error(error.message || '提交评分失败')
|
}
|
} finally {
|
submitting.value = false
|
}
|
}
|
|
// 文件相关方法
|
const isImage = (mediaType) => {
|
return mediaType && mediaType.startsWith('image/')
|
}
|
|
const isVideo = (mediaType) => {
|
return mediaType && mediaType.startsWith('video/')
|
}
|
|
const isDocument = (mediaType) => {
|
return mediaType && (
|
mediaType.includes('pdf') ||
|
mediaType.includes('doc') ||
|
mediaType.includes('text')
|
)
|
}
|
|
const canPreview = (mediaType) => {
|
return isImage(mediaType) || isVideo(mediaType)
|
}
|
|
const formatFileSize = (size) => {
|
if (!size) return '未知大小'
|
const units = ['B', 'KB', 'MB', 'GB']
|
let index = 0
|
while (size >= 1024 && index < units.length - 1) {
|
size /= 1024
|
index++
|
}
|
return `${size.toFixed(1)} ${units[index]}`
|
}
|
|
const previewFile = (file) => {
|
previewFile.value = file
|
previewVisible.value = true
|
}
|
|
const downloadFile = (file) => {
|
window.open(file.fullUrl, '_blank')
|
}
|
|
const formatDate = (dateString) => {
|
if (!dateString) return '未填写'
|
return new Date(dateString).toLocaleDateString('zh-CN')
|
}
|
|
const goBack = () => {
|
router.back()
|
}
|
|
// 组件挂载时加载数据
|
onMounted(() => {
|
loadProjectDetail()
|
})
|
</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 {
|
padding-right: 20px;
|
}
|
|
.rating-section {
|
padding-left: 20px;
|
}
|
|
.description-content,
|
.bio-content {
|
line-height: 1.6;
|
color: #606266;
|
}
|
|
.attachments {
|
border: 1px solid #e4e7ed;
|
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: 12px;
|
font-size: 24px;
|
color: #409eff;
|
}
|
|
.file-details {
|
flex: 1;
|
}
|
|
.file-name {
|
font-weight: 500;
|
color: #303133;
|
margin-bottom: 4px;
|
}
|
|
.file-size {
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.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;
|
margin-bottom: 12px;
|
}
|
|
.rating-item:last-child {
|
margin-bottom: 0;
|
}
|
|
.rating-item .label {
|
color: #606266;
|
}
|
|
.rating-item .value {
|
font-weight: 500;
|
}
|
|
.rating-item .score {
|
color: #409eff;
|
font-size: 18px;
|
}
|
|
.rating-template {
|
border: 1px solid #e4e7ed;
|
border-radius: 4px;
|
padding: 16px;
|
}
|
|
.template-header {
|
display: flex;
|
justify-content: space-between;
|
margin-bottom: 16px;
|
padding-bottom: 12px;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.template-name {
|
font-weight: 500;
|
color: #303133;
|
}
|
|
.template-total {
|
color: #409eff;
|
font-weight: 500;
|
}
|
|
.template-item {
|
margin-bottom: 16px;
|
}
|
|
.template-item:last-child {
|
margin-bottom: 0;
|
}
|
|
.item-header {
|
display: flex;
|
justify-content: space-between;
|
margin-bottom: 8px;
|
}
|
|
.item-name {
|
font-weight: 500;
|
color: #303133;
|
}
|
|
.item-score {
|
color: #909399;
|
font-size: 14px;
|
}
|
|
.total-score {
|
display: flex;
|
justify-content: space-between;
|
margin-top: 16px;
|
padding-top: 12px;
|
border-top: 1px solid #f0f0f0;
|
font-weight: 500;
|
font-size: 16px;
|
}
|
|
.total-score .value {
|
color: #409eff;
|
}
|
|
.submit-section {
|
margin-top: 20px;
|
}
|
|
.preview-content {
|
text-align: center;
|
}
|
|
.unsupported-preview {
|
padding: 40px;
|
color: #909399;
|
}
|
|
.unsupported-preview p {
|
margin-top: 16px;
|
}
|
</style>
|