1626 lines
51 KiB
Vue
1626 lines
51 KiB
Vue
<template>
|
||
<div class="martial-schedule-container">
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
|
||
<h2 class="page-title">编排</h2>
|
||
</div>
|
||
<div class="header-right">
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
:loading="autoArrangeLoading"
|
||
@click="handleAutoArrange"
|
||
>
|
||
{{ isScheduleCompleted ? '重新编排' : '自动编排' }}
|
||
</el-button>
|
||
<el-button size="small" type="danger" @click="showExceptionDialog">
|
||
异常组 <el-badge :value="exceptionList.length" :hidden="exceptionList.length === 0" />
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tabs-section">
|
||
<div class="tab-buttons">
|
||
<el-button
|
||
size="small"
|
||
:type="activeTab === 'competition' ? 'primary' : ''"
|
||
@click="activeTab = 'competition'"
|
||
>
|
||
竞赛分组
|
||
</el-button>
|
||
<el-button
|
||
size="small"
|
||
:type="activeTab === 'venue' ? 'primary' : ''"
|
||
@click="activeTab = 'venue'"
|
||
>
|
||
场地
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 竞赛分组 Tab -->
|
||
<div v-show="activeTab === 'competition'" class="tab-content">
|
||
|
||
<!-- 场地列表 -->
|
||
<div class="venue-list">
|
||
<div class="venue-buttons">
|
||
<el-button
|
||
v-for="venue in venues"
|
||
:key="venue.id"
|
||
size="small"
|
||
:type="selectedVenueId === venue.id ? 'primary' : ''"
|
||
@click="selectedVenueId = venue.id"
|
||
>
|
||
{{ venue.venueName }}
|
||
</el-button>
|
||
<div v-if="venues.length === 0" class="no-venue-hint">
|
||
暂无场地信息,请先在赛事管理中配置场地
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="time-selector">
|
||
<el-button
|
||
v-for="(time, index) in timeSlots"
|
||
:key="index"
|
||
size="small"
|
||
:type="selectedTime === index ? 'primary' : ''"
|
||
@click="selectedTime = index"
|
||
>
|
||
{{ time }}
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 项目卡片列表 -->
|
||
<div v-for="(group, groupIndex) in filteredCompetitionGroups" :key="group.id" class="project-card">
|
||
<!-- 项目头部 - 可点击展开 -->
|
||
<div class="project-header" :class="{ 'project-header-collapsed': !isProjectExpanded(group.id) }" @click="toggleProjectExpand(group.id)">
|
||
<div class="project-info">
|
||
<!-- 展开图标 -->
|
||
<span class="project-expand-icon">
|
||
<el-icon v-if="isProjectExpanded(group.id)"><ArrowDown /></el-icon>
|
||
<el-icon v-else><ArrowRight /></el-icon>
|
||
</span>
|
||
<span class="project-index">{{ groupIndex + 1 }}、</span>
|
||
<span class="project-title">{{ group.title }}</span>
|
||
<span class="project-meta">{{ group.type }}</span>
|
||
<span class="project-meta">{{ getTeamCount(group) }}队</span>
|
||
<span class="project-meta">{{ group.items?.length || 0 }}组</span>
|
||
<span class="project-meta">{{ group.code }}</span>
|
||
</div>
|
||
<div class="project-actions" @click.stop>
|
||
<el-popover
|
||
placement="bottom"
|
||
:width="200"
|
||
trigger="click"
|
||
:disabled="isScheduleCompleted"
|
||
>
|
||
<template #reference>
|
||
<el-button size="small" type="warning" :disabled="isScheduleCompleted">
|
||
移动
|
||
</el-button>
|
||
</template>
|
||
<div class="move-popover">
|
||
<div class="move-label">移动到</div>
|
||
<el-select v-model="moveTargetVenueId" placeholder="选择场地" size="small" style="width: 100%; margin-bottom: 10px;">
|
||
<el-option
|
||
v-for="venue in venues"
|
||
:key="venue.id"
|
||
:label="venue.venueName"
|
||
:value="venue.id"
|
||
></el-option>
|
||
</el-select>
|
||
<el-button size="small" type="primary" @click="quickMoveGroup(group)">移动</el-button>
|
||
</div>
|
||
</el-popover>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 队伍列表 - 可折叠 -->
|
||
<div v-if="isProjectExpanded(group.id)" class="team-list">
|
||
<div
|
||
v-for="(team, teamIndex) in groupItemsByTeam(group.items)"
|
||
:key="team.id"
|
||
class="team-row"
|
||
:class="{ 'team-row-expanded': isTeamExpanded(group.id, team.id) }"
|
||
>
|
||
<!-- 队伍主行 - 可点击展开 -->
|
||
<div class="team-main" @click="toggleTeamExpand(group.id, team.id)">
|
||
<!-- 展开图标 -->
|
||
<span class="expand-icon">
|
||
<el-icon v-if="isTeamExpanded(group.id, team.id)"><ArrowDown /></el-icon>
|
||
<el-icon v-else><ArrowRight /></el-icon>
|
||
</span>
|
||
|
||
<!-- 队伍信息 -->
|
||
<div class="team-info">
|
||
<span class="team-index">{{ teamIndex + 1 }}、</span>
|
||
<span class="team-name">{{ team.schoolUnit }}</span>
|
||
</div>
|
||
|
||
<!-- 选手列表 -->
|
||
<div class="player-list">
|
||
<span
|
||
v-for="(player, playerIndex) in team.players"
|
||
:key="player.id"
|
||
class="player-tag"
|
||
:class="{ 'player-exception': player.status === '异常' }"
|
||
>
|
||
{{ player.playerName }}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="team-actions" @click.stop>
|
||
<el-button
|
||
link
|
||
size="small"
|
||
@click="handleTeamMoveUp(group, teamIndex)"
|
||
:disabled="teamIndex === 0 || isScheduleCompleted"
|
||
title="上移"
|
||
class="move-btn"
|
||
>
|
||
<img src="/img/图标 3@3x.png" class="move-icon" alt="上移" />
|
||
</el-button>
|
||
<el-button
|
||
link
|
||
size="small"
|
||
@click="handleTeamMoveDown(group, teamIndex)"
|
||
:disabled="teamIndex === groupItemsByTeam(group.items).length - 1 || isScheduleCompleted"
|
||
title="下移"
|
||
class="move-btn"
|
||
>
|
||
<img src="/img/图标 4@3x.png" class="move-icon" alt="下移" />
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 展开内容 - 选手详情 -->
|
||
<div v-if="isTeamExpanded(group.id, team.id)" class="team-expand-content">
|
||
<div
|
||
v-for="(player, playerIndex) in team.players"
|
||
:key="player.id"
|
||
class="player-detail-row"
|
||
>
|
||
<span class="player-detail-index">{{ playerIndex + 1 }}</span>
|
||
<span class="player-detail-name">{{ player.playerName }}</span>
|
||
<span class="player-detail-status">
|
||
<el-tag
|
||
:type="player.status === '已签到' ? 'success' : player.status === '异常' ? 'danger' : 'info'"
|
||
size="small"
|
||
>
|
||
{{ player.status || '未签到' }}
|
||
</el-tag>
|
||
</span>
|
||
<span class="player-detail-actions">
|
||
<el-button
|
||
v-if="(player.status || '未签到') === '未签到'"
|
||
link
|
||
size="small"
|
||
@click="markPlayerAsException(group, team, playerIndex)"
|
||
:disabled="isScheduleCompleted"
|
||
style="color: #f56c6c;"
|
||
>
|
||
标记异常
|
||
</el-button>
|
||
<el-button
|
||
v-if="player.status === '异常'"
|
||
link
|
||
size="small"
|
||
@click="removePlayerException(group, team, playerIndex)"
|
||
:disabled="isScheduleCompleted"
|
||
style="color: #67c23a;"
|
||
>
|
||
取消异常
|
||
</el-button>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 场地 Tab -->
|
||
<div v-show="activeTab === 'venue'" class="tab-content">
|
||
<!-- 场地列表 -->
|
||
<div class="venue-list">
|
||
<div class="venue-buttons">
|
||
<el-button
|
||
v-for="venue in venues"
|
||
:key="'venue-tab-' + venue.id"
|
||
size="small"
|
||
:type="selectedVenueIdForVenueTab === venue.id ? 'primary' : ''"
|
||
@click="selectedVenueIdForVenueTab = venue.id"
|
||
>
|
||
{{ venue.venueName }}
|
||
</el-button>
|
||
<div v-if="venues.length === 0" class="no-venue-hint">
|
||
暂无场地信息,请先在赛事管理中配置场地
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="time-selector">
|
||
<el-button
|
||
v-for="(time, index) in timeSlots"
|
||
:key="index"
|
||
size="small"
|
||
:type="selectedTime === index ? 'primary' : ''"
|
||
@click="selectedTime = index"
|
||
>
|
||
{{ time }}
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-table :data="venueData" border stripe size="small">
|
||
<el-table-column label="序号" type="index" width="60" align="center"></el-table-column>
|
||
<el-table-column prop="project" label="项目" min-width="180">
|
||
<template #default="scope">
|
||
<span>{{ scope.row.project }}</span>
|
||
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="participant" label="参人/单位" width="120"></el-table-column>
|
||
<el-table-column prop="team" label="队伍" width="80" align="center"></el-table-column>
|
||
<el-table-column prop="number" label="编号" width="80" align="center"></el-table-column>
|
||
<el-table-column prop="duration" label="合计时间" width="100" align="center"></el-table-column>
|
||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||
<template #default="scope">
|
||
<span v-if="scope.row.status" class="status-active">{{ scope.row.status }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer-actions">
|
||
<el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button>
|
||
<el-button size="small" @click="handleExport" v-if="isScheduleCompleted">导出</el-button>
|
||
<el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button>
|
||
</div>
|
||
|
||
<!-- 确认对话框 -->
|
||
<el-dialog
|
||
title="确定完成编排"
|
||
v-model="confirmDialogVisible"
|
||
width="450px"
|
||
center
|
||
>
|
||
<p style="text-align: center; padding: 20px; color: #606266;">
|
||
完成编排后,不可再次调整。确定完成编排吗?
|
||
</p>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="confirmDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="confirmComplete">确定</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 移动分组对话框 -->
|
||
<el-dialog
|
||
title="移动竞赛分组"
|
||
v-model="moveDialogVisible"
|
||
width="500px"
|
||
center
|
||
>
|
||
<el-form label-width="100px">
|
||
<el-form-item label="目标场地">
|
||
<el-select v-model="moveTargetVenueId" placeholder="请选择场地" style="width: 100%;">
|
||
<el-option
|
||
v-for="venue in venues"
|
||
:key="venue.id"
|
||
:label="venue.venueName"
|
||
:value="venue.id"
|
||
></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="目标时间段">
|
||
<el-select v-model="moveTargetTimeSlot" placeholder="请选择时间段" style="width: 100%;">
|
||
<el-option
|
||
v-for="(time, index) in timeSlots"
|
||
:key="index"
|
||
:label="time"
|
||
:value="index"
|
||
></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="moveDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="confirmMoveGroup">确定</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 异常组对话框 -->
|
||
<el-dialog
|
||
title="异常组参赛人员"
|
||
v-model="exceptionDialogVisible"
|
||
width="700px"
|
||
center
|
||
>
|
||
<el-table :data="exceptionList" border stripe size="small" max-height="400">
|
||
<el-table-column label="序号" type="index" width="60" align="center"></el-table-column>
|
||
<el-table-column prop="groupTitle" label="分组" min-width="180"></el-table-column>
|
||
<el-table-column prop="schoolUnit" label="学校/单位" min-width="150"></el-table-column>
|
||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||
<template #default="scope">
|
||
<el-tag type="danger" size="small">{{ scope.row.status }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="100" align="center">
|
||
<template #default="scope">
|
||
<el-button
|
||
link
|
||
size="small"
|
||
@click="removeFromException(scope.$index)"
|
||
style="color: #409eff;"
|
||
>
|
||
移除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div v-if="exceptionList.length === 0" style="text-align: center; padding: 40px; color: #909399;">
|
||
暂无异常参赛人员
|
||
</div>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="exceptionDialogVisible = false">关闭</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
|
||
import { getVenuesByCompetition } from '@/api/martial/venue'
|
||
import { getCompetitionDetail } from '@/api/martial/competition'
|
||
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule } from '@/api/martial/activitySchedule'
|
||
import { updateCheckInStatus } from '@/api/martial/schedulePlan'
|
||
|
||
export default {
|
||
name: 'MartialScheduleList',
|
||
components: {
|
||
ArrowDown,
|
||
ArrowRight
|
||
},
|
||
data() {
|
||
return {
|
||
competitionId: null,
|
||
orderId: null,
|
||
activeTab: 'competition',
|
||
selectedTime: 0,
|
||
selectedVenueId: null, // 选中的场地ID(竞赛分组Tab)
|
||
selectedVenueIdForVenueTab: null, // 选中的场地ID(场地Tab)
|
||
confirmDialogVisible: false,
|
||
isScheduleCompleted: false, // 是否已完成编排
|
||
loading: false,
|
||
autoArrangeLoading: false, // 自动编排加载状态
|
||
venues: [], // 场地列表(从后端加载)
|
||
competitionInfo: {
|
||
competitionName: '',
|
||
competitionStartTime: '',
|
||
competitionEndTime: ''
|
||
},
|
||
timeSlots: [], // 时间段列表(根据比赛时间动态生成)
|
||
competitionGroups: [], // 竞赛分组(从后端加载)
|
||
|
||
// 移动分组相关
|
||
moveDialogVisible: false,
|
||
moveTargetVenueId: null,
|
||
moveTargetTimeSlot: null,
|
||
moveGroupIndex: null,
|
||
|
||
// 异常组相关
|
||
exceptionDialogVisible: false,
|
||
exceptionList: [], // 异常参赛人员列表
|
||
expandedTeams: {}, // 展开的队伍 { 'groupId-teamId': true }
|
||
expandedProjects: {} // 展开的项目 { 'groupId': true },默认收起
|
||
}
|
||
},
|
||
computed: {
|
||
// 根据选中的场地和时间段过滤竞赛分组
|
||
filteredCompetitionGroups() {
|
||
console.log('=== 过滤调试信息 ===')
|
||
console.log('selectedVenueId:', this.selectedVenueId, 'type:', typeof this.selectedVenueId)
|
||
console.log('selectedTime:', this.selectedTime, 'type:', typeof this.selectedTime)
|
||
console.log('competitionGroups总数:', this.competitionGroups.length)
|
||
|
||
if (!this.selectedVenueId || this.selectedTime === null) {
|
||
console.log('场地或时间未选中,返回空数组')
|
||
return []
|
||
}
|
||
|
||
const filtered = this.competitionGroups.filter(group => {
|
||
console.log(`分组"${group.title}": venueId=${group.venueId}(${typeof group.venueId}), timeSlotIndex=${group.timeSlotIndex}(${typeof group.timeSlotIndex})`)
|
||
const venueMatch = group.venueId === this.selectedVenueId || Number(group.venueId) === Number(this.selectedVenueId)
|
||
const timeMatch = group.timeSlotIndex === this.selectedTime
|
||
console.log(` 场地匹配:${venueMatch}, 时间匹配:${timeMatch}`)
|
||
return venueMatch && timeMatch
|
||
})
|
||
|
||
console.log('过滤后的分组数量:', filtered.length)
|
||
return filtered
|
||
},
|
||
|
||
// 场地标签页的数据 - 根据选中的场地和时间段动态生成
|
||
venueData() {
|
||
if (this.selectedTime === null) {
|
||
return []
|
||
}
|
||
|
||
// 获取选中时间段的所有分组
|
||
let groupsInTimeSlot = this.competitionGroups.filter(
|
||
group => group.timeSlotIndex === this.selectedTime
|
||
)
|
||
|
||
// 如果选中了场地,进一步过滤
|
||
if (this.selectedVenueIdForVenueTab) {
|
||
groupsInTimeSlot = groupsInTimeSlot.filter(group => {
|
||
return group.venueId === this.selectedVenueIdForVenueTab ||
|
||
Number(group.venueId) === Number(this.selectedVenueIdForVenueTab)
|
||
})
|
||
}
|
||
|
||
// 将分组转换为场地视图的数据格式
|
||
return groupsInTimeSlot.map(group => ({
|
||
project: group.title,
|
||
participant: group.type,
|
||
team: 1, // 队伍数量,可以从分组中计算
|
||
number: group.items?.length || 0, // 参赛人数
|
||
duration: 0, // 合计时间,如果后端有数据可以显示
|
||
status: group.venueName, // 显示场地名称
|
||
venueName: group.venueName,
|
||
hint: '' // 可以根据需要添加提示信息
|
||
}))
|
||
}
|
||
},
|
||
mounted() {
|
||
// 从路由获取赛事ID和订单ID
|
||
this.competitionId = this.$route.query.competitionId
|
||
this.orderId = this.$route.query.orderId
|
||
|
||
if (this.competitionId) {
|
||
this.loadCompetitionInfo()
|
||
this.loadVenues()
|
||
this.loadScheduleData()
|
||
} else {
|
||
this.$message.warning('未获取到赛事ID')
|
||
}
|
||
},
|
||
methods: {
|
||
// 检查项目是否展开
|
||
isProjectExpanded(groupId) {
|
||
return this.expandedProjects[groupId] === true
|
||
},
|
||
|
||
// 切换项目展开状态
|
||
toggleProjectExpand(groupId) {
|
||
if (this.expandedProjects[groupId]) {
|
||
delete this.expandedProjects[groupId]
|
||
} else {
|
||
this.expandedProjects[groupId] = true
|
||
}
|
||
// 触发响应式更新
|
||
this.expandedProjects = { ...this.expandedProjects }
|
||
},
|
||
|
||
// 检查队伍是否展开
|
||
isTeamExpanded(groupId, teamId) {
|
||
const key = groupId + '-' + teamId
|
||
return this.expandedTeams[key] === true
|
||
},
|
||
|
||
// 切换队伍展开状态
|
||
toggleTeamExpand(groupId, teamId) {
|
||
const key = groupId + '-' + teamId
|
||
if (this.expandedTeams[key]) {
|
||
delete this.expandedTeams[key]
|
||
} else {
|
||
this.expandedTeams[key] = true
|
||
}
|
||
// 触发响应式更新
|
||
this.expandedTeams = { ...this.expandedTeams }
|
||
},
|
||
|
||
// 标记选手为异常
|
||
markPlayerAsException(group, team, playerIndex) {
|
||
if (this.isScheduleCompleted) {
|
||
this.$message.warning('编排已完成,无法标记异常')
|
||
return
|
||
}
|
||
|
||
const player = team.players[playerIndex]
|
||
if (player) {
|
||
player.status = '异常'
|
||
// 添加到异常列表
|
||
this.exceptionList.push({
|
||
groupId: group.id,
|
||
groupTitle: group.title,
|
||
teamId: team.id,
|
||
schoolUnit: team.schoolUnit,
|
||
playerId: player.id,
|
||
playerName: player.playerName,
|
||
status: '异常'
|
||
})
|
||
|
||
// 调用后端API保存状态
|
||
updateCheckInStatus(player.id, '异常').then(() => {
|
||
this.$message.success(`已将 ${player.playerName} 标记为异常`)
|
||
}).catch(err => {
|
||
console.error('保存异常状态失败:', err)
|
||
})
|
||
}
|
||
},
|
||
|
||
// 取消选手异常状态
|
||
removePlayerException(group, team, playerIndex) {
|
||
if (this.isScheduleCompleted) {
|
||
this.$message.warning('编排已完成,无法取消异常')
|
||
return
|
||
}
|
||
|
||
const player = team.players[playerIndex]
|
||
if (player) {
|
||
player.status = '未签到'
|
||
// 从异常列表中移除
|
||
const idx = this.exceptionList.findIndex(
|
||
e => e.playerId === player.id && e.groupId === group.id
|
||
)
|
||
if (idx !== -1) {
|
||
this.exceptionList.splice(idx, 1)
|
||
}
|
||
|
||
// 调用后端API恢复状态
|
||
updateCheckInStatus(player.id, '未签到').then(() => {
|
||
this.$message.success(`已将 ${player.playerName} 取消异常标记`)
|
||
}).catch(err => {
|
||
console.error('恢复状态失败:', err)
|
||
})
|
||
}
|
||
},
|
||
|
||
// 将选手按学校/单位分组为队伍
|
||
groupItemsByTeam(items) {
|
||
if (!items || items.length === 0) return []
|
||
const teamMap = new Map()
|
||
items.forEach(item => {
|
||
const key = item.schoolUnit
|
||
if (!teamMap.has(key)) {
|
||
teamMap.set(key, {
|
||
id: `team_${item.id}`,
|
||
schoolUnit: key,
|
||
players: [],
|
||
playerIds: []
|
||
})
|
||
}
|
||
teamMap.get(key).players.push(item)
|
||
teamMap.get(key).playerIds.push(item.id)
|
||
})
|
||
return Array.from(teamMap.values())
|
||
},
|
||
|
||
// 获取队伍数量
|
||
getTeamCount(group) {
|
||
if (!group.items || group.items.length === 0) return 0
|
||
const teams = this.groupItemsByTeam(group.items)
|
||
return teams.length
|
||
},
|
||
|
||
// 快速移动分组(从popover中调用)
|
||
async quickMoveGroup(group) {
|
||
if (!this.moveTargetVenueId) {
|
||
this.$message.warning('请选择目标场地')
|
||
return
|
||
}
|
||
|
||
const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId)
|
||
|
||
try {
|
||
// 调用后端API移动分组
|
||
const res = await moveScheduleGroup({
|
||
groupId: group.id,
|
||
targetVenueId: this.moveTargetVenueId,
|
||
targetTimeSlotIndex: this.selectedTime // 保持当前时间段
|
||
})
|
||
|
||
if (res.data.success) {
|
||
// 更新前端数据
|
||
group.venueId = this.moveTargetVenueId
|
||
group.venueName = targetVenue ? targetVenue.venueName : ''
|
||
|
||
this.$message.success(`已移动到 ${group.venueName}`)
|
||
} else {
|
||
this.$message.error(res.data.msg || '移动分组失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('移动分组失败:', error)
|
||
this.$message.error('移动分组失败,请稍后重试')
|
||
}
|
||
},
|
||
|
||
// 获取队伍状态
|
||
getTeamStatus(team) {
|
||
if (!team || !team.players) return '未签到'
|
||
const statuses = team.players.map(p => p.status || '未签到')
|
||
if (statuses.every(s => s === '已签到')) return '已签到'
|
||
if (statuses.every(s => s === '未签到')) return '未签到'
|
||
if (statuses.some(s => s === '异常')) return '部分异常'
|
||
return '部分签到'
|
||
},
|
||
|
||
// 获取队伍状态类型
|
||
getTeamStatusType(team) {
|
||
const status = this.getTeamStatus(team)
|
||
switch(status) {
|
||
case '已签到': return 'success'
|
||
case '部分异常': return 'danger'
|
||
case '部分签到': return 'warning'
|
||
default: return 'info'
|
||
}
|
||
},
|
||
|
||
// 队伍上移
|
||
handleTeamMoveUp(group, teamIndex) {
|
||
const teams = this.groupItemsByTeam(group.items)
|
||
if (teamIndex === 0 || this.isScheduleCompleted) return
|
||
|
||
const currentTeam = teams[teamIndex]
|
||
const prevTeam = teams[teamIndex - 1]
|
||
|
||
// 找到两个队伍在原数组中的位置范围
|
||
const currentPlayerIds = currentTeam.playerIds
|
||
const prevPlayerIds = prevTeam.playerIds
|
||
|
||
// 重新排序:将当前队伍的所有选手移到前一个队伍之前
|
||
const newItems = []
|
||
let addedCurrent = false
|
||
|
||
for (const item of group.items) {
|
||
if (prevPlayerIds.includes(item.id) && !addedCurrent) {
|
||
// 先添加当前队伍的所有选手
|
||
for (const cItem of group.items) {
|
||
if (currentPlayerIds.includes(cItem.id)) {
|
||
newItems.push(cItem)
|
||
}
|
||
}
|
||
addedCurrent = true
|
||
}
|
||
if (!currentPlayerIds.includes(item.id)) {
|
||
newItems.push(item)
|
||
}
|
||
}
|
||
|
||
group.items = newItems
|
||
this.$message.success('上移成功')
|
||
},
|
||
|
||
// 队伍下移
|
||
handleTeamMoveDown(group, teamIndex) {
|
||
const teams = this.groupItemsByTeam(group.items)
|
||
if (teamIndex === teams.length - 1 || this.isScheduleCompleted) return
|
||
|
||
const currentTeam = teams[teamIndex]
|
||
const nextTeam = teams[teamIndex + 1]
|
||
|
||
const currentPlayerIds = currentTeam.playerIds
|
||
const nextPlayerIds = nextTeam.playerIds
|
||
|
||
// 重新排序:将当前队伍的所有选手移到下一个队伍之后
|
||
const newItems = []
|
||
let addedCurrent = false
|
||
|
||
for (const item of group.items) {
|
||
if (!currentPlayerIds.includes(item.id)) {
|
||
newItems.push(item)
|
||
}
|
||
if (nextPlayerIds.includes(item.id)) {
|
||
// 检查是否是下一个队伍的最后一个选手
|
||
const isLastOfNext = item.id === nextPlayerIds[nextPlayerIds.length - 1]
|
||
if (isLastOfNext && !addedCurrent) {
|
||
// 在下一个队伍最后一个选手之后添加当前队伍
|
||
for (const cItem of group.items) {
|
||
if (currentPlayerIds.includes(cItem.id)) {
|
||
newItems.push(cItem)
|
||
}
|
||
}
|
||
addedCurrent = true
|
||
}
|
||
}
|
||
}
|
||
|
||
group.items = newItems
|
||
this.$message.success('下移成功')
|
||
},
|
||
|
||
goBack() {
|
||
this.$router.go(-1)
|
||
},
|
||
|
||
// 加载赛事信息
|
||
async loadCompetitionInfo() {
|
||
try {
|
||
this.loading = true
|
||
const res = await getCompetitionDetail(this.competitionId)
|
||
const data = res.data?.data
|
||
|
||
if (data) {
|
||
this.competitionInfo.competitionName = data.competitionName || data.competition_name || ''
|
||
this.competitionInfo.competitionStartTime = data.competitionStartTime || data.competition_start_time
|
||
this.competitionInfo.competitionEndTime = data.competitionEndTime || data.competition_end_time
|
||
|
||
// 生成时间段
|
||
this.generateTimeSlots()
|
||
}
|
||
} catch (err) {
|
||
console.error('加载赛事信息失败', err)
|
||
this.$message.error('加载赛事信息失败')
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
|
||
// 根据开始和结束时间生成时间段列表
|
||
generateTimeSlots() {
|
||
const startTime = this.competitionInfo.competitionStartTime
|
||
const endTime = this.competitionInfo.competitionEndTime
|
||
|
||
if (!startTime || !endTime) {
|
||
this.$message.warning('赛事时间信息不完整,使用默认时间段')
|
||
this.timeSlots = [
|
||
'2025年11月6日 上午8:30',
|
||
'2025年11月6日 下午13:30'
|
||
]
|
||
return
|
||
}
|
||
|
||
const slots = []
|
||
const start = new Date(startTime)
|
||
const end = new Date(endTime)
|
||
|
||
// 遍历每一天
|
||
let currentDate = new Date(start)
|
||
let dayIndex = 1
|
||
|
||
while (currentDate <= end) {
|
||
const year = currentDate.getFullYear()
|
||
const month = currentDate.getMonth() + 1
|
||
const day = currentDate.getDate()
|
||
const dateStr = `${year}年${month}月${day}日`
|
||
|
||
// 添加上午时段 8:30
|
||
slots.push(`${dateStr} 上午8:30`)
|
||
|
||
// 添加下午时段 13:30
|
||
slots.push(`${dateStr} 下午13:30`)
|
||
|
||
// 下一天
|
||
currentDate.setDate(currentDate.getDate() + 1)
|
||
dayIndex++
|
||
}
|
||
|
||
this.timeSlots = slots
|
||
console.log('生成的时间段:', this.timeSlots)
|
||
},
|
||
|
||
// 加载场地列表
|
||
async loadVenues() {
|
||
try {
|
||
this.loading = true
|
||
const res = await getVenuesByCompetition(this.competitionId)
|
||
const venuesData = res.data?.data?.records || res.data?.data || []
|
||
|
||
if (venuesData.length === 0) {
|
||
this.$message.warning('该赛事暂无场地信息')
|
||
this.venues = []
|
||
} else {
|
||
this.venues = venuesData.map(v => ({
|
||
id: v.id,
|
||
venueName: v.venueName || v.venue_name,
|
||
capacity: v.capacity || 0,
|
||
location: v.location,
|
||
description: v.description
|
||
}))
|
||
// 默认选中第一个场地
|
||
if (this.venues.length > 0) {
|
||
this.selectedVenueId = this.venues[0].id
|
||
this.selectedVenueIdForVenueTab = this.venues[0].id
|
||
}
|
||
console.log('加载的场地数据:', this.venues)
|
||
}
|
||
} catch (err) {
|
||
console.error('加载场地失败', err)
|
||
this.$message.error('加载场地失败')
|
||
this.venues = []
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
|
||
// 加载编排数据
|
||
async loadScheduleData() {
|
||
try {
|
||
this.loading = true
|
||
const res = await getScheduleResult(this.competitionId)
|
||
const data = res.data?.data
|
||
|
||
console.log('=== API返回的编排数据 ===')
|
||
console.log('完整响应:', res.data)
|
||
console.log('data:', data)
|
||
|
||
if (data) {
|
||
// 判断是否有草稿或已完成的编排
|
||
this.isScheduleCompleted = data.isCompleted || false
|
||
|
||
// 加载竞赛分组数据
|
||
if (data.competitionGroups && data.competitionGroups.length > 0) {
|
||
this.competitionGroups = data.competitionGroups.map(group => ({
|
||
id: group.id,
|
||
title: group.title,
|
||
type: group.type,
|
||
count: group.count,
|
||
code: group.code,
|
||
venueId: group.venueId,
|
||
venueName: group.venueName,
|
||
timeSlot: group.timeSlot,
|
||
timeSlotIndex: group.timeSlotIndex,
|
||
items: (group.participants || []).map(p => ({
|
||
id: p.id,
|
||
schoolUnit: p.schoolUnit,
|
||
playerName: p.playerName,
|
||
status: p.status || '未签到',
|
||
sortOrder: p.sortOrder
|
||
}))
|
||
}))
|
||
|
||
// 加载异常组数据
|
||
this.exceptionList = []
|
||
this.competitionGroups.forEach(group => {
|
||
group.items.forEach(item => {
|
||
if (item.status === '异常') {
|
||
this.exceptionList.push({
|
||
groupId: group.id,
|
||
groupTitle: group.title,
|
||
participantId: item.id,
|
||
schoolUnit: item.schoolUnit,
|
||
status: item.status
|
||
})
|
||
}
|
||
})
|
||
})
|
||
|
||
console.log('解析后的competitionGroups:', this.competitionGroups)
|
||
console.log('当前selectedVenueId:', this.selectedVenueId)
|
||
console.log('当前selectedTime:', this.selectedTime)
|
||
this.$message.success(data.isDraft ? '已加载草稿数据' : '已加载编排数据')
|
||
} else {
|
||
// 后端还没有数据,使用默认数据进行展示
|
||
this.competitionGroups = []
|
||
console.log('后端暂无编排数据,等待后端生成')
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('加载编排数据失败', err)
|
||
if (err.response?.status === 404) {
|
||
console.log('编排数据不存在,等待后端生成')
|
||
} else {
|
||
this.$message.error('加载编排数据失败: ' + (err.message || ''))
|
||
}
|
||
this.competitionGroups = []
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
|
||
// 移动分组
|
||
handleMoveGroup(group) {
|
||
if (this.isScheduleCompleted) {
|
||
this.$message.warning('编排已完成,无法移动')
|
||
return
|
||
}
|
||
this.moveGroupIndex = this.competitionGroups.findIndex(g => g.id === group.id)
|
||
this.moveTargetVenueId = group.venueId || null
|
||
this.moveTargetTimeSlot = group.timeSlotIndex || 0
|
||
this.moveDialogVisible = true
|
||
},
|
||
|
||
// 确认移动分组
|
||
async confirmMoveGroup() {
|
||
if (!this.moveTargetVenueId) {
|
||
this.$message.warning('请选择目标场地')
|
||
return
|
||
}
|
||
if (this.moveTargetTimeSlot === null) {
|
||
this.$message.warning('请选择目标时间段')
|
||
return
|
||
}
|
||
|
||
const group = this.competitionGroups[this.moveGroupIndex]
|
||
const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId)
|
||
|
||
try {
|
||
// 调用后端API移动分组
|
||
const res = await moveScheduleGroup({
|
||
groupId: group.id,
|
||
targetVenueId: this.moveTargetVenueId,
|
||
targetTimeSlotIndex: this.moveTargetTimeSlot
|
||
})
|
||
|
||
if (res.data.success) {
|
||
// 更新前端数据
|
||
group.venueId = this.moveTargetVenueId
|
||
group.venueName = targetVenue ? targetVenue.venueName : ''
|
||
group.timeSlotIndex = this.moveTargetTimeSlot
|
||
group.timeSlot = this.timeSlots[this.moveTargetTimeSlot]
|
||
|
||
this.$message.success(`已移动到 ${group.venueName} - ${group.timeSlot}`)
|
||
this.moveDialogVisible = false
|
||
} else {
|
||
this.$message.error(res.data.msg || '移动分组失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('移动分组失败:', error)
|
||
this.$message.error('移动分组失败,请稍后重试')
|
||
}
|
||
},
|
||
|
||
// 标记为异常
|
||
markAsException(group, itemIndex) {
|
||
if (this.isScheduleCompleted) {
|
||
this.$message.warning('编排已完成,无法标记异常')
|
||
return
|
||
}
|
||
|
||
const item = group.items[itemIndex]
|
||
|
||
// 修改状态为异常
|
||
item.status = '异常'
|
||
|
||
// 添加到异常组列表
|
||
this.exceptionList.push({
|
||
groupId: group.id,
|
||
groupTitle: group.title,
|
||
participantId: item.id,
|
||
schoolUnit: item.schoolUnit,
|
||
status: '异常'
|
||
})
|
||
|
||
// 调用后端API保存状态
|
||
updateCheckInStatus(item.id, '异常').then(() => {
|
||
this.$message.success(`已将 ${item.schoolUnit} 标记为异常`)
|
||
}).catch(err => {
|
||
console.error('保存异常状态失败:', err)
|
||
})
|
||
},
|
||
|
||
// 显示异常组对话框
|
||
showExceptionDialog() {
|
||
this.exceptionDialogVisible = true
|
||
},
|
||
|
||
// 从异常组移除
|
||
removeFromException(index) {
|
||
const exceptionItem = this.exceptionList[index]
|
||
|
||
// 在竞赛分组中找到对应的参赛人员并恢复状态
|
||
for (let group of this.competitionGroups) {
|
||
if (group.id === exceptionItem.groupId) {
|
||
for (let item of group.items) {
|
||
if (item.id === exceptionItem.participantId) {
|
||
item.status = '未签到'
|
||
break
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
// 从异常列表中移除
|
||
this.exceptionList.splice(index, 1)
|
||
// 调用后端API恢复状态
|
||
updateCheckInStatus(exceptionItem.participantId, '未签到').then(() => {
|
||
this.$message.success(`已将 ${exceptionItem.schoolUnit} 从异常组移除`)
|
||
}).catch(err => {
|
||
console.error('恢复状态失败:', err)
|
||
})
|
||
},
|
||
|
||
// 触发自动编排
|
||
async handleAutoArrange() {
|
||
try {
|
||
// 确认操作
|
||
await this.$confirm('自动编排将重新生成赛程安排,当前编排数据将被覆盖。确定继续吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
|
||
this.autoArrangeLoading = true
|
||
|
||
// 调用自动编排接口
|
||
const res = await triggerAutoArrange(this.competitionId)
|
||
|
||
if (res.data?.success) {
|
||
this.$message.success('自动编排完成')
|
||
|
||
// 重新加载编排数据
|
||
await this.loadScheduleData()
|
||
} else {
|
||
this.$message.error(res.data?.msg || '自动编排失败')
|
||
}
|
||
} catch (err) {
|
||
if (err !== 'cancel') {
|
||
console.error('自动编排失败', err)
|
||
this.$message.error('自动编排失败: ' + (err.message || ''))
|
||
}
|
||
} finally {
|
||
this.autoArrangeLoading = false
|
||
}
|
||
},
|
||
|
||
// 保存草稿
|
||
async handleSaveDraft() {
|
||
try {
|
||
this.loading = true
|
||
|
||
// 构建保存数据
|
||
const saveData = {
|
||
competitionId: this.competitionId,
|
||
isDraft: true,
|
||
competitionGroups: this.competitionGroups.map(group => ({
|
||
id: group.id,
|
||
title: group.title,
|
||
type: group.type,
|
||
count: group.count,
|
||
code: group.code,
|
||
venueId: group.venueId,
|
||
venueName: group.venueName,
|
||
timeSlot: group.timeSlot,
|
||
timeSlotIndex: group.timeSlotIndex,
|
||
participants: group.items.map((item, index) => ({
|
||
id: item.id,
|
||
schoolUnit: item.schoolUnit,
|
||
status: item.status,
|
||
sortOrder: index + 1
|
||
}))
|
||
}))
|
||
}
|
||
|
||
// 调用保存草稿接口
|
||
await saveDraftSchedule(saveData)
|
||
console.log('保存草稿数据:', saveData)
|
||
this.$message.success('草稿保存成功')
|
||
} catch (err) {
|
||
console.error('保存草稿失败', err)
|
||
this.$message.error('保存草稿失败: ' + (err.message || ''))
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
|
||
handleVenueCommand(command, groupIndex) {
|
||
// 这个方法已废弃,保留以防万一
|
||
},
|
||
handleMoveUp(group, itemIndex) {
|
||
if (itemIndex === 0 || this.isScheduleCompleted) return
|
||
const temp = group.items[itemIndex]
|
||
group.items.splice(itemIndex, 1)
|
||
group.items.splice(itemIndex - 1, 0, temp)
|
||
this.$message.success('上移成功')
|
||
},
|
||
handleMoveDown(group, itemIndex) {
|
||
if (itemIndex === group.items.length - 1 || this.isScheduleCompleted) return
|
||
const temp = group.items[itemIndex]
|
||
group.items.splice(itemIndex, 1)
|
||
group.items.splice(itemIndex + 1, 0, temp)
|
||
this.$message.success('下移成功')
|
||
},
|
||
async handleExport() {
|
||
try {
|
||
this.loading = true
|
||
const res = await exportSchedule(this.competitionId)
|
||
// axios 返回的是 response 对象,需要取 res.data
|
||
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
|
||
const link = document.createElement('a')
|
||
link.href = window.URL.createObjectURL(blob)
|
||
link.download = `赛程表_${this.competitionInfo.competitionName || this.competitionId}.xlsx`
|
||
link.click()
|
||
window.URL.revokeObjectURL(link.href)
|
||
this.$message.success('导出成功')
|
||
} catch (error) {
|
||
console.error('导出失败:', error)
|
||
this.$message.error('导出失败,请稍后重试')
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
handleConfirm() {
|
||
this.confirmDialogVisible = true
|
||
},
|
||
async confirmComplete() {
|
||
try {
|
||
this.loading = true
|
||
|
||
// 检查是否有编排数据
|
||
if (!this.competitionGroups || this.competitionGroups.length === 0) {
|
||
this.$message.warning('请先进行自动编排,生成编排数据后再完成编排')
|
||
this.confirmDialogVisible = false
|
||
return
|
||
}
|
||
|
||
console.log('开始完成编排,竞赛ID:', this.competitionId)
|
||
console.log('编排分组数量:', this.competitionGroups.length)
|
||
|
||
// 1. 先保存当前的草稿数据
|
||
const saveData = {
|
||
competitionId: this.competitionId,
|
||
isDraft: true,
|
||
competitionGroups: this.competitionGroups.map(group => ({
|
||
id: group.id,
|
||
title: group.title,
|
||
type: group.type,
|
||
count: group.count,
|
||
code: group.code,
|
||
venueId: group.venueId,
|
||
venueName: group.venueName,
|
||
timeSlot: group.timeSlot,
|
||
timeSlotIndex: group.timeSlotIndex,
|
||
participants: group.items.map((item, index) => ({
|
||
id: item.id,
|
||
schoolUnit: item.schoolUnit,
|
||
status: item.status,
|
||
sortOrder: index + 1
|
||
}))
|
||
}))
|
||
}
|
||
|
||
console.log('准备保存草稿数据:', saveData)
|
||
|
||
const draftRes = await saveDraftSchedule(saveData)
|
||
console.log('保存草稿成功,返回:', draftRes)
|
||
|
||
// 2. 然后调用锁定接口
|
||
console.log('准备锁定编排,竞赛ID:', this.competitionId)
|
||
const lockRes = await saveAndLockSchedule(this.competitionId)
|
||
console.log('锁定接口返回:', lockRes)
|
||
|
||
// 3. 更新UI状态
|
||
this.isScheduleCompleted = true
|
||
this.confirmDialogVisible = false
|
||
this.$message.success('编排已完成并锁定')
|
||
|
||
// 4. 重新加载编排数据以获取最新状态
|
||
await this.loadScheduleData()
|
||
} catch (err) {
|
||
console.error('完成编排失败', err)
|
||
console.error('错误详情:', err.response?.data || err.message)
|
||
this.$message.error('完成编排失败: ' + (err.response?.data?.msg || err.message || '未知错误'))
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.martial-schedule-container {
|
||
padding: 15px;
|
||
background: #fff;
|
||
|
||
.page-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 15px;
|
||
gap: 10px;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.page-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.tabs-section {
|
||
margin-bottom: 15px;
|
||
|
||
.tab-buttons {
|
||
margin-bottom: 15px;
|
||
padding: 12px;
|
||
background: #fff;
|
||
border: 1px solid #dcdfe6;
|
||
}
|
||
|
||
.tab-content {
|
||
padding: 15px;
|
||
background: #fff;
|
||
border: 1px solid #dcdfe6;
|
||
|
||
.time-selector {
|
||
margin-bottom: 15px;
|
||
// padding: 8px;
|
||
// background: #f5f7fa;
|
||
}
|
||
|
||
// 场地列表样式
|
||
.venue-list {
|
||
margin-bottom: 10px;
|
||
background: #ffffff;
|
||
border: 1px solid #ffffff;
|
||
border-radius: 4px;
|
||
|
||
.venue-list-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.venue-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
|
||
.no-venue-hint {
|
||
padding: 8px 12px;
|
||
color: #f59e0b;
|
||
font-size: 13px;
|
||
background: #fef3c7;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.competition-group {
|
||
margin-bottom: 15px;
|
||
|
||
.group-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px;
|
||
background: #f5f7fa;
|
||
border: 1px solid #dcdfe6;
|
||
border-bottom: none;
|
||
|
||
.group-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.group-title {
|
||
font-weight: 600;
|
||
color: #303133;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.group-meta {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.group-hints {
|
||
flex: 1;
|
||
text-align: center;
|
||
|
||
.hint-text {
|
||
color: #f50057;
|
||
font-size: 11px;
|
||
|
||
&.secondary {
|
||
color: #ff4081;
|
||
}
|
||
}
|
||
}
|
||
|
||
.group-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 新的项目卡片样式
|
||
.project-card {
|
||
margin-bottom: 20px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
|
||
.project-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
background: #fafafa;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
|
||
&:hover {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
&.project-header-collapsed {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.project-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.project-expand-icon {
|
||
width: 20px;
|
||
color: #909399;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.project-index {
|
||
font-weight: 600;
|
||
color: #e6a23c;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.project-title {
|
||
font-weight: 600;
|
||
color: #e6a23c;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.project-meta {
|
||
color: #606266;
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
.project-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
|
||
.team-list {
|
||
.team-row {
|
||
border-bottom: 1px solid #ebeef5;
|
||
background: #fff;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&.team-row-expanded {
|
||
background: #fafafa;
|
||
}
|
||
|
||
.team-main {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.expand-icon {
|
||
width: 20px;
|
||
color: #909399;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.team-info {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 180px;
|
||
|
||
.team-index {
|
||
color: #909399;
|
||
font-size: 13px;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.team-name {
|
||
color: #303133;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.player-list {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 0 20px;
|
||
|
||
.player-tag {
|
||
color: #606266;
|
||
font-size: 13px;
|
||
padding: 2px 8px;
|
||
background: #f4f4f5;
|
||
border-radius: 4px;
|
||
|
||
&.player-exception {
|
||
color: #f56c6c;
|
||
background: #fef0f0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.team-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
min-width: 80px;
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
|
||
.team-expand-content {
|
||
background: #fafafa;
|
||
border-top: 1px dashed #e4e7ed;
|
||
padding: 8px 16px 8px 56px;
|
||
|
||
.player-detail-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.player-detail-index {
|
||
width: 30px;
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.player-detail-name {
|
||
flex: 1;
|
||
color: #303133;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.player-detail-status {
|
||
width: 80px;
|
||
text-align: center;
|
||
}
|
||
|
||
.player-detail-actions {
|
||
width: 100px;
|
||
text-align: right;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.move-popover {
|
||
.move-label {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
margin-bottom: 8px;
|
||
}
|
||
}
|
||
|
||
.group-footer-hints {
|
||
margin-top: 15px;
|
||
padding: 8px 12px;
|
||
background: #fff3e0;
|
||
border-left: 3px solid #ff9800;
|
||
|
||
.footer-hint-text {
|
||
margin: 4px 0;
|
||
color: #f50057;
|
||
font-size: 11px;
|
||
|
||
&.secondary {
|
||
color: #ff4081;
|
||
}
|
||
}
|
||
}
|
||
|
||
.row-hint {
|
||
margin-top: 4px;
|
||
padding: 2px 5px;
|
||
color: #f50057;
|
||
font-size: 11px;
|
||
background: rgba(245, 0, 87, 0.05);
|
||
display: inline-block;
|
||
}
|
||
|
||
.venue-footer-hints {
|
||
margin-top: 15px;
|
||
padding: 8px 12px;
|
||
background: #fff3e0;
|
||
border-left: 3px solid #ff9800;
|
||
|
||
.footer-hint-text {
|
||
margin: 4px 0;
|
||
color: #f50057;
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
|
||
.status-active {
|
||
color: #409eff;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
.footer-actions {
|
||
text-align: center;
|
||
padding: 15px;
|
||
background: #fff;
|
||
border: 1px solid #dcdfe6;
|
||
}
|
||
|
||
.move-btn {
|
||
padding: 4px;
|
||
|
||
&:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.move-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
}
|
||
|
||
// 展开行样式
|
||
.player-expand-list {
|
||
padding: 10px 20px 10px 60px;
|
||
background: #fafafa;
|
||
|
||
.player-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px dashed #eee;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.player-name {
|
||
min-width: 80px;
|
||
color: #606266;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
</style>
|