初始提交:彩票推测系统前端代码
This commit is contained in:
447
src/views/admin/AdminLogin.vue
Normal file
447
src/views/admin/AdminLogin.vue
Normal 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>
|
||||
753
src/views/admin/AnnouncementManagement.vue
Normal file
753
src/views/admin/AnnouncementManagement.vue
Normal 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>
|
||||
926
src/views/admin/Dashboard.vue
Normal file
926
src/views/admin/Dashboard.vue
Normal 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>
|
||||
779
src/views/admin/DltExcelImportManagement.vue
Normal file
779
src/views/admin/DltExcelImportManagement.vue
Normal 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>• 需要包含D3、D4、D5、D6、D7、D8、D9、D10、D11、D12工作表的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>
|
||||
|
||||
277
src/views/admin/DltPredictionManagement.vue
Normal file
277
src/views/admin/DltPredictionManagement.vue
Normal 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>
|
||||
275
src/views/admin/DltPrizeStatistics.vue
Normal file
275
src/views/admin/DltPrizeStatistics.vue
Normal 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>
|
||||
789
src/views/admin/ExcelImportManagement.vue
Normal file
789
src/views/admin/ExcelImportManagement.vue
Normal 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>• 需要包含T1、T2、T3、T4、T5、T6、T7工作表的Excel文件</p>
|
||||
<p>• 导入红球、蓝球、接续系数和组合系数数据到相应的数据库表</p>
|
||||
<p>• 适用于系统初始化或全量数据更新</p>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<h4>🎯 开奖数据导入(覆盖):</h4>
|
||||
<p>• 需要包含T10工作表的Excel文件</p>
|
||||
<p>• 清空lottery_draws表的现有数据,重新导入</p>
|
||||
<p>• 适用于完全替换开奖数据</p>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<h4>➕ 开奖数据追加:</h4>
|
||||
<p>• 需要包含T10工作表的Excel文件</p>
|
||||
<p>• 保留现有数据,只添加新的开奖记录</p>
|
||||
<p>• 自动跳过重复的期号,适用于增量更新</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示弹窗 -->
|
||||
<div v-if="showErrorModal" class="modal-overlay" @click="hideErrorModal">
|
||||
<div class="modal-content error-modal" @click.stop>
|
||||
<h3>❌ 导入失败</h3>
|
||||
<p>{{ errorMessage }}</p>
|
||||
<button class="btn btn-primary" @click="hideErrorModal">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { lotteryApi } from '../../api/index.js'
|
||||
import { userStore } from '../../store/user.js'
|
||||
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>
|
||||
435
src/views/admin/OperationHistory.vue
Normal file
435
src/views/admin/OperationHistory.vue
Normal 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>
|
||||
272
src/views/admin/PredictionManagement.vue
Normal file
272
src/views/admin/PredictionManagement.vue
Normal 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>
|
||||
270
src/views/admin/PrizeStatistics.vue
Normal file
270
src/views/admin/PrizeStatistics.vue
Normal 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
1162
src/views/admin/UserList.vue
Normal file
File diff suppressed because it is too large
Load Diff
11
src/views/admin/UserRole.vue
Normal file
11
src/views/admin/UserRole.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
1089
src/views/admin/VipCodeManagement.vue
Normal file
1089
src/views/admin/VipCodeManagement.vue
Normal file
File diff suppressed because it is too large
Load Diff
611
src/views/admin/layout/AdminLayout.vue
Normal file
611
src/views/admin/layout/AdminLayout.vue
Normal 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>
|
||||
Reference in New Issue
Block a user