DCFronted/src/views/index/AddContestant.vue
2025-05-26 20:35:34 +08:00

678 lines
18 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>