<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="selectedRegion"
|
placeholder="请选择区域"
|
@change="handleRegionChange"
|
style="width: 200px"
|
clearable
|
filterable
|
>
|
<el-option
|
v-for="region in regions"
|
:key="region.id"
|
:label="region.name"
|
:value="region.id"
|
/>
|
</el-select>
|
<el-select
|
v-model="selectedActivity"
|
placeholder="请选择比赛"
|
@change="handleActivityChange"
|
style="width: 220px"
|
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="ratingDetail">
|
<div class="dialog-header">
|
<h4>{{ ratingDetail.judgeName }} 评审结果</h4>
|
<p class="project-info">
|
总分:
|
<span class="total-score">
|
{{ ratingDetail.totalScore?.toFixed(1) ?? '暂无' }}
|
</span>
|
</p>
|
</div>
|
|
<el-table
|
:data="ratingDetail.items || []"
|
border
|
size="small"
|
>
|
<el-table-column prop="itemName" label="评分项" min-width="160" />
|
<el-table-column prop="score" label="得分" width="100" align="center">
|
<template #default="scope">
|
<span class="item-score">{{ scope.row.score?.toFixed(1) ?? '-' }}</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?.toFixed(1) ?? '-' }}</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div class="rating-comment" v-if="ratingDetail.comment">
|
<h5>评语</h5>
|
<p>{{ ratingDetail.comment }}</p>
|
</div>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="handleRatingDetailClose">关闭</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
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 { getLeafRegions } from '@/api/region'
|
import { userApi } from '@/api/user'
|
import { getUserInfo } from '@/utils/auth'
|
|
const router = useRouter()
|
|
interface ActivityItem {
|
id: number
|
name: string
|
pid: number
|
parent?: ActivityItem
|
}
|
|
interface RegionItem {
|
id: number
|
name: string
|
}
|
|
const activities = ref<ActivityItem[]>([])
|
const regions = ref<RegionItem[]>([])
|
const selectedActivity = ref<number | null>(null)
|
const selectedRegion = ref<number | null>(null)
|
const projects = ref<any[]>([])
|
const searchName = ref('')
|
const activitiesLoading = ref(false)
|
const projectsLoading = ref(false)
|
|
// 弹窗相关状态
|
const ratingListVisible = ref(false)
|
const ratingDetailVisible = ref(false)
|
const selectedProject = ref<any | null>(null)
|
const judgeRatings = ref<any[]>([])
|
const ratingDetail = ref<any | null>(null)
|
const ratingsLoading = ref(false)
|
const ratingDetailLoading = ref(false)
|
|
// 分页
|
const currentPage = ref(1)
|
const pageSize = ref(10)
|
const total = ref(0)
|
|
const loadRegions = async () => {
|
try {
|
const data = await getLeafRegions()
|
regions.value = data || []
|
} catch (error: any) {
|
console.error('加载区域列表失败:', error)
|
ElMessage.error(error?.message || '加载区域列表失败')
|
}
|
}
|
|
// 加载比赛列表
|
const loadActivities = async () => {
|
console.log('=== 开始加载比赛列表 ===')
|
activitiesLoading.value = true
|
try {
|
console.log('调用 getActiveActivities...')
|
const data = await getActiveActivities()
|
console.log('比赛数据:', data)
|
|
activities.value = data || []
|
|
if (activities.value.length === 1) {
|
selectedActivity.value = activities.value[0].id
|
console.log('自动选中唯一比赛:', selectedActivity.value)
|
loadProjects()
|
}
|
} catch (error: any) {
|
console.error('加载比赛列表失败:', error)
|
ElMessage.error(error?.message || '加载比赛列表失败')
|
} finally {
|
activitiesLoading.value = false
|
}
|
}
|
|
// 加载项目列表
|
const loadProjects = async () => {
|
if (!selectedActivity.value) {
|
ElMessage.warning('请先选择比赛')
|
return
|
}
|
|
console.log('=== 开始加载项目列表 ===')
|
console.log('selectedActivity.value:', selectedActivity.value)
|
console.log('selectedRegion.value:', selectedRegion.value)
|
console.log('currentPage.value:', currentPage.value)
|
console.log('pageSize.value:', pageSize.value)
|
|
projectsLoading.value = true
|
try {
|
const params: Record<string, unknown> = {
|
activityId: selectedActivity.value,
|
page: currentPage.value,
|
size: pageSize.value
|
}
|
|
if (selectedRegion.value) {
|
params.regionId = selectedRegion.value
|
}
|
|
if (searchName.value && searchName.value.trim()) {
|
params.name = searchName.value.trim()
|
}
|
|
console.log('请求参数:', params)
|
const response = await getProjectReviewApplications(params)
|
console.log('API响应:', response)
|
|
const pageData = response.projectReviewApplications
|
projects.value = pageData?.content || []
|
total.value = pageData?.totalElements || 0
|
} catch (error: any) {
|
console.error('加载项目列表失败:', error)
|
ElMessage.error(error?.message || '加载项目列表失败')
|
} finally {
|
projectsLoading.value = false
|
}
|
}
|
|
// 处理区域选择变化
|
const handleRegionChange = () => {
|
currentPage.value = 1
|
if (selectedActivity.value) {
|
loadProjects()
|
}
|
}
|
|
// 处理比赛选择变化
|
const handleActivityChange = () => {
|
currentPage.value = 1
|
loadProjects()
|
}
|
|
// 清空搜索
|
const handleClear = () => {
|
searchName.value = ''
|
loadProjects()
|
}
|
|
// 分页大小改变
|
const handleSizeChange = (size: number) => {
|
pageSize.value = size
|
currentPage.value = 1
|
loadProjects()
|
}
|
|
// 当前页改变
|
const handleCurrentChange = (page: number) => {
|
currentPage.value = page
|
loadProjects()
|
}
|
|
// 展示评审列表
|
const showRatingList = async (project: any) => {
|
selectedProject.value = project
|
ratingsLoading.value = true
|
ratingListVisible.value = true
|
try {
|
const data = await getRatingStats(project.id)
|
judgeRatings.value = data || []
|
} catch (error: any) {
|
console.error('加载评审列表失败:', error)
|
ElMessage.error(error?.message || '加载评审列表失败')
|
} finally {
|
ratingsLoading.value = false
|
}
|
}
|
|
const handleRatingListClose = () => {
|
ratingListVisible.value = false
|
judgeRatings.value = []
|
}
|
|
// 查看评分详情
|
const viewRatingDetail = async (rating: any) => {
|
ratingDetailVisible.value = true
|
ratingDetailLoading.value = true
|
try {
|
const data = await getJudgeRatingDetail(rating.id)
|
ratingDetail.value = data
|
} catch (error: any) {
|
console.error('加载评分详情失败:', error)
|
ElMessage.error(error?.message || '加载评分详情失败')
|
} finally {
|
ratingDetailLoading.value = false
|
}
|
}
|
|
const handleRatingDetailClose = () => {
|
ratingDetailVisible.value = false
|
ratingDetail.value = null
|
}
|
|
// 格式化时间
|
const formatDate = (value: string | number | Date) => {
|
if (!value) return '-'
|
const date = new Date(value)
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(
|
date.getDate()
|
).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(
|
date.getMinutes()
|
).padStart(2, '0')}`
|
}
|
|
// 获取评审状态类型(基于评审次数)
|
const getReviewStatusType = (ratingCount: number) => {
|
return ratingCount > 0 ? 'success' : 'warning'
|
}
|
|
// 获取评审状态名称(基于评审次数)
|
const getReviewStatusName = (ratingCount: number) => {
|
return ratingCount > 0 ? '已评审' : '未评审'
|
}
|
|
// 获取比赛名称(如果是阶段,返回父比赛名称;如果是比赛,返回自己的名称)
|
const getActivityName = (activity: ActivityItem) => {
|
if (activity.pid > 0 && activity.parent) {
|
return activity.parent.name
|
}
|
return activity.name
|
}
|
|
// 获取活动显示名称(用于搜索和选中时显示)
|
const getActivityDisplayName = (activity: ActivityItem) => {
|
if (activity.pid > 0 && activity.parent) {
|
return `${activity.parent.name} - ${activity.name}`
|
}
|
return activity.name
|
}
|
|
// 权限检查:只有管理员可以查看项目评审
|
const checkPermission = async () => {
|
const userInfo = getUserInfo()
|
if (!userInfo) {
|
router.push('/login')
|
}
|
}
|
|
// 查看项目详情
|
const viewDetails = (projectId: number) => {
|
router.push(`/review/detail/${projectId}`)
|
}
|
|
// 组件挂载时加载数据
|
onMounted(() => {
|
checkPermission()
|
loadRegions()
|
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;
|
font-size: 18px;
|
font-weight: 600;
|
color: #1f2937;
|
}
|
|
.project-info {
|
margin: 0;
|
color: #6b7280;
|
font-size: 13px;
|
}
|
}
|
|
.dialog-footer {
|
text-align: right;
|
}
|
|
.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;
|
}
|
|
/* 操作按钮样式 */
|
.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);
|
}
|
|
.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-form {
|
justify-content: center;
|
flex-wrap: wrap;
|
}
|
}
|
</style>
|