<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;"
|
:disabled="!canModifyRating"
|
/>
|
</div>
|
|
<!-- 评语 -->
|
<div class="comment-section">
|
<h5>评语</h5>
|
<el-input
|
v-model="ratingComment"
|
type="textarea"
|
:rows="4"
|
:placeholder="canModifyRating ? '请输入评语(可选)' : '评语(只读)'"
|
maxlength="500"
|
show-word-limit
|
:disabled="!canModifyRating"
|
/>
|
</div>
|
|
<!-- 提交按钮 -->
|
<div class="submit-section" v-if="canModifyRating">
|
<el-button type="primary" @click="handleSubmitRating" :loading="submitting" style="width: 100%;">
|
提交评分
|
</el-button>
|
</div>
|
|
<!-- Employee用户提示 -->
|
<div class="readonly-notice" v-if="isEmployee && !canModifyRating">
|
<el-alert
|
title="只读模式"
|
description="您以员工身份查看此评审详情,只能查看不能修改评分"
|
type="info"
|
:closable="false"
|
show-icon
|
/>
|
</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, getCurrentJudgeRating } from '@/api/projectReview'
|
import { userApi } from '@/api/user'
|
import { getUserInfo } from '@/utils/auth'
|
|
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 currentJudge = ref(null)
|
const hasJudgePermission = ref(false)
|
const isJudgeInActivity = ref(false)
|
const permissionChecked = ref(false)
|
const existingRating = ref(null)
|
const isEmployee = ref(false)
|
const canModifyRating = ref(false)
|
|
// 计算属性
|
const projectId = computed(() => route.params.id)
|
const stageId = computed(() => route.query.stageId)
|
|
// 权限验证方法
|
const checkPermissions = async () => {
|
try {
|
// 获取当前用户信息
|
const userInfo = getUserInfo()
|
|
if (!userInfo) {
|
ElMessage.error('用户信息获取失败,请重新登录')
|
router.push('/project-review')
|
return false
|
}
|
|
// 检查是否有employee身份
|
if (userInfo.employee) {
|
isEmployee.value = true
|
canModifyRating.value = false // employee只能查看,不能修改
|
permissionChecked.value = true
|
ElMessage.info('您以员工身份查看评审详情,只能查看不能修改评分')
|
return true
|
}
|
|
// 如果没有employee身份,检查judge身份和权限
|
const judgeInfo = await userApi.getCurrentJudgeInfo()
|
|
if (!judgeInfo) {
|
hasJudgePermission.value = false
|
ElMessage.error('您没有评委权限,无法进行评审')
|
router.push('/project-review')
|
return false
|
}
|
|
currentJudge.value = judgeInfo
|
hasJudgePermission.value = true
|
|
// 检查是否在当前比赛阶段的评委列表中
|
if (projectDetail.value && projectDetail.value.stageId) {
|
const isInActivity = await userApi.checkJudgeInActivity(
|
projectDetail.value.stageId,
|
judgeInfo.judgeId
|
)
|
|
if (!isInActivity) {
|
isJudgeInActivity.value = false
|
ElMessage.error('您不是当前比赛的评委,无法进行评审')
|
router.push('/project-review')
|
return false
|
}
|
|
isJudgeInActivity.value = true
|
canModifyRating.value = true // judge有权限修改评分
|
}
|
|
permissionChecked.value = true
|
return true
|
} catch (error) {
|
console.error('权限验证失败:', error)
|
hasJudgePermission.value = false
|
ElMessage.error('权限验证失败,请重新登录')
|
router.push('/project-review')
|
return false
|
}
|
}
|
|
// 加载当前评委已有的评审数据
|
const loadExistingRating = async () => {
|
// employee用户不需要加载评委的评分数据
|
if (isEmployee.value || !hasJudgePermission.value) return
|
|
try {
|
const rating = await getCurrentJudgeRating(parseInt(projectId.value))
|
if (rating) {
|
existingRating.value = rating
|
|
// 如果有已有评分,填充到表单中
|
if (rating.items && rating.items.length > 0) {
|
ratingItems.value = rating.items.map(item => ({
|
id: item.ratingItemId,
|
name: item.ratingItemName,
|
score: item.score,
|
maxScore: item.maxScore || 100
|
}))
|
}
|
|
// 填充评语
|
if (rating.remark) {
|
ratingComment.value = rating.remark
|
}
|
|
ElMessage.success('已加载您之前的评分数据,可以继续编辑')
|
}
|
} catch (error) {
|
console.error('加载已有评分失败:', error)
|
// 不显示错误消息,因为可能是第一次评分
|
}
|
}
|
|
// 加载项目详情
|
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
|
}))
|
}
|
|
// 项目详情加载完成后,进行权限验证
|
const hasPermission = await checkPermissions()
|
if (hasPermission) {
|
// 权限验证通过后,加载已有评分数据
|
await loadExistingRating()
|
}
|
|
} 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 () => {
|
// 权限检查:employee用户不能提交评分
|
if (!canModifyRating.value) {
|
ElMessage.error('您没有权限提交评分')
|
return
|
}
|
|
// 验证stageId
|
if (!stageId.value) {
|
ElMessage.error('缺少比赛阶段信息,请重新进入页面')
|
return
|
}
|
|
// 验证评分
|
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: parseInt(projectId.value),
|
stageId: parseInt(stageId.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>
|