762 lines
19 KiB
Vue
762 lines
19 KiB
Vue
<template>
|
||
<div class="excel-import-management">
|
||
<!-- 页面头部 -->
|
||
<div class="page-header">
|
||
<div class="header-content">
|
||
<h1>📊 Excel数据导入系统</h1>
|
||
<p>管理员专用 - {{ currentLotteryTypeName }}Excel文件数据批量导入</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 权限检查中 -->
|
||
<div v-if="permissionChecking" class="permission-checking">
|
||
<div class="checking-content">
|
||
<div class="loading-spinner"></div>
|
||
<p>正在验证权限...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主要内容 - 只有有权限时才显示 -->
|
||
<div v-else-if="hasPermission" class="import-container">
|
||
<!-- 功能区域 -->
|
||
<div class="function-area">
|
||
<el-row :gutter="20">
|
||
<!-- 完整数据导入 -->
|
||
<el-col :span="8">
|
||
<el-card class="function-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<el-icon><Document /></el-icon>
|
||
<span>完整数据导入</span>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="card-desc">
|
||
<p>上传包含T1-T7工作表的Excel文件,导入红球、蓝球、接续系数和组合系数数据</p>
|
||
</div>
|
||
|
||
<div class="upload-section">
|
||
<div class="file-input-container">
|
||
<input
|
||
type="file"
|
||
ref="fullDataFileInput"
|
||
@change="handleFileSelect($event, 'fullData')"
|
||
accept=".xlsx,.xls"
|
||
class="file-input"
|
||
id="fullDataFile"
|
||
/>
|
||
<label for="fullDataFile" class="file-label">
|
||
<el-icon class="file-icon"><FolderOpened /></el-icon>
|
||
<span class="file-text">
|
||
{{ fullDataFile ? fullDataFile.name : '选择Excel文件(包含T1-T7工作表)' }}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<el-button
|
||
type="primary"
|
||
@click="uploadFullData"
|
||
:disabled="!fullDataFile || fullDataUploading"
|
||
:loading="fullDataUploading"
|
||
style="width: 100%; margin-top: 16px"
|
||
>
|
||
{{ fullDataUploading ? '导入中...' : '开始导入' }}
|
||
</el-button>
|
||
|
||
<div v-if="fullDataResult" class="result-message" :class="fullDataResult.type">
|
||
{{ fullDataResult.message }}
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 开奖数据导入(覆盖) -->
|
||
<el-col :span="8">
|
||
<el-card class="function-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<el-icon><Warning /></el-icon>
|
||
<span>开奖数据导入(覆盖)</span>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="card-desc">
|
||
<p>上传包含T10工作表的Excel文件,清空并重新导入开奖数据</p>
|
||
</div>
|
||
|
||
<div class="upload-section">
|
||
<div class="file-input-container">
|
||
<input
|
||
type="file"
|
||
ref="lotteryFileInput"
|
||
@change="handleFileSelect($event, 'lottery')"
|
||
accept=".xlsx,.xls"
|
||
class="file-input"
|
||
id="lotteryFile"
|
||
/>
|
||
<label for="lotteryFile" class="file-label">
|
||
<el-icon class="file-icon"><FolderOpened /></el-icon>
|
||
<span class="file-text">
|
||
{{ lotteryFile ? lotteryFile.name : '选择Excel文件(包含T10工作表)' }}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<el-button
|
||
type="warning"
|
||
@click="uploadLotteryData"
|
||
:disabled="!lotteryFile || lotteryUploading"
|
||
:loading="lotteryUploading"
|
||
style="width: 100%; margin-top: 16px"
|
||
>
|
||
{{ lotteryUploading ? '导入中...' : '覆盖导入' }}
|
||
</el-button>
|
||
|
||
<div v-if="lotteryResult" class="result-message" :class="lotteryResult.type">
|
||
{{ lotteryResult.message }}
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 开奖数据追加 -->
|
||
<el-col :span="8">
|
||
<el-card class="function-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<el-icon><Plus /></el-icon>
|
||
<span>开奖数据追加</span>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="card-desc">
|
||
<p>上传包含T10工作表的Excel文件,追加导入开奖数据(跳过重复期号)</p>
|
||
</div>
|
||
|
||
<div class="upload-section">
|
||
<div class="file-input-container">
|
||
<input
|
||
type="file"
|
||
ref="appendFileInput"
|
||
@change="handleFileSelect($event, 'append')"
|
||
accept=".xlsx,.xls"
|
||
class="file-input"
|
||
id="appendFile"
|
||
/>
|
||
<label for="appendFile" class="file-label">
|
||
<el-icon class="file-icon"><FolderOpened /></el-icon>
|
||
<span class="file-text">
|
||
{{ appendFile ? appendFile.name : '选择Excel文件(包含T10工作表)' }}
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<el-button
|
||
type="success"
|
||
@click="appendLotteryData"
|
||
:disabled="!appendFile || appendUploading"
|
||
:loading="appendUploading"
|
||
style="width: 100%; margin-top: 16px"
|
||
>
|
||
{{ appendUploading ? '追加中...' : '追加导入' }}
|
||
</el-button>
|
||
|
||
<div v-if="appendResult" class="result-message" :class="appendResult.type">
|
||
{{ appendResult.message }}
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- 导入说明 -->
|
||
<el-card class="info-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<el-icon><InfoFilled /></el-icon>
|
||
<span>导入说明</span>
|
||
</div>
|
||
</template>
|
||
<div class="info-content">
|
||
<div class="info-item">
|
||
<h4>📋 完整数据导入:</h4>
|
||
<p>• 需要包含T1、T2、T3、T4、T5、T6、T7工作表的Excel文件</p>
|
||
<p>• 导入红球、蓝球、接续系数和组合系数数据到相应的数据库表</p>
|
||
<p>• 适用于系统初始化或全量数据更新</p>
|
||
</div>
|
||
|
||
<div class="info-item">
|
||
<h4>🎯 开奖数据导入(覆盖):</h4>
|
||
<p>• 需要包含T10工作表的Excel文件</p>
|
||
<p>• 清空lottery_draws表的现有数据,重新导入</p>
|
||
<p>• 适用于完全替换开奖数据</p>
|
||
</div>
|
||
|
||
<div class="info-item">
|
||
<h4>➕ 开奖数据追加:</h4>
|
||
<p>• 需要包含T10工作表的Excel文件</p>
|
||
<p>• 保留现有数据,只添加新的开奖记录</p>
|
||
<p>• 自动跳过重复的期号,适用于增量更新</p>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
|
||
<!-- 错误提示弹窗 -->
|
||
<div v-if="showErrorModal" class="modal-overlay" @click="hideErrorModal">
|
||
<div class="modal-content error-modal" @click.stop>
|
||
<h3>❌ 导入失败</h3>
|
||
<p>{{ errorMessage }}</p>
|
||
<button class="btn btn-primary" @click="hideErrorModal">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { lotteryApi } from '../../api/index.js'
|
||
import { userStore } from '../../store/user.js'
|
||
export default {
|
||
name: 'AdminExcelImportManagement',
|
||
data() {
|
||
return {
|
||
// 权限验证
|
||
hasPermission: false,
|
||
permissionChecking: true,
|
||
|
||
// 文件对象
|
||
fullDataFile: null,
|
||
lotteryFile: null,
|
||
appendFile: null,
|
||
|
||
// 上传状态
|
||
fullDataUploading: false,
|
||
lotteryUploading: false,
|
||
appendUploading: false,
|
||
|
||
// 结果信息
|
||
fullDataResult: null,
|
||
lotteryResult: null,
|
||
appendResult: null,
|
||
|
||
// 错误处理
|
||
showErrorModal: false,
|
||
errorMessage: ''
|
||
}
|
||
},
|
||
computed: {
|
||
// 当前彩票类型名称
|
||
currentLotteryTypeName() {
|
||
const routePath = this.$route.path
|
||
if (routePath.includes('/ssq')) {
|
||
return '双色球 - '
|
||
} else if (routePath.includes('/dlt')) {
|
||
return '大乐透 - '
|
||
}
|
||
return ''
|
||
}
|
||
},
|
||
async mounted() {
|
||
await this.checkPermission()
|
||
},
|
||
methods: {
|
||
// 检查用户权限
|
||
async checkPermission() {
|
||
try {
|
||
const response = await lotteryApi.getLoginUser()
|
||
|
||
if (response && response.success && response.data) {
|
||
const userRole = response.data.userRole
|
||
if (userRole === 'admin' || userRole === 'superAdmin') {
|
||
this.hasPermission = true
|
||
} else {
|
||
this.showError('无权限访问此页面,仅限管理员或超级管理员使用')
|
||
// 3秒后跳转到管理员登录页
|
||
setTimeout(() => {
|
||
this.$router.push('/cpzsadmin/login')
|
||
}, 3000)
|
||
}
|
||
} else {
|
||
this.showError('获取用户信息失败,请重新登录')
|
||
setTimeout(() => {
|
||
this.$router.push('/cpzsadmin/login')
|
||
}, 3000)
|
||
}
|
||
} catch (error) {
|
||
console.error('权限检查失败:', error)
|
||
this.showError('权限验证失败,请重新登录')
|
||
setTimeout(() => {
|
||
this.$router.push('/cpzsadmin/login')
|
||
}, 3000)
|
||
} finally {
|
||
this.permissionChecking = false
|
||
}
|
||
},
|
||
|
||
// 文件选择处理
|
||
handleFileSelect(event, type) {
|
||
const file = event.target.files[0]
|
||
if (!file) return
|
||
|
||
// 验证文件类型
|
||
if (!this.validateFileType(file)) {
|
||
this.showError('请选择.xlsx或.xls格式的Excel文件')
|
||
event.target.value = ''
|
||
return
|
||
}
|
||
|
||
// 验证文件大小(限制50MB)
|
||
if (file.size > 50 * 1024 * 1024) {
|
||
this.showError('文件大小不能超过50MB')
|
||
event.target.value = ''
|
||
return
|
||
}
|
||
|
||
switch (type) {
|
||
case 'fullData':
|
||
this.fullDataFile = file
|
||
this.fullDataResult = null
|
||
break
|
||
case 'lottery':
|
||
this.lotteryFile = file
|
||
this.lotteryResult = null
|
||
break
|
||
case 'append':
|
||
this.appendFile = file
|
||
this.appendResult = null
|
||
break
|
||
}
|
||
},
|
||
|
||
// 验证文件类型
|
||
validateFileType(file) {
|
||
const allowedTypes = [
|
||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||
'application/vnd.ms-excel' // .xls
|
||
]
|
||
return allowedTypes.includes(file.type) ||
|
||
file.name.endsWith('.xlsx') ||
|
||
file.name.endsWith('.xls')
|
||
},
|
||
|
||
// 上传完整数据
|
||
async uploadFullData() {
|
||
if (!this.fullDataFile) return
|
||
|
||
this.fullDataUploading = true
|
||
|
||
try {
|
||
const response = await lotteryApi.uploadExcelFile(this.fullDataFile)
|
||
this.fullDataResult = {
|
||
type: 'success',
|
||
message: '✅ ' + (response || '完整数据导入成功!')
|
||
}
|
||
|
||
// 清空文件选择
|
||
this.fullDataFile = null
|
||
this.$refs.fullDataFileInput.value = ''
|
||
|
||
} catch (error) {
|
||
console.error('完整数据导入失败:', error)
|
||
this.fullDataResult = {
|
||
type: 'error',
|
||
message: '❌ ' + (error?.response?.data || error?.message || '导入失败,请重试')
|
||
}
|
||
|
||
} finally {
|
||
this.fullDataUploading = false
|
||
}
|
||
},
|
||
|
||
// 上传开奖数据(覆盖)
|
||
async uploadLotteryData() {
|
||
if (!this.lotteryFile) return
|
||
|
||
this.lotteryUploading = true
|
||
|
||
try {
|
||
const response = await lotteryApi.uploadLotteryDrawsFile(this.lotteryFile)
|
||
this.lotteryResult = {
|
||
type: 'success',
|
||
message: '✅ ' + (response || '开奖数据导入成功!')
|
||
}
|
||
|
||
// 清空文件选择
|
||
this.lotteryFile = null
|
||
this.$refs.lotteryFileInput.value = ''
|
||
|
||
} catch (error) {
|
||
console.error('开奖数据导入失败:', error)
|
||
this.lotteryResult = {
|
||
type: 'error',
|
||
message: '❌ ' + (error?.response?.data || error?.message || '导入失败,请重试')
|
||
}
|
||
|
||
} finally {
|
||
this.lotteryUploading = false
|
||
}
|
||
},
|
||
|
||
// 追加开奖数据
|
||
async appendLotteryData() {
|
||
if (!this.appendFile) return
|
||
|
||
this.appendUploading = true
|
||
|
||
try {
|
||
const response = await lotteryApi.appendLotteryDrawsFile(this.appendFile)
|
||
this.appendResult = {
|
||
type: 'success',
|
||
message: '✅ ' + (response || '开奖数据追加成功!')
|
||
}
|
||
|
||
// 清空文件选择
|
||
this.appendFile = null
|
||
this.$refs.appendFileInput.value = ''
|
||
|
||
} catch (error) {
|
||
console.error('开奖数据追加失败:', error)
|
||
this.appendResult = {
|
||
type: 'error',
|
||
message: '❌ ' + (error?.response?.data || error?.message || '追加失败,请重试')
|
||
}
|
||
|
||
} finally {
|
||
this.appendUploading = false
|
||
}
|
||
},
|
||
|
||
// 显示错误信息
|
||
showError(message) {
|
||
this.errorMessage = message
|
||
this.showErrorModal = true
|
||
},
|
||
|
||
// 隐藏错误弹窗
|
||
hideErrorModal() {
|
||
this.showErrorModal = false
|
||
this.errorMessage = ''
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.excel-import-management {
|
||
min-height: 100vh;
|
||
background: var(--color-bg-page, #f0f2f5);
|
||
padding: 24px;
|
||
}
|
||
|
||
/* 页面头部 */
|
||
.page-header {
|
||
text-align: center;
|
||
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
|
||
padding: 30px;
|
||
border-radius: 8px;
|
||
color: white;
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
/* 权限检查样式 */
|
||
.permission-checking {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 400px;
|
||
}
|
||
|
||
.checking-content {
|
||
text-align: center;
|
||
color: white;
|
||
}
|
||
|
||
.checking-content p {
|
||
margin-top: 20px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||
border-top: 4px solid white;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.header-content h1 {
|
||
font-size: 32px;
|
||
margin-bottom: 10px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.header-content p {
|
||
font-size: 16px;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
/* 主容器 */
|
||
.import-container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* 功能区域 */
|
||
.function-area {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.function-card {
|
||
border: none;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
height: 100%;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.card-header .el-icon {
|
||
font-size: 18px;
|
||
color: #409EFF;
|
||
}
|
||
|
||
.card-desc {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.card-desc p {
|
||
color: #666;
|
||
margin: 0;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 上传区域 */
|
||
.upload-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.file-input-container {
|
||
position: relative;
|
||
}
|
||
|
||
.file-input {
|
||
display: none;
|
||
}
|
||
|
||
.file-label {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
border: 2px dashed #dcdfe6;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
background: #fafafa;
|
||
min-height: 60px;
|
||
}
|
||
|
||
.file-label:hover {
|
||
border-color: #409eff;
|
||
background: #ecf5ff;
|
||
}
|
||
|
||
.file-icon {
|
||
font-size: 20px;
|
||
margin-right: 12px;
|
||
color: #409eff;
|
||
}
|
||
|
||
.file-text {
|
||
color: #606266;
|
||
font-size: 14px;
|
||
flex: 1;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* 减少卡片内边距 */
|
||
:deep(.el-card__body) {
|
||
padding: 16px !important;
|
||
}
|
||
|
||
/* 结果消息 */
|
||
.result-message {
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.result-message.success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
|
||
.result-message.error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
|
||
/* 信息卡片 */
|
||
.info-card {
|
||
border: none;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* 信息说明 */
|
||
.info-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.info-item h4 {
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.info-item p {
|
||
color: #666;
|
||
margin: 2px 0;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 模态框 */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
padding: 30px;
|
||
border-radius: 12px;
|
||
max-width: 400px;
|
||
width: 90%;
|
||
text-align: center;
|
||
}
|
||
|
||
.error-modal h3 {
|
||
color: #dc3545;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.error-modal p {
|
||
margin-bottom: 20px;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #409eff;
|
||
color: white;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 1200px) {
|
||
.function-area .el-col {
|
||
margin-bottom: 20px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.excel-import-management {
|
||
padding: 16px;
|
||
}
|
||
|
||
.function-area .el-row {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.function-area .el-col {
|
||
flex: 0 0 100%;
|
||
max-width: 100%;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.page-header {
|
||
padding: 20px;
|
||
}
|
||
|
||
.page-header h1 {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.card-header span {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.card-desc {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.card-desc p {
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.file-label {
|
||
padding: 8px 10px;
|
||
min-height: 45px;
|
||
}
|
||
|
||
.file-text {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.file-icon {
|
||
font-size: 16px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.upload-section {
|
||
gap: 12px;
|
||
}
|
||
|
||
.info-item h4 {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.info-item p {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
</style> |