| | |
| | | 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: 200px" |
| | | style="width: 220px" |
| | | clearable |
| | | filterable |
| | | > |
| | |
| | | </span> |
| | | </el-option> |
| | | </el-select> |
| | | <el-button |
| | | type="primary" |
| | | :icon="Search" |
| | | <el-button |
| | | type="primary" |
| | | :icon="Search" |
| | | @click="loadProjects" |
| | | :loading="projectsLoading" |
| | | > |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <el-table |
| | | :data="projects" |
| | | style="width: 100%" |
| | | <el-table |
| | | :data="projects" |
| | | style="width: 100%" |
| | | v-loading="projectsLoading" |
| | | empty-text="请先选择比赛" |
| | | > |
| | |
| | | <h4>{{ selectedProject.projectName || selectedProject.playerName }} - 评审详情</h4> |
| | | <p class="project-info">参赛人:{{ selectedProject.playerName }} | 联系电话:{{ selectedProject.phone }}</p> |
| | | </div> |
| | | |
| | | <el-table |
| | | :data="judgeRatings" |
| | | |
| | | <el-table |
| | | :data="judgeRatings" |
| | | v-loading="ratingsLoading" |
| | | style="width: 100%" |
| | | > |
| | |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleRatingListClose">关闭</el-button> |
| | |
| | | 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 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="ratingItems" |
| | | v-loading="ratingDetailLoading" |
| | | style="width: 100%" |
| | | |
| | | <el-table |
| | | :data="ratingDetail.items || []" |
| | | border |
| | | size="small" |
| | | > |
| | | <el-table-column prop="itemName" label="评分项目" min-width="150" /> |
| | | <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 }}</span> |
| | | <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 || 100 }}</span> |
| | | <span class="max-score">{{ scope.row.maxScore?.toFixed(1) ?? '-' }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <div v-if="selectedRating.remark" class="rating-comment"> |
| | | <h5>评语:</h5> |
| | | <p>{{ selectedRating.remark }}</p> |
| | | |
| | | <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> |
| | | |
| | | <script setup> |
| | | <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() |
| | | |
| | | // 响应式数据 |
| | | const activities = ref([]) |
| | | const selectedActivity = ref(null) |
| | | const projects = ref([]) |
| | | 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(null) |
| | | const selectedRating = ref(null) |
| | | const judgeRatings = ref([]) |
| | | const ratingItems = ref([]) |
| | | 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 () => { |
| | |
| | | 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.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) |
| | | ElMessage.error(error?.message || '加载比赛列表失败') |
| | | } finally { |
| | | activitiesLoading.value = false |
| | | console.log('=== 比赛列表加载完成 ===') |
| | | } |
| | | } |
| | | |
| | |
| | | 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 = { |
| | | 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('API调用参数:', params) |
| | | |
| | | |
| | | 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 |
| | | |
| | | console.log('设置 projects.value:', projects.value) |
| | | console.log('projects.value.length:', projects.value.length) |
| | | console.log('设置 total.value:', total.value) |
| | | } catch (error) { |
| | | } catch (error: any) { |
| | | console.error('加载项目列表失败:', error) |
| | | ElMessage.error('加载项目列表失败') |
| | | // 如果API调用失败,显示空数据 |
| | | projects.value = [] |
| | | total.value = 0 |
| | | ElMessage.error(error?.message || '加载项目列表失败') |
| | | } finally { |
| | | projectsLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // 处理区域选择变化 |
| | | const handleRegionChange = () => { |
| | | currentPage.value = 1 |
| | | if (selectedActivity.value) { |
| | | loadProjects() |
| | | } |
| | | } |
| | | |
| | | // 处理比赛选择变化 |
| | | const handleActivityChange = (activityId) => { |
| | | const handleActivityChange = () => { |
| | | currentPage.value = 1 |
| | | loadProjects() |
| | | } |
| | |
| | | // 清空搜索 |
| | | const handleClear = () => { |
| | | searchName.value = '' |
| | | currentPage.value = 1 |
| | | if (selectedActivity.value) { |
| | | loadProjects() |
| | | } |
| | | loadProjects() |
| | | } |
| | | |
| | | // 重置搜索 |
| | | const resetSearch = () => { |
| | | searchName.value = '' |
| | | selectedActivity.value = null |
| | | currentPage.value = 1 |
| | | projects.value = [] |
| | | total.value = 0 |
| | | } |
| | | |
| | | // 分页处理 |
| | | const handleSizeChange = (size) => { |
| | | // 分页大小改变 |
| | | const handleSizeChange = (size: number) => { |
| | | pageSize.value = size |
| | | currentPage.value = 1 |
| | | loadProjects() |
| | | } |
| | | |
| | | const handleCurrentChange = (page) => { |
| | | // 当前页改变 |
| | | const handleCurrentChange = (page: number) => { |
| | | 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 |
| | | } |
| | | |
| | | // 展示评审列表 |
| | | const showRatingList = async (project: any) => { |
| | | selectedProject.value = project |
| | | ratingListVisible.value = true |
| | | |
| | | // 加载评审列表 |
| | | await loadJudgeRatings(project.id) |
| | | } |
| | | |
| | | // 加载评委评分列表 |
| | | const loadJudgeRatings = async (activityPlayerId) => { |
| | | ratingsLoading.value = true |
| | | ratingListVisible.value = true |
| | | try { |
| | | const result = await getRatingStats(activityPlayerId) |
| | | console.log('getRatingStats 返回数据:', result) |
| | | |
| | | // 使用正确的字段名 |
| | | judgeRatings.value = result.ratings || [] |
| | | console.log('judgeRatings 设置为:', judgeRatings.value) |
| | | } catch (error) { |
| | | const data = await getRatingStats(project.id) |
| | | judgeRatings.value = data || [] |
| | | } catch (error: any) { |
| | | console.error('加载评审列表失败:', error) |
| | | ElMessage.error('加载评审列表失败') |
| | | judgeRatings.value = [] |
| | | ElMessage.error(error?.message || '加载评审列表失败') |
| | | } 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 handleRatingListClose = () => { |
| | | ratingListVisible.value = false |
| | | judgeRatings.value = [] |
| | | } |
| | | |
| | | // 加载评分明细 |
| | | const loadRatingDetail = async (activityPlayerId, judgeId) => { |
| | | // 查看评分详情 |
| | | const viewRatingDetail = async (rating: any) => { |
| | | ratingDetailVisible.value = true |
| | | 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 = [] |
| | | const data = await getJudgeRatingDetail(rating.id) |
| | | ratingDetail.value = data |
| | | } catch (error: any) { |
| | | console.error('加载评分详情失败:', error) |
| | | ElMessage.error(error?.message || '加载评分详情失败') |
| | | } finally { |
| | | ratingDetailLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // 关闭评审列表弹窗 |
| | | const handleRatingListClose = () => { |
| | | ratingListVisible.value = false |
| | | selectedProject.value = null |
| | | judgeRatings.value = [] |
| | | } |
| | | |
| | | // 关闭评分详情弹窗 |
| | | const handleRatingDetailClose = () => { |
| | | ratingDetailVisible.value = false |
| | | selectedRating.value = null |
| | | ratingItems.value = [] |
| | | ratingDetail.value = null |
| | | } |
| | | |
| | | // 格式化日期 |
| | | 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 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) => { |
| | | const getReviewStatusType = (ratingCount: number) => { |
| | | return ratingCount > 0 ? 'success' : 'warning' |
| | | } |
| | | |
| | | // 获取评审状态名称(基于评审次数) |
| | | const getReviewStatusName = (ratingCount) => { |
| | | const getReviewStatusName = (ratingCount: number) => { |
| | | return ratingCount > 0 ? '已评审' : '未评审' |
| | | } |
| | | |
| | | // 获取比赛名称(如果是阶段,返回父比赛名称;如果是比赛,返回自己的名称) |
| | | const getActivityName = (activity) => { |
| | | const getActivityName = (activity: ActivityItem) => { |
| | | if (activity.pid > 0 && activity.parent) { |
| | | return activity.parent.name |
| | | } |
| | |
| | | } |
| | | |
| | | // 获取活动显示名称(用于搜索和选中时显示) |
| | | const getActivityDisplayName = (activity) => { |
| | | const getActivityDisplayName = (activity: ActivityItem) => { |
| | | if (activity.pid > 0 && activity.parent) { |
| | | return `${activity.parent.name} - ${activity.name}` |
| | | } |
| | | return activity.name |
| | | } |
| | | |
| | | // 获取阶段名称(如果是阶段,返回阶段名称;如果是比赛,返回比赛名称) |
| | | const getStageName = (activity) => { |
| | | 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> |
| | |
| | | /* 弹窗样式 */ |
| | | .dialog-header { |
| | | margin-bottom: 20px; |
| | | |
| | | |
| | | h4 { |
| | | margin: 0 0 8px 0; |
| | | color: #303133; |
| | | font-size: 16px; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2937; |
| | | } |
| | | |
| | | |
| | | .project-info { |
| | | margin: 0; |
| | | color: #606266; |
| | | font-size: 14px; |
| | | color: #6b7280; |
| | | font-size: 13px; |
| | | } |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | .rating-comment { |
| | |
| | | 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; |
| | |
| | | align-items: center; |
| | | } |
| | | |
| | | .search-area { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | /* 操作按钮样式 */ |
| | | .action-btn { |
| | | padding: 8px !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 { |
| | |
| | | gap: 12px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .search-area { |
| | | |
| | | .search-form { |
| | | justify-content: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | } |
| | | </style> |
| | | </style> |