初始提交:彩票推测系统前端代码

This commit is contained in:
lihanqi
2026-01-15 18:16:50 +08:00
commit 492d839e9b
169 changed files with 62221 additions and 0 deletions

View File

@@ -0,0 +1,447 @@
<template>
<div class="admin-login">
<div class="login-container">
<!-- 左侧装饰区域 -->
<div class="login-banner">
<div class="banner-content">
<div class="logo">
<img src="/favicon.ico" alt="Logo" />
<h1>彩票推测系统</h1>
</div>
<div class="banner-text">
<h2>后台管理系统</h2>
<p>专业的数据管理与用户服务</p>
</div>
<div class="banner-features">
<div class="feature-item">
<el-icon><User /></el-icon>
<span>用户管理</span>
</div>
<div class="feature-item">
<el-icon><Key /></el-icon>
<span>会员码管理</span>
</div>
<div class="feature-item">
<el-icon><Document /></el-icon>
<span>数据导入</span>
</div>
</div>
</div>
</div>
<!-- 右侧登录表单 -->
<div class="login-form">
<div class="form-container">
<div class="form-header">
<h2>管理员登录</h2>
<p>请输入您的管理员账号和密码</p>
</div>
<!-- 被踢出提示 -->
<el-alert
v-if="showKickedOutAlert"
title="账号已在其他设备登录"
type="warning"
description="为保障账号安全,您的管理员账号已在其他设备登录,当前会话已失效。请重新登录。"
show-icon
:closable="true"
@close="showKickedOutAlert = false"
style="margin-bottom: 20px"
/>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form-content"
@keyup.enter="handleLogin"
>
<el-form-item prop="userAccount">
<el-input
v-model="loginForm.userAccount"
placeholder="请输入管理员账号"
size="large"
clearable
>
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="userPassword">
<el-input
v-model="loginForm.userPassword"
type="password"
placeholder="请输入密码"
size="large"
show-password
clearable
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
class="login-button"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<div class="login-tips">
<el-alert
v-if="errorMessage"
:title="errorMessage"
type="error"
:closable="true"
@close="errorMessage = ''"
/>
</div>
<div class="form-footer">
<p>© 2024 彩票推测系统 - 后台管理</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock, Key, Document } from '@element-plus/icons-vue'
import { lotteryApi } from '../../api/index.js'
import { userStore } from '../../store/user.js'
export default {
name: 'AdminLogin',
components: {
User,
Lock,
Key,
Document
},
setup() {
const router = useRouter()
const loginFormRef = ref()
const loading = ref(false)
const errorMessage = ref('')
const showKickedOutAlert = ref(false)
const loginForm = reactive({
userAccount: '',
userPassword: ''
})
const loginRules = {
userAccount: [
{ required: true, message: '请输入管理员账号', trigger: 'blur' },
{ min: 3, max: 20, message: '账号长度在 3 到 20 个字符', trigger: 'blur' }
],
userPassword: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
]
}
const handleLogin = async () => {
try {
await loginFormRef.value.validate()
loading.value = true
errorMessage.value = ''
// 使用真实的登录接口
const response = await lotteryApi.userLogin(loginForm.userAccount, loginForm.userPassword)
if (response && response.success) {
// 保存用户信息到session存储
userStore.setUserInfo(response.data)
// 获取完整用户信息,检查角色
const userResponse = await lotteryApi.getLoginUser()
if (userResponse && userResponse.success && userResponse.data) {
const userRole = userResponse.data.userRole
// 检查用户角色是否有权限访问后台
if (userRole && userRole !== 'user') {
// 确保将用户角色信息保存到session中
const userData = userResponse.data
userData.userRole = userRole
userStore.setUserInfo(userData)
ElMessage({
type: 'success',
message: '登录成功,欢迎使用后台管理系统'
})
// 跳转到后台管理首页
router.push('/cpzsadmin/dashboard')
} else {
// 无权限访问
errorMessage.value = '您的账号无权限访问后台管理系统'
userStore.adminLogout() // 使用专门的管理员登出方法
}
} else {
errorMessage.value = userResponse?.message || '获取用户信息失败'
userStore.adminLogout() // 使用专门的管理员登出方法
}
} else {
errorMessage.value = response?.message || '登录失败,请检查账号密码'
}
} catch (error) {
console.error('登录失败:', error)
errorMessage.value = error?.response?.data?.message || '登录失败,请重试'
} finally {
loading.value = false
}
}
// 检查是否因为被踢出而跳转到登录页
onMounted(() => {
if (userStore.isKickedOut) {
showKickedOutAlert.value = true
// 显示提示后重置状态
setTimeout(() => {
userStore.resetKickedOutStatus()
}, 500)
}
})
return {
loginFormRef,
loginForm,
loginRules,
loading,
errorMessage,
showKickedOutAlert,
handleLogin
}
}
}
</script>
<style scoped>
.admin-login {
min-height: 100vh;
height: 100vh;
width: 100vw;
background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
}
.login-container {
width: 100%;
max-width: 1200px;
height: 600px;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
display: flex;
overflow: hidden;
}
/* 左侧装饰区域 */
.login-banner {
flex: 1;
background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
position: relative;
overflow: hidden;
}
.login-banner::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="white" opacity="0.1"/><circle cx="75" cy="75" r="1" fill="white" opacity="0.1"/><circle cx="50" cy="10" r="0.5" fill="white" opacity="0.1"/><circle cx="10" cy="60" r="0.5" fill="white" opacity="0.1"/><circle cx="90" cy="40" r="0.5" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.banner-content {
text-align: center;
color: white;
position: relative;
z-index: 1;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
}
.logo img {
width: 48px;
height: 48px;
margin-right: 15px;
}
.logo h1 {
font-size: 28px;
font-weight: 600;
margin: 0;
}
.banner-text h2 {
font-size: 32px;
font-weight: 700;
margin-bottom: 10px;
}
.banner-text p {
font-size: 16px;
opacity: 0.9;
margin-bottom: 40px;
}
.banner-features {
display: flex;
flex-direction: column;
gap: 20px;
}
.feature-item {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 16px;
opacity: 0.9;
}
.feature-item .el-icon {
font-size: 20px;
}
/* 右侧登录表单 */
.login-form {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
.form-container {
width: 100%;
max-width: 400px;
}
.form-header {
text-align: center;
margin-bottom: 40px;
}
.form-header h2 {
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.form-header p {
color: #666;
font-size: 14px;
}
.login-form-content {
margin-bottom: 30px;
}
.login-button {
width: 100%;
height: 48px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(135deg, #2563eb, #06b6d4);
border: none;
}
.login-button:hover {
background: linear-gradient(135deg, #3b82f6, #22d3ee);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(6, 182, 212, 0.4);
}
.login-tips {
margin-bottom: 20px;
}
.login-tips p {
margin: 5px 0;
font-size: 14px;
}
.form-footer {
text-align: center;
color: #999;
font-size: 12px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.login-container {
flex-direction: column;
height: auto;
max-width: 400px;
}
.login-banner {
padding: 30px 20px;
}
.banner-text h2 {
font-size: 24px;
}
.login-form {
padding: 30px 20px;
}
}
@media (max-width: 480px) {
.admin-login {
padding: 10px;
}
.login-container {
border-radius: 15px;
}
.logo h1 {
font-size: 20px;
}
.banner-text h2 {
font-size: 20px;
}
}
</style>

View File

@@ -0,0 +1,753 @@
<template>
<div class="announcement-management">
<!-- 页面标题 -->
<div class="page-header">
<div class="header-content">
<h1>公告管理</h1>
<p>管理系统公告的发布和维护</p>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-cards">
<el-row :gutter="20">
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Bell /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.total }}</div>
<div class="stat-label">总公告数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon published">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.published }}</div>
<div class="stat-label">已发布</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon draft">
<el-icon><Edit /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.draft }}</div>
<div class="stat-label">草稿</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon offline">
<el-icon><Close /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.offline }}</div>
<div class="stat-label">已下架</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 查询筛选区域 -->
<el-card class="search-card">
<el-form :model="queryForm" :inline="true">
<el-form-item label="公告标题">
<el-input
v-model="queryForm.title"
placeholder="请输入公告标题"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="公告状态">
<div class="custom-select-wrapper">
<select
v-model="queryForm.status"
class="custom-select"
>
<option value="">全部状态</option>
<option :value="0">草稿</option>
<option :value="1">已发布</option>
<option :value="2">已下架</option>
</select>
</div>
</el-form-item>
<el-form-item label="优先级">
<div class="custom-select-wrapper">
<select
v-model="queryForm.priority"
class="custom-select"
>
<option value="">全部优先级</option>
<option :value="0">普通</option>
<option :value="1">置顶</option>
</select>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>
查询
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
<el-button type="success" @click="handleAdd">
<el-icon><Plus /></el-icon>
添加公告
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 公告列表 -->
<el-card class="table-card">
<el-table
:data="announcements"
style="width: 100%"
v-loading="loading"
stripe
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="公告标题" min-width="200" />
<el-table-column prop="content" label="公告内容" min-width="250" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.status === 0" type="info">草稿</el-tag>
<el-tag v-else-if="row.status === 1" type="success">已发布</el-tag>
<el-tag v-else-if="row.status === 2" type="warning">已下架</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag v-if="row.priority === 1" type="danger" effect="dark">置顶</el-tag>
<el-tag v-else type="info">普通</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="发布时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="publisherName" label="发布人" width="120" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleView(row)">
<el-icon><View /></el-icon>
查看
</el-button>
<el-button link type="warning" size="small" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 添加/编辑公告对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="700px"
@close="handleDialogClose"
>
<el-form
:model="announcementForm"
:rules="announcementRules"
ref="announcementFormRef"
label-width="100px"
>
<el-form-item label="公告标题" prop="title">
<el-input
v-model="announcementForm.title"
placeholder="请输入公告标题"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="公告内容" prop="content">
<el-input
v-model="announcementForm.content"
type="textarea"
:rows="6"
placeholder="请输入公告内容"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="公告状态" prop="status">
<el-radio-group v-model="announcementForm.status">
<el-radio :value="0">草稿</el-radio>
<el-radio :value="1">已发布</el-radio>
<el-radio :value="2">已下架</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-radio-group v-model="announcementForm.priority">
<el-radio :value="0">普通</el-radio>
<el-radio :value="1">置顶</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ submitting ? '提交中...' : '确定' }}
</el-button>
</template>
</el-dialog>
<!-- 查看公告详情对话框 -->
<el-dialog
v-model="viewDialogVisible"
title="公告详情"
width="700px"
>
<el-descriptions :column="1" border>
<el-descriptions-item label="公告ID">{{ viewAnnouncement.id }}</el-descriptions-item>
<el-descriptions-item label="公告标题">{{ viewAnnouncement.title }}</el-descriptions-item>
<el-descriptions-item label="公告内容">
<div style="white-space: pre-wrap;">{{ viewAnnouncement.content }}</div>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="viewAnnouncement.status === 0" type="info">草稿</el-tag>
<el-tag v-else-if="viewAnnouncement.status === 1" type="success">已发布</el-tag>
<el-tag v-else-if="viewAnnouncement.status === 2" type="warning">已下架</el-tag>
</el-descriptions-item>
<el-descriptions-item label="优先级">
<el-tag v-if="viewAnnouncement.priority === 1" type="danger">置顶</el-tag>
<el-tag v-else type="info">普通</el-tag>
</el-descriptions-item>
<el-descriptions-item label="发布时间">{{ formatDate(viewAnnouncement.createTime) }}</el-descriptions-item>
<el-descriptions-item label="发布人">{{ viewAnnouncement.publisherName }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDate(viewAnnouncement.updateTime) }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="viewDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Bell,
CircleCheck,
Edit,
Close,
Search,
Refresh,
Plus,
View,
Delete
} from '@element-plus/icons-vue'
import { lotteryApi } from '../../api/index.js'
export default {
name: 'AnnouncementManagement',
components: {
Bell,
CircleCheck,
Edit,
Close,
Search,
Refresh,
Plus,
View,
Delete
},
setup() {
const loading = ref(false)
const submitting = ref(false)
const dialogVisible = ref(false)
const viewDialogVisible = ref(false)
const dialogTitle = ref('添加公告')
const announcementFormRef = ref()
// 统计数据
const stats = reactive({
total: 0,
published: 0,
draft: 0,
offline: 0
})
// 查询表单
const queryForm = reactive({
title: '',
status: '',
priority: ''
})
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0
})
// 公告列表
const announcements = ref([])
// 公告表单
const announcementForm = reactive({
id: null,
title: '',
content: '',
status: 1,
priority: 0
})
// 查看公告数据
const viewAnnouncement = ref({})
// 表单验证规则
const announcementRules = {
title: [
{ required: true, message: '请输入公告标题', trigger: 'blur' },
{ min: 1, max: 100, message: '标题长度在1到100个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入公告内容', trigger: 'blur' },
{ min: 1, max: 500, message: '内容长度在1到500个字符', trigger: 'blur' }
],
status: [
{ required: true, message: '请选择公告状态', trigger: 'change' }
]
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
if (typeof date === 'string') return date
return new Date(date).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 加载公告列表
const loadAnnouncements = async () => {
loading.value = true
try {
const params = {
current: pagination.current,
pageSize: pagination.pageSize,
title: queryForm.title,
status: queryForm.status,
priority: queryForm.priority
}
const response = await lotteryApi.getAnnouncementList(params)
if (response && response.success) {
announcements.value = response.data.records || []
pagination.total = response.data.total || 0
// 更新统计数据
updateStats()
} else {
ElMessage.error(response?.message || '加载公告列表失败')
}
} catch (error) {
console.error('加载公告列表失败:', error)
ElMessage.error('加载公告列表失败')
} finally {
loading.value = false
}
}
// 更新统计数据
const updateStats = async () => {
try {
// 获取所有公告进行统计
const response = await lotteryApi.getAnnouncementList({
current: 1,
pageSize: 1000
})
if (response && response.success) {
const allAnnouncements = response.data.records || []
stats.total = allAnnouncements.length
stats.published = allAnnouncements.filter(a => a.status === 1).length
stats.draft = allAnnouncements.filter(a => a.status === 0).length
stats.offline = allAnnouncements.filter(a => a.status === 2).length
}
} catch (error) {
console.error('更新统计数据失败:', error)
}
}
// 查询
const handleQuery = () => {
pagination.current = 1
loadAnnouncements()
}
// 重置
const handleReset = () => {
queryForm.title = ''
queryForm.status = ''
queryForm.priority = ''
pagination.current = 1
loadAnnouncements()
}
// 添加公告
const handleAdd = () => {
dialogTitle.value = '添加公告'
announcementForm.id = null
announcementForm.title = ''
announcementForm.content = ''
announcementForm.status = 1
announcementForm.priority = 0
dialogVisible.value = true
}
// 查看公告
const handleView = (row) => {
viewAnnouncement.value = { ...row }
viewDialogVisible.value = true
}
// 编辑公告
const handleEdit = (row) => {
dialogTitle.value = '编辑公告'
announcementForm.id = row.id
announcementForm.title = row.title
announcementForm.content = row.content
announcementForm.status = row.status
announcementForm.priority = row.priority
dialogVisible.value = true
}
// 删除公告
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除这条公告吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const response = await lotteryApi.deleteAnnouncement(row.id)
if (response && response.success) {
ElMessage.success('删除成功')
loadAnnouncements()
} else {
ElMessage.error(response?.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除公告失败:', error)
ElMessage.error('删除公告失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
try {
await announcementFormRef.value.validate()
submitting.value = true
const data = {
title: announcementForm.title,
content: announcementForm.content,
status: announcementForm.status,
priority: announcementForm.priority
}
let response
if (announcementForm.id) {
// 编辑
data.id = announcementForm.id
response = await lotteryApi.updateAnnouncement(data)
} else {
// 添加
response = await lotteryApi.addAnnouncement(data)
}
if (response && response.success) {
ElMessage.success(announcementForm.id ? '更新成功' : '添加成功')
dialogVisible.value = false
loadAnnouncements()
} else {
ElMessage.error(response?.message || '操作失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('提交失败:', error)
ElMessage.error('操作失败')
}
} finally {
submitting.value = false
}
}
// 关闭对话框
const handleDialogClose = () => {
announcementFormRef.value?.resetFields()
}
// 分页大小变化
const handleSizeChange = (val) => {
pagination.pageSize = val
pagination.current = 1
loadAnnouncements()
}
// 当前页变化
const handleCurrentChange = (val) => {
pagination.current = val
loadAnnouncements()
}
// 初始化
onMounted(() => {
loadAnnouncements()
})
return {
loading,
submitting,
dialogVisible,
viewDialogVisible,
dialogTitle,
announcementFormRef,
stats,
queryForm,
pagination,
announcements,
announcementForm,
viewAnnouncement,
announcementRules,
formatDate,
handleQuery,
handleReset,
handleAdd,
handleView,
handleEdit,
handleDelete,
handleSubmit,
handleDialogClose,
handleSizeChange,
handleCurrentChange
}
}
}
</script>
<style scoped>
.announcement-management {
padding: 20px;
}
.page-header {
background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
color: white;
padding: 30px;
border-radius: 12px;
margin-bottom: 20px;
}
.header-content h1 {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 600;
}
.header-content p {
margin: 0;
opacity: 0.9;
font-size: 14px;
}
.stats-cards {
margin-bottom: 20px;
}
.stat-card {
border-radius: 12px;
border: none;
}
.stat-content {
display: flex;
align-items: center;
gap: 15px;
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #2563eb, #06b6d4);
color: white;
font-size: 24px;
}
.stat-icon.published {
background: linear-gradient(135deg, #10b981, #34d399);
}
.stat-icon.draft {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
}
.stat-icon.offline {
background: linear-gradient(135deg, #ef4444, #f87171);
}
.stat-info {
flex: 1;
}
.stat-number {
font-size: 24px;
font-weight: 600;
color: #333;
}
.stat-label {
font-size: 14px;
color: #666;
margin-top: 4px;
}
.search-card {
margin-bottom: 20px;
border-radius: 12px;
overflow: visible !important;
}
.search-card :deep(.el-card__body) {
overflow: visible !important;
}
.table-card {
border-radius: 12px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
:deep(.el-table) {
border-radius: 8px;
}
:deep(.el-table th) {
background-color: #f5f7fa;
font-weight: 600;
}
:deep(.el-card__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
padding: 20px;
margin: 0;
}
:deep(.el-dialog__title) {
color: white;
font-weight: 600;
font-size: 18px;
}
:deep(.el-dialog__headerbtn .el-dialog__close) {
color: white;
font-size: 20px;
}
:deep(.el-dialog__body) {
padding: 20px;
}
/* 自定义下拉框样式 */
.custom-select-wrapper {
position: relative;
width: 100%;
}
.custom-select {
width: 100%;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: white;
font-size: 14px;
color: #606266;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23606266'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.custom-select:hover {
border-color: #c0c4cc;
}
.custom-select:focus {
outline: none;
border-color: #409eff;
}
</style>

View File

@@ -0,0 +1,926 @@
<template>
<div class="admin-dashboard">
<!-- 页面标题 -->
<div class="page-header">
<h1>控制面板</h1>
<p>欢迎使用后台管理系统</p>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<el-row :gutter="20">
<el-col :xs="12" :sm="8" :md="4">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon users">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.totalUsers }}</div>
<div class="stat-label">总用户数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon vip">
<el-icon><Key /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.vipUsers }}</div>
<div class="stat-label">VIP用户</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon codes">
<el-icon><Ticket /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.totalCodes }}</div>
<div class="stat-label">会员码总数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon predictions">
<el-icon><TrendCharts /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.totalPredictions }}</div>
<div class="stat-label">推测记录</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon pv-total">
<el-icon><View /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ pvStats.totalPV }}</div>
<div class="stat-label">总访问量</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon pv-today">
<el-icon><Sunny /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ pvStats.todayPV }}</div>
<div class="stat-label">今日访问</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- PV趋势图 -->
<div class="pv-chart-section">
<el-card>
<template #header>
<div class="card-header">
<el-icon><DataLine /></el-icon>
<span>访问量趋势</span>
<div class="header-actions">
<el-radio-group v-model="pvDays" size="small" @change="loadPVStats">
<el-radio-button :value="7">近7天</el-radio-button>
<el-radio-button :value="30">近30天</el-radio-button>
<el-radio-button :value="90">近90天</el-radio-button>
</el-radio-group>
</div>
</div>
</template>
<div ref="pvChartRef" class="pv-chart" v-loading="pvChartLoading"></div>
</el-card>
</div>
<!-- 快捷操作 -->
<div class="quick-actions">
<el-card>
<template #header>
<div class="card-header">
<el-icon><Operation /></el-icon>
<span>快捷操作</span>
</div>
</template>
<div class="actions-grid">
<div class="action-item" @click="$router.push('/cpzsadmin/vip-code')">
<div class="action-icon">
<el-icon><Key /></el-icon>
</div>
<div class="action-text">
<h3>会员码管理</h3>
<p>生成和管理会员码</p>
</div>
</div>
<div class="action-item" @click="$router.push('/cpzsadmin/excel-import')">
<div class="action-icon">
<el-icon><Document /></el-icon>
</div>
<div class="action-text">
<h3>数据导入</h3>
<p>导入Excel数据</p>
</div>
</div>
<div class="action-item" @click="$router.push('/cpzsadmin/user-list')">
<div class="action-icon">
<el-icon><User /></el-icon>
</div>
<div class="action-text">
<h3>用户管理</h3>
<p>管理用户信息</p>
</div>
</div>
</div>
</el-card>
</div>
<!-- 系统信息 -->
<div class="system-info">
<el-row>
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<el-icon><Clock /></el-icon>
<span>最近操作</span>
<div class="header-actions">
<el-button type="text" @click="$router.push('/cpzsadmin/operation-history')">
更多
<el-icon class="el-icon--right"><ArrowRight /></el-icon>
</el-button>
</div>
</div>
</template>
<div class="recent-actions">
<el-table
:data="historyList"
v-loading="historyLoading"
stripe
style="width: 100%"
>
<el-table-column prop="operationTime" label="操作时间" width="180">
<template #default="{ row }">
{{ formatDate(row.operationTime) }}
</template>
</el-table-column>
<el-table-column prop="operationModule" label="操作模块" width="120">
<template #default="{ row }">
<el-tag>{{ getModuleText(row.operationModule) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationType" label="操作类型" width="150">
<template #default="{ row }">
<el-tag :type="getOperationType(row.operationType)">
{{ row.operationType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="userName" label="操作人" width="120">
<template #default="{ row }">
{{ row.userName || '-' }}
</template>
</el-table-column>
<el-table-column prop="operationResult" label="操作结果" width="100">
<template #default="{ row }">
<el-tag :type="row.operationResult === '成功' ? 'success' : 'danger'">
{{ row.operationResult }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationDetail" label="详细信息" min-width="200">
<template #default="{ row }">
{{ row.operationDetail || row.resultMessage || '-' }}
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import {
User,
Key,
Ticket,
TrendCharts,
Operation,
Document,
Setting,
InfoFilled,
Clock,
ArrowRight,
View,
Sunny,
DataLine
} from '@element-plus/icons-vue'
import { userStore } from '../../store/user.js'
import { lotteryApi } from '../../api/index.js'
import { useRouter } from 'vue-router'
import * as echarts from 'echarts'
export default {
name: 'AdminDashboard',
components: {
User,
Key,
Ticket,
TrendCharts,
Operation,
Document,
Setting,
InfoFilled,
Clock,
ArrowRight,
View,
Sunny,
DataLine
},
setup() {
const router = useRouter()
// 统计数据
const stats = reactive({
totalUsers: 0,
vipUsers: 0,
totalCodes: 0,
totalPredictions: 0
})
// PV统计数据
const pvStats = reactive({
totalPV: 0,
todayPV: 0
})
// PV图表相关
const pvChartRef = ref(null)
const pvDays = ref(7)
const pvChartLoading = ref(false)
let pvChart = null
// 管理员信息
const adminInfo = reactive({
userName: '管理员'
})
// 当前时间
const currentTime = ref('')
let timeInterval = null
let loginCheckInterval = null // 新增登录检查定时器
// 操作历史
const historyList = ref([])
const historyLoading = ref(false)
// 检查登录状态
const checkLoginStatus = () => {
if (!userStore.isAdminLoggedIn()) {
console.log('定期检查发现管理员登录态已过期,正在跳转到登录页...')
userStore.adminLogout()
router.push('/cpzsadmin/login')
}
}
// 更新时间
const updateTime = () => {
currentTime.value = new Date().toLocaleString('zh-CN')
}
// 加载统计数据
const loadStats = async () => {
try {
console.log('开始加载统计数据...')
// 首先检查登录状态
if (!userStore.isAdminLoggedIn()) {
console.log('检测到管理员未登录或登录态已过期,正在跳转到登录页...')
userStore.adminLogout() // 清除可能存在的过期会话
router.push('/cpzsadmin/login')
return
}
// 获取用户统计数据
const userCountResponse = await lotteryApi.getUserCount()
if (userCountResponse && userCountResponse.success) {
stats.totalUsers = userCountResponse.data.totalUserCount || 0
stats.vipUsers = userCountResponse.data.vipUserCount || 0
} else {
console.error('获取用户统计数据失败:', userCountResponse?.message)
// 使用默认值
stats.totalUsers = 0
stats.vipUsers = 0
}
// 获取会员码统计数据
console.log('开始获取会员码统计数据...')
const vipCodeCountResponse = await lotteryApi.getVipCodeCount()
if (vipCodeCountResponse && vipCodeCountResponse.success) {
stats.totalCodes = vipCodeCountResponse.data.totalCount || 0
console.log('设置会员码总数:', stats.totalCodes)
} else {
console.error('获取会员码统计数据失败:', vipCodeCountResponse?.message)
stats.totalCodes = 0
}
// 获取推测记录总数
console.log('开始获取推测记录总数...')
const totalPredictResponse = await lotteryApi.getTotalPredictCount()
console.log('推测记录总数API响应:', totalPredictResponse)
if (totalPredictResponse && totalPredictResponse.success) {
stats.totalPredictions = totalPredictResponse.data.totalCount || 0
console.log('设置推测记录总数:', stats.totalPredictions)
} else {
console.error('获取推测记录总数失败:', totalPredictResponse?.message)
stats.totalPredictions = 0
}
console.log('统计数据加载完成:', stats)
} catch (error) {
console.error('加载统计数据失败:', error)
// 发生错误时使用默认值
stats.totalUsers = 0
stats.vipUsers = 0
stats.totalCodes = 0
stats.totalPredictions = 0
}
}
// 加载PV统计数据
const loadPVStats = async () => {
try {
pvChartLoading.value = true
// 获取总PV
const totalResponse = await lotteryApi.getTotalPageViews()
if (totalResponse && totalResponse.success) {
pvStats.totalPV = totalResponse.data || 0
}
// 获取今日PV
const todayResponse = await lotteryApi.getTodayPageViews()
if (todayResponse && todayResponse.success) {
pvStats.todayPV = todayResponse.data || 0
}
// 获取近N天PV统计并渲染图表
const statsResponse = await lotteryApi.getPageViewsByDays(pvDays.value)
if (statsResponse && statsResponse.success) {
const data = statsResponse.data || {}
renderPVChart(data)
}
} catch (error) {
console.error('加载PV统计数据失败:', error)
} finally {
pvChartLoading.value = false
}
}
// 渲染PV图表
const renderPVChart = (data) => {
if (!pvChartRef.value) return
// 初始化或获取图表实例
if (!pvChart) {
pvChart = echarts.init(pvChartRef.value)
}
// 将数据按日期排序
const sortedDates = Object.keys(data).sort()
const values = sortedDates.map(date => data[date])
// 格式化日期显示(只显示月-日)
const formattedDates = sortedDates.map(date => {
const parts = date.split('-')
return `${parts[1]}-${parts[2]}`
})
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params) => {
const idx = params[0].dataIndex
return `${sortedDates[idx]}<br/>访问量: ${params[0].value}`
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: formattedDates,
axisLine: {
lineStyle: {
color: '#e0e0e0'
}
},
axisLabel: {
color: '#666',
rotate: pvDays.value > 7 ? 45 : 0
}
},
yAxis: {
type: 'value',
minInterval: 1,
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
lineStyle: {
color: '#f0f0f0'
}
},
axisLabel: {
color: '#666'
}
},
series: [
{
name: '访问量',
type: 'line',
data: values,
smooth: true,
symbol: 'circle',
symbolSize: 8,
lineStyle: {
width: 3,
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#409EFF' },
{ offset: 1, color: '#67C23A' }
])
},
itemStyle: {
color: '#409EFF',
borderColor: '#fff',
borderWidth: 2
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(103, 194, 58, 0.05)' }
])
},
emphasis: {
itemStyle: {
color: '#409EFF',
borderColor: '#409EFF',
borderWidth: 3,
shadowColor: 'rgba(64, 158, 255, 0.5)',
shadowBlur: 10
}
}
}
]
}
pvChart.setOption(option)
}
// 窗口大小变化时调整图表大小
const handleResize = () => {
if (pvChart) {
pvChart.resize()
}
}
// 加载操作历史
const loadOperationHistory = async () => {
try {
historyLoading.value = true
// 构建查询参数
const params = {
// 不传任何过滤条件,获取全部历史
}
// 调用统一接口获取操作历史
const response = await lotteryApi.getOperationHistoryList(params)
console.log('操作历史接口响应:', response)
if (response && response.success) {
// 处理响应数据
const data = response.data || []
// 取前10条记录
historyList.value = data.slice(0, 10)
} else {
historyList.value = []
}
} catch (error) {
console.error('加载操作历史失败:', error)
historyList.value = []
} finally {
historyLoading.value = false
}
}
// 获取操作模块文本
const getModuleText = (module) => {
const modules = {
0: '会员码管理',
1: 'Excel导入',
2: '用户管理'
}
return modules[module] || '未知模块'
}
// 获取操作类型标签样式
const getOperationType = (type) => {
const types = {
'完整数据导入': 'primary',
'开奖数据覆盖导入': 'warning',
'开奖数据追加': 'success',
'生成会员码': 'info',
'删除会员码': 'danger'
}
return types[type] || 'info'
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 获取管理员信息
const loadAdminInfo = () => {
const user = userStore.getUserInfo()
if (user) {
adminInfo.userName = user.userName || user.username || '管理员'
}
}
onMounted(async () => {
console.log('Dashboard组件已挂载开始初始化...')
loadStats()
loadOperationHistory()
loadAdminInfo()
// 启动时间更新
updateTime()
timeInterval = setInterval(updateTime, 1000)
// 定期检查登录状态每60秒检查一次
loginCheckInterval = setInterval(checkLoginStatus, 60000)
// 加载PV统计数据
await nextTick()
loadPVStats()
// 监听窗口大小变化
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
if (loginCheckInterval) {
clearInterval(loginCheckInterval)
}
// 清理图表实例
if (pvChart) {
pvChart.dispose()
pvChart = null
}
// 移除窗口大小变化监听
window.removeEventListener('resize', handleResize)
})
return {
stats,
pvStats,
pvChartRef,
pvDays,
pvChartLoading,
loadPVStats,
adminInfo,
currentTime,
historyList,
historyLoading,
getModuleText,
getOperationType,
formatDate
}
}
}
</script>
<style scoped>
.admin-dashboard {
padding: 20px;
}
/* 页面标题 */
.page-header {
margin-bottom: 24px;
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
padding: 30px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.page-header h1 {
font-size: 28px;
font-weight: 600;
color: white;
margin-bottom: 8px;
text-align: center;
}
.page-header p {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
margin: 0;
text-align: center;
}
/* 统计卡片 */
.stats-grid {
margin-bottom: 24px;
}
.stat-card {
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
}
.stat-content {
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
}
.stat-icon.users {
background: linear-gradient(135deg, #667eea, #764ba2);
}
.stat-icon.vip {
background: linear-gradient(135deg, #28a745, #20c997);
}
.stat-icon.codes {
background: linear-gradient(135deg, #fd7e14, #ffc107);
}
.stat-icon.predictions {
background: linear-gradient(135deg, #17a2b8, #6f42c1);
}
.stat-icon.pv-total {
background: linear-gradient(135deg, #e74c3c, #c0392b);
}
.stat-icon.pv-today {
background: linear-gradient(135deg, #f39c12, #e67e22);
}
.stat-info {
flex: 1;
}
.stat-number {
font-size: 24px;
font-weight: 600;
color: #333;
line-height: 1;
}
.stat-label {
font-size: 14px;
color: #666;
margin-top: 4px;
}
/* PV图表区域 */
.pv-chart-section {
margin-bottom: 24px;
}
.pv-chart {
width: 100%;
height: 300px;
}
/* 快捷操作 */
.quick-actions {
margin-bottom: 24px;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #333;
}
.card-header .el-icon {
font-size: 18px;
color: #409EFF;
}
.header-actions {
margin-left: auto; /* Push content to the right */
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.action-item {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.action-item:hover {
border-color: #409EFF;
background-color: #f0f9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
.action-icon {
width: 48px;
height: 48px;
border-radius: 8px;
background: linear-gradient(135deg, #409EFF, #67c23a);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
.action-text h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 4px 0;
}
.action-text p {
font-size: 14px;
color: #666;
margin: 0;
}
/* 系统信息 */
.system-info {
margin-bottom: 24px;
}
.info-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-weight: 600;
color: #333;
}
.info-value {
color: #666;
}
/* 最近操作 */
.recent-actions {
max-height: 200px;
overflow-y: auto;
}
.empty-actions {
text-align: center;
color: #999;
padding: 40px 20px;
}
.action-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.action-record {
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
border-left: 3px solid #409EFF;
}
.action-time {
font-size: 12px;
color: #999;
margin-bottom: 4px;
}
.action-desc {
font-size: 14px;
color: #333;
line-height: 1.4;
}
/* 响应式设计 */
@media (max-width: 768px) {
.admin-dashboard {
padding: 15px;
}
.stats-grid .el-col {
margin-bottom: 16px;
}
.actions-grid {
grid-template-columns: 1fr;
}
.system-info .el-col {
margin-bottom: 16px;
}
}
</style>

View File

@@ -0,0 +1,779 @@
<template>
<div class="excel-import-management">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<h1>📊 大乐透Excel数据导入系统</h1>
<p>管理员专用 - 大乐透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>上传包含D3-D12工作表的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文件包含D3-D12工作表' }}
</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>上传包含D1工作表的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文件包含D1工作表' }}
</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>上传包含D1工作表的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文件包含D1工作表' }}
</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> 需要包含D3D4D5D6D7D8D9D10D11D12工作表的Excel文件</p>
<p> 导入大乐透前区历史数据后区历史数据和系数数据到相应的数据库表</p>
<p> 适用于系统初始化或全量数据更新</p>
</div>
<div class="info-item">
<h4>🎯 开奖数据导入覆盖</h4>
<p> 需要包含D1工作表的Excel文件</p>
<p> 清空dlt_draw_record表的现有数据重新导入大乐透开奖数据</p>
<p> 适用于完全替换开奖数据</p>
</div>
<div class="info-item">
<h4> 开奖数据追加</h4>
<p> 需要包含D1工作表的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 dltLotteryApi from '../../api/dlt/index.js'
import { userStore } from '../../store/user.js'
import {
ElCard,
ElRow,
ElCol,
ElButton,
ElIcon
} from 'element-plus'
import {
Document,
Warning,
Plus,
InfoFilled,
FolderOpened
} from '@element-plus/icons-vue'
export default {
name: 'AdminDltExcelImportManagement',
components: {
ElCard,
ElRow,
ElCol,
ElButton,
ElIcon,
Document,
Warning,
Plus,
InfoFilled,
FolderOpened
},
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: ''
}
},
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 dltLotteryApi.uploadDltExcelFile(this.fullDataFile)
this.fullDataResult = {
type: 'success',
message: '✅ ' + (response.data || response || '大乐透完整数据导入成功!')
}
// 清空文件选择
this.fullDataFile = null
this.$refs.fullDataFileInput.value = ''
} catch (error) {
console.error('大乐透完整数据导入失败:', error)
this.fullDataResult = {
type: 'error',
message: '❌ ' + (error?.response?.data?.message || error?.response?.data || error?.message || '导入失败,请重试')
}
} finally {
this.fullDataUploading = false
}
},
// 上传开奖数据(覆盖)
async uploadLotteryData() {
if (!this.lotteryFile) return
this.lotteryUploading = true
try {
const response = await dltLotteryApi.uploadDltDrawsFile(this.lotteryFile)
this.lotteryResult = {
type: 'success',
message: '✅ ' + (response.data || response || '大乐透开奖数据导入成功!')
}
// 清空文件选择
this.lotteryFile = null
this.$refs.lotteryFileInput.value = ''
} catch (error) {
console.error('大乐透开奖数据导入失败:', error)
this.lotteryResult = {
type: 'error',
message: '❌ ' + (error?.response?.data?.message || error?.response?.data || error?.message || '导入失败,请重试')
}
} finally {
this.lotteryUploading = false
}
},
// 追加开奖数据
async appendLotteryData() {
if (!this.appendFile) return
this.appendUploading = true
try {
const response = await dltLotteryApi.appendDltDrawsFile(this.appendFile)
this.appendResult = {
type: 'success',
message: '✅ ' + (response.data || response || '大乐透开奖数据追加成功!')
}
// 清空文件选择
this.appendFile = null
this.$refs.appendFileInput.value = ''
} catch (error) {
console.error('大乐透开奖数据追加失败:', error)
this.appendResult = {
type: 'error',
message: '❌ ' + (error?.response?.data?.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: #f5f5f5;
padding: 24px;
}
/* 页面头部 */
.page-header {
text-align: center;
background: linear-gradient(135deg, #f39c12, #e74c3c);
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: #f39c12;
}
.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: #f39c12;
background: #fef9f0;
}
.file-icon {
font-size: 20px;
margin-right: 12px;
color: #f39c12;
}
.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: #f39c12;
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>

View File

@@ -0,0 +1,277 @@
<template>
<div class="prediction-management">
<el-card class="filter-card">
<el-form :inline="true" :model="queryForm" class="demo-form-inline">
<el-form-item label="用户ID">
<el-input v-model="queryForm.userId" placeholder="请输入用户ID" clearable @input="handleInputChange" />
</el-form-item>
<el-form-item label="中奖结果">
<div class="custom-select-wrapper">
<select v-model="queryForm.predictResult" class="custom-select" @change="handleSelectChange">
<option value="">全部结果</option>
<option value="一等奖">一等奖</option>
<option value="二等奖">二等奖</option>
<option value="三等奖">三等奖</option>
<option value="四等奖">四等奖</option>
<option value="五等奖">五等奖</option>
<option value="六等奖">六等奖</option>
<option value="七等奖">七等奖</option>
<option value="八等奖">八等奖</option>
<option value="九等奖">九等奖</option>
<option value="未中奖">未中奖</option>
<option value="待开奖">待开奖</option>
</select>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<el-table :data="tableData" style="width: 100%" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="drawId" label="期号" width="100" />
<el-table-column label="前区号码" min-width="180">
<template #default="scope">
<div class="ball-container">
<span class="red-ball">{{ scope.row.frontendBall1 }}</span>
<span class="red-ball">{{ scope.row.frontendBall2 }}</span>
<span class="red-ball">{{ scope.row.frontendBall3 }}</span>
<span class="red-ball">{{ scope.row.frontendBall4 }}</span>
<span class="red-ball">{{ scope.row.frontendBall5 }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="后区号码" width="100">
<template #default="scope">
<div class="ball-container">
<span class="blue-ball">{{ scope.row.backendBall1 }}</span>
<span class="blue-ball">{{ scope.row.backendBall2 }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="predictResult" label="中奖结果" width="120">
<template #default="scope">
<el-tag :type="getPredictResultType(scope.row.predictResult)">{{ scope.row.predictResult }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="predictTime" label="推测时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.predictTime) }}
</template>
</el-table-column>
<el-table-column prop="bonus" label="奖金" width="100">
<template #default="scope">
{{ scope.row.bonus > 0 ? `¥${scope.row.bonus}` : '-' }}
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { lotteryApi } from '../../api/index.js'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
const route = useRoute()
const loading = ref(false)
const tableData = ref([])
let searchTimer = null
const queryForm = reactive({
userId: '',
predictResult: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const getPredictResultType = (result) => {
if (result === '未中奖') return 'info'
if (result === '待开奖') return 'warning'
return 'success'
}
const formatDate = (timestamp) => {
if (!timestamp) return '-'
const date = new Date(timestamp)
return date.toLocaleString()
}
const loadData = async () => {
loading.value = true
try {
const params = {
userId: queryForm.userId,
predictResult: queryForm.predictResult,
current: pagination.currentPage,
pageSize: pagination.pageSize
}
const res = await lotteryApi.getAllDltPredictRecords(params)
if (res.success) {
tableData.value = res.data.records
pagination.total = res.data.total
} else {
ElMessage.error(res.message || '获取数据失败')
}
} catch (error) {
console.error('获取推测记录失败:', error)
ElMessage.error('获取数据失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.currentPage = 1
loadData()
}
const resetQuery = () => {
queryForm.userId = ''
queryForm.predictResult = ''
handleSearch()
}
const handleSizeChange = (val) => {
pagination.pageSize = val
loadData()
}
const handleCurrentChange = (val) => {
pagination.currentPage = val
loadData()
}
// 输入框变化时防抖搜索
const handleInputChange = () => {
if (searchTimer) {
clearTimeout(searchTimer)
}
searchTimer = setTimeout(() => {
pagination.currentPage = 1
loadData()
}, 500) // 500ms 防抖
}
// 筛选框变化时立即搜索
const handleSelectChange = () => {
pagination.currentPage = 1
loadData()
}
onMounted(() => {
if (route.query.userId) {
queryForm.userId = route.query.userId
}
loadData()
})
</script>
<style scoped>
.prediction-management {
padding: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.ball-container {
display: flex;
gap: 5px;
}
.red-ball {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #f56c6c;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
.blue-ball {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #409eff;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
/* 自定义下拉框样式 */
.custom-select-wrapper {
position: relative;
display: inline-block;
}
.custom-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: #fff;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23606266' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: 14px;
height: 32px;
line-height: 32px;
outline: none;
padding: 0 30px 0 15px;
transition: border-color .2s cubic-bezier(.645,.045,.355,1);
width: 180px;
cursor: pointer;
}
.custom-select:hover {
border-color: #c0c4cc;
}
.custom-select:focus {
border-color: #409eff;
}
.custom-select option {
padding: 5px;
color: #606266;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,275 @@
<template>
<div class="prize-statistics">
<el-card class="filter-card">
<el-form :inline="true" :model="queryForm" class="demo-form-inline">
<el-form-item label="用户ID">
<el-input v-model="queryForm.userId" placeholder="请输入用户ID" clearable @input="handleInputChange" />
</el-form-item>
<el-form-item label="奖项等级">
<div class="custom-select-wrapper">
<select v-model="queryForm.prizeGrade" class="custom-select" @change="handleSelectChange">
<option value="">全部奖项</option>
<option value="一等奖">一等奖</option>
<option value="二等奖">二等奖</option>
<option value="三等奖">三等奖</option>
<option value="四等奖">四等奖</option>
<option value="五等奖">五等奖</option>
<option value="六等奖">六等奖</option>
<option value="七等奖">七等奖</option>
<option value="八等奖">八等奖</option>
<option value="九等奖">九等奖</option>
</select>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<el-table :data="tableData" style="width: 100%" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="drawId" label="期号" width="100" />
<el-table-column label="前区号码" min-width="180">
<template #default="scope">
<div class="ball-container">
<span class="red-ball">{{ scope.row.frontendBall1 }}</span>
<span class="red-ball">{{ scope.row.frontendBall2 }}</span>
<span class="red-ball">{{ scope.row.frontendBall3 }}</span>
<span class="red-ball">{{ scope.row.frontendBall4 }}</span>
<span class="red-ball">{{ scope.row.frontendBall5 }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="后区号码" width="100">
<template #default="scope">
<div class="ball-container">
<span class="blue-ball">{{ scope.row.backendBall1 }}</span>
<span class="blue-ball">{{ scope.row.backendBall2 }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="predictResult" label="中奖等级" width="120">
<template #default="scope">
<el-tag :type="getPredictResultType(scope.row.predictResult)">{{ scope.row.predictResult }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="bonus" label="奖金" width="120">
<template #default="scope">
<span style="color: #f56c6c; font-weight: bold;">{{ scope.row.bonus > 0 ? `¥${scope.row.bonus}` : '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="predictTime" label="推测时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.predictTime) }}
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { lotteryApi } from '../../api/index.js'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
const route = useRoute()
const loading = ref(false)
const tableData = ref([])
let searchTimer = null
const queryForm = reactive({
userId: '',
prizeGrade: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const getPredictResultType = (result) => {
if (result === '未中奖') return 'info'
if (result === '待开奖') return 'warning'
return 'success'
}
const formatDate = (timestamp) => {
if (!timestamp) return '-'
const date = new Date(timestamp)
return date.toLocaleString()
}
const loadData = async () => {
loading.value = true
try {
const params = {
userId: queryForm.userId,
prizeGrade: queryForm.prizeGrade,
current: pagination.currentPage,
pageSize: pagination.pageSize
}
const res = await lotteryApi.getAdminDltPrizeStatistics(params)
if (res.success) {
tableData.value = res.data.records
pagination.total = res.data.total
} else {
ElMessage.error(res.message || '获取数据失败')
}
} catch (error) {
console.error('获取奖金统计失败:', error)
ElMessage.error('获取数据失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.currentPage = 1
loadData()
}
const resetQuery = () => {
queryForm.userId = ''
queryForm.prizeGrade = ''
handleSearch()
}
const handleSizeChange = (val) => {
pagination.pageSize = val
loadData()
}
const handleCurrentChange = (val) => {
pagination.currentPage = val
loadData()
}
// 输入框变化时防抖搜索
const handleInputChange = () => {
if (searchTimer) {
clearTimeout(searchTimer)
}
searchTimer = setTimeout(() => {
pagination.currentPage = 1
loadData()
}, 500) // 500ms 防抖
}
// 筛选框变化时立即搜索
const handleSelectChange = () => {
pagination.currentPage = 1
loadData()
}
onMounted(() => {
if (route.query.userId) {
queryForm.userId = route.query.userId
}
loadData()
})
</script>
<style scoped>
.prize-statistics {
padding: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.ball-container {
display: flex;
gap: 5px;
}
.red-ball {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #f56c6c;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
.blue-ball {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #409eff;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
/* 自定义下拉框样式 */
.custom-select-wrapper {
position: relative;
display: inline-block;
}
.custom-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: #fff;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23606266' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: 14px;
height: 32px;
line-height: 32px;
outline: none;
padding: 0 30px 0 15px;
transition: border-color .2s cubic-bezier(.645,.045,.355,1);
width: 180px;
cursor: pointer;
}
.custom-select:hover {
border-color: #c0c4cc;
}
.custom-select:focus {
border-color: #409eff;
}
.custom-select option {
padding: 5px;
color: #606266;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,789 @@
<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> 需要包含T1T2T3T4T5T6T7工作表的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'
import {
ElCard,
ElRow,
ElCol,
ElButton,
ElIcon
} from 'element-plus'
import {
Document,
Warning,
Plus,
InfoFilled,
FolderOpened
} from '@element-plus/icons-vue'
export default {
name: 'AdminExcelImportManagement',
components: {
ElCard,
ElRow,
ElCol,
ElButton,
ElIcon,
Document,
Warning,
Plus,
InfoFilled,
FolderOpened
},
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: #f5f5f5;
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>

View File

@@ -0,0 +1,435 @@
<template>
<div class="operation-history">
<!-- 页面标题 -->
<div class="page-header">
<div class="header-content">
<h1>操作历史管理</h1>
<p>查看和管理系统操作历史记录</p>
</div>
</div>
<!-- 筛选器 -->
<el-card class="filter-card">
<template #header>
<div class="card-header">
<el-icon><Filter /></el-icon>
<span>筛选选项</span>
</div>
</template>
<el-form :inline="true" :model="filterForm" class="filter-form">
<el-form-item label="关键词">
<el-input
v-model="filterForm.keyword"
placeholder="搜索详细信息"
clearable
@input="handleFilter"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="操作模块">
<div class="custom-select-wrapper">
<select
v-model="filterForm.operationModule"
class="custom-select"
@change="handleFilter"
>
<option value="">全部模块</option>
<option value="0">会员码管理</option>
<option value="1">Excel导入</option>
</select>
</div>
</el-form-item>
<el-form-item label="操作结果">
<div class="custom-select-wrapper">
<select
v-model="filterForm.operationResult"
class="custom-select"
@change="handleFilter"
>
<option value="">全部结果</option>
<option value="成功">成功</option>
<option value="失败">失败</option>
</select>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFilter">搜索</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作历史列表 -->
<el-card class="history-card">
<template #header>
<div class="card-header">
<el-icon><Clock /></el-icon>
<span>操作历史列表</span>
<div class="header-actions">
<el-button @click="refreshHistory">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</template>
<el-table
:data="historyList"
v-loading="loading"
stripe
style="width: 100%"
>
<el-table-column prop="operationTime" label="操作时间" width="180">
<template #default="{ row }">
{{ formatDate(row.operationTime) }}
</template>
</el-table-column>
<el-table-column prop="operationModule" label="操作模块" width="120">
<template #default="{ row }">
<el-tag>{{ getModuleText(row.operationModule) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationType" label="操作类型" width="150">
<template #default="{ row }">
<el-tag :type="getOperationType(row.operationType)">
{{ row.operationType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="userName" label="操作人" width="120">
<template #default="{ row }">
{{ row.userName || '-' }}
</template>
</el-table-column>
<el-table-column prop="operationResult" label="操作结果" width="100">
<template #default="{ row }">
<el-tag :type="row.operationResult === '成功' ? 'success' : 'danger'">
{{ row.operationResult }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationDetail" label="详细信息" min-width="200">
<template #default="{ row }">
{{ row.operationDetail || row.resultMessage || '-' }}
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50]"
:total="pagination.total"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
Clock,
Refresh,
Search,
Filter
} from '@element-plus/icons-vue'
import { lotteryApi } from '../../api/index.js'
import { userStore } from '../../store/user.js'
export default {
name: 'OperationHistory',
components: {
Clock,
Refresh,
Search,
Filter
},
setup() {
// 筛选表单
const filterForm = reactive({
operationModule: '',
operationResult: '',
keyword: ''
})
// 操作历史
const historyList = ref([])
const loading = ref(false)
const pagination = reactive({
current: 1,
size: 10,
total: 0
})
// 初始化
onMounted(() => {
loadOperationHistory()
})
// 加载操作历史
const loadOperationHistory = async () => {
try {
loading.value = true
// 构建查询参数
const params = {
operationModule: filterForm.operationModule,
operationResult: filterForm.operationResult,
keyword: filterForm.keyword
}
// 调用统一接口获取操作历史
const response = await lotteryApi.getOperationHistoryList(params)
console.log('操作历史接口响应:', response)
if (response && response.success) {
// 处理响应数据
const data = response.data || []
// 简单的前端分页
const startIndex = (pagination.current - 1) * pagination.size
const endIndex = startIndex + pagination.size
historyList.value = data.slice(startIndex, endIndex)
pagination.total = data.length
} else {
ElMessage.error('获取操作历史失败: ' + (response?.message || '未知错误'))
historyList.value = []
pagination.total = 0
}
} catch (error) {
console.error('加载操作历史失败:', error)
ElMessage.error('加载操作历史失败: ' + (error?.message || '未知错误'))
historyList.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
// 刷新历史
const refreshHistory = () => {
pagination.current = 1
loadOperationHistory()
}
// 筛选处理
const handleFilter = () => {
pagination.current = 1
loadOperationHistory()
}
// 重置筛选
const resetFilter = () => {
filterForm.operationModule = ''
filterForm.operationResult = ''
filterForm.keyword = ''
pagination.current = 1
loadOperationHistory()
}
// 分页处理
const handleSizeChange = (size) => {
pagination.size = size
pagination.current = 1
loadOperationHistory()
}
const handleCurrentChange = (current) => {
pagination.current = current
loadOperationHistory()
}
// 获取操作模块文本
const getModuleText = (module) => {
const modules = {
0: '会员码管理',
1: 'Excel导入',
2: '用户管理'
}
return modules[module] || '未知模块'
}
// 获取操作类型标签样式
const getOperationType = (type) => {
const types = {
'完整数据导入': 'primary',
'开奖数据覆盖导入': 'warning',
'开奖数据追加': 'success',
'生成会员码': 'info',
'删除会员码': 'danger'
}
return types[type] || 'info'
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
return {
filterForm,
historyList,
loading,
pagination,
handleFilter,
resetFilter,
refreshHistory,
handleSizeChange,
handleCurrentChange,
getModuleText,
getOperationType,
formatDate
}
}
}
</script>
<style scoped>
.operation-history {
padding: 20px;
}
/* 页面标题 */
.page-header {
margin-bottom: 24px;
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
padding: 30px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.header-content h1 {
font-size: 28px;
font-weight: 600;
color: white;
margin-bottom: 8px;
text-align: center;
}
.header-content p {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
margin: 0;
text-align: center;
}
/* 卡片样式 */
.filter-card, .history-card {
margin-bottom: 24px;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #333;
}
.card-header .el-icon {
font-size: 18px;
color: #409EFF;
}
.header-actions {
margin-left: auto;
}
/* 筛选表单 */
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
/* 自定义下拉框样式 */
.custom-select-wrapper {
position: relative;
width: 100%;
min-width: 160px;
}
.custom-select {
width: 100%;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: white;
font-size: 14px;
color: #606266;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23606266'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.custom-select:hover {
border-color: #c0c4cc;
}
.custom-select:focus {
outline: none;
border-color: #409eff;
}
/* 分页 */
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: center;
}
/* 响应式设计 */
@media (max-width: 768px) {
.operation-history {
padding: 15px;
}
.filter-form {
flex-direction: column;
}
.filter-form .el-form-item {
margin-right: 0;
width: 100%;
}
.header-actions {
margin-top: 8px;
margin-left: 0;
}
}
</style>

View File

@@ -0,0 +1,272 @@
<template>
<div class="prediction-management">
<el-card class="filter-card">
<el-form :inline="true" :model="queryForm" class="demo-form-inline">
<el-form-item label="用户ID">
<el-input v-model="queryForm.userId" placeholder="请输入用户ID" clearable @input="handleInputChange" />
</el-form-item>
<el-form-item label="中奖结果">
<div class="custom-select-wrapper">
<select v-model="queryForm.predictResult" class="custom-select" @change="handleSelectChange">
<option value="">全部结果</option>
<option value="一等奖">一等奖</option>
<option value="二等奖">二等奖</option>
<option value="三等奖">三等奖</option>
<option value="四等奖">四等奖</option>
<option value="五等奖">五等奖</option>
<option value="六等奖">六等奖</option>
<option value="未中奖">未中奖</option>
<option value="待开奖">待开奖</option>
</select>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<el-table :data="tableData" style="width: 100%" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="drawId" label="期号" width="100" />
<el-table-column label="红球" min-width="200">
<template #default="scope">
<div class="ball-container">
<span class="red-ball">{{ scope.row.redBall1 }}</span>
<span class="red-ball">{{ scope.row.redBall2 }}</span>
<span class="red-ball">{{ scope.row.redBall3 }}</span>
<span class="red-ball">{{ scope.row.redBall4 }}</span>
<span class="red-ball">{{ scope.row.redBall5 }}</span>
<span class="red-ball">{{ scope.row.redBall6 }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="蓝球" width="80">
<template #default="scope">
<span class="blue-ball">{{ scope.row.blueBall }}</span>
</template>
</el-table-column>
<el-table-column prop="predictResult" label="中奖结果" width="120">
<template #default="scope">
<el-tag :type="getPredictResultType(scope.row.predictResult)">{{ scope.row.predictResult }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="predictTime" label="推测时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.predictTime) }}
</template>
</el-table-column>
<el-table-column prop="bonus" label="奖金" width="100">
<template #default="scope">
{{ scope.row.bonus > 0 ? `¥${scope.row.bonus}` : '-' }}
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { lotteryApi } from '../../api/index.js'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
const route = useRoute()
const loading = ref(false)
const tableData = ref([])
let searchTimer = null
const queryForm = reactive({
userId: '',
predictResult: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const getPredictResultType = (result) => {
if (result === '未中奖') return 'info'
if (result === '待开奖') return 'warning'
return 'success'
}
const formatDate = (timestamp) => {
if (!timestamp) return '-'
const date = new Date(timestamp)
return date.toLocaleString()
}
const loadData = async () => {
loading.value = true
try {
const params = {
userId: queryForm.userId,
predictResult: queryForm.predictResult,
current: pagination.currentPage,
pageSize: pagination.pageSize
}
const res = await lotteryApi.getAllSsqPredictRecords(params)
if (res.success) {
tableData.value = res.data.records
pagination.total = res.data.total
} else {
ElMessage.error(res.message || '获取数据失败')
}
} catch (error) {
console.error('获取推测记录失败:', error)
ElMessage.error('获取数据失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.currentPage = 1
loadData()
}
const resetQuery = () => {
queryForm.userId = ''
queryForm.predictResult = ''
handleSearch()
}
const handleSizeChange = (val) => {
pagination.pageSize = val
loadData()
}
const handleCurrentChange = (val) => {
pagination.currentPage = val
loadData()
}
// 输入框变化时防抖搜索
const handleInputChange = () => {
if (searchTimer) {
clearTimeout(searchTimer)
}
searchTimer = setTimeout(() => {
pagination.currentPage = 1
loadData()
}, 500) // 500ms 防抖
}
// 筛选框变化时立即搜索
const handleSelectChange = () => {
pagination.currentPage = 1
loadData()
}
onMounted(() => {
if (route.query.userId) {
queryForm.userId = route.query.userId
}
loadData()
})
</script>
<style scoped>
.prediction-management {
padding: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.ball-container {
display: flex;
gap: 5px;
}
.red-ball {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #f56c6c;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
.blue-ball {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #409eff;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
/* 自定义下拉框样式 */
.custom-select-wrapper {
position: relative;
display: inline-block;
}
.custom-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: #fff;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23606266' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: 14px;
height: 32px;
line-height: 32px;
outline: none;
padding: 0 30px 0 15px;
transition: border-color .2s cubic-bezier(.645,.045,.355,1);
width: 180px;
cursor: pointer;
}
.custom-select:hover {
border-color: #c0c4cc;
}
.custom-select:focus {
border-color: #409eff;
}
.custom-select option {
padding: 5px;
color: #606266;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,270 @@
<template>
<div class="prize-statistics">
<el-card class="filter-card">
<el-form :inline="true" :model="queryForm" class="demo-form-inline">
<el-form-item label="用户ID">
<el-input v-model="queryForm.userId" placeholder="请输入用户ID" clearable @input="handleInputChange" />
</el-form-item>
<el-form-item label="奖项等级">
<div class="custom-select-wrapper">
<select v-model="queryForm.prizeGrade" class="custom-select" @change="handleSelectChange">
<option value="">全部奖项</option>
<option value="一等奖">一等奖</option>
<option value="二等奖">二等奖</option>
<option value="三等奖">三等奖</option>
<option value="四等奖">四等奖</option>
<option value="五等奖">五等奖</option>
<option value="六等奖">六等奖</option>
</select>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<el-table :data="tableData" style="width: 100%" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="drawId" label="期号" width="100" />
<el-table-column label="红球" min-width="200">
<template #default="scope">
<div class="ball-container">
<span class="red-ball">{{ scope.row.redBall1 }}</span>
<span class="red-ball">{{ scope.row.redBall2 }}</span>
<span class="red-ball">{{ scope.row.redBall3 }}</span>
<span class="red-ball">{{ scope.row.redBall4 }}</span>
<span class="red-ball">{{ scope.row.redBall5 }}</span>
<span class="red-ball">{{ scope.row.redBall6 }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="蓝球" width="80">
<template #default="scope">
<span class="blue-ball">{{ scope.row.blueBall }}</span>
</template>
</el-table-column>
<el-table-column prop="predictResult" label="中奖等级" width="120">
<template #default="scope">
<el-tag :type="getPredictResultType(scope.row.predictResult)">{{ scope.row.predictResult }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="bonus" label="奖金" width="120">
<template #default="scope">
<span style="color: #f56c6c; font-weight: bold;">{{ scope.row.bonus > 0 ? `¥${scope.row.bonus}` : '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="predictTime" label="推测时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.predictTime) }}
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { lotteryApi } from '../../api/index.js'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
const route = useRoute()
const loading = ref(false)
const tableData = ref([])
let searchTimer = null
const queryForm = reactive({
userId: '',
prizeGrade: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const getPredictResultType = (result) => {
if (result === '未中奖') return 'info'
if (result === '待开奖') return 'warning'
return 'success'
}
const formatDate = (timestamp) => {
if (!timestamp) return '-'
const date = new Date(timestamp)
return date.toLocaleString()
}
const loadData = async () => {
loading.value = true
try {
const params = {
userId: queryForm.userId,
prizeGrade: queryForm.prizeGrade,
current: pagination.currentPage,
pageSize: pagination.pageSize
}
const res = await lotteryApi.getAdminPrizeStatistics(params)
if (res.success) {
tableData.value = res.data.records
pagination.total = res.data.total
} else {
ElMessage.error(res.message || '获取数据失败')
}
} catch (error) {
console.error('获取奖金统计失败:', error)
ElMessage.error('获取数据失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.currentPage = 1
loadData()
}
const resetQuery = () => {
queryForm.userId = ''
queryForm.prizeGrade = ''
handleSearch()
}
const handleSizeChange = (val) => {
pagination.pageSize = val
loadData()
}
const handleCurrentChange = (val) => {
pagination.currentPage = val
loadData()
}
// 输入框变化时防抖搜索
const handleInputChange = () => {
if (searchTimer) {
clearTimeout(searchTimer)
}
searchTimer = setTimeout(() => {
pagination.currentPage = 1
loadData()
}, 500) // 500ms 防抖
}
// 筛选框变化时立即搜索
const handleSelectChange = () => {
pagination.currentPage = 1
loadData()
}
onMounted(() => {
if (route.query.userId) {
queryForm.userId = route.query.userId
}
loadData()
})
</script>
<style scoped>
.prize-statistics {
padding: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.ball-container {
display: flex;
gap: 5px;
}
.red-ball {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #f56c6c;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
.blue-ball {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #409eff;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
/* 自定义下拉框样式 */
.custom-select-wrapper {
position: relative;
display: inline-block;
}
.custom-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: #fff;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23606266' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: 14px;
height: 32px;
line-height: 32px;
outline: none;
padding: 0 30px 0 15px;
transition: border-color .2s cubic-bezier(.645,.045,.355,1);
width: 180px;
cursor: pointer;
}
.custom-select:hover {
border-color: #c0c4cc;
}
.custom-select:focus {
border-color: #409eff;
}
.custom-select option {
padding: 5px;
color: #606266;
background-color: #fff;
}
</style>

1162
src/views/admin/UserList.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
<template>
</template>
<script>
</script>
<style scoped>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,611 @@
<template>
<div class="admin-layout">
<el-container class="layout-container">
<!-- 侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '240px'" class="sidebar">
<div class="sidebar-header">
<div class="logo" :class="{ 'collapsed': isCollapse }">
<img src="/assets/admin/logo.svg" alt="Logo" class="logo-icon" v-show="!isCollapse" />
<h2 v-show="!isCollapse">后台管理</h2>
</div>
</div>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:unique-opened="true"
router
background-color="#001529"
text-color="#fff"
active-text-color="#409EFF"
class="sidebar-menu"
>
<el-menu-item index="/cpzsadmin/dashboard">
<el-icon><DataBoard /></el-icon>
<template #title>控制面板</template>
</el-menu-item>
<el-menu-item index="/cpzsadmin/user-list" v-if="userRole === 'superAdmin'">
<el-icon><User /></el-icon>
<template #title>用户管理</template>
</el-menu-item>
<el-menu-item index="/cpzsadmin/vip-code">
<el-icon><Key /></el-icon>
<template #title>会员码管理</template>
</el-menu-item>
<el-sub-menu index="data-import">
<template #title>
<el-icon><Document /></el-icon>
<span>数据导入</span>
</template>
<el-menu-item index="/cpzsadmin/excel-import/ssq">
<el-icon><DataBoard /></el-icon>
<template #title>双色球</template>
</el-menu-item>
<el-menu-item index="/cpzsadmin/excel-import/dlt">
<el-icon><DataBoard /></el-icon>
<template #title>大乐透</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="prediction-management">
<template #title>
<el-icon><Aim /></el-icon>
<span>推测管理</span>
</template>
<el-menu-item index="/cpzsadmin/prediction/ssq">
<el-icon><DataBoard /></el-icon>
<template #title>双色球</template>
</el-menu-item>
<el-menu-item index="/cpzsadmin/prediction/dlt">
<el-icon><DataBoard /></el-icon>
<template #title>大乐透</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="prize-statistics">
<template #title>
<el-icon><Trophy /></el-icon>
<span>奖金统计</span>
</template>
<el-menu-item index="/cpzsadmin/prize-statistics/ssq">
<el-icon><DataBoard /></el-icon>
<template #title>双色球</template>
</el-menu-item>
<el-menu-item index="/cpzsadmin/prize-statistics/dlt">
<el-icon><DataBoard /></el-icon>
<template #title>大乐透</template>
</el-menu-item>
</el-sub-menu>
<el-menu-item index="/cpzsadmin/operation-history">
<el-icon><Clock /></el-icon>
<template #title>操作历史</template>
</el-menu-item>
<el-menu-item index="/cpzsadmin/announcement">
<el-icon><Bell /></el-icon>
<template #title>公告管理</template>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 主容器 -->
<el-container class="main-container">
<!-- 顶部导航 -->
<el-header class="header">
<div class="header-left">
<el-button
link
@click="toggleSidebar"
class="toggle-button"
>
<el-icon :size="20">
<Fold v-if="!isCollapse" />
<Expand v-else />
</el-icon>
</el-button>
<el-breadcrumb separator="/" class="breadcrumb">
<el-breadcrumb-item :to="{ path: '/cpzsadmin/dashboard' }">首页</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index">
{{ item }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<div class="header-actions">
<el-button link @click="refreshPage" class="refresh-button">
<el-icon><Refresh /></el-icon>
</el-button>
<!-- 用户信息 -->
<div class="user-info">
<el-avatar :size="32" :src="userAvatar" />
<span class="username">{{ userName }}</span>
</div>
<!-- 注销按钮 -->
<el-button
type="danger"
@click="directLogout"
class="direct-logout"
size="small"
>
<el-icon :size="16"><SwitchButton /></el-icon>
<span style="font-size: 16px;">注销</span>
</el-button>
</div>
</div>
</el-header>
<!-- 主内容区域 -->
<el-main class="main-content">
<div class="page-container">
<router-view />
</div>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
DataBoard,
User,
Key,
Document,
Fold,
Expand,
ArrowDown,
Refresh,
Setting,
SwitchButton,
Clock,
Bell,
Aim,
Trophy
} from '@element-plus/icons-vue'
import { userStore } from '../../../store/user.js'
import { lotteryApi } from '../../../api/index.js'
export default {
name: 'AdminLayout',
components: {
DataBoard,
User,
Key,
Document,
Fold,
Expand,
ArrowDown,
Bell,
Refresh,
Setting,
SwitchButton,
Clock
},
setup() {
const route = useRoute()
const router = useRouter()
const isCollapse = ref(false)
const userName = ref('管理员')
const userAvatar = ref('https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png')
const userRole = ref('')
// 当前激活的菜单项
const activeMenu = computed(() => route.path)
// 面包屑导航
const breadcrumbs = computed(() => {
const path = route.path.split('/')
if (path[2] === 'dashboard') return []
const map = {
'user-list': ['用户管理'],
'vip-code': ['会员码管理'],
'excel-import': {
'ssq': ['数据导入', '双色球'],
'dlt': ['数据导入', '大乐透'],
'default': ['数据导入']
},
'prediction': {
'ssq': ['推测管理', '双色球'],
'dlt': ['推测管理', '大乐透'],
'default': ['推测管理']
},
'prize-statistics': {
'ssq': ['奖金统计', '双色球'],
'dlt': ['奖金统计', '大乐透'],
'default': ['奖金统计']
},
'operation-history': ['操作历史']
}
if (path[2] === 'excel-import' && path[3]) {
return map['excel-import'][path[3]] || map['excel-import']['default']
}
if (path[2] === 'prediction' && path[3]) {
return map['prediction'][path[3]] || map['prediction']['default']
}
if (path[2] === 'prize-statistics' && path[3]) {
return map['prize-statistics'][path[3]] || map['prize-statistics']['default']
}
return map[path[2]] || [path[2]]
})
// 切换侧边栏
const toggleSidebar = () => {
isCollapse.value = !isCollapse.value
}
// 刷新页面
const refreshPage = () => {
window.location.reload()
}
// 处理下拉菜单命令
const handleCommand = (command) => {
console.log('接收到下拉菜单命令:', command)
switch (command) {
case 'profile':
console.log('跳转到个人信息页面')
router.push('/cpzsadmin/profile')
break
case 'settings':
console.log('跳转到系统设置页面')
router.push('/cpzsadmin/settings')
break
case 'logout':
console.log('执行注销操作')
handleLogout()
break
default:
console.log('未知命令:', command)
}
}
// 退出登录
const handleLogout = () => {
ElMessageBox.confirm('确认退出后台管理系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
// 调用后端注销接口
const res = await lotteryApi.userLogout()
console.log('注销API响应:', res)
// 无论API响应如何都清除session状态
userStore.adminLogout()
ElMessage({
type: 'success',
message: '已安全退出系统'
})
// 确保跳转到登录页
setTimeout(() => {
router.push('/cpzsadmin/login')
}, 100)
} catch (error) {
console.error('注销过程中出错:', error)
// 即使出错也清除session状态并跳转
userStore.adminLogout()
ElMessage({
type: 'warning',
message: '注销过程中出现错误,已强制退出'
})
setTimeout(() => {
router.push('/cpzsadmin/login')
}, 100)
}
}).catch(() => {
// 用户取消操作
})
}
// 直接注销,不使用确认对话框
const directLogout = () => {
try {
// 调用后端注销接口
lotteryApi.userLogout()
.then(() => {
console.log('注销API调用成功')
})
.catch((error) => {
console.error('注销API调用失败:', error)
})
.finally(() => {
// 无论成功失败都清除session状态
userStore.adminLogout()
// 显示成功消息
ElMessage({
type: 'success',
message: '已安全退出系统'
})
// 直接跳转到登录页
window.location.href = '/cpzsadmin/login'
})
} catch (error) {
console.error('注销过程中出错:', error)
// 强制清除状态并跳转
userStore.adminLogout()
window.location.href = '/cpzsadmin/login'
}
}
// 获取用户信息
onMounted(() => {
const user = userStore.getUserInfo()
if (user) {
userName.value = user.userName || user.username || '管理员'
userRole.value = user.userRole || 'admin'
if (user.avatar) {
userAvatar.value = user.avatar
}
}
})
return {
isCollapse,
userName,
userAvatar,
userRole,
activeMenu,
breadcrumbs,
toggleSidebar,
refreshPage,
handleCommand,
handleLogout,
directLogout
}
}
}
</script>
<style scoped>
.admin-layout {
height: 100vh;
width: 100vw;
display: flex;
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.layout-container {
height: 100%;
width: 100%;
}
/* 侧边栏 */
.sidebar {
background-color: #001529;
transition: width 0.3s;
overflow-y: auto;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
position: relative;
height: 100%;
}
.sidebar-header {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #002140;
border-bottom: 1px solid #001529;
}
.logo {
display: flex;
align-items: center;
padding: 0 16px;
transition: all 0.3s;
}
.logo.collapsed {
justify-content: center;
}
.logo img.logo-icon {
width: 32px;
height: 32px;
margin-right: 12px;
}
.logo h2 {
color: #fff;
font-size: 18px;
font-weight: 600;
margin: 0;
white-space: nowrap;
}
.sidebar-menu {
border: none;
}
.sidebar-menu .el-menu-item,
.sidebar-menu .el-sub-menu__title {
height: 50px;
line-height: 50px;
}
/* 顶部导航 */
.header {
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 60px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.toggle-button {
font-size: 18px;
color: #666;
padding: 8px;
border-radius: 4px;
transition: all 0.3s;
}
.toggle-button:hover {
background-color: #f5f5f5;
color: #409EFF;
}
.refresh-button {
font-size: 18px;
color: #666;
padding: 8px;
border-radius: 4px;
transition: all 0.3s;
}
.refresh-button:hover {
background-color: #f5f5f5;
color: #409EFF;
}
.breadcrumb {
font-size: 14px;
}
.header-right {
display: flex;
align-items: center;
}
.header-actions {
display: flex;
align-items: center;
gap: 0px;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 6px;
}
.username {
font-size: 14px;
color: #333;
font-weight: 500;
}
.dropdown-item-content {
display: flex;
align-items: center;
gap: 8px;
}
/* 添加直接注销按钮样式 */
.direct-logout {
margin-left: 10px;
margin-right: 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
/* 主内容区域 */
.main-content {
background-color: #f5f5f5;
padding: 20px;
overflow-y: auto;
height: calc(100vh - 60px);
}
.page-container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-height: calc(100vh - 120px);
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
z-index: 1000;
height: 100vh;
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.show {
transform: translateX(0);
}
.header {
padding: 0 15px;
}
.header-left {
gap: 15px;
}
.username {
display: none;
}
.main-content {
padding: 15px;
}
}
/* 滚动条样式 */
.sidebar::-webkit-scrollbar,
.main-content::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track,
.main-content::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb,
.main-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover,
.main-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>