<template>
|
<div class="promotion-container">
|
<el-card>
|
<template #header>
|
<div class="card-header">
|
<h3 class="card-title">比赛晋级管理</h3>
|
</div>
|
</template>
|
|
<!-- 搜索栏 -->
|
<el-form :inline="true" class="search-form">
|
<el-form-item label="比赛名称">
|
<el-input
|
v-model="searchForm.name"
|
placeholder="请输入比赛名称"
|
@keyup.enter="loadData"
|
style="width: 200px"
|
clearable
|
/>
|
</el-form-item>
|
|
<el-form-item>
|
<el-button type="primary" @click="loadData" :loading="loading">
|
搜索
|
</el-button>
|
<el-button @click="resetSearch">重置</el-button>
|
</el-form-item>
|
</el-form>
|
|
<!-- 数据表格 -->
|
<el-table
|
:data="competitions"
|
style="width: 100%"
|
v-loading="loading"
|
empty-text="暂无比赛数据"
|
>
|
<el-table-column prop="competitionName" label="比赛名称" min-width="150">
|
<template #default="scope">
|
<div class="competition-info">
|
<div class="main-name">{{ scope.row.competitionName }}</div>
|
<div class="stage-name">{{ scope.row.stageName }}</div>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="maxParticipants" label="最大人数" width="100" align="center">
|
<template #default="scope">
|
<el-tag type="info">{{ scope.row.maxParticipants || '不限' }}</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="currentCount" label="当前数量" width="100" align="center">
|
<template #default="scope">
|
<el-button
|
type="text"
|
@click="viewParticipants(scope.row)"
|
class="count-link"
|
>
|
{{ scope.row.currentCount }}
|
</el-button>
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
<template #default="scope">
|
<el-tag :type="getStatusType(scope.row.status)">
|
{{ getStatusText(scope.row.status) }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="startTime" label="开始时间" width="180">
|
<template #default="scope">
|
{{ formatDate(scope.row.startTime) }}
|
</template>
|
</el-table-column>
|
|
<el-table-column prop="endTime" label="结束时间" width="180">
|
<template #default="scope">
|
{{ formatDate(scope.row.endTime) }}
|
</template>
|
</el-table-column>
|
|
<el-table-column label="操作" width="150" fixed="right">
|
<template #default="scope">
|
<!-- 只有非第一阶段才显示晋级按钮 -->
|
<el-button
|
v-if="scope.row.sortOrder > 1"
|
type="primary"
|
size="small"
|
@click="selectPromotionCandidates(scope.row)"
|
:disabled="scope.row.state !== 1"
|
>
|
选择晋级人员
|
</el-button>
|
<!-- 第一阶段显示提示文本 -->
|
<span v-else class="no-promotion-text">首轮比赛</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<!-- 分页 -->
|
<div class="pagination-container">
|
<el-pagination
|
v-model:current-page="pagination.page"
|
v-model:page-size="pagination.size"
|
:page-sizes="[10, 20, 50, 100]"
|
:total="pagination.total"
|
layout="total, sizes, prev, pager, next, jumper"
|
@size-change="handleSizeChange"
|
@current-change="handleCurrentChange"
|
/>
|
</div>
|
</el-card>
|
|
<!-- 晋级人员选择对话框 -->
|
<el-dialog
|
v-model="promotionDialogVisible"
|
title="选择晋级人员"
|
width="80%"
|
:before-close="handlePromotionDialogClose"
|
>
|
<div v-if="selectedCompetition">
|
<div class="dialog-header">
|
<h4>{{ selectedCompetition.competitionName }} - {{ selectedCompetition.stageName }}</h4>
|
<div class="promotion-info">
|
<p>从 <strong>{{ promotableData.previousStageName }}</strong> 晋级到 <strong>{{ promotableData.currentStageName }}</strong></p>
|
<p>上一阶段总人数:{{ promotableData.totalCount }}人,可选择晋级:{{ promotableData.selectableCount }}人</p>
|
</div>
|
</div>
|
|
<!-- 搜索框 -->
|
<div class="search-container">
|
<el-input
|
v-model="searchKeyword"
|
placeholder="搜索项目名称或参赛人员姓名..."
|
clearable
|
style="width: 300px; margin-bottom: 15px"
|
@input="handleSearch"
|
>
|
<template #prefix>
|
<el-icon><Search /></el-icon>
|
</template>
|
</el-input>
|
</div>
|
|
<!-- 参赛人员列表 -->
|
<div class="table-info">
|
<el-alert
|
title="提示"
|
:description="`参赛者已按平均分从高到低排序,建议选择前 ${promotableData.selectableCount} 名进行晋级`"
|
type="info"
|
show-icon
|
:closable="false"
|
style="margin-bottom: 15px"
|
/>
|
</div>
|
|
<el-table
|
:data="participants"
|
v-loading="participantsLoading"
|
@selection-change="handleSelectionChange"
|
style="width: 100%"
|
:row-class-name="getRowClassName"
|
>
|
<el-table-column type="selection" width="55" />
|
<el-table-column type="index" label="排名" width="60" align="center" />
|
<el-table-column prop="playerName" label="参赛者姓名" min-width="120" />
|
<el-table-column prop="projectName" label="项目名称" min-width="150" />
|
<el-table-column prop="phone" label="联系电话" width="120" />
|
<el-table-column prop="averageScore" label="平均分" width="100" align="center" sortable>
|
<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="ratingCount" label="评审次数" width="100" align="center">
|
<template #default="scope">
|
<el-tag :type="scope.row.ratingCount > 0 ? 'success' : 'info'">
|
{{ scope.row.ratingCount }}
|
</el-tag>
|
</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>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<span class="selected-info">已选择 {{ selectedParticipants.length }} 人</span>
|
<el-button @click="handlePromotionDialogClose">取消</el-button>
|
<el-button
|
type="primary"
|
@click="confirmPromotion"
|
:disabled="selectedParticipants.length === 0"
|
:loading="promotionLoading"
|
>
|
确认晋级
|
</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted } from 'vue'
|
import { useRouter } from 'vue-router'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { Search } from '@element-plus/icons-vue'
|
import PromotionApi from '@/api/promotion'
|
|
const router = useRouter()
|
|
// 响应式数据
|
const loading = ref(false)
|
const participantsLoading = ref(false)
|
const promotionLoading = ref(false)
|
const promotionDialogVisible = ref(false)
|
|
// 搜索表单
|
const searchForm = reactive({
|
name: ''
|
})
|
|
// 比赛数据
|
const competitions = ref([])
|
|
// 分页数据
|
const pagination = reactive({
|
page: 1,
|
size: 10,
|
total: 0
|
})
|
|
// 选中的比赛
|
const selectedCompetition = ref(null)
|
|
// 参赛人员数据
|
const participants = ref([])
|
const originalParticipants = ref([]) // 保存原始参赛者数据用于搜索
|
const selectedParticipants = ref([])
|
|
// 搜索关键词
|
const searchKeyword = ref('')
|
|
// 可晋级参赛者数据
|
const promotableData = ref({
|
participants: [],
|
selectableCount: 0,
|
totalCount: 0,
|
previousStageName: '',
|
currentStageName: ''
|
})
|
|
// 加载比赛数据
|
const loadData = async () => {
|
loading.value = true
|
try {
|
// 尝试使用真实API
|
const data = await PromotionApi.getPromotionCompetitions({
|
name: searchForm.name,
|
page: pagination.page,
|
size: pagination.size
|
})
|
|
competitions.value = data
|
pagination.total = data.length
|
} catch (error) {
|
console.warn('API调用失败,使用模拟数据:', error)
|
|
// API失败时使用模拟数据
|
const mockData = [
|
{
|
id: 1,
|
competitionName: '2027创新创业大赛',
|
stageName: '海选',
|
maxParticipants: null,
|
currentCount: 15,
|
status: 1,
|
startTime: '2024-01-01T00:00:00',
|
endTime: '2024-03-31T23:59:59'
|
},
|
{
|
id: 2,
|
competitionName: '2027创新创业大赛',
|
stageName: '初赛',
|
maxParticipants: 50,
|
currentCount: 8,
|
status: 1,
|
startTime: '2024-04-01T00:00:00',
|
endTime: '2024-06-30T23:59:59'
|
},
|
{
|
id: 3,
|
competitionName: '2027创新创业大赛',
|
stageName: '决赛',
|
maxParticipants: 10,
|
currentCount: 0,
|
status: 0,
|
startTime: '2024-07-01T00:00:00',
|
endTime: '2024-08-31T23:59:59'
|
}
|
]
|
|
let filteredData = mockData
|
if (searchForm.name) {
|
filteredData = mockData.filter(item =>
|
item.competitionName.includes(searchForm.name) ||
|
item.stageName.includes(searchForm.name)
|
)
|
}
|
|
competitions.value = filteredData
|
pagination.total = filteredData.length
|
} finally {
|
loading.value = false
|
}
|
}
|
|
// 重置搜索
|
const resetSearch = () => {
|
searchForm.name = ''
|
pagination.page = 1
|
loadData()
|
}
|
|
// 分页处理
|
const handleSizeChange = (size) => {
|
pagination.size = size
|
pagination.page = 1
|
loadData()
|
}
|
|
const handleCurrentChange = (page) => {
|
pagination.page = page
|
loadData()
|
}
|
|
// 查看参赛人员
|
const viewParticipants = (competition) => {
|
router.push({
|
path: '/project-review',
|
query: {
|
competitionId: competition.id,
|
competitionName: competition.competitionName,
|
stageName: competition.stageName
|
}
|
})
|
}
|
|
// 选择晋级人员
|
const selectPromotionCandidates = async (competition) => {
|
selectedCompetition.value = competition
|
promotionDialogVisible.value = true
|
await loadPromotableParticipants(competition.id)
|
}
|
|
// 加载可晋级参赛人员
|
const loadPromotableParticipants = async (currentStageId) => {
|
participantsLoading.value = true
|
try {
|
// 使用新的API获取可晋级参赛者
|
const data = await PromotionApi.getPromotableParticipants(currentStageId)
|
promotableData.value = data
|
originalParticipants.value = data.participants
|
participants.value = data.participants
|
} catch (error) {
|
console.warn('获取可晋级参赛者API失败,使用模拟数据:', error)
|
// API失败时使用模拟数据
|
const mockData = {
|
participants: [
|
{
|
id: 1,
|
playerId: 101,
|
playerName: 'UK2025',
|
projectName: '智能家居系统',
|
phone: '13800138001',
|
averageScore: 85.5,
|
ratingCount: 3,
|
applyTime: '2024-01-15T10:30:00',
|
state: 1
|
},
|
{
|
id: 2,
|
playerId: 102,
|
playerName: '张三',
|
projectName: 'AI图像识别',
|
phone: '13800138002',
|
averageScore: 92.0,
|
ratingCount: 3,
|
applyTime: '2024-01-16T14:20:00',
|
state: 1
|
},
|
{
|
id: 3,
|
playerId: 103,
|
playerName: '李四',
|
projectName: '区块链应用',
|
phone: '13800138003',
|
averageScore: 78.3,
|
ratingCount: 2,
|
applyTime: '2024-01-17T09:15:00',
|
state: 1
|
}
|
],
|
selectableCount: 10,
|
totalCount: 15,
|
previousStageName: '海选',
|
currentStageName: '初赛'
|
}
|
|
promotableData.value = mockData
|
originalParticipants.value = mockData.participants
|
participants.value = mockData.participants
|
} finally {
|
participantsLoading.value = false
|
}
|
}
|
|
// 加载参赛人员(保留原方法以兼容其他功能)
|
const loadParticipants = async (competitionId) => {
|
participantsLoading.value = true
|
try {
|
// 尝试使用真实API获取参赛人员
|
const participantsData = await PromotionApi.getCompetitionParticipants(competitionId)
|
participants.value = participantsData
|
} catch (error) {
|
console.warn('获取参赛人员API失败,使用模拟数据:', error)
|
// API失败时使用模拟数据
|
const mockParticipants = [
|
{
|
id: 1,
|
playerName: 'UK2025',
|
projectName: '智能家居系统',
|
phone: '13800138001',
|
averageScore: 85.5,
|
ratingCount: 3,
|
applyTime: '2024-01-15T10:30:00'
|
},
|
{
|
id: 2,
|
playerName: '张三',
|
projectName: 'AI图像识别',
|
phone: '13800138002',
|
averageScore: 92.0,
|
ratingCount: 3,
|
applyTime: '2024-01-16T14:20:00'
|
},
|
{
|
id: 3,
|
playerName: '李四',
|
projectName: '区块链应用',
|
phone: '13800138003',
|
averageScore: 78.3,
|
ratingCount: 2,
|
applyTime: '2024-01-17T09:15:00'
|
}
|
]
|
|
participants.value = mockParticipants
|
} finally {
|
participantsLoading.value = false
|
}
|
}
|
|
// 处理选择变化
|
const handleSelectionChange = (selection) => {
|
selectedParticipants.value = selection
|
}
|
|
// 确认晋级
|
const confirmPromotion = async () => {
|
if (selectedParticipants.value.length === 0) {
|
ElMessage.warning('请选择要晋级的人员')
|
return
|
}
|
|
try {
|
await ElMessageBox.confirm(
|
`确定要将选中的 ${selectedParticipants.value.length} 名参赛者晋级到下一阶段吗?`,
|
'确认晋级',
|
{
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
}
|
)
|
|
promotionLoading.value = true
|
|
try {
|
// 尝试使用真实API执行晋级
|
const participantIds = selectedParticipants.value.map(p => p.id)
|
const result = await PromotionApi.promoteParticipants(
|
selectedCompetition.value.id,
|
participantIds,
|
null // 目标阶段ID,这里可以根据需要设置
|
)
|
|
ElMessage.success(result.message || `成功晋级 ${result.promotedCount} 名人员`)
|
} catch (error) {
|
console.warn('晋级API失败,使用模拟操作:', error)
|
// API失败时模拟成功
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
ElMessage.success(`成功晋级 ${selectedParticipants.value.length} 名人员`)
|
}
|
|
handlePromotionDialogClose()
|
loadData() // 重新加载数据
|
} catch {
|
// 用户取消
|
} finally {
|
promotionLoading.value = false
|
}
|
}
|
|
// 关闭晋级对话框
|
const handlePromotionDialogClose = () => {
|
promotionDialogVisible.value = false
|
selectedCompetition.value = null
|
participants.value = []
|
originalParticipants.value = []
|
selectedParticipants.value = []
|
searchKeyword.value = '' // 重置搜索关键词
|
promotableData.value = {
|
participants: [],
|
selectableCount: 0,
|
totalCount: 0,
|
previousStageName: '',
|
currentStageName: ''
|
}
|
}
|
|
// 获取状态类型
|
const getStatusType = (status) => {
|
switch (status) {
|
case 1: return 'success' // 进行中
|
case 0: return 'warning' // 未发布
|
case 2: return 'danger' // 关闭
|
default: return 'info'
|
}
|
}
|
|
// 获取状态文本
|
const getStatusText = (status) => {
|
switch (status) {
|
case 1: return '进行中'
|
case 0: return '未发布'
|
case 2: return '关闭'
|
default: return '未知'
|
}
|
}
|
|
// 格式化日期
|
const formatDate = (dateString) => {
|
if (!dateString) return '-'
|
const date = new Date(dateString)
|
return date.toLocaleString('zh-CN', {
|
year: 'numeric',
|
month: '2-digit',
|
day: '2-digit',
|
hour: '2-digit',
|
minute: '2-digit'
|
})
|
}
|
|
// 获取表格行样式类名
|
const getRowClassName = ({ rowIndex }) => {
|
// 高亮推荐晋级的人员(前selectableCount名)
|
if (rowIndex < promotableData.value.selectableCount) {
|
return 'recommended-row'
|
}
|
return ''
|
}
|
|
// 搜索处理方法
|
const handleSearch = () => {
|
if (!searchKeyword.value.trim()) {
|
// 如果搜索关键词为空,显示所有原始数据
|
participants.value = originalParticipants.value
|
} else {
|
// 根据关键词过滤参赛者
|
const keyword = searchKeyword.value.toLowerCase().trim()
|
participants.value = originalParticipants.value.filter(participant => {
|
const projectName = participant.projectName?.toLowerCase() || ''
|
const playerName = participant.playerName?.toLowerCase() || ''
|
|
return projectName.includes(keyword) || playerName.includes(keyword)
|
})
|
}
|
|
// 清空已选择的参赛者(因为列表已改变)
|
selectedParticipants.value = []
|
}
|
|
// 组件挂载时加载数据
|
onMounted(() => {
|
loadData()
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.promotion-container {
|
padding: 20px;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.card-title {
|
margin: 0;
|
font-size: 18px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.search-form {
|
margin-bottom: 20px;
|
padding: 20px;
|
background: #f8f9fa;
|
border-radius: 8px;
|
}
|
|
.competition-info {
|
.main-name {
|
font-weight: 600;
|
color: #303133;
|
margin-bottom: 4px;
|
}
|
|
.stage-name {
|
font-size: 12px;
|
color: #409eff;
|
background: #ecf5ff;
|
padding: 2px 8px;
|
border-radius: 12px;
|
display: inline-block;
|
}
|
}
|
|
.count-link {
|
font-weight: 600;
|
color: #409eff;
|
|
&:hover {
|
color: #66b1ff;
|
}
|
}
|
|
.score {
|
font-weight: 600;
|
color: #67c23a;
|
}
|
|
.no-score {
|
color: #909399;
|
font-size: 12px;
|
}
|
|
.no-promotion-text {
|
color: #909399;
|
font-size: 12px;
|
font-style: italic;
|
}
|
|
.pagination-container {
|
margin-top: 20px;
|
display: flex;
|
justify-content: center;
|
}
|
|
.dialog-header {
|
margin-bottom: 20px;
|
padding-bottom: 15px;
|
border-bottom: 1px solid #ebeef5;
|
|
h4 {
|
margin: 0 0 8px 0;
|
color: #303133;
|
font-size: 16px;
|
font-weight: 600;
|
}
|
|
.promotion-info {
|
p {
|
margin: 4px 0;
|
color: #606266;
|
font-size: 14px;
|
|
strong {
|
color: #409eff;
|
font-weight: 600;
|
}
|
}
|
}
|
}
|
|
// 推荐晋级行样式
|
:deep(.recommended-row) {
|
background-color: #f0f9ff !important;
|
|
&:hover {
|
background-color: #e1f5fe !important;
|
}
|
}
|
|
.dialog-footer {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
|
.selected-info {
|
color: #606266;
|
font-size: 14px;
|
}
|
}
|
</style>
|