| | |
| | | @keyup.enter="handleSearch" |
| | | @clear="handleClear" |
| | | /> |
| | | <el-select |
| | | v-model="searchForm.state" |
| | | placeholder="比赛状态" |
| | | style="width: 160px" |
| | | clearable |
| | | @change="handleStateChange" |
| | | > |
| | | <el-option |
| | | v-for="option in stateOptions" |
| | | :key="option.value" |
| | | :label="option.label" |
| | | :value="option.value" |
| | | /> |
| | | </el-select> |
| | | <el-button type="primary" @click="handleSearch"> |
| | | <el-icon><Search /></el-icon> |
| | | 查询 |
| | |
| | | <el-tag :type="getStatusType(row.stateName)">{{ row.stateName }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="120" fixed="right" align="center"> |
| | | <el-table-column label="操作" width="220" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <div class="table-actions"> |
| | | <el-button |
| | | text |
| | | :icon="Edit" |
| | | size="small" |
| | | <el-button |
| | | text |
| | | :icon="User" |
| | | size="small" |
| | | @click="handleViewPlayers(row)" |
| | | class="action-btn players-btn" |
| | | title="查看报名人员" |
| | | /> |
| | | <el-button |
| | | text |
| | | :icon="Download" |
| | | size="small" |
| | | @click="handleExportPlayers(row)" |
| | | class="action-btn export-btn" |
| | | title="导出报名人员" |
| | | /> |
| | | <el-button |
| | | text |
| | | :icon="View" |
| | | size="small" |
| | | @click="handleView(row)" |
| | | class="action-btn view-btn" |
| | | title="详情" |
| | | /> |
| | | <el-button |
| | | text |
| | | :icon="Edit" |
| | | size="small" |
| | | @click="handleEdit(row)" |
| | | class="action-btn edit-btn" |
| | | title="编辑" |
| | | /> |
| | | <el-button |
| | | text |
| | | :icon="Delete" |
| | | size="small" |
| | | <el-button |
| | | text |
| | | :icon="Delete" |
| | | size="small" |
| | | @click="handleDelete(row)" |
| | | class="action-btn delete-btn" |
| | | title="删除" |
| | |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 查看报名人员弹窗 --> |
| | | <el-dialog |
| | | v-model="playerDialogVisible" |
| | | :title="'报名人员 - ' + (currentActivity?.name || '')" |
| | | width="80%" |
| | | @close="handlePlayerDialogClose" |
| | | > |
| | | <!-- 弹窗工具栏 --> |
| | | <div class="dialog-toolbar"> |
| | | <el-button type="primary" :icon="Download" @click="handleExportPlayersFromDialog"> |
| | | 导出Excel |
| | | </el-button> |
| | | </div> |
| | | |
| | | <el-table :data="playerList" v-loading="playerListLoading" style="width: 100%"> |
| | | <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="140" /> |
| | | <el-table-column prop="applyTime" label="申请时间" width="180" /> |
| | | <el-table-column prop="state" label="状态" width="100" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getPlayerStateType(row.state)">{{ getPlayerStateText(row.state) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- 报名人员分页 --> |
| | | <div class="pagination" v-if="playerPagination.total > 0"> |
| | | <el-pagination |
| | | v-model:current-page="playerPagination.page" |
| | | v-model:page-size="playerPagination.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="playerPagination.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | @size-change="handlePlayerSizeChange" |
| | | @current-change="handlePlayerCurrentChange" |
| | | /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="playerDialogVisible = false">关闭</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { reactive, ref, onMounted } from 'vue' |
| | | import { reactive, ref, onMounted, onActivated, watch } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useRouter } from 'vue-router' |
| | | import { getActivities } from '@/api/activity' |
| | | import { Search, Plus, Edit, Delete } from '@element-plus/icons-vue' |
| | | import { getActivities, deleteActivity } from '@/api/activity' |
| | | // @ts-ignore |
| | | import { PlayerApi } from '@/api/player' |
| | | import { Search, Plus, Edit, Delete, View, User, Download } from '@element-plus/icons-vue' |
| | | |
| | | console.log('=== activity-list.vue 组件开始加载 ===') |
| | | |
| | | const loading = ref(false) |
| | | const router = useRouter() |
| | | |
| | | // 搜索表单 |
| | | const searchForm = reactive({ |
| | | name: '' |
| | | name: '', |
| | | state: '' |
| | | }) |
| | | |
| | | const stateOptions = [ |
| | | { label: '未发布', value: 0 }, |
| | | { label: '发布', value: 1 }, |
| | | { label: '关闭', value: 2 } |
| | | ] |
| | | |
| | | // 分页信息 |
| | | const pagination = reactive({ |
| | |
| | | // 表格数据 |
| | | const tableData = ref([]) |
| | | |
| | | // 报名人员弹窗相关 |
| | | const playerDialogVisible = ref(false) |
| | | const playerListLoading = ref(false) |
| | | const playerList = ref([]) |
| | | const playerPagination = reactive({ |
| | | page: 1, |
| | | size: 10, |
| | | total: 0 |
| | | }) |
| | | const currentActivity = ref<any>(null) |
| | | |
| | | // 调试用途:监听表格数据变化 |
| | | watch( |
| | | tableData, |
| | | newVal => { |
| | | console.log('=== tableData 发生变化 ===', newVal) |
| | | console.log('tableData.value.length:', newVal.length) |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | // 获取状态标签类型 |
| | | const getStatusType = (status: string) => { |
| | | const typeMap: Record<string, string> = { |
| | | '进行中': 'success', |
| | | '报名中': 'warning', |
| | | '待开始': 'info', |
| | | '已结束': 'info' |
| | | 已发布: 'success', |
| | | 发布: 'success', |
| | | 进行中: 'success', |
| | | 报名中: 'warning', |
| | | 待开始: 'info', |
| | | 已结束: 'info', |
| | | 关闭: 'danger' |
| | | } |
| | | return typeMap[status] || 'info' |
| | | } |
| | | |
| | | // 获取报名人员状态标签类型 |
| | | const getPlayerStateType = (state: number) => { |
| | | const typeMap: Record<number, string> = { |
| | | 0: 'info', // 未审核 |
| | | 1: 'success', // 审核通过 |
| | | 2: 'danger' // 审核驳回 |
| | | } |
| | | return typeMap[state] || 'info' |
| | | } |
| | | |
| | | // 获取报名人员状态文本 |
| | | const getPlayerStateText = (state: number) => { |
| | | const textMap: Record<number, string> = { |
| | | 0: '未审核', |
| | | 1: '审核通过', |
| | | 2: '审核驳回' |
| | | } |
| | | return textMap[state] || '未知' |
| | | } |
| | | |
| | | // 搜索 |
| | | const handleSearch = () => { |
| | | pagination.page = 1 |
| | | loadData() |
| | | } |
| | | |
| | | const handleStateChange = () => { |
| | | pagination.page = 1 |
| | | loadData() |
| | | } |
| | |
| | | loadData() |
| | | } |
| | | |
| | | // 新增比赛 |
| | | // 新增比赛 |
| | | const handleAdd = () => { |
| | | router.push('/activity/new') |
| | | } |
| | | |
| | | // 编辑比赛 |
| | | // 编辑比赛 |
| | | const handleEdit = (row: any) => { |
| | | router.push(`/activity/edit/${row.id}`) |
| | | } |
| | | |
| | | // 删除比赛 |
| | | // 查看详情 |
| | | const handleView = (row: any) => { |
| | | router.push(`/activity/${row.id}`) |
| | | } |
| | | |
| | | // 查看报名人员弹窗关闭处理 |
| | | const handlePlayerDialogClose = () => { |
| | | // 清空当前活动 |
| | | currentActivity.value = null |
| | | // 清空报名人员列表 |
| | | playerList.value = [] |
| | | // 重置分页 |
| | | playerPagination.page = 1 |
| | | playerPagination.total = 0 |
| | | } |
| | | |
| | | // 查看报名人员 |
| | | const handleViewPlayers = async (row: any) => { |
| | | // 设置当前活动 |
| | | currentActivity.value = row |
| | | // 重置分页 |
| | | playerPagination.page = 1 |
| | | playerPagination.size = 10 |
| | | // 显示弹窗 |
| | | playerDialogVisible.value = true |
| | | // 加载数据 |
| | | await loadPlayerList() |
| | | } |
| | | |
| | | // 加载报名人员列表 |
| | | const loadPlayerList = async () => { |
| | | if (!currentActivity.value) return |
| | | |
| | | playerListLoading.value = true |
| | | try { |
| | | // @ts-ignore 忽略TypeScript检查,因为函数实际支持更多参数 |
| | | const data = await PlayerApi.getApplications('', currentActivity.value.id, null, playerPagination.page - 1, playerPagination.size) |
| | | |
| | | playerList.value = data.content || [] |
| | | playerPagination.total = data.totalElements || 0 |
| | | } catch (e: any) { |
| | | console.error('加载报名人员失败:', e) |
| | | ElMessage.error(e?.message || '加载报名人员失败') |
| | | } finally { |
| | | playerListLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // 报名人员分页处理 |
| | | const handlePlayerSizeChange = (size: number) => { |
| | | playerPagination.size = size |
| | | loadPlayerList() |
| | | } |
| | | |
| | | const handlePlayerCurrentChange = (page: number) => { |
| | | playerPagination.page = page |
| | | loadPlayerList() |
| | | } |
| | | |
| | | // 从弹窗导出报名人员Excel |
| | | const handleExportPlayersFromDialog = async () => { |
| | | if (!currentActivity.value) { |
| | | ElMessage.error('当前没有选中的比赛') |
| | | return |
| | | } |
| | | |
| | | try { |
| | | // 构造导出URL,使用完整的API路径 |
| | | const exportUrl = `/api/player/export/applications?activityId=${currentActivity.value.id}` |
| | | |
| | | // 创建一个隐藏的a标签来触发下载 |
| | | const link = document.createElement('a') |
| | | link.href = exportUrl |
| | | link.download = `报名人员_${currentActivity.value.name}_${new Date().getTime()}.xlsx` |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | |
| | | ElMessage.success('导出成功') |
| | | } catch (error) { |
| | | console.error('导出失败:', error) |
| | | ElMessage.error('导出失败: ' + (error as Error).message) |
| | | } |
| | | } |
| | | |
| | | // 导出报名人员(原列表中的导出功能保持不变) |
| | | const handleExportPlayers = async (row: any) => { |
| | | try { |
| | | // 构造导出URL,使用完整的API路径 |
| | | const exportUrl = `/api/player/export/applications?activityId=${row.id}` |
| | | |
| | | // 创建一个隐藏的a标签来触发下载 |
| | | const link = document.createElement('a') |
| | | link.href = exportUrl |
| | | link.download = `报名人员_${row.name}_${new Date().getTime()}.xlsx` |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | |
| | | ElMessage.success('导出成功') |
| | | } catch (error) { |
| | | console.error('导出失败:', error) |
| | | ElMessage.error('导出失败: ' + (error as Error).message) |
| | | } |
| | | } |
| | | |
| | | // 删除比赛 |
| | | const handleDelete = async (row: any) => { |
| | | try { |
| | | await ElMessageBox.confirm(`确定要删除比赛"${row.name}"吗?`, '提示', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }) |
| | | |
| | | await ElMessageBox.confirm( |
| | | `确定要删除比赛 “${row.name}” 吗?`, |
| | | '提示', |
| | | { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | } |
| | | ) |
| | | |
| | | await deleteActivity(row.id) |
| | | ElMessage.success('删除成功') |
| | | loadData() |
| | | } catch { |
| | | // 用户取消 |
| | | // 用户取消操作 |
| | | } |
| | | } |
| | | |
| | |
| | | const loadData = async () => { |
| | | loading.value = true |
| | | try { |
| | | const data = await getActivities(pagination.page - 1, pagination.size, searchForm.name || '') |
| | | tableData.value = (data && data.content) ? data.content : [] |
| | | pagination.total = (data && data.totalElements) ? data.totalElements : 0 |
| | | } catch (e) { |
| | | ElMessage.error((e && e.message) ? e.message : '加载比赛列表失败') |
| | | const keyword = (searchForm.name || '').trim() |
| | | // 使用条件调用避免TypeScript错误 |
| | | let data; |
| | | if (searchForm.state !== '') { |
| | | // @ts-ignore 忽略TypeScript检查,因为函数实际支持4个参数 |
| | | data = await getActivities(pagination.page - 1, pagination.size, keyword, Number(searchForm.state)) |
| | | } else { |
| | | data = await getActivities(pagination.page - 1, pagination.size, keyword) |
| | | } |
| | | |
| | | // 数据映射:将 API 返回字段转换为表格需要的字段 |
| | | const mappedData = (data?.content || []).map((item: any) => ({ |
| | | ...item, |
| | | playerCount: item.playerCount || 0, |
| | | stateName: item.stateName || '' |
| | | })) |
| | | |
| | | tableData.value = mappedData |
| | | pagination.total = data?.totalElements || 0 |
| | | } catch (e: any) { |
| | | console.error('加载数据失败:', e) |
| | | ElMessage.error(e?.message || '加载比赛列表失败') |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadData() |
| | | }) |
| | | |
| | | // 页面激活时重新加载数据(解决从新增/编辑返回后列表不同步的问题) |
| | | onActivated(() => { |
| | | loadData() |
| | | }) |
| | | </script> |
| | |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | /* 搜索框样式 */ |
| | | /* 搜索按钮 */ |
| | | .search-icon { |
| | | color: #999; |
| | | } |
| | |
| | | background: rgba(245, 108, 108, 0.1) !important; |
| | | } |
| | | |
| | | .view-btn { |
| | | color: #67C23A; |
| | | } |
| | | |
| | | .view-btn:hover { |
| | | color: #5daf34; |
| | | transform: scale(1.2); |
| | | background: rgba(103, 194, 58, 0.1) !important; |
| | | } |
| | | |
| | | .players-btn { |
| | | color: #E6A23C; |
| | | } |
| | | |
| | | .players-btn:hover { |
| | | color: #cf9236; |
| | | transform: scale(1.2); |
| | | background: rgba(230, 162, 60, 0.1) !important; |
| | | } |
| | | |
| | | .export-btn { |
| | | color: #909399; |
| | | } |
| | | |
| | | .export-btn:hover { |
| | | color: #a6a9ad; |
| | | transform: scale(1.2); |
| | | background: rgba(144, 147, 153, 0.1) !important; |
| | | } |
| | | |
| | | .pagination { |
| | | margin-top: 20px; |
| | | display: flex; |
| | |
| | | align-items: stretch; |
| | | gap: 16px; |
| | | } |
| | | |
| | | |
| | | .toolbar { |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | |
| | | .toolbar .el-input { |
| | | width: 100% !important; |
| | | max-width: 280px; |