<template>
|
<div class="review-container">
|
<!-- 页面标题区域 -->
|
<div class="page-header">
|
<div class="title-section">
|
<h1 class="page-title">项目评审</h1>
|
<p class="page-subtitle">管理比赛项目的评审流程,查看评分结果和项目详情</p>
|
</div>
|
|
</div>
|
|
<!-- 搜索工具栏 -->
|
<div class="search-toolbar">
|
<div class="search-form">
|
<el-input
|
v-model="searchName"
|
placeholder="请输入项目名称"
|
@keyup.enter="loadProjects"
|
@clear="handleClear"
|
style="width: 180px"
|
clearable
|
/>
|
<el-select
|
v-model="selectedActivity"
|
placeholder="请选择比赛"
|
@change="handleActivityChange"
|
style="width: 200px"
|
clearable
|
filterable
|
>
|
<template #prefix>
|
<el-icon><Trophy /></el-icon>
|
</template>
|
<el-option
|
v-for="activity in activities"
|
:key="activity.id"
|
:label="getActivityDisplayName(activity)"
|
:value="activity.id"
|
>
|
<span>{{ getActivityName(activity) }}</span>
|
<span v-if="activity.pid > 0" style="color: #409eff; margin-left: 8px;">
|
{{ activity.name }}
|
</span>
|
</el-option>
|
</el-select>
|
<el-button
|
type="primary"
|
:icon="Search"
|
@click="loadProjects"
|
:loading="projectsLoading"
|
>
|
查询
|
</el-button>
|
</div>
|
</div>
|
|
<el-table
|
:data="projects"
|
style="width: 100%"
|
v-loading="projectsLoading"
|
empty-text="请先选择比赛"
|
>
|
<el-table-column prop="projectName" label="项目名称" min-width="150">
|
<template #default="scope">
|
{{ scope.row.projectName || '未填写项目名称' }}
|
</template>
|
</el-table-column>
|
<el-table-column prop="playerName" label="参赛人姓名" min-width="120" />
|
<el-table-column prop="phone" label="联系电话" min-width="120" />
|
<el-table-column prop="ratingCount" label="评审次数" width="100" align="center">
|
<template #default="scope">
|
<el-button
|
text
|
:type="scope.row.ratingCount > 0 ? 'success' : 'info'"
|
@click="showRatingList(scope.row)"
|
:disabled="scope.row.ratingCount === 0"
|
class="rating-count-btn"
|
>
|
{{ scope.row.ratingCount }}
|
</el-button>
|
</template>
|
</el-table-column>
|
<el-table-column prop="averageScore" label="平均分" width="100" align="center">
|
<template #default="scope">
|
<span v-if="scope.row.averageScore > 0" class="score">
|
{{ scope.row.averageScore.toFixed(1) }}
|
</span>
|
<span v-else class="no-score">未评分</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="applyTime" label="报名时间" width="180">
|
<template #default="scope">
|
{{ formatDate(scope.row.applyTime) }}
|
</template>
|
</el-table-column>
|
<el-table-column prop="reviewStatus" label="评审状态" width="100" align="center">
|
<template #default="scope">
|
<el-tag :type="getReviewStatusType(scope.row.ratingCount)">
|
{{ getReviewStatusName(scope.row.ratingCount) }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="80" align="center">
|
<template #default="scope">
|
<el-button
|
text
|
:icon="View"
|
@click="viewDetails(scope.row.id)"
|
class="action-btn view-btn"
|
title="详情评审"
|
/>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<!-- 分页 -->
|
<div class="pagination-container" v-if="total > 0">
|
<el-pagination
|
v-model:current-page="currentPage"
|
v-model:page-size="pageSize"
|
:page-sizes="[10, 20, 50, 100]"
|
:total="total"
|
layout="total, sizes, prev, pager, next, jumper"
|
@size-change="handleSizeChange"
|
@current-change="handleCurrentChange"
|
/>
|
</div>
|
|
<!-- 评审列表弹窗 -->
|
<el-dialog
|
v-model="ratingListVisible"
|
title="评审列表"
|
width="60%"
|
:before-close="handleRatingListClose"
|
>
|
<div v-if="selectedProject">
|
<div class="dialog-header">
|
<h4>{{ selectedProject.projectName || selectedProject.playerName }} - 评审详情</h4>
|
<p class="project-info">参赛人:{{ selectedProject.playerName }} | 联系电话:{{ selectedProject.phone }}</p>
|
</div>
|
|
<el-table
|
:data="judgeRatings"
|
v-loading="ratingsLoading"
|
style="width: 100%"
|
>
|
<el-table-column prop="judgeName" label="评委姓名" min-width="120" />
|
<el-table-column prop="totalScore" label="评分" width="100" align="center">
|
<template #default="scope">
|
<span v-if="scope.row.hasRated && scope.row.totalScore" class="score">
|
{{ scope.row.totalScore.toFixed(1) }}
|
</span>
|
<span v-else class="no-score">未评分</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="hasRated" label="状态" width="100" align="center">
|
<template #default="scope">
|
<el-tag :type="scope.row.hasRated ? 'success' : 'info'">
|
{{ scope.row.hasRated ? '已评分' : '未评分' }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="100" align="center">
|
<template #default="scope">
|
<el-button
|
text
|
type="primary"
|
@click="viewRatingDetail(scope.row)"
|
:disabled="!scope.row.hasRated"
|
size="small"
|
>
|
查看详情
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="handleRatingListClose">关闭</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
|
<!-- 评分详情弹窗 -->
|
<el-dialog
|
v-model="ratingDetailVisible"
|
title="评分详情"
|
width="50%"
|
:before-close="handleRatingDetailClose"
|
>
|
<div v-if="selectedRating">
|
<div class="rating-detail-header">
|
<h4>评委:{{ selectedRating.judgeName }}</h4>
|
<p>总分:<span class="total-score">{{ selectedRating.totalScore.toFixed(1) }}</span></p>
|
</div>
|
|
<el-table
|
:data="ratingItems"
|
v-loading="ratingDetailLoading"
|
style="width: 100%"
|
>
|
<el-table-column prop="itemName" label="评分项目" min-width="150" />
|
<el-table-column prop="score" label="得分" width="100" align="center">
|
<template #default="scope">
|
<span class="item-score">{{ scope.row.score }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="maxScore" label="满分" width="100" align="center">
|
<template #default="scope">
|
<span class="max-score">{{ scope.row.maxScore || 100 }}</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div v-if="selectedRating.remark" class="rating-comment">
|
<h5>评语:</h5>
|
<p>{{ selectedRating.remark }}</p>
|
</div>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="handleRatingDetailClose">关闭</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, onMounted } from 'vue'
|
import { useRouter } from 'vue-router'
|
import { ElMessage } from 'element-plus'
|
import { Search, Trophy, View } from '@element-plus/icons-vue'
|
import { getActiveActivities, getRatingStats, getJudgeRatingDetail } from '@/api/projectReview'
|
import { getProjectReviewApplications } from '@/api/projectReviewNew'
|
import { userApi } from '@/api/user'
|
import { getUserInfo } from '@/utils/auth'
|
|
const router = useRouter()
|
|
// 响应式数据
|
const activities = ref([])
|
const selectedActivity = ref(null)
|
const projects = ref([])
|
const searchName = ref('')
|
const activitiesLoading = ref(false)
|
const projectsLoading = ref(false)
|
|
// 评审弹窗相关数据
|
const ratingListVisible = ref(false)
|
const ratingDetailVisible = ref(false)
|
const selectedProject = ref(null)
|
const selectedRating = ref(null)
|
const judgeRatings = ref([])
|
const ratingItems = ref([])
|
const ratingsLoading = ref(false)
|
const ratingDetailLoading = ref(false)
|
|
// 分页数据
|
const currentPage = ref(1)
|
const pageSize = ref(10)
|
const total = ref(0)
|
|
// 加载比赛列表
|
const loadActivities = async () => {
|
console.log('=== 开始加载比赛列表 ===')
|
activitiesLoading.value = true
|
try {
|
console.log('调用 getActiveActivities...')
|
const data = await getActiveActivities()
|
console.log('getActiveActivities 返回数据:', data)
|
activities.value = data
|
console.log('activities.value 设置为:', activities.value)
|
console.log('activities.value.length:', activities.value?.length)
|
} catch (error) {
|
console.error('加载比赛列表失败:', error)
|
ElMessage.error(error.message)
|
} finally {
|
activitiesLoading.value = false
|
console.log('=== 比赛列表加载完成 ===')
|
}
|
}
|
|
// 加载项目列表
|
const loadProjects = async () => {
|
if (!selectedActivity.value) {
|
ElMessage.warning('请先选择比赛')
|
return
|
}
|
|
console.log('=== 开始加载项目列表 ===')
|
console.log('selectedActivity.value:', selectedActivity.value)
|
console.log('currentPage.value:', currentPage.value)
|
console.log('pageSize.value:', pageSize.value)
|
|
projectsLoading.value = true
|
try {
|
const params = {
|
activityId: selectedActivity.value,
|
page: currentPage.value,
|
size: pageSize.value
|
}
|
|
// 如果有搜索名称,添加到参数中
|
if (searchName.value && searchName.value.trim()) {
|
params.name = searchName.value.trim()
|
}
|
|
console.log('API调用参数:', params)
|
|
const response = await getProjectReviewApplications(params)
|
console.log('API响应:', response)
|
|
// 处理响应数据 - 新的分页结构
|
const pageData = response.projectReviewApplications
|
projects.value = pageData?.content || []
|
total.value = pageData?.totalElements || 0
|
|
console.log('设置 projects.value:', projects.value)
|
console.log('projects.value.length:', projects.value.length)
|
console.log('设置 total.value:', total.value)
|
} catch (error) {
|
console.error('加载项目列表失败:', error)
|
ElMessage.error('加载项目列表失败')
|
// 如果API调用失败,显示空数据
|
projects.value = []
|
total.value = 0
|
} finally {
|
projectsLoading.value = false
|
}
|
}
|
|
// 处理比赛选择变化
|
const handleActivityChange = (activityId) => {
|
currentPage.value = 1
|
loadProjects()
|
}
|
|
// 清空搜索
|
const handleClear = () => {
|
searchName.value = ''
|
currentPage.value = 1
|
if (selectedActivity.value) {
|
loadProjects()
|
}
|
}
|
|
// 重置搜索
|
const resetSearch = () => {
|
searchName.value = ''
|
selectedActivity.value = null
|
currentPage.value = 1
|
projects.value = []
|
total.value = 0
|
}
|
|
// 分页处理
|
const handleSizeChange = (size) => {
|
pageSize.value = size
|
currentPage.value = 1
|
loadProjects()
|
}
|
|
const handleCurrentChange = (page) => {
|
currentPage.value = page
|
loadProjects()
|
}
|
|
// 查看详情
|
const viewDetails = async (projectId) => {
|
// 传递stageId参数,selectedActivity.value就是当前选中的stageId
|
const stageId = selectedActivity.value
|
if (!stageId) {
|
ElMessage.warning('请先选择比赛阶段')
|
return
|
}
|
|
try {
|
// 获取当前用户信息
|
const userInfo = getUserInfo()
|
if (!userInfo) {
|
ElMessage.error('用户未登录,请重新登录')
|
return
|
}
|
|
// 检查用户是否有employee身份
|
const hasEmployeeRole = !!userInfo.employee
|
|
if (hasEmployeeRole) {
|
// 如果用户有employee身份,直接允许查看(不需要权限检查)
|
console.log('用户具有员工身份,允许查看所有评分记录')
|
router.push(`/project-review/${projectId}/detail?stageId=${stageId}`)
|
return
|
}
|
|
// 如果用户只有judge身份(没有employee身份),需要检查评委权限
|
const judgeInfo = await userApi.getCurrentJudgeInfo()
|
|
if (!judgeInfo) {
|
ElMessage.error('您没有评委权限,无法进行评审')
|
return
|
}
|
|
// 检查评委是否有当前比赛阶段的权限
|
const hasPermission = await userApi.checkJudgeInActivity(stageId, judgeInfo.judgeId)
|
|
if (!hasPermission) {
|
ElMessage.error('您没有当前比赛阶段的评审权限,无法进入评审页面')
|
return
|
}
|
|
// 权限检查通过,跳转到评审页面
|
router.push(`/project-review/${projectId}/detail?stageId=${stageId}`)
|
} catch (error) {
|
console.error('权限检查失败:', error)
|
ElMessage.error('权限验证失败,请重新登录')
|
}
|
}
|
|
// 显示评审列表
|
const showRatingList = async (project) => {
|
if (project.ratingCount === 0) {
|
ElMessage.warning('该项目暂无评审记录')
|
return
|
}
|
|
selectedProject.value = project
|
ratingListVisible.value = true
|
|
// 加载评审列表
|
await loadJudgeRatings(project.id)
|
}
|
|
// 加载评委评分列表
|
const loadJudgeRatings = async (activityPlayerId) => {
|
ratingsLoading.value = true
|
try {
|
const result = await getRatingStats(activityPlayerId)
|
console.log('getRatingStats 返回数据:', result)
|
|
// 使用正确的字段名
|
judgeRatings.value = result.ratings || []
|
console.log('judgeRatings 设置为:', judgeRatings.value)
|
} catch (error) {
|
console.error('加载评审列表失败:', error)
|
ElMessage.error('加载评审列表失败')
|
judgeRatings.value = []
|
} finally {
|
ratingsLoading.value = false
|
}
|
}
|
|
// 查看评分详情
|
const viewRatingDetail = async (rating) => {
|
if (!rating.hasRated) {
|
ElMessage.warning('该评委尚未评分')
|
return
|
}
|
|
selectedRating.value = rating
|
ratingDetailVisible.value = true
|
|
// 加载评分明细
|
await loadRatingDetail(selectedProject.value.id, rating.judgeId)
|
}
|
|
// 加载评分明细
|
const loadRatingDetail = async (activityPlayerId, judgeId) => {
|
ratingDetailLoading.value = true
|
try {
|
console.log('加载评分明细,activityPlayerId:', activityPlayerId, 'judgeId:', judgeId)
|
|
const result = await getJudgeRatingDetail(activityPlayerId, judgeId)
|
console.log('评分明细API返回:', result)
|
|
if (result && result.items) {
|
ratingItems.value = result.items.map(item => ({
|
itemName: item.ratingItemName,
|
score: item.score,
|
maxScore: item.maxScore || 100
|
}))
|
|
// 更新选中评分的备注信息
|
if (selectedRating.value) {
|
selectedRating.value.remark = result.remark
|
}
|
} else {
|
ratingItems.value = []
|
}
|
} catch (error) {
|
console.error('加载评分明细失败:', error)
|
ElMessage.error('加载评分明细失败: ' + (error.message || '未知错误'))
|
ratingItems.value = []
|
} finally {
|
ratingDetailLoading.value = false
|
}
|
}
|
|
// 关闭评审列表弹窗
|
const handleRatingListClose = () => {
|
ratingListVisible.value = false
|
selectedProject.value = null
|
judgeRatings.value = []
|
}
|
|
// 关闭评分详情弹窗
|
const handleRatingDetailClose = () => {
|
ratingDetailVisible.value = false
|
selectedRating.value = null
|
ratingItems.value = []
|
}
|
|
// 格式化日期
|
const formatDate = (dateString) => {
|
if (!dateString) return '-'
|
return new Date(dateString).toLocaleString('zh-CN')
|
}
|
|
// 获取状态类型
|
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 getReviewStatusType = (ratingCount) => {
|
return ratingCount > 0 ? 'success' : 'warning'
|
}
|
|
// 获取评审状态名称(基于评审次数)
|
const getReviewStatusName = (ratingCount) => {
|
return ratingCount > 0 ? '已评审' : '未评审'
|
}
|
|
// 获取比赛名称(如果是阶段,返回父比赛名称;如果是比赛,返回自己的名称)
|
const getActivityName = (activity) => {
|
if (activity.pid > 0 && activity.parent) {
|
return activity.parent.name
|
}
|
return activity.name
|
}
|
|
// 获取活动显示名称(用于搜索和选中时显示)
|
const getActivityDisplayName = (activity) => {
|
if (activity.pid > 0 && activity.parent) {
|
return `${activity.parent.name} - ${activity.name}`
|
}
|
return activity.name
|
}
|
|
// 获取阶段名称(如果是阶段,返回阶段名称;如果是比赛,返回比赛名称)
|
const getStageName = (activity) => {
|
return activity.name
|
}
|
|
|
|
// 组件挂载时加载数据
|
onMounted(() => {
|
loadActivities()
|
})
|
</script>
|
|
<style scoped>
|
.review-container {
|
padding: 20px;
|
}
|
|
/* 页面标题区域 */
|
.page-header {
|
margin-bottom: 24px;
|
}
|
|
.title-section {
|
text-align: left;
|
}
|
|
.page-title {
|
font-size: 24px;
|
font-weight: 600;
|
color: #1f2937;
|
margin: 0 0 8px 0;
|
}
|
|
.page-subtitle {
|
font-size: 14px;
|
color: #6b7280;
|
margin: 0;
|
line-height: 1.5;
|
}
|
|
/* 评审次数按钮样式 */
|
.rating-count-btn {
|
font-weight: 500;
|
}
|
|
/* 弹窗样式 */
|
.dialog-header {
|
margin-bottom: 20px;
|
|
h4 {
|
margin: 0 0 8px 0;
|
color: #303133;
|
font-size: 16px;
|
font-weight: 600;
|
}
|
|
.project-info {
|
margin: 0;
|
color: #606266;
|
font-size: 14px;
|
}
|
}
|
|
.rating-detail-header {
|
margin-bottom: 20px;
|
|
h4 {
|
margin: 0 0 8px 0;
|
color: #303133;
|
font-size: 16px;
|
font-weight: 600;
|
}
|
|
.total-score {
|
color: #409eff;
|
font-weight: 600;
|
font-size: 18px;
|
}
|
}
|
|
.rating-comment {
|
margin-top: 20px;
|
padding: 16px;
|
background-color: #f5f7fa;
|
border-radius: 8px;
|
|
h5 {
|
margin: 0 0 8px 0;
|
color: #303133;
|
font-size: 14px;
|
font-weight: 600;
|
}
|
|
p {
|
margin: 0;
|
color: #606266;
|
line-height: 1.6;
|
}
|
}
|
|
.score {
|
color: #67c23a;
|
font-weight: 600;
|
}
|
|
.no-score {
|
color: #909399;
|
}
|
|
.item-score {
|
color: #409eff;
|
font-weight: 500;
|
}
|
|
.max-score {
|
color: #909399;
|
}
|
|
/* 搜索工具栏 */
|
.search-toolbar {
|
display: flex;
|
justify-content: flex-end;
|
margin-bottom: 20px;
|
}
|
|
.search-form {
|
display: flex;
|
gap: 12px;
|
align-items: center;
|
}
|
|
.search-area {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
}
|
|
/* 操作按钮样式 */
|
.action-btn {
|
padding: 8px !important;
|
margin: 0 6px;
|
border-radius: 6px;
|
transition: all 0.2s ease;
|
}
|
|
.view-btn {
|
color: #3b82f6 !important;
|
}
|
|
.view-btn:hover {
|
background-color: rgba(59, 130, 246, 0.1) !important;
|
transform: scale(1.2);
|
}
|
|
.score {
|
color: #67c23a;
|
font-weight: 600;
|
}
|
|
.no-score {
|
color: #909399;
|
font-style: italic;
|
}
|
|
.pagination-container {
|
margin-top: 20px;
|
display: flex;
|
justify-content: center;
|
}
|
|
:deep(.el-table) {
|
border-radius: 4px;
|
}
|
|
:deep(.el-table th) {
|
background-color: #fafafa;
|
}
|
|
:deep(.el-tag) {
|
border-radius: 12px;
|
}
|
|
/* 响应式适配 */
|
@media (max-width: 768px) {
|
.search-toolbar {
|
flex-direction: column;
|
gap: 12px;
|
align-items: stretch;
|
}
|
|
.search-area {
|
justify-content: center;
|
flex-wrap: wrap;
|
}
|
}
|
</style>
|