678 lines
18 KiB
Vue
678 lines
18 KiB
Vue
<template>
|
||
<div class="add-contestant">
|
||
<div class="page-header">
|
||
<h1>添加新赛事</h1>
|
||
<div class="header-actions">
|
||
<button class="btn-excel" @click="handleExcelImport">
|
||
<i class="fas fa-file-excel"></i>
|
||
通过表格添加
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Excel导入弹窗 -->
|
||
<div v-if="showExcelDialog" class="excel-dialog-overlay">
|
||
<div class="excel-dialog">
|
||
<h3>通过Excel导入赛事信息</h3>
|
||
<div class="excel-upload-area" @click="triggerFileInput" @dragover.prevent @drop.prevent="handleFileDrop"
|
||
:class="{ 'is-dragover': isDragover }">
|
||
<input type="file" ref="fileInput" accept=".xlsx" @change="handleFileSelect" style="display: none">
|
||
<div class="upload-content">
|
||
<i class="fas fa-file-excel"></i>
|
||
<p>点击或拖拽Excel文件到此处</p>
|
||
</div>
|
||
</div>
|
||
<div v-if="eventPreviewData.length > 0" class="preview-table">
|
||
<h4>赛事信息表预览</h4>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th v-for="h in eventPreviewHeaders" :key="h">{{ h }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(item, index) in eventPreviewData" :key="index">
|
||
<td v-for="h in eventPreviewHeaders" :key="h">{{ item[h] }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div v-if="playerPreviewData.length > 0" class="preview-table">
|
||
<h4>选手报名表预览</h4>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th v-for="h in playerPreviewHeaders" :key="h">{{ h }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(item, index) in playerPreviewData" :key="index">
|
||
<td v-for="h in playerPreviewHeaders" :key="h">{{ item[h] }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="dialog-actions">
|
||
<button class="confirm-btn" @click="confirmImport" :disabled="eventPreviewData.length === 0">确认导入</button>
|
||
<button class="cancel-btn" @click="closeExcelDialog">取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form @submit.prevent="handleSubmit" class="contest-form">
|
||
<div class="form-group">
|
||
<label for="name">赛事名称</label>
|
||
<input type="text" id="name" v-model="formData.name" required placeholder="请输入赛事名称">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="format">赛制类型</label>
|
||
<select id="format" v-model="formData.format" required>
|
||
<option value="">请选择赛制类型</option>
|
||
<option value="single">单败淘汰</option>
|
||
<option value="double">双败淘汰</option>
|
||
<option value="count">积分赛</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="organizer">组织者</label>
|
||
<input type="text" id="organizer" v-model="formData.organizer" required placeholder="请输入组织者姓名">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="qq_code">QQ号</label>
|
||
<input type="text" id="qq_code" v-model="formData.qq_code" required placeholder="请输入QQ号">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="start_time">开始时间</label>
|
||
<input type="date" id="start_time" v-model="formData.start_time" required>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="end_time">结束时间</label>
|
||
<input type="date" id="end_time" v-model="formData.end_time" required>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button type="submit" class="submit-btn">提交</button>
|
||
<button type="button" class="cancel-btn" @click="handleCancel">取消</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { addTournament, addSignUp, getTournamentList } from '@/api/tournament'
|
||
import * as XLSX from 'xlsx'
|
||
|
||
const router = useRouter()
|
||
const showExcelDialog = ref(false)
|
||
const isDragover = ref(false)
|
||
const fileInput = ref(null)
|
||
const eventPreviewData = ref([])
|
||
const eventPreviewHeaders = ref([])
|
||
const playerPreviewData = ref([])
|
||
const playerPreviewHeaders = ref([])
|
||
|
||
const formData = ref({
|
||
name: '',
|
||
format: '',
|
||
organizer: '',
|
||
qq_code: '',
|
||
status: 'prepare',
|
||
start_time: '',
|
||
end_time: '',
|
||
description: ''
|
||
})
|
||
|
||
// Excel赛事信息字段与表单字段的映射
|
||
const excelFieldMap = {
|
||
'赛事名称': 'name',
|
||
'开始时间': 'start_time',
|
||
'结束时间': 'end_time',
|
||
'组织者': 'organizer',
|
||
'联系方式': 'qq_code',
|
||
'赛制类型': 'format',
|
||
}
|
||
|
||
// 赛制类型中英文映射
|
||
const formatMap = {
|
||
'单败淘汰': 'single',
|
||
'双败淘汰': 'double',
|
||
'积分赛': 'count'
|
||
}
|
||
|
||
// 阵营中英文映射
|
||
const factionMap = {
|
||
'盟军': 'allied',
|
||
'苏联': 'soviet',
|
||
'帝国': 'empire',
|
||
'OB': 'ob',
|
||
'解说': 'voice',
|
||
'随机': 'random'
|
||
}
|
||
const parseDate = (val) => {
|
||
if (!val) return ''
|
||
if (!isNaN(val) && Number(val) > 30000 && Number(val) < 90000) {
|
||
const date = new Date(Math.round((Number(val) - 25569) * 86400 * 1000))
|
||
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
|
||
}
|
||
const match = String(val).match(/(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})日?/)
|
||
if (match) {
|
||
return `${match[1]}/${parseInt(match[2], 10)}/${parseInt(match[3], 10)}`
|
||
}
|
||
return String(val).replace(/-/g, '/').slice(0, 10)
|
||
}
|
||
|
||
|
||
const handleSubmit = async () => {
|
||
try {
|
||
const formatDate = (dateStr) => {
|
||
const [year, month, day] = dateStr.split('-')
|
||
return `${year}/${month}/${day}`
|
||
}
|
||
|
||
// 构建API请求数据
|
||
const tournamentData = {
|
||
name: formData.value.name,
|
||
format: formData.value.format,
|
||
organizer: formData.value.organizer,
|
||
qq_code: String(formData.value.qq_code), // 确保 qq_code 是字符串类型
|
||
status: 'prepare',
|
||
start_time: formatDate(formData.value.start_time),
|
||
end_time: formatDate(formData.value.end_time)
|
||
}
|
||
|
||
// 调用API添加赛事
|
||
await addTournament(tournamentData)
|
||
|
||
alert('添加赛事成功!')
|
||
router.push('/competition')
|
||
} catch (error) {
|
||
console.error('提交失败:', error)
|
||
alert(error.response?.data?.message || '添加赛事失败,请重试')
|
||
}
|
||
}
|
||
|
||
const handleCancel = () => {
|
||
router.back()
|
||
}
|
||
|
||
const handleExcelImport = () => {
|
||
showExcelDialog.value = true
|
||
}
|
||
|
||
const handleFileDrop = (e) => {
|
||
isDragover.value = false
|
||
const file = e.dataTransfer.files[0]
|
||
if (file) {
|
||
processExcelFile(file)
|
||
}
|
||
}
|
||
|
||
const handleFileSelect = (e) => {
|
||
const file = e.target.files[0]
|
||
if (file) {
|
||
processExcelFile(file)
|
||
}
|
||
}
|
||
|
||
const processExcelFile = (file) => {
|
||
eventPreviewData.value = []
|
||
eventPreviewHeaders.value = []
|
||
playerPreviewData.value = []
|
||
playerPreviewHeaders.value = []
|
||
const reader = new FileReader()
|
||
reader.onload = (e) => {
|
||
try {
|
||
const data = new Uint8Array(e.target.result)
|
||
const workbook = XLSX.read(data, { type: 'array' })
|
||
const playerSheetName = workbook.SheetNames.find(name => name === '选手报名表')
|
||
if (playerSheetName) {
|
||
const sheet = workbook.Sheets[playerSheetName]
|
||
const jsonData = XLSX.utils.sheet_to_json(sheet, { header: 1 })
|
||
if (jsonData.length >= 2) {
|
||
let headerRowIdx = -1
|
||
for (let i = 0; i < jsonData.length; i++) {
|
||
if (!jsonData[i]) continue
|
||
const normRow = jsonData[i].map(cell => String(cell || '').replace(/\s+/g, '').replace(/ /g, '').trim())
|
||
if (normRow.includes('序号')) {
|
||
headerRowIdx = i
|
||
break
|
||
}
|
||
}
|
||
if (headerRowIdx !== -1) {
|
||
const headers = jsonData[headerRowIdx].map(cell => String(cell || '').replace(/\s+/g, '').replace(/ /g, '').trim())
|
||
const players = []
|
||
for (let i = headerRowIdx + 1; i < jsonData.length; i++) {
|
||
const row = jsonData[i]
|
||
if (!row || row.length === 0 || row.every(cell => cell === undefined || cell === '')) continue
|
||
const player = {}
|
||
headers.forEach((h, idx) => {
|
||
player[h] = row[idx]
|
||
})
|
||
players.push(player)
|
||
}
|
||
playerPreviewData.value = players
|
||
playerPreviewHeaders.value = headers
|
||
}
|
||
}
|
||
}
|
||
const eventSheetName = workbook.SheetNames.find(name => name === '赛事表')
|
||
if (eventSheetName) {
|
||
const firstSheet = workbook.Sheets[eventSheetName]
|
||
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 })
|
||
if (jsonData.length >= 2) {
|
||
let headerRowIdx = -1
|
||
let headerColIdx = -1
|
||
for (let i = 0; i < jsonData.length; i++) {
|
||
if (!jsonData[i]) continue
|
||
for (let j = 0; j < jsonData[i].length; j++) {
|
||
const cell = typeof jsonData[i][j] === 'string' ? jsonData[i][j].trim() : jsonData[i][j]
|
||
if (Object.keys(excelFieldMap).includes(cell)) {
|
||
headerRowIdx = i
|
||
headerColIdx = j
|
||
break
|
||
}
|
||
}
|
||
if (headerRowIdx !== -1) break
|
||
}
|
||
if (headerRowIdx !== -1 && headerColIdx !== -1) {
|
||
let dataRow = null
|
||
for (let i = headerRowIdx + 1; i < jsonData.length; i++) {
|
||
if (jsonData[i] && jsonData[i].slice(headerColIdx).some(cell => cell !== undefined && cell !== '')) {
|
||
dataRow = jsonData[i]
|
||
break
|
||
}
|
||
}
|
||
if (dataRow) {
|
||
const headers = jsonData[headerRowIdx].slice(headerColIdx).map(h => typeof h === 'string' ? h.trim() : h)
|
||
const dataCells = dataRow.slice(headerColIdx)
|
||
const previewObj = {}
|
||
Object.keys(excelFieldMap).forEach((h) => {
|
||
const idx = headers.indexOf(h)
|
||
let val = idx !== -1 ? dataCells[idx] : ''
|
||
if (["开始时间", "结束时间"].includes(h)) {
|
||
val = parseDate(val)
|
||
} else if (h === "联系方式") { // 处理 qq_code
|
||
val = String(val || '') // 确保 qq_code 是字符串类型
|
||
}
|
||
previewObj[h] = val
|
||
})
|
||
eventPreviewData.value = [previewObj]
|
||
eventPreviewHeaders.value = Object.keys(excelFieldMap)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
alert('Excel文件格式错误,请检查文件内容')
|
||
}
|
||
}
|
||
reader.readAsArrayBuffer(file)
|
||
}
|
||
|
||
const confirmImport = async () => {
|
||
if (eventPreviewData.value.length > 0) {
|
||
const excelRow = eventPreviewData.value[0]
|
||
const tournamentData = {}
|
||
Object.keys(excelFieldMap).forEach((excelKey) => {
|
||
const apiKey = excelFieldMap[excelKey]
|
||
if (apiKey === 'qq_code') {
|
||
tournamentData[apiKey] = String(excelRow[excelKey])
|
||
} else if (apiKey === 'format') {
|
||
const formatValue = formatMap[excelRow[excelKey]]
|
||
if (formatValue) {
|
||
tournamentData[apiKey] = formatValue
|
||
} else {
|
||
throw new Error('赛制类型必须是:单败淘汰、双败淘汰或积分赛')
|
||
}
|
||
} else {
|
||
tournamentData[apiKey] = excelRow[excelKey]
|
||
}
|
||
})
|
||
tournamentData.status = 'prepare'
|
||
let tournamentName = tournamentData.name
|
||
|
||
try {
|
||
await addTournament(tournamentData)
|
||
alert('赛事导入成功!')
|
||
|
||
const tournamentList = await getTournamentList()
|
||
console.log('获取到的赛事列表:', tournamentList)
|
||
console.log('要匹配的赛事名称:', tournamentName)
|
||
|
||
const matchedTournament = tournamentList.find(t => t.name === tournamentName)
|
||
console.log('匹配到的赛事:', matchedTournament)
|
||
|
||
if (!matchedTournament || !matchedTournament.id) {
|
||
throw new Error(`未找到对应的赛事信息: ${tournamentName}`)
|
||
}
|
||
|
||
if (playerPreviewData.value.length > 0) {
|
||
for (const row of playerPreviewData.value) {
|
||
console.log('处理选手数据:', row)
|
||
const isTeam = !!row['团队参赛选手ID']
|
||
const signUpData = {
|
||
id: matchedTournament.id,
|
||
tournament_name: tournamentName,
|
||
type: isTeam ? 'teamname' : 'individual',
|
||
team_name: row['队伍名称或者个人参赛名称'] || '',
|
||
sign_name: row['队伍名称或者个人参赛名称'],
|
||
faction: factionMap[row['阵营']] || 'random', // 使用阵营映射
|
||
qq_code: String(row['QQ'] || '')
|
||
}
|
||
console.log('提交的报名数据:', signUpData)
|
||
|
||
if (!signUpData.id || !signUpData.sign_name) {
|
||
console.error('报名数据缺少必需字段:', signUpData)
|
||
throw new Error(`报名数据不完整,请检查数据: ${JSON.stringify(row)}`)
|
||
}
|
||
|
||
try {
|
||
await addSignUp(signUpData)
|
||
} catch (error) {
|
||
console.error('报名失败:', error)
|
||
alert(`报名失败: ${row['队伍名称或者个人参赛名称']},${error.message}`)
|
||
}
|
||
}
|
||
alert('选手报名表导入完成!')
|
||
}
|
||
|
||
router.push('/competition')
|
||
} catch (error) {
|
||
console.error('导入失败:', error)
|
||
alert(error.response?.data?.message || '赛事导入失败,请重试')
|
||
}
|
||
closeExcelDialog()
|
||
}
|
||
}
|
||
|
||
const closeExcelDialog = () => {
|
||
showExcelDialog.value = false
|
||
eventPreviewData.value = []
|
||
eventPreviewHeaders.value = []
|
||
playerPreviewData.value = []
|
||
playerPreviewHeaders.value = []
|
||
if (fileInput.value) {
|
||
fileInput.value.value = ''
|
||
}
|
||
}
|
||
|
||
const triggerFileInput = () => {
|
||
if (fileInput.value) {
|
||
fileInput.value.click()
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.add-contestant {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 30px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.page-header h1 {
|
||
font-size: 24px;
|
||
color: #1a237e;
|
||
margin: 0;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.btn-back,
|
||
.btn-excel {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 16px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn-back {
|
||
background: white;
|
||
color: #666;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
.btn-back:hover {
|
||
background: #f5f5f5;
|
||
border-color: #ccc;
|
||
}
|
||
|
||
.btn-excel {
|
||
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
|
||
color: white;
|
||
border: none;
|
||
}
|
||
|
||
.btn-excel:hover {
|
||
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.contest-form {
|
||
background: white;
|
||
padding: 30px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
border-color: #1a237e;
|
||
outline: none;
|
||
box-shadow: 0 0 0 2px rgba(26, 35, 126, 0.1);
|
||
}
|
||
|
||
.form-group input::placeholder {
|
||
color: #999;
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.submit-btn,
|
||
.cancel-btn {
|
||
padding: 12px 24px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.submit-btn {
|
||
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
|
||
color: white;
|
||
border: none;
|
||
}
|
||
|
||
.submit-btn:hover {
|
||
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.cancel-btn {
|
||
background: white;
|
||
color: #666;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
.cancel-btn:hover {
|
||
background: #f5f5f5;
|
||
border-color: #ccc;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.add-contestant {
|
||
padding: 15px;
|
||
}
|
||
|
||
.contest-form {
|
||
padding: 20px;
|
||
}
|
||
|
||
.form-actions {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.submit-btn,
|
||
.cancel-btn {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.excel-dialog-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.excel-dialog {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
width: 600px;
|
||
max-width: 90%;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.excel-upload-area {
|
||
border: 2px dashed #ddd;
|
||
border-radius: 8px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.excel-upload-area.is-dragover {
|
||
border-color: #416bdf;
|
||
background: rgba(65, 107, 223, 0.1);
|
||
}
|
||
|
||
.upload-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.upload-content i {
|
||
font-size: 48px;
|
||
color: #416bdf;
|
||
}
|
||
|
||
.upload-tip {
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.preview-table {
|
||
margin: 20px 0;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.preview-table table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.preview-table th,
|
||
.preview-table td {
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
text-align: left;
|
||
max-width: 200px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.preview-table th {
|
||
background: #f5f5f5;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.dialog-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.confirm-btn {
|
||
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 8px 20px;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: background 0.2s, box-shadow 0.2s;
|
||
box-shadow: 0 2px 8px rgba(65, 107, 223, 0.08);
|
||
}
|
||
|
||
.confirm-btn:hover {
|
||
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
|
||
box-shadow: 0 4px 16px rgba(65, 107, 223, 0.15);
|
||
}
|
||
|
||
.confirm-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
</style> |